fix card_multiselect filtering and quantity controls

This commit is contained in:
badblocks 2025-03-13 15:48:26 -07:00
parent 6e4c6040bd
commit b97ddde71c
52 changed files with 1689 additions and 2268 deletions

View file

@ -1,4 +1,7 @@
from django.contrib import admin
from .models import TradeOffer
from .models import TradeOffer, TradeOfferHaveCard, TradeOfferWantCard, TradeAcceptance
admin.site.register(TradeOffer)
admin.site.register(TradeOffer)
admin.site.register(TradeOfferHaveCard)
admin.site.register(TradeOfferWantCard)
admin.site.register(TradeAcceptance)

View file

@ -1,4 +1,4 @@
# Generated by Django 5.1.2 on 2025-03-09 05:08
# Generated by Django 5.1.2 on 2025-03-13 01:46
import django.db.models.deletion
from django.db import migrations, models
@ -18,14 +18,10 @@ class Migration(migrations.Migration):
name='TradeOffer',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('manually_closed', models.BooleanField(db_index=True, default=False)),
('is_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')),
],
),
@ -33,7 +29,7 @@ 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'), ('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)),
('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')], 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)),
@ -48,6 +44,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.PositiveIntegerField(default=1)),
('qty_accepted', models.PositiveIntegerField(default=0, editable=False)),
('card', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='cards.card')),
('trade_offer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trade_offer_have_cards', to='trades.tradeoffer')),
],
@ -65,6 +62,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.PositiveIntegerField(default=1)),
('qty_accepted', models.PositiveIntegerField(default=0, editable=False)),
('card', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='cards.card')),
('trade_offer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trade_offer_want_cards', to='trades.tradeoffer')),
],
@ -77,8 +75,4 @@ 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,7 +7,7 @@ from accounts.models import FriendCode
class TradeOffer(models.Model):
id = models.AutoField(primary_key=True)
manually_closed = models.BooleanField(default=False, db_index=True)
is_closed = models.BooleanField(default=False, db_index=True)
hash = models.CharField(max_length=9, editable=False)
initiated_by = models.ForeignKey(
"accounts.FriendCode",
@ -28,12 +28,6 @@ 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()])
@ -46,66 +40,6 @@ class TradeOffer(models.Model):
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)
class Meta:
indexes = [
models.Index(fields=['manually_closed']),
]
class TradeOfferHaveCard(models.Model):
"""
Through model for TradeOffer.have_cards.
@ -119,9 +53,11 @@ class TradeOfferHaveCard(models.Model):
)
card = models.ForeignKey("cards.Card", on_delete=models.PROTECT, db_index=True)
quantity = models.PositiveIntegerField(default=1)
# New field to track number of accepted cards for this entry.
qty_accepted = models.PositiveIntegerField(default=0, editable=False)
def __str__(self):
return f"{self.card.name} x{self.quantity}"
return f"{self.card.name} x{self.quantity} (Accepted: {self.qty_accepted})"
class Meta:
unique_together = ("trade_offer", "card")
@ -138,9 +74,11 @@ class TradeOfferWantCard(models.Model):
)
card = models.ForeignKey("cards.Card", on_delete=models.PROTECT)
quantity = models.PositiveIntegerField(default=1)
# New field for tracking accepted count.
qty_accepted = models.PositiveIntegerField(default=0, editable=False)
def __str__(self):
return f"{self.card.name} x{self.quantity}"
return f"{self.card.name} x{self.quantity} (Accepted: {self.qty_accepted})"
class Meta:
unique_together = ("trade_offer", "card")
@ -171,73 +109,25 @@ class TradeAcceptance(models.Model):
requested_card = models.ForeignKey(
"cards.Card",
on_delete=models.PROTECT,
related_name='accepted_requested',
db_index=True
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,
related_name='accepted_offered',
db_index=True
related_name='accepted_offered'
)
state = models.CharField(
max_length=25,
choices=AcceptanceState.choices,
default=AcceptanceState.ACCEPTED,
db_index=True
default=AcceptanceState.ACCEPTED
)
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,
@ -247,18 +137,10 @@ class TradeAcceptance(models.Model):
@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,
@ -266,41 +148,24 @@ class TradeAcceptance(models.Model):
@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:
- The requested_card is associated with the trade offer's have_cards (via the through model).
- The offered_card is associated with the trade offer's want_cards (via the through model).
- The trade offer is not already closed.
- The total number of active acceptances for each chosen card does not exceed the available quantity.
"""
# Validate that requested_card is in trade_offer.have_cards
# Validate that the requested and offered cards exist in the through tables.
try:
have_through_obj = self.trade_offer.trade_offer_have_cards.get(card=self.requested_card)
except TradeOfferHaveCard.DoesNotExist:
raise ValidationError("The requested card must be one of the trade offer's available cards (have_cards).")
# Validate that offered_card is in trade_offer.want_cards
try:
want_through_obj = self.trade_offer.trade_offer_want_cards.get(card=self.offered_card)
except TradeOfferWantCard.DoesNotExist:
raise ValidationError("The offered card must be one of the trade offer's requested cards (want_cards).")
# For new acceptances, do not allow creation if the trade offer is closed.
if not self.pk and self.trade_offer.is_closed:
raise ValidationError("This trade offer is closed. No more acceptances are allowed.")
@ -317,20 +182,15 @@ class TradeAcceptance(models.Model):
if self.pk:
active_acceptances = active_acceptances.exclude(pk=self.pk)
# Count active acceptances for the requested card.
requested_count = active_acceptances.filter(requested_card=self.requested_card).count()
if requested_count >= have_through_obj.quantity:
raise ValidationError("This requested card has been fully accepted.")
# Count active acceptances for the offered card.
offered_count = active_acceptances.filter(offered_card=self.offered_card).count()
if offered_count >= want_through_obj.quantity:
raise ValidationError("This offered card has already been fully used.")
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,
@ -346,17 +206,12 @@ class TradeAcceptance(models.Model):
]:
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)
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.
"""
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.THANKED_BY_BOTH,
self.AcceptanceState.REJECTED_BY_INITIATOR,
@ -364,10 +219,7 @@ class TradeAcceptance(models.Model):
]:
raise ValueError(f"No transitions allowed from the terminal state '{self.state}'.")
allowed = [x for x,y in self.get_allowed_state_transitions(user)]
print(allowed)
print(new_state)
allowed = [x for x, y in self.get_allowed_state_transitions(user)]
if new_state not in allowed:
raise ValueError(f"Transition from {self.state} to {new_state} is not allowed.")
@ -378,7 +230,6 @@ class TradeAcceptance(models.Model):
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"])
@ -389,57 +240,50 @@ class TradeAcceptance(models.Model):
f"offered_card={self.offered_card}, state={self.state})")
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,
},
}
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,
},
}
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

@ -22,23 +22,55 @@ def validate_want_cards_rarity(sender, instance, action, **kwargs):
if action == "post_add":
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()
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,
]
@receiver(post_save, sender=TradeOfferWantCard)
@receiver(post_delete, sender=TradeOfferWantCard)
def update_aggregates_from_want_card(sender, instance, **kwargs):
def update_qty_for_trade_offer(trade_offer, card, side):
if side == 'have':
count = TradeAcceptance.objects.filter(
trade_offer=trade_offer,
requested_card=card,
state__in=ACTIVE_STATES
).count()
TradeOfferHaveCard.objects.filter(
trade_offer=trade_offer,
card=card
).update(qty_accepted=count)
if count >= TradeOfferHaveCard.objects.filter(trade_offer=trade_offer, card=card).first().quantity:
trade_offer.is_closed = True
trade_offer.save(update_fields=["is_closed"])
elif side == 'want':
count = TradeAcceptance.objects.filter(
trade_offer=trade_offer,
offered_card=card,
state__in=ACTIVE_STATES
).count()
TradeOfferWantCard.objects.filter(
trade_offer=trade_offer,
card=card
).update(qty_accepted=count)
if count >= TradeOfferWantCard.objects.filter(trade_offer=trade_offer, card=card).first().quantity:
trade_offer.is_closed = True
trade_offer.save(update_fields=["is_closed"])
def update_all_qty(instance):
trade_offer = instance.trade_offer
if trade_offer and hasattr(trade_offer, 'update_aggregates'):
trade_offer.update_aggregates()
update_qty_for_trade_offer(trade_offer, instance.requested_card, 'have')
update_qty_for_trade_offer(trade_offer, instance.offered_card, 'want')
@receiver(post_save, sender=TradeAcceptance)
def trade_acceptance_post_save(sender, instance, **kwargs):
update_all_qty(instance)
@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()
def trade_acceptance_post_delete(sender, instance, **kwargs):
update_all_qty(instance)

View file

@ -90,23 +90,11 @@ class TradeOfferAllListView(ListView):
)
)
.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()
)
)
)
if show_closed:
queryset = queryset.filter(is_active=False)
queryset = queryset.filter(is_closed=True)
else:
queryset = queryset.filter(is_active=True)
queryset = queryset.filter(is_closed=False)
offers_page = request.GET.get("offers_page")
offers_paginator = Paginator(queryset, 10)
@ -135,18 +123,6 @@ class TradeOfferMyListView(LoginRequiredMixin, ListView):
)
)
.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):
@ -173,9 +149,9 @@ class TradeOfferMyListView(LoginRequiredMixin, ListView):
queryset = self.get_queryset().filter(initiated_by=selected_friend_code)
if show_closed:
queryset = queryset.filter(is_active=False)
queryset = queryset.filter(is_closed=True)
else:
queryset = queryset.filter(is_active=True)
queryset = queryset.filter(is_closed=False)
offers_page = request.GET.get("offers_page")
offers_paginator = Paginator(queryset, 10)
@ -267,8 +243,8 @@ class TradeOfferDeleteView(LoginRequiredMixin, DeleteView):
return self.render_to_response(context)
else:
if trade_offer.acceptances.count() > 0:
trade_offer.manually_closed = True
trade_offer.save(update_fields=["manually_closed"])
trade_offer.is_closed = True
trade_offer.save(update_fields=["is_closed"])
messages.success(request, "Trade offer has been marked as closed.")
return HttpResponseRedirect(self.get_success_url())
else:
@ -330,9 +306,7 @@ class TradeOfferSearchView(LoginRequiredMixin, ListView):
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")
is_closed=False,
).exclude(initiated_by__in=self.request.user.friend_codes.all())
# Chain filters for offered selections (i.e. the user "has" cards).
@ -408,13 +382,6 @@ class TradeOfferDetailView(LoginRequiredMixin, DetailView):
)
)
)
.annotate(
is_active=Case(
When(manually_closed=False, then=Value(True)),
default=Value(False),
output_field=BooleanField()
)
)
)
def get_context_data(self, **kwargs):
@ -436,9 +403,7 @@ class TradeOfferDetailView(LoginRequiredMixin, DetailView):
# 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
@ -466,7 +431,7 @@ class TradeAcceptanceCreateView(LoginRequiredMixin, CreateView):
def dispatch(self, request, *args, **kwargs):
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:
if self.trade_offer.initiated_by_id in request.user.friend_codes.values_list("id", flat=True) or self.trade_offer.is_closed:
raise PermissionDenied("You cannot accept this trade offer.")
if not request.user.friend_codes.exists():
raise PermissionDenied("No friend codes available for your account.")
@ -485,13 +450,6 @@ class TradeAcceptanceCreateView(LoginRequiredMixin, CreateView):
)
)
)
.annotate(
is_active=Case(
When(manually_closed=False, then=Value(True)),
default=Value(False),
output_field=BooleanField()
)
)
.get(pk=self.kwargs['offer_pk'])
)
@ -520,10 +478,11 @@ class TradeAcceptanceUpdateView(LoginRequiredMixin, UpdateView):
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.")
friend_codes = request.user.friend_codes.values_list("id", flat=True)
if self.object.accepted_by_id not in friend_codes and self.object.trade_offer.initiated_by_id not in friend_codes:
raise PermissionDenied("You are not authorized to update this acceptance.")
return super().dispatch(request, *args, **kwargs)
def get_form_kwargs(self):