224 lines
9.5 KiB
Python
224 lines
9.5 KiB
Python
from django.views.generic import TemplateView, DeleteView, CreateView, ListView, DetailView, FormView
|
|
from django.urls import reverse_lazy
|
|
from django.http import HttpResponseRedirect
|
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
from django.shortcuts import get_object_or_404
|
|
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 .models import TradeOffer
|
|
from .forms import TradeOfferUpdateForm, TradeOfferAcceptForm
|
|
from cards.models import Card
|
|
|
|
class TradeOfferCreateView(LoginRequiredMixin, CreateView):
|
|
model = TradeOffer
|
|
template_name = "trades/trade_offer_create.html"
|
|
success_url = reverse_lazy("trade_offer_list")
|
|
fields = ["want_cards", "have_cards", "initiated_by"]
|
|
|
|
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)
|
|
return HttpResponseRedirect(self.get_success_url())
|
|
|
|
class TradeOfferListView(LoginRequiredMixin, ListView):
|
|
model = TradeOffer
|
|
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")
|
|
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"]
|
|
|
|
now = timezone.now()
|
|
seven_days_ago = now - timezone.timedelta(days=7)
|
|
|
|
if show_completed:
|
|
qs = qs.filter(Q(state=TradeOffer.State.RECEIVED))
|
|
else:
|
|
qs = qs.filter(updated_at__gte=seven_days_ago).exclude(state=TradeOffer.State.RECEIVED)
|
|
|
|
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")
|
|
|
|
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"]
|
|
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"
|
|
|
|
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
|
|
|