pkmntrade.club/src/pkmntrade_club/accounts/views.py
badbl0cks af2f48a491
refactor(db): update cursor rules and enhance deployment rollback script
- Standardized string formatting in cursor rules for consistency.
- Added a new rollback deployment script to facilitate blue-green deployment strategy.
- Removed outdated seed data files and introduced new rarity mappings for better data management.
- Improved model relationships and query optimizations in various views and admin configurations.
- Enhanced caching strategies across templates to improve performance and reduce load times, including jitter in cache settings for better performance.
- Refactored card-related views and templates to utilize new model fields and relationships.
2025-06-19 15:42:36 -07:00

494 lines
19 KiB
Python

from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied
from django.db.models import BooleanField, Case, Q, Value, When
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.http import urlencode
from django.views.generic import (
CreateView,
DeleteView,
TemplateView,
UpdateView,
View,
)
from pkmntrade_club.accounts.forms import FriendCodeForm, UserSettingsForm
from pkmntrade_club.accounts.models import FriendCode
from pkmntrade_club.common.mixins import ReusablePaginationMixin
from pkmntrade_club.trades.mixins import FriendCodeRequiredMixin
from pkmntrade_club.trades.models import TradeAcceptance, TradeOffer
class AddFriendCodeView(LoginRequiredMixin, CreateView):
"""
Add a new friend code for the current user. If the user does not yet have a default,
the newly added code will automatically become the default.
"""
model = FriendCode
form_class = FriendCodeForm
template_name = "friend_codes/add_friend_code.html"
def get_success_url(self):
base_url = reverse("dashboard")
return f"{base_url}?{urlencode({'tab': 'friend_codes'})}"
def form_valid(self, form):
form.instance.user = self.request.user
messages.success(self.request, "Friend code added successfully.")
return super().form_valid(form)
class DeleteFriendCodeView(LoginRequiredMixin, DeleteView):
"""
Remove an existing friend code.
Prevent deletion if the friend code is bound to any trade offers.
Also, prevent deletion if the friend code is either the only one or
is set as the default friend code.
"""
model = FriendCode
template_name = "friend_codes/confirm_delete_friend_code.html"
context_object_name = "friend_code"
def get_success_url(self):
base_url = reverse("dashboard")
return f"{base_url}?{urlencode({'tab': 'friend_codes'})}"
def get_queryset(self):
# Only allow deletion of friend codes owned by the current user.
return FriendCode.objects.filter(user=self.request.user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
friend_code = self.get_object()
user = self.request.user
# Determine if the deletion should be disabled.
disable_delete = False
error_message = None
if user.friend_codes.count() == 1:
disable_delete = True
error_message = "Cannot delete your only friend code."
elif user.default_friend_code == friend_code:
disable_delete = True
error_message = (
"Cannot delete your default friend code. "
"Please set a different default first."
)
context["disable_delete"] = disable_delete
context["error_message"] = error_message
return context
def post(self, request, *args, **kwargs):
self.object = self.get_object()
user = self.object.user
if user.friend_codes.count() == 1:
messages.error(request, "Cannot remove your only friend code.")
return redirect(self.get_success_url())
if user.default_friend_code == self.object:
messages.error(
request,
"Cannot delete your default friend code. Please set a different default first.",
)
return redirect(self.get_success_url())
trade_offer_exists = TradeOffer.objects.filter(
initiated_by_id=self.object.pk
).exists()
trade_acceptance_exists = TradeAcceptance.objects.filter(
accepted_by_id=self.object.pk
).exists()
if trade_offer_exists or trade_acceptance_exists:
messages.error(
request,
"Cannot remove this friend code because there are existing trade offers associated with it.",
)
return redirect(self.get_success_url())
self.object.delete()
messages.success(request, "Friend code removed successfully.")
return redirect(self.get_success_url())
class ChangeDefaultFriendCodeView(LoginRequiredMixin, View):
"""
Change the default friend code for the current user.
"""
def post(self, request, *args, **kwargs):
friend_code_id = kwargs.get("pk")
friend_code = get_object_or_404(
FriendCode, pk=friend_code_id, user=request.user
)
request.user.set_default_friend_code(friend_code)
messages.success(request, "Default friend code updated successfully.")
base_url = reverse("dashboard")
query_string = urlencode({"tab": "friend_codes"})
return redirect(f"{base_url}?{query_string}")
class EditFriendCodeView(LoginRequiredMixin, UpdateView):
"""
Edit the in-game name for a friend code.
The friend code itself is displayed as plain text.
Also includes "Set Default" and "Delete" buttons in the template.
"""
model = FriendCode
# Only the in_game_name field is editable
fields = ["in_game_name"]
template_name = "friend_codes/edit_friend_code.html"
context_object_name = "friend_code"
def get_success_url(self):
base_url = reverse("dashboard")
return f"{base_url}?{urlencode({'tab': 'friend_codes'})}"
def get_queryset(self):
# Ensure the user can only edit their own friend codes
return FriendCode.objects.filter(user=self.request.user)
def form_valid(self, form):
messages.success(self.request, "Friend code updated successfully.")
return super().form_valid(form)
class DashboardView(
LoginRequiredMixin, FriendCodeRequiredMixin, ReusablePaginationMixin, TemplateView
):
template_name = "account/dashboard.html"
def post(self, request, *args, **kwargs):
if "update_settings" in request.POST:
from pkmntrade_club.accounts.forms import UserSettingsForm
form = UserSettingsForm(request.POST, instance=request.user)
if form.is_valid():
form.save()
messages.success(request, "Settings updated successfully.")
else:
messages.error(request, "Please correct the errors below.")
return self.get(request, *args, **kwargs)
def get_selected_friend_code(self):
friend_codes = self.request.user.friend_codes.all()
friend_code_param = self.request.GET.get("friend_code")
if friend_code_param:
try:
selected_friend_code = friend_codes.get(pk=friend_code_param)
except friend_codes.model.DoesNotExist:
selected_friend_code = (
self.request.user.default_friend_code or friend_codes.first()
)
else:
selected_friend_code = (
self.request.user.default_friend_code or friend_codes.first()
)
if not selected_friend_code:
raise PermissionDenied(
"You do not have an active friend code associated with your account."
)
return selected_friend_code
def get_dashboard_offers_paginated(self, page_param):
selected_friend_code = self.get_selected_friend_code()
queryset = TradeOffer.objects.filter(
initiated_by=selected_friend_code, is_closed=False
)
object_list, pagination_context = self.paginate_data(queryset, int(page_param))
return {"object_list": object_list, "page_obj": pagination_context}
def get_involved_acceptances(self, selected_friend_code):
terminal_states = [
TradeAcceptance.AcceptanceState.THANKED_BY_INITIATOR,
TradeAcceptance.AcceptanceState.THANKED_BY_ACCEPTOR,
TradeAcceptance.AcceptanceState.THANKED_BY_BOTH,
TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR,
TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR,
]
involved = (
TradeAcceptance.objects.filter(
Q(trade_offer__initiated_by=selected_friend_code)
| Q(accepted_by=selected_friend_code)
)
.select_related(
"trade_offer__initiated_by__user",
"accepted_by__user",
"requested_card__rarity",
"requested_card__cardset",
"offered_card__rarity",
"offered_card__cardset",
)
.prefetch_related(
"requested_card__translations",
"offered_card__translations",
)
.order_by("-updated_at")
)
return involved.exclude(state__in=terminal_states)
def get_trade_acceptances_waiting_paginated(self, page_param):
selected_friend_code = self.get_selected_friend_code()
involved = self.get_involved_acceptances(selected_friend_code)
from django.db.models import Q
waiting = involved.filter(
Q(
trade_offer__initiated_by=selected_friend_code,
state__in=[
TradeAcceptance.AcceptanceState.ACCEPTED,
TradeAcceptance.AcceptanceState.RECEIVED,
],
)
| Q(
accepted_by=selected_friend_code,
state__in=[TradeAcceptance.AcceptanceState.SENT],
)
)
object_list, pagination_context = self.paginate_data(waiting, int(page_param))
return {"object_list": object_list, "page_obj": pagination_context}
def get_other_party_trade_acceptances_paginated(self, page_param):
selected_friend_code = self.get_selected_friend_code()
involved = self.get_involved_acceptances(selected_friend_code)
from django.db.models import Q
waiting = involved.filter(
Q(
trade_offer__initiated_by=selected_friend_code,
state__in=[
TradeAcceptance.AcceptanceState.ACCEPTED,
TradeAcceptance.AcceptanceState.RECEIVED,
],
)
| Q(
accepted_by=selected_friend_code,
state__in=[TradeAcceptance.AcceptanceState.SENT],
)
)
others = involved.exclude(pk__in=waiting.values("pk"))
object_list, pagination_context = self.paginate_data(others, int(page_param))
return {"object_list": object_list, "page_obj": pagination_context}
def get_closed_offers_paginated(self, page_param):
selected_friend_code = self.get_selected_friend_code()
queryset = TradeOffer.objects.filter(
initiated_by=selected_friend_code, is_closed=True
)
object_list, pagination_context = self.paginate_data(queryset, int(page_param))
return {"object_list": object_list, "page_obj": pagination_context}
def get_closed_acceptances_paginated(self, page_param):
selected_friend_code = self.get_selected_friend_code()
terminal_success_states = [
TradeAcceptance.AcceptanceState.THANKED_BY_INITIATOR,
TradeAcceptance.AcceptanceState.THANKED_BY_ACCEPTOR,
TradeAcceptance.AcceptanceState.THANKED_BY_BOTH,
]
acceptance_qs = (
TradeAcceptance.objects.filter(
Q(trade_offer__initiated_by=selected_friend_code)
| Q(accepted_by=selected_friend_code),
state__in=terminal_success_states,
)
.select_related(
"trade_offer__initiated_by__user",
"accepted_by__user",
"requested_card__rarity",
"requested_card__cardset",
"offered_card__rarity",
"offered_card__cardset",
)
.prefetch_related(
"requested_card__translations",
"offered_card__translations",
)
.order_by("-updated_at")
)
object_list, pagination_context = self.paginate_data(
acceptance_qs, int(page_param)
)
return {"object_list": object_list, "page_obj": pagination_context}
def get_rejected_by_me_paginated(self, page_param):
selected_friend_code = self.get_selected_friend_code()
rejection = (
TradeAcceptance.objects.filter(
Q(
trade_offer__initiated_by=selected_friend_code,
state=TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR,
)
| Q(
accepted_by=selected_friend_code,
state=TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR,
)
)
.select_related(
"trade_offer__initiated_by__user",
"accepted_by__user",
"requested_card__rarity",
"requested_card__cardset",
"offered_card__rarity",
"offered_card__cardset",
)
.prefetch_related(
"requested_card__translations",
"offered_card__translations",
)
.order_by("-updated_at")
)
object_list, pagination_context = self.paginate_data(rejection, int(page_param))
return {"object_list": object_list, "page_obj": pagination_context}
def get_rejected_by_them_paginated(self, page_param):
selected_friend_code = self.get_selected_friend_code()
rejection = (
TradeAcceptance.objects.filter(
Q(
trade_offer__initiated_by=selected_friend_code,
state=TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR,
)
| Q(
accepted_by=selected_friend_code,
state=TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR,
)
)
.select_related(
"trade_offer__initiated_by__user",
"accepted_by__user",
"requested_card__rarity",
"requested_card__cardset",
"offered_card__rarity",
"offered_card__cardset",
)
.prefetch_related(
"requested_card__translations",
"offered_card__translations",
)
.order_by("-updated_at")
)
object_list, pagination_context = self.paginate_data(rejection, int(page_param))
return {"object_list": object_list, "page_obj": pagination_context}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
request = self.request
selected_friend_code = self.get_selected_friend_code()
context["selected_friend_code"] = selected_friend_code
# Get the default friend code's primary key if it exists
default_pk = getattr(request.user.default_friend_code, "pk", None)
# Annotate friend codes with is_default flag
context["friend_codes"] = request.user.friend_codes.all().annotate(
is_default=Case(
When(pk=default_pk, then=Value(True)),
default=Value(False),
output_field=BooleanField(),
)
)
ajax_section = request.GET.get("ajax_section")
if ajax_section == "dashboard_offers":
offers_page = request.GET.get("page", 1)
else:
offers_page = request.GET.get("offers_page", 1)
if ajax_section == "waiting_acceptances":
waiting_page = request.GET.get("page", 1)
else:
waiting_page = request.GET.get("waiting_page", 1)
if ajax_section == "other_party_acceptances":
other_page = request.GET.get("page", 1)
else:
other_page = request.GET.get("other_page", 1)
if ajax_section == "closed_offers":
closed_offers_page = request.GET.get("page", 1)
else:
closed_offers_page = request.GET.get("closed_offers_page", 1)
if ajax_section == "closed_acceptances":
closed_acceptances_page = request.GET.get("page", 1)
else:
closed_acceptances_page = request.GET.get("closed_acceptances_page", 1)
if ajax_section == "rejected_by_me":
rejected_by_me_page = request.GET.get("page", 1)
else:
rejected_by_me_page = request.GET.get("rejected_by_me_page", 1)
if ajax_section == "rejected_by_them":
rejected_by_them_page = request.GET.get("page", 1)
else:
rejected_by_them_page = request.GET.get("rejected_by_them_page", 1)
context["dashboard_offers_paginated"] = self.get_dashboard_offers_paginated(
offers_page
)
context["trade_acceptances_waiting_paginated"] = (
self.get_trade_acceptances_waiting_paginated(waiting_page)
)
context["other_party_trade_acceptances_paginated"] = (
self.get_other_party_trade_acceptances_paginated(other_page)
)
context["closed_offers_paginated"] = self.get_closed_offers_paginated(
closed_offers_page
)
context["closed_acceptances_paginated"] = self.get_closed_acceptances_paginated(
closed_acceptances_page
)
context["rejected_by_me_paginated"] = self.get_rejected_by_me_paginated(
rejected_by_me_page
)
context["rejected_by_them_paginated"] = self.get_rejected_by_them_paginated(
rejected_by_them_page
)
context["settings_form"] = UserSettingsForm(instance=request.user)
context["active_tab"] = request.GET.get("tab", "dash")
return context
# Handle AJAX requests to return only the trade offer list fragment
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
ajax_section = request.GET.get("ajax_section")
if request.headers.get("X-Requested-With") == "XMLHttpRequest" and ajax_section:
if ajax_section == "dashboard_offers":
fragment_context = context.get("dashboard_offers_paginated", {})
elif ajax_section == "waiting_acceptances":
fragment_context = context.get(
"trade_acceptances_waiting_paginated", {}
)
elif ajax_section == "other_party_acceptances":
fragment_context = context.get(
"other_party_trade_acceptances_paginated", {}
)
elif ajax_section == "closed_offers":
fragment_context = context.get("closed_offers_paginated", {})
elif ajax_section == "closed_acceptances":
fragment_context = context.get("closed_acceptances_paginated", {})
elif ajax_section == "rejected_by_me":
fragment_context = context.get("rejected_by_me_paginated", {})
elif ajax_section == "rejected_by_them":
fragment_context = context.get("rejected_by_them_paginated", {})
else:
fragment_context = {}
if fragment_context:
return render(
request,
"trades/_trade_offer_list.html",
{
"offers": fragment_context.get("object_list", []),
"page_obj": fragment_context.get("page_obj"),
},
)
return super().get(request, *args, **kwargs)