pkmntrade.club/trades/forms.py

209 lines
9.2 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.
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