pkmntrade.club/trades/signals.py

255 lines
No EOL
11 KiB
Python

from django.db.models.signals import post_save, post_delete, pre_save
from django.dispatch import receiver
from django.db.models import F
from trades.models import TradeOfferHaveCard, TradeOfferWantCard, TradeAcceptance
from django.db import transaction
from accounts.models import CustomUser
from datetime import timedelta
from django.utils import timezone
import uuid
import hashlib
from django.conf import settings
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,
]
def adjust_qty_for_trade_offer(trade_offer, card, side, delta):
"""
Increment (or decrement) qty_accepted by delta for the given card on the specified side.
"""
if side == 'have':
TradeOfferHaveCard.objects.filter(
trade_offer=trade_offer,
card=card
).update(qty_accepted=F('qty_accepted') + delta)
elif side == 'want':
TradeOfferWantCard.objects.filter(
trade_offer=trade_offer,
card=card
).update(qty_accepted=F('qty_accepted') + delta)
def update_trade_offer_closed_status(trade_offer):
"""
Check if both sides of the trade offer meet the quantity requirement.
Mark the trade_offer as closed if all cards have qty_accepted
greater than or equal to quantity; otherwise, mark it as open.
"""
have_complete = not TradeOfferHaveCard.objects.filter(
trade_offer=trade_offer,
qty_accepted__lt=F('quantity')
).exists()
want_complete = not TradeOfferWantCard.objects.filter(
trade_offer=trade_offer,
qty_accepted__lt=F('quantity')
).exists()
closed = have_complete or want_complete
if trade_offer.is_closed != closed:
trade_offer.is_closed = closed
trade_offer.save(update_fields=["is_closed"])
@receiver(pre_save, sender=TradeAcceptance)
def trade_acceptance_pre_save(sender, instance, **kwargs):
if instance.pk:
old_instance = TradeAcceptance.objects.get(pk=instance.pk)
instance._old_state = old_instance.state
@receiver(post_save, sender=TradeAcceptance)
def trade_acceptance_post_save(sender, instance, created, **kwargs):
delta = 0
if created:
if instance.state in ACTIVE_STATES:
delta = 1
else:
old_state = getattr(instance, '_old_state', None)
if old_state is not None:
if old_state in ACTIVE_STATES and instance.state not in ACTIVE_STATES:
delta = -1
elif old_state not in ACTIVE_STATES and instance.state in ACTIVE_STATES:
delta = 1
if delta != 0:
trade_offer = instance.trade_offer
adjust_qty_for_trade_offer(trade_offer, instance.requested_card, side='have', delta=delta)
adjust_qty_for_trade_offer(trade_offer, instance.offered_card, side='want', delta=delta)
update_trade_offer_closed_status(trade_offer)
@receiver(post_delete, sender=TradeAcceptance)
def trade_acceptance_post_delete(sender, instance, **kwargs):
if instance.state in ACTIVE_STATES:
delta = -1
trade_offer = instance.trade_offer
adjust_qty_for_trade_offer(trade_offer, instance.requested_card, side='have', delta=delta)
adjust_qty_for_trade_offer(trade_offer, instance.offered_card, side='want', delta=delta)
update_trade_offer_closed_status(trade_offer)
@receiver(post_save, sender=TradeAcceptance)
def trade_acceptance_email_notification(sender, instance, created, **kwargs):
# Only proceed if the update was triggered by an acting user.
if not hasattr(instance, "_actioning_user"):
print("No actioning user")
return
# check if were in debug mode
if settings.DEBUG:
print("DEBUG: skipping email notification in debug mode")
return
acting_user = instance._actioning_user
state = instance.state
if state == TradeAcceptance.AcceptanceState.ACCEPTED:
state = "accepted"
elif state == TradeAcceptance.AcceptanceState.SENT:
state = "sent"
elif state == TradeAcceptance.AcceptanceState.RECEIVED:
state = "received"
elif state == TradeAcceptance.AcceptanceState.THANKED_BY_INITIATOR:
state = "thanked_by_initiator"
elif state == TradeAcceptance.AcceptanceState.THANKED_BY_ACCEPTOR:
state = "thanked_by_acceptor"
elif state == TradeAcceptance.AcceptanceState.THANKED_BY_BOTH:
state = "thanked_by_both"
elif state == TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR:
state = "rejected_by_initiator"
elif state == TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR:
state = "rejected_by_acceptor"
else:
return
print("state", state)
print("acting_user", acting_user)
# Determine the non-acting party:
if instance.trade_offer.initiated_by == acting_user:
# The initiator made the change; notify the acceptor.
recipient_user = instance.accepted_by.user
email_template = "trades/email/trade_update_" + state + ".txt"
email_subject = "[PKMN Trade Club] Trade Update"
else:
# The acceptor made the change; notify the initiator.
recipient_user = instance.trade_offer.initiated_by.user
email_template = "trades/email/trade_update_" + state + ".txt"
email_subject = "[PKMN Trade Club] Trade Update"
is_initiator = instance.trade_offer.initiated_by == acting_user
from django.template.loader import render_to_string
email_context = {
"has_card": instance.requested_card,
"want_card": instance.offered_card,
"hash": instance.hash,
"acting_user": acting_user.username,
"recipient_user": recipient_user.username,
"acting_user_friend_code": instance.trade_offer.initiated_by.friend_code if is_initiator else instance.accepted_by.friend_code,
"is_initiator": is_initiator,
"pk": instance.pk,
}
print("email_context", email_context)
email_body = render_to_string(email_template, email_context)
from django.core.mail import send_mail
send_mail(
email_subject,
email_body,
None, # Django will use DEFAULT_FROM_EMAIL from settings
[recipient_user.email],
)
# Clean up the temporary attribute.
del instance._actioning_user
@receiver(post_save, sender=TradeAcceptance)
def trade_acceptance_reputation_update(sender, instance, created, **kwargs):
"""
Update the denormalized reputation score on the user model based on
state transitions for TradeAcceptance.
- THANKED_BY_BOTH: both the initiator and the acceptor receive +1 when transitioning
into this state, and -1 when leaving it.
- REJECTED_BY_INITIATOR: only the acceptor gets -1 when transitioning into it (and +1 when leaving it).
- REJECTED_BY_ACCEPTOR: only the initiator gets -1 when transitioning into it (and +1 when leaving it).
Creation events are ignored because trade acceptances are never created with a terminal state.
"""
if created:
return # No action on creation as terminal states are not expected.
thanks_delta = 0
rejection_delta_initiator = 0 # Delta for the initiator's reputation
rejection_delta_acceptor = 0 # Delta for the acceptor's reputation
old_state = getattr(instance, '_old_state', None)
if old_state is None:
return
# Handle THANKED_BY_BOTH transitions
if old_state != TradeAcceptance.AcceptanceState.THANKED_BY_BOTH and instance.state == TradeAcceptance.AcceptanceState.THANKED_BY_BOTH:
thanks_delta = 1
elif old_state == TradeAcceptance.AcceptanceState.THANKED_BY_BOTH and instance.state != TradeAcceptance.AcceptanceState.THANKED_BY_BOTH:
thanks_delta = -1
# Handle REJECTED_BY_INITIATOR transitions (affects the acceptor)
if old_state != TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR and instance.state == TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR:
rejection_delta_acceptor = -1
elif old_state == TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR and instance.state != TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR:
rejection_delta_acceptor = 1
# Handle REJECTED_BY_ACCEPTOR transitions (affects the initiator)
if old_state != TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR and instance.state == TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR:
rejection_delta_initiator = -1
elif old_state == TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR and instance.state != TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR:
rejection_delta_initiator = 1
# Apply reputation updates:
# For THANKED_BY_BOTH, update both users.
if thanks_delta:
CustomUser.objects.filter(pk=instance.trade_offer.initiated_by.user.pk).update(
reputation_score=F("reputation_score") + thanks_delta
)
CustomUser.objects.filter(pk=instance.accepted_by.user.pk).update(
reputation_score=F("reputation_score") + thanks_delta
)
# For REJECTED_BY_INITIATOR, update only the acceptor.
if rejection_delta_acceptor:
CustomUser.objects.filter(pk=instance.accepted_by.user.pk).update(
reputation_score=F("reputation_score") + rejection_delta_acceptor
)
# For REJECTED_BY_ACCEPTOR, update only the initiator.
if rejection_delta_initiator:
CustomUser.objects.filter(pk=instance.trade_offer.initiated_by.user.pk).update(
reputation_score=F("reputation_score") + rejection_delta_initiator
)
@receiver(post_delete, sender=TradeAcceptance)
def trade_acceptance_reputation_delete(sender, instance, **kwargs):
"""
When a TradeAcceptance is deleted, adjust the reputation score for the
affected user(s) by reversing any reputation changes previously applied.
- If the deleted instance was in THANKED_BY_BOTH: subtract 1 from both parties.
- If it was in REJECTED_BY_INITIATOR: add 1 to the acceptor.
- If it was in REJECTED_BY_ACCEPTOR: add 1 to the initiator.
"""
if instance.state == TradeAcceptance.AcceptanceState.THANKED_BY_BOTH:
CustomUser.objects.filter(pk=instance.trade_offer.initiated_by.user.pk).update(
reputation_score=F("reputation_score") - 1
)
CustomUser.objects.filter(pk=instance.accepted_by.user.pk).update(
reputation_score=F("reputation_score") - 1
)
if instance.state == TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR:
CustomUser.objects.filter(pk=instance.accepted_by.user.pk).update(
reputation_score=F("reputation_score") + 1
)
if instance.state == TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR:
CustomUser.objects.filter(pk=instance.trade_offer.initiated_by.user.pk).update(
reputation_score=F("reputation_score") + 1
)