Bugfixes for emails and bugfixes for trade acceptance quantities being checked on create, closes #1

This commit is contained in:
badblocks 2025-04-08 00:59:40 -07:00
parent 32da8157a6
commit bd7a65975f
21 changed files with 95 additions and 86 deletions

View file

@ -62,7 +62,7 @@ class TradeAcceptanceCreateForm(forms.ModelForm):
TradeAcceptance.AcceptanceState.RECEIVED,
]
available_requested_ids = []
for through_obj in trade_offer.trade_offer_have_cards.all():
for through_obj in trade_offer.have_cards_available:
active_count = trade_offer.acceptances.filter(
requested_card=through_obj.card,
state__in=active_states
@ -73,7 +73,7 @@ class TradeAcceptanceCreateForm(forms.ModelForm):
# 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():
for through_obj in trade_offer.want_cards_available:
active_count = trade_offer.acceptances.filter(
offered_card=through_obj.card,
state__in=active_states

View file

@ -1,4 +1,4 @@
# Generated by Django 5.1.2 on 2025-04-07 04:19
# Generated by Django 5.1.2 on 2025-04-08 06:24
import django.db.models.deletion
from django.db import migrations, models

View file

@ -26,28 +26,22 @@ class TradeOfferManager(models.Manager):
def get_queryset(self):
qs = super().get_queryset().select_related(
"initiated_by",
"initiated_by__user",
).prefetch_related(
"trade_offer_have_cards__card",
"trade_offer_want_cards__card",
"acceptances",
"acceptances__accepted_by",
"acceptances__requested_card",
"acceptances__offered_card",
"acceptances__accepted_by__user",
)
cutoff = timezone.now() - timedelta(days=28)
qs = qs.filter(created_at__gte=cutoff)
return qs.order_by("-updated_at")
class TradeOfferAllManager(models.Manager):
def get_queryset(self):
# Return all trade offers without filtering by the cutoff.
return super().get_queryset()
class TradeOffer(models.Model):
objects = TradeOfferManager()
all_offers = TradeOfferAllManager()
id = models.AutoField(primary_key=True)
is_closed = models.BooleanField(default=False, db_index=True)
@ -106,6 +100,17 @@ class TradeOffer(models.Model):
# Use super().save() here to avoid recursion.
super(TradeOffer, self).save(update_fields=["rarity_level", "rarity_icon"])
# New derived properties for available cards
@property
def have_cards_available(self):
# Returns the list of have_cards (through objects) that still have available quantity.
return [item for item in self.trade_offer_have_cards.all() if item.quantity > item.qty_accepted]
@property
def want_cards_available(self):
# Returns the list of want_cards (through objects) that still have available quantity.
return [item for item in self.trade_offer_want_cards.all() if item.quantity > item.qty_accepted]
class TradeOfferHaveCard(models.Model):
"""
Through model for TradeOffer.have_cards.
@ -175,6 +180,16 @@ class TradeAcceptance(models.Model):
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'
# DRY improvement: define active states once as a class-level constant.
ACTIVE_STATES = [
AcceptanceState.ACCEPTED,
AcceptanceState.SENT,
AcceptanceState.RECEIVED,
AcceptanceState.THANKED_BY_INITIATOR,
AcceptanceState.THANKED_BY_ACCEPTOR,
AcceptanceState.THANKED_BY_BOTH,
]
trade_offer = models.ForeignKey(
TradeOffer,
@ -187,13 +202,11 @@ class TradeAcceptance(models.Model):
on_delete=models.PROTECT,
related_name='trade_acceptances'
)
# The acceptor selects one card the initiator is offering (from have_cards)
requested_card = models.ForeignKey(
"cards.Card",
on_delete=models.PROTECT,
related_name='accepted_requested'
)
# And one card from the initiator's wanted cards (from want_cards)
offered_card = models.ForeignKey(
"cards.Card",
on_delete=models.PROTECT,
@ -266,40 +279,25 @@ class TradeAcceptance(models.Model):
return not self.is_completed_or_rejected
def clean(self):
# Validate that the requested and offered cards exist in the through tables.
from django.core.exceptions import ValidationError
try:
have_through_obj = self.trade_offer.trade_offer_have_cards.get(card_id=self.requested_card_id)
have_card = self.trade_offer.trade_offer_have_cards.get(card_id=self.requested_card_id)
except TradeOfferHaveCard.DoesNotExist:
raise ValidationError("The requested card must be one of the trade offer's available cards (have_cards).")
try:
want_through_obj = self.trade_offer.trade_offer_want_cards.get(card_id=self.offered_card_id)
want_card = self.trade_offer.trade_offer_want_cards.get(card_id=self.offered_card_id)
except TradeOfferWantCard.DoesNotExist:
raise ValidationError("The offered card must be one of the trade offer's requested cards (want_cards).")
if not self.pk and self.trade_offer.is_closed:
raise ValidationError("This trade offer is closed. No more acceptances are allowed.")
active_states = [
self.AcceptanceState.ACCEPTED,
self.AcceptanceState.SENT,
self.AcceptanceState.RECEIVED,
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)
if self.pk:
active_acceptances = active_acceptances.exclude(pk=self.pk)
requested_count = active_acceptances.filter(requested_card_id=self.requested_card_id).count()
if requested_count >= have_through_obj.quantity:
raise ValidationError("This requested card has been fully accepted.")
offered_count = active_acceptances.filter(offered_card_id=self.offered_card_id).count()
if offered_count >= want_through_obj.quantity:
raise ValidationError("This offered card has already been fully used.")
# Only perform these validations on creation (when self.pk is None).
if self.pk is None:
if self.trade_offer.is_closed:
raise ValidationError("This trade offer is closed. No more acceptances are allowed.")
# Use direct comparison with qty_accepted and quantity.
if have_card.qty_accepted >= have_card.quantity:
raise ValidationError("The requested card has no available quantity.")
if want_card.qty_accepted >= want_card.quantity:
raise ValidationError("The offered card has no available quantity.")
def get_step_number(self):
if self.state in [

View file

@ -58,6 +58,10 @@ def update_trade_offer_closed_status(trade_offer):
@receiver(pre_save, sender=TradeAcceptance)
def trade_acceptance_pre_save(sender, instance, **kwargs):
# Skip signal processing during raw fixture load or when saving a new instance
if kwargs.get("raw", False) or instance._state.adding:
return
if instance.pk:
old_instance = TradeAcceptance.objects.get(pk=instance.pk)
instance._old_state = old_instance.state
@ -98,9 +102,9 @@ def trade_acceptance_email_notification(sender, instance, created, **kwargs):
return
# check if were in debug mode
if settings.DEBUG:
print("DEBUG: skipping email notification in debug mode")
return
# if settings.DEBUG:
# print("DEBUG: skipping email notification in debug mode")
# return
acting_user = instance._actioning_user
state = instance.state
@ -126,14 +130,16 @@ def trade_acceptance_email_notification(sender, instance, created, **kwargs):
# Determine the non-acting party:
if instance.trade_offer.initiated_by == acting_user:
if instance.trade_offer.initiated_by.user.pk == acting_user.pk:
# The initiator made the change; notify the acceptor.
recipient_user = instance.accepted_by.user
else:
elif instance.accepted_by.user.pk == acting_user.pk:
# The acceptor made the change; notify the initiator.
recipient_user = instance.trade_offer.initiated_by.user
else:
return
is_initiator = instance.trade_offer.initiated_by == acting_user
is_initiator = instance.trade_offer.initiated_by.user.pk == acting_user.pk
email_context = {
"has_card": instance.requested_card,
@ -145,7 +151,7 @@ def trade_acceptance_email_notification(sender, instance, created, **kwargs):
"recipient_user_ign": instance.accepted_by.in_game_name if is_initiator else instance.trade_offer.initiated_by.in_game_name,
"acting_user_friend_code": instance.trade_offer.initiated_by.friend_code if is_initiator else instance.accepted_by.friend_code,
"is_initiator": is_initiator,
"domain": Site.objects.get_current().domain,
"domain": "https://" + Site.objects.get_current().domain,
"pk": instance.pk,
}
email_template = "email/trades/trade_update_" + state + ".txt"