pkmntrade.club/trades/forms.py

183 lines
7.9 KiB
Python

from django import forms
from django.core.exceptions import ValidationError
from .models import TradeOffer, TradeAcceptance
from accounts.models import FriendCode
from cards.models import Card
from django.forms import ModelForm
from trades.models import TradeOfferHaveCard, TradeOfferWantCard
class NoValidationMultipleChoiceField(forms.MultipleChoiceField):
def validate(self, value):
# Override the validation to skip checking against defined choices
pass
class TradeOfferAcceptForm(forms.Form):
friend_code = forms.ModelChoiceField(
queryset=FriendCode.objects.none(),
label="Select a Friend Code to Accept This Trade Offer"
)
def __init__(self, *args, **kwargs):
# Expecting a keyword argument `friend_codes` with the user's friend codes.
friend_codes = kwargs.pop("friend_codes")
super().__init__(*args, **kwargs)
self.fields["friend_code"].queryset = friend_codes
class TradeAcceptanceCreateForm(forms.ModelForm):
"""
Form for creating a TradeAcceptance.
Expects the caller to pass:
- trade_offer: the instance of TradeOffer this acceptance is for.
- friend_codes: a queryset of FriendCode objects for the current user.
- default_friend_code (optional): the user's default FriendCode.
It filters available requested and offered cards based on what's still available.
"""
class Meta:
model = TradeAcceptance
fields = ["accepted_by", "requested_card", "offered_card"]
def __init__(self, *args, trade_offer=None, friend_codes=None, default_friend_code=None, **kwargs):
if trade_offer is None:
raise ValueError("trade_offer must be provided to filter choices.")
super().__init__(*args, **kwargs)
self.trade_offer = trade_offer
if friend_codes is None:
raise ValueError("friend_codes must be provided")
# Set the accepted_by queryset to the user's friend codes.
self.fields["accepted_by"].queryset = friend_codes
# If the user only has one friend code, preset the field and use a HiddenInput.
if friend_codes.count() == 1:
self.initial["accepted_by"] = friend_codes.first().pk
self.fields["accepted_by"].widget = forms.HiddenInput()
# Otherwise, if a default friend code is provided and it is in the queryset, preselect it.
elif default_friend_code and friend_codes.filter(pk=default_friend_code.pk).exists():
self.initial["accepted_by"] = default_friend_code.pk
# Update available requested_card choices from the TradeOffer's "have" side.
active_states = [
TradeAcceptance.AcceptanceState.ACCEPTED,
TradeAcceptance.AcceptanceState.SENT,
TradeAcceptance.AcceptanceState.RECEIVED,
]
self.fields["requested_card"].queryset = trade_offer.have_cards_available_qs
self.fields["offered_card"].queryset = trade_offer.want_cards_available_qs
def clean(self):
"""
Ensure the instance has its trade_offer set before model-level validation occurs.
This prevents errors when the model clean() method attempts to access trade_offer.
"""
self.instance.trade_offer = self.trade_offer
return super().clean()
class ButtonRadioSelect(forms.RadioSelect):
template_name = "widgets/button_radio_select.html"
class TradeAcceptanceTransitionForm(forms.Form):
state = forms.ChoiceField(widget=forms.HiddenInput())
def __init__(self, *args, instance=None, user=None, **kwargs):
"""
Initializes the form with allowed transitions from the provided instance.
:param instance: A TradeAcceptance instance.
"""
super().__init__(*args, **kwargs)
if instance is None:
raise ValueError("A TradeAcceptance instance must be provided")
self.instance = instance
self.user = user
self.fields["state"].choices = instance.get_allowed_state_transitions(user)
class TradeOfferCreateForm(ModelForm):
# Override the default fields to capture quantity info in the format 'card_id:quantity'
have_cards = NoValidationMultipleChoiceField(widget=forms.SelectMultiple, required=True)
want_cards = NoValidationMultipleChoiceField(widget=forms.SelectMultiple, required=True)
class Meta:
model = TradeOffer
fields = ["want_cards", "have_cards", "initiated_by"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Populate choices from Card model using the new field 'rarity_level' instead of the removed relation.
cards = Card.objects.order_by("name", "rarity_level")
choices = [(str(card.pk), card.name) for card in cards]
self.fields["have_cards"].choices = choices
self.fields["want_cards"].choices = choices
def clean_have_cards(self):
data = self.data.getlist("have_cards")
parsed = {}
for item in data:
if ':' not in item:
# Ignore any input without a colon.
continue
parts = item.split(':')
card_id = parts[0]
try:
# Only parse quantity when a colon is present.
quantity = int(parts[1])
except ValueError:
raise forms.ValidationError(f"Invalid quantity provided in {item}")
parsed[card_id] = parsed.get(card_id, 0) + quantity
# Validate that each have card does not exceed the max quantity of 20.
for card_id, quantity in parsed.items():
if quantity > 20:
card = Card.objects.get(pk=card_id)
raise forms.ValidationError(
f"Maximum allowed quantity for each have card is 20. Card {card} has {quantity}."
)
# Ensure no more than 20 unique have cards are selected.
if len(parsed) > 20:
raise forms.ValidationError("You can only select a maximum of 20 unique have cards.")
return parsed
def clean_want_cards(self):
data = self.data.getlist("want_cards")
parsed = {}
for item in data:
if ':' not in item:
continue
parts = item.split(':')
card_id = parts[0]
try:
quantity = int(parts[1])
except ValueError:
raise forms.ValidationError(f"Invalid quantity provided in {item}")
parsed[card_id] = parsed.get(card_id, 0) + quantity
# Validate that each want card does not exceed the max quantity of 20.
for card_id, quantity in parsed.items():
if quantity > 20:
# look up card name
card = Card.objects.get(pk=card_id)
raise forms.ValidationError(
f"Maximum allowed quantity for each want card is 20. Card {card} has {quantity}."
)
# Ensure no more than 20 unique want cards are selected.
if len(parsed) > 20:
raise forms.ValidationError("You can only select a maximum of 20 unique want cards.")
return parsed
def save(self, commit=True):
instance = super().save(commit=False)
if commit:
instance.save()
# Clear any existing through model entries in case of update
TradeOfferHaveCard.objects.filter(trade_offer=instance).delete()
TradeOfferWantCard.objects.filter(trade_offer=instance).delete()
# Create through entries for have_cards
for card_id, quantity in self.cleaned_data["have_cards"].items():
card = Card.objects.get(pk=card_id)
TradeOfferHaveCard.objects.create(trade_offer=instance, card=card, quantity=quantity)
# Create through entries for want_cards
for card_id, quantity in self.cleaned_data["want_cards"].items():
card = Card.objects.get(pk=card_id)
TradeOfferWantCard.objects.create(trade_offer=instance, card=card, quantity=quantity)
return instance