Refactor database models to majorly increase queries needed and decrease load times of home from 30 secs to 5 sec (we will be caching the rest to decrease even further via background tasks)
This commit is contained in:
parent
f7a9b2f823
commit
86c7eba10a
25 changed files with 1941 additions and 1560 deletions
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 5.1.2 on 2025-03-16 18:18
|
||||
# Generated by Django 5.1.2 on 2025-03-17 20:39
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
from django.contrib import admin
|
||||
from .models import CardSet, Deck, Card, Rarity, DeckNameTranslation, CardNameTranslation
|
||||
from .models import Deck, Card, DeckNameTranslation, CardNameTranslation
|
||||
|
||||
admin.site.register(CardSet)
|
||||
admin.site.register(Deck)
|
||||
admin.site.register(Card)
|
||||
admin.site.register(Rarity)
|
||||
admin.site.register(DeckNameTranslation)
|
||||
admin.site.register(CardNameTranslation)
|
||||
|
|
@ -3,3 +3,6 @@ from django.apps import AppConfig
|
|||
|
||||
class CardsConfig(AppConfig):
|
||||
name = "cards"
|
||||
|
||||
def ready(self):
|
||||
import cards.signals
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 5.1.2 on 2025-03-16 18:18
|
||||
# Generated by Django 5.1.2 on 2025-03-17 20:39
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
|
@ -16,28 +16,23 @@ class Migration(migrations.Migration):
|
|||
name='Card',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=64)),
|
||||
('name', models.CharField(max_length=128)),
|
||||
('cardset', models.CharField(max_length=8)),
|
||||
('cardnum', models.IntegerField()),
|
||||
('style', models.CharField(max_length=255)),
|
||||
('rarity_icon', models.CharField(max_length=8)),
|
||||
('rarity_level', models.IntegerField()),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CardSet',
|
||||
name='Deck',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=64)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Rarity',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('normalized_id', models.IntegerField()),
|
||||
('name', models.CharField(max_length=64)),
|
||||
('icons', models.CharField(max_length=64)),
|
||||
('hex_color', models.CharField(max_length=9)),
|
||||
('cardset', models.CharField(max_length=8)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
|
|
@ -53,22 +48,6 @@ class Migration(migrations.Migration):
|
|||
('card', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='name_translations', to='cards.card')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='card',
|
||||
name='cardset',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='cards', to='cards.cardset'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Deck',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=64)),
|
||||
('hex_color', models.CharField(max_length=9)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('cardset', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='decks', to='cards.cardset')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='card',
|
||||
name='decks',
|
||||
|
|
@ -85,20 +64,4 @@ class Migration(migrations.Migration):
|
|||
('deck', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='name_translations', to='cards.deck')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='card',
|
||||
name='rarity',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='cards', to='cards.rarity'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RarityNameTranslation',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=64)),
|
||||
('language', models.CharField(max_length=64)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('rarity', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='name_translations', to='cards.rarity')),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -23,91 +23,29 @@ class CardNameTranslation(models.Model):
|
|||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class RarityNameTranslation(models.Model):
|
||||
id = models.AutoField(primary_key=True)
|
||||
name = models.CharField(max_length=64)
|
||||
rarity = models.ForeignKey("Rarity", on_delete=models.PROTECT, related_name='name_translations')
|
||||
language = models.CharField(max_length=64)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class CardSet(models.Model):
|
||||
id = models.AutoField(primary_key=True)
|
||||
name = models.CharField(max_length=64)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Deck(models.Model):
|
||||
id = models.AutoField(primary_key=True)
|
||||
name = models.CharField(max_length=64)
|
||||
hex_color = models.CharField(max_length=9)
|
||||
cardset = models.ForeignKey("CardSet", on_delete=models.PROTECT, related_name='decks')
|
||||
cardset = models.CharField(max_length=8)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Rarity(models.Model):
|
||||
id = models.AutoField(primary_key=True)
|
||||
normalized_id = models.IntegerField(null=False)
|
||||
name = models.CharField(max_length=64)
|
||||
icons = models.CharField(max_length=64)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
# Custom Manager for Card model
|
||||
class CardPrefetchManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.select_related("cardset", "rarity")
|
||||
.prefetch_related(
|
||||
"decks",
|
||||
"decks__cardset",
|
||||
)
|
||||
)
|
||||
|
||||
class CardManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.select_related("cardset", "rarity")
|
||||
)
|
||||
|
||||
class Card(models.Model):
|
||||
id = models.AutoField(primary_key=True)
|
||||
name = models.CharField(max_length=64)
|
||||
name = models.CharField(max_length=128)
|
||||
decks = models.ManyToManyField("Deck")
|
||||
cardset = models.ForeignKey("CardSet", on_delete=models.PROTECT, related_name='cards')
|
||||
cardset = models.CharField(max_length=8)
|
||||
cardnum = models.IntegerField()
|
||||
rarity = models.ForeignKey(Rarity, on_delete=models.PROTECT, related_name='cards')
|
||||
style = models.CharField(max_length=255, null=False)
|
||||
rarity_icon = models.CharField(max_length=8)
|
||||
rarity_level = models.IntegerField()
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
# Use the custom manager to ensure optimized querysets everywhere.
|
||||
objects = CardPrefetchManager()
|
||||
objects_no_prefetch = CardManager()
|
||||
|
||||
def __str__(self):
|
||||
# For display, we show the original rarity icons.
|
||||
return f"{self.name} {self.rarity.icons} {self.cardset.name}"
|
||||
|
||||
@property
|
||||
def normalized_rarity(self):
|
||||
"""
|
||||
Returns the canonical rarity id for trade logic.
|
||||
"""
|
||||
return self.rarity.normalized_id
|
||||
return f"{self.name} {self.rarity_icon} {self.cardset}"
|
||||
17
cards/signals.py
Normal file
17
cards/signals.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from django.db.models.signals import m2m_changed
|
||||
from django.dispatch import receiver
|
||||
from .models import Card
|
||||
|
||||
@receiver(m2m_changed, sender=Card.decks.through)
|
||||
def update_card_style(sender, instance, action, **kwargs):
|
||||
if action == "post_add":
|
||||
decks = instance.decks.all()
|
||||
num_decks = decks.count()
|
||||
if num_decks == 1:
|
||||
instance.style = "background-color: " + decks.first().hex_color + ";"
|
||||
elif num_decks >= 2:
|
||||
hex_colors = [deck.hex_color for deck in decks]
|
||||
instance.style = f"background: linear-gradient(to right, {', '.join(hex_colors)});"
|
||||
else:
|
||||
instance.style = "background: linear-gradient(to right, #AAAAAA, #AAAAAA, #AAAAAA);"
|
||||
instance.save(update_fields=["style"])
|
||||
|
|
@ -6,13 +6,12 @@ register = template.Library()
|
|||
|
||||
@register.inclusion_tag("templatetags/card_badge.html")
|
||||
def card_badge(card, quantity=1):
|
||||
# Freeze the decks queryset once so that both the iteration and count use the same data
|
||||
decks = list(card.decks.all()) if card else []
|
||||
return {
|
||||
'card': card,
|
||||
'quantity': quantity,
|
||||
'decks': decks,
|
||||
'num_decks': len(decks),
|
||||
'style': card.style,
|
||||
'name': card.name,
|
||||
'rarity': card.rarity_icon,
|
||||
'cardset': card.cardset,
|
||||
}
|
||||
|
||||
@register.filter
|
||||
|
|
@ -21,9 +20,10 @@ def card_badge_inline(card, quantity=1):
|
|||
Renders an inline card badge.
|
||||
"""
|
||||
html = render_to_string("templatetags/card_badge.html", {
|
||||
'card': card,
|
||||
'quantity': quantity,
|
||||
'decks': list(card.decks.all()) if card else [],
|
||||
'num_decks': len(list(card.decks.all())) if card else 0,
|
||||
'style': card.style,
|
||||
'name': card.name,
|
||||
'rarity': card.rarity_icon,
|
||||
'cardset': card.cardset,
|
||||
})
|
||||
return mark_safe(html)
|
||||
|
|
@ -47,7 +47,7 @@ INSTALLED_APPS = [
|
|||
'allauth.socialaccount.providers.google',
|
||||
"crispy_forms",
|
||||
"crispy_tailwind",
|
||||
#"debug_toolbar",
|
||||
"debug_toolbar",
|
||||
"el_pagination",
|
||||
"tailwind",
|
||||
"theme",
|
||||
|
|
@ -56,7 +56,7 @@ INSTALLED_APPS = [
|
|||
"cards",
|
||||
"home",
|
||||
"trades.apps.TradesConfig",
|
||||
"silk",
|
||||
#"silk",
|
||||
]
|
||||
|
||||
SILKY_PYTHON_PROFILER = True
|
||||
|
|
@ -73,13 +73,13 @@ MIDDLEWARE = [
|
|||
"whitenoise.middleware.WhiteNoiseMiddleware", # WhiteNoise
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
#"debug_toolbar.middleware.DebugToolbarMiddleware", # Django Debug Toolbar
|
||||
"debug_toolbar.middleware.DebugToolbarMiddleware", # Django Debug Toolbar
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"allauth.account.middleware.AccountMiddleware", # django-allauth
|
||||
'silk.middleware.SilkyMiddleware',
|
||||
#'silk.middleware.SilkyMiddleware',
|
||||
"django_browser_reload.middleware.BrowserReloadMiddleware",
|
||||
#"django_project.middleware.AutoLoginMiddleware",
|
||||
]
|
||||
|
|
@ -299,6 +299,6 @@ else:
|
|||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
|
||||
"LOCATION": "django_site_cache",
|
||||
"LOCATION": "site-cache",
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ urlpatterns = [
|
|||
path('account/', include('accounts.urls')),
|
||||
path("trades/", include("trades.urls")),
|
||||
path("__reload__/", include("django_browser_reload.urls")),
|
||||
path('silk/', include('silk.urls', namespace='silk')),
|
||||
#path('silk/', include('silk.urls', namespace='silk')),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
from collections import defaultdict
|
||||
from collections import defaultdict, OrderedDict
|
||||
from django.views.generic import TemplateView
|
||||
from django.urls import reverse_lazy
|
||||
from django.db.models import Count, Q, Prefetch, Sum, F, IntegerField, Value, BooleanField, Case, When
|
||||
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||
from trades.models import TradeOffer, TradeAcceptance, TradeOfferHaveCard, TradeOfferWantCard
|
||||
from cards.models import Card, CardSet, Rarity
|
||||
from cards.models import Card
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_page
|
||||
from django.template.response import TemplateResponse
|
||||
from django.http import HttpResponseRedirect
|
||||
from silk.profiling.profiler import silk_profile
|
||||
#from silk.profiling.profiler import silk_profile
|
||||
|
||||
class HomePageView(TemplateView):
|
||||
template_name = "home/home.html"
|
||||
|
||||
@silk_profile(name='Home Page')
|
||||
#@silk_profile(name='Home Page')
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context["cards"] = Card.objects.all().order_by("name", "rarity__pk")
|
||||
context["cards"] = Card.objects.all().order_by("name")
|
||||
|
||||
# Reuse base trade offer queryset for market stats
|
||||
base_offer_qs = TradeOffer.objects.filter(is_closed=False)
|
||||
|
|
@ -29,46 +29,44 @@ class HomePageView(TemplateView):
|
|||
|
||||
# Most Offered Cards
|
||||
context["most_offered_cards"] = (
|
||||
Card.objects_no_prefetch.filter(tradeofferhavecard__isnull=False)
|
||||
Card.objects.filter(tradeofferhavecard__isnull=False)
|
||||
.annotate(offer_count=Sum("tradeofferhavecard__quantity"))
|
||||
.order_by("-offer_count")[:6]
|
||||
)
|
||||
|
||||
# Most Wanted Cards
|
||||
context["most_wanted_cards"] = (
|
||||
Card.objects_no_prefetch.filter(tradeofferwantcard__isnull=False)
|
||||
Card.objects.filter(tradeofferwantcard__isnull=False)
|
||||
.annotate(offer_count=Sum("tradeofferwantcard__quantity"))
|
||||
.order_by("-offer_count")[:6]
|
||||
)
|
||||
|
||||
# Least Offered Cards
|
||||
context["least_offered_cards"] = (
|
||||
Card.objects_no_prefetch.annotate(offer_count=Sum("tradeofferhavecard__quantity"))
|
||||
Card.objects.annotate(offer_count=Sum("tradeofferhavecard__quantity"))
|
||||
.order_by("offer_count")[:6]
|
||||
)
|
||||
|
||||
featured = {}
|
||||
# Featured "All" offers
|
||||
# Build featured offers with custom ordering
|
||||
featured = OrderedDict()
|
||||
# Featured "All" offers remains fixed at the top
|
||||
featured["All"] = base_offer_qs.order_by("created_at")[:6]
|
||||
|
||||
# Get the normalized ids for rarities with pk<=5.
|
||||
normalized_ids = list(
|
||||
Rarity.objects.filter(pk__lte=5).values_list("normalized_id", flat=True).distinct()
|
||||
)
|
||||
|
||||
rarity_map = {
|
||||
rarity.normalized_id: rarity.icons
|
||||
for rarity in Rarity.objects.filter(pk__lte=5)
|
||||
}
|
||||
|
||||
# For each normalized id (sorted descending), filter base offers that have the matching trade offer rarity.
|
||||
for norm in sorted(normalized_ids, reverse=True):
|
||||
offers_qs = base_offer_qs.filter(
|
||||
rarity__normalized_id=norm # now using trade_offer.rarity
|
||||
).order_by("created_at").distinct()[:6]
|
||||
icon_label = rarity_map.get(norm)
|
||||
if icon_label:
|
||||
featured[icon_label] = offers_qs
|
||||
# Pull out distinct (rarity_level, rarity_icon) tuples
|
||||
distinct_rarities = base_offer_qs.values_list("rarity_level", "rarity_icon").distinct()
|
||||
|
||||
# Prepare a list that holds tuples of (rarity_level, rarity_icon, offers)
|
||||
rarity_offers = []
|
||||
for rarity_level, rarity_icon in distinct_rarities:
|
||||
offers = base_offer_qs.filter(rarity_level=rarity_level).order_by("created_at")[:6]
|
||||
rarity_offers.append((rarity_level, rarity_icon, offers))
|
||||
|
||||
# Sort by rarity_level (from greatest to least)
|
||||
rarity_offers.sort(key=lambda x: x[0], reverse=True)
|
||||
|
||||
# Add the sorted offers to the OrderedDict
|
||||
for rarity_level, rarity_icon, offers in rarity_offers:
|
||||
featured[rarity_icon] = offers
|
||||
|
||||
context["featured_offers"] = featured
|
||||
|
||||
|
|
|
|||
|
|
@ -1,112 +0,0 @@
|
|||
[
|
||||
{
|
||||
"model": "cards.rarity",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"icons": "🔷",
|
||||
"normalized_id": 1,
|
||||
"name": "Common",
|
||||
"created_at": "2025-02-16T06:54:40.993Z",
|
||||
"updated_at": "2025-02-16T06:54:40.993Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "cards.rarity",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"icons": "🔷🔷",
|
||||
"normalized_id": 2,
|
||||
"name": "Uncommon",
|
||||
"created_at": "2025-02-16T06:54:44.213Z",
|
||||
"updated_at": "2025-02-16T06:54:44.213Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "cards.rarity",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"icons": "🔷🔷🔷",
|
||||
"normalized_id": 3,
|
||||
"name": "Rare",
|
||||
"created_at": "2025-02-16T06:54:47.297Z",
|
||||
"updated_at": "2025-02-16T06:54:47.297Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "cards.rarity",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"icons": "🔷🔷🔷🔷",
|
||||
"normalized_id": 4,
|
||||
"name": "Double Rare",
|
||||
"created_at": "2025-02-16T06:54:50.363Z",
|
||||
"updated_at": "2025-02-16T06:54:50.363Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "cards.rarity",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"icons": "⭐️",
|
||||
"normalized_id": 5,
|
||||
"name": "Full Art Rare",
|
||||
"created_at": "2025-02-16T06:54:59.888Z",
|
||||
"updated_at": "2025-02-16T06:54:59.888Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "cards.rarity",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"icons": "⭐️⭐️",
|
||||
"normalized_id": 6,
|
||||
"name": "Super Rare",
|
||||
"created_at": "2025-02-16T06:55:02.853Z",
|
||||
"updated_at": "2025-02-16T06:55:02.853Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "cards.rarity",
|
||||
"pk": 7,
|
||||
"fields": {
|
||||
"icons": "🌟🌟",
|
||||
"normalized_id": 6,
|
||||
"name": "Special Art Rare",
|
||||
"created_at": "2025-02-16T06:55:02.853Z",
|
||||
"updated_at": "2025-02-16T06:55:02.853Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "cards.rarity",
|
||||
"pk": 8,
|
||||
"fields": {
|
||||
"icons": "⭐️⭐️⭐️",
|
||||
"normalized_id": 7,
|
||||
"name": "Immersive Rare",
|
||||
"created_at": "2025-02-16T06:55:05.728Z",
|
||||
"updated_at": "2025-02-16T06:55:05.728Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "cards.rarity",
|
||||
"pk": 9,
|
||||
"fields": {
|
||||
"icons": "👑",
|
||||
"normalized_id": 8,
|
||||
"name": "Crown Rare",
|
||||
"created_at": "2025-02-16T06:55:13.907Z",
|
||||
"updated_at": "2025-02-16T06:55:13.907Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "cards.rarity",
|
||||
"pk": 10,
|
||||
"fields": {
|
||||
"icons": "🅿️",
|
||||
"normalized_id": 9,
|
||||
"name": "Promo",
|
||||
"created_at": "2025-02-16T06:55:13.907Z",
|
||||
"updated_at": "2025-02-16T06:55:13.907Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
[
|
||||
{
|
||||
"model": "cards.cardset",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Promo-A",
|
||||
"created_at": "2025-02-16T07:54:38.986Z",
|
||||
"updated_at": "2025-02-16T07:54:38.986Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "cards.cardset",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "A1",
|
||||
"created_at": "2025-02-16T07:54:04.325Z",
|
||||
"updated_at": "2025-02-16T07:54:04.325Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "cards.cardset",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "A1a",
|
||||
"created_at": "2025-02-16T07:54:08.471Z",
|
||||
"updated_at": "2025-02-16T07:54:08.471Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "cards.cardset",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "A2",
|
||||
"created_at": "2025-02-16T07:54:11.435Z",
|
||||
"updated_at": "2025-02-16T07:54:11.435Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Promo-A",
|
||||
"cardset": 1,
|
||||
"cardset": "Promo-A",
|
||||
"hex_color": "#1070EB",
|
||||
"created_at": "2025-02-16T07:55:34.988Z",
|
||||
"updated_at": "2025-02-16T07:55:34.988Z"
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Genetic Apex: Mewtwo",
|
||||
"cardset": 2,
|
||||
"cardset": "A1",
|
||||
"hex_color": "#8040E0",
|
||||
"created_at": "2025-02-16T07:54:57.445Z",
|
||||
"updated_at": "2025-02-16T07:54:57.445Z"
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "Genetic Apex: Charizard",
|
||||
"cardset": 2,
|
||||
"cardset": "A1",
|
||||
"hex_color": "#E00202",
|
||||
"created_at": "2025-02-16T07:54:52.381Z",
|
||||
"updated_at": "2025-02-16T07:54:52.381Z"
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "Genetic Apex: Pikachu",
|
||||
"cardset": 2,
|
||||
"cardset": "A1",
|
||||
"hex_color": "#EB8600",
|
||||
"created_at": "2025-02-16T07:55:05.097Z",
|
||||
"updated_at": "2025-02-16T07:55:05.097Z"
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "Mythical Island",
|
||||
"cardset": 3,
|
||||
"cardset": "A1a",
|
||||
"hex_color": "#20AA80",
|
||||
"created_at": "2025-02-16T07:55:11.916Z",
|
||||
"updated_at": "2025-02-16T07:55:11.916Z"
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
"pk": 6,
|
||||
"fields": {
|
||||
"name": "Space-Time Smackdown: Dialga",
|
||||
"cardset": 4,
|
||||
"cardset": "A2",
|
||||
"hex_color": "#302FD9",
|
||||
"created_at": "2025-02-16T07:55:17.582Z",
|
||||
"updated_at": "2025-02-16T07:55:17.582Z"
|
||||
|
|
@ -70,7 +70,7 @@
|
|||
"pk": 7,
|
||||
"fields": {
|
||||
"name": "Space-Time Smackdown: Palkia",
|
||||
"cardset": 4,
|
||||
"cardset": "A2",
|
||||
"hex_color": "#CF36E0",
|
||||
"created_at": "2025-02-16T07:55:27.503Z",
|
||||
"updated_at": "2025-02-16T07:55:27.503Z"
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -6,6 +6,8 @@
|
|||
"is_closed": false,
|
||||
"hash": "8775ce1cz",
|
||||
"initiated_by": 1,
|
||||
"rarity_icon": "🔷🔷",
|
||||
"rarity_level": 2,
|
||||
"created_at": "2025-03-13T04:38:41.385Z",
|
||||
"updated_at": "2025-03-13T04:38:41.385Z"
|
||||
}
|
||||
|
|
@ -17,6 +19,8 @@
|
|||
"is_closed": false,
|
||||
"hash": "daa6300dz",
|
||||
"initiated_by": 1,
|
||||
"rarity_icon": "🔷",
|
||||
"rarity_level": 1,
|
||||
"created_at": "2025-03-13T04:39:25.777Z",
|
||||
"updated_at": "2025-03-13T04:39:25.777Z"
|
||||
}
|
||||
|
|
@ -28,6 +32,8 @@
|
|||
"is_closed": false,
|
||||
"hash": "e6cdbdf8z",
|
||||
"initiated_by": 1,
|
||||
"rarity_icon": "🔷🔷🔷",
|
||||
"rarity_level": 3,
|
||||
"created_at": "2025-03-13T04:40:07.727Z",
|
||||
"updated_at": "2025-03-13T04:40:07.727Z"
|
||||
}
|
||||
|
|
@ -39,6 +45,8 @@
|
|||
"is_closed": false,
|
||||
"hash": "a975713ez",
|
||||
"initiated_by": 1,
|
||||
"rarity_icon": "🔷🔷🔷🔷",
|
||||
"rarity_level": 4,
|
||||
"created_at": "2025-03-13T04:40:29.957Z",
|
||||
"updated_at": "2025-03-13T04:40:29.957Z"
|
||||
}
|
||||
|
|
@ -50,6 +58,8 @@
|
|||
"is_closed": false,
|
||||
"hash": "37dc0786z",
|
||||
"initiated_by": 1,
|
||||
"rarity_icon": "⭐️",
|
||||
"rarity_level": 5,
|
||||
"created_at": "2025-03-13T04:41:00.359Z",
|
||||
"updated_at": "2025-03-13T04:41:00.359Z"
|
||||
}
|
||||
|
|
@ -61,6 +71,8 @@
|
|||
"is_closed": false,
|
||||
"hash": "f10208bdz",
|
||||
"initiated_by": 1,
|
||||
"rarity_icon": "🔷",
|
||||
"rarity_level": 1,
|
||||
"created_at": "2025-03-13T04:41:31.231Z",
|
||||
"updated_at": "2025-03-13T04:41:31.231Z"
|
||||
}
|
||||
|
|
@ -72,6 +84,8 @@
|
|||
"is_closed": false,
|
||||
"hash": "88963192z",
|
||||
"initiated_by": 1,
|
||||
"rarity_icon": "🔷🔷",
|
||||
"rarity_level": 2,
|
||||
"created_at": "2025-03-13T04:43:07.737Z",
|
||||
"updated_at": "2025-03-13T04:43:07.737Z"
|
||||
}
|
||||
|
|
@ -83,6 +97,8 @@
|
|||
"is_closed": false,
|
||||
"hash": "31a7aee8z",
|
||||
"initiated_by": 1,
|
||||
"rarity_icon": "🔷🔷🔷",
|
||||
"rarity_level": 3,
|
||||
"created_at": "2025-03-13T04:44:05.193Z",
|
||||
"updated_at": "2025-03-13T04:44:05.193Z"
|
||||
}
|
||||
|
|
@ -94,6 +110,8 @@
|
|||
"is_closed": false,
|
||||
"hash": "7a79e1f7z",
|
||||
"initiated_by": 1,
|
||||
"rarity_icon": "🔷🔷🔷🔷",
|
||||
"rarity_level": 4,
|
||||
"created_at": "2025-03-13T04:44:35.634Z",
|
||||
"updated_at": "2025-03-13T04:44:35.634Z"
|
||||
}
|
||||
|
|
@ -105,6 +123,8 @@
|
|||
"is_closed": false,
|
||||
"hash": "a465a255z",
|
||||
"initiated_by": 1,
|
||||
"rarity_icon": "⭐️",
|
||||
"rarity_level": 5,
|
||||
"created_at": "2025-03-13T04:45:02.040Z",
|
||||
"updated_at": "2025-03-13T04:45:02.040Z"
|
||||
}
|
||||
|
|
@ -116,6 +136,8 @@
|
|||
"is_closed": false,
|
||||
"hash": "9d871edbz",
|
||||
"initiated_by": 1,
|
||||
"rarity_icon": "🔷🔷🔷🔷",
|
||||
"rarity_level": 4,
|
||||
"created_at": "2025-03-13T04:45:34.815Z",
|
||||
"updated_at": "2025-03-13T04:45:34.815Z"
|
||||
}
|
||||
|
|
@ -127,6 +149,8 @@
|
|||
"is_closed": false,
|
||||
"hash": "32b34a89z",
|
||||
"initiated_by": 2,
|
||||
"rarity_icon": "🔷",
|
||||
"rarity_level": 1,
|
||||
"created_at": "2025-03-13T04:54:17.809Z",
|
||||
"updated_at": "2025-03-13T04:54:17.809Z"
|
||||
}
|
||||
|
|
@ -138,6 +162,8 @@
|
|||
"is_closed": false,
|
||||
"hash": "f747edbdz",
|
||||
"initiated_by": 2,
|
||||
"rarity_icon": "🔷",
|
||||
"rarity_level": 1,
|
||||
"created_at": "2025-03-13T04:55:33.344Z",
|
||||
"updated_at": "2025-03-13T04:55:33.344Z"
|
||||
}
|
||||
|
|
@ -149,6 +175,8 @@
|
|||
"is_closed": false,
|
||||
"hash": "9a13333dz",
|
||||
"initiated_by": 2,
|
||||
"rarity_icon": "🔷",
|
||||
"rarity_level": 1,
|
||||
"created_at": "2025-03-13T04:58:02.062Z",
|
||||
"updated_at": "2025-03-13T04:58:02.062Z"
|
||||
}
|
||||
|
|
@ -160,6 +188,8 @@
|
|||
"is_closed": false,
|
||||
"hash": "5b0d6871z",
|
||||
"initiated_by": 2,
|
||||
"rarity_icon": "🔷",
|
||||
"rarity_level": 1,
|
||||
"created_at": "2025-03-13T04:59:11.177Z",
|
||||
"updated_at": "2025-03-13T04:59:11.177Z"
|
||||
}
|
||||
|
|
@ -171,6 +201,8 @@
|
|||
"is_closed": false,
|
||||
"hash": "f012360cz",
|
||||
"initiated_by": 2,
|
||||
"rarity_icon": "🔷🔷",
|
||||
"rarity_level": 2,
|
||||
"created_at": "2025-03-13T05:00:49.530Z",
|
||||
"updated_at": "2025-03-13T05:00:49.530Z"
|
||||
}
|
||||
|
|
@ -182,6 +214,8 @@
|
|||
"is_closed": false,
|
||||
"hash": "a6e927eaz",
|
||||
"initiated_by": 2,
|
||||
"rarity_icon": "🔷🔷",
|
||||
"rarity_level": 2,
|
||||
"created_at": "2025-03-13T05:00:53.037Z",
|
||||
"updated_at": "2025-03-13T05:00:53.037Z"
|
||||
}
|
||||
|
|
@ -193,6 +227,8 @@
|
|||
"is_closed": false,
|
||||
"hash": "a5ec89b7z",
|
||||
"initiated_by": 2,
|
||||
"rarity_icon": "🔷🔷",
|
||||
"rarity_level": 2,
|
||||
"created_at": "2025-03-13T05:02:36.926Z",
|
||||
"updated_at": "2025-03-13T05:02:36.926Z"
|
||||
}
|
||||
|
|
@ -204,6 +240,8 @@
|
|||
"is_closed": false,
|
||||
"hash": "ebf6a095z",
|
||||
"initiated_by": 2,
|
||||
"rarity_icon": "🔷🔷",
|
||||
"rarity_level": 2,
|
||||
"created_at": "2025-03-13T05:03:39.241Z",
|
||||
"updated_at": "2025-03-13T05:03:39.241Z"
|
||||
}
|
||||
|
|
@ -215,6 +253,8 @@
|
|||
"is_closed": false,
|
||||
"hash": "c7541b41z",
|
||||
"initiated_by": 2,
|
||||
"rarity_icon": "🔷🔷",
|
||||
"rarity_level": 2,
|
||||
"created_at": "2025-03-13T05:05:22.304Z",
|
||||
"updated_at": "2025-03-13T05:05:22.304Z"
|
||||
}
|
||||
|
|
@ -226,6 +266,8 @@
|
|||
"is_closed": false,
|
||||
"hash": "2b97019dz",
|
||||
"initiated_by": 2,
|
||||
"rarity_icon": "🔷🔷",
|
||||
"rarity_level": 2,
|
||||
"created_at": "2025-03-13T05:08:31.437Z",
|
||||
"updated_at": "2025-03-13T05:08:31.437Z"
|
||||
}
|
||||
|
|
@ -237,6 +279,8 @@
|
|||
"is_closed": false,
|
||||
"hash": "5d90ca78z",
|
||||
"initiated_by": 2,
|
||||
"rarity_icon": "🔷🔷",
|
||||
"rarity_level": 2,
|
||||
"created_at": "2025-03-13T05:09:40.853Z",
|
||||
"updated_at": "2025-03-13T05:09:40.853Z"
|
||||
}
|
||||
|
|
@ -105,7 +105,11 @@
|
|||
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="https://www.gravatar.com/profile/" target="_blank" rel="noopener noreferrer">
|
||||
Profile
|
||||
Profile
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,10 @@
|
|||
<div class="relative inline-block">
|
||||
{% if num_decks == 1 %}
|
||||
<div class="grid grid-cols-4 grid-rows-2 my-2 px-2 py-2 h-16 w-36 text-white shadow-md shadow-black/50" style="background-color: {{ decks.0.hex_color }};">
|
||||
{% elif num_decks == 2 %}
|
||||
<div class="grid grid-cols-4 grid-rows-2 my-2 px-2 py-2 h-16 w-36 text-white shadow-md shadow-black/50" style="background: linear-gradient(to right, {{ decks.0.hex_color }}, {{ decks.1.hex_color }});">
|
||||
{% elif num_decks >= 3 %}
|
||||
<div class="grid grid-cols-4 grid-rows-2 my-2 px-2 py-2 h-16 w-36 text-white shadow-md shadow-black/50" style="background: linear-gradient(to right, {{ decks.0.hex_color }}, {{ decks.1.hex_color }}, {{ decks.2.hex_color }});">
|
||||
{% else %}
|
||||
<div class="grid grid-cols-4 grid-rows-2 px-2 py-2 h-16 w-36 text-white bg-gray-600 shadow-md shadow-black/50">
|
||||
{% endif %}
|
||||
<div class="row-span-1 col-span-4 truncate text-ellipsis self-start font-semibold leading-tight text-sm max-w-7/8">{{ card.name }}</div>
|
||||
<div class="row-start-2 col-span-2 truncate self-end align-bottom text-xs">{{ card.rarity.icons }}</div>
|
||||
<div class="row-start-2 col-start-3 col-span-2 text-right truncate self-end align-bottom font-semibold leading-none text-sm">{{ card.cardset.name }}</div>
|
||||
<div class="grid grid-cols-4 grid-rows-2 my-2 px-2 py-2 h-16 w-36 text-white shadow-md shadow-black/50" style="{{ style }}">
|
||||
<div class="row-span-1 col-span-4 truncate text-ellipsis self-start font-semibold leading-tight text-sm max-w-7/8">{{ name }}</div>
|
||||
<div class="row-start-2 col-span-2 truncate self-end align-bottom text-xs">{{ rarity }}</div>
|
||||
<div class="row-start-2 col-start-3 col-span-2 text-right truncate self-end align-bottom font-semibold leading-none text-sm">{{ cardset }}</div>
|
||||
</div>
|
||||
<span class="card-quantity-badge absolute top-3.5 right-1 bg-gray-600 text-white text-xs font-semibold rounded-full px-2">
|
||||
{% if is_template %}__QUANTITY__{% else %}{{ quantity }}{% endif %}
|
||||
{{ quantity }}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{% load gravatar card_badge cache %}
|
||||
|
||||
{% cache 60 trade_offer offer.pk %}
|
||||
{% cache 60 trade_offer offer_pk %}
|
||||
<script>
|
||||
if (!window.tradeOfferCard) {
|
||||
window.tradeOfferCard = function() {
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
<div class="flip-face front col-start-1 row-start-1 grid grid-cols-1 auto-rows-min gap-2 content-between">
|
||||
<!-- Header -->
|
||||
<div class="self-start">
|
||||
<a href="{% url 'trade_offer_detail' pk=offer.pk %}" class="no-underline block">
|
||||
<a href="{% url 'trade_offer_detail' pk=offer_pk %}" class="no-underline block">
|
||||
<div class="py-4 mx-2 sm:mx-4">
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<div class="flex justify-center items-center">
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
<div class="flex justify-center items-center">
|
||||
<div class="avatar">
|
||||
<div class="w-10 rounded-full">
|
||||
{{ offer.initiated_by.user.email|gravatar:40 }}
|
||||
{{ initiated_by_email|gravatar:40 }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
</div>
|
||||
<!-- Main Trade Offer Row -->
|
||||
<div class="self-start">
|
||||
<a href="{% url 'trade_offer_detail' pk=offer.pk %}" class="no-underline block">
|
||||
<a href="{% url 'trade_offer_detail' pk=offer_pk %}" class="no-underline block">
|
||||
<div class="px-2 pb-0">
|
||||
<div class="grid grid-cols-2 gap-2 items-center">
|
||||
<div class="flex flex-col items-center">
|
||||
|
|
@ -106,7 +106,7 @@
|
|||
</div>
|
||||
<div class="self-end">
|
||||
<div class="flex justify-between px-2 pb-2">
|
||||
<div class="text-gray-500 text-sm tooltip tooltip-right" data-tip="ID: {{ offer.hash }}">
|
||||
<div class="text-gray-500 text-sm tooltip tooltip-right" data-tip="ID: {{ offer_hash }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="size-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
|
|
@ -129,7 +129,7 @@
|
|||
<!-- Placed in the same grid cell as the front face -->
|
||||
<div class="flip-face back col-start-1 row-start-1 grid grid-cols-1 auto-rows-min gap-2 content-between" style="transform: rotateY(180deg);">
|
||||
<div class="self-start">
|
||||
<a href="{% url 'trade_offer_detail' pk=offer.pk %}" class="no-underline">
|
||||
<a href="{% url 'trade_offer_detail' pk=offer_pk %}" class="no-underline">
|
||||
<div class="py-4 mx-2 sm:mx-4">
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<div class="flex justify-center items-center">
|
||||
|
|
@ -138,7 +138,7 @@
|
|||
<div class="flex justify-center items-center">
|
||||
<div class="avatar">
|
||||
<div class="w-10 rounded-full">
|
||||
{{ offer.initiated_by.user.email|gravatar:40 }}
|
||||
{{ initiated_by_email|gravatar:40 }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -152,9 +152,9 @@
|
|||
<div class="self-start">
|
||||
<div class="px-2 pb-0">
|
||||
<div class="overflow-hidden">
|
||||
{% if offer.acceptances.first %}
|
||||
{% if acceptances.0 %}
|
||||
<div class="space-y-3">
|
||||
{% with acceptance=offer.acceptances.first %}
|
||||
{% with acceptance=acceptances.0 %}
|
||||
<a href="{% url 'trade_acceptance_update' pk=acceptance.pk %}" class="no-underline"
|
||||
data-tooltip-html='<div class="flex items-center space-x-2">
|
||||
<div class="avatar">
|
||||
|
|
@ -182,7 +182,7 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
<div x-show="acceptanceExpanded" x-collapse.duration.500ms class="space-y-3">
|
||||
{% for acceptance in offer.acceptances.all|slice:"1:" %}
|
||||
{% for acceptance in acceptances|slice:"1:" %}
|
||||
<a href="{% url 'trade_acceptance_update' pk=acceptance.pk %}" class="no-underline"
|
||||
data-tooltip-html='<div class="flex items-center space-x-2">
|
||||
<div class="avatar">
|
||||
|
|
@ -209,7 +209,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center my-1 h-5">
|
||||
{% if offer.acceptances.all|length > 1 %}
|
||||
{% if acceptances|length > 1 %}
|
||||
<svg @click="acceptanceExpanded = !acceptanceExpanded"
|
||||
x-bind:class="{ 'rotate-180': acceptanceExpanded }"
|
||||
class="transition-transform duration-500 h-5 w-5 cursor-pointer"
|
||||
|
|
@ -230,10 +230,10 @@
|
|||
</div>
|
||||
<div class="px-1 text-center">
|
||||
<span class="text-sm font-semibold">
|
||||
Acceptances ({{ offer.acceptances.all|length }})
|
||||
Acceptances ({{ acceptances|length }})
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-gray-500 text-sm tooltip tooltip-left" data-tip="ID: {{ offer.hash }}">
|
||||
<div class="text-gray-500 text-sm tooltip tooltip-left" data-tip="ID: {{ offer_hash }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="size-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 5.1.2 on 2025-03-16 18:18
|
||||
# Generated by Django 5.1.2 on 2025-03-17 20:39
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
|
@ -20,10 +20,11 @@ class Migration(migrations.Migration):
|
|||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('is_closed', models.BooleanField(db_index=True, default=False)),
|
||||
('hash', models.CharField(editable=False, max_length=9)),
|
||||
('rarity_icon', models.CharField(max_length=8, null=True)),
|
||||
('rarity_level', models.IntegerField(null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('initiated_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='initiated_trade_offers', to='accounts.friendcode')),
|
||||
('rarity', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, to='cards.rarity')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
|
|
|
|||
|
|
@ -7,30 +7,18 @@ from accounts.models import FriendCode
|
|||
class TradeOfferManager(models.Manager):
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset().select_related("initiated_by", "initiated_by__user", "rarity")
|
||||
queryset = queryset.prefetch_related(
|
||||
Prefetch(
|
||||
"trade_offer_want_cards",
|
||||
queryset=TradeOfferWantCard.objects.select_related("card").prefetch_related('card__decks').annotate(
|
||||
total_quantity=Sum("quantity"),
|
||||
total_accepted=Sum("qty_accepted")
|
||||
).order_by("total_quantity", "id")
|
||||
),
|
||||
Prefetch(
|
||||
"trade_offer_have_cards",
|
||||
queryset=TradeOfferHaveCard.objects.select_related("card").prefetch_related('card__decks').annotate(
|
||||
total_quantity=Sum("quantity"),
|
||||
total_accepted=Sum("qty_accepted")
|
||||
).order_by("total_quantity", "id")
|
||||
),
|
||||
Prefetch(
|
||||
"acceptances",
|
||||
queryset=TradeAcceptance.objects.select_related("accepted_by", "accepted_by__user", "requested_card", "offered_card")
|
||||
),
|
||||
queryset = super().get_queryset().select_related(
|
||||
"initiated_by__user",
|
||||
).prefetch_related(
|
||||
"trade_offer_have_cards__card",
|
||||
"trade_offer_want_cards__card",
|
||||
"acceptances",
|
||||
"acceptances__requested_card",
|
||||
"acceptances__offered_card",
|
||||
"acceptances__accepted_by__user",
|
||||
).order_by("-updated_at")
|
||||
return queryset
|
||||
|
||||
|
||||
class TradeOffer(models.Model):
|
||||
objects = TradeOfferManager()
|
||||
|
||||
|
|
@ -42,14 +30,8 @@ class TradeOffer(models.Model):
|
|||
on_delete=models.PROTECT,
|
||||
related_name='initiated_trade_offers'
|
||||
)
|
||||
rarity = models.ForeignKey(
|
||||
"cards.Rarity",
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True,
|
||||
editable=False,
|
||||
db_index=True
|
||||
)
|
||||
rarity_icon = models.CharField(max_length=8, null=True)
|
||||
rarity_level = models.IntegerField(null=True)
|
||||
want_cards = models.ManyToManyField(
|
||||
"cards.Card",
|
||||
related_name='trade_offers_want',
|
||||
|
|
|
|||
|
|
@ -5,36 +5,40 @@ from .models import TradeOffer
|
|||
from cards.models import Card
|
||||
from django.db.models import F
|
||||
from trades.models import TradeOfferHaveCard, TradeOfferWantCard, TradeAcceptance
|
||||
from django.db import transaction
|
||||
|
||||
def validate_and_set_trade_offer_rarity(instance):
|
||||
"""
|
||||
Ensures all cards on both sides share the same rarity and sets the TradeOffer.rarity
|
||||
if it hasn't been set already.
|
||||
Ensures all cards on both sides share the same rarity and sets the TradeOffer's
|
||||
rarity_level and rarity_icon if they haven't been set already.
|
||||
"""
|
||||
# Combine cards from both sides.
|
||||
combined_cards = list(instance.have_cards.all()) + list(instance.want_cards.all())
|
||||
if not combined_cards:
|
||||
return
|
||||
|
||||
# Gather the Rarity instances from the cards.
|
||||
rarities = {card.normalized_rarity for card in combined_cards}
|
||||
rarities = {card.rarity_level for card in combined_cards}
|
||||
if len(rarities) > 1:
|
||||
raise ValidationError("All cards in a trade offer must have the same rarity.")
|
||||
|
||||
# If trade offer's rarity isn't set yet, update it.
|
||||
if instance.rarity is None:
|
||||
instance.rarity = combined_cards[0].normalized_rarity
|
||||
instance.save(update_fields=["rarity"])
|
||||
updated_fields = []
|
||||
if instance.rarity_level is None:
|
||||
instance.rarity_level = combined_cards[0].rarity_level
|
||||
updated_fields.append("rarity_level")
|
||||
if instance.rarity_icon is None:
|
||||
instance.rarity_icon = combined_cards[0].rarity_icon
|
||||
updated_fields.append("rarity_icon")
|
||||
if updated_fields:
|
||||
instance.save(update_fields=updated_fields)
|
||||
|
||||
@receiver(m2m_changed, sender=TradeOffer.have_cards.through)
|
||||
def validate_have_cards_rarity(sender, instance, action, **kwargs):
|
||||
if action == "post_add":
|
||||
validate_and_set_trade_offer_rarity(instance)
|
||||
transaction.on_commit(lambda: validate_and_set_trade_offer_rarity(instance))
|
||||
|
||||
@receiver(m2m_changed, sender=TradeOffer.want_cards.through)
|
||||
def validate_want_cards_rarity(sender, instance, action, **kwargs):
|
||||
if action == "post_add":
|
||||
validate_and_set_trade_offer_rarity(instance)
|
||||
transaction.on_commit(lambda: validate_and_set_trade_offer_rarity(instance))
|
||||
|
||||
ACTIVE_STATES = [
|
||||
TradeAcceptance.AcceptanceState.ACCEPTED,
|
||||
|
|
|
|||
|
|
@ -20,8 +20,17 @@ def render_trade_offer(context, offer):
|
|||
if card.quantity > card.qty_accepted
|
||||
]
|
||||
|
||||
acceptances = [acceptance for acceptance in list(offer.acceptances.all())
|
||||
if acceptance.is_active
|
||||
]
|
||||
|
||||
return {
|
||||
'offer': offer,
|
||||
'offer_pk': offer.pk,
|
||||
'offer_hash': offer.hash,
|
||||
'rarity_icon': offer.rarity_icon,
|
||||
'initiated_by_email': offer.initiated_by.user.email,
|
||||
'initiated_by_username': offer.initiated_by.user.username,
|
||||
'acceptances': acceptances,
|
||||
'have_cards_available': have_cards_available,
|
||||
'want_cards_available': want_cards_available,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from .models import TradeOffer, TradeAcceptance
|
|||
from .forms import (TradeOfferAcceptForm,
|
||||
TradeAcceptanceCreateForm, TradeOfferCreateForm, TradeAcceptanceTransitionForm)
|
||||
from cards.models import Card
|
||||
from silk.profiling.profiler import silk_profile
|
||||
#from silk.profiling.profiler import silk_profile
|
||||
|
||||
class TradeOfferCreateView(LoginRequiredMixin, CreateView):
|
||||
model = TradeOffer
|
||||
|
|
@ -42,7 +42,7 @@ class TradeOfferCreateView(LoginRequiredMixin, CreateView):
|
|||
context = super().get_context_data(**kwargs)
|
||||
from cards.models import Card
|
||||
# Ensure available_cards is a proper QuerySet
|
||||
context["cards"] = Card.objects.all().order_by("name", "rarity__pk") \
|
||||
context["cards"] = Card.objects.all().order_by("name", "rarity_level") \
|
||||
.select_related("rarity", "cardset") \
|
||||
.prefetch_related("decks")
|
||||
friend_codes = self.request.user.friend_codes.all()
|
||||
|
|
@ -68,7 +68,7 @@ class TradeOfferAllListView(ListView):
|
|||
model = TradeOffer
|
||||
template_name = "trades/trade_offer_all_list.html"
|
||||
|
||||
@silk_profile(name="Trade Offer All List- Get Context Data")
|
||||
#@silk_profile(name="Trade Offer All List- Get Context Data")
|
||||
def get_context_data(self, *, object_list=None, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
request = self.request
|
||||
|
|
@ -88,14 +88,14 @@ class TradeOfferAllListView(ListView):
|
|||
context["all_trade_offers_paginated"] = offers_paginator.get_page(offers_page)
|
||||
return context
|
||||
|
||||
@silk_profile(name="Trade Offer All List- Render to Response")
|
||||
#@silk_profile(name="Trade Offer All 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":
|
||||
page = self.request.GET.get("page")
|
||||
show_closed = self.request.GET.get("show_closed", "false").lower() == "true"
|
||||
|
||||
queryset = TradeOffer.objects.all()
|
||||
queryset = TradeOffer.objects
|
||||
if show_closed:
|
||||
queryset = queryset.filter(is_closed=True)
|
||||
else:
|
||||
|
|
@ -185,7 +185,7 @@ class TradeOfferMyListView(LoginRequiredMixin, ListView):
|
|||
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")
|
||||
#@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
|
||||
|
|
@ -206,7 +206,7 @@ class TradeOfferMyListView(LoginRequiredMixin, ListView):
|
|||
|
||||
return context
|
||||
|
||||
@silk_profile(name="Trade Offer My List- Render to Response")
|
||||
#@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":
|
||||
|
|
@ -318,7 +318,7 @@ class TradeOfferSearchView(ListView):
|
|||
results.append((card_id, qty))
|
||||
return results
|
||||
|
||||
@silk_profile(name="Trade Offer Search- Get Queryset")
|
||||
#@silk_profile(name="Trade Offer Search- Get Queryset")
|
||||
def get_queryset(self):
|
||||
from django.db.models import F
|
||||
# For a GET request (initial load), return an empty queryset.
|
||||
|
|
@ -355,18 +355,17 @@ class TradeOfferSearchView(ListView):
|
|||
|
||||
return qs.distinct()
|
||||
|
||||
@silk_profile(name="Trade Offer Search- Post")
|
||||
#@silk_profile(name="Trade Offer Search- Post")
|
||||
def post(self, request, *args, **kwargs):
|
||||
# For POST, simply process the search through get().
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
@silk_profile(name="Trade Offer Search- Get Context Data")
|
||||
#@silk_profile(name="Trade Offer Search- Get Context Data")
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
from cards.models import Card
|
||||
# Populate available_cards to re-populate the multiselects.
|
||||
context["cards"] = Card.objects.all().order_by("name", "rarity__pk") \
|
||||
.select_related("rarity", "cardset")
|
||||
context["cards"] = Card.objects.all().order_by("name")
|
||||
if self.request.method == "POST":
|
||||
context["offered_cards"] = self.request.POST.getlist("offered_cards")
|
||||
context["wanted_cards"] = self.request.POST.getlist("wanted_cards")
|
||||
|
|
@ -375,7 +374,7 @@ class TradeOfferSearchView(ListView):
|
|||
context["wanted_cards"] = []
|
||||
return context
|
||||
|
||||
@silk_profile(name="Trade Offer Search- Render to Response")
|
||||
#@silk_profile(name="Trade Offer Search- Render to Response")
|
||||
def render_to_response(self, context, **response_kwargs):
|
||||
"""
|
||||
Render the AJAX fragment if the request is AJAX; otherwise, render the complete page.
|
||||
|
|
@ -395,7 +394,7 @@ class TradeOfferDetailView(LoginRequiredMixin, DetailView):
|
|||
model = TradeOffer
|
||||
template_name = "trades/trade_offer_detail.html"
|
||||
|
||||
@silk_profile(name="Trade Offer Detail- Get Context Data")
|
||||
#@silk_profile(name="Trade Offer Detail- Get Context Data")
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
trade_offer = self.get_object()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue