finished conversion to tailwind

This commit is contained in:
badblocks 2025-03-11 23:45:27 -07:00
parent 6e2843c60e
commit d62956d465
50 changed files with 2490 additions and 1273 deletions

View file

@ -5,5 +5,6 @@ class TradesConfig(AppConfig):
name = "trades"
def ready(self):
# This import registers the signal handlers defined in trades/signals.py.
# This import registers the signal handlers defined in trades/signals.py,
# ensuring that denormalized field updates occur whenever related objects change.
import trades.signals

View file

@ -46,11 +46,11 @@ class TradeAcceptanceCreateForm(forms.ModelForm):
raise ValueError("friend_codes must be provided")
self.fields["accepted_by"].queryset = friend_codes
# Update active_states to include only states that mean the acceptance is still "open".
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.
@ -83,67 +83,24 @@ class TradeAcceptanceCreateForm(forms.ModelForm):
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"]
class ButtonRadioSelect(forms.RadioSelect):
template_name = "widgets/button_radio_select.html"
def __init__(self, *args, friend_codes=None, **kwargs):
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)
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")
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'
@ -166,10 +123,14 @@ class TradeOfferCreateForm(ModelForm):
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:
quantity = int(parts[1]) if len(parts) > 1 else 1
# 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
@ -179,10 +140,12 @@ class TradeOfferCreateForm(ModelForm):
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]) if len(parts) > 1 else 1
quantity = int(parts[1])
except ValueError:
raise forms.ValidationError(f"Invalid quantity provided in {item}")
parsed[card_id] = parsed.get(card_id, 0) + quantity

View file

@ -1,4 +1,4 @@
# Generated by Django 5.1.2 on 2025-03-07 01:04
# Generated by Django 5.1.2 on 2025-03-09 05:08
import django.db.models.deletion
from django.db import migrations, models
@ -18,10 +18,14 @@ class Migration(migrations.Migration):
name='TradeOffer',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('manually_closed', models.BooleanField(default=False)),
('hash', models.CharField(editable=False, max_length=8)),
('manually_closed', models.BooleanField(db_index=True, default=False)),
('hash', models.CharField(editable=False, max_length=9)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('total_have_quantity', models.PositiveIntegerField(default=0, editable=False)),
('total_want_quantity', models.PositiveIntegerField(default=0, editable=False)),
('total_have_accepted', models.PositiveIntegerField(default=0, editable=False)),
('total_want_accepted', models.PositiveIntegerField(default=0, editable=False)),
('initiated_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='initiated_trade_offers', to='accounts.friendcode')),
],
),
@ -29,7 +33,8 @@ class Migration(migrations.Migration):
name='TradeAcceptance',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('state', models.CharField(choices=[('ACCEPTED', 'Accepted'), ('SENT', 'Sent'), ('RECEIVED', 'Received'), ('COMPLETED', 'Completed'), ('REJECTED_BY_INITIATOR', 'Rejected by Initiator'), ('REJECTED_BY_ACCEPTOR', 'Rejected by Acceptor')], default='ACCEPTED', max_length=25)),
('state', models.CharField(choices=[('ACCEPTED', 'Accepted'), ('SENT', 'Sent'), ('RECEIVED', 'Received'), ('THANKED_BY_INITIATOR', 'Thanked by Initiator'), ('THANKED_BY_ACCEPTOR', 'Thanked by Acceptor'), ('THANKED_BY_BOTH', 'Thanked by Both'), ('REJECTED_BY_INITIATOR', 'Rejected by Initiator'), ('REJECTED_BY_ACCEPTOR', 'Rejected by Acceptor')], db_index=True, default='ACCEPTED', max_length=25)),
('hash', models.CharField(blank=True, editable=False, max_length=9)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('accepted_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='trade_acceptances', to='accounts.friendcode')),
@ -72,4 +77,8 @@ class Migration(migrations.Migration):
name='want_cards',
field=models.ManyToManyField(related_name='trade_offers_want', through='trades.TradeOfferWantCard', to='cards.card'),
),
migrations.AddIndex(
model_name='tradeoffer',
index=models.Index(fields=['manually_closed'], name='trades_trad_manuall_b3b74c_idx'),
),
]

View file

