finished conversion to tailwind
This commit is contained in:
parent
6e2843c60e
commit
d62956d465
50 changed files with 2490 additions and 1273 deletions
334
trades/models.py
334
trades/models.py
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue