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 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, TradeAcceptanceUpdateForm, TradeOfferCreateForm) from cards.models import Card 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() # Standardize parameter names: use "have_cards" and "want_cards" initial["have_cards"] = self.request.GET.getlist("have_cards") initial["want_cards"] = self.request.GET.getlist("want_cards") # If the user has only one friend code, set it as the default. 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) 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): # Double-check that the chosen friend code is owned by the current user. 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 TradeOfferListView(LoginRequiredMixin, ListView): model = TradeOffer # Fallback model; our context data will hold separate querysets. template_name = "trades/trade_offer_list.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) request = self.request # Determine if the user wants to see completed (closed) items. show_completed = request.GET.get("show_completed", "false").lower() == "true" context["show_completed"] = show_completed # Get all friend codes for the current user. friend_codes = request.user.friend_codes.all() friend_code_param = 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 = request.user.default_friend_code or friend_codes.first() else: selected_friend_code = 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.") context["selected_friend_code"] = selected_friend_code context["friend_codes"] = friend_codes # ----- My Trade Offers ----- if show_completed: my_trade_offers = TradeOffer.objects.filter(initiated_by=selected_friend_code).order_by("-updated_at") my_trade_offers = [offer for offer in my_trade_offers if offer.is_closed] else: my_trade_offers = TradeOffer.objects.filter(initiated_by=selected_friend_code).order_by("-updated_at") my_trade_offers = [offer for offer in my_trade_offers if not offer.is_closed] # ----- Trade Acceptances involving the user ----- terminal_states = [ TradeAcceptance.AcceptanceState.COMPLETED, TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR, TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR, ] involved_acceptances = TradeAcceptance.objects.filter( Q(trade_offer__initiated_by=selected_friend_code) | Q(accepted_by=selected_friend_code) ).order_by("-updated_at") if show_completed: involved_acceptances = involved_acceptances.filter(state__in=terminal_states) else: involved_acceptances = involved_acceptances.exclude(state__in=terminal_states) # ----- Split Acceptances into "Waiting for Your Response" and "Other" ----- 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=TradeAcceptance.AcceptanceState.SENT) ) other_trade_acceptances = involved_acceptances.exclude(pk__in=waiting_acceptances.values("pk")) # ----- Paginate Each Section Separately ----- offers_page = request.GET.get("offers_page") waiting_page = request.GET.get("waiting_page") other_page = request.GET.get("other_page") offers_paginator = Paginator(my_trade_offers, 10) waiting_paginator = Paginator(waiting_acceptances, 10) other_paginator = Paginator(other_trade_acceptances, 10) context["my_trade_offers_paginated"] = offers_paginator.get_page(offers_page) context["trade_acceptances_waiting_paginated"] = waiting_paginator.get_page(waiting_page) context["other_trade_acceptances_paginated"] = other_paginator.get_page(other_page) return context 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 not in request.user.friend_codes.all(): 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.COMPLETED, 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.COMPLETED, 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: # There are terminal acceptances: mark the offer as closed. trade_offer.manually_closed = True trade_offer.save(update_fields=["manually_closed"]) messages.success(request, "Trade offer has been marked as closed.") return HttpResponseRedirect(self.get_success_url()) else: # No acceptances: proceed with deletion. messages.success(request, "Trade offer has been deleted.") return super().delete(request, *args, **kwargs) class TradeOfferSearchView(LoginRequiredMixin, ListView): model = TradeOffer template_name = "trades/trade_offer_search.html" context_object_name = "trade_offers" def get_queryset(self): qs = super().get_queryset().filter(state=TradeOffer.State.INITIATED).prefetch_related("have_cards", "want_cards").select_related("initiated_by", "accepted_by") offered_card = self.request.GET.get("offered_card", "").strip() wanted_cards = self.request.GET.getlist("wanted_cards") if not offered_card and not wanted_cards: return qs.none() if offered_card: try: offered_card_id = int(offered_card) except ValueError: qs = qs.none() else: qs = qs.filter(have_cards__id=offered_card_id) if wanted_cards: valid_wanted_cards = [] for card_str in wanted_cards: try: valid_wanted_cards.append(int(card_str)) except ValueError: qs = qs.none() break if valid_wanted_cards: qs = qs.filter(want_cards__id__in=valid_wanted_cards) return qs def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["offered_card"] = self.request.GET.get("offered_card", "") context["wanted_cards"] = self.request.GET.getlist("wanted_cards") context["available_cards"] = Card.objects.order_by("name", "rarity__pk").select_related("rarity", "cardset") return context 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" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) trade_offer = self.get_object() active_states = [ TradeAcceptance.AcceptanceState.ACCEPTED, TradeAcceptance.AcceptanceState.SENT, TradeAcceptance.AcceptanceState.RECEIVED, TradeAcceptance.AcceptanceState.COMPLETED, ] context["acceptances"] = trade_offer.acceptances.filter(state__in=active_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 # 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 ) 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 = get_object_or_404(TradeOffer, pk=kwargs.get("offer_pk")) # Disallow acceptance if the current user is the offer initiator or if the offer is closed. if self.trade_offer.initiated_by in request.user.friend_codes.all() or self.trade_offer.is_closed: raise PermissionDenied("You cannot accept this trade offer.") return super().dispatch(request, *args, **kwargs) 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 form_valid(self, form): form.instance.trade_offer = self.trade_offer self.object = form.save() return HttpResponseRedirect(self.get_success_url()) 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 = TradeAcceptanceUpdateForm template_name = "trades/trade_acceptance_update.html" def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs["friend_codes"] = self.request.user.friend_codes.all() return kwargs def form_valid(self, form): try: # Use the model's update_state logic. form.instance.update_state(form.cleaned_data["state"]) 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})