256 lines
No EOL
11 KiB
Python
256 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.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
|
|
) |