@ -7,8 +7,8 @@ from accounts.models import FriendCode
class TradeOffer(models.Model):
id = models.AutoField(primary_key=True)
manually_closed = models.BooleanField(default=False)
hash = models.CharField(max_length=8, editable=False)
manually_closed = models.BooleanField(default=False, db_index=True)
hash = models.CharField(max_length=9, editable=False)
initiated_by = models.ForeignKey(
"accounts.FriendCode",
on_delete=models.PROTECT,
@ -28,6 +28,12 @@ class TradeOffer(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# New denormalized fields for aggregated counts
total_have_quantity = models.PositiveIntegerField(default=0, editable=False)
total_want_quantity = models.PositiveIntegerField(default=0, editable=False)
total_have_accepted = models.PositiveIntegerField(default=0, editable=False)
total_want_accepted = models.PositiveIntegerField(default=0, editable=False)
def __str__(self):
want_names = ", ".join([x.name for x in self.want_cards.all()])
have_names = ", ".join([x.name for x in self.have_cards.all()])
@ -37,44 +43,69 @@ class TradeOffer(models.Model):
is_new = self.pk is None
super().save(*args, **kwargs)
if is_new and not self.hash:
self.hash = hashlib.md5(str(self.id).encode('utf-8')).hexdigest()[:8]
self.hash = hashlib.md5((str(self.id) + "z").encode("utf-8")).hexdigest()[:8] + "z"
super().save(update_fields=["hash"])
def update_aggregates(self):
"""
Recalculate and update aggregated fields from related have/want cards and acceptances.
"""
from django.db.models import Sum
from trades.models import TradeAcceptance
# Calculate total quantities from through models
have_agg = self.trade_offer_have_cards.aggregate(total=Sum("quantity"))
want_agg = self.trade_offer_want_cards.aggregate(total=Sum("quantity"))
self.total_have_quantity = have_agg["total"] or 0
self.total_want_quantity = want_agg["total"] or 0
# Define acceptance states that count as active.
active_states = [
TradeAcceptance.AcceptanceState.ACCEPTED,
TradeAcceptance.AcceptanceState.SENT,
TradeAcceptance.AcceptanceState.RECEIVED,
TradeAcceptance.AcceptanceState.THANKED_BY_INITIATOR,
TradeAcceptance.AcceptanceState.THANKED_BY_ACCEPTOR,
TradeAcceptance.AcceptanceState.THANKED_BY_BOTH,
]
# Compute accepted counts based on matching card IDs.
have_card_ids = list(self.trade_offer_have_cards.values_list("card_id", flat=True))
want_card_ids = list(self.trade_offer_want_cards.values_list("card_id", flat=True))
self.total_have_accepted = TradeAcceptance.objects.filter(
trade_offer=self,
state__in=active_states,
requested_card_id__in=have_card_ids,
).count()
self.total_want_accepted = TradeAcceptance.objects.filter(
trade_offer=self,
state__in=active_states,
offered_card_id__in=want_card_ids,
).count()
# Save updated aggregate values so they are denormalized in the database.
self.save(update_fields=[
"total_have_quantity",
"total_want_quantity",
"total_have_accepted",
"total_want_accepted",
])
@property
def is_closed(self):
if self.manually_closed:
return True
# Utilize denormalized fields for faster check.
return not (self.total_have_accepted < self.total_have_quantity and
self.total_want_accepted < self.total_want_quantity)
from .models import TradeAcceptance # local import to avoid circular dependencies
active_states = [
TradeAcceptance.AcceptanceState.ACCEPTED,
TradeAcceptance.AcceptanceState.SENT,
TradeAcceptance.AcceptanceState.RECEIVED,
TradeAcceptance.AcceptanceState.COMPLETED
class Meta:
indexes = [
models.Index(fields=['manually_closed']),
]
closed_have = True
for through_obj in self.trade_offer_have_cards.all():
accepted_count = self.acceptances.filter(
requested_card=through_obj.card,
state__in=active_states
).count()
if accepted_count < through_obj.quantity:
closed_have = False
break
closed_want = True
for through_obj in self.trade_offer_want_cards.all():
accepted_count = self.acceptances.filter(
offered_card=through_obj.card,
state__in=active_states
).count()
if accepted_count < through_obj.quantity:
closed_want = False
break
return closed_have or closed_want
class TradeOfferHaveCard(models.Model):
"""
Through model for TradeOffer.have_cards.
@ -83,13 +114,14 @@ class TradeOfferHaveCard(models.Model):
trade_offer = models.ForeignKey(
TradeOffer,
on_delete=models.CASCADE,
related_name='trade_offer_have_cards'
related_name='trade_offer_have_cards',
db_index=True
)
card = models.ForeignKey("cards.Card", on_delete=models.PROTECT)
card = models.ForeignKey("cards.Card", on_delete=models.PROTECT, db_index=True)
quantity = models.PositiveIntegerField(default=1)
def __str__(self):
return f"{self.card.name} x{self.quantity} (Have side for offer {self.trade_offer.hash})"
return f"{self.card.name} x{self.quantity}"
class Meta:
unique_together = ("trade_offer", "card")
@ -108,7 +140,7 @@ class TradeOfferWantCard(models.Model):
quantity = models.PositiveIntegerField(default=1)
def __str__(self):
return f"{self.card.name} x{self.quantity} (Want side for offer {self.trade_offer.hash})"
return f"{self.card.name} x{self.quantity}"
class Meta:
unique_together = ("trade_offer", "card")
@ -118,14 +150,17 @@ class TradeAcceptance(models.Model):
ACCEPTED = 'ACCEPTED', 'Accepted'
SENT = 'SENT', 'Sent'
RECEIVED = 'RECEIVED', 'Received'
COMPLETED = 'COMPLETED', 'Completed'
THANKED_BY_INITIATOR = 'THANKED_BY_INITIATOR', 'Thanked by Initiator'
THANKED_BY_ACCEPTOR = 'THANKED_BY_ACCEPTOR', 'Thanked by Acceptor'
THANKED_BY_BOTH = 'THANKED_BY_BOTH', 'Thanked by Both'
REJECTED_BY_INITIATOR = 'REJECTED_BY_INITIATOR', 'Rejected by Initiator'
REJECTED_BY_ACCEPTOR = 'REJECTED_BY_ACCEPTOR', 'Rejected by Acceptor'
trade_offer = models.ForeignKey(
TradeOffer,
on_delete=models.CASCADE,
related_name='acceptances'
related_name='acceptances',
db_index=True
)
accepted_by = models.ForeignKey(
"accounts.FriendCode",
@ -136,22 +171,115 @@ class TradeAcceptance(models.Model):
requested_card = models.ForeignKey(
"cards.Card",
on_delete=models.PROTECT,
related_name='accepted_requested'
related_name='accepted_requested',
db_index=True
)
# And one card from the initiator's wanted cards (from want_cards)
offered_card = models.ForeignKey(
"cards.Card",
on_delete=models.PROTECT,
related_name='accepted_offered'
related_name='accepted_offered',
db_index=True
)
state = models.CharField(
max_length=25,
choices=AcceptanceState.choices,
default=AcceptanceState.ACCEPTED
default=AcceptanceState.ACCEPTED,
db_index=True
)
hash = models.CharField(max_length=9, editable=False, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def mark_thanked(self, friend_code):
"""
Mark this acceptance as "thanked" by the given friend_code.
Allowed transitions:
- If the current state is RECEIVED:
* If the initiator thanks, transition to THANKED_BY_INITIATOR.
* If the acceptor thanks, transition to THANKED_BY_ACCEPTOR.
- If already partially thanked:
* If state is THANKED_BY_INITIATOR and the acceptor thanks, transition to THANKED_BY_BOTH.
* If state is THANKED_BY_ACCEPTOR and the initiator thanks, transition to THANKED_BY_BOTH.
Only parties involved in the trade (either the initiator or the acceptor) can mark it as thanked.
"""
if self.state not in [self.AcceptanceState.RECEIVED,
self.AcceptanceState.THANKED_BY_INITIATOR,
self.AcceptanceState.THANKED_BY_ACCEPTOR]:
raise ValidationError("Cannot mark thanked in the current state.")
if friend_code == self.trade_offer.initiated_by:
# Initiator is marking thanks.
if self.state == self.AcceptanceState.RECEIVED:
self.state = self.AcceptanceState.THANKED_BY_INITIATOR
elif self.state == self.AcceptanceState.THANKED_BY_ACCEPTOR:
self.state = self.AcceptanceState.THANKED_BY_BOTH
elif self.state == self.AcceptanceState.THANKED_BY_INITIATOR:
# Already thanked by the initiator.
return
elif friend_code == self.accepted_by:
# Acceptor is marking thanks.
if self.state == self.AcceptanceState.RECEIVED:
self.state = self.AcceptanceState.THANKED_BY_ACCEPTOR
elif self.state == self.AcceptanceState.THANKED_BY_INITIATOR:
self.state = self.AcceptanceState.THANKED_BY_BOTH
elif self.state == self.AcceptanceState.THANKED_BY_ACCEPTOR:
# Already thanked by the acceptor.
return
else:
from django.core.exceptions import PermissionDenied
raise PermissionDenied("You are not a party to this trade acceptance.")
self.save(update_fields=["state"])
@property
def is_completed(self):
"""
Computed boolean property indicating whether the trade acceptance has been
marked as thanked by one or both parties.
"""
return self.state in {
self.AcceptanceState.RECEIVED,
self.AcceptanceState.THANKED_BY_INITIATOR,
self.AcceptanceState.THANKED_BY_ACCEPTOR,
self.AcceptanceState.THANKED_BY_BOTH,
}
@property
def is_thanked(self):
"""
Computed boolean property indicating whether the trade acceptance has been
marked as thanked by one or both parties.
"""
return self.state == self.AcceptanceState.THANKED_BY_BOTH
@property
def is_rejected(self):
"""
Computed boolean property that is True if the trade acceptance has been rejected
by either the initiator or the acceptor.
"""
return self.state in {
self.AcceptanceState.REJECTED_BY_INITIATOR,
self.AcceptanceState.REJECTED_BY_ACCEPTOR,
}
@property
def is_completed_or_rejected(self):
"""
Computed boolean property that is True if the trade acceptance is either completed
(i.e., thanked) or rejected.
"""
return self.is_completed or self.is_rejected
@property
def is_active(self):
"""
Computed boolean property that is True if the trade acceptance is still active,
meaning it is neither completed (thanked) nor rejected.
"""
return not self.is_completed_or_rejected
def clean(self):
"""
Validate that:
@ -180,7 +308,9 @@ class TradeAcceptance(models.Model):
self.AcceptanceState.ACCEPTED,
self.AcceptanceState.SENT,
self.AcceptanceState.RECEIVED,
self.AcceptanceState.COMPLETED,
self.AcceptanceState.THANKED_BY_INITIATOR,
self.AcceptanceState.THANKED_BY_ACCEPTOR,
self.AcceptanceState.THANKED_BY_BOTH,
]
active_acceptances = self.trade_offer.acceptances.filter(state__in=active_states)
@ -197,59 +327,119 @@ class TradeAcceptance(models.Model):
if offered_count >= want_through_obj.quantity:
raise ValidationError("This offered card has already been fully used.")
def update_state(self, new_state):
def get_step_number(self):
"""
Return the step number for the current state.
"""
if self.state in [
self.AcceptanceState.THANKED_BY_INITIATOR,
self.AcceptanceState.THANKED_BY_ACCEPTOR,
]:
return 4
elif self.state in [
self.AcceptanceState.THANKED_BY_BOTH,
]:
return 5
elif self.state in [
self.AcceptanceState.REJECTED_BY_INITIATOR,
self.AcceptanceState.REJECTED_BY_ACCEPTOR,
]:
return 0
else:
# .choices is a list of tuples, so we need to find the index of the tuple that contains the state.
return (next(index for index, choice in enumerate(self.AcceptanceState.choices) if choice[0] == self.state) + 1)
def update_state(self, new_state, user):
"""
Update the trade acceptance state.
Allowed transitions:
- ACCEPTED -> SENT
- SENT -> RECEIVED
- RECEIVED -> COMPLETED
Additionally, from any active state a transition to:
REJECTED_BY_INITIATOR or REJECTED_BY_ACCEPTOR is allowed.
Once in COMPLETED or any rejection state, no further transitions are allowed.
"""
if new_state not in [choice[0] for choice in self.AcceptanceState.choices]:
raise ValueError(f"'{new_state}' is not a valid state.")
# Terminal states: no further transitions allowed.
if self.state in [
self.AcceptanceState.COMPLETED,
self.AcceptanceState.THANKED_BY_BOTH,
self.AcceptanceState.REJECTED_BY_INITIATOR,
self.AcceptanceState.REJECTED_BY_ACCEPTOR
]:
raise ValueError(f"No transitions allowed from the terminal state '{self.state}'.")
allowed_transitions = {
self.AcceptanceState.ACCEPTED: {
self.AcceptanceState.SENT,
self.AcceptanceState.REJECTED_BY_INITIATOR,
self.AcceptanceState.REJECTED_BY_ACCEPTOR,
},
self.AcceptanceState.SENT: {
self.AcceptanceState.RECEIVED,
self.AcceptanceState.REJECTED_BY_INITIATOR,
self.AcceptanceState.REJECTED_BY_ACCEPTOR,
},
self.AcceptanceState.RECEIVED: {
self.AcceptanceState.COMPLETED,
self.AcceptanceState.REJECTED_BY_INITIATOR,
self.AcceptanceState.REJECTED_BY_ACCEPTOR,
},
}
allowed = [x for x,y in self.get_allowed_state_transitions(user)]
print(allowed)
print(new_state)
if new_state not in allowed_transitions.get(self.state, {}):
if new_state not in allowed:
raise ValueError(f"Transition from {self.state} to {new_state} is not allowed.")
self.state = new_state
self.save(update_fields=["state"])
def save(self, *args, **kwargs):
is_new = self.pk is None
super().save(*args, **kwargs)
if is_new and not self.hash:
# Append "y" so all trade acceptance hashes differ from trade offers.
self.hash = hashlib.md5((str(self.id) + "y").encode("utf-8")).hexdigest()[:8] + "y"
super().save(update_fields=["hash"])
def __str__(self):
return (f"TradeAcceptance(offer_hash={self.trade_offer.hash}, "
f"accepted_by={self.accepted_by}, "
f"requested_card={self.requested_card}, "
f"offered_card={self.offered_card}, state={self.state})")
class Meta:
# Unique constraints have been removed because validations now allow
# multiple active acceptances per card based on the available quantity.
pass
def get_allowed_state_transitions(self, user):
"""
Returns a list of allowed state transitions as tuples (value, display_label)
based on the current state of this trade acceptance.
"""
if self.trade_offer.initiated_by in user.friend_codes.all():
allowed_transitions = {
self.AcceptanceState.ACCEPTED: {
self.AcceptanceState.SENT,
self.AcceptanceState.REJECTED_BY_INITIATOR,
},
self.AcceptanceState.SENT: {
self.AcceptanceState.REJECTED_BY_INITIATOR,
},
self.AcceptanceState.RECEIVED: {
self.AcceptanceState.THANKED_BY_INITIATOR,
self.AcceptanceState.REJECTED_BY_INITIATOR,
},
self.AcceptanceState.THANKED_BY_INITIATOR: {
self.AcceptanceState.REJECTED_BY_INITIATOR,
},
self.AcceptanceState.THANKED_BY_ACCEPTOR: {
self.AcceptanceState.REJECTED_BY_INITIATOR,
self.AcceptanceState.THANKED_BY_BOTH,
},
}
elif self.accepted_by in user.friend_codes.all():
allowed_transitions = {
self.AcceptanceState.ACCEPTED: {
self.AcceptanceState.REJECTED_BY_ACCEPTOR,
},
self.AcceptanceState.SENT: {
self.AcceptanceState.RECEIVED,
self.AcceptanceState.REJECTED_BY_ACCEPTOR,
},
self.AcceptanceState.RECEIVED: {
self.AcceptanceState.THANKED_BY_ACCEPTOR,
self.AcceptanceState.REJECTED_BY_ACCEPTOR,
},
self.AcceptanceState.THANKED_BY_ACCEPTOR: {
self.AcceptanceState.REJECTED_BY_ACCEPTOR,
},
self.AcceptanceState.THANKED_BY_INITIATOR: {
self.AcceptanceState.REJECTED_BY_ACCEPTOR,
self.AcceptanceState.THANKED_BY_BOTH,
},
}
else:
allowed_transitions = {}
allowed = allowed_transitions.get(self.state, {})
# Return as a list of tuples (state_value, human-readable label)
return [(state, self.AcceptanceState(state).label) for state in allowed]

View file

@ -1,8 +1,9 @@
from django.core.exceptions import ValidationError
from django.db.models.signals import m2m_changed
from django.db.models.signals import m2m_changed, post_save, post_delete
from django.dispatch import receiver
from .models import TradeOffer
from cards.models import Card
from trades.models import TradeOfferHaveCard, TradeOfferWantCard, TradeAcceptance
def check_trade_offer_rarity(instance):
combined_cards = list(instance.have_cards.all()) + list(instance.want_cards.all())
@ -19,4 +20,25 @@ def validate_have_cards_rarity(sender, instance, action, **kwargs):
@receiver(m2m_changed, sender=TradeOffer.want_cards.through)
def validate_want_cards_rarity(sender, instance, action, **kwargs):
if action == "post_add":
check_trade_offer_rarity(instance)
check_trade_offer_rarity(instance)
@receiver(post_save, sender=TradeOfferHaveCard)
@receiver(post_delete, sender=TradeOfferHaveCard)
def update_aggregates_from_have_card(sender, instance, **kwargs):
trade_offer = instance.trade_offer
if trade_offer and hasattr(trade_offer, 'update_aggregates'):
trade_offer.update_aggregates()
@receiver(post_save, sender=TradeOfferWantCard)
@receiver(post_delete, sender=TradeOfferWantCard)
def update_aggregates_from_want_card(sender, instance, **kwargs):
trade_offer = instance.trade_offer
if trade_offer and hasattr(trade_offer, 'update_aggregates'):
trade_offer.update_aggregates()
@receiver(post_save, sender=TradeAcceptance)
@receiver(post_delete, sender=TradeAcceptance)
def update_aggregates_from_acceptance(sender, instance, **kwargs):
trade_offer = instance.trade_offer
if trade_offer and hasattr(trade_offer, 'update_aggregates'):
trade_offer.update_aggregates()

View file

@ -8,47 +8,32 @@ def render_trade_offer(context, offer):
Renders a trade offer including detailed trade acceptance information.
Groups acceptances for each card on both the have and want sides.
"""
request = context.get('request')
current_friend_code = (
getattr(request.user, 'friendcode', None)
if request and request.user.is_authenticated
else None
)
# Get all acceptances with optimized queries.
acceptances = offer.acceptances.all().select_related(
'accepted_by', 'requested_card', 'offered_card'
)
# Use the already prefetched acceptances.
acceptances = offer.acceptances.all()
have_cards_available = []
want_cards_available = []
# Build grouping for the have side.
have_acceptances_data = []
for have in offer.trade_offer_have_cards.all():
group = {
'card': have.card,
'quantity': have.quantity,
# Filter acceptances where the requested_card matches the have card.
'acceptances': [
acc for acc in acceptances if acc.requested_card_id == have.card.id
],
}
have_acceptances_data.append(group)
for card in offer.trade_offer_have_cards.all():
if all(acc.requested_card_id != card.card_id for acc in acceptances):
have_cards_available.append(card)
# Build grouping for the want side.
want_acceptances_data = []
for want in offer.trade_offer_want_cards.all():
group = {
'card': want.card,
'quantity': want.quantity,
# Filter acceptances where the offered_card matches the want card.
'acceptances': [
acc for acc in acceptances if acc.offered_card_id == want.card.id
],
}
want_acceptances_data.append(group)
for card in offer.trade_offer_want_cards.all():
if all(acc.offered_card_id != card.card_id for acc in acceptances):
want_cards_available.append(card)
return {
'offer': offer,
'have_acceptances_data': have_acceptances_data,
'want_acceptances_data': want_acceptances_data,
'current_friend_code': current_friend_code,
'have_cards_available': have_cards_available,
'want_cards_available': want_cards_available,
}
@register.inclusion_tag('templatetags/trade_acceptance.html', takes_context=True)
def render_trade_acceptance(context, acceptance):
"""
Renders a simple trade acceptance view with a single row and simplified header/footer.
"""
return {
"acceptance": acceptance,
"request": context.get("request"),
}

View file

@ -6,7 +6,7 @@ from django.shortcuts import get_object_or_404, render
from django.core.exceptions import PermissionDenied, ValidationError
from django.views.generic.edit import FormMixin
from django.utils import timezone
from django.db.models import Q
from django.db.models import Q, Case, When, Value, BooleanField, Prefetch, F
from django.utils.decorators import method_decorator
from django.views.decorators.http import require_http_methods
from django.core.paginator import Paginator
@ -14,7 +14,7 @@ from django.contrib import messages
from .models import TradeOffer, TradeAcceptance
from .forms import (TradeOfferAcceptForm,
TradeAcceptanceCreateForm, TradeAcceptanceUpdateForm, TradeOfferCreateForm)
TradeAcceptanceCreateForm, TradeOfferCreateForm, TradeAcceptanceTransitionForm)
from cards.models import Card
class TradeOfferCreateView(LoginRequiredMixin, CreateView):
@ -23,6 +23,11 @@ class TradeOfferCreateView(LoginRequiredMixin, CreateView):
template_name = "trades/trade_offer_create.html"
success_url = reverse_lazy("trade_offer_list")
def dispatch(self, request, *args, **kwargs):
if not request.user.friend_codes.exists():
raise PermissionDenied("No friend codes available for your account.")
return super().dispatch(request, *args, **kwargs)
def get_form(self, form_class=None):
form = super().get_form(form_class)
# Restrict the 'initiated_by' choices to friend codes owned by the logged-in user.
@ -31,16 +36,19 @@ class TradeOfferCreateView(LoginRequiredMixin, CreateView):
def get_initial(self):
initial = super().get_initial()
# Standardize parameter names: use "have_cards" and "want_cards"
initial["have_cards"] = self.request.GET.getlist("have_cards")
initial["want_cards"] = self.request.GET.getlist("want_cards")
# If the user has only one friend code, set it as the default.
if self.request.user.friend_codes.count() == 1:
initial["initiated_by"] = self.request.user.friend_codes.first().pk
return initial
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
from cards.models import Card
# Ensure available_cards is a proper QuerySet
context["available_cards"] = Card.objects.all().order_by("name", "rarity__pk") \
.select_related("rarity", "cardset") \
.prefetch_related("decks")
friend_codes = self.request.user.friend_codes.all()
if "initiated_by" in self.request.GET:
try:
@ -54,7 +62,6 @@ class TradeOfferCreateView(LoginRequiredMixin, CreateView):
return context
def form_valid(self, form):
# Double-check that the chosen friend code is owned by the current user.
friend_codes = self.request.user.friend_codes.all()
if form.cleaned_data.get("initiated_by") not in friend_codes:
raise PermissionDenied("You cannot initiate trade offers for friend codes that do not belong to you.")
@ -62,18 +69,46 @@ class TradeOfferCreateView(LoginRequiredMixin, CreateView):
return HttpResponseRedirect(self.get_success_url())
class TradeOfferListView(LoginRequiredMixin, ListView):
model = TradeOffer # Fallback model; our context data will hold separate querysets.
model = TradeOffer # Fallback model; our context data holds separate filtered querysets.
template_name = "trades/trade_offer_list.html"
def dispatch(self, request, *args, **kwargs):
if request.user.is_authenticated and not request.user.friend_codes.exists():
raise PermissionDenied("No friend codes available for your account.")
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return (
TradeOffer.objects.select_related('initiated_by')
.prefetch_related(
'trade_offer_have_cards__card',
'trade_offer_want_cards__card',
Prefetch(
'acceptances',
queryset=TradeAcceptance.objects.select_related('accepted_by', 'requested_card', 'offered_card')
)
)
.order_by("-updated_at")
.annotate(
is_active=Case(
When(
manually_closed=False,
total_have_quantity__gt=F('total_have_accepted'),
total_want_quantity__gt=F('total_want_accepted'),
then=Value(True)
),
default=Value(False),
output_field=BooleanField()
)
)
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
request = self.request
show_closed = request.GET.get("show_closed", "false").lower() == "true"
context["show_closed"] = show_closed
# Determine if the user wants to see completed (closed) items.
show_completed = request.GET.get("show_completed", "false").lower() == "true"
context["show_completed"] = show_completed
# Get all friend codes for the current user.
friend_codes = request.user.friend_codes.all()
friend_code_param = request.GET.get("friend_code")
if friend_code_param:
@ -90,28 +125,33 @@ class TradeOfferListView(LoginRequiredMixin, ListView):
context["selected_friend_code"] = selected_friend_code
context["friend_codes"] = friend_codes
# ----- My Trade Offers -----
if show_completed:
my_trade_offers = TradeOffer.objects.filter(initiated_by=selected_friend_code).order_by("-updated_at")
my_trade_offers = [offer for offer in my_trade_offers if offer.is_closed]
queryset = self.get_queryset().filter(initiated_by=selected_friend_code)
if show_closed:
queryset = queryset.filter(is_active=False)
else:
my_trade_offers = TradeOffer.objects.filter(initiated_by=selected_friend_code).order_by("-updated_at")
my_trade_offers = [offer for offer in my_trade_offers if not offer.is_closed]
queryset = queryset.filter(is_active=True)
offers_page = request.GET.get("offers_page")
offers_paginator = Paginator(queryset, 10)
context["my_trade_offers_paginated"] = offers_paginator.get_page(offers_page)
# ----- Trade Acceptances involving the user -----
# Update terminal states to include the thanked and rejected states.
terminal_states = [
TradeAcceptance.AcceptanceState.COMPLETED,
TradeAcceptance.AcceptanceState.THANKED_BY_INITIATOR,
TradeAcceptance.AcceptanceState.THANKED_BY_ACCEPTOR,
TradeAcceptance.AcceptanceState.THANKED_BY_BOTH,
TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR,
TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR,
]
involved_acceptances = TradeAcceptance.objects.filter(
involved_acceptances_qs = TradeAcceptance.objects.filter(
Q(trade_offer__initiated_by=selected_friend_code) | Q(accepted_by=selected_friend_code)
).order_by("-updated_at")
if show_completed:
involved_acceptances = involved_acceptances.filter(state__in=terminal_states)
if show_closed:
involved_acceptances = involved_acceptances_qs.filter(state__in=terminal_states)
else:
involved_acceptances = involved_acceptances.exclude(state__in=terminal_states)
involved_acceptances = involved_acceptances_qs.exclude(state__in=terminal_states)
# ----- Split Acceptances into "Waiting for Your Response" and "Other" -----
waiting_acceptances = involved_acceptances.filter(
@ -119,22 +159,20 @@ class TradeOfferListView(LoginRequiredMixin, ListView):
TradeAcceptance.AcceptanceState.ACCEPTED,
TradeAcceptance.AcceptanceState.RECEIVED,
]) |
Q(accepted_by=selected_friend_code, state=TradeAcceptance.AcceptanceState.SENT)
Q(accepted_by=selected_friend_code, state__in=[
TradeAcceptance.AcceptanceState.SENT
])
)
other_trade_acceptances = involved_acceptances.exclude(pk__in=waiting_acceptances.values("pk"))
other_party_trade_acceptances = involved_acceptances.exclude(pk__in=waiting_acceptances.values("pk"))
# ----- Paginate Each Section Separately -----
offers_page = request.GET.get("offers_page")
waiting_page = request.GET.get("waiting_page")
other_page = request.GET.get("other_page")
offers_paginator = Paginator(my_trade_offers, 10)
waiting_paginator = Paginator(waiting_acceptances, 10)
other_paginator = Paginator(other_trade_acceptances, 10)
other_party_paginator = Paginator(other_party_trade_acceptances, 10)
context["my_trade_offers_paginated"] = offers_paginator.get_page(offers_page)
context["trade_acceptances_waiting_paginated"] = waiting_paginator.get_page(waiting_page)
context["other_trade_acceptances_paginated"] = other_paginator.get_page(other_page)
context["other_party_trade_acceptances_paginated"] = other_party_paginator.get_page(other_page)
return context
class TradeOfferDeleteView(LoginRequiredMixin, DeleteView):
@ -144,7 +182,7 @@ class TradeOfferDeleteView(LoginRequiredMixin, DeleteView):
def dispatch(self, request, *args, **kwargs):
trade_offer = self.get_object()
if trade_offer.initiated_by not in request.user.friend_codes.all():
if trade_offer.initiated_by_id not in request.user.friend_codes.values_list("id", flat=True):
raise PermissionDenied("You are not authorized to delete or close this trade offer.")
return super().dispatch(request, *args, **kwargs)
@ -152,7 +190,9 @@ class TradeOfferDeleteView(LoginRequiredMixin, DeleteView):
context = super().get_context_data(**kwargs)
trade_offer = self.get_object()
terminal_states = [
TradeAcceptance.AcceptanceState.COMPLETED,
TradeAcceptance.AcceptanceState.THANKED_BY_INITIATOR,
TradeAcceptance.AcceptanceState.THANKED_BY_ACCEPTOR,
TradeAcceptance.AcceptanceState.THANKED_BY_BOTH,
TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR,
TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR,
]
@ -168,68 +208,133 @@ class TradeOfferDeleteView(LoginRequiredMixin, DeleteView):
def post(self, request, *args, **kwargs):
trade_offer = self.get_object()
terminal_states = [
TradeAcceptance.AcceptanceState.COMPLETED,
TradeAcceptance.AcceptanceState.THANKED_BY_INITIATOR,
TradeAcceptance.AcceptanceState.THANKED_BY_ACCEPTOR,
TradeAcceptance.AcceptanceState.THANKED_BY_BOTH,
TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR,
TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR,
]
active_acceptances = trade_offer.acceptances.exclude(state__in=terminal_states)
if active_acceptances.exists():
messages.error(request, "Cannot delete or close this trade offer because there are active acceptances.")
context = self.get_context_data(object=trade_offer)
return self.render_to_response(context)
else:
if trade_offer.acceptances.count() > 0:
# There are terminal acceptances: mark the offer as closed.
trade_offer.manually_closed = True
trade_offer.save(update_fields=["manually_closed"])
messages.success(request, "Trade offer has been marked as closed.")
return HttpResponseRedirect(self.get_success_url())
else:
# No acceptances: proceed with deletion.
messages.success(request, "Trade offer has been deleted.")
return super().delete(request, *args, **kwargs)
class TradeOfferSearchView(LoginRequiredMixin, ListView):
"""
Reworked trade offer search view using POST.
This view allows users to search active trade offers based on the cards they have and/or want.
The POST parameters (offered_cards and wanted_cards) are expected to be in the format 'card_id:quantity'.
If both types of selections are provided, the resultant queryset must satisfy both conditions.
Offers initiated by any of the user's friend codes are excluded.
When the request is AJAX (via X-Requested-With header), only the search results fragment
(_search_results.html) is rendered. On GET (initial page load), the search results queryset
is empty.
"""
model = TradeOffer
context_object_name = "search_results"
template_name = "trades/trade_offer_search.html"
context_object_name = "trade_offers"
paginate_by = 10
http_method_names = ["get", "post"]
def parse_selections(self, selection_list):
"""
Parse a list of selections (each formatted as 'card_id:quantity') into a list of tuples.
Defaults the quantity to 1 if missing.
"""
results = []
for item in selection_list:
parts = item.split(":")
try:
card_id = int(parts[0])
except ValueError:
continue # Skip invalid values.
qty = 1
if len(parts) > 1:
try:
qty = int(parts[1])
except ValueError:
qty = 1
results.append((card_id, qty))
return results
def get_queryset(self):
qs = super().get_queryset().filter(state=TradeOffer.State.INITIATED).prefetch_related("have_cards", "want_cards").select_related("initiated_by", "accepted_by")
offered_card = self.request.GET.get("offered_card", "").strip()
wanted_cards = self.request.GET.getlist("wanted_cards")
if not offered_card and not wanted_cards:
return qs.none()
from django.db.models import F
# For a GET request (initial load), return an empty queryset.
if self.request.method == "GET":
return TradeOffer.objects.none()
if offered_card:
try:
offered_card_id = int(offered_card)
except ValueError:
qs = qs.none()
else:
qs = qs.filter(have_cards__id=offered_card_id)
if wanted_cards:
valid_wanted_cards = []
for card_str in wanted_cards:
try:
valid_wanted_cards.append(int(card_str))
except ValueError:
qs = qs.none()
break
if valid_wanted_cards:
qs = qs.filter(want_cards__id__in=valid_wanted_cards)
return qs
# Parse the POST data for offered and wanted selections.
offered_selections = self.parse_selections(self.request.POST.getlist("offered_cards"))
wanted_selections = self.parse_selections(self.request.POST.getlist("wanted_cards"))
# If no selections are provided, return an empty queryset.
if not offered_selections and not wanted_selections:
return TradeOffer.objects.none()
qs = TradeOffer.objects.filter(
manually_closed=False,
total_have_accepted__lt=F("total_have_quantity"),
total_want_accepted__lt=F("total_want_quantity")
).exclude(initiated_by__in=self.request.user.friend_codes.all())
# Chain filters for offered selections (i.e. the user "has" cards).
if offered_selections:
for card_id, qty in offered_selections:
qs = qs.filter(
trade_offer_want_cards__card_id=card_id,
trade_offer_want_cards__quantity__gte=qty,
)
# Chain filters for wanted selections (i.e. the user "wants" cards).
if wanted_selections:
for card_id, qty in wanted_selections:
qs = qs.filter(
trade_offer_have_cards__card_id=card_id,
trade_offer_have_cards__quantity__gte=qty,
)
return qs.distinct()
def post(self, request, *args, **kwargs):
# For POST, simply process the search through get().
return self.get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["offered_card"] = self.request.GET.get("offered_card", "")
context["wanted_cards"] = self.request.GET.getlist("wanted_cards")
context["available_cards"] = Card.objects.order_by("name", "rarity__pk").select_related("rarity", "cardset")
from cards.models import Card
# Populate available_cards to re-populate the multiselects.
context["available_cards"] = Card.objects.all().order_by("name", "rarity__pk") \
.select_related("rarity", "cardset")
if self.request.method == "POST":
context["offered_cards"] = self.request.POST.getlist("offered_cards")
context["wanted_cards"] = self.request.POST.getlist("wanted_cards")
else:
context["offered_cards"] = []
context["wanted_cards"] = []
return context
def render_to_response(self, context, **response_kwargs):
"""
Render the AJAX fragment if the request is AJAX; otherwise, render the complete page.
"""
if self.request.headers.get("X-Requested-With") == "XMLHttpRequest":
from django.shortcuts import render
return render(self.request, "trades/_search_results.html", context)
else:
return super().render_to_response(context, **response_kwargs)
class TradeOfferDetailView(LoginRequiredMixin, DetailView):
"""
Displays the details of a TradeOffer along with its active acceptances.
@ -239,16 +344,55 @@ class TradeOfferDetailView(LoginRequiredMixin, DetailView):
model = TradeOffer
template_name = "trades/trade_offer_detail.html"
def dispatch(self, request, *args, **kwargs):
if not request.user.friend_codes.exists():
raise PermissionDenied("No friend codes available for your account.")
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return (
TradeOffer.objects.select_related('initiated_by')
.prefetch_related(
'trade_offer_have_cards__card',
'trade_offer_want_cards__card',
Prefetch(
'acceptances',
queryset=TradeAcceptance.objects.select_related(
'accepted_by', 'requested_card', 'offered_card'
)
)
)
.annotate(
is_active=Case(
When(manually_closed=False, then=Value(True)),
default=Value(False),
output_field=BooleanField()
)
)
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
trade_offer = self.get_object()
active_states = [
TradeAcceptance.AcceptanceState.ACCEPTED,
TradeAcceptance.AcceptanceState.SENT,
TradeAcceptance.AcceptanceState.RECEIVED,
TradeAcceptance.AcceptanceState.COMPLETED,
# Define terminal (closed) acceptance states based on our new system:
terminal_states = [
TradeAcceptance.AcceptanceState.THANKED_BY_INITIATOR,
TradeAcceptance.AcceptanceState.THANKED_BY_ACCEPTOR,
TradeAcceptance.AcceptanceState.THANKED_BY_BOTH,
TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR,
TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR,
]
context["acceptances"] = trade_offer.acceptances.filter(state__in=active_states)
# For example, if you want to separate active from terminal acceptances:
context["acceptances"] = trade_offer.acceptances.all()
# Option 1: Filter active acceptances using the queryset lookup.
context["active_acceptances"] = trade_offer.acceptances.exclude(state__in=terminal_states)
# Option 2: Or filter using the computed property (if you prefer to work with Python iterables):
# context["active_acceptances"] = [acc for acc in trade_offer.acceptances.all() if acc.is_active]
user_friend_codes = self.request.user.friend_codes.all()
# Add context flag and deletion URL if the current user is the initiator
@ -275,16 +419,40 @@ class TradeAcceptanceCreateView(LoginRequiredMixin, CreateView):
template_name = "trades/trade_acceptance_create.html"
def dispatch(self, request, *args, **kwargs):
self.trade_offer = get_object_or_404(TradeOffer, pk=kwargs.get("offer_pk"))
# Disallow acceptance if the current user is the offer initiator or if the offer is closed.
if self.trade_offer.initiated_by in request.user.friend_codes.all() or self.trade_offer.is_closed:
self.trade_offer = self.get_trade_offer()
if self.trade_offer.initiated_by_id in request.user.friend_codes.values_list("id", flat=True) or not self.trade_offer.is_active:
raise PermissionDenied("You cannot accept this trade offer.")
if not request.user.friend_codes.exists():
raise PermissionDenied("No friend codes available for your account.")
return super().dispatch(request, *args, **kwargs)
def get_trade_offer(self):
return (
TradeOffer.objects.select_related('initiated_by')
.prefetch_related(
'trade_offer_want_cards__card',
'trade_offer_have_cards__card',
Prefetch(
'acceptances',
queryset=TradeAcceptance.objects.select_related(
'accepted_by', 'requested_card', 'offered_card'
)
)
)
.annotate(
is_active=Case(
When(manually_closed=False, then=Value(True)),
default=Value(False),
output_field=BooleanField()
)
)
.get(pk=self.kwargs['offer_pk'])
)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["trade_offer"] = self.trade_offer
kwargs["friend_codes"] = self.request.user.friend_codes.all()
kwargs['trade_offer'] = self.trade_offer
kwargs['friend_codes'] = self.request.user.friend_codes.all()
return kwargs
def form_valid(self, form):
@ -301,18 +469,33 @@ class TradeAcceptanceUpdateView(LoginRequiredMixin, UpdateView):
The allowed state transitions are provided via the form.
"""
model = TradeAcceptance
form_class = TradeAcceptanceUpdateForm
form_class = TradeAcceptanceTransitionForm
template_name = "trades/trade_acceptance_update.html"
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.accepted_by_id not in request.user.friend_codes.values_list("id", flat=True):
raise PermissionDenied("You are not authorized to update this acceptance.")
if not request.user.friend_codes.exists():
raise PermissionDenied("No friend codes available for your account.")
return super().dispatch(request, *args, **kwargs)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["friend_codes"] = self.request.user.friend_codes.all()
# Pass the current instance to the form so it can set proper allowed transitions.
kwargs["instance"] = self.object
kwargs["user"] = self.request.user
return kwargs
def form_valid(self, form):
new_state = form.cleaned_data["state"]
#match the new state to the TradeAcceptance.AcceptanceState enum
if new_state not in TradeAcceptance.AcceptanceState:
form.add_error("state", "Invalid state transition.")
return self.form_invalid(form)
try:
# Use the model's update_state logic.
form.instance.update_state(form.cleaned_data["state"])
# pass the new state and the current user to the update_state method
form.instance.update_state(new_state, self.request.user)
except ValueError as e:
form.add_error("state", str(e))
return self.form_invalid(form)