Move profile and settings into the new unified dashboard, showing user info in one place
This commit is contained in:
parent
2d826734a0
commit
7edefe23c3
37 changed files with 726 additions and 500 deletions
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 5.1.2 on 2025-03-29 21:23
|
||||
# Generated by Django 5.1.2 on 2025-03-31 22:48
|
||||
|
||||
import accounts.models
|
||||
import django.contrib.auth.models
|
||||
|
|
@ -33,6 +33,7 @@ class Migration(migrations.Migration):
|
|||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('show_friend_code_on_link_previews', models.BooleanField(default=False, help_text='This will primarily affect share link previews on X, Discord, etc.', verbose_name='Show Friend Code on Link Previews')),
|
||||
('reputation_score', models.PositiveIntegerField(default=0)),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ class CustomUser(AbstractUser):
|
|||
verbose_name="Show Friend Code on Link Previews",
|
||||
help_text="This will primarily affect share link previews on X, Discord, etc."
|
||||
)
|
||||
reputation_score = models.PositiveIntegerField(default=0)
|
||||
|
||||
def __str__(self):
|
||||
return self.email
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@ from .views import (
|
|||
DeleteFriendCodeView,
|
||||
ChangeDefaultFriendCodeView,
|
||||
EditFriendCodeView,
|
||||
SettingsView,
|
||||
ProfileView,
|
||||
DashboardView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
|
|
@ -16,6 +15,5 @@ urlpatterns = [
|
|||
path("friend-codes/edit/<int:pk>/", EditFriendCodeView.as_view(), name="edit_friend_code"),
|
||||
path("friend-codes/delete/<int:pk>/", DeleteFriendCodeView.as_view(), name="delete_friend_code"),
|
||||
path("friend-codes/default/<int:pk>/", ChangeDefaultFriendCodeView.as_view(), name="change_default_friend_code"),
|
||||
path("settings/", SettingsView.as_view(), name="settings"),
|
||||
path("profile/", ProfileView.as_view(), name="profile"),
|
||||
path("dashboard/", DashboardView.as_view(), name="dashboard"),
|
||||
]
|
||||
|
|
@ -7,6 +7,8 @@ from accounts.models import FriendCode, CustomUser
|
|||
from accounts.forms import FriendCodeForm, UserSettingsForm
|
||||
from django.db.models import Case, When, Value, BooleanField
|
||||
from trades.models import TradeOffer, TradeAcceptance
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from trades.mixins import FriendCodeRequiredMixin
|
||||
|
||||
class ListFriendCodesView(LoginRequiredMixin, ListView):
|
||||
"""
|
||||
|
|
@ -127,30 +129,6 @@ class ChangeDefaultFriendCodeView(LoginRequiredMixin, View):
|
|||
messages.success(request, "Default friend code updated successfully.")
|
||||
return redirect("list_friend_codes")
|
||||
|
||||
# Updated SettingsView to update the new user setting.
|
||||
class SettingsView(LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
Display account navigation links and allow the user to update their friend code
|
||||
visibility setting.
|
||||
"""
|
||||
model = CustomUser
|
||||
form_class = UserSettingsForm
|
||||
template_name = "account/settings.html"
|
||||
success_url = reverse_lazy("settings")
|
||||
|
||||
def get_object(self):
|
||||
return self.request.user
|
||||
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, "Settings updated successfully.")
|
||||
return super().form_valid(form)
|
||||
|
||||
class ProfileView(LoginRequiredMixin, TemplateView):
|
||||
"""
|
||||
Display the user's profile.
|
||||
"""
|
||||
template_name = "account/profile.html"
|
||||
|
||||
class EditFriendCodeView(LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
Edit the in-game name for a friend code.
|
||||
|
|
@ -171,3 +149,146 @@ class EditFriendCodeView(LoginRequiredMixin, UpdateView):
|
|||
def form_valid(self, form):
|
||||
messages.success(self.request, "Friend code updated successfully.")
|
||||
return super().form_valid(form)
|
||||
|
||||
class DashboardView(LoginRequiredMixin, FriendCodeRequiredMixin, TemplateView):
|
||||
template_name = "account/dashboard.html"
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if 'update_settings' in request.POST:
|
||||
from 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):
|
||||
from django.core.paginator import Paginator
|
||||
selected_friend_code = self.get_selected_friend_code()
|
||||
queryset = TradeOffer.objects.filter(initiated_by=selected_friend_code, is_closed=False)
|
||||
return Paginator(queryset, 10).get_page(page_param)
|
||||
|
||||
def get_involved_acceptances(self, selected_friend_code):
|
||||
from django.db.models import Q
|
||||
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)
|
||||
).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])
|
||||
)
|
||||
from django.core.paginator import Paginator
|
||||
return Paginator(waiting, 10).get_page(page_param)
|
||||
|
||||
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"))
|
||||
from django.core.paginator import Paginator
|
||||
return Paginator(others, 10).get_page(page_param)
|
||||
|
||||
def get_closed_offers_paginated(self, page_param):
|
||||
from django.core.paginator import Paginator
|
||||
selected_friend_code = self.get_selected_friend_code()
|
||||
queryset = TradeOffer.objects.filter(initiated_by=selected_friend_code, is_closed=True)
|
||||
return Paginator(queryset, 10).get_page(page_param)
|
||||
|
||||
def get_closed_acceptances_paginated(self, page_param):
|
||||
from django.db.models import Q
|
||||
from django.core.paginator import Paginator
|
||||
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
|
||||
).order_by("-updated_at")
|
||||
return Paginator(acceptance_qs, 10).get_page(page_param)
|
||||
|
||||
def get_rejected_by_me_paginated(self, page_param):
|
||||
from django.db.models import Q
|
||||
from django.core.paginator import Paginator
|
||||
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)
|
||||
).order_by("-updated_at")
|
||||
return Paginator(rejection, 10).get_page(page_param)
|
||||
|
||||
def get_rejected_by_them_paginated(self, page_param):
|
||||
from django.db.models import Q
|
||||
from django.core.paginator import Paginator
|
||||
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)
|
||||
).order_by("-updated_at")
|
||||
return Paginator(rejection, 10).get_page(page_param)
|
||||
|
||||
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
|
||||
context["friend_codes"] = request.user.friend_codes.all()
|
||||
offers_page = request.GET.get("offers_page", 1)
|
||||
waiting_page = request.GET.get("waiting_page", 1)
|
||||
other_page = request.GET.get("other_page", 1)
|
||||
closed_offers_page = request.GET.get("closed_offers_page", 1)
|
||||
closed_acceptances_page = request.GET.get("closed_acceptances_page", 1)
|
||||
rejected_by_me_page = request.GET.get("rejected_by_me_page", 1)
|
||||
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)
|
||||
from accounts.forms import UserSettingsForm
|
||||
context["settings_form"] = UserSettingsForm(instance=request.user)
|
||||
context["active_tab"] = request.GET.get("tab", "dash")
|
||||
return context
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 5.1.2 on 2025-03-29 21:23
|
||||
# Generated by Django 5.1.2 on 2025-03-31 22:48
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.inclusion_tag("templatetags/pagination_controls.html", takes_context=True)
|
||||
def render_pagination(context, page_obj):
|
||||
"""
|
||||
Renders pagination controls given a page_obj.
|
||||
The controls use values like page_obj.number, page_obj.has_previous, etc.
|
||||
"""
|
||||
return {"page_obj": page_obj}
|
||||
|
|
@ -148,13 +148,13 @@ class CardListView(ReusablePaginationMixin, ListView):
|
|||
# For non-grouped mode, transform the built-in paginator page
|
||||
if "page_obj" in context:
|
||||
page = context["page_obj"]
|
||||
# Create a unified pagination context dict
|
||||
# Create a unified pagination context dict with updated keys
|
||||
custom_page_obj = {
|
||||
"number": page.number,
|
||||
"has_previous": page.has_previous(),
|
||||
"has_next": page.has_next(),
|
||||
"previous_page": page.previous_page_number() if page.has_previous() else 1,
|
||||
"next_page": page.next_page_number() if page.has_next() else page.paginator.num_pages,
|
||||
"previous_page_number": page.previous_page_number() if page.has_previous() else 1,
|
||||
"next_page_number": page.next_page_number() if page.has_next() else page.paginator.num_pages,
|
||||
"paginator": {"num_pages": page.paginator.num_pages},
|
||||
}
|
||||
context["page_obj"] = custom_page_obj
|
||||
|
|
|
|||
0
common/__init__.py
Normal file
0
common/__init__.py
Normal file
5
common/apps.py
Normal file
5
common/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CommonConfig(AppConfig):
|
||||
name = 'common'
|
||||
0
common/templatetags/__init__.py
Normal file
0
common/templatetags/__init__.py
Normal file
10
common/templatetags/pagination_tags.py
Normal file
10
common/templatetags/pagination_tags.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.inclusion_tag("templatetags/pagination_controls.html", takes_context=True)
|
||||
def render_pagination(context, page_obj, hide_if_one_page=True):
|
||||
"""
|
||||
Renders the pagination controls given a page_obj. Optionally hides the controls if there is only one page.
|
||||
"""
|
||||
return {"page_obj": page_obj, "hide_if_one_page": hide_if_one_page}
|
||||
|
|
@ -52,6 +52,7 @@ INSTALLED_APPS = [
|
|||
"tailwind",
|
||||
"theme",
|
||||
#"django_browser_reload",
|
||||
"common",
|
||||
"accounts",
|
||||
"cards",
|
||||
"home",
|
||||
|
|
|
|||
179
theme/templates/account/dashboard.html
Normal file
179
theme/templates/account/dashboard.html
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n static crispy_forms_tags gravatar %}
|
||||
|
||||
{% block head_title %}{{ _('Dashboard') }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto" x-data="{ activeTab: '{{ active_tab|default:'dash' }}' }">
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<div class="tabs tabs-border mb-8">
|
||||
<button type="button" :class="{'tab': true, 'tab-active': activeTab === 'dash'}" @click="activeTab = 'dash'">{{ _('Dash') }}</button>
|
||||
<button type="button" :class="{'tab': true, 'tab-active': activeTab === 'dashboard_offers'}" @click="activeTab = 'dashboard_offers'">{{ _('Your Trade Offers') }} ({{ dashboard_offers_paginated.paginator.count }})</button>
|
||||
<button type="button" :class="{'tab': true, 'tab-active': activeTab === 'waiting_on_you'}" @click="activeTab = 'waiting_on_you'">{{ _('Waiting on You') }} ({{ trade_acceptances_waiting_paginated.paginator.count }})</button>
|
||||
<button type="button" :class="{'tab': true, 'tab-active': activeTab === 'waiting_on_them'}" @click="activeTab = 'waiting_on_them'">{{ _('Waiting on Them') }} ({{ other_party_trade_acceptances_paginated.paginator.count }})</button>
|
||||
<button type="button" :class="{'tab': true, 'tab-active': activeTab === 'trade_history'}" @click="activeTab = 'trade_history'">{{ _('Trade History') }}</button>
|
||||
<button type="button" :class="{'tab': true, 'tab-active': activeTab === 'profile'}" @click="activeTab = 'profile'">{{ _('Profile') }}</button>
|
||||
<button type="button" :class="{'tab': true, 'tab-active': activeTab === 'settings'}" @click="activeTab = 'settings'">{{ _('Settings') }}</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab Panels -->
|
||||
|
||||
<!-- Dash Tab - Dashboard Summary -->
|
||||
<div x-show="activeTab === 'dash'">
|
||||
<div class="card bg-base-100 shadow-xl mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">{{ _('Account Summary') }}</h2>
|
||||
<p><strong>{{ _('Username:') }}</strong> {{ request.user.username }}</p>
|
||||
<p><strong>{{ _('Default Friend Code:') }}</strong> {{ selected_friend_code.friend_code }}</p>
|
||||
<p><strong>{{ _('Reputation Score:') }}</strong> {{ request.user.reputation_score }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow-xl mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">{{ _('Trade Summary') }}</h2>
|
||||
<div class="stats shadow">
|
||||
<div class="stat">
|
||||
<div class="stat-title">{{ _('Your Trade Offers') }}</div>
|
||||
<div class="stat-value">{{ dashboard_offers_paginated.paginator.count }}</div>
|
||||
<div class="stat-desc">{{ _('Active Offers') }}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">{{ _('Waiting on You') }}</div>
|
||||
<div class="stat-value">{{ trade_acceptances_waiting_paginated.paginator.count }}</div>
|
||||
<div class="stat-desc">{{ _('Pending Requests') }}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">{{ _('Waiting on Them') }}</div>
|
||||
<div class="stat-value">{{ other_party_trade_acceptances_paginated.paginator.count }}</div>
|
||||
<div class="stat-desc">{{ _('Pending Responses') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">{{ _('Quick Actions') }}</h2>
|
||||
<div class="flex space-x-4">
|
||||
<a href="{% url 'trade_offer_create' %}" class="btn btn-primary">{{ _('Create New Offer') }}</a>
|
||||
<a href="{% url 'trade_offer_list' %}" class="btn btn-secondary">{{ _('View All Offers') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Your Trade Offers Tab -->
|
||||
<div x-show="activeTab === 'dashboard_offers'" x-data="tradeOffersPagination('{% url 'dashboard' %}?ajax_section=dashboard_offers')">
|
||||
{% include 'trades/_trade_offer_list.html' with offers=dashboard_offers_paginated %}
|
||||
</div>
|
||||
|
||||
<!-- Waiting on You Tab -->
|
||||
<div x-show="activeTab === 'waiting_on_you'" x-data="tradeOffersPagination('{% url 'dashboard' %}?ajax_section=waiting_acceptances')">
|
||||
{% include 'trades/_trade_offer_list.html' with offers=trade_acceptances_waiting_paginated %}
|
||||
</div>
|
||||
|
||||
<!-- Waiting on Them Tab -->
|
||||
<div x-show="activeTab === 'waiting_on_them'" x-data="tradeOffersPagination('{% url 'dashboard' %}?ajax_section=other_party_acceptances')">
|
||||
{% include 'trades/_trade_offer_list.html' with offers=other_party_trade_acceptances_paginated %}
|
||||
</div>
|
||||
|
||||
<!-- Trade History Tab -->
|
||||
<div x-show="activeTab === 'trade_history'">
|
||||
<div class="divider">{{ _('Closed Offers') }} ({{ closed_offers_paginated.paginator.count }})</div>
|
||||
<div class="mb-8">
|
||||
{% include 'trades/_trade_offer_list.html' with offers=closed_offers_paginated %}
|
||||
</div>
|
||||
<div class="divider">{{ _('Closed Acceptances') }} ({{ closed_acceptances_paginated.paginator.count }})</div>
|
||||
<div class="mb-8">
|
||||
{% include 'trades/_trade_offer_list.html' with offers=closed_acceptances_paginated %}
|
||||
</div>
|
||||
<div class="divider">{{ _('Rejected by Them') }} ({{ rejected_by_them_paginated.paginator.count }})</div>
|
||||
<div class="mb-8">
|
||||
{% include 'trades/_trade_offer_list.html' with offers=rejected_by_them_paginated %}
|
||||
</div>
|
||||
<div class="divider">{{ _('Rejected by Me') }} ({{ rejected_by_me_paginated.paginator.count }})</div>
|
||||
<div class="mb-8">
|
||||
{% include 'trades/_trade_offer_list.html' with offers=rejected_by_me_paginated %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile Tab -->
|
||||
<div x-show="activeTab === 'profile'">
|
||||
<div class="card card-border bg-base-100 shadow-lg mx-auto p-6 mb-4">
|
||||
{% with gravatar_profile=request.user.email|gravatar_profile_data %}
|
||||
{% if gravatar_profile %}
|
||||
<div class="hovercard-profile mb-4 text-center">
|
||||
<img src="{{ gravatar_profile.thumbnailUrl }}" alt="{{ gravatar_profile.displayName|default:request.user.username }}" class="rounded-full mb-2 mx-auto" />
|
||||
<h3 class="text-xl mb-2">{{ gravatar_profile.displayName|default:request.user.username }}</h3>
|
||||
<a href="{{ gravatar_profile.profileUrl }}" target="_blank" class="btn btn-primary">{{ _('View Gravatar Profile') }}</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-center">{{ _('No Gravatar profile data available.') }}</p>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<div class="divider"></div>
|
||||
<h2 class="text-lg font-semibold">{{ _('What is Gravatar?') }}</h2>
|
||||
<p class="mb-4">{{ _('Gravatar (Globally Recognized Avatar) is a free service that links your email address to a profile picture and, optionally, a profile. Many websites use Gravatar to display your avatar automatically.') }}</p>
|
||||
<h2 class="text-lg font-semibold">{{ _('How does it work?') }}</h2>
|
||||
<p class="mb-4">{{ _('If you have set up a Gravatar, your profile picture will appear whenever you use your email on supported sites. Updates made on Gravatar will reflect here.') }}</p>
|
||||
<h2 class="text-lg font-semibold">{{ _('Is it safe? What about privacy?') }}</h2>
|
||||
<p class="mb-4">{{ _('Gravatar is optional, and your email is hashed to maintain privacy. Your personal data remains secure.') }}</p>
|
||||
<h2 class="text-lg font-semibold">{{ _('Want to update or add a Gravatar?') }}</h2>
|
||||
<p class="mb-4">{{ _('Go to Gravatar.com to set up or change your avatar or profile. Your changes will appear here once saved!') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Tab -->
|
||||
<div x-show="activeTab === 'settings'">
|
||||
<div class="card card-border bg-base-100 shadow-lg mx-auto p-6 mb-4">
|
||||
<form method="post" action="{% url 'dashboard' %}">
|
||||
{% csrf_token %}
|
||||
{{ settings_form|crispy }}
|
||||
<a href="{% url 'list_friend_codes' %}" class="link link-secondary">{{ _('Edit Friend Codes') }}</a>
|
||||
<button type="submit" name="update_settings" class="w-full btn btn-success mt-4">{{ _('Save Settings') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<a href="{% url 'account_logout' %}" class="btn btn-warning">{{ _('Sign Out') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function tradeOffersPagination(baseUrl) {
|
||||
return {
|
||||
baseUrl: baseUrl,
|
||||
_hasChangePageListener: false,
|
||||
loadPage(page) {
|
||||
let url = new URL(this.baseUrl, window.location.origin);
|
||||
url.searchParams.set("page", page);
|
||||
fetch(url, {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
this.$el.innerHTML = html;
|
||||
this.init();
|
||||
});
|
||||
},
|
||||
init() {
|
||||
if (!this._hasChangePageListener) {
|
||||
this.$el.addEventListener('change-page', event => {
|
||||
let page = event.detail.page;
|
||||
this.loadPage(page);
|
||||
});
|
||||
this._hasChangePageListener = true;
|
||||
}
|
||||
this.$el.querySelectorAll("a.ajax-page-link").forEach(link => {
|
||||
link.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
let page = link.getAttribute("data-page");
|
||||
this.loadPage(page);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n gravatar%}
|
||||
|
||||
{% block head_title %}{% trans "Settings" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto">
|
||||
<h1 class="text-3xl font-semibold text-center mb-6">{% trans "Profile" %}</h1>
|
||||
<div class="card card-border bg-base-100 shadow-lg w-4/5 mx-auto">
|
||||
<div class="card-body">
|
||||
<div class="hovercard-preview">
|
||||
<iframe src="https://gravatar.com/{{ user.email|gravatar_hash }}.card" width="100%" height="344px" style="border:0; margin:0; padding:0;" onload="resizeIframe(this)"></iframe>
|
||||
</div>
|
||||
<p class="text-center">All profile information is managed through Gravatar.</p>
|
||||
<div class="text-center mt-4 flex flex-col gap-4 mx-auto">
|
||||
<a href="https://gravatar.com/profile/" target="_blank" rel="noopener noreferrer" class="btn btn-primary">
|
||||
Edit Profile on Gravatar
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="{% url 'settings' %}" class="btn btn-secondary">Back to Settings</a>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<h2 class="text-lg font-semibold pt-0">What is Gravatar?</h2>
|
||||
<p class="mb-4">Gravatar (Globally Recognized Avatar) is a free service that links your email address to a profile picture and, optionally, a profile. Many websites, including this one, use Gravatar to display your avatar and profile automatically.</p>
|
||||
|
||||
<h2 class="text-lg font-semibold">How does it work?</h2>
|
||||
<p class="mb-4">If you've set up a Gravatar, your profile picture will appear here whenever you use your email on supported sites. When someone hovers over or clicks on your avatar, your Gravatar profile will also appear if you have one. If you don't have a Gravatar yet, you'll see a default image instead.</p>
|
||||
|
||||
<h2 class="text-lg font-semibold">Is it safe? What about privacy?</h2>
|
||||
<p class="mb-4">Gravatar is completely optional, opt-in, and prioritizes your security and privacy. Your email is never shared and only a hashed version is sent to Gravatar, protecting your identity while ensuring that your email address is not exposed to bots or scrapers. Your personal data remains secure, and you maintain full control over your public profile.</p>
|
||||
|
||||
<h2 class="text-lg font-semibold">Want to update or add a Gravatar?</h2>
|
||||
<p class="mb-4">Go to Gravatar.com to set up or change your avatar or profile. Your updates will appear here once saved!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n crispy_forms_tags %}
|
||||
|
||||
{% block head_title %}{% trans "Settings" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto space-y-8">
|
||||
<h1 class="text-3xl font-bold text-center mb-6">{% trans "Settings" %}</h1>
|
||||
|
||||
<!-- Account Navigation Section -->
|
||||
<div class="card card-border bg-base-100 shadow-lg w-4/5 mx-auto p-6">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button type="submit" class="w-full btn btn-success mt-4">
|
||||
{% trans "Save Settings" %}
|
||||
</button>
|
||||
</form>
|
||||
<div class="divider my-4"></div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-row gap-4">
|
||||
<a href="{% url 'profile' %}" class="btn btn-secondary w-20 grow-1">
|
||||
{% trans "Profile" %}
|
||||
</a>
|
||||
<a href="{% url 'list_friend_codes' %}" class="btn btn-primary w-20 grow-1">
|
||||
{% trans "Friend Codes" %}
|
||||
</a>
|
||||
</div>
|
||||
<a href="{% url 'account_logout' %}" class="btn btn-warning">
|
||||
{% trans "Sign Out" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -2,8 +2,7 @@
|
|||
{% url 'home' as home_url %}
|
||||
{% url 'trade_offer_list' as trade_offer_list_url %}
|
||||
{% url 'cards:card_list' as cards_list_url %}
|
||||
{% url 'settings' as settings_url %}
|
||||
|
||||
{% url 'dashboard' as dashboard_url %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
|
@ -68,13 +67,7 @@
|
|||
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow">
|
||||
<li><a href="{% url 'home' %}">Home</a></li>
|
||||
<li><a href="{% url 'cards:card_list' %}">Cards</a></li>
|
||||
<li>
|
||||
<a>Trades</a>
|
||||
<ul class="p-2">
|
||||
<li><a href="{% url 'trade_offer_list' %}">All Offers</a></li>
|
||||
<li><a href="{% url 'trade_offer_my_list' %}">My Trades</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="{% url 'trade_offer_list' %}">Trades</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<a class="btn btn-ghost text-xl" href="{% url 'home' %}">
|
||||
|
|
@ -89,15 +82,7 @@
|
|||
<ul class="menu menu-horizontal px-1">
|
||||
<li><a href="{% url 'home' %}">Home</a></li>
|
||||
<li><a href="{% url 'cards:card_list' %}">Cards</a></li>
|
||||
<li>
|
||||
<details>
|
||||
<summary>Trades</summary>
|
||||
<ul class="p-2 w-32 z-10">
|
||||
<li><a href="{% url 'trade_offer_list' %}">All Offers</a></li>
|
||||
<li><a href="{% url 'trade_offer_my_list' %}">My Trades</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
<li><a href="{% url 'trade_offer_list' %}">Trades</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
|
|
@ -121,8 +106,8 @@
|
|||
tabindex="0"
|
||||
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-32 p-2 shadow">
|
||||
<li>
|
||||
<a class="justify-between" href="{% url 'settings' %}">
|
||||
Settings
|
||||
<a class="justify-between" href="{% url 'dashboard' %}">
|
||||
Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li><a href="{% url 'account_logout' %}">Sign Out</a></li>
|
||||
|
|
@ -164,9 +149,9 @@
|
|||
<svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 0 1 0 3.75H5.625a1.875 1.875 0 0 1 0-3.75Z" /></svg>
|
||||
<span class="dock-label">Trades</span>
|
||||
</button>
|
||||
<button @click="window.location.href = '{{ settings_url }}'" class="{% if request.path == settings_url %}dock-active{% endif %}">
|
||||
<button @click="window.location.href = '{{ dashboard_url }}'" class="{% if request.path == dashboard_url or request.path == settings_url %}dock-active{% endif %}">
|
||||
{% if user.is_authenticated %}<div tabindex="0" role="button" class="avatar"><div class="w-6 rounded-full">{{ user.email|gravatar:40 }}</div></div>{% else %}<svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="currentColor" stroke-linejoin="miter" stroke-linecap="butt"><circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"></circle><path d="m22,13.25v-2.5l-2.318-.966c-.167-.581-.395-1.135-.682-1.654l.954-2.318-1.768-1.768-2.318.954c-.518-.287-1.073-.515-1.654-.682l-.966-2.318h-2.5l-.966,2.318c-.581.167-1.135.395-1.654.682l-2.318-.954-1.768,1.768.954,2.318c-.287.518-.515,1.073-.682,1.654l-2.318.966v2.5l2.318.966c.167.581.395,1.135.682,1.654l-.954,2.318,1.768,1.768,2.318-.954c.518.287,1.073.515,1.654.682l.966,2.318h2.5l.966-2.318c.581-.167,1.135-.395,1.654-.682l2.318.954,1.768-1.768-.954-2.318c.287-.518.515-1.073.682-1.654l2.318-.966Z" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"></path></g></svg>{% endif %}
|
||||
<span class="dock-label">Settings</span>
|
||||
<span class="dock-label">Dashboard</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -7,29 +7,6 @@
|
|||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if is_paginated %}
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
{% if page_obj.has_previous %}
|
||||
<button type="button" class="btn btn-sm"
|
||||
@click="$dispatch('change-page-{{ side }}', { page: {{ page_obj.previous_page_number }} })">
|
||||
Previous
|
||||
</button>
|
||||
{% else %}
|
||||
<span></span>
|
||||
{% endif %}
|
||||
{% if paginator.num_pages > 1 %}
|
||||
<span class="text-sm">Page {{ page_obj.number }} of {{ paginator.num_pages }}</span>
|
||||
{% endif %}
|
||||
{% if page_obj.has_next %}
|
||||
<button type="button" class="btn btn-sm"
|
||||
@click="$dispatch('change-page-{{ side }}', { page: {{ page_obj.next_page_number }} })">
|
||||
Next
|
||||
</button>
|
||||
{% else %}
|
||||
<span></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="text-gray-500">No trade offers found.</p>
|
||||
{% endif %}
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
{% endif %}
|
||||
|
||||
<div class="mt-4 flex flex-row justify-between">
|
||||
<a href="{% url 'settings' %}" class="btn btn-secondary">Back to Settings</a>
|
||||
<a href="{% url 'dashboard' %}" class="btn btn-secondary">Back to Dashboard</a>
|
||||
<a href="{% url 'add_friend_code' %}" class="btn btn-primary">Add a New Friend Code</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% load trade_offer_tags %}
|
||||
{% load trade_offer_tags pagination_tags %}
|
||||
{% comment %}
|
||||
This snippet renders a grid of trade offer cards (or acceptance cards) along with pagination controls.
|
||||
For a TradeOffer, we use {% render_trade_offer %}; for a TradeAcceptance, {% render_trade_acceptance %}.
|
||||
|
|
@ -18,3 +18,4 @@
|
|||
<div>No trade offers available.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% render_pagination offers %}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
{% include "trades/_trade_offer_list.html" %}
|
||||
{% if offers.has_other_pages %}
|
||||
<nav aria-label="Trade offers pagination" class="mt-6">
|
||||
<ul class="flex justify-center space-x-2">
|
||||
{% if offers.has_previous %}
|
||||
<li>
|
||||
<a class="btn btn-outline ajax-page-link" data-page="{{ offers.previous_page_number }}" href="#">
|
||||
Previous
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<span class="btn btn-outline btn-disabled">Previous</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for num in offers.paginator.page_range %}
|
||||
<li>
|
||||
<a class="btn btn-outline ajax-page-link {% if offers.number == num %}btn-active{% endif %}" data-page="{{ num }}" href="#">
|
||||
{{ num }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
{% if offers.has_next %}
|
||||
<li>
|
||||
<a class="btn btn-outline ajax-page-link" data-page="{{ offers.next_page_number }}" href="#">
|
||||
Next
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<span class="btn btn-outline btn-disabled">Next</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
16
theme/templates/trades/email/trade_update_accepted.txt
Normal file
16
theme/templates/trades/email/trade_update_accepted.txt
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
Hello {{ recipient_user }},
|
||||
|
||||
Great news! {{ acting_user }} ({{ acting_user_friend_code }}) has accepted your trade offer.
|
||||
|
||||
Trade Details:
|
||||
- #{{ hash }}
|
||||
- They are offering: {{ want_card }}
|
||||
- They want: {{ has_card }}
|
||||
|
||||
What's next? You can now mark the trade as "Sent" once you've offered the card to them in the app, or reject the trade if needed.
|
||||
|
||||
Visit your dashboard to manage this trade:
|
||||
{% url 'dashboard' %}
|
||||
|
||||
Happy trading!
|
||||
PKMN Trade Club
|
||||
16
theme/templates/trades/email/trade_update_received.txt
Normal file
16
theme/templates/trades/email/trade_update_received.txt
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
Hello {{ recipient_user }},
|
||||
|
||||
{{ acting_user }} ({{ acting_user_friend_code }}) has marked your trade as "Received".
|
||||
|
||||
Trade Details:
|
||||
- #{{ hash }}
|
||||
- Card you sent: {{ has_card }}
|
||||
- Card they offered: {{ want_card }}
|
||||
|
||||
What's next? Send a thank you to this user to increase their reputation!
|
||||
|
||||
Visit your dashboard to send thanks:
|
||||
{% url 'dashboard' %}
|
||||
|
||||
Happy trading!
|
||||
PKMN Trade Club
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
Hello {{ recipient_user }},
|
||||
|
||||
We're sorry to inform you that {{ acting_user }} ({{ acting_user_friend_code }}) has canceled their trade acceptance.
|
||||
|
||||
Trade Details:
|
||||
- #{{ hash }}
|
||||
- Card you were going to send: {{ has_card }}
|
||||
- Card they were offering: {{ want_card }}
|
||||
|
||||
Your trade offer is still active and available for other users to accept.
|
||||
|
||||
Visit your dashboard to manage your trade offers:
|
||||
{% url 'dashboard' %}
|
||||
|
||||
Happy trading!
|
||||
PKMN Trade Club
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
Hello {{ recipient_user }},
|
||||
|
||||
We're sorry to inform you that {{ acting_user }} ({{ acting_user_friend_code }}) has rejected the trade.
|
||||
|
||||
Trade Details:
|
||||
- #{{ hash }}
|
||||
- Card you were going to receive: {{ has_card }}
|
||||
- Card you were offering: {{ want_card }}
|
||||
|
||||
Don't worry - there are plenty of other trade opportunities available! You can browse our marketplace for similar trades.
|
||||
|
||||
Visit the marketplace:
|
||||
https://pkmntrade.club{% url 'trade_offer_list' %}
|
||||
|
||||
Better luck with your next trade!
|
||||
PKMN Trade Club
|
||||
16
theme/templates/trades/email/trade_update_sent.txt
Normal file
16
theme/templates/trades/email/trade_update_sent.txt
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
Hello {{ recipient_user }},
|
||||
|
||||
{{ acting_user }} ({{ acting_user_friend_code }}) has marked your trade as "Sent".
|
||||
|
||||
Trade Details:
|
||||
- #{{ hash }}
|
||||
- Card being sent to you: {{ has_card }}
|
||||
- Card you're offering: {{ want_card }}
|
||||
|
||||
What's next? Once you respond to the trade in the app, please mark the trade as "Received" in your dashboard.
|
||||
|
||||
Visit your dashboard to manage this trade:
|
||||
{% url 'dashboard' %}
|
||||
|
||||
Happy trading!
|
||||
PKMN Trade Club
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
Hello {{ recipient_user }},
|
||||
|
||||
{{ acting_user }} ({{ acting_user_friend_code }}) has sent their thanks for the successful trade!
|
||||
|
||||
Trade Details:
|
||||
- #{{ hash }}
|
||||
- Card you sent: {{ has_card }}
|
||||
- Card they offered: {{ want_card }}
|
||||
|
||||
What's next? Send a thank you to this user to increase their reputation!
|
||||
|
||||
Visit your dashboard to send thanks:
|
||||
{% url 'dashboard' %}
|
||||
|
||||
Happy trading!
|
||||
PKMN Trade Club
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
Hello {{ recipient_user }},
|
||||
|
||||
{{ acting_user }} ({{ acting_user_friend_code }}) has sent their thanks for the successful trade!
|
||||
|
||||
Trade Details:
|
||||
- #{{ hash }}
|
||||
- Card {% if is_initiator %}you{% else %}they{% endif %} sent: {{ has_card }}
|
||||
- Card {% if is_initiator %}they{% else %}you{% endif %} offered: {{ want_card }}
|
||||
|
||||
This trade is now completed; no further actions can be made.
|
||||
|
||||
Thank you for using PKMN Trade Club.
|
||||
|
||||
Happy trading!
|
||||
PKMN Trade Club
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
Hello {{ recipient_user }},
|
||||
|
||||
{{ acting_user }} ({{ acting_user_friend_code }}) has sent their thanks for the successful trade!
|
||||
|
||||
Trade Details:
|
||||
- #{{ hash }}
|
||||
- Card they sent: {{ has_card }}
|
||||
- Card you offered: {{ want_card }}
|
||||
|
||||
What's next? Send a thank you to this user to increase their reputation!
|
||||
|
||||
Visit your dashboard to send thanks:
|
||||
{% url 'dashboard' %}
|
||||
|
||||
Happy trading!
|
||||
PKMN Trade Club
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load static pagination_tags %}
|
||||
|
||||
{% block title %}All Trade Offers{% endblock title %}
|
||||
{% block title %}Trade Offers{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-4xl mt-6" x-data="{ allExpanded: false }">
|
||||
<!-- Header-->
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h1 class="text-2xl font-bold">All Trade Offers</h1>
|
||||
<h1 class="text-2xl font-bold">Trade Offers</h1>
|
||||
<div>
|
||||
<form method="get" class="flex items-center gap-4" x-data>
|
||||
<label class="cursor-pointer flex items-center gap-2">
|
||||
|
|
@ -27,47 +27,23 @@
|
|||
<div class="flex justify-end mb-4">
|
||||
<a href="{% url 'trade_offer_create' %}" class="btn btn-success">Create New Offer</a>
|
||||
</div>
|
||||
<div
|
||||
id="all-trade-offers"
|
||||
x-data="tradeOffersPagination('{% url 'trade_offer_list' %}?')"
|
||||
x-init="init()"
|
||||
>
|
||||
{% include "trades/_trade_offer_list_paginated.html" with offers=all_trade_offers_paginated %}
|
||||
<div id="all-trade-offers"
|
||||
x-data="{
|
||||
page: {{ all_trade_offers_paginated.number|default:1 }},
|
||||
loadOffers() {
|
||||
let url = new URL('{% url 'trade_offer_list' %}', window.location.origin);
|
||||
let params = new URLSearchParams(window.location.search);
|
||||
params.set('page', this.page);
|
||||
url.search = params.toString();
|
||||
fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' }})
|
||||
.then(response => response.text())
|
||||
.then(html => { this.$el.innerHTML = html; });
|
||||
}
|
||||
}"
|
||||
x-init="loadOffers()"
|
||||
x-on:change-page.window="page = $event.detail.page; loadOffers()">
|
||||
{% include "trades/_trade_offer_list.html" with offers=all_trade_offers_paginated %}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function tradeOffersPagination(baseUrl) {
|
||||
return {
|
||||
baseUrl: baseUrl,
|
||||
loadPage(page) {
|
||||
let url = new URL(this.baseUrl, window.location.origin);
|
||||
url.searchParams.set("page", page);
|
||||
this.$el.innerHTML = '<div class="flex justify-center items-center w-full mt-10"><span class="loading loading-dots loading-xl"></span></div>';
|
||||
fetch(url, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
this.$el.innerHTML = html;
|
||||
// Reinitialize the click events after injecting the new fragment.
|
||||
this.init();
|
||||
});
|
||||
},
|
||||
init() {
|
||||
// Bind click events for AJAX pagination links within this component.
|
||||
this.$el.querySelectorAll("a.ajax-page-link").forEach(link => {
|
||||
link.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
const page = link.getAttribute("data-page");
|
||||
this.loadPage(page);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}My Trades{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-4xl mt-6" x-data="{ allExpanded: false }">
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<a href="{% url 'trade_offer_create' %}" class="btn btn-success">Create New Offer</a>
|
||||
<div>
|
||||
<form method="get" class="flex flex-wrap justify-end space-x-4 gap-2" x-data>
|
||||
<label class="cursor-pointer flex items-center space-x-2 h-10">
|
||||
<span x-text="allExpanded ? 'Collapse All' : 'Expand All'"></span>
|
||||
<input type="checkbox" name="all_expanded" value="true" class="toggle toggle-primary" @click="allExpanded = !allExpanded; $dispatch('toggle-all', { expanded: allExpanded })">
|
||||
</label>
|
||||
<label class="cursor-pointer flex items-center space-x-2 h-10">
|
||||
<span class="font-medium">Only Closed</span>
|
||||
<input type="checkbox" name="show_closed" value="true" class="toggle toggle-primary" @change="$el.form.submit()" {% if show_closed %}checked{% endif %}>
|
||||
</label>
|
||||
|
||||
{% include "trades/_friend_code_select.html" with friend_codes=friend_codes selected_friend_code=selected_friend_code field_name="friend_code" label="" %}
|
||||
|
||||
<button type="submit" class="btn btn-primary" x-show="false">Apply</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-8">
|
||||
<!-- Section: Waiting for Your Response -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold mb-4">Waiting for Your Response</h2>
|
||||
<div
|
||||
id="waiting-acceptances"
|
||||
x-data="tradeOffersPagination('{% url 'trade_offer_my_list' %}?ajax_section=waiting_acceptances')"
|
||||
x-init="init()"
|
||||
>
|
||||
{% include "trades/_trade_offer_list_paginated.html" with offers=trade_acceptances_waiting_paginated %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section: Waiting for Their Response -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold mb-4">Waiting for Their Response</h2>
|
||||
<div
|
||||
id="other-acceptances"
|
||||
x-data="tradeOffersPagination('{% url 'trade_offer_my_list' %}?ajax_section=other_party_acceptances')"
|
||||
x-init="init()"
|
||||
>
|
||||
{% include "trades/_trade_offer_list_paginated.html" with offers=other_party_trade_acceptances_paginated %}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<!-- Section: My Trade Offers -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold mb-4">My Trade Offers</h2>
|
||||
<div
|
||||
id="my-trade-offers"
|
||||
x-data="tradeOffersPagination('{% url 'trade_offer_my_list' %}?ajax_section=my_trade_offers')"
|
||||
x-init="init()">
|
||||
{% include "trades/_trade_offer_list_paginated.html" with offers=my_trade_offers_paginated %}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function tradeOffersPagination(baseUrl) {
|
||||
return {
|
||||
baseUrl: baseUrl,
|
||||
loadPage(page) {
|
||||
let url = new URL(this.baseUrl, window.location.origin);
|
||||
url.searchParams.set("page", page);
|
||||
fetch(url, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
this.$el.innerHTML = html;
|
||||
// Reinitialize click events after content update.
|
||||
this.init();
|
||||
});
|
||||
},
|
||||
init() {
|
||||
// Bind click events on pagination links within the component.
|
||||
this.$el.querySelectorAll("a.ajax-page-link").forEach(link => {
|
||||
link.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
let page = link.getAttribute("data-page");
|
||||
this.loadPage(page);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
{% if not hide_if_one_page or page_obj.paginator.num_pages > 1 %}
|
||||
<div class="flex flex-col justify-center items-center mt-4">
|
||||
<div class="flex justify-center items-center space-x-2">
|
||||
<!-- First Button -->
|
||||
<button class="btn btn-outline btn-md"
|
||||
|
|
@ -8,22 +10,22 @@
|
|||
<!-- Previous Button -->
|
||||
<button class="btn btn-outline btn-md"
|
||||
{% if not page_obj.has_previous %}disabled{% endif %}
|
||||
@click="$dispatch('change-page', { page: {{ page_obj.previous_page }} })">
|
||||
@click="$dispatch('change-page', { page: {% if page_obj.has_previous %}{{ page_obj.previous_page_number }}{% else %}1{% endif %} })">
|
||||
Prev
|
||||
</button>
|
||||
<!-- Goto Page -->
|
||||
<span class="flex items-center space-x-1 gap-2">
|
||||
<input type="number" min="1" max="{{ page_obj.paginator.num_pages }}" value="{{ page_obj.number }}"
|
||||
class="input input-xs text-center" id="gotoPageInput">
|
||||
class="input input-xs text-center" id="gotoPageInput_{{ page_obj.number }}" title="Enter page number">
|
||||
<button class="btn btn-outline btn-md"
|
||||
@click="(document.getElementById('gotoPageInput').value >= 1 && document.getElementById('gotoPageInput').value <= {{ page_obj.paginator.num_pages }}) && $dispatch('change-page', { page: parseInt(document.getElementById('gotoPageInput').value) })">
|
||||
@click="(document.getElementById('gotoPageInput_{{ page_obj.number }}').value >= 1 && document.getElementById('gotoPageInput_{{ page_obj.number }}').value <= {{ page_obj.paginator.num_pages }}) && $dispatch('change-page', { page: parseInt(document.getElementById('gotoPageInput_{{ page_obj.number }}').value) })">
|
||||
Go
|
||||
</button>
|
||||
</span>
|
||||
<!-- Next Button -->
|
||||
<button class="btn btn-outline btn-md"
|
||||
{% if not page_obj.has_next %}disabled{% endif %}
|
||||
@click="$dispatch('change-page', { page: {{ page_obj.next_page }} })">
|
||||
@click="$dispatch('change-page', { page: {% if page_obj.has_next %}{{ page_obj.next_page_number }}{% else %}{{ page_obj.number }}{% endif %} })">
|
||||
Next
|
||||
</button>
|
||||
<!-- Last Button -->
|
||||
|
|
@ -36,3 +38,5 @@
|
|||
<div class="flex items-center justify-center mt-2">
|
||||
<span>Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 5.1.2 on 2025-03-29 21:23
|
||||
# Generated by Django 5.1.2 on 2025-03-31 22:48
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
|
|
|||
|
|
@ -334,6 +334,7 @@ class TradeAcceptance(models.Model):
|
|||
if new_state not in allowed:
|
||||
raise ValueError(f"Transition from {self.state} to {new_state} is not allowed.")
|
||||
|
||||
self._actioning_user = user
|
||||
self.state = new_state
|
||||
self.save(update_fields=["state"])
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,12 @@ from django.dispatch import receiver
|
|||
from django.db.models import F
|
||||
from trades.models import TradeOfferHaveCard, TradeOfferWantCard, TradeAcceptance
|
||||
from django.db import transaction
|
||||
from accounts.models import CustomUser
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
import uuid
|
||||
import hashlib
|
||||
from django.conf import settings
|
||||
|
||||
ACTIVE_STATES = [
|
||||
TradeAcceptance.AcceptanceState.ACCEPTED,
|
||||
|
|
@ -81,3 +87,169 @@ def trade_acceptance_post_delete(sender, instance, **kwargs):
|
|||
adjust_qty_for_trade_offer(trade_offer, instance.requested_card, side='have', delta=delta)
|
||||
adjust_qty_for_trade_offer(trade_offer, instance.offered_card, side='want', delta=delta)
|
||||
update_trade_offer_closed_status(trade_offer)
|
||||
|
||||
@receiver(post_save, sender=TradeAcceptance)
|
||||
def trade_acceptance_email_notification(sender, instance, created, **kwargs):
|
||||
# Only proceed if the update was triggered by an acting user.
|
||||
if not hasattr(instance, "_actioning_user"):
|
||||
print("No actioning user")
|
||||
return
|
||||
|
||||
# check if were in debug mode
|
||||
if settings.DEBUG:
|
||||
print("DEBUG: skipping email notification in debug mode")
|
||||
return
|
||||
|
||||
acting_user = instance._actioning_user
|
||||
state = instance.state
|
||||
|
||||
if state == TradeAcceptance.AcceptanceState.ACCEPTED:
|
||||
state = "accepted"
|
||||
elif state == TradeAcceptance.AcceptanceState.SENT:
|
||||
state = "sent"
|
||||
elif state == TradeAcceptance.AcceptanceState.RECEIVED:
|
||||
state = "received"
|
||||
elif state == TradeAcceptance.AcceptanceState.THANKED_BY_INITIATOR:
|
||||
state = "thanked_by_initiator"
|
||||
elif state == TradeAcceptance.AcceptanceState.THANKED_BY_ACCEPTOR:
|
||||
state = "thanked_by_acceptor"
|
||||
elif state == TradeAcceptance.AcceptanceState.THANKED_BY_BOTH:
|
||||
state = "thanked_by_both"
|
||||
elif state == TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR:
|
||||
state = "rejected_by_initiator"
|
||||
elif state == TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR:
|
||||
state = "rejected_by_acceptor"
|
||||
else:
|
||||
return
|
||||
|
||||
print("state", state)
|
||||
print("acting_user", acting_user)
|
||||
|
||||
# Determine the non-acting party:
|
||||
if instance.trade_offer.initiated_by == acting_user:
|
||||
# The initiator made the change; notify the acceptor.
|
||||
recipient_user = instance.accepted_by.user
|
||||
email_template = "trades/email/trade_update_" + state + ".txt"
|
||||
email_subject = "[PKMN Trade Club] Trade Update"
|
||||
else:
|
||||
# The acceptor made the change; notify the initiator.
|
||||
recipient_user = instance.trade_offer.initiated_by.user
|
||||
email_template = "trades/email/trade_update_" + state + ".txt"
|
||||
email_subject = "[PKMN Trade Club] Trade Update"
|
||||
|
||||
is_initiator = instance.trade_offer.initiated_by == acting_user
|
||||
|
||||
from django.template.loader import render_to_string
|
||||
email_context = {
|
||||
"has_card": instance.requested_card,
|
||||
"want_card": instance.offered_card,
|
||||
"hash": instance.hash,
|
||||
"acting_user": acting_user.username,
|
||||
"recipient_user": recipient_user.username,
|
||||
"acting_user_friend_code": instance.trade_offer.initiated_by.friend_code if is_initiator else instance.accepted_by.friend_code,
|
||||
"is_initiator": is_initiator,
|
||||
"pk": instance.pk,
|
||||
}
|
||||
print("email_context", email_context)
|
||||
email_body = render_to_string(email_template, email_context)
|
||||
|
||||
from django.core.mail import send_mail
|
||||
send_mail(
|
||||
email_subject,
|
||||
email_body,
|
||||
None, # Django will use DEFAULT_FROM_EMAIL from settings
|
||||
[recipient_user.email],
|
||||
)
|
||||
|
||||
# Clean up the temporary attribute.
|
||||
del instance._actioning_user
|
||||
|
||||
@receiver(post_save, sender=TradeAcceptance)
|
||||
def trade_acceptance_reputation_update(sender, instance, created, **kwargs):
|
||||
"""
|
||||
Update the denormalized reputation score on the user model based on
|
||||
state transitions for TradeAcceptance.
|
||||
|
||||
- THANKED_BY_BOTH: both the initiator and the acceptor receive +1 when transitioning
|
||||
into this state, and -1 when leaving it.
|
||||
- REJECTED_BY_INITIATOR: only the acceptor gets -1 when transitioning into it (and +1 when leaving it).
|
||||
- REJECTED_BY_ACCEPTOR: only the initiator gets -1 when transitioning into it (and +1 when leaving it).
|
||||
|
||||
Creation events are ignored because trade acceptances are never created with a terminal state.
|
||||
"""
|
||||
if created:
|
||||
return # No action on creation as terminal states are not expected.
|
||||
|
||||
thanks_delta = 0
|
||||
rejection_delta_initiator = 0 # Delta for the initiator's reputation
|
||||
rejection_delta_acceptor = 0 # Delta for the acceptor's reputation
|
||||
|
||||
old_state = getattr(instance, '_old_state', None)
|
||||
if old_state is None:
|
||||
return
|
||||
|
||||
# Handle THANKED_BY_BOTH transitions
|
||||
if old_state != TradeAcceptance.AcceptanceState.THANKED_BY_BOTH and instance.state == TradeAcceptance.AcceptanceState.THANKED_BY_BOTH:
|
||||
thanks_delta = 1
|
||||
elif old_state == TradeAcceptance.AcceptanceState.THANKED_BY_BOTH and instance.state != TradeAcceptance.AcceptanceState.THANKED_BY_BOTH:
|
||||
thanks_delta = -1
|
||||
|
||||
# Handle REJECTED_BY_INITIATOR transitions (affects the acceptor)
|
||||
if old_state != TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR and instance.state == TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR:
|
||||
rejection_delta_acceptor = -1
|
||||
elif old_state == TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR and instance.state != TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR:
|
||||
rejection_delta_acceptor = 1
|
||||
|
||||
# Handle REJECTED_BY_ACCEPTOR transitions (affects the initiator)
|
||||
if old_state != TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR and instance.state == TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR:
|
||||
rejection_delta_initiator = -1
|
||||
elif old_state == TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR and instance.state != TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR:
|
||||
rejection_delta_initiator = 1
|
||||
|
||||
# Apply reputation updates:
|
||||
# For THANKED_BY_BOTH, update both users.
|
||||
if thanks_delta:
|
||||
CustomUser.objects.filter(pk=instance.trade_offer.initiated_by.user.pk).update(
|
||||
reputation_score=F("reputation_score") + thanks_delta
|
||||
)
|
||||
CustomUser.objects.filter(pk=instance.accepted_by.user.pk).update(
|
||||
reputation_score=F("reputation_score") + thanks_delta
|
||||
)
|
||||
|
||||
# For REJECTED_BY_INITIATOR, update only the acceptor.
|
||||
if rejection_delta_acceptor:
|
||||
CustomUser.objects.filter(pk=instance.accepted_by.user.pk).update(
|
||||
reputation_score=F("reputation_score") + rejection_delta_acceptor
|
||||
)
|
||||
|
||||
# For REJECTED_BY_ACCEPTOR, update only the initiator.
|
||||
if rejection_delta_initiator:
|
||||
CustomUser.objects.filter(pk=instance.trade_offer.initiated_by.user.pk).update(
|
||||
reputation_score=F("reputation_score") + rejection_delta_initiator
|
||||
)
|
||||
|
||||
@receiver(post_delete, sender=TradeAcceptance)
|
||||
def trade_acceptance_reputation_delete(sender, instance, **kwargs):
|
||||
"""
|
||||
When a TradeAcceptance is deleted, adjust the reputation score for the
|
||||
affected user(s) by reversing any reputation changes previously applied.
|
||||
|
||||
- If the deleted instance was in THANKED_BY_BOTH: subtract 1 from both parties.
|
||||
- If it was in REJECTED_BY_INITIATOR: add 1 to the acceptor.
|
||||
- If it was in REJECTED_BY_ACCEPTOR: add 1 to the initiator.
|
||||
"""
|
||||
if instance.state == TradeAcceptance.AcceptanceState.THANKED_BY_BOTH:
|
||||
CustomUser.objects.filter(pk=instance.trade_offer.initiated_by.user.pk).update(
|
||||
reputation_score=F("reputation_score") - 1
|
||||
)
|
||||
CustomUser.objects.filter(pk=instance.accepted_by.user.pk).update(
|
||||
reputation_score=F("reputation_score") - 1
|
||||
)
|
||||
if instance.state == TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR:
|
||||
CustomUser.objects.filter(pk=instance.accepted_by.user.pk).update(
|
||||
reputation_score=F("reputation_score") + 1
|
||||
)
|
||||
if instance.state == TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR:
|
||||
CustomUser.objects.filter(pk=instance.trade_offer.initiated_by.user.pk).update(
|
||||
reputation_score=F("reputation_score") + 1
|
||||
)
|
||||
|
|
@ -563,7 +563,7 @@ class TradeOfferSecurityTests(TestCase):
|
|||
# Test without login
|
||||
urls_to_test = [
|
||||
reverse("trade_offer_create"),
|
||||
reverse("trade_offer_my_list"),
|
||||
reverse("trade_offer_dashboard"),
|
||||
reverse("trade_acceptance_create", kwargs={"offer_pk": self.trade_offer.pk}),
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ from .views import (
|
|||
TradeOfferCreateView,
|
||||
TradeOfferCreateConfirmView,
|
||||
TradeOfferAllListView,
|
||||
TradeOfferMyListView,
|
||||
TradeOfferDetailView,
|
||||
TradeAcceptanceCreateView,
|
||||
TradeAcceptanceUpdateView,
|
||||
|
|
@ -16,8 +15,7 @@ from .views import (
|
|||
urlpatterns = [
|
||||
path("create/", TradeOfferCreateView.as_view(), name="trade_offer_create"),
|
||||
path("create/confirm/", TradeOfferCreateConfirmView.as_view(), name="trade_offer_confirm_create"),
|
||||
path("all/", TradeOfferAllListView.as_view(), name="trade_offer_list"),
|
||||
path("my/", TradeOfferMyListView.as_view(), name="trade_offer_my_list"),
|
||||
path("", TradeOfferAllListView.as_view(), name="trade_offer_list"),
|
||||
path("search/", TradeOfferSearchView.as_view(), name="trade_offer_search"),
|
||||
path("<int:pk>/", TradeOfferDetailView.as_view(), name="trade_offer_detail"),
|
||||
path("<int:pk>.png", TradeOfferPNGView.as_view(), name="trade_offer_png"),
|
||||
|
|
|
|||
116
trades/views.py
116
trades/views.py
|
|
@ -104,120 +104,11 @@ class TradeOfferAllListView(ListView):
|
|||
paginated_offers = Paginator(queryset, 10).get_page(page)
|
||||
return render(
|
||||
self.request,
|
||||
"trades/_trade_offer_list_paginated.html",
|
||||
"trades/_trade_offer_list.html",
|
||||
{"offers": paginated_offers}
|
||||
)
|
||||
return super().render_to_response(context, **response_kwargs)
|
||||
|
||||
class TradeOfferMyListView(LoginRequiredMixin, FriendCodeRequiredMixin, ListView):
|
||||
model = TradeOffer
|
||||
template_name = "trades/trade_offer_my_list.html"
|
||||
|
||||
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, FriendCodeRequiredMixin, DeleteView):
|
||||
model = TradeOffer
|
||||
success_url = reverse_lazy("trade_offer_list")
|
||||
|
|
@ -526,6 +417,8 @@ class TradeAcceptanceCreateView(LoginRequiredMixin, FriendCodeRequiredMixin, Cre
|
|||
|
||||
def form_valid(self, form):
|
||||
form.instance.trade_offer = self.trade_offer
|
||||
# Set the actioning user before saving
|
||||
form.instance._actioning_user = self.request.user
|
||||
self.object = form.save()
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
|
|
@ -616,7 +509,6 @@ class TradeOfferPNGView(View):
|
|||
# If the image is already generated and stored, serve it directly.
|
||||
if trade_offer.image:
|
||||
trade_offer.image.open()
|
||||
print(f"Serving cached image for trade offer {trade_offer.pk}")
|
||||
return HttpResponse(trade_offer.image.read(), content_type="image/png")
|
||||
|
||||
# Acquire PostgreSQL advisory lock to prevent concurrent generation.
|
||||
|
|
@ -629,10 +521,8 @@ class TradeOfferPNGView(View):
|
|||
trade_offer.refresh_from_db()
|
||||
if trade_offer.image:
|
||||
trade_offer.image.open()
|
||||
print(f"Serving recently-cached image for trade offer {trade_offer.pk}")
|
||||
return HttpResponse(trade_offer.image.read(), content_type="image/png")
|
||||
|
||||
print(f"Generating PNG for trade offer {trade_offer.pk}")
|
||||
# Generate PNG using Playwright as before.
|
||||
from trades.templatetags import trade_offer_tags
|
||||
tag_context = trade_offer_tags.render_trade_offer_png(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue