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. 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, **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 # Filter accepted_by to those friend codes that belong to the current user. if friend_codes is None: raise ValueError("friend_codes must be provided") self.fields["accepted_by"].queryset = friend_codes active_states = [ TradeAcceptance.AcceptanceState.ACCEPTED, TradeAcceptance.AcceptanceState.SENT, TradeAcceptance.AcceptanceState.RECEIVED, TradeAcceptance.AcceptanceState.COMPLETED, ] # Build available requested_card choices from the TradeOffer's "have" side. 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) # Similarly, build 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 TradeAcceptanceUpdateForm(forms.ModelForm): """ Form for updating the state of an existing TradeAcceptance. Based on the current state and which party is acting (initiator vs. acceptor), this form limits available state transitions. """ class Meta: model = TradeAcceptance fields = ["state"] def __init__(self, *args, friend_codes=None, **kwargs): super().__init__(*args, **kwargs) instance = self.instance allowed_choices = [] # Allowed transitions for a TradeAcceptance: # - From ACCEPTED: # • If the initiator is acting, allow SENT and REJECTED_BY_INITIATOR. # • If the acceptor is acting, allow REJECTED_BY_ACCEPTOR. # - From SENT: # • If the acceptor is acting, allow RECEIVED and REJECTED_BY_ACCEPTOR. # • If the initiator is acting, allow REJECTED_BY_INITIATOR. # - From RECEIVED: # • If the initiator is acting, allow COMPLETED and REJECTED_BY_INITIATOR. # • If the acceptor is acting, allow REJECTED_BY_ACCEPTOR. if friend_codes is None: raise ValueError("friend_codes must be provided") if instance.state == TradeAcceptance.AcceptanceState.ACCEPTED: if instance.trade_offer.initiated_by in friend_codes: allowed_choices = [ (TradeAcceptance.AcceptanceState.SENT, "Sent"), (TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR, "Rejected by Initiator"), ] elif instance.accepted_by in friend_codes: allowed_choices = [ (TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR, "Rejected by Acceptor"), ] elif instance.state == TradeAcceptance.AcceptanceState.SENT: if instance.accepted_by in friend_codes: allowed_choices = [ (TradeAcceptance.AcceptanceState.RECEIVED, "Received"), (TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR, "Rejected by Acceptor"), ] elif instance.trade_offer.initiated_by in friend_codes: allowed_choices = [ (TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR, "Rejected by Initiator"), ] elif instance.state == TradeAcceptance.AcceptanceState.RECEIVED: if instance.trade_offer.initiated_by in friend_codes: allowed_choices = [ (TradeAcceptance.AcceptanceState.COMPLETED, "Completed"), (TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR, "Rejected by Initiator"), ] elif instance.accepted_by in friend_codes: allowed_choices = [ (TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR, "Rejected by Acceptor"), ] if allowed_choices: self.fields["state"].choices = allowed_choices else: self.fields.pop("state") 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: parts = item.split(':') card_id = parts[0] try: quantity = int(parts[1]) if len(parts) > 1 else 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: parts = item.split(':') card_id = parts[0] try: quantity = int(parts[1]) if len(parts) > 1 else 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