progress on conversion to tailwind

This commit is contained in:
badblocks 2025-03-06 21:28:36 -08:00
parent 6a872124c6
commit 6e2843c60e
110 changed files with 4997 additions and 1691 deletions

View file

@ -1,187 +1,195 @@
from django.views.generic import TemplateView, DeleteView, CreateView, ListView, DetailView, FormView
from django.views.generic import TemplateView, DeleteView, CreateView, ListView, DetailView, UpdateView, FormView
from django.urls import reverse_lazy
from django.http import HttpResponseRedirect
from django.http import HttpResponseRedirect, JsonResponse
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import get_object_or_404
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
from .forms import TradeOfferUpdateForm, TradeOfferAcceptForm
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")
fields = ["want_cards", "have_cards", "initiated_by"]
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):
# Save the object without committing m2m fields immediately.
self.object = form.save(commit=False)
self.object.save()
try:
# This call will trigger the m2m signals and may raise a ValidationError.
form.save_m2m()
except ValidationError as e:
# Attach the error message to the "have_cards" field (or as a non-field error)
form.add_error("have_cards", e.messages[0])
return self.form_invalid(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
model = TradeOffer # Fallback model; our context data will hold separate querysets.
template_name = "trades/trade_offer_list.html"
def get_queryset(self):
qs = super().get_queryset().prefetch_related("have_cards", "want_cards").select_related("initiated_by", "accepted_by")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
request = self.request
show_completed = request.GET.get("show_completed", "").lower() in ["true", "1"]
my_trades = request.GET.get("my_trades", "").lower() in ["true", "1"]
# 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
now = timezone.now()
seven_days_ago = now - timezone.timedelta(days=7)
# 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:
qs = qs.filter(Q(state=TradeOffer.State.RECEIVED))
involved_acceptances = involved_acceptances.filter(state__in=terminal_states)
else:
qs = qs.filter(updated_at__gte=seven_days_ago).exclude(state=TradeOffer.State.RECEIVED)
involved_acceptances = involved_acceptances.exclude(state__in=terminal_states)
if my_trades:
friend_codes = self.request.user.friend_codes.all()
qs = qs.filter(Q(initiated_by__in=friend_codes) | Q(accepted_by__in=friend_codes))
return qs.order_by("-updated_at")
# ----- 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"))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["show_completed"] = self.request.GET.get("show_completed", "").lower() in ["true", "1"]
context["my_trades"] = self.request.GET.get("my_trades", "").lower() in ["true", "1"]
# ----- 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 TradeOfferUpdateView(LoginRequiredMixin, FormMixin, DetailView):
"""
Merged view that displays trade offer details and renders a form used for either:
- Accepting an offer (if in INITIATED state and not initiated by the current user), or
- Performing an allowed state transition via the update form.
"""
model = TradeOffer
template_name = "trades/trade_offer_update.html"
success_url = reverse_lazy("trade_offer_list")
def get_user_friend_codes(self):
return self.request.user.friend_codes.all()
def get_form_class(self):
trade_offer = self.get_object()
user_friend_codes = self.get_user_friend_codes()
if trade_offer.state == trade_offer.State.INITIATED and trade_offer.initiated_by not in user_friend_codes:
return TradeOfferAcceptForm
return TradeOfferUpdateForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["friend_codes"] = self.get_user_friend_codes()
if self.get_form_class() == TradeOfferUpdateForm:
kwargs["instance"] = self.get_object()
return kwargs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
form_class = self.get_form_class()
if "form" not in context:
context["form"] = self.get_form(form_class)
context["action"] = "accept" if form_class == TradeOfferAcceptForm else "update"
trade_offer = self.object
user_friend_codes = self.get_user_friend_codes()
seven_days_ago = timezone.now() - timezone.timedelta(days=7)
can_delete = False
if trade_offer.initiated_by in user_friend_codes:
if trade_offer.state == trade_offer.State.INITIATED:
can_delete = True
elif trade_offer.state == trade_offer.State.SENT and trade_offer.updated_at < seven_days_ago:
can_delete = True
elif trade_offer.accepted_by in user_friend_codes:
if trade_offer.state in [trade_offer.State.ACCEPTED, trade_offer.State.RECEIVED]:
if trade_offer.updated_at < seven_days_ago:
can_delete = True
context["can_delete"] = can_delete
return context
def post(self, request, *args, **kwargs):
self.object = self.get_object()
user_friend_codes = self.get_user_friend_codes()
form_class = self.get_form_class()
form = self.get_form(form_class)
if form_class == TradeOfferAcceptForm:
if not (self.object.state == self.object.State.INITIATED and
self.object.initiated_by not in user_friend_codes):
raise PermissionDenied("You are not allowed to accept this trade offer.")
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
"""
Save the instance and its many-to-many fields in a try/except block to catch
ValidationError raised by the m2m_changed signal (or a custom validation method).
"""
trade_offer = self.get_object()
# For example, if you want to perform a pre-save validation of card rarities,
# you might call a model method (that you define) like:
try:
trade_offer.validate_card_rarities()
except ValueError as e:
form.add_error("have_cards", str(e))
return self.form_invalid(form)
# For the m2m part, manually save to catch errors from the signal:
self.object = form.save(commit=False)
# Process state change or friend code acceptance:
if isinstance(form, TradeOfferAcceptForm):
chosen_friend_code = form.cleaned_data["friend_code"]
trade_offer.accepted_by = chosen_friend_code
try:
trade_offer.update_state(TradeOffer.State.ACCEPTED)
except ValueError as e:
# Attach as non-field error (or on a specific field if you prefer)
form.add_error(None, str(e))
return self.form_invalid(form)
else:
new_state = form.cleaned_data["state"]
try:
trade_offer.update_state(new_state)
except ValueError as e:
form.add_error("state", str(e))
return self.form_invalid(form)
try:
# Save instance and its m2m fields; any ValidationError raised here (e.g.,
# from the m2m_changed signals) will be caught.
self.object.save() # Save the TradeOffer instance.
form.save_m2m() # This call triggers the m2m_changed signals.
except ValidationError as e:
# Here we attach the signal error (from card rarities) to the form so that
# the user can see it. You can attach it to a specific field or as a non-field error.
form.add_error("have_cards", e.messages[0])
return self.form_invalid(form)
return HttpResponseRedirect(self.get_success_url())
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"
@ -222,3 +230,94 @@ class TradeOfferSearchView(LoginRequiredMixin, ListView):
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})