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 )