progress on conversion to tailwind
This commit is contained in:
parent
6a872124c6
commit
6e2843c60e
110 changed files with 4997 additions and 1691 deletions
297
trades/models.py
297
trades/models.py
|
|
@ -1,72 +1,255 @@
|
|||
from django.db import models
|
||||
import hashlib # <-- import hashlib for computing md5
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Q
|
||||
import hashlib
|
||||
from cards.models import Card
|
||||
from friend_codes.models import FriendCode
|
||||
from accounts.models import FriendCode
|
||||
|
||||
class TradeOffer(models.Model):
|
||||
id = models.AutoField(primary_key=True)
|
||||
hash = models.CharField(max_length=8, editable=False)
|
||||
initiated_by = models.ForeignKey("friend_codes.FriendCode", on_delete=models.PROTECT, related_name='initiated_by')
|
||||
accepted_by = models.ForeignKey("friend_codes.FriendCode", on_delete=models.PROTECT, null=True, blank=True, related_name='accepted_by')
|
||||
want_cards = models.ManyToManyField("cards.Card", related_name='trade_offers_want')
|
||||
have_cards = models.ManyToManyField("cards.Card", related_name='trade_offers_have')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
id = models.AutoField(primary_key=True)
|
||||
manually_closed = models.BooleanField(default=False)
|
||||
hash = models.CharField(max_length=8, editable=False)
|
||||
initiated_by = models.ForeignKey(
|
||||
"accounts.FriendCode",
|
||||
on_delete=models.PROTECT,
|
||||
related_name='initiated_trade_offers'
|
||||
)
|
||||
# Use custom through models to support multiples.
|
||||
want_cards = models.ManyToManyField(
|
||||
"cards.Card",
|
||||
related_name='trade_offers_want',
|
||||
through="TradeOfferWantCard"
|
||||
)
|
||||
have_cards = models.ManyToManyField(
|
||||
"cards.Card",
|
||||
related_name='trade_offers_have',
|
||||
through="TradeOfferHaveCard"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class State(models.TextChoices):
|
||||
INITIATED = 'INITIATED', 'Initiated'
|
||||
ACCEPTED = 'ACCEPTED', 'Accepted'
|
||||
SENT = 'SENT', 'Sent'
|
||||
RECEIVED = 'RECEIVED', 'Received'
|
||||
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()])
|
||||
return f"Want: {want_names} -> Have: {have_names}"
|
||||
|
||||
state = models.CharField(
|
||||
max_length=10,
|
||||
choices=State.choices,
|
||||
default=State.INITIATED,
|
||||
)
|
||||
def save(self, *args, **kwargs):
|
||||
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]
|
||||
super().save(update_fields=["hash"])
|
||||
|
||||
def __str__(self):
|
||||
return f"Want: {', '.join([x.name for x in self.want_cards.all()])} -> Have: {', '.join([x.name for x in self.have_cards.all()])}"
|
||||
@property
|
||||
def is_closed(self):
|
||||
if self.manually_closed:
|
||||
return True
|
||||
|
||||
def update_state(self, new_state):
|
||||
from .models import TradeAcceptance # local import to avoid circular dependencies
|
||||
active_states = [
|
||||
TradeAcceptance.AcceptanceState.ACCEPTED,
|
||||
TradeAcceptance.AcceptanceState.SENT,
|
||||
TradeAcceptance.AcceptanceState.RECEIVED,
|
||||
TradeAcceptance.AcceptanceState.COMPLETED
|
||||
]
|
||||
|
||||
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):
|
||||
"""
|
||||
Explicitly update the trade state to new_state if allowed.
|
||||
|
||||
Allowed transitions:
|
||||
- INITIATED -> ACCEPTED
|
||||
- ACCEPTED -> SENT
|
||||
- SENT -> RECEIVED
|
||||
|
||||
Raises:
|
||||
ValueError: If the new_state is not allowed.
|
||||
Through model for TradeOffer.have_cards.
|
||||
Represents the card the initiator is offering along with the quantity available.
|
||||
"""
|
||||
allowed_transitions = {
|
||||
self.State.INITIATED: self.State.ACCEPTED,
|
||||
self.State.ACCEPTED: self.State.SENT,
|
||||
self.State.SENT: self.State.RECEIVED
|
||||
}
|
||||
trade_offer = models.ForeignKey(
|
||||
TradeOffer,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='trade_offer_have_cards'
|
||||
)
|
||||
card = models.ForeignKey("cards.Card", on_delete=models.PROTECT)
|
||||
quantity = models.PositiveIntegerField(default=1)
|
||||
|
||||
# Check that new_state is one of the defined State choices
|
||||
if new_state not in [choice[0] for choice in self.State.choices]:
|
||||
raise ValueError(f"'{new_state}' is not a valid state.")
|
||||
def __str__(self):
|
||||
return f"{self.card.name} x{self.quantity} (Have side for offer {self.trade_offer.hash})"
|
||||
|
||||
# If the current state is already final, no further transition is allowed.
|
||||
if self.state not in allowed_transitions:
|
||||
raise ValueError(f"No transitions allowed from the final state '{self.state}'.")
|
||||
class Meta:
|
||||
unique_together = ("trade_offer", "card")
|
||||
|
||||
# Verify that the desired new_state is the valid transition for the current state.
|
||||
if allowed_transitions[self.state] != new_state:
|
||||
raise ValueError(f"Transition from {self.state} to {new_state} is not allowed.")
|
||||
class TradeOfferWantCard(models.Model):
|
||||
"""
|
||||
Through model for TradeOffer.want_cards.
|
||||
Represents the card the initiator is requesting along with the quantity requested.
|
||||
"""
|
||||
trade_offer = models.ForeignKey(
|
||||
TradeOffer,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='trade_offer_want_cards'
|
||||
)
|
||||
card = models.ForeignKey("cards.Card", on_delete=models.PROTECT)
|
||||
quantity = models.PositiveIntegerField(default=1)
|
||||
|
||||
self.state = new_state
|
||||
# Save all changes so that any in-memory modifications (like accepted_by) are persisted.
|
||||
self.save() # Changed from self.save(update_fields=["state"])
|
||||
def __str__(self):
|
||||
return f"{self.card.name} x{self.quantity} (Want side for offer {self.trade_offer.hash})"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Determine if the object is being created (i.e. it doesn't yet have a pk)
|
||||
is_new = self.pk is None
|
||||
super().save(*args, **kwargs)
|
||||
# Once the object has a pk, compute and save the hash if it hasn't been set yet.
|
||||
if is_new and not self.hash:
|
||||
self.hash = hashlib.md5(str(self.id).encode('utf-8')).hexdigest()[:8]
|
||||
super().save(update_fields=["hash"])
|
||||
class Meta:
|
||||
unique_together = ("trade_offer", "card")
|
||||
|
||||
class TradeAcceptance(models.Model):
|
||||
class AcceptanceState(models.TextChoices):
|
||||
ACCEPTED = 'ACCEPTED', 'Accepted'
|
||||
SENT = 'SENT', 'Sent'
|
||||
RECEIVED = 'RECEIVED', 'Received'
|
||||
COMPLETED = 'COMPLETED', 'Completed'
|
||||
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'
|
||||
)
|
||||
accepted_by = models.ForeignKey(
|
||||
"accounts.FriendCode",
|
||||
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,
|
||||
related_name='accepted_offered'
|
||||
)
|
||||
state = models.CharField(
|
||||
max_length=25,
|
||||
choices=AcceptanceState.choices,
|
||||
default=AcceptanceState.ACCEPTED
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
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
|
||||
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.")
|
||||
|
||||
active_states = [
|
||||
self.AcceptanceState.ACCEPTED,
|
||||
self.AcceptanceState.SENT,
|
||||
self.AcceptanceState.RECEIVED,
|
||||
self.AcceptanceState.COMPLETED,
|
||||
]
|
||||
|
||||
active_acceptances = self.trade_offer.acceptances.filter(state__in=active_states)
|
||||
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 update_state(self, new_state):
|
||||
"""
|
||||
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.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,
|
||||
},
|
||||
}
|
||||
|
||||
if new_state not in allowed_transitions.get(self.state, {}):
|
||||
raise ValueError(f"Transition from {self.state} to {new_state} is not allowed.")
|
||||
|
||||
self.state = new_state
|
||||
self.save(update_fields=["state"])
|
||||
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue