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.core.mail import send_mail from django.conf import settings from django.template.loader import render_to_string from django.contrib.sites.models import Site 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"): 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 # 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 else: # The acceptor made the change; notify the initiator. recipient_user = instance.trade_offer.initiated_by.user is_initiator = instance.trade_offer.initiated_by == acting_user email_context = { "has_card": instance.requested_card, "want_card": instance.offered_card, "hash": instance.hash, "acting_user": acting_user.username, "acting_user_ign": instance.trade_offer.initiated_by.in_game_name if is_initiator else instance.accepted_by.in_game_name, "recipient_user": recipient_user.username, "recipient_user_ign": instance.accepted_by.in_game_name if is_initiator else instance.trade_offer.initiated_by.in_game_name, "acting_user_friend_code": instance.trade_offer.initiated_by.friend_code if is_initiator else instance.accepted_by.friend_code, "is_initiator": is_initiator, "domain": Site.objects.get_current().domain, "pk": instance.pk, } email_template = "email/trades/trade_update_" + state + ".txt" email_subject = render_to_string("email/common/subject.txt", email_context) email_subject += render_to_string("email/trades/trade_update_" + state + "_subject.txt", email_context) email_body = render_to_string(email_template, email_context) print("initiated by: ", instance.trade_offer.initiated_by, ", accepted by: ", instance.accepted_by, ", acting user: ", acting_user, ", recipient user: ", recipient_user, ", state: ", state) send_mail( email_subject, email_body, None, [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 )