Refactor card badge and multiselect template tags to properly implement and/or improve caching and context handling
- Updated `card_badge` and `card_multiselect` template tags to utilize `reverse_lazy` for URL resolution. - Enhanced caching mechanisms in `card_badge.html` and `card_multiselect.html` to improve performance. - Introduced a new template `_card_multiselect_options.html` for rendering multiselect options. - Improved context management in `card_multiselect` to handle selected cards and dynamic placeholders. - Added error handling for query hashing in `card_multiselect` to ensure robustness. - Updated `trade_offer_tags` to optimize database queries using `select_related` for related objects.
This commit is contained in:
parent
7d94dc001f
commit
4e50e1545c
10 changed files with 234 additions and 163 deletions
|
|
@ -1,41 +1,46 @@
|
||||||
from django import template
|
from django import template
|
||||||
|
from django.conf import settings
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.urls import reverse
|
from django.urls import reverse_lazy
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
@register.inclusion_tag("templatetags/card_badge.html")
|
@register.inclusion_tag("templatetags/card_badge.html", takes_context=True)
|
||||||
def card_badge(card, quantity=None, expanded=False):
|
def card_badge(context, card, quantity=None, expanded=False):
|
||||||
url = reverse('cards:card_detail', args=[card.pk])
|
"""
|
||||||
return {
|
Renders a card badge.
|
||||||
|
"""
|
||||||
|
url = reverse_lazy('cards:card_detail', args=[card.pk])
|
||||||
|
tag_context = {
|
||||||
'quantity': quantity,
|
'quantity': quantity,
|
||||||
'style': card.style,
|
'style': card.style,
|
||||||
'name': card.name,
|
'name': card.name,
|
||||||
'rarity': card.rarity_icon,
|
'rarity': card.rarity_icon,
|
||||||
'cardset': card.cardset,
|
'cardset': card.cardset,
|
||||||
'expanded': expanded,
|
'expanded': expanded,
|
||||||
|
'cache_key': f'card_badge_{card.pk}_{quantity}_{expanded}',
|
||||||
'url': url,
|
'url': url,
|
||||||
}
|
}
|
||||||
|
context.update(tag_context)
|
||||||
|
return context
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def card_badge_inline(card, quantity=None):
|
def card_badge_inline(card, quantity=None):
|
||||||
"""
|
"""
|
||||||
Renders an inline card badge.
|
Renders an inline card badge by directly rendering the template.
|
||||||
"""
|
"""
|
||||||
url = reverse('cards:card_detail', args=[card.pk])
|
url = reverse_lazy('cards:card_detail', args=[card.pk])
|
||||||
html = render_to_string("templatetags/card_badge.html", {
|
tag_context = {
|
||||||
'quantity': quantity,
|
'quantity': quantity,
|
||||||
'style': card.style,
|
'style': card.style,
|
||||||
'name': card.name,
|
'name': card.name,
|
||||||
'rarity': card.rarity_icon,
|
'rarity': card.rarity_icon,
|
||||||
'cardset': card.cardset,
|
'cardset': card.cardset,
|
||||||
'expanded': True,
|
'expanded': True,
|
||||||
|
'cache_key': f'card_badge_{card.pk}_{quantity}_{True}',
|
||||||
|
'CACHE_TIMEOUT': settings.CACHE_TIMEOUT,
|
||||||
'url': url,
|
'url': url,
|
||||||
})
|
}
|
||||||
return mark_safe(html)
|
html = render_to_string("templatetags/card_badge.html", tag_context)
|
||||||
|
return mark_safe(html)
|
||||||
@register.filter
|
|
||||||
def addstr(arg1, arg2):
|
|
||||||
"""concatenate arg1 & arg2"""
|
|
||||||
return str(arg1) + str(arg2)
|
|
||||||
|
|
@ -1,54 +1,72 @@
|
||||||
|
import uuid
|
||||||
from django import template
|
from django import template
|
||||||
from cards.models import Card
|
from cards.models import Card
|
||||||
|
from django.db.models.query import QuerySet
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
@register.inclusion_tag('templatetags/card_multiselect.html')
|
@register.filter
|
||||||
def card_multiselect(field_name, label, placeholder, cards=None, selected_values=None, cache_timeout=86400):
|
def get_item(dictionary, key):
|
||||||
|
"""Allows accessing dictionary items using a variable key in templates."""
|
||||||
|
return dictionary.get(key)
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def fetch_all_cards():
|
||||||
|
"""Simple tag to fetch all Card objects."""
|
||||||
|
return Card.objects.order_by('pk').all()
|
||||||
|
|
||||||
|
@register.inclusion_tag('templatetags/card_multiselect.html', takes_context=True)
|
||||||
|
def card_multiselect(context, field_name, label, placeholder, cards=None, selected_values=None):
|
||||||
"""
|
"""
|
||||||
Renders a multiselect field for choosing cards while supporting quantity data.
|
Prepares context for rendering a card multiselect input.
|
||||||
|
Database querying and rendering are handled within the template's cache block.
|
||||||
Updated to allow `card_filter` to be either a dictionary (of lookup parameters) or a QuerySet.
|
|
||||||
This is useful when you want to limit available cards based on your new trades models (e.g. showing only
|
|
||||||
cards that appear in active trade offers).
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
- field_name: The name attribute for the select tag.
|
|
||||||
- label: Label text to show above the selector.
|
|
||||||
- placeholder: Placeholder text to show in the select.
|
|
||||||
- selected_values: (Optional) A list of selected values; if a value includes a quantity it should be in the format "card_id:quantity".
|
|
||||||
- cache_timeout: (Optional) Cache timeout (in seconds) for the options block.
|
|
||||||
- cache_key: (Optional) Cache key.
|
|
||||||
"""
|
"""
|
||||||
if selected_values is None:
|
if selected_values is None:
|
||||||
selected_values = []
|
selected_values = []
|
||||||
# Create a mapping {card_id: quantity}
|
|
||||||
selected_cards = {}
|
selected_cards = {}
|
||||||
for val in selected_values:
|
for val in selected_values:
|
||||||
parts = str(val).split(':')
|
parts = str(val).split(':')
|
||||||
card_id = parts[0]
|
if len(parts) >= 1 and parts[0]:
|
||||||
quantity = parts[1] if len(parts) > 1 else 1
|
card_id = parts[0]
|
||||||
selected_cards[card_id] = quantity
|
quantity = parts[1] if len(parts) > 1 else 1
|
||||||
|
selected_cards[str(card_id)] = quantity
|
||||||
|
|
||||||
if cards is None:
|
effective_field_name = field_name if field_name is not None else 'card_multiselect'
|
||||||
cards = Card.objects.all()
|
effective_label = label if label is not None else 'Card'
|
||||||
|
effective_placeholder = placeholder if placeholder is not None else 'Select Cards'
|
||||||
|
|
||||||
# Loop through available cards and attach pre‑selected quantity
|
selected_cards_key_part = json.dumps(selected_cards, sort_keys=True)
|
||||||
for card in cards:
|
|
||||||
pk_str = str(card.pk)
|
|
||||||
if pk_str in selected_cards:
|
|
||||||
card.selected_quantity = selected_cards[pk_str]
|
|
||||||
card.selected = True
|
|
||||||
else:
|
|
||||||
card.selected_quantity = 1
|
|
||||||
card.selected = False
|
|
||||||
|
|
||||||
return {
|
has_passed_cards = isinstance(cards, QuerySet)
|
||||||
'field_name': field_name,
|
|
||||||
'field_id': field_name, # using the name as id for simplicity
|
if has_passed_cards:
|
||||||
'label': label,
|
try:
|
||||||
'cards': cards,
|
query_string = str(cards.query)
|
||||||
'placeholder': placeholder,
|
passed_cards_identifier = hashlib.sha256(query_string.encode('utf-8')).hexdigest()
|
||||||
'selected_values': list(selected_cards.keys()),
|
except Exception as e:
|
||||||
'cache_timeout': cache_timeout
|
logging.warning(f"Could not generate query hash for card_multiselect. Error: {e}")
|
||||||
}
|
passed_cards_identifier = 'specific_qs_fallback_' + str(uuid.uuid4())
|
||||||
|
else:
|
||||||
|
passed_cards_identifier = 'all_cards'
|
||||||
|
|
||||||
|
# Define the variables specific to this tag
|
||||||
|
tag_specific_context = {
|
||||||
|
'field_name': effective_field_name,
|
||||||
|
'field_id': effective_field_name,
|
||||||
|
'label': effective_label,
|
||||||
|
'placeholder': effective_placeholder,
|
||||||
|
'passed_cards': cards if has_passed_cards else None,
|
||||||
|
'has_passed_cards': has_passed_cards,
|
||||||
|
'selected_cards': selected_cards,
|
||||||
|
'selected_cards_key_part': selected_cards_key_part,
|
||||||
|
'passed_cards_identifier': passed_cards_identifier,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update the original context with the tag-specific variables
|
||||||
|
# This preserves CACHE_TIMEOUT and other parent context variables
|
||||||
|
context.update(tag_specific_context)
|
||||||
|
|
||||||
|
return context # Return the MODIFIED original context
|
||||||
30
theme/templatetags/_card_multiselect_options.html
Normal file
30
theme/templatetags/_card_multiselect_options.html
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{% load card_badge card_multiselect %}
|
||||||
|
<option value="" disabled>{{ placeholder }}</option>
|
||||||
|
{% for card in cards_to_render %}
|
||||||
|
{% with card_id_str=card.pk|stringformat:"s" %} {# Ensure card PK is string for lookup #}
|
||||||
|
{% if card_id_str in selected_cards %}
|
||||||
|
<option
|
||||||
|
value="{{ card.pk }}:{{ selected_cards|get_item:card_id_str }}"
|
||||||
|
data-card-id="{{ card.pk }}"
|
||||||
|
data-quantity="{{ selected_cards|get_item:card_id_str }}"
|
||||||
|
selected
|
||||||
|
data-html-content='<div class="m-2">{{ card|card_badge_inline:selected_cards|get_item:card_id_str }}</div>'
|
||||||
|
data-name="{{ card.name }}"
|
||||||
|
data-rarity="{{ card.rarity_icon }}"
|
||||||
|
data-cardset="{{ card.cardset }}">
|
||||||
|
{{ card.name }} {{ card.rarity_icon }} {{ card.cardset }}
|
||||||
|
</option>
|
||||||
|
{% else %}
|
||||||
|
<option
|
||||||
|
value="{{ card.pk }}:1"
|
||||||
|
data-card-id="{{ card.pk }}"
|
||||||
|
data-quantity="1"
|
||||||
|
data-html-content='<div class="m-2">{{ card|card_badge_inline:"" }}</div>'
|
||||||
|
data-name="{{ card.name }}"
|
||||||
|
data-rarity="{{ card.rarity_icon }}"
|
||||||
|
data-cardset="{{ card.cardset }}">
|
||||||
|
{{ card.name }} {{ card.rarity_icon }} {{ card.cardset }}
|
||||||
|
</option>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endfor %}
|
||||||
|
|
@ -1,26 +1,29 @@
|
||||||
<a href="{{ url }}" @click.stop>
|
{% load cache %}
|
||||||
<div class="relative block">
|
{% cache CACHE_TIMEOUT card_badge cache_key %}
|
||||||
{% if not expanded %}
|
<a href="{{ url }}" @click.stop>
|
||||||
<div class="flex flex-row items-center h-[32px] p-1.5 w-30 sm:w-40 text-white shadow-lg" style="{{ style }}">
|
<div class="relative block">
|
||||||
<div class="grow"><div class="truncate text-ellipsis font-semibold leading-tight text-sm marquee-calc w-4/5 sm:w-24">{{ name }}</div></div>
|
{% if not expanded %}
|
||||||
<div class="grow-0 shrink-0 text-right truncate font-semibold leading-tight text-sm">{{ cardset }}</div>
|
<div class="flex flex-row items-center h-[32px] p-1.5 w-30 sm:w-40 text-white shadow-lg" style="{{ style }}">
|
||||||
{% if quantity != None %}
|
<div class="grow"><div class="truncate text-ellipsis font-semibold leading-tight text-sm marquee-calc w-4/5 sm:w-24">{{ name }}</div></div>
|
||||||
<div class="grow-0 shrink-0 relative w-fit ps-1">
|
<div class="grow-0 shrink-0 text-right truncate font-semibold leading-tight text-sm">{{ cardset }}</div>
|
||||||
<div class="card-quantity-badge relative bg-gray-600 text-white text-sm font-semibold rounded-full text-center size-max px-1.5">{{ quantity }}</div>
|
{% if quantity != None %}
|
||||||
</div>
|
<div class="grow-0 shrink-0 relative w-fit ps-1">
|
||||||
{% endif %}
|
<div class="card-quantity-badge relative bg-gray-600 text-white text-sm font-semibold rounded-full text-center size-max px-1.5">{{ quantity }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% endif %}
|
||||||
<div class="grid grid-rows-2 grid-cols-4 h-[52px] p-1.5 w-30 sm:w-40 text-white shadow-lg" style="{{ style }}">
|
</div>
|
||||||
<div class="row-start-1 col-start-1 {% if quantity != None %}col-span-3{% else %}col-span-4{% endif %} self-start leading-tight truncate text-ellipsis"><span class="font-semibold text-sm marquee-calc">{{ name }}</span></div>
|
{% else %}
|
||||||
{% if quantity != None %}
|
<div class="grid grid-rows-2 grid-cols-4 h-[52px] p-1.5 w-30 sm:w-40 text-white shadow-lg" style="{{ style }}">
|
||||||
<div class="row-start-1 col-start-4 col-span-1 self-start ms-auto leading-tight relative w-fit ps-1">
|
<div class="row-start-1 col-start-1 {% if quantity != None %}col-span-3{% else %}col-span-4{% endif %} self-start leading-tight truncate text-ellipsis"><span class="font-semibold text-sm marquee-calc">{{ name }}</span></div>
|
||||||
<div class="card-quantity-badge relative bg-gray-600 text-white text-sm font-semibold rounded-full text-center size-max px-1.5">{{ quantity }}</div>
|
{% if quantity != None %}
|
||||||
</div>
|
<div class="row-start-1 col-start-4 col-span-1 self-start ms-auto leading-tight relative w-fit ps-1">
|
||||||
{% endif %}
|
<div class="card-quantity-badge relative bg-gray-600 text-white text-sm font-semibold rounded-full text-center size-max px-1.5">{{ quantity }}</div>
|
||||||
<div class="row-start-2 col-start-1 col-span-3 truncate self-end text-xs text-transparent">{{ rarity }}</div>
|
</div>
|
||||||
<div class="row-start-2 col-start-4 col-span-1 self-end text-right truncate font-semibold leading-tight text-sm">{{ cardset }}</div>
|
{% endif %}
|
||||||
</div>
|
<div class="row-start-2 col-start-1 col-span-3 truncate self-end text-xs text-transparent">{{ rarity }}</div>
|
||||||
{% endif %}
|
<div class="row-start-2 col-start-4 col-span-1 self-end text-right truncate font-semibold leading-tight text-sm">{{ cardset }}</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endcache %}
|
||||||
|
|
@ -1,22 +1,16 @@
|
||||||
{% load cache card_badge %}
|
{% load cache card_badge %}
|
||||||
|
{% load cache card_multiselect %}
|
||||||
|
|
||||||
<label for="{{ field_id }}" class="label">
|
<label for="{{ field_id }}" class="label">
|
||||||
<span class="label-text">{{ label }}</span>
|
<span class="label-text">{{ label }}</span>
|
||||||
</label>
|
</label>
|
||||||
<select name="{{ field_name }}" id="{{ field_id }}" class="select select-bordered w-full card-multiselect" data-placeholder="{{ placeholder }}" multiple x-cloak>
|
<select name="{{ field_name }}" id="{{ field_id }}" class="select select-bordered w-full card-multiselect" data-placeholder="{{ placeholder }}" multiple x-cloak>
|
||||||
{% cache cache_timeout card_multiselect selected_values|join:"," %}
|
{% cache CACHE_TIMEOUT card_multiselect field_name label placeholder passed_cards_identifier selected_cards_key_part %}
|
||||||
<option value="" disabled>{{ placeholder }}</option>
|
{% if has_passed_cards %}
|
||||||
{% for card in cards %}
|
{% include "templatetags/_card_multiselect_options.html" with cards_to_render=passed_cards selected_cards=selected_cards placeholder=placeholder %}
|
||||||
<option
|
{% else %}
|
||||||
value="{{ card.pk }}"
|
{% fetch_all_cards as all_db_cards %}
|
||||||
data-card-id="{{ card.pk }}"
|
{% include "templatetags/_card_multiselect_options.html" with cards_to_render=all_db_cards selected_cards=selected_cards placeholder=placeholder %}
|
||||||
data-quantity="{{ card.selected_quantity }}"
|
{% endif %}
|
||||||
{% if card.selected %}selected{% endif %}
|
|
||||||
data-html-content='<div class="m-2">{{ card|card_badge_inline:"__QUANTITY__" }}</div>'
|
|
||||||
data-name="{{ card.name }}"
|
|
||||||
data-rarity="{{ card.rarity_icon }}"
|
|
||||||
data-cardset="{{ card.cardset }}">
|
|
||||||
{{ card.name }} {{ card.rarity_icon }} {{ card.cardset }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
{% endcache %}
|
{% endcache %}
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -1,58 +1,60 @@
|
||||||
{% load gravatar card_badge %}
|
{% load gravatar card_badge %}
|
||||||
|
|
||||||
<div class="card card-border bg-base-100 shadow-lg max-w-90 mx-auto">
|
{% cache CACHE_TIMEOUT trade_acceptance cache_key %}
|
||||||
<!-- Header -->
|
<div class="card card-border bg-base-100 shadow-lg max-w-90 mx-auto">
|
||||||
<a href="{% url 'trade_acceptance_update' pk=acceptance.pk %}">
|
<!-- Header -->
|
||||||
<div class="my-4 mx-4">
|
<a href="{% url 'trade_acceptance_update' pk=acceptance.pk %}">
|
||||||
<div class="flex justify-start items-center">
|
<div class="my-4 mx-4">
|
||||||
<!-- Left: Initiator's avatar and "Has" -->
|
<div class="flex justify-start items-center">
|
||||||
<div class="flex items-center">
|
<!-- Left: Initiator's avatar and "Has" -->
|
||||||
<div class="avatar me-2 tooltip tooltip-top cursor-default" @click.stop.prevent data-tip="{{ acceptance.trade_offer.initiated_by.user.username }} | {{ acceptance.trade_offer.initiated_by.user.reputation_score }} rep">
|
<div class="flex items-center">
|
||||||
<div class="max-w-10 rounded-full">
|
<div class="avatar me-2 tooltip tooltip-top cursor-default" @click.stop.prevent data-tip="{{ acceptance.trade_offer.initiated_by.user.username }} | {{ acceptance.trade_offer.initiated_by.user.reputation_score }} rep">
|
||||||
{{ acceptance.trade_offer.initiated_by.user.email|gravatar:40 }}
|
<div class="max-w-10 rounded-full">
|
||||||
|
{{ acceptance.trade_offer.initiated_by.user.email|gravatar:40 }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="text-gray-500 text-xs">
|
||||||
<div class="text-gray-500 text-xs">
|
{% if acceptance.is_initiator_state %}
|
||||||
{% if acceptance.is_initiator_state %}
|
{{ acceptance.action_label_2 }}
|
||||||
{{ acceptance.action_label_2 }}
|
{% else %}
|
||||||
{% else %}
|
Waiting on {{ acceptance.trade_offer.initiated_by.user.username }} to {{ acceptance.next_action_label }}...
|
||||||
Waiting on {{ acceptance.trade_offer.initiated_by.user.username }} to {{ acceptance.next_action_label }}...
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<!-- Main Card Row: Single row with the acceptance's cards -->
|
|
||||||
<div class="px-2 pb-0">
|
|
||||||
<div class="grid grid-cols-2 items-center gap-2">
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
{% card_badge acceptance.requested_card %}
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
{% card_badge acceptance.offered_card %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="{% url 'trade_acceptance_update' pk=acceptance.pk %}">
|
|
||||||
<div class="my-4 mx-4">
|
|
||||||
<div class="flex justify-end items-center">
|
|
||||||
<!-- Right: "Wants" with the acceptor's avatar -->
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="text-gray-500 text-xs text-right">
|
|
||||||
{% if acceptance.is_acceptor_state %}
|
|
||||||
{{ acceptance.action_label_2 }}
|
|
||||||
{% else %}
|
|
||||||
Waiting on {{ acceptance.accepted_by.user.username }} to {{ acceptance.next_action_label }}...
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="avatar ms-2 tooltip tooltip-bottom cursor-default" @click.stop.prevent data-tip="{{ acceptance.accepted_by.user.username}} | {{ acceptance.accepted_by.user.reputation_score }} rep">
|
|
||||||
<div class="max-w-10 rounded-full">
|
|
||||||
{{ acceptance.accepted_by.user.email|gravatar:40 }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
|
<!-- Main Card Row: Single row with the acceptance's cards -->
|
||||||
|
<div class="px-2 pb-0">
|
||||||
|
<div class="grid grid-cols-2 items-center gap-2">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
{% card_badge acceptance.requested_card %}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
{% card_badge acceptance.offered_card %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
<a href="{% url 'trade_acceptance_update' pk=acceptance.pk %}">
|
||||||
</div>
|
<div class="my-4 mx-4">
|
||||||
|
<div class="flex justify-end items-center">
|
||||||
|
<!-- Right: "Wants" with the acceptor's avatar -->
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="text-gray-500 text-xs text-right">
|
||||||
|
{% if acceptance.is_acceptor_state %}
|
||||||
|
{{ acceptance.action_label_2 }}
|
||||||
|
{% else %}
|
||||||
|
Waiting on {{ acceptance.accepted_by.user.username }} to {{ acceptance.next_action_label }}...
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="avatar ms-2 tooltip tooltip-bottom cursor-default" @click.stop.prevent data-tip="{{ acceptance.accepted_by.user.username}} | {{ acceptance.accepted_by.user.reputation_score }} rep">
|
||||||
|
<div class="max-w-10 rounded-full">
|
||||||
|
{{ acceptance.accepted_by.user.email|gravatar:40 }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endcache %}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{% load gravatar card_badge cache %}
|
{% load gravatar card_badge cache %}
|
||||||
|
|
||||||
{% cache 60 trade_offer offer_pk %}
|
{% cache CACHE_TIMEOUT trade_offer cache_key %}
|
||||||
<div x-data="{ flipped: {{flipped|lower}}, offerExpanded: {{flipped|yesno:'false,true'}}, acceptanceExpanded: {{flipped|lower}} }" x-ref="tradeOffer" class="transition-all duration-500 trade-offer-card">
|
<div x-data="{ flipped: {{flipped|lower}}, offerExpanded: {{flipped|yesno:'false,true'}}, acceptanceExpanded: {{flipped|lower}} }" x-ref="tradeOffer" class="transition-all duration-500 trade-offer-card">
|
||||||
<div class="flip-container">
|
<div class="flip-container">
|
||||||
<div class="flip-inner grid grid-cols-1 grid-rows-1 card bg-base-100 card-border shadow-lg w-90 transform transition-transform duration-500 ease-in-out{%if flipped %} rotate-y-180{% endif %}"
|
<div class="flip-inner grid grid-cols-1 grid-rows-1 card bg-base-100 card-border shadow-lg w-90 transform transition-transform duration-500 ease-in-out{%if flipped %} rotate-y-180{% endif %}"
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,5 @@ class TradesConfig(AppConfig):
|
||||||
name = "trades"
|
name = "trades"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
# This import registers the signal handlers defined in trades/signals.py,
|
# Implicitly connect signal handlers decorated with @receiver.
|
||||||
# ensuring that denormalized field updates occur whenever related objects change.
|
|
||||||
import trades.signals
|
import trades.signals
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from django.db.models.signals import post_save, post_delete, pre_save
|
from django.db.models.signals import post_save, post_delete, pre_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from trades.models import TradeOfferHaveCard, TradeOfferWantCard, TradeAcceptance
|
from trades.models import TradeOfferHaveCard, TradeOfferWantCard, TradeAcceptance, TradeOffer
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from accounts.models import CustomUser
|
from accounts.models import CustomUser
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
@ -12,6 +12,8 @@ from django.core.mail import send_mail
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.contrib.sites.models import Site
|
from django.contrib.sites.models import Site
|
||||||
|
from django.core.cache import cache
|
||||||
|
import logging
|
||||||
|
|
||||||
POSITIVE_STATES = [
|
POSITIVE_STATES = [
|
||||||
TradeAcceptance.AcceptanceState.ACCEPTED,
|
TradeAcceptance.AcceptanceState.ACCEPTED,
|
||||||
|
|
@ -178,7 +180,7 @@ def trade_acceptance_reputation_update(sender, instance, created, **kwargs):
|
||||||
state transitions for TradeAcceptance.
|
state transitions for TradeAcceptance.
|
||||||
|
|
||||||
- THANKED_BY_BOTH: both the initiator and the acceptor receive +1 when transitioning
|
- THANKED_BY_BOTH: both the initiator and the acceptor receive +1 when transitioning
|
||||||
into this state, and -1 when leaving it.
|
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_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).
|
- REJECTED_BY_ACCEPTOR: only the initiator gets -1 when transitioning into it (and +1 when leaving it).
|
||||||
|
|
||||||
|
|
@ -259,4 +261,18 @@ def trade_acceptance_reputation_delete(sender, instance, **kwargs):
|
||||||
if instance.state == TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR:
|
if instance.state == TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR:
|
||||||
CustomUser.objects.filter(pk=instance.trade_offer.initiated_by.user.pk).update(
|
CustomUser.objects.filter(pk=instance.trade_offer.initiated_by.user.pk).update(
|
||||||
reputation_score=F("reputation_score") + 1
|
reputation_score=F("reputation_score") + 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@receiver(post_save, sender=TradeOfferHaveCard)
|
||||||
|
@receiver(post_delete, sender=TradeOfferHaveCard)
|
||||||
|
@receiver(post_save, sender=TradeOfferWantCard)
|
||||||
|
@receiver(post_delete, sender=TradeOfferWantCard)
|
||||||
|
@receiver(post_save, sender=TradeAcceptance)
|
||||||
|
@receiver(post_delete, sender=TradeAcceptance)
|
||||||
|
def bubble_up_trade_offer_updates(sender, instance, **kwargs):
|
||||||
|
"""
|
||||||
|
Bubble up updates to the TradeOffer model when TradeOfferHaveCard, TradeOfferWantCard,
|
||||||
|
or TradeAcceptance instances are created, updated, or deleted.
|
||||||
|
"""
|
||||||
|
if instance.trade_offer:
|
||||||
|
instance.trade_offer.save(update_fields=['updated_at'])
|
||||||
|
|
@ -9,8 +9,10 @@ def render_trade_offer(context, offer):
|
||||||
Renders a trade offer including detailed trade acceptance information.
|
Renders a trade offer including detailed trade acceptance information.
|
||||||
Freezes the through-model querysets to avoid extra DB hits.
|
Freezes the through-model querysets to avoid extra DB hits.
|
||||||
"""
|
"""
|
||||||
trade_offer_have_cards = list(offer.trade_offer_have_cards.all())
|
trade_offer_have_cards = list(offer.trade_offer_have_cards.select_related('card').all())
|
||||||
trade_offer_want_cards = list(offer.trade_offer_want_cards.all())
|
trade_offer_want_cards = list(offer.trade_offer_want_cards.select_related('card').all())
|
||||||
|
acceptances = list(offer.acceptances.select_related('accepted_by__user', 'requested_card', 'offered_card').all())
|
||||||
|
|
||||||
|
|
||||||
have_cards_available = [
|
have_cards_available = [
|
||||||
card for card in trade_offer_have_cards
|
card for card in trade_offer_have_cards
|
||||||
|
|
@ -21,18 +23,14 @@ def render_trade_offer(context, offer):
|
||||||
if card.quantity > card.qty_accepted
|
if card.quantity > card.qty_accepted
|
||||||
]
|
]
|
||||||
|
|
||||||
acceptances = list(offer.acceptances.all())
|
|
||||||
|
|
||||||
# Determine if the offer should show its back side (acceptances view) by default.
|
|
||||||
# If either side has no available cards, then flip the offer.
|
|
||||||
if not have_cards_available or not want_cards_available:
|
if not have_cards_available or not want_cards_available:
|
||||||
flipped = True
|
flipped = True
|
||||||
else:
|
else:
|
||||||
flipped = False
|
flipped = False
|
||||||
|
|
||||||
return {
|
tag_context = {
|
||||||
'offer_pk': offer.pk,
|
'offer_pk': offer.pk,
|
||||||
'flipped': flipped, # new flag to control the default face
|
'flipped': flipped,
|
||||||
'offer_hash': offer.hash,
|
'offer_hash': offer.hash,
|
||||||
'rarity_icon': offer.rarity_icon,
|
'rarity_icon': offer.rarity_icon,
|
||||||
'initiated_by_email': offer.initiated_by.user.email,
|
'initiated_by_email': offer.initiated_by.user.email,
|
||||||
|
|
@ -43,19 +41,25 @@ def render_trade_offer(context, offer):
|
||||||
'want_cards_available': want_cards_available,
|
'want_cards_available': want_cards_available,
|
||||||
'num_cards_available': len(have_cards_available) + len(want_cards_available),
|
'num_cards_available': len(have_cards_available) + len(want_cards_available),
|
||||||
'on_detail_page': context.get("request").path.endswith("trades/"+str(offer.pk)+"/"),
|
'on_detail_page': context.get("request").path.endswith("trades/"+str(offer.pk)+"/"),
|
||||||
|
'cache_key': f'trade_offer_{offer.pk}_{offer.updated_at.timestamp()}_{flipped}',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context.update(tag_context)
|
||||||
|
return context
|
||||||
|
|
||||||
@register.inclusion_tag('templatetags/trade_acceptance.html', takes_context=True)
|
@register.inclusion_tag('templatetags/trade_acceptance.html', takes_context=True)
|
||||||
def render_trade_acceptance(context, acceptance):
|
def render_trade_acceptance(context, acceptance):
|
||||||
"""
|
"""
|
||||||
Renders a simple trade acceptance view with a single row and simplified header/footer.
|
Renders a simple trade acceptance view with a single row and simplified header/footer.
|
||||||
"""
|
"""
|
||||||
|
tag_context = {
|
||||||
return {
|
|
||||||
"acceptance": acceptance,
|
"acceptance": acceptance,
|
||||||
"request": context.get("request"),
|
'cache_key': f'trade_acceptance_{acceptance.pk}_{acceptance.updated_at.timestamp()}',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context.update(tag_context)
|
||||||
|
return context
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def get_action_label(acceptance, state_value):
|
def get_action_label(acceptance, state_value):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue