from django.views.generic import TemplateView, DeleteView, CreateView, ListView, DetailView, UpdateView, FormView from django.urls import reverse_lazy from django.http import HttpResponseRedirect, JsonResponse from django.contrib.auth.mixins import LoginRequiredMixin from django.shortcuts import get_object_or_404, render from django.core.exceptions import PermissionDenied, ValidationError from django.views.generic.edit import FormMixin from django.utils import timezone from django.db.models import Q, Case, When, Value, BooleanField, Prefetch, F from django.utils.decorators import method_decorator from django.views.decorators.http import require_http_methods from django.core.paginator import Paginator from django.contrib import messages from .models import TradeOffer, TradeAcceptance from .forms import (TradeOfferAcceptForm, TradeAcceptanceCreateForm, TradeOfferCreateForm, TradeAcceptanceTransitionForm) from cards.models import Card #from silk.profiling.profiler import silk_profile class TradeOfferCreateView(LoginRequiredMixin, CreateView): model = TradeOffer form_class = TradeOfferCreateForm template_name = "trades/trade_offer_create.html" success_url = reverse_lazy("trade_offer_list") def get_form(self, form_class=None): form = super().get_form(form_class) # Restrict the 'initiated_by' choices to friend codes owned by the logged-in user. form.fields["initiated_by"].queryset = self.request.user.friend_codes.all() return form def get_initial(self): initial = super().get_initial() initial["have_cards"] = self.request.GET.getlist("have_cards") initial["want_cards"] = self.request.GET.getlist("want_cards") if self.request.user.friend_codes.count() == 1: initial["initiated_by"] = self.request.user.friend_codes.first().pk return initial def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) from cards.models import Card # Ensure available_cards is a proper QuerySet context["cards"] = Card.objects.all().order_by("name", "rarity_level") friend_codes = self.request.user.friend_codes.all() if "initiated_by" in self.request.GET: try: selected_friend_code = friend_codes.get(pk=self.request.GET.get("initiated_by")) 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() context["friend_codes"] = friend_codes context["selected_friend_code"] = selected_friend_code return context def form_valid(self, form): friend_codes = self.request.user.friend_codes.all() if form.cleaned_data.get("initiated_by") not in friend_codes: raise PermissionDenied("You cannot initiate trade offers for friend codes that do not belong to you.") self.object = form.save() return HttpResponseRedirect(self.get_success_url()) class TradeOfferAllListView(ListView): model = TradeOffer template_name = "trades/trade_offer_all_list.html" #@silk_profile(name="Trade Offer All List- Get Context Data") def get_context_data(self, *, object_list=None, **kwargs): context = super().get_context_data(**kwargs) request = self.request show_closed = request.GET.get("show_closed", "false").lower() == "true" context["show_closed"] = show_closed # Build the queryset with our related objects. queryset = TradeOffer.objects if show_closed: queryset = queryset.filter(is_closed=True) else: queryset = queryset.filter(is_closed=False) # On initial load, use the 'offers_page' parameter. offers_page = request.GET.get("offers_page") offers_paginator = Paginator(queryset, 10) context["all_trade_offers_paginated"] = offers_paginator.get_page(offers_page) return context #@silk_profile(name="Trade Offer All List- Render to Response") def render_to_response(self, context, **response_kwargs): # For AJAX requests, return only the paginated fragment. if self.request.headers.get("X-Requested-With") == "XMLHttpRequest": page = self.request.GET.get("page") show_closed = self.request.GET.get("show_closed", "false").lower() == "true" queryset = TradeOffer.objects if show_closed: queryset = queryset.filter(is_closed=True) else: queryset = queryset.filter(is_closed=False) paginated_offers = Paginator(queryset, 10).get_page(page) return render( self.request, "trades/_trade_offer_list_paginated.html", {"offers": paginated_offers} ) return super().render_to_response(context, **response_kwargs) class TradeOfferMyListView(LoginRequiredMixin, ListView): model = TradeOffer # Fallback model; our context data holds separate filtered querysets. template_name = "trades/trade_offer_my_list.html" def dispatch(self, request, *args, **kwargs): if request.user.is_authenticated and not request.user.friend_codes.exists(): raise PermissionDenied("No friend codes available for your account.") return super().dispatch(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_show_closed(self): return self.request.GET.get("show_closed", "false").lower() == "true" def get_my_trade_offers_paginated(self, page_param): selected_friend_code = self.get_selected_friend_code() queryset = self.get_queryset().filter(initiated_by=selected_friend_code) if self.get_show_closed(): queryset = queryset.filter(is_closed=True) else: queryset = queryset.filter(is_closed=False) return Paginator(queryset, 10).get_page(page_param) 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_acceptances_qs = TradeAcceptance.objects.filter( Q(trade_offer__initiated_by=selected_friend_code) | Q(accepted_by=selected_friend_code) ).order_by("-updated_at") if self.get_show_closed(): return involved_acceptances_qs.filter(state__in=terminal_states) return involved_acceptances_qs.exclude(state__in=terminal_states) def get_trade_acceptances_waiting_paginated(self, page_param): selected_friend_code = self.get_selected_friend_code() involved_acceptances = self.get_involved_acceptances(selected_friend_code) waiting_acceptances = involved_acceptances.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]) ) return Paginator(waiting_acceptances, 10).get_page(page_param) def get_other_party_trade_acceptances_paginated(self, page_param): selected_friend_code = self.get_selected_friend_code() involved_acceptances = self.get_involved_acceptances(selected_friend_code) waiting_acceptances = involved_acceptances.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]) ) other_acceptances = involved_acceptances.exclude(pk__in=waiting_acceptances.values("pk")) return Paginator(other_acceptances, 10).get_page(page_param) #@silk_profile(name="Trade Offer My List- Get Context Data") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) request = self.request show_closed = self.get_show_closed() context["show_closed"] = show_closed selected_friend_code = self.get_selected_friend_code() context["selected_friend_code"] = selected_friend_code context["friend_codes"] = request.user.friend_codes.all() # Use request params for initial full page load (could be None) offers_page = request.GET.get("offers_page") waiting_page = request.GET.get("waiting_page") other_page = request.GET.get("other_page") context["my_trade_offers_paginated"] = self.get_my_trade_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) return context #@silk_profile(name="Trade Offer My List- Render to Response") def render_to_response(self, context, **response_kwargs): # For AJAX requests, return only the paginated fragment. if self.request.headers.get("X-Requested-With") == "XMLHttpRequest": ajax_section = self.request.GET.get("ajax_section") page = self.request.GET.get("page") if ajax_section == "my_trade_offers": offers = self.get_my_trade_offers_paginated(page) elif ajax_section == "waiting_acceptances": offers = self.get_trade_acceptances_waiting_paginated(page) elif ajax_section == "other_party_acceptances": offers = self.get_other_party_trade_acceptances_paginated(page) else: # Fallback to my trade offers. offers = self.get_my_trade_offers_paginated(page) return render(self.request, "trades/_trade_offer_list_paginated.html", {"offers": offers}) return super().render_to_response(context, **response_kwargs) class TradeOfferDeleteView(LoginRequiredMixin, DeleteView): model = TradeOffer success_url = reverse_lazy("trade_offer_list") template_name = "trades/trade_offer_delete.html" def dispatch(self, request, *args, **kwargs): trade_offer = self.get_object() if trade_offer.initiated_by_id not in request.user.friend_codes.values_list("id", flat=True): raise PermissionDenied("You are not authorized to delete or close this trade offer.") return super().dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) trade_offer = self.get_object() 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, ] active_acceptances = trade_offer.acceptances.exclude(state__in=terminal_states) if trade_offer.acceptances.count() == 0: context["action"] = "delete" elif trade_offer.acceptances.count() > 0 and not active_acceptances.exists(): context["action"] = "close" else: context["action"] = None return context def post(self, request, *args, **kwargs): trade_offer = self.get_object() 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, ] active_acceptances = trade_offer.acceptances.exclude(state__in=terminal_states) if active_acceptances.exists(): messages.error(request, "Cannot delete or close this trade offer because there are active acceptances.") context = self.get_context_data(object=trade_offer) return self.render_to_response(context) else: if trade_offer.acceptances.count() > 0: trade_offer.is_closed = True trade_offer.save(update_fields=["is_closed"]) messages.success(request, "Trade offer has been marked as closed.") return HttpResponseRedirect(self.get_success_url()) else: messages.success(request, "Trade offer has been deleted.") return super().delete(request, *args, **kwargs) class TradeOfferSearchView(ListView): """ Reworked trade offer search view using POST. This view allows users to search active trade offers based on the cards they have and/or want. The POST parameters (offered_cards and wanted_cards) are expected to be in the format 'card_id:quantity'. If both types of selections are provided, the resultant queryset must satisfy both conditions. Offers initiated by any of the user's friend codes are excluded. When the request is AJAX (via X-Requested-With header), only the search results fragment (_search_results.html) is rendered. On GET (initial page load), the search results queryset is empty. """ model = TradeOffer context_object_name = "search_results" template_name = "trades/trade_offer_search.html" paginate_by = 10 http_method_names = ["get", "post"] def parse_selections(self, selection_list): """ Parse a list of selections (each formatted as 'card_id:quantity') into a list of tuples. Defaults the quantity to 1 if missing. """ results = [] for item in selection_list: parts = item.split(":") try: card_id = int(parts[0]) except ValueError: continue # Skip invalid values. qty = 1 if len(parts) > 1: try: qty = int(parts[1]) except ValueError: qty = 1 results.append((card_id, qty)) return results #@silk_profile(name="Trade Offer Search- Get Queryset") def get_queryset(self): from django.db.models import F # For a GET request (initial load), return an empty queryset. if self.request.method == "GET": return TradeOffer.objects.none() # Parse the POST data for offered and wanted selections. offered_selections = self.parse_selections(self.request.POST.getlist("offered_cards")) wanted_selections = self.parse_selections(self.request.POST.getlist("wanted_cards")) # If no selections are provided, return an empty queryset. if not offered_selections and not wanted_selections: return TradeOffer.objects.none() qs = TradeOffer.objects.filter( is_closed=False, ).exclude(initiated_by__in=self.request.user.friend_codes.all()) # Chain filters for offered selections (i.e. the user "has" cards). if offered_selections: for card_id, qty in offered_selections: qs = qs.filter( trade_offer_want_cards__card_id=card_id, trade_offer_want_cards__quantity__gte=qty, ) # Chain filters for wanted selections (i.e. the user "wants" cards). if wanted_selections: for card_id, qty in wanted_selections: qs = qs.filter( trade_offer_have_cards__card_id=card_id, trade_offer_have_cards__quantity__gte=qty, ) return qs.distinct() #@silk_profile(name="Trade Offer Search- Post") def post(self, request, *args, **kwargs): # For POST, simply process the search through get(). return self.get(request, *args, **kwargs) #@silk_profile(name="Trade Offer Search- Get Context Data") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) from cards.models import Card # Populate available_cards to re-populate the multiselects. context["cards"] = Card.objects.all().order_by("name") if self.request.method == "POST": context["offered_cards"] = self.request.POST.getlist("offered_cards") context["wanted_cards"] = self.request.POST.getlist("wanted_cards") else: context["offered_cards"] = [] context["wanted_cards"] = [] return context #@silk_profile(name="Trade Offer Search- Render to Response") def render_to_response(self, context, **response_kwargs): """ Render the AJAX fragment if the request is AJAX; otherwise, render the complete page. """ if self.request.headers.get("X-Requested-With") == "XMLHttpRequest": from django.shortcuts import render return render(self.request, "trades/_search_results.html", context) else: return super().render_to_response(context, **response_kwargs) class TradeOfferDetailView(LoginRequiredMixin, DetailView): """ Displays the details of a TradeOffer along with its active acceptances. If the offer is still open and the current user is not its initiator, an acceptance form is provided to create a new acceptance. """ model = TradeOffer template_name = "trades/trade_offer_detail.html" #@silk_profile(name="Trade Offer Detail- Get Context Data") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) trade_offer = self.get_object() # Define terminal (closed) acceptance states based on our new system: 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, ] # For example, if you want to separate active from terminal acceptances: context["acceptances"] = trade_offer.acceptances.all() # Option 1: Filter active acceptances using the queryset lookup. context["active_acceptances"] = trade_offer.acceptances.exclude(state__in=terminal_states) user_friend_codes = self.request.user.friend_codes.all() # Add context flag and deletion URL if the current user is the initiator if trade_offer.initiated_by in user_friend_codes: context["is_initiator"] = True context["delete_close_url"] = reverse_lazy("trade_offer_delete", kwargs={"pk": trade_offer.pk}) else: context["is_initiator"] = False # Determine the user's default friend code (or fallback as needed). default_friend_code = self.request.user.default_friend_code or user_friend_codes.first() # If the current user is not the initiator and the offer is open, allow a new acceptance. if trade_offer.initiated_by not in user_friend_codes and not trade_offer.is_closed: context["acceptance_form"] = TradeAcceptanceCreateForm( trade_offer=trade_offer, friend_codes=user_friend_codes, default_friend_code=default_friend_code ) return context class TradeAcceptanceCreateView(LoginRequiredMixin, CreateView): """ View to create a new TradeAcceptance. The URL should provide 'offer_pk' so that the proper TradeOffer can be identified. """ model = TradeAcceptance form_class = TradeAcceptanceCreateForm template_name = "trades/trade_acceptance_create.html" def dispatch(self, request, *args, **kwargs): self.trade_offer = self.get_trade_offer() if self.trade_offer.initiated_by_id in request.user.friend_codes.values_list("id", flat=True) or self.trade_offer.is_closed: raise PermissionDenied("You cannot accept this trade offer.") if not request.user.friend_codes.exists(): raise PermissionDenied("No friend codes available for your account.") return super().dispatch(request, *args, **kwargs) def get_trade_offer(self): return ( TradeOffer.objects.get(pk=self.kwargs['offer_pk']) ) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['trade_offer'] = self.trade_offer kwargs['friend_codes'] = self.request.user.friend_codes.all() return kwargs def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["trade_offer"] = self.trade_offer return context def form_valid(self, form): form.instance.trade_offer = self.trade_offer self.object = form.save() return HttpResponseRedirect(self.get_success_url()) def form_invalid(self, form): """ If the form submission includes a 'next' URL (sent as a hidden field from the detail page), render the trade offer detail template for a better UX. Otherwise, fall back to the default CreateView behavior. """ next_url = self.request.POST.get("next") if next_url: friend_codes = self.request.user.friend_codes.all() is_initiator = self.trade_offer.initiated_by in friend_codes context = { "object": self.trade_offer, "trade_offer": self.trade_offer, "acceptance_form": form, "friend_codes": friend_codes, "is_initiator": is_initiator, "delete_close_url": reverse_lazy("trade_offer_delete", kwargs={"pk": self.trade_offer.pk}) if is_initiator else None, } # Render the detail page with the form errors return render(self.request, "trades/trade_offer_detail.html", context) return super().form_invalid(form) def get_success_url(self): return reverse_lazy("trade_offer_detail", kwargs={"pk": self.trade_offer.pk}) class TradeAcceptanceUpdateView(LoginRequiredMixin, UpdateView): """ View to update the state of an existing TradeAcceptance. The allowed state transitions are provided via the form. """ model = TradeAcceptance form_class = TradeAcceptanceTransitionForm template_name = "trades/trade_acceptance_update.html" def dispatch(self, request, *args, **kwargs): self.object = self.get_object() if not request.user.friend_codes.exists(): raise PermissionDenied("No friend codes available for your account.") friend_codes = request.user.friend_codes.values_list("id", flat=True) if self.object.accepted_by_id not in friend_codes and self.object.trade_offer.initiated_by_id not in friend_codes: raise PermissionDenied("You are not authorized to update this acceptance.") return super().dispatch(request, *args, **kwargs) def get_form_kwargs(self): kwargs = super().get_form_kwargs() # Pass the current instance to the form so it can set proper allowed transitions. kwargs["instance"] = self.object kwargs["user"] = self.request.user return kwargs def form_valid(self, form): new_state = form.cleaned_data["state"] #match the new state to the TradeAcceptance.AcceptanceState enum if new_state not in TradeAcceptance.AcceptanceState: form.add_error("state", "Invalid state transition.") return self.form_invalid(form) try: # pass the new state and the current user to the update_state method form.instance.update_state(new_state, self.request.user) except ValueError as e: form.add_error("state", str(e)) return self.form_invalid(form) return HttpResponseRedirect(self.get_success_url()) def get_success_url(self): return reverse_lazy("trade_offer_detail", kwargs={"pk": self.object.trade_offer.pk})