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, ] available_requested_ids = [] for through_obj in trade_offer.trade_offer_have_cards.all(): active_count = trade_offer.acceptances.filter( requested_card=through_obj.card, state__in=active_states ).count() if active_count < through_obj.quantity: available_requested_ids.append(through_obj.card.id) self.fields["requested_card"].queryset = Card.objects.filter(id__in=available_requested_ids) # Update available offered_card choices from the TradeOffer's "want" side. available_offered_ids = [] for through_obj in trade_offer.trade_offer_want_cards.all(): active_count = trade_offer.acceptances.filter( offered_card=through_obj.card, state__in=active_states ).count() if active_count < through_obj.quantity: available_offered_ids.append(through_obj.card.id) self.fields["offered_card"].queryset = Card.objects.filter(id__in=available_offered_ids) 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 cards = Card.objects.order_by("name", "rarity__pk") 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 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 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