diff --git a/manage.py b/manage.py
index 138d284..d84abce 100755
--- a/manage.py
+++ b/manage.py
@@ -1,11 +1,15 @@
#!/usr/bin/env -S uv run
"""Django's command-line utility for administrative tasks."""
+
import os
import sys
+
def main():
"""Run administrative tasks."""
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pkmntrade_club.django_project.settings")
+ os.environ.setdefault(
+ "DJANGO_SETTINGS_MODULE", "pkmntrade_club.django_project.settings"
+ )
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
try:
diff --git a/src/pkmntrade_club/__init__.py b/src/pkmntrade_club/__init__.py
index 8d1f1f7..bcdfc83 100644
--- a/src/pkmntrade_club/__init__.py
+++ b/src/pkmntrade_club/__init__.py
@@ -2,4 +2,4 @@
from pkmntrade_club._version import __version__, get_version, get_version_info
-__all__ = ['__version__', 'get_version', 'get_version_info']
+__all__ = ["__version__", "get_version", "get_version_info"]
diff --git a/src/pkmntrade_club/_version.py b/src/pkmntrade_club/_version.py
index 6f90c71..77921b5 100644
--- a/src/pkmntrade_club/_version.py
+++ b/src/pkmntrade_club/_version.py
@@ -1,5 +1,6 @@
from importlib.metadata import version, PackageNotFoundError
from setuptools_scm import get_version
+
"""
Version module for pkmntrade.club
@@ -7,55 +8,57 @@ This module provides version information from git tags via setuptools-scm.
"""
try:
- __version__ = version("pkmntrade-club")
+ __version__ = version("pkmntrade-club")
except PackageNotFoundError:
- # Package is not installed, try to get version from setuptools_scm
- try:
- __version__ = get_version(root='../../..', relative_to=__file__)
- except (ImportError, LookupError):
- __version__ = "0.0.0+unknown"
+ # Package is not installed, try to get version from setuptools_scm
+ try:
+ __version__ = get_version(root="../../..", relative_to=__file__)
+ except (ImportError, LookupError):
+ __version__ = "0.0.0+unknown"
+
def get_version():
- """Return the current version."""
- return __version__
+ """Return the current version."""
+ return __version__
+
def get_version_info():
- """Return detailed version information."""
- import re
-
- # Parse version string (e.g., "1.2.3", "1.2.3.dev4+gabc1234", "1.2.3-prerelease")
- match = re.match(
- r'^(\d+)\.(\d+)\.(\d+)'
- r'(?:\.dev(\d+))?'
- r'(?:\+g([a-f0-9]+))?'
- r'(?:-(.+))?$',
- __version__
- )
-
- if match:
- major, minor, patch, dev, git_sha, prerelease = match.groups()
+ """Return detailed version information."""
+ import re
+
+ # Parse version string (e.g., "1.2.3", "1.2.3.dev4+gabc1234", "1.2.3-prerelease")
+ match = re.match(
+ r"^(\d+)\.(\d+)\.(\d+)"
+ r"(?:\.dev(\d+))?"
+ r"(?:\+g([a-f0-9]+))?"
+ r"(?:-(.+))?$",
+ __version__,
+ )
+
+ if match:
+ major, minor, patch, dev, git_sha, prerelease = match.groups()
+ return {
+ "version": __version__,
+ "major": int(major),
+ "minor": int(minor),
+ "patch": int(patch),
+ "dev": int(dev) if dev else None,
+ "git_sha": git_sha,
+ "prerelease": prerelease,
+ "is_release": dev is None and not prerelease,
+ "is_prerelease": bool(prerelease),
+ "is_dev": dev is not None,
+ }
+
return {
- 'version': __version__,
- 'major': int(major),
- 'minor': int(minor),
- 'patch': int(patch),
- 'dev': int(dev) if dev else None,
- 'git_sha': git_sha,
- 'prerelease': prerelease,
- 'is_release': dev is None and not prerelease,
- 'is_prerelease': bool(prerelease),
- 'is_dev': dev is not None
+ "version": __version__,
+ "major": 0,
+ "minor": 0,
+ "patch": 0,
+ "dev": None,
+ "git_sha": None,
+ "prerelease": None,
+ "is_release": False,
+ "is_prerelease": False,
+ "is_dev": True,
}
-
- return {
- 'version': __version__,
- 'major': 0,
- 'minor': 0,
- 'patch': 0,
- 'dev': None,
- 'git_sha': None,
- 'prerelease': None,
- 'is_release': False,
- 'is_prerelease': False,
- 'is_dev': True
-}
\ No newline at end of file
diff --git a/src/pkmntrade_club/accounts/adapter.py b/src/pkmntrade_club/accounts/adapter.py
index b41e761..b39fed3 100644
--- a/src/pkmntrade_club/accounts/adapter.py
+++ b/src/pkmntrade_club/accounts/adapter.py
@@ -1,4 +1,3 @@
-from django.conf import settings
from allauth.account.adapter import DefaultAccountAdapter
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
@@ -10,4 +9,4 @@ class NoSignupAccountAdapter(DefaultAccountAdapter):
class NoSignupSocialAccountAdapter(DefaultSocialAccountAdapter):
def is_open_for_signup(self, request):
- return False
\ No newline at end of file
+ return False
diff --git a/src/pkmntrade_club/accounts/admin.py b/src/pkmntrade_club/accounts/admin.py
index f3bd5c3..bd475e3 100644
--- a/src/pkmntrade_club/accounts/admin.py
+++ b/src/pkmntrade_club/accounts/admin.py
@@ -1,5 +1,4 @@
from django.contrib import admin
-from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin
from .forms import CustomUserCreationForm, CustomUserChangeForm
diff --git a/src/pkmntrade_club/accounts/apps.py b/src/pkmntrade_club/accounts/apps.py
index df72a2b..408657e 100644
--- a/src/pkmntrade_club/accounts/apps.py
+++ b/src/pkmntrade_club/accounts/apps.py
@@ -2,5 +2,5 @@ from django.apps import AppConfig
class AccountsConfig(AppConfig):
- default_auto_field = 'django.db.models.BigAutoField'
- name = 'pkmntrade_club.accounts'
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "pkmntrade_club.accounts"
diff --git a/src/pkmntrade_club/accounts/forms.py b/src/pkmntrade_club/accounts/forms.py
index 6788d85..0aac8fe 100644
--- a/src/pkmntrade_club/accounts/forms.py
+++ b/src/pkmntrade_club/accounts/forms.py
@@ -2,15 +2,13 @@ from django import forms
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser, FriendCode
from allauth.account.forms import SignupForm
-from crispy_tailwind.tailwind import CSSContainer
-from crispy_forms.helper import FormHelper
-from crispy_forms.layout import Layout, Field, Submit
+
class CustomUserChangeForm(UserChangeForm):
-
class Meta:
model = CustomUser
- fields = ['email']
+ fields = ["email"]
+
class FriendCodeForm(forms.ModelForm):
class Meta:
@@ -27,23 +25,27 @@ class FriendCodeForm(forms.ModelForm):
friend_code_formatted = f"{friend_code_clean[:4]}-{friend_code_clean[4:8]}-{friend_code_clean[8:12]}-{friend_code_clean[12:16]}"
return friend_code_formatted
-class CustomUserCreationForm(SignupForm):
+class CustomUserCreationForm(SignupForm):
class Meta(UserCreationForm.Meta):
model = CustomUser
- fields = ['email', 'username', 'friend_code']
+ fields = ["email", "username", "friend_code"]
email = forms.EmailField(
required=True,
label="Email",
- widget=forms.TextInput(attrs={'placeholder': 'Email', 'class':'dark:bg-base-100'})
+ widget=forms.TextInput(
+ attrs={"placeholder": "Email", "class": "dark:bg-base-100"}
+ ),
)
username = forms.CharField(
max_length=24,
required=True,
label="Username",
- widget=forms.TextInput(attrs={'placeholder': 'Username', 'class':'dark:bg-base-100'})
+ widget=forms.TextInput(
+ attrs={"placeholder": "Username", "class": "dark:bg-base-100"}
+ ),
)
friend_code = forms.CharField(
@@ -51,14 +53,18 @@ class CustomUserCreationForm(SignupForm):
required=True,
label="Friend Code",
help_text="Enter your friend code in the format XXXX-XXXX-XXXX-XXXX.",
- widget=forms.TextInput(attrs={'placeholder': 'XXXX-XXXX-XXXX-XXXX', 'class':'dark:bg-base-100'})
+ widget=forms.TextInput(
+ attrs={"placeholder": "XXXX-XXXX-XXXX-XXXX", "class": "dark:bg-base-100"}
+ ),
)
in_game_name = forms.CharField(
max_length=16,
required=True,
label="In-Game Name",
help_text="Enter your in-game name.",
- widget=forms.TextInput(attrs={'placeholder': 'In-Game Name', 'class':'dark:bg-base-100'})
+ widget=forms.TextInput(
+ attrs={"placeholder": "In-Game Name", "class": "dark:bg-base-100"}
+ ),
)
def __init__(self, *args, **kwargs):
@@ -78,13 +84,14 @@ class CustomUserCreationForm(SignupForm):
friend_code_instance = FriendCode.objects.create(
friend_code=self.cleaned_data["friend_code"],
in_game_name=self.cleaned_data["in_game_name"],
- user=user
+ user=user,
)
user.default_friend_code = friend_code_instance
user.save()
return user
+
class UserSettingsForm(forms.ModelForm):
class Meta:
model = CustomUser
- fields = ['show_friend_code_on_link_previews', 'enable_email_notifications']
\ No newline at end of file
+ fields = ["show_friend_code_on_link_previews", "enable_email_notifications"]
diff --git a/src/pkmntrade_club/accounts/management/commands/clear_cache.py b/src/pkmntrade_club/accounts/management/commands/clear_cache.py
index 8c6863f..96a2b7b 100644
--- a/src/pkmntrade_club/accounts/management/commands/clear_cache.py
+++ b/src/pkmntrade_club/accounts/management/commands/clear_cache.py
@@ -1,7 +1,8 @@
from django.core.management.base import BaseCommand
from django.core.cache import cache
+
class Command(BaseCommand):
def handle(self, *args, **kwargs):
cache.clear()
- self.stdout.write('Cleared cache\n')
\ No newline at end of file
+ self.stdout.write("Cleared cache\n")
diff --git a/src/pkmntrade_club/accounts/models.py b/src/pkmntrade_club/accounts/models.py
index 65610e7..a78a15b 100644
--- a/src/pkmntrade_club/accounts/models.py
+++ b/src/pkmntrade_club/accounts/models.py
@@ -3,24 +3,28 @@ from django.db import models
from django.core.exceptions import ValidationError
import re
+
def validate_friend_code(value):
"""Validate that friend code follows the format XXXX-XXXX-XXXX-XXXX where X is a digit."""
- if not re.match(r'^\d{4}-\d{4}-\d{4}-\d{4}$', value):
+ if not re.match(r"^\d{4}-\d{4}-\d{4}-\d{4}$", value):
raise ValidationError(
- 'Friend code must be in format XXXX-XXXX-XXXX-XXXX where X is a digit.'
+ "Friend code must be in format XXXX-XXXX-XXXX-XXXX where X is a digit."
)
+
class CustomUser(AbstractUser):
- default_friend_code = models.ForeignKey("FriendCode", on_delete=models.SET_NULL, null=True, blank=True)
+ default_friend_code = models.ForeignKey(
+ "FriendCode", on_delete=models.SET_NULL, null=True, blank=True
+ )
show_friend_code_on_link_previews = models.BooleanField(
default=False,
verbose_name="Show Friend Code on Link Previews",
- help_text="This will primarily affect share link previews on X, Discord, etc."
+ help_text="This will primarily affect share link previews on X, Discord, etc.",
)
enable_email_notifications = models.BooleanField(
default=True,
verbose_name="Enable Email Notifications",
- help_text="Receive trade notifications via email."
+ help_text="Receive trade notifications via email.",
)
reputation_score = models.IntegerField(default=0)
@@ -47,10 +51,13 @@ class CustomUser(AbstractUser):
self.default_friend_code = other_codes.first()
self.save(update_fields=["default_friend_code"])
+
class FriendCode(models.Model):
friend_code = models.CharField(max_length=19, validators=[validate_friend_code])
in_game_name = models.CharField(max_length=14, null=False, blank=False)
- user = models.ForeignKey(CustomUser, on_delete=models.PROTECT, related_name='friend_codes')
+ user = models.ForeignKey(
+ CustomUser, on_delete=models.PROTECT, related_name="friend_codes"
+ )
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -67,4 +74,4 @@ class FriendCode(models.Model):
self.user.save(update_fields=["default_friend_code"])
def __str__(self):
- return self.friend_code
\ No newline at end of file
+ return self.friend_code
diff --git a/src/pkmntrade_club/accounts/templatetags/gravatar.py b/src/pkmntrade_club/accounts/templatetags/gravatar.py
index c2005a0..a719cd6 100644
--- a/src/pkmntrade_club/accounts/templatetags/gravatar.py
+++ b/src/pkmntrade_club/accounts/templatetags/gravatar.py
@@ -6,15 +6,17 @@ from django.utils.safestring import mark_safe
register = template.Library()
+
@register.filter
def gravatar_hash(email):
"""
Returns the hash of the email.
"""
- email_encoded = email.strip().lower().encode('utf-8')
+ email_encoded = email.strip().lower().encode("utf-8")
email_hash = hashlib.sha256(email_encoded).hexdigest()
return email_hash
+
@register.filter
def gravatar_url(email, size=20):
"""
@@ -23,20 +25,22 @@ def gravatar_url(email, size=20):
"""
default = "retro"
email_hash = gravatar_hash(email)
- params = urlencode({'d': default, 's': str(size)})
+ params = urlencode({"d": default, "s": str(size)})
params = params.replace("&", "&")
return f"https://www.gravatar.com/avatar/{email_hash}?{params}"
+
@register.filter
def gravatar_profile_url(email=None):
"""
Returns the Gravatar Profile URL for a given email.
"""
if email is None:
- return f"https://www.gravatar.com/profile"
+ return "https://www.gravatar.com/profile"
email_hash = gravatar_hash(email)
return f"https://secure.gravatar.com/{email_hash}"
+
@register.filter
def gravatar(email, size=20):
"""
@@ -48,6 +52,7 @@ def gravatar(email, size=20):
html = f'
'
return mark_safe(html)
+
@register.filter
def gravatar_no_hover(email, size=20):
"""
@@ -59,6 +64,7 @@ def gravatar_no_hover(email, size=20):
html = f'
'
return mark_safe(html)
+
@register.filter
def gravatar_profile_data(email):
"""
diff --git a/src/pkmntrade_club/accounts/tests.py b/src/pkmntrade_club/accounts/tests.py
index b6c8695..6d00785 100644
--- a/src/pkmntrade_club/accounts/tests.py
+++ b/src/pkmntrade_club/accounts/tests.py
@@ -9,34 +9,34 @@ from django.core.exceptions import ValidationError
from django.contrib.sessions.middleware import SessionMiddleware
from pkmntrade_club.accounts.models import FriendCode
-from pkmntrade_club.accounts.forms import FriendCodeForm, CustomUserCreationForm, UserSettingsForm
+from pkmntrade_club.accounts.forms import (
+ FriendCodeForm,
+ CustomUserCreationForm,
+ UserSettingsForm,
+)
from pkmntrade_club.accounts.templatetags import gravatar
from pkmntrade_club.trades.models import TradeOffer
from tests.utils.rarity import RARITY_MAPPING
# Create your tests here.
+
# -----------------------------
# Model Tests
# -----------------------------
class CustomUserModelTests(TestCase):
def setUp(self):
self.user = get_user_model().objects.create_user(
- username="testuser",
- email="test@example.com",
- password="password123"
+ username="testuser", email="test@example.com", password="password123"
)
+
def test_set_default_friend_code(self):
"""User can manually set a friend code as their default."""
fc1 = FriendCode.objects.create(
- friend_code="1234-5678-9012-3456",
- user=self.user,
- in_game_name="GameOne"
+ friend_code="1234-5678-9012-3456", user=self.user, in_game_name="GameOne"
)
fc2 = FriendCode.objects.create(
- friend_code="2345-6789-0123-4567",
- user=self.user,
- in_game_name="GameTwo"
+ friend_code="2345-6789-0123-4567", user=self.user, in_game_name="GameTwo"
)
# Manually set fc2 as default.
self.user.set_default_friend_code(fc2)
@@ -48,14 +48,10 @@ class CustomUserModelTests(TestCase):
Attempting to set a friend code that does not belong to the user should raise an exception.
"""
other_user = get_user_model().objects.create_user(
- username="otheruser",
- email="other@example.com",
- password="password456"
+ username="otheruser", email="other@example.com", password="password456"
)
fc_other = FriendCode.objects.create(
- friend_code="3456-7890-1234-5678",
- user=other_user,
- in_game_name="OtherGame"
+ friend_code="3456-7890-1234-5678", user=other_user, in_game_name="OtherGame"
)
with self.assertRaises(ValidationError):
self.user.set_default_friend_code(fc_other)
@@ -66,14 +62,10 @@ class CustomUserModelTests(TestCase):
the default should be reassigned to another friend code.
"""
fc1 = FriendCode.objects.create(
- friend_code="1234-5678-9012-3456",
- user=self.user,
- in_game_name="GameOne"
+ friend_code="1234-5678-9012-3456", user=self.user, in_game_name="GameOne"
)
fc2 = FriendCode.objects.create(
- friend_code="2345-6789-0123-4567",
- user=self.user,
- in_game_name="GameTwo"
+ friend_code="2345-6789-0123-4567", user=self.user, in_game_name="GameTwo"
)
# Set fc2 as default.
self.user.set_default_friend_code(fc2)
@@ -89,9 +81,7 @@ class CustomUserModelTests(TestCase):
should be prohibited.
"""
fc = FriendCode.objects.create(
- friend_code="1234-5678-9012-3456",
- user=self.user,
- in_game_name="OnlyGame"
+ friend_code="1234-5678-9012-3456", user=self.user, in_game_name="OnlyGame"
)
self.user.refresh_from_db()
self.assertEqual(self.user.default_friend_code, fc)
@@ -104,21 +94,19 @@ class CustomUserModelTests(TestCase):
the current default should remain unchanged.
"""
fc1 = FriendCode.objects.create(
- friend_code="1234-5678-9012-3456",
- user=self.user,
- in_game_name="GameOne"
+ friend_code="1234-5678-9012-3456", user=self.user, in_game_name="GameOne"
)
fc2 = FriendCode.objects.create(
- friend_code="2345-6789-0123-4567",
- user=self.user,
- in_game_name="GameTwo"
+ friend_code="2345-6789-0123-4567", user=self.user, in_game_name="GameTwo"
)
# By default, fc1 is the default friend code.
self.assertEqual(self.user.default_friend_code, fc1)
try:
self.user.remove_default_friend_code(fc2)
- except Exception as e:
- self.fail("remove_default_friend_code raised an exception when removing a non-default code.")
+ except Exception:
+ self.fail(
+ "remove_default_friend_code raised an exception when removing a non-default code."
+ )
self.user.refresh_from_db()
self.assertEqual(self.user.default_friend_code, fc1)
@@ -129,9 +117,7 @@ class CustomUserModelTests(TestCase):
class FriendCodeModelTests(TestCase):
def setUp(self):
self.user = get_user_model().objects.create_user(
- username="testuser2",
- email="test2@example.com",
- password="password123"
+ username="testuser2", email="test2@example.com", password="password123"
)
def test_default_set_on_creation(self):
@@ -142,7 +128,7 @@ class FriendCodeModelTests(TestCase):
fc = FriendCode.objects.create(
friend_code="1234-5678-9012-3456",
user=self.user,
- in_game_name="GameDefault"
+ in_game_name="GameDefault",
)
self.user.refresh_from_db()
self.assertEqual(self.user.default_friend_code, fc)
@@ -155,14 +141,14 @@ class FriendCodeModelTests(TestCase):
fc1 = FriendCode.objects.create(
friend_code="1111-1111-1111-1111",
user=self.user,
- in_game_name="PrimaryGame"
+ in_game_name="PrimaryGame",
)
# fc1 becomes the default automatically.
self.assertEqual(self.user.default_friend_code, fc1)
fc2 = FriendCode.objects.create(
friend_code="2222-2222-2222-2222",
user=self.user,
- in_game_name="SecondaryGame"
+ in_game_name="SecondaryGame",
)
self.user.refresh_from_db()
self.assertEqual(self.user.default_friend_code, fc1)
@@ -174,39 +160,34 @@ class FriendCodeModelTests(TestCase):
class FriendCodeFormTests(TestCase):
def test_valid_friend_code(self):
"""Ensure valid friend code is cleaned and formatted properly."""
- form_data = {
- "friend_code": "1234567890123456",
- "in_game_name": "GameTest"
- }
+ form_data = {"friend_code": "1234567890123456", "in_game_name": "GameTest"}
form = FriendCodeForm(data=form_data)
self.assertTrue(form.is_valid())
self.assertEqual(form.cleaned_data["friend_code"], "1234-5678-9012-3456")
def test_invalid_friend_code_length(self):
"""Friend codes with incorrect length should cause validation errors."""
- form_data = {
- "friend_code": "12345",
- "in_game_name": "GameTest"
- }
+ form_data = {"friend_code": "12345", "in_game_name": "GameTest"}
form = FriendCodeForm(data=form_data)
self.assertFalse(form.is_valid())
- self.assertIn("Friend code must be exactly 16 digits long.", form.errors["friend_code"])
+ self.assertIn(
+ "Friend code must be exactly 16 digits long.", form.errors["friend_code"]
+ )
def test_invalid_friend_code_characters(self):
"""Friend codes containing non-digit characters should cause validation errors."""
- form_data = {
- "friend_code": "12345678901234ab",
- "in_game_name": "GameTest"
- }
+ form_data = {"friend_code": "12345678901234ab", "in_game_name": "GameTest"}
form = FriendCodeForm(data=form_data)
self.assertFalse(form.is_valid())
- self.assertIn("Friend code must be exactly 16 digits long.", form.errors["friend_code"])
+ self.assertIn(
+ "Friend code must be exactly 16 digits long.", form.errors["friend_code"]
+ )
def test_friend_code_with_whitespace(self):
"""Ensure that leading/trailing whitespace is stripped."""
form_data = {
"friend_code": " 1234567890123456 ",
- "in_game_name": "WhitespaceGame"
+ "in_game_name": "WhitespaceGame",
}
form = FriendCodeForm(data=form_data)
self.assertTrue(form.is_valid())
@@ -216,7 +197,7 @@ class FriendCodeFormTests(TestCase):
"""Proper dashes in the input should be accepted."""
form_data = {
"friend_code": "1234-5678-9012-3456",
- "in_game_name": "ExtraDashGame"
+ "in_game_name": "ExtraDashGame",
}
form = FriendCodeForm(data=form_data)
self.assertTrue(form.is_valid())
@@ -292,7 +273,9 @@ class CustomUserCreationFormTests(TestCase):
}
form = CustomUserCreationForm(data=form_data)
self.assertFalse(form.is_valid())
- self.assertIn("Friend code must be exactly 16 digits long.", form.errors["friend_code"])
+ self.assertIn(
+ "Friend code must be exactly 16 digits long.", form.errors["friend_code"]
+ )
def test_invalid_custom_user_creation_password_mismatch(self):
"""
@@ -318,7 +301,7 @@ class UserSettingsFormTests(TestCase):
self.user = get_user_model().objects.create_user(
username="settingsuser",
email="settings@example.com",
- password="password123"
+ password="password123",
)
def test_toggle_show_friend_code_on_link_previews(self):
@@ -337,9 +320,7 @@ class UserSettingsFormTests(TestCase):
class FriendCodeViewsTests(TestCase):
def setUp(self):
self.user = get_user_model().objects.create_user(
- username="viewuser",
- email="viewuser@example.com",
- password="password123"
+ username="viewuser", email="viewuser@example.com", password="password123"
)
# Log in this user.
self.client.login(username="viewuser", password="password123")
@@ -347,12 +328,12 @@ class FriendCodeViewsTests(TestCase):
self.friend_code1 = FriendCode.objects.create(
friend_code="7777-7777-7777-7777",
user=self.user,
- in_game_name="ViewGameOne"
+ in_game_name="ViewGameOne",
)
self.friend_code2 = FriendCode.objects.create(
friend_code="8888-8888-8888-8888",
user=self.user,
- in_game_name="ViewGameTwo"
+ in_game_name="ViewGameTwo",
)
# By default, friend_code1 is the default.
@@ -390,8 +371,7 @@ class FriendCodeViewsTests(TestCase):
self.assertRedirects(response, reverse("list_friend_codes"))
self.assertTrue(
FriendCode.objects.filter(
- user=self.user,
- friend_code="9999-9999-9999-9999"
+ user=self.user, friend_code="9999-9999-9999-9999"
).exists()
)
# Ensure that adding a new friend code does not change the default.
@@ -404,10 +384,16 @@ class FriendCodeViewsTests(TestCase):
data = {"friend_code": "invalidfriendcode", "in_game_name": "InvalidGame"}
response = self.client.post(url, data)
# Extract the form from the response's context. If response.context is a list, use its first element.
- context = response.context[0] if isinstance(response.context, list) else response.context
+ context = (
+ response.context[0]
+ if isinstance(response.context, list)
+ else response.context
+ )
form = context.get("form")
self.assertIsNotNone(form, "Form not found in response context")
- self.assertFormError(form, "friend_code", "Friend code must be exactly 16 digits long.")
+ self.assertFormError(
+ form, "friend_code", "Friend code must be exactly 16 digits long."
+ )
def test_edit_friend_code_view(self):
"""Test editing the in-game name of an existing friend code."""
@@ -425,14 +411,10 @@ class FriendCodeViewsTests(TestCase):
def test_edit_friend_code_view_wrong_user(self):
"""A user should not be able to edit a friend code that does not belong to them."""
other_user = get_user_model().objects.create_user(
- username="otheruser",
- email="other@example.com",
- password="password1234"
+ username="otheruser", email="other@example.com", password="password1234"
)
friend_code_other = FriendCode.objects.create(
- friend_code="0000-0000-0000-0000",
- user=other_user,
- in_game_name="OtherGame"
+ friend_code="0000-0000-0000-0000", user=other_user, in_game_name="OtherGame"
)
url = reverse("edit_friend_code", kwargs={"pk": friend_code_other.pk})
response = self.client.get(url)
@@ -443,25 +425,25 @@ class FriendCodeViewsTests(TestCase):
url = reverse("edit_friend_code", kwargs={"pk": self.friend_code2.pk})
new_data = {"in_game_name": ""} # in_game_name is required.
response = self.client.post(url, new_data)
- context = response.context[0] if isinstance(response.context, list) else response.context
+ context = (
+ response.context[0]
+ if isinstance(response.context, list)
+ else response.context
+ )
form = context.get("form")
self.assertIsNotNone(form, "Form not found in response context")
self.assertFormError(form, "in_game_name", "This field is required.")
-
+
def test_delete_friend_code_view_only_code(self):
"""
If the user has only one friend code, deletion should be disabled.
This test uses a new user with a single friend code.
"""
user_only = get_user_model().objects.create_user(
- username="onlyuser",
- email="onlyuser@example.com",
- password="password123"
+ username="onlyuser", email="onlyuser@example.com", password="password123"
)
friend_code_only = FriendCode.objects.create(
- friend_code="4444-4444-4444-4444",
- user=user_only,
- in_game_name="SoloGame"
+ friend_code="4444-4444-4444-4444", user=user_only, in_game_name="SoloGame"
)
self.client.logout()
self.client.login(username="onlyuser", password="password123")
@@ -492,7 +474,7 @@ class FriendCodeViewsTests(TestCase):
initiated_by=self.friend_code2,
is_closed=False,
rarity_icon=RARITY_MAPPING[5],
- rarity_level=5
+ rarity_level=5,
)
url = reverse("delete_friend_code", kwargs={"pk": self.friend_code2.pk})
response = self.client.post(url, {})
@@ -517,14 +499,10 @@ class FriendCodeViewsTests(TestCase):
def test_change_default_friend_code_view_not_owned(self):
"""A friend code that does not belong to the current user should result in a 404."""
other_user = get_user_model().objects.create_user(
- username="otheruser2",
- email="other2@example.com",
- password="password789"
+ username="otheruser2", email="other2@example.com", password="password789"
)
friend_code_other = FriendCode.objects.create(
- friend_code="1111-1111-1111-1111",
- user=other_user,
- in_game_name="NotMine"
+ friend_code="1111-1111-1111-1111", user=other_user, in_game_name="NotMine"
)
url = reverse("change_default_friend_code", kwargs={"pk": friend_code_other.pk})
response = self.client.post(url, {})
@@ -561,12 +539,12 @@ class FriendCodeViewsTests(TestCase):
other_user = get_user_model().objects.create_user(
username="otherdeluser",
email="otherdel@example.com",
- password="password321"
+ password="password321",
)
friend_code_other = FriendCode.objects.create(
friend_code="2222-2222-2222-2222",
user=other_user,
- in_game_name="OtherDelete"
+ in_game_name="OtherDelete",
)
url = reverse("delete_friend_code", kwargs={"pk": friend_code_other.pk})
response = self.client.get(url)
diff --git a/src/pkmntrade_club/accounts/urls.py b/src/pkmntrade_club/accounts/urls.py
index 8e9b106..202d789 100644
--- a/src/pkmntrade_club/accounts/urls.py
+++ b/src/pkmntrade_club/accounts/urls.py
@@ -9,8 +9,20 @@ from .views import (
urlpatterns = [
path("friend-codes/add/", AddFriendCodeView.as_view(), name="add_friend_code"),
- path("friend-codes/edit//", EditFriendCodeView.as_view(), name="edit_friend_code"),
- path("friend-codes/delete//", DeleteFriendCodeView.as_view(), name="delete_friend_code"),
- path("friend-codes/default//", ChangeDefaultFriendCodeView.as_view(), name="change_default_friend_code"),
+ path(
+ "friend-codes/edit//",
+ EditFriendCodeView.as_view(),
+ name="edit_friend_code",
+ ),
+ path(
+ "friend-codes/delete//",
+ DeleteFriendCodeView.as_view(),
+ name="delete_friend_code",
+ ),
+ path(
+ "friend-codes/default//",
+ ChangeDefaultFriendCodeView.as_view(),
+ name="change_default_friend_code",
+ ),
path("dashboard/", DashboardView.as_view(), name="dashboard"),
-]
\ No newline at end of file
+]
diff --git a/src/pkmntrade_club/accounts/views.py b/src/pkmntrade_club/accounts/views.py
index fbecc1e..34e81b7 100644
--- a/src/pkmntrade_club/accounts/views.py
+++ b/src/pkmntrade_club/accounts/views.py
@@ -1,9 +1,14 @@
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
-from django.urls import reverse_lazy
from django.shortcuts import redirect, get_object_or_404, render
-from django.views.generic import ListView, CreateView, DeleteView, View, TemplateView, UpdateView
-from pkmntrade_club.accounts.models import FriendCode, CustomUser
+from django.views.generic import (
+ CreateView,
+ DeleteView,
+ View,
+ TemplateView,
+ UpdateView,
+)
+from pkmntrade_club.accounts.models import FriendCode
from pkmntrade_club.accounts.forms import FriendCodeForm, UserSettingsForm
from django.db.models import Case, When, Value, BooleanField
from pkmntrade_club.trades.models import TradeOffer, TradeAcceptance
@@ -13,14 +18,17 @@ from pkmntrade_club.common.mixins import ReusablePaginationMixin
from django.urls import reverse
from django.utils.http import urlencode
+
class AddFriendCodeView(LoginRequiredMixin, CreateView):
"""
Add a new friend code for the current user. If the user does not yet have a default,
the newly added code will automatically become the default.
"""
+
model = FriendCode
form_class = FriendCodeForm
template_name = "friend_codes/add_friend_code.html"
+
def get_success_url(self):
base_url = reverse("dashboard")
return f"{base_url}?{urlencode({'tab': 'friend_codes'})}"
@@ -30,6 +38,7 @@ class AddFriendCodeView(LoginRequiredMixin, CreateView):
messages.success(self.request, "Friend code added successfully.")
return super().form_valid(form)
+
class DeleteFriendCodeView(LoginRequiredMixin, DeleteView):
"""
Remove an existing friend code.
@@ -37,9 +46,11 @@ class DeleteFriendCodeView(LoginRequiredMixin, DeleteView):
Also, prevent deletion if the friend code is either the only one or
is set as the default friend code.
"""
+
model = FriendCode
template_name = "friend_codes/confirm_delete_friend_code.html"
context_object_name = "friend_code"
+
def get_success_url(self):
base_url = reverse("dashboard")
return f"{base_url}?{urlencode({'tab': 'friend_codes'})}"
@@ -81,48 +92,59 @@ class DeleteFriendCodeView(LoginRequiredMixin, DeleteView):
if user.default_friend_code == self.object:
messages.error(
request,
- "Cannot delete your default friend code. Please set a different default first."
+ "Cannot delete your default friend code. Please set a different default first.",
)
return redirect(self.get_success_url())
- trade_offer_exists = TradeOffer.objects.filter(initiated_by_id=self.object.pk).exists()
- trade_acceptance_exists = TradeAcceptance.objects.filter(accepted_by_id=self.object.pk).exists()
+ trade_offer_exists = TradeOffer.objects.filter(
+ initiated_by_id=self.object.pk
+ ).exists()
+ trade_acceptance_exists = TradeAcceptance.objects.filter(
+ accepted_by_id=self.object.pk
+ ).exists()
if trade_offer_exists or trade_acceptance_exists:
messages.error(
request,
- "Cannot remove this friend code because there are existing trade offers associated with it."
+ "Cannot remove this friend code because there are existing trade offers associated with it.",
)
return redirect(self.get_success_url())
-
+
self.object.delete()
messages.success(request, "Friend code removed successfully.")
return redirect(self.get_success_url())
+
class ChangeDefaultFriendCodeView(LoginRequiredMixin, View):
"""
Change the default friend code for the current user.
"""
+
def post(self, request, *args, **kwargs):
friend_code_id = kwargs.get("pk")
- friend_code = get_object_or_404(FriendCode, pk=friend_code_id, user=request.user)
+ friend_code = get_object_or_404(
+ FriendCode, pk=friend_code_id, user=request.user
+ )
request.user.set_default_friend_code(friend_code)
messages.success(request, "Default friend code updated successfully.")
base_url = reverse("dashboard")
query_string = urlencode({"tab": "friend_codes"})
return redirect(f"{base_url}?{query_string}")
+
class EditFriendCodeView(LoginRequiredMixin, UpdateView):
"""
Edit the in-game name for a friend code.
The friend code itself is displayed as plain text.
Also includes "Set Default" and "Delete" buttons in the template.
"""
+
model = FriendCode
# Only the in_game_name field is editable
- fields = ['in_game_name']
+ fields = ["in_game_name"]
template_name = "friend_codes/edit_friend_code.html"
context_object_name = "friend_code"
+
def get_success_url(self):
base_url = reverse("dashboard")
return f"{base_url}?{urlencode({'tab': 'friend_codes'})}"
@@ -135,12 +157,16 @@ class EditFriendCodeView(LoginRequiredMixin, UpdateView):
messages.success(self.request, "Friend code updated successfully.")
return super().form_valid(form)
-class DashboardView(LoginRequiredMixin, FriendCodeRequiredMixin, ReusablePaginationMixin, TemplateView):
+
+class DashboardView(
+ LoginRequiredMixin, FriendCodeRequiredMixin, ReusablePaginationMixin, TemplateView
+):
template_name = "account/dashboard.html"
def post(self, request, *args, **kwargs):
- if 'update_settings' in request.POST:
+ if "update_settings" in request.POST:
from pkmntrade_club.accounts.forms import UserSettingsForm
+
form = UserSettingsForm(request.POST, instance=request.user)
if form.is_valid():
form.save()
@@ -156,21 +182,30 @@ class DashboardView(LoginRequiredMixin, FriendCodeRequiredMixin, ReusablePaginat
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()
+ 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()
+ 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.")
+ 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):
selected_friend_code = self.get_selected_friend_code()
- queryset = TradeOffer.objects.filter(initiated_by=selected_friend_code, is_closed=False)
+ queryset = TradeOffer.objects.filter(
+ initiated_by=selected_friend_code, is_closed=False
+ )
object_list, pagination_context = self.paginate_data(queryset, int(page_param))
return {"object_list": object_list, "page_obj": pagination_context}
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,
@@ -179,7 +214,8 @@ class DashboardView(LoginRequiredMixin, FriendCodeRequiredMixin, ReusablePaginat
TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR,
]
involved = TradeAcceptance.objects.filter(
- Q(trade_offer__initiated_by=selected_friend_code) | Q(accepted_by=selected_friend_code)
+ 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)
@@ -187,12 +223,19 @@ class DashboardView(LoginRequiredMixin, FriendCodeRequiredMixin, ReusablePaginat
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])
+ 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],
+ )
)
object_list, pagination_context = self.paginate_data(waiting, int(page_param))
return {"object_list": object_list, "page_obj": pagination_context}
@@ -201,12 +244,19 @@ class DashboardView(LoginRequiredMixin, FriendCodeRequiredMixin, ReusablePaginat
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])
+ 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"))
object_list, pagination_context = self.paginate_data(others, int(page_param))
@@ -214,12 +264,15 @@ class DashboardView(LoginRequiredMixin, FriendCodeRequiredMixin, ReusablePaginat
def get_closed_offers_paginated(self, page_param):
selected_friend_code = self.get_selected_friend_code()
- queryset = TradeOffer.objects.filter(initiated_by=selected_friend_code, is_closed=True)
+ queryset = TradeOffer.objects.filter(
+ initiated_by=selected_friend_code, is_closed=True
+ )
object_list, pagination_context = self.paginate_data(queryset, int(page_param))
return {"object_list": object_list, "page_obj": pagination_context}
def get_closed_acceptances_paginated(self, page_param):
from django.db.models import Q
+
selected_friend_code = self.get_selected_friend_code()
terminal_success_states = [
TradeAcceptance.AcceptanceState.THANKED_BY_INITIATOR,
@@ -227,28 +280,45 @@ class DashboardView(LoginRequiredMixin, FriendCodeRequiredMixin, ReusablePaginat
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
+ Q(trade_offer__initiated_by=selected_friend_code)
+ | Q(accepted_by=selected_friend_code),
+ state__in=terminal_success_states,
).order_by("-updated_at")
- object_list, pagination_context = self.paginate_data(acceptance_qs, int(page_param))
+ object_list, pagination_context = self.paginate_data(
+ acceptance_qs, int(page_param)
+ )
return {"object_list": object_list, "page_obj": pagination_context}
def get_rejected_by_me_paginated(self, page_param):
from django.db.models import Q
+
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)
+ 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")
object_list, pagination_context = self.paginate_data(rejection, int(page_param))
return {"object_list": object_list, "page_obj": pagination_context}
def get_rejected_by_them_paginated(self, page_param):
from django.db.models import Q
+
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)
+ 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")
object_list, pagination_context = self.paginate_data(rejection, int(page_param))
return {"object_list": object_list, "page_obj": pagination_context}
@@ -258,19 +328,19 @@ class DashboardView(LoginRequiredMixin, FriendCodeRequiredMixin, ReusablePaginat
request = self.request
selected_friend_code = self.get_selected_friend_code()
context["selected_friend_code"] = selected_friend_code
-
+
# Get the default friend code's primary key if it exists
default_pk = getattr(request.user.default_friend_code, "pk", None)
-
+
# Annotate friend codes with is_default flag
context["friend_codes"] = request.user.friend_codes.all().annotate(
is_default=Case(
When(pk=default_pk, then=Value(True)),
default=Value(False),
- output_field=BooleanField()
+ output_field=BooleanField(),
)
)
-
+
ajax_section = request.GET.get("ajax_section")
if ajax_section == "dashboard_offers":
offers_page = request.GET.get("page", 1)
@@ -307,14 +377,28 @@ class DashboardView(LoginRequiredMixin, FriendCodeRequiredMixin, ReusablePaginat
else:
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 pkmntrade_club.accounts.forms import UserSettingsForm
+ 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
+ )
+
context["settings_form"] = UserSettingsForm(instance=request.user)
context["active_tab"] = request.GET.get("tab", "dash")
return context
@@ -327,9 +411,13 @@ class DashboardView(LoginRequiredMixin, FriendCodeRequiredMixin, ReusablePaginat
if ajax_section == "dashboard_offers":
fragment_context = context.get("dashboard_offers_paginated", {})
elif ajax_section == "waiting_acceptances":
- fragment_context = context.get("trade_acceptances_waiting_paginated", {})
+ fragment_context = context.get(
+ "trade_acceptances_waiting_paginated", {}
+ )
elif ajax_section == "other_party_acceptances":
- fragment_context = context.get("other_party_trade_acceptances_paginated", {})
+ fragment_context = context.get(
+ "other_party_trade_acceptances_paginated", {}
+ )
elif ajax_section == "closed_offers":
fragment_context = context.get("closed_offers_paginated", {})
elif ajax_section == "closed_acceptances":
@@ -342,8 +430,12 @@ class DashboardView(LoginRequiredMixin, FriendCodeRequiredMixin, ReusablePaginat
fragment_context = {}
if fragment_context:
- return render(request, "trades/_trade_offer_list.html", {
- "offers": fragment_context.get("object_list", []),
- "page_obj": fragment_context.get("page_obj")
- })
- return super().get(request, *args, **kwargs)
\ No newline at end of file
+ return render(
+ request,
+ "trades/_trade_offer_list.html",
+ {
+ "offers": fragment_context.get("object_list", []),
+ "page_obj": fragment_context.get("page_obj"),
+ },
+ )
+ return super().get(request, *args, **kwargs)
diff --git a/src/pkmntrade_club/cards/mixins.py b/src/pkmntrade_club/cards/mixins.py
index 575d62c..18d887c 100644
--- a/src/pkmntrade_club/cards/mixins.py
+++ b/src/pkmntrade_club/cards/mixins.py
@@ -1,10 +1,12 @@
from math import ceil
+
class ReusablePaginationMixin:
"""
A mixin that encapsulates reusable pagination logic.
Use in Django ListViews to generate custom pagination context.
"""
+
per_page = 10 # Default; can be overridden in your view.
def paginate_data(self, data_list, page_number):
@@ -39,4 +41,4 @@ class ReusablePaginationMixin:
"next_page": page_number + 1 if page_number < num_pages else num_pages,
"paginator": {"num_pages": num_pages},
}
- return items, pagination_context
\ No newline at end of file
+ return items, pagination_context
diff --git a/src/pkmntrade_club/cards/models.py b/src/pkmntrade_club/cards/models.py
index b3376ef..d443a6a 100644
--- a/src/pkmntrade_club/cards/models.py
+++ b/src/pkmntrade_club/cards/models.py
@@ -106,7 +106,7 @@ class AttackCost(models.Model):
unique_together = ("attack", "energy")
def __str__(self):
- return f"{self.attack.name} {_("requires")} {self.quantity} {self.energy.name}"
+ return f"{self.attack.name} {_('requires')} {self.quantity} {self.energy.name}"
class Attack(TranslatableModel):
diff --git a/src/pkmntrade_club/cards/signals.py b/src/pkmntrade_club/cards/signals.py
index dd87f2a..af9a8ee 100644
--- a/src/pkmntrade_club/cards/signals.py
+++ b/src/pkmntrade_club/cards/signals.py
@@ -2,15 +2,16 @@ from django.db.models.signals import m2m_changed
from django.dispatch import receiver
from .models import Card
+
def color_is_dark(bg_color):
"""
Determine if a given hexadecimal color is dark.
This function accepts a 6-digit hex color string (with or without a leading '#').
It calculates the brightness using the formula:
-
+
brightness = (0.299 * red) + (0.587 * green) + (0.114 * blue)
-
+
A brightness value less than or equal to 186 indicates that the color is dark.
Args:
@@ -20,7 +21,7 @@ def color_is_dark(bg_color):
bool: True if the color is dark (brightness <= 186), False otherwise.
"""
# Remove the leading '#' if it exists.
- color = bg_color[1:7] if bg_color[0] == '#' else bg_color
+ color = bg_color[1:7] if bg_color[0] == "#" else bg_color
# Convert the hex color components to integers.
r = int(color[0:2], 16)
@@ -29,9 +30,10 @@ def color_is_dark(bg_color):
# Compute brightness based on weighted RGB values.
brightness = (r * 0.299) + (g * 0.587) + (b * 0.114)
-
+
return brightness <= 200
+
@receiver(m2m_changed, sender=Card.decks.through)
def update_card_style(sender, instance, action, **kwargs):
if action == "post_add":
@@ -41,11 +43,15 @@ def update_card_style(sender, instance, action, **kwargs):
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)});"
+ instance.style = (
+ f"background: linear-gradient(to right, {', '.join(hex_colors)});"
+ )
else:
- instance.style = "background: linear-gradient(to right, #AAAAAA, #AAAAAA, #AAAAAA);"
+ instance.style = (
+ "background: linear-gradient(to right, #AAAAAA, #AAAAAA, #AAAAAA);"
+ )
if not color_is_dark(decks.first().hex_color):
instance.style += "color: var(--color-gray-700); text-shadow: 0 0 0 var(--color-gray-700);"
else:
instance.style += "text-shadow: 0 0 0 #fff;"
- instance.save(update_fields=["style"])
\ No newline at end of file
+ instance.save(update_fields=["style"])
diff --git a/src/pkmntrade_club/cards/templatetags/card_badge.py b/src/pkmntrade_club/cards/templatetags/card_badge.py
index ace3b17..adc2930 100644
--- a/src/pkmntrade_club/cards/templatetags/card_badge.py
+++ b/src/pkmntrade_club/cards/templatetags/card_badge.py
@@ -6,41 +6,43 @@ from django.urls import reverse_lazy
register = template.Library()
+
@register.inclusion_tag("templatetags/card_badge.html", takes_context=True)
def card_badge(context, card, quantity=None, expanded=False):
"""
Renders a card badge.
"""
- url = reverse_lazy('cards:card_detail', args=[card.pk])
+ url = reverse_lazy("cards:card_detail", args=[card.pk])
tag_context = {
- 'quantity': quantity,
- 'style': card.style,
- 'name': card.name,
- 'rarity': card.rarity_icon,
- 'cardset': card.cardset,
- 'expanded': expanded,
- 'cache_key': f'card_badge_{card.pk}_{quantity}_{expanded}',
- 'url': url,
+ "quantity": quantity,
+ "style": card.style,
+ "name": card.name,
+ "rarity": card.rarity_icon,
+ "cardset": card.cardset,
+ "expanded": expanded,
+ "cache_key": f"card_badge_{card.pk}_{quantity}_{expanded}",
+ "url": url,
}
context.update(tag_context)
return context
+
@register.filter
def card_badge_inline(card, quantity=None):
"""
Renders an inline card badge by directly rendering the template.
"""
- url = reverse_lazy('cards:card_detail', args=[card.pk])
+ url = reverse_lazy("cards:card_detail", args=[card.pk])
tag_context = {
- 'quantity': quantity,
- 'style': card.style,
- 'name': card.name,
- 'rarity': card.rarity_icon,
- 'cardset': card.cardset,
- 'expanded': True,
- 'cache_key': f'card_badge_{card.pk}_{quantity}_{True}',
- 'CACHE_TIMEOUT': settings.CACHE_TIMEOUT,
- 'url': url,
+ "quantity": quantity,
+ "style": card.style,
+ "name": card.name,
+ "rarity": card.rarity_icon,
+ "cardset": card.cardset,
+ "expanded": True,
+ "cache_key": f"card_badge_{card.pk}_{quantity}_{True}",
+ "CACHE_TIMEOUT": settings.CACHE_TIMEOUT,
+ "url": url,
}
html = render_to_string("templatetags/card_badge.html", tag_context)
- return mark_safe(html)
\ No newline at end of file
+ return mark_safe(html)
diff --git a/src/pkmntrade_club/cards/templatetags/card_multiselect.py b/src/pkmntrade_club/cards/templatetags/card_multiselect.py
index dc03500..65b43a3 100644
--- a/src/pkmntrade_club/cards/templatetags/card_multiselect.py
+++ b/src/pkmntrade_club/cards/templatetags/card_multiselect.py
@@ -5,20 +5,26 @@ from django.db.models.query import QuerySet
import json
import hashlib
import logging
+
register = template.Library()
+
@register.filter
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()
+ 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):
+
+@register.inclusion_tag("templatetags/card_multiselect.html", takes_context=True)
+def card_multiselect(
+ context, field_name, label, placeholder, cards=None, selected_values=None
+):
"""
Prepares context for rendering a card multiselect input.
Database querying and rendering are handled within the template's cache block.
@@ -28,15 +34,15 @@ def card_multiselect(context, field_name, label, placeholder, cards=None, select
selected_cards = {}
for val in selected_values:
- parts = str(val).split(':')
+ parts = str(val).split(":")
if len(parts) >= 1 and parts[0]:
card_id = parts[0]
quantity = parts[1] if len(parts) > 1 else 1
selected_cards[str(card_id)] = quantity
- effective_field_name = field_name if field_name is not None else 'card_multiselect'
- effective_label = label if label is not None else 'Card'
- effective_placeholder = placeholder if placeholder is not None else 'Select Cards'
+ effective_field_name = field_name if field_name is not None else "card_multiselect"
+ effective_label = label if label is not None else "Card"
+ effective_placeholder = placeholder if placeholder is not None else "Select Cards"
selected_cards_key_part = json.dumps(selected_cards, sort_keys=True)
@@ -45,28 +51,32 @@ def card_multiselect(context, field_name, label, placeholder, cards=None, select
if has_passed_cards:
try:
query_string = str(cards.query)
- passed_cards_identifier = hashlib.sha256(query_string.encode('utf-8')).hexdigest()
+ passed_cards_identifier = hashlib.sha256(
+ query_string.encode("utf-8")
+ ).hexdigest()
except Exception as e:
- logging.warning(f"Could not generate query hash for card_multiselect. Error: {e}")
- passed_cards_identifier = 'specific_qs_fallback_' + str(uuid.uuid4())
+ 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'
+ 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,
+ "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
\ No newline at end of file
+ return context # Return the MODIFIED original context
diff --git a/src/pkmntrade_club/cards/tests.py b/src/pkmntrade_club/cards/tests.py
index 75e73b8..85219a2 100644
--- a/src/pkmntrade_club/cards/tests.py
+++ b/src/pkmntrade_club/cards/tests.py
@@ -6,11 +6,21 @@ from django.urls import reverse
from django.utils import timezone
from pkmntrade_club.accounts.models import CustomUser, FriendCode
-from pkmntrade_club.cards.models import Card, Deck, DeckNameTranslation, CardNameTranslation
-from pkmntrade_club.trades.models import TradeOffer, TradeOfferHaveCard, TradeOfferWantCard
-from pkmntrade_club.cards.templatetags import card_badge, card_multiselect
+from pkmntrade_club.cards.models import (
+ Card,
+ Deck,
+ DeckNameTranslation,
+ CardNameTranslation,
+)
+from pkmntrade_club.trades.models import (
+ TradeOffer,
+ TradeOfferHaveCard,
+ TradeOfferWantCard,
+)
+from pkmntrade_club.cards.templatetags import card_multiselect
from tests.utils.rarity import RARITY_MAPPING
+
class CardsModelsTests(TestCase):
def setUp(self):
self.deck = Deck.objects.create(
@@ -22,7 +32,7 @@ class CardsModelsTests(TestCase):
cardnum=1,
style="default",
rarity_icon=RARITY_MAPPING[1],
- rarity_level=1
+ rarity_level=1,
)
# Establish many-to-many relationship.
self.card.decks.add(self.deck)
@@ -44,7 +54,8 @@ class CardsModelsTests(TestCase):
card_translation = CardNameTranslation.objects.create(
name="Card Translated", card=self.card, language="en"
)
- self.assertEqual(str(card_translation), "Card Translated")
+ self.assertEqual(str(card_translation), "Card Translated")
+
class CardTemplatetagsTests(TestCase):
def setUp(self):
@@ -55,12 +66,12 @@ class CardTemplatetagsTests(TestCase):
cardnum=2,
style="background: green;",
rarity_icon="☆",
- rarity_level=2
+ rarity_level=2,
)
def test_card_badge_inclusion_tag(self):
"""Test the card_badge inclusion tag renders correctly."""
- template_str = '{% load card_badge %}{% card_badge card quantity=3 %}'
+ template_str = "{% load card_badge %}{% card_badge card quantity=3 %}"
t = Template(template_str)
c = Context({"card": self.card})
rendered = t.render(c)
@@ -71,7 +82,7 @@ class CardTemplatetagsTests(TestCase):
def test_card_badge_inline_filter(self):
"""Test the card_badge_inline filter returns safe HTML with correct data."""
- template_str = '{% load card_badge %}{{ card|card_badge_inline:5 }}'
+ template_str = "{% load card_badge %}{{ card|card_badge_inline:5 }}"
t = Template(template_str)
c = Context({"card": self.card})
rendered = t.render(c)
@@ -142,7 +153,8 @@ class CardTemplatetagsTests(TestCase):
selected_values=[],
)
# Verify that the context's cards match those in the database.
- self.assertEqual(list(context["cards"]), default_cards)
+ self.assertEqual(list(context["cards"]), default_cards)
+
class CardsViewsTests(TestCase):
def setUp(self):
@@ -161,7 +173,7 @@ class CardsViewsTests(TestCase):
cardnum=1,
style="default",
rarity_icon=RARITY_MAPPING[1],
- rarity_level=1
+ rarity_level=1,
)
def test_card_detail_view_context(self):
@@ -198,9 +210,7 @@ class CardsViewsTests(TestCase):
Helper method to create a trade offer for the 'have' side with a custom updated_at.
"""
offer = TradeOffer.objects.create(initiated_by=self.friendcode)
- TradeOfferHaveCard.objects.create(
- trade_offer=offer, card=self.card, quantity=1
- )
+ TradeOfferHaveCard.objects.create(trade_offer=offer, card=self.card, quantity=1)
# Adjust updated_at so that ordering can be tested.
new_time = timezone.now() + timedelta(minutes=updated_delta_minutes)
TradeOffer.objects.filter(pk=offer.pk).update(updated_at=new_time)
@@ -212,9 +222,7 @@ class CardsViewsTests(TestCase):
Helper method to create a trade offer for the 'want' side with a custom updated_at.
"""
offer = TradeOffer.objects.create(initiated_by=self.friendcode)
- TradeOfferWantCard.objects.create(
- trade_offer=offer, card=self.card, quantity=1
- )
+ TradeOfferWantCard.objects.create(trade_offer=offer, card=self.card, quantity=1)
new_time = timezone.now() + timedelta(minutes=updated_delta_minutes)
TradeOffer.objects.filter(pk=offer.pk).update(updated_at=new_time)
offer.refresh_from_db()
@@ -285,4 +293,4 @@ class CardsViewsTests(TestCase):
trade_offers_oldest = response_oldest.context.get("trade_offers")
self.assertEqual(len(trade_offers_oldest), 2)
self.assertEqual(trade_offers_oldest[0].pk, offer1.pk)
- self.assertEqual(trade_offers_oldest[1].pk, offer2.pk)
\ No newline at end of file
+ self.assertEqual(trade_offers_oldest[1].pk, offer2.pk)
diff --git a/src/pkmntrade_club/cards/urls.py b/src/pkmntrade_club/cards/urls.py
index 599427f..3338d08 100644
--- a/src/pkmntrade_club/cards/urls.py
+++ b/src/pkmntrade_club/cards/urls.py
@@ -9,8 +9,16 @@ from .views import (
app_name = "cards"
urlpatterns = [
- path('', CardListView.as_view(), name='card_list'),
- path('/', CardDetailView.as_view(), name='card_detail'),
- path('/trade-offers-have/', TradeOfferHaveCardListView.as_view(), name='card_trade_offer_have_list'),
- path('/trade-offers-want/', TradeOfferWantCardListView.as_view(), name='card_trade_offer_want_list'),
+ path("", CardListView.as_view(), name="card_list"),
+ path("/", CardDetailView.as_view(), name="card_detail"),
+ path(
+ "/trade-offers-have/",
+ TradeOfferHaveCardListView.as_view(),
+ name="card_trade_offer_have_list",
+ ),
+ path(
+ "/trade-offers-want/",
+ TradeOfferWantCardListView.as_view(),
+ name="card_trade_offer_want_list",
+ ),
]
diff --git a/src/pkmntrade_club/cards/views.py b/src/pkmntrade_club/cards/views.py
index 08a7bb0..f4c1cf2 100644
--- a/src/pkmntrade_club/cards/views.py
+++ b/src/pkmntrade_club/cards/views.py
@@ -1,12 +1,14 @@
-from django.views.generic import TemplateView
-from django.urls import reverse_lazy
-from django.views.generic import UpdateView, DeleteView, CreateView, ListView, DetailView
+from django.views.generic import (
+ ListView,
+ DetailView,
+)
from pkmntrade_club.cards.models import Card
from pkmntrade_club.trades.models import TradeOffer
from pkmntrade_club.common.mixins import ReusablePaginationMixin
from django.views import View
from django.shortcuts import get_object_or_404, render
+
class CardDetailView(DetailView):
model = Card
template_name = "cards/card_detail.html"
@@ -16,15 +18,20 @@ class CardDetailView(DetailView):
context = super().get_context_data(**kwargs)
card = self.get_object()
# Count of trade offers where the card appears as a "have" in a trade.
- context['trade_offer_have_count'] = TradeOffer.objects.filter(
- trade_offer_have_cards__card=card
- ).distinct().count()
+ context["trade_offer_have_count"] = (
+ TradeOffer.objects.filter(trade_offer_have_cards__card=card)
+ .distinct()
+ .count()
+ )
# Count of trade offers where the card appears as a "want" in a trade.
- context['trade_offer_want_count'] = TradeOffer.objects.filter(
- trade_offer_want_cards__card=card
- ).distinct().count()
+ context["trade_offer_want_count"] = (
+ TradeOffer.objects.filter(trade_offer_want_cards__card=card)
+ .distinct()
+ .count()
+ )
return context
+
class TradeOfferHaveCardListView(ReusablePaginationMixin, View):
def get(self, request, pk):
card = get_object_or_404(Card, pk=pk)
@@ -48,6 +55,7 @@ class TradeOfferHaveCardListView(ReusablePaginationMixin, View):
# Render the partial template to be injected via AJAX
return render(request, "trades/_trade_offer_list.html", context)
+
class TradeOfferWantCardListView(ReusablePaginationMixin, View):
def get(self, request, pk):
card = get_object_or_404(Card, pk=pk)
@@ -72,6 +80,8 @@ class TradeOfferWantCardListView(ReusablePaginationMixin, View):
}
# Render the partial template containing the new pagination controls
return render(request, "trades/_trade_offer_list.html", context)
+
+
class CardListView(ReusablePaginationMixin, ListView):
model = Card
# Removed built-in pagination; using custom mixin instead
@@ -119,12 +129,20 @@ class CardListView(ReusablePaginationMixin, ListView):
flat_cards.sort(key=lambda x: x["group"].lower())
elif group_by == "rarity":
for card in all_cards:
- flat_cards.append({"group": card.rarity_icon, "sort_group": card.rarity_level, "card": card})
+ flat_cards.append(
+ {
+ "group": card.rarity_icon,
+ "sort_group": card.rarity_level,
+ "card": card,
+ }
+ )
flat_cards.sort(key=lambda x: x["sort_group"], reverse=True)
page_number = self.get_page_number()
self.per_page = 36
- page_flat_cards, pagination_context = self.paginate_data(flat_cards, page_number)
+ page_flat_cards, pagination_context = self.paginate_data(
+ flat_cards, page_number
+ )
page_groups = []
for item in page_flat_cards:
@@ -141,8 +159,10 @@ class CardListView(ReusablePaginationMixin, ListView):
else:
page_number = self.get_page_number()
self.per_page = 36
- paginated_cards, pagination_context = self.paginate_data(self.get_queryset(), page_number)
+ paginated_cards, pagination_context = self.paginate_data(
+ self.get_queryset(), page_number
+ )
context["cards"] = paginated_cards
context["page_obj"] = pagination_context
context["object_list"] = self.get_queryset()
- return context
\ No newline at end of file
+ return context
diff --git a/src/pkmntrade_club/common/apps.py b/src/pkmntrade_club/common/apps.py
index 6e2cde5..7427944 100644
--- a/src/pkmntrade_club/common/apps.py
+++ b/src/pkmntrade_club/common/apps.py
@@ -5,4 +5,4 @@ class CommonConfig(AppConfig):
name = "pkmntrade_club.common"
def ready(self):
- pass
\ No newline at end of file
+ pass
diff --git a/src/pkmntrade_club/common/context_processors.py b/src/pkmntrade_club/common/context_processors.py
index 7950ded..b286201 100644
--- a/src/pkmntrade_club/common/context_processors.py
+++ b/src/pkmntrade_club/common/context_processors.py
@@ -1,12 +1,14 @@
from django.conf import settings
+
def cache_settings(request):
return {
- 'CACHE_TIMEOUT': settings.CACHE_TIMEOUT,
+ "CACHE_TIMEOUT": settings.CACHE_TIMEOUT,
}
+
def version_info(request):
return {
- 'VERSION': settings.VERSION,
- 'VERSION_INFO': settings.VERSION_INFO,
- }
\ No newline at end of file
+ "VERSION": settings.VERSION,
+ "VERSION_INFO": settings.VERSION_INFO,
+ }
diff --git a/src/pkmntrade_club/common/mixins.py b/src/pkmntrade_club/common/mixins.py
index 6290fdc..0df467a 100644
--- a/src/pkmntrade_club/common/mixins.py
+++ b/src/pkmntrade_club/common/mixins.py
@@ -26,9 +26,13 @@ class ReusablePaginationMixin:
"number": page.number,
"has_previous": page.has_previous(),
"has_next": page.has_next(),
- "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 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 paginator.num_pages
+ ),
"paginator": {"num_pages": paginator.num_pages},
- "count": paginator.count
+ "count": paginator.count,
}
- return page.object_list, pagination_context
\ No newline at end of file
+ return page.object_list, pagination_context
diff --git a/src/pkmntrade_club/common/templatetags/pagination_tags.py b/src/pkmntrade_club/common/templatetags/pagination_tags.py
index a9a2890..0840881 100644
--- a/src/pkmntrade_club/common/templatetags/pagination_tags.py
+++ b/src/pkmntrade_club/common/templatetags/pagination_tags.py
@@ -2,9 +2,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}
\ No newline at end of file
+ return {"page_obj": page_obj, "hide_if_one_page": hide_if_one_page}
diff --git a/src/pkmntrade_club/django_project/__init__.py b/src/pkmntrade_club/django_project/__init__.py
index 1e3599b..5568b6d 100644
--- a/src/pkmntrade_club/django_project/__init__.py
+++ b/src/pkmntrade_club/django_project/__init__.py
@@ -2,4 +2,4 @@
# Django starts so that shared_task will use this app.
from .celery import app as celery_app
-__all__ = ('celery_app',)
\ No newline at end of file
+__all__ = ("celery_app",)
diff --git a/src/pkmntrade_club/django_project/asgi.py b/src/pkmntrade_club/django_project/asgi.py
index 11400ef..715ac8b 100644
--- a/src/pkmntrade_club/django_project/asgi.py
+++ b/src/pkmntrade_club/django_project/asgi.py
@@ -2,6 +2,8 @@ import os
from django.core.asgi import get_asgi_application
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pkmntrade_club.django_project.settings')
+os.environ.setdefault(
+ "DJANGO_SETTINGS_MODULE", "pkmntrade_club.django_project.settings"
+)
application = get_asgi_application()
diff --git a/src/pkmntrade_club/django_project/celery.py b/src/pkmntrade_club/django_project/celery.py
index 44c9d6c..7896883 100644
--- a/src/pkmntrade_club/django_project/celery.py
+++ b/src/pkmntrade_club/django_project/celery.py
@@ -3,15 +3,17 @@ import os
from celery import Celery
# Set the default Django settings module for the 'celery' program.
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pkmntrade_club.django_project.settings')
+os.environ.setdefault(
+ "DJANGO_SETTINGS_MODULE", "pkmntrade_club.django_project.settings"
+)
-app = Celery('django_project')
+app = Celery("django_project")
# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
-app.config_from_object('django.conf:settings', namespace='CELERY')
+app.config_from_object("django.conf:settings", namespace="CELERY")
# Load task modules from all registered Django apps.
app.autodiscover_tasks()
@@ -19,4 +21,4 @@ app.autodiscover_tasks()
@app.task(bind=True, ignore_result=True)
def debug_task(self):
- print(f'Request: {self.request!r}')
\ No newline at end of file
+ print(f"Request: {self.request!r}")
diff --git a/src/pkmntrade_club/django_project/settings.py b/src/pkmntrade_club/django_project/settings.py
index d048108..eb152d8 100644
--- a/src/pkmntrade_club/django_project/settings.py
+++ b/src/pkmntrade_club/django_project/settings.py
@@ -9,67 +9,76 @@ from pkmntrade_club._version import __version__, get_version_info
# set default values to local dev values
env = environ.Env(
- DEBUG=(bool, False), # MUST STAY FALSE FOR DEFAULT FOR SECURITY REASONS (e.g. if app can't access .env, prevent showing debug output)
+ DEBUG=(
+ bool,
+ False,
+ ), # MUST STAY FALSE FOR DEFAULT FOR SECURITY REASONS (e.g. if app can't access .env, prevent showing debug output)
DISABLE_SIGNUPS=(bool, True),
DISABLE_CACHE=(bool, True),
- DJANGO_DATABASE_URL=(str, 'postgresql://postgres@localhost:5432/postgres?sslmode=disable'),
- DJANGO_EMAIL_HOST=(str, ''),
+ DJANGO_DATABASE_URL=(
+ str,
+ "postgresql://postgres@localhost:5432/postgres?sslmode=disable",
+ ),
+ DJANGO_EMAIL_HOST=(str, ""),
DJANGO_EMAIL_PORT=(int, 587),
- DJANGO_EMAIL_USER=(str, ''),
- DJANGO_EMAIL_PASSWORD=(str, ''),
+ DJANGO_EMAIL_USER=(str, ""),
+ DJANGO_EMAIL_PASSWORD=(str, ""),
DJANGO_EMAIL_USE_TLS=(bool, True),
- DJANGO_DEFAULT_FROM_EMAIL=(str, ''),
- DJANGO_EMAIL_SUBJECT_PREFIX=(str, ''),
- SECRET_KEY=(str, '0000000000000000000000000000000000000000000000000000000000000000'),
- ALLOWED_HOSTS=(str, 'localhost,127.0.0.1'),
- PUBLIC_HOST=(str, 'localhost'),
- ACCOUNT_EMAIL_VERIFICATION=(str, 'none'),
- SCHEME=(str, 'http'),
- REDIS_URL=(str, 'redis://localhost:6379'),
+ DJANGO_DEFAULT_FROM_EMAIL=(str, ""),
+ DJANGO_EMAIL_SUBJECT_PREFIX=(str, ""),
+ SECRET_KEY=(
+ str,
+ "0000000000000000000000000000000000000000000000000000000000000000",
+ ),
+ ALLOWED_HOSTS=(str, "localhost,127.0.0.1"),
+ PUBLIC_HOST=(str, "localhost"),
+ ACCOUNT_EMAIL_VERIFICATION=(str, "none"),
+ SCHEME=(str, "http"),
+ REDIS_URL=(str, "redis://localhost:6379"),
CACHE_TIMEOUT=(int, 604800),
- TIME_ZONE=(str, 'America/Los_Angeles'),
+ TIME_ZONE=(str, "America/Los_Angeles"),
)
LOGGING = {
- 'version': 1,
- 'disable_existing_loggers': False,
- 'formatters': {
- 'verbose': {
- 'format': '%(asctime)s %(name)-12s %(levelname)-8s %(message)s',
+ "version": 1,
+ "disable_existing_loggers": False,
+ "formatters": {
+ "verbose": {
+ "format": "%(asctime)s %(name)-12s %(levelname)-8s %(message)s",
},
},
- 'handlers': {
- 'console': {
- 'level': 'INFO',
- 'class': 'logging.StreamHandler',
- 'stream': sys.stdout,
- 'formatter': 'verbose',
- 'filters': [],
+ "handlers": {
+ "console": {
+ "level": "INFO",
+ "class": "logging.StreamHandler",
+ "stream": sys.stdout,
+ "formatter": "verbose",
+ "filters": [],
},
},
- 'loggers': {
- 'django': {
- 'handlers': ['console'],
- 'level': 'INFO',
+ "loggers": {
+ "django": {
+ "handlers": ["console"],
+ "level": "INFO",
},
- 'django.server': {
- 'handlers': ['console'],
- 'level': 'INFO',
- 'propagate': False,
+ "django.server": {
+ "handlers": ["console"],
+ "level": "INFO",
+ "propagate": False,
},
- 'granian.access': {
- 'handlers': ['console'],
- 'level': 'INFO',
- 'propagate': False,
+ "granian.access": {
+ "handlers": ["console"],
+ "level": "INFO",
+ "propagate": False,
},
- '_granian': {
- 'handlers': ['console'],
- 'level': 'INFO',
- 'propagate': False,
+ "_granian": {
+ "handlers": ["console"],
+ "level": "INFO",
+ "propagate": False,
},
- '': {
- 'handlers': ['console'],
- 'level': 'INFO',
+ "": {
+ "handlers": ["console"],
+ "level": "INFO",
},
},
}
@@ -78,14 +87,14 @@ LOGGING = {
BASE_DIR = Path(__file__).resolve().parent.parent
# Take environment variables from .env file
-environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
+environ.Env.read_env(os.path.join(BASE_DIR, ".env"))
-SCHEME = env('SCHEME')
-PUBLIC_HOST = env('PUBLIC_HOST')
-REDIS_URL = env('REDIS_URL')
-CACHE_TIMEOUT = env('CACHE_TIMEOUT')
-DISABLE_SIGNUPS = env('DISABLE_SIGNUPS')
-DISABLE_CACHE = env('DISABLE_CACHE')
+SCHEME = env("SCHEME")
+PUBLIC_HOST = env("PUBLIC_HOST")
+REDIS_URL = env("REDIS_URL")
+CACHE_TIMEOUT = env("CACHE_TIMEOUT")
+DISABLE_SIGNUPS = env("DISABLE_SIGNUPS")
+DISABLE_CACHE = env("DISABLE_CACHE")
VERSION = __version__
VERSION_INFO = get_version_info()
@@ -95,34 +104,38 @@ VERSION_INFO = get_version_info()
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
# SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = env('SECRET_KEY')
+SECRET_KEY = env("SECRET_KEY")
# https://docs.djangoproject.com/en/dev/ref/settings/#debug
# SECURITY WARNING: don't run with debug turned on in production!
-DEBUG = env('DEBUG')
+DEBUG = env("DEBUG")
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
-ALLOWED_HOSTS = env('ALLOWED_HOSTS').split(',')
+ALLOWED_HOSTS = env("ALLOWED_HOSTS").split(",")
try:
current_web_worker_hostname = socket.gethostname()
ALLOWED_HOSTS.append(current_web_worker_hostname)
- logging.getLogger(__name__).info(f"Added {current_web_worker_hostname} to allowed hosts.")
+ logging.getLogger(__name__).info(
+ f"Added {current_web_worker_hostname} to allowed hosts."
+ )
except Exception:
- logging.getLogger(__name__).info(f"Error determining server hostname for allowed hosts.")
+ logging.getLogger(__name__).info(
+ "Error determining server hostname for allowed hosts."
+ )
CSRF_TRUSTED_ORIGINS = [f"{SCHEME}://{PUBLIC_HOST}"]
-SHORTHAND_DATETIME_FORMAT = 'Y-m-d P'
-SHORTHAND_DATE_FORMAT = 'Y-m-d'
+SHORTHAND_DATETIME_FORMAT = "Y-m-d P"
+SHORTHAND_DATE_FORMAT = "Y-m-d"
FIRST_PARTY_APPS = [
- 'pkmntrade_club.accounts',
- 'pkmntrade_club.cards',
- 'pkmntrade_club.common',
- 'pkmntrade_club.home',
- 'pkmntrade_club.theme',
- 'pkmntrade_club.trades',
+ "pkmntrade_club.accounts",
+ "pkmntrade_club.cards",
+ "pkmntrade_club.common",
+ "pkmntrade_club.home",
+ "pkmntrade_club.theme",
+ "pkmntrade_club.trades",
]
# Application definition
@@ -140,20 +153,20 @@ INSTALLED_APPS = [
"django_celery_beat",
"allauth",
"allauth.account",
- 'allauth.socialaccount.providers.google',
+ "allauth.socialaccount.providers.google",
"crispy_forms",
"crispy_tailwind",
"tailwind",
"django_linear_migrations",
- 'health_check',
- 'health_check.db',
- 'health_check.cache',
- 'health_check.storage',
- 'health_check.contrib.migrations',
- 'health_check.contrib.celery',
- 'health_check.contrib.celery_ping',
- 'health_check.contrib.psutil',
- 'health_check.contrib.redis',
+ "health_check",
+ "health_check.db",
+ "health_check.cache",
+ "health_check.storage",
+ "health_check.contrib.migrations",
+ "health_check.contrib.celery",
+ "health_check.contrib.celery_ping",
+ "health_check.contrib.psutil",
+ "health_check.contrib.redis",
"meta",
"parler",
] + FIRST_PARTY_APPS
@@ -165,12 +178,12 @@ if DEBUG:
"debug_toolbar",
]
-TAILWIND_APP_NAME = 'theme'
+TAILWIND_APP_NAME = "theme"
-META_SITE_NAME = 'PKMN Trade Club'
+META_SITE_NAME = "PKMN Trade Club"
META_SITE_PROTOCOL = SCHEME
META_USE_SITES = True
-META_IMAGE_URL = f'{SCHEME}://{PUBLIC_HOST}/'
+META_IMAGE_URL = f"{SCHEME}://{PUBLIC_HOST}/"
# https://docs.djangoproject.com/en/dev/ref/settings/#middleware
MIDDLEWARE = [
@@ -193,22 +206,22 @@ if DEBUG:
]
HEALTH_CHECK = {
- 'DISK_USAGE_MAX': 90, # percent
- 'MEMORY_MIN': 100, # in MB
+ "DISK_USAGE_MAX": 90, # percent
+ "MEMORY_MIN": 100, # in MB
}
DAISY_SETTINGS = {
- 'SITE_TITLE': 'PKMN Trade Club Admin',
- 'DONT_SUPPORT_ME': True,
+ "SITE_TITLE": "PKMN Trade Club Admin",
+ "DONT_SUPPORT_ME": True,
}
# https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf
-ROOT_URLCONF = 'pkmntrade_club.django_project.urls'
+ROOT_URLCONF = "pkmntrade_club.django_project.urls"
# https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application
-WSGI_APPLICATION = 'pkmntrade_club.django_project.wsgi.app'
+WSGI_APPLICATION = "pkmntrade_club.django_project.wsgi.app"
-ASGI_APPLICATION = 'pkmntrade_club.django_project.asgi.application'
+ASGI_APPLICATION = "pkmntrade_club.django_project.asgi.application"
# https://docs.djangoproject.com/en/dev/ref/settings/#templates
TEMPLATES = [
@@ -231,7 +244,7 @@ TEMPLATES = [
# https://docs.djangoproject.com/en/dev/ref/settings/#databases
DATABASES = {
- 'default': env.db(var="DJANGO_DATABASE_URL"),
+ "default": env.db(var="DJANGO_DATABASE_URL"),
}
# Password validation
@@ -256,12 +269,10 @@ AUTH_PASSWORD_VALIDATORS = [
# https://docs.djangoproject.com/en/dev/ref/settings/#language-code
LANGUAGE_CODE = "en-us"
-LANGUAGES = (
- ('en', _("English")),
-)
+LANGUAGES = (("en", _("English")),)
# https://docs.djangoproject.com/en/dev/ref/settings/#time-zone
-TIME_ZONE = env('TIME_ZONE')
+TIME_ZONE = env("TIME_ZONE")
# https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-USE_I18N
USE_I18N = True
@@ -270,7 +281,7 @@ USE_I18N = True
USE_TZ = True
# https://docs.djangoproject.com/en/dev/ref/settings/#locale-paths
-LOCALE_PATHS = [BASE_DIR / 'locale']
+LOCALE_PATHS = [BASE_DIR / "locale"]
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
@@ -305,24 +316,24 @@ STORAGES = {
# Default primary key field type
# https://docs.djangoproject.com/en/stable/ref/settings/#default-auto-field
-DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# django-crispy-forms
# https://django-crispy-forms.readthedocs.io/en/latest/install.html#template-packs
-CRISPY_ALLOWED_TEMPLATE_PACKS = 'tailwind'
+CRISPY_ALLOWED_TEMPLATE_PACKS = "tailwind"
CRISPY_TEMPLATE_PACK = "tailwind"
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
-EMAIL_HOST = env('DJANGO_EMAIL_HOST')
-EMAIL_PORT = env('DJANGO_EMAIL_PORT')
-EMAIL_HOST_USER = env('DJANGO_EMAIL_USER')
-EMAIL_HOST_PASSWORD = env('DJANGO_EMAIL_PASSWORD')
-EMAIL_USE_TLS = env('DJANGO_EMAIL_USE_TLS')
-EMAIL_SUBJECT_PREFIX = env('DJANGO_EMAIL_SUBJECT_PREFIX')
+EMAIL_HOST = env("DJANGO_EMAIL_HOST")
+EMAIL_PORT = env("DJANGO_EMAIL_PORT")
+EMAIL_HOST_USER = env("DJANGO_EMAIL_USER")
+EMAIL_HOST_PASSWORD = env("DJANGO_EMAIL_PASSWORD")
+EMAIL_USE_TLS = env("DJANGO_EMAIL_USE_TLS")
+EMAIL_SUBJECT_PREFIX = env("DJANGO_EMAIL_SUBJECT_PREFIX")
# https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email
-DEFAULT_FROM_EMAIL = env('DJANGO_DEFAULT_FROM_EMAIL')
+DEFAULT_FROM_EMAIL = env("DJANGO_DEFAULT_FROM_EMAIL")
# django-debug-toolbar
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html
@@ -335,7 +346,7 @@ INTERNAL_IPS = [
hostname, _, ips = socket.gethostbyname_ex(socket.gethostname())
for ip in ips:
INTERNAL_IPS.append(ip)
- INTERNAL_IPS.append(".".join(ip.rsplit(".")[:-1])+ ".1")
+ INTERNAL_IPS.append(".".join(ip.rsplit(".")[:-1]) + ".1")
# https://docs.djangoproject.com/en/dev/topics/auth/customizing/#substituting-a-custom-user-model
AUTH_USER_MODEL = "accounts.CustomUser"
@@ -345,12 +356,10 @@ AUTH_USER_MODEL = "accounts.CustomUser"
SITE_ID = 1
PARLER_LANGUAGES = {
- SITE_ID: (
- {'code': 'en'},
- ),
- 'default': {
- 'fallbacks': ['en'],
- 'hide_untranslated': False,
+ SITE_ID: ({"code": "en"},),
+ "default": {
+ "fallbacks": ["en"],
+ "hide_untranslated": False,
},
}
@@ -368,13 +377,13 @@ AUTHENTICATION_BACKENDS = (
)
# https://django-allauth.readthedocs.io/en/latest/configuration.html
if DISABLE_SIGNUPS:
- ACCOUNT_ADAPTER = 'pkmntrade_club.accounts.adapter.NoSignupAccountAdapter'
-SOCIALACCOUNT_ADAPTER = 'pkmntrade_club.accounts.adapter.NoSignupSocialAccountAdapter' # always disable social account signups
+ ACCOUNT_ADAPTER = "pkmntrade_club.accounts.adapter.NoSignupAccountAdapter"
+SOCIALACCOUNT_ADAPTER = "pkmntrade_club.accounts.adapter.NoSignupSocialAccountAdapter" # always disable social account signups
ACCOUNT_SESSION_REMEMBER = True
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = True
ACCOUNT_AUTHENTICATION_METHOD = "username_email"
ACCOUNT_EMAIL_REQUIRED = True
-ACCOUNT_EMAIL_VERIFICATION = env('ACCOUNT_EMAIL_VERIFICATION')
+ACCOUNT_EMAIL_VERIFICATION = env("ACCOUNT_EMAIL_VERIFICATION")
ACCOUNT_EMAIL_NOTIFICATIONS = True
ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS = False
ACCOUNT_DEFAULT_HTTP_PROTOCOL = SCHEME
@@ -395,7 +404,7 @@ SOCIALACCOUNT_ONLY = False
SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies"
SESSION_COOKIE_HTTPONLY = True
-SESSION_COOKIE_SECURE = PUBLIC_HOST != 'localhost' or PUBLIC_HOST != '127.0.0.1'
+SESSION_COOKIE_SECURE = PUBLIC_HOST != "localhost" or PUBLIC_HOST != "127.0.0.1"
# auto-detection doesn't work properly sometimes, so we'll just use the DEBUG setting
DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": lambda request: DEBUG}
diff --git a/src/pkmntrade_club/django_project/urls.py b/src/pkmntrade_club/django_project/urls.py
index 61cac00..84a15be 100644
--- a/src/pkmntrade_club/django_project/urls.py
+++ b/src/pkmntrade_club/django_project/urls.py
@@ -4,11 +4,11 @@ from debug_toolbar.toolbar import debug_toolbar_urls
urlpatterns = [
path("admin/", admin.site.urls),
- path('account/', include('pkmntrade_club.accounts.urls')),
+ path("account/", include("pkmntrade_club.accounts.urls")),
path("accounts/", include("allauth.urls")),
path("", include("pkmntrade_club.home.urls")),
path("cards/", include("pkmntrade_club.cards.urls")),
- path("health/", include('health_check.urls')),
+ path("health/", include("health_check.urls")),
path("trades/", include("pkmntrade_club.trades.urls")),
path("__reload__/", include("django_browser_reload.urls")),
] + debug_toolbar_urls()
diff --git a/src/pkmntrade_club/django_project/wsgi.py b/src/pkmntrade_club/django_project/wsgi.py
index 4c5ab87..711707d 100644
--- a/src/pkmntrade_club/django_project/wsgi.py
+++ b/src/pkmntrade_club/django_project/wsgi.py
@@ -2,6 +2,8 @@ import os
from django.core.wsgi import get_wsgi_application
-os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pkmntrade_club.django_project.settings")
+os.environ.setdefault(
+ "DJANGO_SETTINGS_MODULE", "pkmntrade_club.django_project.settings"
+)
app = get_wsgi_application()
diff --git a/src/pkmntrade_club/home/tests.py b/src/pkmntrade_club/home/tests.py
index 9c51303..a174be0 100644
--- a/src/pkmntrade_club/home/tests.py
+++ b/src/pkmntrade_club/home/tests.py
@@ -2,241 +2,221 @@ from django.test import TestCase, Client, RequestFactory
from django.urls import reverse
from django.contrib.auth import get_user_model
from pkmntrade_club.cards.models import Card, Deck
-from pkmntrade_club.trades.models import TradeOffer, TradeOfferHaveCard, TradeOfferWantCard
+from pkmntrade_club.trades.models import (
+ TradeOffer,
+ TradeOfferHaveCard,
+ TradeOfferWantCard,
+)
from pkmntrade_club.accounts.models import FriendCode
from pkmntrade_club.home.views import HomePageView
-import json
from collections import OrderedDict
from unittest.mock import patch, MagicMock
-from django.core.exceptions import ObjectDoesNotExist
-import importlib
from tests.utils.rarity import RARITY_MAPPING
User = get_user_model()
+
class HomePageViewTests(TestCase):
"""Test suite for the HomePageView."""
-
+
@classmethod
def setUpTestData(cls):
"""Set up data for all test methods."""
# Create a user
cls.user = User.objects.create_user(
- username='testuser',
- email='testuser@example.com',
- password='testpass123'
+ username="testuser", email="testuser@example.com", password="testpass123"
)
-
+
# Create a friend code for the user
cls.friend_code = FriendCode.objects.create(
- user=cls.user,
- friend_code='SW-1234-5678-9012',
- in_game_name='TestTrainer'
+ user=cls.user, friend_code="SW-1234-5678-9012", in_game_name="TestTrainer"
)
-
+
# Create decks
cls.deck1 = Deck.objects.create(
- name='Test Deck 1',
- hex_color='#FF0000',
- cardset='TEST01'
+ name="Test Deck 1", hex_color="#FF0000", cardset="TEST01"
)
-
+
# Create cards with different rarities
cls.common_card = Card.objects.create(
- name='Common Test Card',
- cardset='TEST01',
+ name="Common Test Card",
+ cardset="TEST01",
cardnum=1,
- style='normal',
- rarity_icon='★',
- rarity_level=1
+ style="normal",
+ rarity_icon="★",
+ rarity_level=1,
)
cls.common_card.decks.add(cls.deck1)
-
+
cls.rare_card = Card.objects.create(
- name='Rare Test Card',
- cardset='TEST01',
+ name="Rare Test Card",
+ cardset="TEST01",
cardnum=2,
- style='normal',
- rarity_icon='★★★',
- rarity_level=3
+ style="normal",
+ rarity_icon="★★★",
+ rarity_level=3,
)
cls.rare_card.decks.add(cls.deck1)
-
+
cls.ultra_rare_card = Card.objects.create(
- name='Ultra Rare Test Card',
- cardset='TEST01',
+ name="Ultra Rare Test Card",
+ cardset="TEST01",
cardnum=3,
- style='normal',
- rarity_icon='★★★★',
- rarity_level=4
+ style="normal",
+ rarity_icon="★★★★",
+ rarity_level=4,
)
cls.ultra_rare_card.decks.add(cls.deck1)
-
+
# Create trade offers with consistent rarities
cls.common_trade = TradeOffer.objects.create(
- initiated_by=cls.friend_code,
- rarity_icon=RARITY_MAPPING[1],
- rarity_level=1
+ initiated_by=cls.friend_code, rarity_icon=RARITY_MAPPING[1], rarity_level=1
)
-
+
cls.rare_trade = TradeOffer.objects.create(
- initiated_by=cls.friend_code,
- rarity_icon=RARITY_MAPPING[3],
- rarity_level=3
+ initiated_by=cls.friend_code, rarity_icon=RARITY_MAPPING[3], rarity_level=3
)
-
+
# Add have and want cards with the SAME rarity for each trade
TradeOfferHaveCard.objects.create(
- trade_offer=cls.common_trade,
- card=cls.common_card,
- quantity=2
+ trade_offer=cls.common_trade, card=cls.common_card, quantity=2
)
-
+
TradeOfferHaveCard.objects.create(
- trade_offer=cls.rare_trade,
- card=cls.rare_card,
- quantity=1
+ trade_offer=cls.rare_trade, card=cls.rare_card, quantity=1
)
-
+
# Add want cards with the SAME rarity as the have cards for each trade
TradeOfferWantCard.objects.create(
- trade_offer=cls.common_trade,
- card=cls.common_card,
- quantity=1
+ trade_offer=cls.common_trade, card=cls.common_card, quantity=1
)
-
+
TradeOfferWantCard.objects.create(
trade_offer=cls.rare_trade,
card=cls.rare_card, # Changed from ultra_rare_card to match the rarity
- quantity=1
+ quantity=1,
)
-
+
def setUp(self):
"""Set up before each test method."""
self.client = Client()
- self.url = reverse('home')
+ self.url = reverse("home")
self.factory = RequestFactory()
-
+
def test_home_page_status_code(self):
"""Test that the home page returns a 200 status code."""
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
-
+
def test_home_page_template(self):
"""Test that the home page uses the correct template."""
response = self.client.get(self.url)
- self.assertTemplateUsed(response, 'home/home.html')
-
+ self.assertTemplateUsed(response, "home/home.html")
+
def test_home_page_context_cards(self):
"""Test that the home page contains all cards in the context."""
response = self.client.get(self.url)
- self.assertIn('cards', response.context)
- self.assertEqual(response.context['cards'].count(), 3)
-
+ self.assertIn("cards", response.context)
+ self.assertEqual(response.context["cards"].count(), 3)
+
def test_home_page_context_recent_offers(self):
"""Test that the home page contains recent offers in the context."""
response = self.client.get(self.url)
- self.assertIn('recent_offers', response.context)
- self.assertEqual(len(response.context['recent_offers']), 2)
+ self.assertIn("recent_offers", response.context)
+ self.assertEqual(len(response.context["recent_offers"]), 2)
# Recent offers should be ordered by most recent first
- self.assertEqual(response.context['recent_offers'][0], self.rare_trade)
-
+ self.assertEqual(response.context["recent_offers"][0], self.rare_trade)
+
def test_home_page_context_most_offered_cards(self):
"""Test that the home page contains most offered cards in the context."""
response = self.client.get(self.url)
- self.assertIn('most_offered_cards', response.context)
- most_offered = list(response.context['most_offered_cards'])
+ self.assertIn("most_offered_cards", response.context)
+ most_offered = list(response.context["most_offered_cards"])
self.assertEqual(len(most_offered), 2)
# Common card should be most offered (quantity of 2)
self.assertEqual(most_offered[0], self.common_card)
-
+
def test_home_page_context_most_wanted_cards(self):
"""Test that the home page contains most wanted cards in the context."""
response = self.client.get(self.url)
- self.assertIn('most_wanted_cards', response.context)
- most_wanted = list(response.context['most_wanted_cards'])
+ self.assertIn("most_wanted_cards", response.context)
+ most_wanted = list(response.context["most_wanted_cards"])
self.assertEqual(len(most_wanted), 2)
-
+
def test_home_page_context_least_offered_cards(self):
"""Test that the home page contains least offered cards in the context."""
response = self.client.get(self.url)
- self.assertIn('least_offered_cards', response.context)
-
+ self.assertIn("least_offered_cards", response.context)
+
def test_home_page_context_featured_offers(self):
"""Test that the home page contains featured offers in the context."""
response = self.client.get(self.url)
- self.assertIn('featured_offers', response.context)
- featured = response.context['featured_offers']
+ self.assertIn("featured_offers", response.context)
+ featured = response.context["featured_offers"]
# Should be an OrderedDict
self.assertIsInstance(featured, OrderedDict)
# Should contain "All" category
self.assertIn("All", featured)
# Should contain both rarity icons
- self.assertIn('★★★', featured)
- self.assertIn('★', featured)
+ self.assertIn("★★★", featured)
+ self.assertIn("★", featured)
# Higher rarity should come before lower rarity
keys = list(featured.keys())
# First key should be "All"
self.assertEqual(keys[0], "All")
# Higher rarity (★★★) should come before lower rarity (★)
- self.assertIn('★★★', keys)
- self.assertIn('★', keys)
- self.assertTrue(keys.index('★★★') < keys.index('★'))
-
+ self.assertIn("★★★", keys)
+ self.assertIn("★", keys)
+ self.assertTrue(keys.index("★★★") < keys.index("★"))
+
def test_closed_offers_not_shown(self):
"""Test that closed offers are not shown on the home page."""
# Close one of the trade offers
self.common_trade.is_closed = True
self.common_trade.save()
-
+
response = self.client.get(self.url)
- recent_offers = response.context['recent_offers']
+ recent_offers = response.context["recent_offers"]
# Should only show the rare trade now
self.assertEqual(len(recent_offers), 1)
self.assertEqual(recent_offers[0], self.rare_trade)
-
+
def test_home_page_with_no_data(self):
"""Test home page rendering when there's no trade data."""
# Delete all trade offers
TradeOffer.objects.all().delete()
-
+
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
# Should have empty lists for offers
- self.assertEqual(len(response.context['recent_offers']), 0)
-
+ self.assertEqual(len(response.context["recent_offers"]), 0)
+
def test_home_page_with_authenticated_user(self):
"""Test that the home page works for authenticated users."""
- self.client.login(username='testuser', password='testpass123')
+ self.client.login(username="testuser", password="testpass123")
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
-
+
def test_rarity_sorting_in_featured_offers(self):
"""Test that offers are sorted by rarity level in descending order."""
# Create a new ultra rare trade with consistent rarity
ultra_trade = TradeOffer.objects.create(
- initiated_by=self.friend_code,
- rarity_icon='★★★★',
- rarity_level=4
+ initiated_by=self.friend_code, rarity_icon="★★★★", rarity_level=4
)
-
+
# Add have and want cards with the same rarity
TradeOfferHaveCard.objects.create(
- trade_offer=ultra_trade,
- card=self.ultra_rare_card,
- quantity=1
+ trade_offer=ultra_trade, card=self.ultra_rare_card, quantity=1
)
-
+
TradeOfferWantCard.objects.create(
- trade_offer=ultra_trade,
- card=self.ultra_rare_card,
- quantity=1
+ trade_offer=ultra_trade, card=self.ultra_rare_card, quantity=1
)
-
+
response = self.client.get(self.url)
- featured = response.context['featured_offers']
+ featured = response.context["featured_offers"]
keys = list(featured.keys())
-
+
# Order should be: "All", "★★★★" (level 4), "★★★" (level 3), "★" (level 1)
self.assertEqual(keys[0], "All")
self.assertEqual(keys[1], "★★★★")
@@ -246,216 +226,202 @@ class HomePageViewTests(TestCase):
class HomePageViewMockTests(TestCase):
"""Test suite using mocks for HomePageView."""
-
+
def setUp(self):
self.factory = RequestFactory()
self.view = HomePageView()
-
- @patch('trades.models.TradeOffer.objects')
- @patch('cards.models.Card.objects')
+
+ @patch("trades.models.TradeOffer.objects")
+ @patch("cards.models.Card.objects")
def test_get_context_data_with_mocks(self, mock_card_objects, mock_offer_objects):
"""Test get_context_data using mocks."""
# Set up request
- request = self.factory.get(reverse('home'))
+ request = self.factory.get(reverse("home"))
self.view.request = request
-
+
# Mock the queryset responses
mock_offer_filter = MagicMock()
mock_offer_objects.filter.return_value = mock_offer_filter
mock_offer_filter.order_by.return_value = []
-
+
mock_card_filter = MagicMock()
mock_card_objects.filter.return_value = mock_card_filter
mock_card_objects.annotate.return_value = mock_card_filter
mock_card_objects.all.return_value.order_by.return_value = []
mock_card_filter.annotate.return_value = mock_card_filter
mock_card_filter.order_by.return_value = []
-
+
mock_offer_filter.values_list.return_value.distinct.return_value = []
-
+
# Call the method
context = self.view.get_context_data()
-
+
# Verify the expected context keys exist
- self.assertIn('cards', context)
- self.assertIn('recent_offers', context)
- self.assertIn('most_offered_cards', context)
- self.assertIn('most_wanted_cards', context)
- self.assertIn('least_offered_cards', context)
- self.assertIn('featured_offers', context)
-
- @patch('trades.models.TradeOffer.objects')
+ self.assertIn("cards", context)
+ self.assertIn("recent_offers", context)
+ self.assertIn("most_offered_cards", context)
+ self.assertIn("most_wanted_cards", context)
+ self.assertIn("least_offered_cards", context)
+ self.assertIn("featured_offers", context)
+
+ @patch("trades.models.TradeOffer.objects")
def test_empty_featured_offers(self, mock_offer_objects):
"""Test handling of empty featured offers."""
# Set up request
- request = self.factory.get(reverse('home'))
+ request = self.factory.get(reverse("home"))
self.view.request = request
-
+
# Configure mock to return empty queryset
mock_offer_filter = MagicMock()
mock_offer_objects.filter.return_value = mock_offer_filter
mock_offer_filter.order_by.return_value = []
mock_offer_filter.values_list.return_value.distinct.return_value = []
-
+
# Call the method
context = self.view.get_context_data()
-
+
# Verify the featured_offers is an OrderedDict but with just the "All" key
- self.assertIsInstance(context['featured_offers'], OrderedDict)
- self.assertIn("All", context['featured_offers'])
- self.assertEqual(len(context['featured_offers']), 1)
-
- @patch('trades.models.TradeOffer.objects.filter')
+ self.assertIsInstance(context["featured_offers"], OrderedDict)
+ self.assertIn("All", context["featured_offers"])
+ self.assertEqual(len(context["featured_offers"]), 1)
+
+ @patch("trades.models.TradeOffer.objects.filter")
def test_exception_handling(self, mock_filter):
"""Test that exceptions are handled gracefully."""
# Set up request
- request = self.factory.get(reverse('home'))
+ request = self.factory.get(reverse("home"))
self.view.request = request
-
+
# Configure mock to raise an exception
mock_filter.side_effect = Exception("Database error")
-
+
# Call the method - should not raise an exception
- with self.assertLogs(level='ERROR') as cm:
+ with self.assertLogs(level="ERROR") as cm:
context = self.view.get_context_data()
-
+
# Check if error was logged
- self.assertIn("Unhandled error in HomePageView.get_context_data", cm.output[0])
-
+ self.assertIn(
+ "Unhandled error in HomePageView.get_context_data", cm.output[0]
+ )
+
# Verify fallback values were set
- self.assertEqual(len(context['cards']), 0)
- self.assertEqual(len(context['recent_offers']), 0)
- self.assertEqual(len(context['most_offered_cards']), 0)
- self.assertEqual(len(context['most_wanted_cards']), 0)
- self.assertEqual(len(context['least_offered_cards']), 0)
- self.assertIsInstance(context['featured_offers'], OrderedDict)
- self.assertEqual(len(context['featured_offers']), 1)
- self.assertIn("All", context['featured_offers'])
+ self.assertEqual(len(context["cards"]), 0)
+ self.assertEqual(len(context["recent_offers"]), 0)
+ self.assertEqual(len(context["most_offered_cards"]), 0)
+ self.assertEqual(len(context["most_wanted_cards"]), 0)
+ self.assertEqual(len(context["least_offered_cards"]), 0)
+ self.assertIsInstance(context["featured_offers"], OrderedDict)
+ self.assertEqual(len(context["featured_offers"]), 1)
+ self.assertIn("All", context["featured_offers"])
+
class HomePageEdgeCaseTests(TestCase):
"""Test edge cases for the home page."""
-
+
def setUp(self):
self.client = Client()
- self.url = reverse('home')
-
+ self.url = reverse("home")
+
# Create a user
self.user = User.objects.create_user(
- username='testuser',
- email='testuser@example.com',
- password='testpass123'
+ username="testuser", email="testuser@example.com", password="testpass123"
)
-
+
# Create a friend code for the user
self.friend_code = FriendCode.objects.create(
- user=self.user,
- friend_code='SW-1234-5678-9012',
- in_game_name='TestTrainer'
+ user=self.user, friend_code="SW-1234-5678-9012", in_game_name="TestTrainer"
)
-
+
def test_home_page_with_no_cards(self):
"""Test home page with no cards in the database."""
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
- self.assertEqual(len(response.context['cards']), 0)
-
+ self.assertEqual(len(response.context["cards"]), 0)
+
def test_home_page_with_many_offers(self):
"""Test home page with many offers to verify pagination or limiting works."""
# Create a card
card = Card.objects.create(
- name='Test Card',
- cardset='TEST01',
+ name="Test Card",
+ cardset="TEST01",
cardnum=1,
- style='normal',
- rarity_icon='★',
- rarity_level=1
+ style="normal",
+ rarity_icon="★",
+ rarity_level=1,
)
-
+
# Create 20 trade offers
for i in range(20):
trade = TradeOffer.objects.create(
- initiated_by=self.friend_code,
- rarity_icon='★',
- rarity_level=1
+ initiated_by=self.friend_code, rarity_icon="★", rarity_level=1
)
-
+
# Add have and want cards
- TradeOfferHaveCard.objects.create(
- trade_offer=trade,
- card=card,
- quantity=1
- )
-
- TradeOfferWantCard.objects.create(
- trade_offer=trade,
- card=card,
- quantity=1
- )
-
+ TradeOfferHaveCard.objects.create(trade_offer=trade, card=card, quantity=1)
+
+ TradeOfferWantCard.objects.create(trade_offer=trade, card=card, quantity=1)
+
response = self.client.get(self.url)
-
+
# Check that recent_offers is limited to 6 as per the view
- self.assertEqual(len(response.context['recent_offers']), 6)
-
+ self.assertEqual(len(response.context["recent_offers"]), 6)
+
def test_home_page_with_invalid_parameters(self):
"""Test home page with invalid GET parameters."""
# The view should ignore invalid parameters
response = self.client.get(f"{self.url}?invalid=param&another=invalid")
self.assertEqual(response.status_code, 200)
-
+
def test_performance_with_large_dataset(self):
"""Test performance with a larger dataset (basic check)."""
# Create a card
card = Card.objects.create(
- name='Performance Test Card',
- cardset='PERF01',
+ name="Performance Test Card",
+ cardset="PERF01",
cardnum=1,
- style='normal',
- rarity_icon='★',
- rarity_level=1
+ style="normal",
+ rarity_icon="★",
+ rarity_level=1,
)
-
+
# Create 50 trade offers with different rarities
for i in range(50):
rarity_level = (i % 5) + 1 # 1-5
- rarity_icon = '★' * rarity_level
-
+ rarity_icon = "★" * rarity_level
+
trade = TradeOffer.objects.create(
initiated_by=self.friend_code,
rarity_icon=rarity_icon,
- rarity_level=rarity_level
+ rarity_level=rarity_level,
)
-
+
# Add have and want cards with the same rarity
rarity_card = Card.objects.create(
- name=f'Performance Test Card {i}',
- cardset='PERF01',
- cardnum=i+10,
- style='normal',
+ name=f"Performance Test Card {i}",
+ cardset="PERF01",
+ cardnum=i + 10,
+ style="normal",
rarity_icon=rarity_icon,
- rarity_level=rarity_level
+ rarity_level=rarity_level,
)
-
+
TradeOfferHaveCard.objects.create(
- trade_offer=trade,
- card=rarity_card,
- quantity=1
+ trade_offer=trade, card=rarity_card, quantity=1
)
-
+
TradeOfferWantCard.objects.create(
- trade_offer=trade,
- card=rarity_card,
- quantity=1
+ trade_offer=trade, card=rarity_card, quantity=1
)
-
+
# Basic performance test - just checking it completes without timeout
import time
+
start = time.time()
response = self.client.get(self.url)
end = time.time()
-
+
self.assertEqual(response.status_code, 200)
-
+
# Should be reasonably fast (adjust threshold as needed)
execution_time = end - start
self.assertLess(execution_time, 2.0) # Should complete in under 2 seconds
@@ -463,129 +429,116 @@ class HomePageEdgeCaseTests(TestCase):
class TemplateRenderingTests(TestCase):
"""Tests focused on template rendering."""
-
+
@classmethod
def setUpTestData(cls):
# Create a user
cls.user = User.objects.create_user(
- username='testuser',
- email='testuser@example.com',
- password='testpass123'
+ username="testuser", email="testuser@example.com", password="testpass123"
)
-
+
# Create a friend code for the user
cls.friend_code = FriendCode.objects.create(
- user=cls.user,
- friend_code='SW-1234-5678-9012',
- in_game_name='TestTrainer'
+ user=cls.user, friend_code="SW-1234-5678-9012", in_game_name="TestTrainer"
)
-
+
# Create a card
cls.card = Card.objects.create(
- name='Test Card',
- cardset='TEST01',
+ name="Test Card",
+ cardset="TEST01",
cardnum=1,
- style='normal',
- rarity_icon='★',
- rarity_level=1
+ style="normal",
+ rarity_icon="★",
+ rarity_level=1,
)
-
+
# Create a trade offer
cls.trade = TradeOffer.objects.create(
- initiated_by=cls.friend_code,
- rarity_icon='★',
- rarity_level=1
+ initiated_by=cls.friend_code, rarity_icon="★", rarity_level=1
)
-
+
# Add have and want cards
TradeOfferHaveCard.objects.create(
- trade_offer=cls.trade,
- card=cls.card,
- quantity=1
+ trade_offer=cls.trade, card=cls.card, quantity=1
)
-
+
TradeOfferWantCard.objects.create(
- trade_offer=cls.trade,
- card=cls.card,
- quantity=1
+ trade_offer=cls.trade, card=cls.card, quantity=1
)
-
+
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
-
+
def test_template_used(self):
"""Test that the correct template is used."""
- response = self.client.get(reverse('home'))
- self.assertTemplateUsed(response, 'home/home.html')
-
+ response = self.client.get(reverse("home"))
+ self.assertTemplateUsed(response, "home/home.html")
+
def test_context_variables_exist(self):
"""Test that all expected context variables exist."""
- response = self.client.get(reverse('home'))
-
+ response = self.client.get(reverse("home"))
+
# Check all required context variables
expected_keys = [
- 'cards',
- 'recent_offers',
- 'most_offered_cards',
- 'most_wanted_cards',
- 'least_offered_cards',
- 'featured_offers',
+ "cards",
+ "recent_offers",
+ "most_offered_cards",
+ "most_wanted_cards",
+ "least_offered_cards",
+ "featured_offers",
]
-
+
for key in expected_keys:
self.assertIn(key, response.context)
-
+
def test_view_with_pagination_params(self):
"""Test that view handles pagination parameters correctly, if applicable."""
# Create additional trade offers if pagination is implemented
for i in range(10):
trade = TradeOffer.objects.create(
- initiated_by=self.friend_code,
- rarity_icon='★',
- rarity_level=1
+ initiated_by=self.friend_code, rarity_icon="★", rarity_level=1
)
-
+
# Add have and want cards
TradeOfferHaveCard.objects.create(
- trade_offer=trade,
- card=self.card,
- quantity=1
+ trade_offer=trade, card=self.card, quantity=1
)
-
+
TradeOfferWantCard.objects.create(
- trade_offer=trade,
- card=self.card,
- quantity=1
+ trade_offer=trade, card=self.card, quantity=1
)
-
+
# Test with page parameter
response = self.client.get(f"{reverse('home')}?page=1")
self.assertEqual(response.status_code, 200)
-
+
# Test with invalid page parameter
response = self.client.get(f"{reverse('home')}?page=999")
- self.assertEqual(response.status_code, 200) # Should still render with default page
-
+ self.assertEqual(
+ response.status_code, 200
+ ) # Should still render with default page
+
# Test with non-numeric page parameter
response = self.client.get(f"{reverse('home')}?page=abc")
self.assertEqual(response.status_code, 200) # Should handle gracefully
-
- @patch('home.views.HomePageView.get_context_data')
+
+ @patch("home.views.HomePageView.get_context_data")
def test_view_renders_with_missing_context(self, mock_get_context):
"""Test that view renders even with incomplete context data."""
# Return incomplete context
- mock_get_context.return_value = {'cards': []}
-
+ mock_get_context.return_value = {"cards": []}
+
# Should still render without error even with missing context variables
- response = self.client.get(reverse('home'))
+ response = self.client.get(reverse("home"))
self.assertEqual(response.status_code, 200)
-
+
def test_compatibility_with_multiple_django_versions(self):
"""Ensure compatibility with different Django versions."""
import django
+
# Simply log the Django version - the test itself verifies the page renders
# with the current version
django_version = django.get_version()
- response = self.client.get(reverse('home'))
+ response = self.client.get(reverse("home"))
self.assertEqual(response.status_code, 200)
diff --git a/src/pkmntrade_club/home/views.py b/src/pkmntrade_club/home/views.py
index cc05847..22c66af 100644
--- a/src/pkmntrade_club/home/views.py
+++ b/src/pkmntrade_club/home/views.py
@@ -1,54 +1,58 @@
-from collections import defaultdict, OrderedDict
+from collections import 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.db.models import (
+ Sum,
+)
from django.db.models.functions import Coalesce
-from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
-from pkmntrade_club.trades.models import TradeOffer, TradeAcceptance, TradeOfferHaveCard, TradeOfferWantCard
+from pkmntrade_club.trades.models import (
+ TradeOffer,
+)
from pkmntrade_club.cards.models import Card
-from django.utils.decorators import method_decorator
-from django.template.response import TemplateResponse
-from django.http import HttpResponseRedirect
import logging
-from django.views import View
-from django.http import HttpResponse
-import contextlib
logger = logging.getLogger(__name__)
+
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)
-
+
try:
# Get all cards ordered by name, exclude cards with rarity level > 5
- context["cards"] = Card.objects.filter(rarity_level__lte=5).order_by("name", "rarity_level")
-
+ context["cards"] = Card.objects.filter(rarity_level__lte=5).order_by(
+ "name", "rarity_level"
+ )
+
# Reuse base trade offer queryset for market stats
base_offer_qs = TradeOffer.objects.filter(is_closed=False)
-
+
# Recent Offers
try:
recent_offers_qs = base_offer_qs.order_by("-created_at")[:6]
context["recent_offers"] = recent_offers_qs
- context["cache_key_recent_offers"] = f"recent_offers_{recent_offers_qs.values_list('pk', 'updated_at')}"
+ context["cache_key_recent_offers"] = (
+ f"recent_offers_{recent_offers_qs.values_list('pk', 'updated_at')}"
+ )
except Exception as e:
logger.error(f"Error fetching recent offers: {str(e)}")
context["recent_offers"] = []
context["cache_key_recent_offers"] = "recent_offers_error"
-
+
# Most Offered Cards
try:
most_offered_cards_qs = (
- Card.objects.filter(tradeofferhavecard__isnull=False).filter(rarity_level__lte=5)
+ Card.objects.filter(tradeofferhavecard__isnull=False)
+ .filter(rarity_level__lte=5)
.annotate(offer_count=Sum("tradeofferhavecard__quantity"))
.order_by("-offer_count")[:6]
)
context["most_offered_cards"] = most_offered_cards_qs
- context["cache_key_most_offered_cards"] = f"most_offered_cards_{most_offered_cards_qs.values_list('pk', 'updated_at')}"
+ context["cache_key_most_offered_cards"] = (
+ f"most_offered_cards_{most_offered_cards_qs.values_list('pk', 'updated_at')}"
+ )
except Exception as e:
logger.error(f"Error fetching most offered cards: {str(e)}")
context["most_offered_cards"] = []
@@ -56,26 +60,32 @@ class HomePageView(TemplateView):
# Most Wanted Cards
try:
most_wanted_cards_qs = (
- Card.objects.filter(tradeofferwantcard__isnull=False).filter(rarity_level__lte=5)
+ Card.objects.filter(tradeofferwantcard__isnull=False)
+ .filter(rarity_level__lte=5)
.annotate(offer_count=Sum("tradeofferwantcard__quantity"))
.order_by("-offer_count")[:6]
)
context["most_wanted_cards"] = most_wanted_cards_qs
- context["cache_key_most_wanted_cards"] = f"most_wanted_cards_{most_wanted_cards_qs.values_list('pk', 'updated_at')}"
+ context["cache_key_most_wanted_cards"] = (
+ f"most_wanted_cards_{most_wanted_cards_qs.values_list('pk', 'updated_at')}"
+ )
except Exception as e:
logger.error(f"Error fetching most wanted cards: {str(e)}")
context["most_wanted_cards"] = []
-
+
# Least Offered Cards
try:
least_offered_cards_qs = (
- Card.objects.filter(rarity_level__lte=5).annotate(
+ Card.objects.filter(rarity_level__lte=5)
+ .annotate(
offer_count=Coalesce(Sum("tradeofferhavecard__quantity"), 0)
)
.order_by("offer_count")[:6]
)
context["least_offered_cards"] = least_offered_cards_qs
- context["cache_key_least_offered_cards"] = f"least_offered_cards_{least_offered_cards_qs.values_list('pk', 'updated_at')}"
+ context["cache_key_least_offered_cards"] = (
+ f"least_offered_cards_{least_offered_cards_qs.values_list('pk', 'updated_at')}"
+ )
except Exception as e:
logger.error(f"Error fetching least offered cards: {str(e)}")
context["least_offered_cards"] = []
@@ -88,22 +98,22 @@ class HomePageView(TemplateView):
except Exception as e:
logger.error(f"Error fetching 'All' featured offers: {str(e)}")
featured["All"] = []
-
- # *** we only show All Featured Offers for now,
+
+ # *** we only show All Featured Offers for now,
# *** we will add rarity-tabbed featured offers later
# try:
# # 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
@@ -114,16 +124,20 @@ class HomePageView(TemplateView):
# Generate a cache key based on the pks and updated_at timestamps of all featured offers
# *** we will separate cache keys for each featured section later
all_offer_identifiers = []
- for section_name,section_offers in featured.items():
+ for section_name, section_offers in featured.items():
# featured_section is a QuerySet. Fetch (pk, updated_at) tuples.
- identifiers = section_offers.values_list('pk', 'updated_at')
+ identifiers = section_offers.values_list("pk", "updated_at")
# Format each tuple as "pk_timestamp" and add to the list
- section_strings = [f"{section_name}_{pk}_{ts.timestamp()}" for pk, ts in identifiers]
+ section_strings = [
+ f"{section_name}_{pk}_{ts.timestamp()}" for pk, ts in identifiers
+ ]
all_offer_identifiers.extend(section_strings)
-
+
# Join all identifiers into a single string, sorted for consistency regardless of order
combined_identifiers = "|".join(sorted(all_offer_identifiers))
- context["cache_key_featured_offers"] = f"featured_offers_{combined_identifiers}"
+ context["cache_key_featured_offers"] = (
+ f"featured_offers_{combined_identifiers}"
+ )
except Exception as e:
logger.error(f"Unhandled error in HomePageView.get_context_data: {str(e)}")
# Provide fallback empty data
@@ -133,9 +147,9 @@ class HomePageView(TemplateView):
context["most_wanted_cards"] = []
context["least_offered_cards"] = []
context["featured_offers"] = OrderedDict([("All", [])])
-
+
return context
-
+
def get(self, request, *args, **kwargs):
"""Override get method to add caching"""
return super().get(request, *args, **kwargs)
diff --git a/src/pkmntrade_club/tests/utils/rarity.py b/src/pkmntrade_club/tests/utils/rarity.py
index 24bf7dd..900eb00 100644
--- a/src/pkmntrade_club/tests/utils/rarity.py
+++ b/src/pkmntrade_club/tests/utils/rarity.py
@@ -6,5 +6,5 @@ RARITY_MAPPING = {
5: "⭐️",
6: "⭐️⭐️",
7: "⭐️⭐️⭐️",
- 8: "👑"
-}
\ No newline at end of file
+ 8: "👑",
+}
diff --git a/src/pkmntrade_club/theme/apps.py b/src/pkmntrade_club/theme/apps.py
index 71e85a3..8f60b15 100644
--- a/src/pkmntrade_club/theme/apps.py
+++ b/src/pkmntrade_club/theme/apps.py
@@ -2,4 +2,4 @@ from django.apps import AppConfig
class ThemeConfig(AppConfig):
- name = 'pkmntrade_club.theme'
+ name = "pkmntrade_club.theme"
diff --git a/src/pkmntrade_club/trades/forms.py b/src/pkmntrade_club/trades/forms.py
index 69f037c..af2a3e2 100644
--- a/src/pkmntrade_club/trades/forms.py
+++ b/src/pkmntrade_club/trades/forms.py
@@ -1,20 +1,21 @@
from django import forms
-from django.core.exceptions import ValidationError
from .models import TradeOffer, TradeAcceptance
from pkmntrade_club.accounts.models import FriendCode
from pkmntrade_club.cards.models import Card
from django.forms import ModelForm
from pkmntrade_club.trades.models import TradeOfferHaveCard, TradeOfferWantCard
+
class NoValidationMultipleChoiceField(forms.MultipleChoiceField):
def validate(self, value):
# Override the validation to skip checking against defined choices
pass
+
class TradeOfferAcceptForm(forms.Form):
friend_code = forms.ModelChoiceField(
queryset=FriendCode.objects.none(),
- label="Select a Friend Code to Accept This Trade Offer"
+ label="Select a Friend Code to Accept This Trade Offer",
)
def __init__(self, *args, **kwargs):
@@ -23,6 +24,7 @@ class TradeOfferAcceptForm(forms.Form):
super().__init__(*args, **kwargs)
self.fields["friend_code"].queryset = friend_codes
+
class TradeAcceptanceCreateForm(forms.ModelForm):
"""
Form for creating a TradeAcceptance.
@@ -32,11 +34,19 @@ class TradeAcceptanceCreateForm(forms.ModelForm):
- default_friend_code (optional): the user's default FriendCode.
It filters available requested and offered cards based on what's still available.
"""
+
class Meta:
model = TradeAcceptance
fields = ["accepted_by", "requested_card", "offered_card"]
- def __init__(self, *args, trade_offer=None, friend_codes=None, default_friend_code=None, **kwargs):
+ def __init__(
+ self,
+ *args,
+ trade_offer=None,
+ friend_codes=None,
+ default_friend_code=None,
+ **kwargs,
+ ):
if trade_offer is None:
raise ValueError("trade_offer must be provided to filter choices.")
super().__init__(*args, **kwargs)
@@ -52,16 +62,23 @@ class TradeAcceptanceCreateForm(forms.ModelForm):
self.initial["accepted_by"] = friend_codes.first().pk
self.fields["accepted_by"].widget = forms.HiddenInput()
# Otherwise, if a default friend code is provided and it is in the queryset, preselect it.
- elif default_friend_code and friend_codes.filter(pk=default_friend_code.pk).exists():
+ elif (
+ default_friend_code
+ and friend_codes.filter(pk=default_friend_code.pk).exists()
+ ):
self.initial["accepted_by"] = default_friend_code.pk
available_have_items = trade_offer.have_cards_available
requested_card_pks = [item.card.pk for item in available_have_items]
- self.fields["requested_card"].queryset = Card.objects.filter(pk__in=requested_card_pks).order_by('name')
+ self.fields["requested_card"].queryset = Card.objects.filter(
+ pk__in=requested_card_pks
+ ).order_by("name")
available_want_items = trade_offer.want_cards_available
offered_card_pks = [item.card.pk for item in available_want_items]
- self.fields["offered_card"].queryset = Card.objects.filter(pk__in=offered_card_pks).order_by('name')
+ self.fields["offered_card"].queryset = Card.objects.filter(
+ pk__in=offered_card_pks
+ ).order_by("name")
def clean(self):
"""
@@ -71,9 +88,11 @@ class TradeAcceptanceCreateForm(forms.ModelForm):
self.instance.trade_offer = self.trade_offer
return super().clean()
+
class ButtonRadioSelect(forms.RadioSelect):
template_name = "widgets/button_radio_select.html"
+
class TradeAcceptanceTransitionForm(forms.Form):
state = forms.ChoiceField(widget=forms.HiddenInput())
@@ -87,13 +106,18 @@ class TradeAcceptanceTransitionForm(forms.Form):
raise ValueError("A TradeAcceptance instance must be provided")
self.instance = instance
self.user = user
-
+
self.fields["state"].choices = instance.get_allowed_state_transitions(user)
+
class TradeOfferCreateForm(ModelForm):
# Override the default fields to capture quantity info in the format 'card_id:quantity'
- have_cards = NoValidationMultipleChoiceField(widget=forms.SelectMultiple, required=True)
- want_cards = NoValidationMultipleChoiceField(widget=forms.SelectMultiple, required=True)
+ have_cards = NoValidationMultipleChoiceField(
+ widget=forms.SelectMultiple, required=True
+ )
+ want_cards = NoValidationMultipleChoiceField(
+ widget=forms.SelectMultiple, required=True
+ )
class Meta:
model = TradeOffer
@@ -111,10 +135,10 @@ class TradeOfferCreateForm(ModelForm):
data = self.data.getlist("have_cards")
parsed = {}
for item in data:
- if ':' not in item:
+ if ":" not in item:
# Ignore any input without a colon.
continue
- parts = item.split(':')
+ parts = item.split(":")
card_id = parts[0]
try:
# Only parse quantity when a colon is present.
@@ -131,16 +155,18 @@ class TradeOfferCreateForm(ModelForm):
)
# Ensure no more than 20 unique have cards are selected.
if len(parsed) > 20:
- raise forms.ValidationError("You can only select a maximum of 20 unique have cards.")
+ raise forms.ValidationError(
+ "You can only select a maximum of 20 unique have cards."
+ )
return parsed
def clean_want_cards(self):
data = self.data.getlist("want_cards")
parsed = {}
for item in data:
- if ':' not in item:
+ if ":" not in item:
continue
- parts = item.split(':')
+ parts = item.split(":")
card_id = parts[0]
try:
quantity = int(parts[1])
@@ -157,7 +183,9 @@ class TradeOfferCreateForm(ModelForm):
)
# Ensure no more than 20 unique want cards are selected.
if len(parsed) > 20:
- raise forms.ValidationError("You can only select a maximum of 20 unique want cards.")
+ raise forms.ValidationError(
+ "You can only select a maximum of 20 unique want cards."
+ )
return parsed
def save(self, commit=True):
@@ -167,15 +195,19 @@ class TradeOfferCreateForm(ModelForm):
# Clear any existing through model entries in case of update
TradeOfferHaveCard.objects.filter(trade_offer=instance).delete()
TradeOfferWantCard.objects.filter(trade_offer=instance).delete()
-
+
# Create through entries for have_cards
for card_id, quantity in self.cleaned_data["have_cards"].items():
card = Card.objects.get(pk=card_id)
- TradeOfferHaveCard.objects.create(trade_offer=instance, card=card, quantity=quantity)
+ TradeOfferHaveCard.objects.create(
+ trade_offer=instance, card=card, quantity=quantity
+ )
# Create through entries for want_cards
for card_id, quantity in self.cleaned_data["want_cards"].items():
card = Card.objects.get(pk=card_id)
- TradeOfferWantCard.objects.create(trade_offer=instance, card=card, quantity=quantity)
+ TradeOfferWantCard.objects.create(
+ trade_offer=instance, card=card, quantity=quantity
+ )
return instance
diff --git a/src/pkmntrade_club/trades/mixins.py b/src/pkmntrade_club/trades/mixins.py
index 36d4272..3e5caf9 100644
--- a/src/pkmntrade_club/trades/mixins.py
+++ b/src/pkmntrade_club/trades/mixins.py
@@ -1,37 +1,49 @@
from pkmntrade_club.cards.models import Card
from django.core.exceptions import PermissionDenied
+
class TradeOfferContextMixin:
def get_context_data(self, **kwargs):
# Start with any context passed in.
context = kwargs.copy()
# Include available cards requirements for multiselect fields.
context.setdefault("cards", Card.objects.all().order_by("name", "rarity_level"))
-
+
# Provide friend_codes and selected_friend_code as in TradeOfferCreateView
friend_codes = self.request.user.friend_codes.all()
context["friend_codes"] = friend_codes
-
+
if "initiated_by" in self.request.GET:
try:
- selected_friend_code = friend_codes.get(pk=self.request.GET.get("initiated_by"))
+ selected_friend_code = friend_codes.get(
+ pk=self.request.GET.get("initiated_by")
+ )
except friend_codes.model.DoesNotExist:
- selected_friend_code = self.request.user.default_friend_code or friend_codes.first()
+ 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()
+ selected_friend_code = (
+ self.request.user.default_friend_code or friend_codes.first()
+ )
context["selected_friend_code"] = selected_friend_code
-
- return context
+
+ return context
+
class FriendCodeRequiredMixin:
"""
Mixin to ensure the authenticated user has at least one friend code.
This mixin must be placed after LoginRequiredMixin in the view's inheritance order.
"""
+
def dispatch(self, request, *args, **kwargs):
# Since LoginRequiredMixin guarantees that request.user is authenticated,
# we assume request.user has the attribute `friend_codes`. If no friend code exists,
# raise a PermissionDenied error.
- if not getattr(request.user, 'friend_codes', None) or not request.user.friend_codes.exists():
+ if (
+ not getattr(request.user, "friend_codes", None)
+ or not request.user.friend_codes.exists()
+ ):
raise PermissionDenied("No friend codes available for your account.")
- return super().dispatch(request, *args, **kwargs)
\ No newline at end of file
+ return super().dispatch(request, *args, **kwargs)
diff --git a/src/pkmntrade_club/trades/models.py b/src/pkmntrade_club/trades/models.py
index 1e50eff..a78d451 100644
--- a/src/pkmntrade_club/trades/models.py
+++ b/src/pkmntrade_club/trades/models.py
@@ -1,13 +1,10 @@
from django.db import models
from django.core.exceptions import ValidationError
-from django.db.models import Q, Count, Prefetch, F, Sum, Max
+from django.db.models import Prefetch
import hashlib
-from pkmntrade_club.cards.models import Card
-from pkmntrade_club.accounts.models import FriendCode
-from datetime import timedelta
-from django.utils import timezone
import uuid
+
def generate_tradeoffer_hash():
"""
Generates a unique 9-character hash for a TradeOffer.
@@ -15,6 +12,7 @@ def generate_tradeoffer_hash():
"""
return hashlib.md5(uuid.uuid4().hex.encode("utf-8")).hexdigest()[:8] + "z"
+
def generate_tradeacceptance_hash():
"""
Generates a unique 9-character hash for a TradeAcceptance.
@@ -22,34 +20,36 @@ def generate_tradeacceptance_hash():
"""
return hashlib.md5(uuid.uuid4().hex.encode("utf-8")).hexdigest()[:8] + "y"
-class TradeOfferManager(models.Manager):
+class TradeOfferManager(models.Manager):
def get_queryset(self):
qs = super().get_queryset()
# Prefetch for have_cards (through model: TradeOfferHaveCard)
# Ensures 'card' is select_related and 'Meta.ordering' is respected/applied.
prefetch_have_cards = Prefetch(
- 'trade_offer_have_cards',
- queryset=TradeOfferHaveCard.objects.select_related('card').order_by('card__name')
+ "trade_offer_have_cards",
+ queryset=TradeOfferHaveCard.objects.select_related("card").order_by(
+ "card__name"
+ ),
)
# Prefetch for want_cards (through model: TradeOfferWantCard)
# Ensures 'card' is select_related and 'Meta.ordering' is respected/applied.
prefetch_want_cards = Prefetch(
- 'trade_offer_want_cards',
- queryset=TradeOfferWantCard.objects.select_related('card').order_by('card__name')
+ "trade_offer_want_cards",
+ queryset=TradeOfferWantCard.objects.select_related("card").order_by(
+ "card__name"
+ ),
)
# Prefetch for acceptances
# Ensures related 'accepted_by__user', 'requested_card', 'offered_card' are fetched.
prefetch_acceptances = Prefetch(
- 'acceptances',
+ "acceptances",
queryset=TradeAcceptance.objects.select_related(
- 'accepted_by__user',
- 'requested_card',
- 'offered_card'
- ).order_by('-created_at') # Sensible default ordering for acceptances
+ "accepted_by__user", "requested_card", "offered_card"
+ ).order_by("-created_at"), # Sensible default ordering for acceptances
)
qs = qs.select_related(
@@ -60,11 +60,12 @@ class TradeOfferManager(models.Manager):
prefetch_acceptances,
# If direct access like offer.have_cards.all() (the M2M to Card, not through model)
# is heavily used AND causes N+1s (e.g. via __str__), uncomment these:
- Prefetch('have_cards'),
- Prefetch('want_cards'),
+ Prefetch("have_cards"),
+ Prefetch("want_cards"),
)
-
- return qs.order_by("-updated_at") # Default ordering for TradeOffer querysets
+
+ return qs.order_by("-updated_at") # Default ordering for TradeOffer querysets
+
class TradeOffer(models.Model):
objects = TradeOfferManager()
@@ -75,20 +76,16 @@ class TradeOffer(models.Model):
initiated_by = models.ForeignKey(
"accounts.FriendCode",
on_delete=models.PROTECT,
- related_name='initiated_trade_offers'
+ related_name="initiated_trade_offers",
)
rarity_icon = models.CharField(max_length=8, null=True)
rarity_level = models.IntegerField(null=True)
- image = models.ImageField(upload_to='trade_offers/', null=True, blank=True)
+ image = models.ImageField(upload_to="trade_offers/", null=True, blank=True)
want_cards = models.ManyToManyField(
- "cards.Card",
- related_name='trade_offers_want',
- through="TradeOfferWantCard"
+ "cards.Card", related_name="trade_offers_want", through="TradeOfferWantCard"
)
have_cards = models.ManyToManyField(
- "cards.Card",
- related_name='trade_offers_have',
- through="TradeOfferHaveCard"
+ "cards.Card", related_name="trade_offers_have", through="TradeOfferHaveCard"
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -105,7 +102,7 @@ class TradeOffer(models.Model):
def update_rarity_fields(self):
"""
- Recalculates and updates the rarity_level and rarity_icon fields based on
+ Recalculates and updates the rarity_level and rarity_icon fields based on
the associated have_cards and want_cards.
Enforces that all cards in the trade offer share the same rarity.
@@ -118,11 +115,16 @@ class TradeOffer(models.Model):
# Enforce same rarity across all cards.
rarity_levels = {card.rarity_level for card in cards}
if len(rarity_levels) > 1:
- raise ValidationError("All cards in a trade offer must have the same rarity.")
+ raise ValidationError(
+ "All cards in a trade offer must have the same rarity."
+ )
first_card = cards[0]
if first_card.rarity_level > 5:
raise ValidationError("Cannot trade cards above one-star rarity.")
- if self.rarity_level != first_card.rarity_level or self.rarity_icon != first_card.rarity_icon:
+ if (
+ self.rarity_level != first_card.rarity_level
+ or self.rarity_icon != first_card.rarity_icon
+ ):
self.rarity_level = first_card.rarity_level
self.rarity_icon = first_card.rarity_icon
# Use super().save() here to avoid recursion.
@@ -131,23 +133,33 @@ class TradeOffer(models.Model):
@property
def have_cards_available(self):
# Returns the list of have_cards (through objects) that still have available quantity.
- return [item for item in self.trade_offer_have_cards.all() if item.quantity > item.qty_accepted]
+ return [
+ item
+ for item in self.trade_offer_have_cards.all()
+ if item.quantity > item.qty_accepted
+ ]
@property
def want_cards_available(self):
# Returns the list of want_cards (through objects) that still have available quantity.
- return [item for item in self.trade_offer_want_cards.all() if item.quantity > item.qty_accepted]
+ return [
+ item
+ for item in self.trade_offer_want_cards.all()
+ if item.quantity > item.qty_accepted
+ ]
+
class TradeOfferHaveCard(models.Model):
"""
Through model for TradeOffer.have_cards.
Represents the card the initiator is offering along with the quantity available.
"""
+
trade_offer = models.ForeignKey(
TradeOffer,
on_delete=models.CASCADE,
- related_name='trade_offer_have_cards',
- db_index=True
+ related_name="trade_offer_have_cards",
+ db_index=True,
)
card = models.ForeignKey("cards.Card", on_delete=models.PROTECT, db_index=True)
quantity = models.PositiveIntegerField(default=1)
@@ -171,17 +183,17 @@ class TradeOfferHaveCard(models.Model):
class Meta:
unique_together = ("trade_offer", "card")
- ordering = ['card__name']
+ ordering = ["card__name"]
+
class TradeOfferWantCard(models.Model):
"""
Through model for TradeOffer.want_cards.
Represents the card the initiator is requesting along with the quantity requested.
"""
+
trade_offer = models.ForeignKey(
- TradeOffer,
- on_delete=models.CASCADE,
- related_name='trade_offer_want_cards'
+ TradeOffer, on_delete=models.CASCADE, related_name="trade_offer_want_cards"
)
card = models.ForeignKey("cards.Card", on_delete=models.PROTECT)
quantity = models.PositiveIntegerField(default=1)
@@ -205,19 +217,20 @@ class TradeOfferWantCard(models.Model):
class Meta:
unique_together = ("trade_offer", "card")
- ordering = ['card__name']
+ ordering = ["card__name"]
+
class TradeAcceptance(models.Model):
class AcceptanceState(models.TextChoices):
- ACCEPTED = 'ACCEPTED', 'Accepted'
- SENT = 'SENT', 'Sent'
- RECEIVED = 'RECEIVED', 'Received'
- THANKED_BY_INITIATOR = 'THANKED_BY_INITIATOR', 'Thanked by Initiator'
- THANKED_BY_ACCEPTOR = 'THANKED_BY_ACCEPTOR', 'Thanked by Acceptor'
- THANKED_BY_BOTH = 'THANKED_BY_BOTH', 'Thanked by Both'
- REJECTED_BY_INITIATOR = 'REJECTED_BY_INITIATOR', 'Rejected by Initiator'
- REJECTED_BY_ACCEPTOR = 'REJECTED_BY_ACCEPTOR', 'Rejected by Acceptor'
-
+ ACCEPTED = "ACCEPTED", "Accepted"
+ SENT = "SENT", "Sent"
+ RECEIVED = "RECEIVED", "Received"
+ THANKED_BY_INITIATOR = "THANKED_BY_INITIATOR", "Thanked by Initiator"
+ THANKED_BY_ACCEPTOR = "THANKED_BY_ACCEPTOR", "Thanked by Acceptor"
+ THANKED_BY_BOTH = "THANKED_BY_BOTH", "Thanked by Both"
+ REJECTED_BY_INITIATOR = "REJECTED_BY_INITIATOR", "Rejected by Initiator"
+ REJECTED_BY_ACCEPTOR = "REJECTED_BY_ACCEPTOR", "Rejected by Acceptor"
+
# DRY improvement: define active states once as a class-level constant.
POSITIVE_STATES = [
AcceptanceState.ACCEPTED,
@@ -229,30 +242,21 @@ class TradeAcceptance(models.Model):
]
trade_offer = models.ForeignKey(
- TradeOffer,
- on_delete=models.CASCADE,
- related_name='acceptances',
- db_index=True
+ TradeOffer, on_delete=models.CASCADE, related_name="acceptances", db_index=True
)
accepted_by = models.ForeignKey(
"accounts.FriendCode",
on_delete=models.PROTECT,
- related_name='trade_acceptances'
+ related_name="trade_acceptances",
)
requested_card = models.ForeignKey(
- "cards.Card",
- on_delete=models.PROTECT,
- related_name='accepted_requested'
+ "cards.Card", on_delete=models.PROTECT, related_name="accepted_requested"
)
offered_card = models.ForeignKey(
- "cards.Card",
- on_delete=models.PROTECT,
- related_name='accepted_offered'
+ "cards.Card", on_delete=models.PROTECT, related_name="accepted_offered"
)
state = models.CharField(
- max_length=25,
- choices=AcceptanceState.choices,
- default=AcceptanceState.ACCEPTED
+ max_length=25, choices=AcceptanceState.choices, default=AcceptanceState.ACCEPTED
)
hash = models.CharField(max_length=9, editable=False, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
@@ -307,11 +311,14 @@ class TradeAcceptance(models.Model):
return self.get_action_label_for_state(self.AcceptanceState.SENT)
elif self.state == self.AcceptanceState.SENT:
return self.get_action_label_for_state(self.AcceptanceState.RECEIVED)
- elif self.state == self.AcceptanceState.RECEIVED or self.state == self.AcceptanceState.THANKED_BY_ACCEPTOR or self.state == self.AcceptanceState.THANKED_BY_INITIATOR:
+ elif (
+ self.state == self.AcceptanceState.RECEIVED
+ or self.state == self.AcceptanceState.THANKED_BY_ACCEPTOR
+ or self.state == self.AcceptanceState.THANKED_BY_INITIATOR
+ ):
return self.get_action_label_for_state(self.AcceptanceState.THANKED_BY_BOTH)
else:
return None
-
@classmethod
def get_action_label_for_state_2(cls, state_value):
@@ -331,12 +338,20 @@ class TradeAcceptance(models.Model):
@property
def is_initiator_state(self):
- return self.state in [self.AcceptanceState.SENT.value, self.AcceptanceState.THANKED_BY_INITIATOR.value, self.AcceptanceState.THANKED_BY_BOTH.value]
-
+ return self.state in [
+ self.AcceptanceState.SENT.value,
+ self.AcceptanceState.THANKED_BY_INITIATOR.value,
+ self.AcceptanceState.THANKED_BY_BOTH.value,
+ ]
+
@property
def is_acceptor_state(self):
- return self.state in [self.AcceptanceState.ACCEPTED.value, self.AcceptanceState.RECEIVED.value, self.AcceptanceState.THANKED_BY_ACCEPTOR.value, self.AcceptanceState.THANKED_BY_BOTH.value]
-
+ return self.state in [
+ self.AcceptanceState.ACCEPTED.value,
+ self.AcceptanceState.RECEIVED.value,
+ self.AcceptanceState.THANKED_BY_ACCEPTOR.value,
+ self.AcceptanceState.THANKED_BY_BOTH.value,
+ ]
@property
def is_completed(self):
@@ -368,19 +383,30 @@ class TradeAcceptance(models.Model):
def clean(self):
from django.core.exceptions import ValidationError
+
try:
- have_card = self.trade_offer.trade_offer_have_cards.get(card_id=self.requested_card_id)
+ have_card = self.trade_offer.trade_offer_have_cards.get(
+ card_id=self.requested_card_id
+ )
except TradeOfferHaveCard.DoesNotExist:
- raise ValidationError("The requested card must be one of the trade offer's available cards (have_cards).")
+ raise ValidationError(
+ "The requested card must be one of the trade offer's available cards (have_cards)."
+ )
try:
- want_card = self.trade_offer.trade_offer_want_cards.get(card_id=self.offered_card_id)
+ want_card = self.trade_offer.trade_offer_want_cards.get(
+ card_id=self.offered_card_id
+ )
except TradeOfferWantCard.DoesNotExist:
- raise ValidationError("The offered card must be one of the trade offer's requested cards (want_cards).")
+ raise ValidationError(
+ "The offered card must be one of the trade offer's requested cards (want_cards)."
+ )
# Only perform these validations on creation (when self.pk is None).
if self.pk is None:
if self.trade_offer.is_closed:
- raise ValidationError("This trade offer is closed. No more acceptances are allowed.")
+ raise ValidationError(
+ "This trade offer is closed. No more acceptances are allowed."
+ )
# Use direct comparison with qty_accepted and quantity.
if have_card.qty_accepted >= have_card.quantity:
raise ValidationError("The requested card has no available quantity.")
@@ -403,26 +429,42 @@ class TradeAcceptance(models.Model):
]:
return 0
else:
- return next(index for index, choice in enumerate(self.AcceptanceState.choices) if choice[0] == self.state) + 1
+ return (
+ next(
+ index
+ for index, choice in enumerate(self.AcceptanceState.choices)
+ if choice[0] == self.state
+ )
+ + 1
+ )
def update_state(self, new_state, user):
if new_state not in [choice[0] for choice in self.AcceptanceState.choices]:
raise ValueError(f"'{new_state}' is not a valid state.")
- if (new_state == self.AcceptanceState.THANKED_BY_ACCEPTOR and self.state == self.AcceptanceState.THANKED_BY_INITIATOR) or \
- (new_state == self.AcceptanceState.THANKED_BY_INITIATOR and self.state == self.AcceptanceState.THANKED_BY_ACCEPTOR):
+ if (
+ new_state == self.AcceptanceState.THANKED_BY_ACCEPTOR
+ and self.state == self.AcceptanceState.THANKED_BY_INITIATOR
+ ) or (
+ new_state == self.AcceptanceState.THANKED_BY_INITIATOR
+ and self.state == self.AcceptanceState.THANKED_BY_ACCEPTOR
+ ):
new_state = self.AcceptanceState.THANKED_BY_BOTH
if self.state in [
self.AcceptanceState.THANKED_BY_BOTH,
self.AcceptanceState.REJECTED_BY_INITIATOR,
- self.AcceptanceState.REJECTED_BY_ACCEPTOR
+ self.AcceptanceState.REJECTED_BY_ACCEPTOR,
]:
- raise ValueError(f"No transitions allowed from the terminal state '{self.state}'.")
+ raise ValueError(
+ f"No transitions allowed from the terminal state '{self.state}'."
+ )
allowed = [x for x, y in self.get_allowed_state_transitions(user)]
if new_state not in allowed:
- raise ValueError(f"Transition from {self.state} to {new_state} is not allowed.")
+ raise ValueError(
+ f"Transition from {self.state} to {new_state} is not allowed."
+ )
self._actioning_user = user
self.state = new_state
@@ -434,10 +476,12 @@ class TradeAcceptance(models.Model):
super().save(*args, **kwargs)
def __str__(self):
- return (f"TradeAcceptance(offer_hash={self.trade_offer.hash}, "
- f"accepted_by={self.accepted_by}, "
- f"requested_card={self.requested_card}, "
- f"offered_card={self.offered_card}, state={self.state})")
+ return (
+ f"TradeAcceptance(offer_hash={self.trade_offer.hash}, "
+ f"accepted_by={self.accepted_by}, "
+ f"requested_card={self.requested_card}, "
+ f"offered_card={self.offered_card}, state={self.state})"
+ )
def get_allowed_state_transitions(self, user):
if self.trade_offer.initiated_by in user.friend_codes.all():
@@ -453,7 +497,7 @@ class TradeAcceptance(models.Model):
self.AcceptanceState.THANKED_BY_INITIATOR,
self.AcceptanceState.REJECTED_BY_INITIATOR,
},
- self.AcceptanceState.THANKED_BY_INITIATOR: { },
+ self.AcceptanceState.THANKED_BY_INITIATOR: {},
self.AcceptanceState.THANKED_BY_ACCEPTOR: {
self.AcceptanceState.REJECTED_BY_INITIATOR,
self.AcceptanceState.THANKED_BY_BOTH,
@@ -469,10 +513,10 @@ class TradeAcceptance(models.Model):
self.AcceptanceState.REJECTED_BY_ACCEPTOR,
},
self.AcceptanceState.RECEIVED: {
- self.AcceptanceState.THANKED_BY_ACCEPTOR, #allow early thanks (uses THANKED_BY_ACCEPTOR state)
- self.AcceptanceState.REJECTED_BY_ACCEPTOR
+ self.AcceptanceState.THANKED_BY_ACCEPTOR, # allow early thanks (uses THANKED_BY_ACCEPTOR state)
+ self.AcceptanceState.REJECTED_BY_ACCEPTOR,
},
- self.AcceptanceState.THANKED_BY_ACCEPTOR: { },
+ self.AcceptanceState.THANKED_BY_ACCEPTOR: {},
self.AcceptanceState.THANKED_BY_INITIATOR: {
self.AcceptanceState.THANKED_BY_BOTH,
},
diff --git a/src/pkmntrade_club/trades/signals.py b/src/pkmntrade_club/trades/signals.py
index 37caddb..e11c6e3 100644
--- a/src/pkmntrade_club/trades/signals.py
+++ b/src/pkmntrade_club/trades/signals.py
@@ -1,19 +1,15 @@
from django.db.models.signals import post_save, post_delete, pre_save
from django.dispatch import receiver
from django.db.models import F
-from pkmntrade_club.trades.models import TradeOfferHaveCard, TradeOfferWantCard, TradeAcceptance, TradeOffer
-from django.db import transaction
+from pkmntrade_club.trades.models import (
+ TradeOfferHaveCard,
+ TradeOfferWantCard,
+ TradeAcceptance,
+)
from pkmntrade_club.accounts.models import CustomUser
-from datetime import timedelta
-from django.utils import timezone
-import uuid
-import hashlib
from django.core.mail import send_mail
-from django.conf import settings
from django.template.loader import render_to_string
from django.contrib.sites.models import Site
-from django.core.cache import cache
-import logging
POSITIVE_STATES = [
TradeAcceptance.AcceptanceState.ACCEPTED,
@@ -24,20 +20,20 @@ POSITIVE_STATES = [
TradeAcceptance.AcceptanceState.THANKED_BY_BOTH,
]
+
def adjust_qty_for_trade_offer(trade_offer, card, side, delta):
"""
Increment (or decrement) qty_accepted by delta for the given card on the specified side.
"""
- if side == 'have':
- TradeOfferHaveCard.objects.filter(
- trade_offer=trade_offer,
- card=card
- ).update(qty_accepted=F('qty_accepted') + delta)
- elif side == 'want':
- TradeOfferWantCard.objects.filter(
- trade_offer=trade_offer,
- card=card
- ).update(qty_accepted=F('qty_accepted') + delta)
+ if side == "have":
+ TradeOfferHaveCard.objects.filter(trade_offer=trade_offer, card=card).update(
+ qty_accepted=F("qty_accepted") + delta
+ )
+ elif side == "want":
+ TradeOfferWantCard.objects.filter(trade_offer=trade_offer, card=card).update(
+ qty_accepted=F("qty_accepted") + delta
+ )
+
def update_trade_offer_closed_status(trade_offer):
"""
@@ -46,18 +42,17 @@ def update_trade_offer_closed_status(trade_offer):
greater than or equal to quantity; otherwise, mark it as open.
"""
have_complete = not TradeOfferHaveCard.objects.filter(
- trade_offer=trade_offer,
- qty_accepted__lt=F('quantity')
+ trade_offer=trade_offer, qty_accepted__lt=F("quantity")
).exists()
want_complete = not TradeOfferWantCard.objects.filter(
- trade_offer=trade_offer,
- qty_accepted__lt=F('quantity')
+ trade_offer=trade_offer, qty_accepted__lt=F("quantity")
).exists()
closed = have_complete or want_complete
if trade_offer.is_closed != closed:
trade_offer.is_closed = closed
trade_offer.save(update_fields=["is_closed"])
+
@receiver(pre_save, sender=TradeAcceptance)
def trade_acceptance_pre_save(sender, instance, **kwargs):
# Skip signal processing during raw fixture load or when saving a new instance
@@ -68,6 +63,7 @@ def trade_acceptance_pre_save(sender, instance, **kwargs):
old_instance = TradeAcceptance.objects.get(pk=instance.pk)
instance._old_state = old_instance.state
+
@receiver(post_save, sender=TradeAcceptance)
def trade_acceptance_post_save(sender, instance, created, **kwargs):
delta = 0
@@ -75,7 +71,7 @@ def trade_acceptance_post_save(sender, instance, created, **kwargs):
if instance.state in POSITIVE_STATES:
delta = 1
else:
- old_state = getattr(instance, '_old_state', None)
+ old_state = getattr(instance, "_old_state", None)
if old_state is not None:
if old_state in POSITIVE_STATES and instance.state not in POSITIVE_STATES:
delta = -1
@@ -84,19 +80,29 @@ def trade_acceptance_post_save(sender, instance, created, **kwargs):
if delta != 0:
trade_offer = instance.trade_offer
- 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)
+ 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_delete, sender=TradeAcceptance)
def trade_acceptance_post_delete(sender, instance, **kwargs):
if instance.state in POSITIVE_STATES:
delta = -1
trade_offer = instance.trade_offer
- 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)
+ 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.
@@ -132,7 +138,6 @@ def trade_acceptance_email_notification(sender, instance, created, **kwargs):
else:
return
-
# Determine the non-acting party:
if instance.trade_offer.initiated_by.user.pk == acting_user.pk:
# The initiator made the change; notify the acceptor.
@@ -153,17 +158,31 @@ def trade_acceptance_email_notification(sender, instance, created, **kwargs):
"want_card": instance.offered_card,
"hash": instance.hash,
"acting_user": acting_user.username,
- "acting_user_ign": instance.trade_offer.initiated_by.in_game_name if is_initiator else instance.accepted_by.in_game_name,
+ "acting_user_ign": (
+ instance.trade_offer.initiated_by.in_game_name
+ if is_initiator
+ else instance.accepted_by.in_game_name
+ ),
"recipient_user": recipient_user.username,
- "recipient_user_ign": instance.accepted_by.in_game_name if is_initiator else instance.trade_offer.initiated_by.in_game_name,
- "acting_user_friend_code": instance.trade_offer.initiated_by.friend_code if is_initiator else instance.accepted_by.friend_code,
+ "recipient_user_ign": (
+ instance.accepted_by.in_game_name
+ if is_initiator
+ else instance.trade_offer.initiated_by.in_game_name
+ ),
+ "acting_user_friend_code": (
+ instance.trade_offer.initiated_by.friend_code
+ if is_initiator
+ else instance.accepted_by.friend_code
+ ),
"is_initiator": is_initiator,
"domain": "https://" + Site.objects.get_current().domain,
"pk": instance.pk,
}
email_template = "email/trades/trade_update_" + state + ".txt"
email_subject = render_to_string("email/common/subject.txt", email_context)
- email_subject += render_to_string("email/trades/trade_update_" + state + "_subject.txt", email_context)
+ email_subject += render_to_string(
+ "email/trades/trade_update_" + state + "_subject.txt", email_context
+ )
email_body = render_to_string(email_template, email_context)
send_mail(
@@ -173,17 +192,18 @@ def trade_acceptance_email_notification(sender, instance, created, **kwargs):
[recipient_user.email],
)
+
@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
+ 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:
@@ -191,28 +211,46 @@ def trade_acceptance_reputation_update(sender, instance, created, **kwargs):
thanks_delta = 0
rejection_delta_initiator = 0 # Delta for the initiator's reputation
- rejection_delta_acceptor = 0 # Delta for the acceptor's reputation
+ rejection_delta_acceptor = 0 # Delta for the acceptor's reputation
- old_state = getattr(instance, '_old_state', None)
+ 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:
+ 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:
+ 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:
+ 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:
+ 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:
+ 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:
+ elif (
+ old_state == TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR
+ and instance.state != TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR
+ ):
rejection_delta_initiator = 1
# Apply reputation updates:
@@ -237,12 +275,13 @@ def trade_acceptance_reputation_update(sender, instance, created, **kwargs):
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.
@@ -263,6 +302,7 @@ def trade_acceptance_reputation_delete(sender, instance, **kwargs):
reputation_score=F("reputation_score") + 1
)
+
@receiver(post_save, sender=TradeOfferHaveCard)
@receiver(post_delete, sender=TradeOfferHaveCard)
@receiver(post_save, sender=TradeOfferWantCard)
@@ -274,9 +314,11 @@ def bubble_up_trade_offer_updates(sender, instance, **kwargs):
Bubble up updated_at to the TradeOffer model when related instances change.
Also invalidates any cached image by deleting the file.
"""
- trade_offer = getattr(instance, 'trade_offer', None)
+ trade_offer = getattr(instance, "trade_offer", None)
if trade_offer and trade_offer.image:
- trade_offer.image.delete(save=True) # deleting the image will trigger a save, which updates the updated_at field
+ trade_offer.image.delete(
+ save=True
+ ) # deleting the image will trigger a save, which updates the updated_at field
elif trade_offer:
- trade_offer.save(update_fields=['updated_at'])
\ No newline at end of file
+ trade_offer.save(update_fields=["updated_at"])
diff --git a/src/pkmntrade_club/trades/templatetags/trade_offer_tags.py b/src/pkmntrade_club/trades/templatetags/trade_offer_tags.py
index 2978445..56d92e1 100644
--- a/src/pkmntrade_club/trades/templatetags/trade_offer_tags.py
+++ b/src/pkmntrade_club/trades/templatetags/trade_offer_tags.py
@@ -1,9 +1,10 @@
from django import template
from math import ceil
-from pkmntrade_club.trades.models import TradeAcceptance
+
register = template.Library()
-@register.inclusion_tag('templatetags/trade_offer.html', takes_context=True)
+
+@register.inclusion_tag("templatetags/trade_offer.html", takes_context=True)
def render_trade_offer(context, offer):
"""
Renders a trade offer including detailed trade acceptance information.
@@ -15,14 +16,11 @@ def render_trade_offer(context, offer):
trade_offer_want_cards = list(offer.trade_offer_want_cards.all())
acceptances = list(offer.acceptances.all())
-
have_cards_available = [
- card for card in trade_offer_have_cards
- if card.quantity > card.qty_accepted
+ card for card in trade_offer_have_cards if card.quantity > card.qty_accepted
]
want_cards_available = [
- card for card in trade_offer_want_cards
- if card.quantity > card.qty_accepted
+ card for card in trade_offer_want_cards if card.quantity > card.qty_accepted
]
if not have_cards_available or not want_cards_available:
@@ -31,37 +29,41 @@ def render_trade_offer(context, offer):
flipped = False
tag_context = {
- 'offer_pk': offer.pk,
- 'flipped': flipped,
- '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,
- 'initiated_reputation': offer.initiated_by.user.reputation_score,
- 'acceptances': acceptances,
- 'have_cards_available': have_cards_available,
- 'want_cards_available': 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)+"/"),
- 'cache_key': f'trade_offer_{offer.pk}_{offer.updated_at.timestamp()}_{flipped}',
+ "offer_pk": offer.pk,
+ "flipped": flipped,
+ "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,
+ "initiated_reputation": offer.initiated_by.user.reputation_score,
+ "acceptances": acceptances,
+ "have_cards_available": have_cards_available,
+ "want_cards_available": 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) + "/"
+ ),
+ "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):
"""
Renders a simple trade acceptance view with a single row and simplified header/footer.
"""
tag_context = {
"acceptance": acceptance,
- 'cache_key': f'trade_acceptance_{acceptance.pk}_{acceptance.updated_at.timestamp()}',
+ "cache_key": f"trade_acceptance_{acceptance.pk}_{acceptance.updated_at.timestamp()}",
}
context.update(tag_context)
return context
+
@register.filter
def get_action_label(acceptance, state_value):
"""
@@ -69,25 +71,27 @@ def get_action_label(acceptance, state_value):
"""
return acceptance.get_action_label_for_state(state_value)
+
@register.filter
def action_button_class(state_value):
"""
Returns daisyUI button classes based on the provided state value.
"""
mapping = {
- 'ACCEPTED': 'btn btn-primary',
- 'SENT': 'btn btn-info',
- 'RECEIVED': 'btn btn-info',
- 'THANKED_BY_INITIATOR': 'btn btn-success',
- 'THANKED_BY_ACCEPTOR': 'btn btn-success',
- 'THANKED_BY_BOTH': 'btn btn-success',
- 'REJECTED_BY_INITIATOR': 'btn btn-error',
- 'REJECTED_BY_ACCEPTOR': 'btn btn-error',
+ "ACCEPTED": "btn btn-primary",
+ "SENT": "btn btn-info",
+ "RECEIVED": "btn btn-info",
+ "THANKED_BY_INITIATOR": "btn btn-success",
+ "THANKED_BY_ACCEPTOR": "btn btn-success",
+ "THANKED_BY_BOTH": "btn btn-success",
+ "REJECTED_BY_INITIATOR": "btn btn-error",
+ "REJECTED_BY_ACCEPTOR": "btn btn-error",
}
# Return a default style if the state isn't in the mapping.
- return mapping.get(state_value, 'btn btn-outline')
+ return mapping.get(state_value, "btn btn-outline")
-@register.inclusion_tag('templatetags/trade_offer_png.html', takes_context=True)
+
+@register.inclusion_tag("templatetags/trade_offer_png.html", takes_context=True)
def render_trade_offer_png(context, offer, show_friend_code=False):
CARD_HEIGHT = 32
CARD_WIDTH = 160
@@ -96,24 +100,29 @@ def render_trade_offer_png(context, offer, show_friend_code=False):
CARD_WIDTH_PADDING = 64
EXPANDED_CARD_WIDTH_PADDING = 80
CARD_COL_GAP = 4
- OUTPUT_PADDING = 24 # height padding is handled by the HTML
+ OUTPUT_PADDING = 24 # height padding is handled by the HTML
have_cards_available = offer.have_cards_available
want_cards_available = offer.want_cards_available
num_cards = max(len(have_cards_available), len(want_cards_available))
expanded = (len(have_cards_available) + len(want_cards_available)) > 4
if expanded:
- num_cards = ceil(num_cards / 2) # 2 cards per row if expanded
- image_height = (num_cards * CARD_HEIGHT) + ((num_cards - 1) * CARD_COL_GAP) + HEADER_HEIGHT + FOOTER_HEIGHT
+ num_cards = ceil(num_cards / 2) # 2 cards per row if expanded
+ image_height = (
+ (num_cards * CARD_HEIGHT)
+ + ((num_cards - 1) * CARD_COL_GAP)
+ + HEADER_HEIGHT
+ + FOOTER_HEIGHT
+ )
if expanded:
image_width = (4 * CARD_WIDTH) + EXPANDED_CARD_WIDTH_PADDING
else:
image_width = (2 * CARD_WIDTH) + CARD_WIDTH_PADDING
- image_width += OUTPUT_PADDING
- image_height += OUTPUT_PADDING # height padding is handled by the HTML, but we need to also calculate it here for og meta tag use
-
+ image_width += OUTPUT_PADDING
+ image_height += OUTPUT_PADDING # height padding is handled by the HTML, but we need to also calculate it here for og meta tag use
+
request = context.get("request")
if request.get_host().startswith("localhost"):
base_url = "http://{0}".format(request.get_host())
@@ -121,23 +130,23 @@ def render_trade_offer_png(context, offer, show_friend_code=False):
base_url = "https://{0}".format(request.get_host())
tag_context = {
- '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,
- 'have_cards_available': have_cards_available,
- 'want_cards_available': want_cards_available,
- 'in_game_name': offer.initiated_by.in_game_name,
- 'friend_code': offer.initiated_by.friend_code,
- 'show_friend_code': show_friend_code,
- 'num_cards_available': len(have_cards_available) + len(want_cards_available),
- 'expanded': expanded,
- 'image_width': image_width,
- 'image_height': image_height,
- 'base_url': base_url,
- 'cache_key': f'trade_offer_png_{offer.pk}_{offer.updated_at.timestamp()}_{expanded}',
+ "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,
+ "have_cards_available": have_cards_available,
+ "want_cards_available": want_cards_available,
+ "in_game_name": offer.initiated_by.in_game_name,
+ "friend_code": offer.initiated_by.friend_code,
+ "show_friend_code": show_friend_code,
+ "num_cards_available": len(have_cards_available) + len(want_cards_available),
+ "expanded": expanded,
+ "image_width": image_width,
+ "image_height": image_height,
+ "base_url": base_url,
+ "cache_key": f"trade_offer_png_{offer.pk}_{offer.updated_at.timestamp()}_{expanded}",
}
context.update(tag_context)
- return context
\ No newline at end of file
+ return context
diff --git a/src/pkmntrade_club/trades/tests.py b/src/pkmntrade_club/trades/tests.py
index c188c16..a38de08 100644
--- a/src/pkmntrade_club/trades/tests.py
+++ b/src/pkmntrade_club/trades/tests.py
@@ -20,6 +20,7 @@ from pkmntrade_club.trades.forms import (
)
from tests.utils.rarity import RARITY_MAPPING
+
# ------------------------------------------------------------------------
# Model Tests
# ------------------------------------------------------------------------
@@ -35,17 +36,29 @@ class TradeOfferModelTest(TestCase):
# Create cards with the same rarity (valid scenario)
self.card1 = Card.objects.create(
- name="Card1", cardset="set1", cardnum=1, style="default",
- rarity_icon=RARITY_MAPPING[1], rarity_level=1
+ name="Card1",
+ cardset="set1",
+ cardnum=1,
+ style="default",
+ rarity_icon=RARITY_MAPPING[1],
+ rarity_level=1,
)
self.card2 = Card.objects.create(
- name="Card2", cardset="set1", cardnum=2, style="default",
- rarity_icon=RARITY_MAPPING[1], rarity_level=1
+ name="Card2",
+ cardset="set1",
+ cardnum=2,
+ style="default",
+ rarity_icon=RARITY_MAPPING[1],
+ rarity_level=1,
)
# Create a card with a different rarity (to test invalid trade offers)
self.card3 = Card.objects.create(
- name="Card3", cardset="set1", cardnum=3, style="default",
- rarity_icon=RARITY_MAPPING[8], rarity_level=8
+ name="Card3",
+ cardset="set1",
+ cardnum=3,
+ style="default",
+ rarity_icon=RARITY_MAPPING[8],
+ rarity_level=8,
)
# Create a valid trade offer with consistent rarity details
@@ -92,17 +105,27 @@ class TradeAcceptanceModelTest(TestCase):
username="initiator", email="init@example.com", password="password"
)
self.initiator_friend_code = FriendCode.objects.create(
- friend_code="5555-6666-7777-8888", in_game_name="InitInGame", user=self.other_user
+ friend_code="5555-6666-7777-8888",
+ in_game_name="InitInGame",
+ user=self.other_user,
)
# Create two cards (with the same rarity)
self.card1 = Card.objects.create(
- name="CardA", cardset="setA", cardnum=1, style="default",
- rarity_icon=RARITY_MAPPING[2], rarity_level=2
+ name="CardA",
+ cardset="setA",
+ cardnum=1,
+ style="default",
+ rarity_icon=RARITY_MAPPING[2],
+ rarity_level=2,
)
self.card2 = Card.objects.create(
- name="CardB", cardset="setA", cardnum=2, style="default",
- rarity_icon=RARITY_MAPPING[2], rarity_level=2
+ name="CardB",
+ cardset="setA",
+ cardnum=2,
+ style="default",
+ rarity_icon=RARITY_MAPPING[2],
+ rarity_level=2,
)
# Create a trade offer by the initiator.
@@ -150,9 +173,7 @@ class TradeAcceptanceModelTest(TestCase):
self.acceptance.update_state(
TradeAcceptance.AcceptanceState.SENT, user=self.other_user
)
- self.assertEqual(
- self.acceptance.state, TradeAcceptance.AcceptanceState.SENT
- )
+ self.assertEqual(self.acceptance.state, TradeAcceptance.AcceptanceState.SENT)
def test_signal_adjusts_qty_accepted(self):
"""
@@ -206,12 +227,20 @@ class TradeOfferFormTest(TestCase):
)
# Create two cards with the same rarity details.
self.card1 = Card.objects.create(
- name="FormCard1", cardset="formset", cardnum=1, style="default",
- rarity_icon=RARITY_MAPPING[3], rarity_level=3
+ name="FormCard1",
+ cardset="formset",
+ cardnum=1,
+ style="default",
+ rarity_icon=RARITY_MAPPING[3],
+ rarity_level=3,
)
self.card2 = Card.objects.create(
- name="FormCard2", cardset="formset", cardnum=2, style="default",
- rarity_icon=RARITY_MAPPING[3], rarity_level=3
+ name="FormCard2",
+ cardset="formset",
+ cardnum=2,
+ style="default",
+ rarity_icon=RARITY_MAPPING[3],
+ rarity_level=3,
)
def test_trade_offer_create_form_valid(self):
@@ -219,7 +248,7 @@ class TradeOfferFormTest(TestCase):
A valid POST using colon-separated quantity strings should succeed.
"""
# Build a QueryDict with multiple values for each list field.
- qd = QueryDict('', mutable=True)
+ qd = QueryDict("", mutable=True)
qd.setlist("have_cards", [f"{self.card1.pk}:2"])
qd.setlist("want_cards", [f"{self.card2.pk}:3"])
# 'initiated_by' is a normal field so we can update it directly.
@@ -231,7 +260,7 @@ class TradeOfferFormTest(TestCase):
"""
If quantity cannot be parsed as an integer a ValidationError should be raised.
"""
- qd = QueryDict('', mutable=True)
+ qd = QueryDict("", mutable=True)
# Provide an invalid quantity ("two" instead of an integer).
qd.setlist("have_cards", [f"{self.card1.pk}:two"])
qd.setlist("want_cards", [f"{self.card2.pk}:3"])
@@ -244,7 +273,7 @@ class TradeOfferFormTest(TestCase):
"""
An entry missing a colon should be ignored.
"""
- qd = QueryDict('', mutable=True)
+ qd = QueryDict("", mutable=True)
# No colon present in the selections.
qd.setlist("have_cards", [f"{self.card1.pk}"])
qd.setlist("want_cards", [f"{self.card2.pk}"])
@@ -283,9 +312,7 @@ class TradeOfferFormTest(TestCase):
"""Test that TradeOfferAcceptForm correctly sets the friend_code queryset."""
friend_codes = FriendCode.objects.filter(pk=self.friend_code.pk)
form = TradeOfferAcceptForm(friend_codes=friend_codes)
- self.assertEqual(
- list(form.fields["friend_code"].queryset), list(friend_codes)
- )
+ self.assertEqual(list(form.fields["friend_code"].queryset), list(friend_codes))
def test_trade_acceptance_transition_form(self):
"""Test that the transition form provides only allowed transitions."""
@@ -312,7 +339,10 @@ class TradeOfferFormTest(TestCase):
)
form = TradeAcceptanceTransitionForm(instance=acceptance, user=other_user)
# Compare the form's state choices with the allowed transitions.
- allowed = [choice[0] for choice in acceptance.get_allowed_state_transitions(user=other_user)]
+ allowed = [
+ choice[0]
+ for choice in acceptance.get_allowed_state_transitions(user=other_user)
+ ]
form_choices = [choice[0] for choice in form.fields["state"].choices]
for choice in allowed:
self.assertIn(choice, form_choices)
@@ -337,12 +367,20 @@ class TradeViewsTest(TestCase):
# Create sample cards.
self.card1 = Card.objects.create(
- name="ViewCard1", cardset="setV", cardnum=1, style="default",
- rarity_icon=RARITY_MAPPING[7], rarity_level=7
+ name="ViewCard1",
+ cardset="setV",
+ cardnum=1,
+ style="default",
+ rarity_icon=RARITY_MAPPING[7],
+ rarity_level=7,
)
self.card2 = Card.objects.create(
- name="ViewCard2", cardset="setV", cardnum=2, style="default",
- rarity_icon=RARITY_MAPPING[7], rarity_level=7
+ name="ViewCard2",
+ cardset="setV",
+ cardnum=2,
+ style="default",
+ rarity_icon=RARITY_MAPPING[7],
+ rarity_level=7,
)
# Create a trade offer initiated by the logged-in user's friend code.
self.trade_offer = TradeOffer.objects.create(initiated_by=self.friend_code)
@@ -387,7 +425,9 @@ class TradeViewsTest(TestCase):
Instead, if no active acceptances remain it should mark the offer as closed.
"""
# Create a trade offer with an active acceptance.
- trade_offer_with_acceptance = TradeOffer.objects.create(initiated_by=self.friend_code)
+ trade_offer_with_acceptance = TradeOffer.objects.create(
+ initiated_by=self.friend_code
+ )
# Use quantity=2 so the trade offer isn't automatically closed when one acceptance is created
TradeOfferHaveCard.objects.create(
trade_offer=trade_offer_with_acceptance, card=self.card1, quantity=2
@@ -403,10 +443,13 @@ class TradeViewsTest(TestCase):
offered_card=self.card2,
state=TradeAcceptance.AcceptanceState.ACCEPTED,
)
- delete_url = reverse("trade_offer_delete", kwargs={"pk": trade_offer_with_acceptance.pk})
+ delete_url = reverse(
+ "trade_offer_delete", kwargs={"pk": trade_offer_with_acceptance.pk}
+ )
# --- Patch the view's get_object() method to return our trade offer ---
from pkmntrade_club.trades.views import TradeOfferDeleteView
+
orig_get_object = TradeOfferDeleteView.get_object
TradeOfferDeleteView.get_object = lambda self: trade_offer_with_acceptance
@@ -445,21 +488,27 @@ class TradeViewsTest(TestCase):
state=TradeAcceptance.AcceptanceState.ACCEPTED,
)
update_url = reverse("trade_acceptance_update", kwargs={"pk": acceptance.pk})
-
+
# First, try an invalid state update.
response = self.client.post(update_url, {"state": "INVALID_STATE"})
self.assertEqual(response.status_code, 200)
-
+
form = response.context.get("form")
self.assertIsNotNone(form, "Form should be present in the response context.")
self.assertIn(
- "state", form.errors,
- "Expected an error on the 'state' field when an invalid state is submitted."
+ "state",
+ form.errors,
+ "Expected an error on the 'state' field when an invalid state is submitted.",
+ )
+ self.assertTrue(
+ form.errors["state"], "The 'state' field should have error messages."
)
- self.assertTrue(form.errors["state"], "The 'state' field should have error messages.")
# Next, if there is an allowed valid transition, try it.
- allowed_states = [choice[0] for choice in acceptance.get_allowed_state_transitions(user=self.user)]
+ allowed_states = [
+ choice[0]
+ for choice in acceptance.get_allowed_state_transitions(user=self.user)
+ ]
if allowed_states:
valid_state = allowed_states[0]
response = self.client.post(update_url, {"state": valid_state})
@@ -493,12 +542,20 @@ class TradeOfferSecurityTests(TestCase):
# Create test cards with proper rarity levels
self.card1 = Card.objects.create(
- name="SecCard1", cardset="secset", cardnum=1, style="default",
- rarity_icon=RARITY_MAPPING[3], rarity_level=3
+ name="SecCard1",
+ cardset="secset",
+ cardnum=1,
+ style="default",
+ rarity_icon=RARITY_MAPPING[3],
+ rarity_level=3,
)
self.card2 = Card.objects.create(
- name="SecCard2", cardset="secset", cardnum=2, style="default",
- rarity_icon=RARITY_MAPPING[3], rarity_level=3
+ name="SecCard2",
+ cardset="secset",
+ cardnum=2,
+ style="default",
+ rarity_icon=RARITY_MAPPING[3],
+ rarity_level=3,
)
# Create a trade offer by user1
@@ -536,14 +593,14 @@ class TradeOfferSecurityTests(TestCase):
self.client.login(username="user3", password="password3")
response = self.client.post(
reverse("trade_acceptance_update", kwargs={"pk": acceptance.pk}),
- {"state": TradeAcceptance.AcceptanceState.SENT}
+ {"state": TradeAcceptance.AcceptanceState.SENT},
)
self.assertEqual(response.status_code, 403)
def test_cross_user_friend_code_manipulation(self):
"""Test that users cannot use other users' friend codes."""
self.client.login(username="user2", password="password2")
-
+
# Try to create a trade offer using user1's friend code
response = self.client.get(
reverse("trade_offer_create"),
@@ -551,12 +608,10 @@ class TradeOfferSecurityTests(TestCase):
"initiated_by": self.fc1.pk, # User1's friend code
"have_cards": [f"{self.card1.pk}:1"],
"want_cards": [f"{self.card2.pk}:1"],
- }
+ },
)
self.assertEqual(response.status_code, 200) # Form should fail validation
- self.assertFalse(
- TradeOffer.objects.filter(initiated_by=self.fc1).count() > 1
- )
+ self.assertFalse(TradeOffer.objects.filter(initiated_by=self.fc1).count() > 1)
def test_authenticated_only_views(self):
"""Test that authenticated-only views are properly protected."""
@@ -564,18 +619,20 @@ class TradeOfferSecurityTests(TestCase):
urls_to_test = [
reverse("trade_offer_create"),
reverse("trade_offer_dashboard"),
- reverse("trade_acceptance_create", kwargs={"offer_pk": self.trade_offer.pk}),
+ reverse(
+ "trade_acceptance_create", kwargs={"offer_pk": self.trade_offer.pk}
+ ),
]
-
+
# First ensure we're logged out
self.client.logout()
-
+
for url in urls_to_test:
response = self.client.get(url)
self.assertRedirects(
response,
f"/accounts/login/?next={url}",
- msg_prefix=f"URL {url} should require authentication"
+ msg_prefix=f"URL {url} should require authentication",
)
@@ -588,19 +645,31 @@ class TradeOfferEdgeCasesTest(TestCase):
self.friend_code = FriendCode.objects.create(
friend_code="3333-4444-5555-6666", in_game_name="EdgeUser", user=self.user
)
-
+
# Create test cards with different rarities using proper levels and icons
self.common_card = Card.objects.create(
- name="CommonCard", cardset="edgeset", cardnum=1, style="default",
- rarity_icon=RARITY_MAPPING[1], rarity_level=1
+ name="CommonCard",
+ cardset="edgeset",
+ cardnum=1,
+ style="default",
+ rarity_icon=RARITY_MAPPING[1],
+ rarity_level=1,
)
self.rare_card = Card.objects.create(
- name="RareCard", cardset="edgeset", cardnum=2, style="default",
- rarity_icon=RARITY_MAPPING[5], rarity_level=5
+ name="RareCard",
+ cardset="edgeset",
+ cardnum=2,
+ style="default",
+ rarity_icon=RARITY_MAPPING[5],
+ rarity_level=5,
)
self.crown_card = Card.objects.create(
- name="CrownCard", cardset="edgeset", cardnum=3, style="default",
- rarity_icon=RARITY_MAPPING[8], rarity_level=8
+ name="CrownCard",
+ cardset="edgeset",
+ cardnum=3,
+ style="default",
+ rarity_icon=RARITY_MAPPING[8],
+ rarity_level=8,
)
self.client = Client()
@@ -614,7 +683,7 @@ class TradeOfferEdgeCasesTest(TestCase):
"initiated_by": self.friend_code.pk,
"have_cards": [f"{self.common_card.pk}:0"],
"want_cards": [f"{self.common_card.pk}:1"],
- }
+ },
)
self.assertEqual(response.status_code, 200)
self.assertFalse(
@@ -629,7 +698,7 @@ class TradeOfferEdgeCasesTest(TestCase):
"initiated_by": self.friend_code.pk,
"have_cards": [f"{self.common_card.pk}:-1"],
"want_cards": [f"{self.common_card.pk}:1"],
- }
+ },
)
self.assertEqual(response.status_code, 200)
self.assertFalse(
@@ -644,7 +713,7 @@ class TradeOfferEdgeCasesTest(TestCase):
"initiated_by": self.friend_code.pk,
"have_cards": [f"{self.common_card.pk}:1"],
"want_cards": [f"{self.crown_card.pk}:1"],
- }
+ },
)
self.assertEqual(response.status_code, 200)
self.assertFalse(
@@ -657,12 +726,9 @@ class TradeOfferEdgeCasesTest(TestCase):
reverse("trade_offer_create"),
{
"initiated_by": self.friend_code.pk,
- "have_cards": [
- f"{self.common_card.pk}:1",
- f"{self.common_card.pk}:1"
- ],
+ "have_cards": [f"{self.common_card.pk}:1", f"{self.common_card.pk}:1"],
"want_cards": [f"{self.common_card.pk}:1"],
- }
+ },
)
self.assertEqual(response.status_code, 200)
self.assertFalse(
@@ -682,16 +748,28 @@ class TradeSearchTests(TestCase):
# Create test cards with proper rarity levels
self.card1 = Card.objects.create(
- name="SearchCard1", cardset="sc1", cardnum=1, style="default",
- rarity_icon=RARITY_MAPPING[4], rarity_level=4
+ name="SearchCard1",
+ cardset="sc1",
+ cardnum=1,
+ style="default",
+ rarity_icon=RARITY_MAPPING[4],
+ rarity_level=4,
)
self.card2 = Card.objects.create(
- name="SearchCard2", cardset="sc1", cardnum=2, style="default",
- rarity_icon=RARITY_MAPPING[4], rarity_level=4
+ name="SearchCard2",
+ cardset="sc1",
+ cardnum=2,
+ style="default",
+ rarity_icon=RARITY_MAPPING[4],
+ rarity_level=4,
)
self.card3 = Card.objects.create(
- name="SearchCard3", cardset="sc1", cardnum=3, style="default",
- rarity_icon=RARITY_MAPPING[4], rarity_level=4
+ name="SearchCard3",
+ cardset="sc1",
+ cardnum=3,
+ style="default",
+ rarity_icon=RARITY_MAPPING[4],
+ rarity_level=4,
)
# Create some trade offers
@@ -719,7 +797,7 @@ class TradeSearchTests(TestCase):
reverse("trade_offer_search"),
{
"have_cards": [f"{self.card2.pk}:1"],
- }
+ },
)
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, self.trade_offer1.initiated_by.in_game_name)
@@ -731,7 +809,7 @@ class TradeSearchTests(TestCase):
reverse("trade_offer_search"),
{
"want_cards": [f"{self.card1.pk}:1"],
- }
+ },
)
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, self.trade_offer1.initiated_by.in_game_name)
@@ -743,7 +821,7 @@ class TradeSearchTests(TestCase):
reverse("trade_offer_search"),
{
"have_cards": ["999999:1"], # Non-existent card ID
- }
+ },
)
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, self.trade_offer1.initiated_by.in_game_name)
@@ -753,12 +831,12 @@ class TradeSearchTests(TestCase):
"""Test that closed trades don't appear in search results."""
self.trade_offer1.is_closed = True
self.trade_offer1.save()
-
+
response = self.client.post(
reverse("trade_offer_search"),
{
"have_cards": [f"{self.card2.pk}:1"],
- }
+ },
)
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, self.trade_offer1.initiated_by.in_game_name)
@@ -773,30 +851,50 @@ class TradeAcceptanceComplexTests(TestCase):
self.acceptor = User.objects.create_user(
username="acceptor", email="accept@example.com", password="password"
)
-
+
self.initiator_fc = FriendCode.objects.create(
- friend_code="1234-5678-9012-3456", in_game_name="InitUser", user=self.initiator
+ friend_code="1234-5678-9012-3456",
+ in_game_name="InitUser",
+ user=self.initiator,
)
self.acceptor_fc = FriendCode.objects.create(
- friend_code="6543-2109-8765-4321", in_game_name="AcceptUser", user=self.acceptor
+ friend_code="6543-2109-8765-4321",
+ in_game_name="AcceptUser",
+ user=self.acceptor,
)
# Create test cards with proper rarity levels
self.card1 = Card.objects.create(
- name="ComplexCard1", cardset="cx1", cardnum=1, style="default",
- rarity_icon=RARITY_MAPPING[6], rarity_level=6
+ name="ComplexCard1",
+ cardset="cx1",
+ cardnum=1,
+ style="default",
+ rarity_icon=RARITY_MAPPING[6],
+ rarity_level=6,
)
self.card2 = Card.objects.create(
- name="ComplexCard2", cardset="cx1", cardnum=2, style="default",
- rarity_icon=RARITY_MAPPING[6], rarity_level=6
+ name="ComplexCard2",
+ cardset="cx1",
+ cardnum=2,
+ style="default",
+ rarity_icon=RARITY_MAPPING[6],
+ rarity_level=6,
)
self.card3 = Card.objects.create(
- name="ComplexCard3", cardset="cx1", cardnum=3, style="default",
- rarity_icon=RARITY_MAPPING[6], rarity_level=6
+ name="ComplexCard3",
+ cardset="cx1",
+ cardnum=3,
+ style="default",
+ rarity_icon=RARITY_MAPPING[6],
+ rarity_level=6,
)
self.card4 = Card.objects.create(
- name="ComplexCard4", cardset="cx1", cardnum=4, style="default",
- rarity_icon=RARITY_MAPPING[6], rarity_level=6
+ name="ComplexCard4",
+ cardset="cx1",
+ cardnum=4,
+ style="default",
+ rarity_icon=RARITY_MAPPING[6],
+ rarity_level=6,
)
# Create a trade offer with multiple quantities
@@ -819,58 +917,67 @@ class TradeAcceptanceComplexTests(TestCase):
def test_multiple_acceptances_quantity_limit(self):
"""Test that multiple acceptances cannot exceed the offer's quantity limit."""
self.client.login(username="acceptor", password="password")
-
+
# Create first acceptance
response1 = self.client.post(
- reverse("trade_acceptance_create", kwargs={"offer_pk": self.trade_offer.pk}),
+ reverse(
+ "trade_acceptance_create", kwargs={"offer_pk": self.trade_offer.pk}
+ ),
{
"accepted_by": self.acceptor_fc.pk,
"requested_card": self.card1.pk,
"offered_card": self.card2.pk,
- }
+ },
)
self.assertEqual(response1.status_code, 302) # Successful creation
# Create second acceptance
response2 = self.client.post(
- reverse("trade_acceptance_create", kwargs={"offer_pk": self.trade_offer.pk}),
+ reverse(
+ "trade_acceptance_create", kwargs={"offer_pk": self.trade_offer.pk}
+ ),
{
"accepted_by": self.acceptor_fc.pk,
"requested_card": self.card1.pk,
"offered_card": self.card2.pk,
- }
+ },
)
self.assertEqual(response2.status_code, 302) # Successful creation
# Try to create a fourth acceptance (should fail as only 3 are allowed)
response3 = self.client.post(
- reverse("trade_acceptance_create", kwargs={"offer_pk": self.trade_offer.pk}),
+ reverse(
+ "trade_acceptance_create", kwargs={"offer_pk": self.trade_offer.pk}
+ ),
{
"accepted_by": self.acceptor_fc.pk,
"requested_card": self.card1.pk,
"offered_card": self.card2.pk,
- }
+ },
)
self.assertEqual(response3.status_code, 302) # Successful creation
response4 = self.client.post(
- reverse("trade_acceptance_create", kwargs={"offer_pk": self.trade_offer.pk}),
+ reverse(
+ "trade_acceptance_create", kwargs={"offer_pk": self.trade_offer.pk}
+ ),
{
"accepted_by": self.acceptor_fc.pk,
"requested_card": self.card1.pk,
"offered_card": self.card2.pk,
- }
+ },
)
self.assertEqual(response4.status_code, 200) # Should fail
self.assertEqual(
- self.trade_offer.acceptances.count(), 3,
- "Should not allow more acceptances than the quantity limit"
+ self.trade_offer.acceptances.count(),
+ 3,
+ "Should not allow more acceptances than the quantity limit",
)
def test_complex_state_transitions(self):
"""Test complex state transition scenarios."""
self.client.login(username="acceptor", password="password")
-
+
# Create an acceptance
acceptance = TradeAcceptance.objects.create(
trade_offer=self.trade_offer,
@@ -890,14 +997,14 @@ class TradeAcceptanceComplexTests(TestCase):
for invalid_state in invalid_transitions:
response = self.client.post(
reverse("trade_acceptance_update", kwargs={"pk": acceptance.pk}),
- {"state": invalid_state}
+ {"state": invalid_state},
)
self.assertEqual(response.status_code, 200) # Should stay on form
acceptance.refresh_from_db()
self.assertEqual(
acceptance.state,
TradeAcceptance.AcceptanceState.ACCEPTED,
- f"Invalid transition to {invalid_state} should not be allowed"
+ f"Invalid transition to {invalid_state} should not be allowed",
)
# Test valid state transition sequence
@@ -912,14 +1019,12 @@ class TradeAcceptanceComplexTests(TestCase):
self.client.login(username=user.username, password="password")
response = self.client.post(
reverse("trade_acceptance_update", kwargs={"pk": acceptance.pk}),
- {"state": state}
+ {"state": state},
)
self.assertEqual(response.status_code, 302) # Should redirect on success
acceptance.refresh_from_db()
self.assertEqual(
acceptance.state,
state,
- f"Valid transition to {state} should be allowed"
+ f"Valid transition to {state} should be allowed",
)
-
-
diff --git a/src/pkmntrade_club/trades/urls.py b/src/pkmntrade_club/trades/urls.py
index ce4cf7a..4b63a25 100644
--- a/src/pkmntrade_club/trades/urls.py
+++ b/src/pkmntrade_club/trades/urls.py
@@ -13,12 +13,24 @@ 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(
+ "create/confirm/",
+ TradeOfferCreateConfirmView.as_view(),
+ name="trade_offer_confirm_create",
+ ),
path("", TradeOfferAllListView.as_view(), name="trade_offer_list"),
path("search/", TradeOfferSearchView.as_view(), name="trade_offer_search"),
path("/", TradeOfferDetailView.as_view(), name="trade_offer_detail"),
path(".png", TradeOfferPNGView.as_view(), name="trade_offer_png"),
path("delete//", TradeOfferDeleteView.as_view(), name="trade_offer_delete"),
- path("accept/", TradeAcceptanceCreateView.as_view(), name="trade_acceptance_create"),
- path("update//", TradeAcceptanceUpdateView.as_view(), name="trade_acceptance_update"),
+ path(
+ "accept/",
+ TradeAcceptanceCreateView.as_view(),
+ name="trade_acceptance_create",
+ ),
+ path(
+ "update//",
+ TradeAcceptanceUpdateView.as_view(),
+ name="trade_acceptance_update",
+ ),
]
diff --git a/src/pkmntrade_club/trades/views.py b/src/pkmntrade_club/trades/views.py
index 8a25d45..5eb5c93 100644
--- a/src/pkmntrade_club/trades/views.py
+++ b/src/pkmntrade_club/trades/views.py
@@ -1,25 +1,35 @@
-from django.template import RequestContext
-from django.views.generic import DeleteView, CreateView, ListView, DetailView, UpdateView
-from django.views import View
-from django.urls import reverse_lazy
-from django.http import HttpResponseRedirect
-from django.contrib.auth.mixins import LoginRequiredMixin
-from django.shortcuts import render
-from django.core.exceptions import PermissionDenied, ValidationError
-from django.core.paginator import Paginator
from django.contrib import messages
-from meta.views import Meta
-from .models import TradeOffer, TradeAcceptance
-from .forms import (TradeAcceptanceCreateForm, TradeOfferCreateForm, TradeAcceptanceTransitionForm)
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.core.exceptions import PermissionDenied, ValidationError
+from django.http import HttpResponseRedirect
+from django.shortcuts import render
from django.template.loader import render_to_string
-from pkmntrade_club.trades.templatetags.trade_offer_tags import render_trade_offer_png
+from django.urls import reverse_lazy
+from django.views import View
+from django.views.generic import (
+ CreateView,
+ DeleteView,
+ DetailView,
+ ListView,
+ UpdateView,
+)
+from meta.views import Meta
from playwright.sync_api import sync_playwright
-from django.conf import settings
-from .mixins import FriendCodeRequiredMixin
+
from pkmntrade_club.common.mixins import ReusablePaginationMixin
+from pkmntrade_club.trades.templatetags.trade_offer_tags import render_trade_offer_png
+
+from .forms import (
+ TradeAcceptanceCreateForm,
+ TradeAcceptanceTransitionForm,
+ TradeOfferCreateForm,
+)
+from .mixins import FriendCodeRequiredMixin
+from .models import TradeAcceptance, TradeOffer
+
class TradeOfferCreateView(LoginRequiredMixin, CreateView):
- http_method_names = ['get'] # restricts this view to GET only
+ http_method_names = ["get"] # restricts this view to GET only
model = TradeOffer
form_class = TradeOfferCreateForm
template_name = "trades/trade_offer_create.html"
@@ -42,20 +52,30 @@ class TradeOfferCreateView(LoginRequiredMixin, CreateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
from pkmntrade_club.cards.models import Card
+
# Ensure available_cards is a proper QuerySet
- context["cards"] = Card.objects.filter(rarity_level__lte=5).order_by("name", "rarity_level")
+ context["cards"] = Card.objects.filter(rarity_level__lte=5).order_by(
+ "name", "rarity_level"
+ )
friend_codes = self.request.user.friend_codes.all()
if "initiated_by" in self.request.GET:
try:
- selected_friend_code = friend_codes.get(pk=self.request.GET.get("initiated_by"))
+ selected_friend_code = friend_codes.get(
+ pk=self.request.GET.get("initiated_by")
+ )
except friend_codes.model.DoesNotExist:
- selected_friend_code = self.request.user.default_friend_code or friend_codes.first()
+ 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()
+ selected_friend_code = (
+ self.request.user.default_friend_code or friend_codes.first()
+ )
context["friend_codes"] = friend_codes
context["selected_friend_code"] = selected_friend_code
return context
+
class TradeOfferAllListView(ReusablePaginationMixin, ListView):
model = TradeOffer
template_name = "trades/trade_offer_all_list.html"
@@ -93,14 +113,21 @@ class TradeOfferAllListView(ReusablePaginationMixin, ListView):
page_number = self.get_page_number()
self.per_page = 10
- paginated_offers, pagination_context = self.paginate_data(queryset, page_number)
+ paginated_offers, pagination_context = self.paginate_data(
+ queryset, page_number
+ )
return render(
self.request,
"trades/_trade_offer_list.html",
- {"offers": paginated_offers, "page_obj": pagination_context, "expanded": expanded}
+ {
+ "offers": paginated_offers,
+ "page_obj": pagination_context,
+ "expanded": expanded,
+ },
)
return super().render_to_response(context, **response_kwargs)
+
class TradeOfferDeleteView(LoginRequiredMixin, FriendCodeRequiredMixin, DeleteView):
model = TradeOffer
success_url = reverse_lazy("trade_offer_list")
@@ -108,8 +135,12 @@ class TradeOfferDeleteView(LoginRequiredMixin, FriendCodeRequiredMixin, DeleteVi
def dispatch(self, request, *args, **kwargs):
self.object = super().get_object()
- if self.object.initiated_by_id not in request.user.friend_codes.values_list("id", flat=True):
- raise PermissionDenied("You are not authorized to delete or close this trade offer.")
+ if self.object.initiated_by_id not in request.user.friend_codes.values_list(
+ "id", flat=True
+ ):
+ raise PermissionDenied(
+ "You are not authorized to delete or close this trade offer."
+ )
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
@@ -143,8 +174,8 @@ class TradeOfferDeleteView(LoginRequiredMixin, FriendCodeRequiredMixin, DeleteVi
active_acceptances = trade_offer.acceptances.exclude(state__in=terminal_states)
if active_acceptances.exists():
messages.error(
- request,
- "Cannot close this trade offer while there are active acceptances. Please reject all acceptances before closing, or finish the trades."
+ request,
+ "Cannot close this trade offer while there are active acceptances. Please reject all acceptances before closing, or finish the trades.",
)
context = self.get_context_data()
return self.render_to_response(context)
@@ -158,6 +189,7 @@ class TradeOfferDeleteView(LoginRequiredMixin, FriendCodeRequiredMixin, DeleteVi
messages.success(request, "Trade offer has been deleted.")
return super().delete(request, *args, **kwargs)
+
class TradeOfferSearchView(ListView):
"""
Reworked trade offer search view using POST.
@@ -171,6 +203,7 @@ class TradeOfferSearchView(ListView):
(_search_results.html) is rendered. On GET (initial page load), the search results queryset
is empty.
"""
+
model = TradeOffer
context_object_name = "search_results"
template_name = "trades/trade_offer_search.html"
@@ -198,7 +231,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):
# For a GET request (initial load), return an empty queryset.
if self.request.method == "GET":
@@ -215,7 +248,7 @@ class TradeOfferSearchView(ListView):
qs = TradeOffer.objects.filter(
is_closed=False,
)
-
+
if self.request.user.is_authenticated:
qs = qs.exclude(initiated_by__in=self.request.user.friend_codes.all())
@@ -237,17 +270,20 @@ 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 pkmntrade_club.cards.models import Card
+
# Populate available_cards to re-populate the multiselects. Exclude cards with rarity level > 5.
- context["cards"] = Card.objects.filter(rarity_level__lte=5).order_by("name", "rarity_level")
+ context["cards"] = Card.objects.filter(rarity_level__lte=5).order_by(
+ "name", "rarity_level"
+ )
if self.request.method == "POST":
context["have_cards"] = self.request.POST.getlist("have_cards")
context["want_cards"] = self.request.POST.getlist("want_cards")
@@ -256,35 +292,40 @@ class TradeOfferSearchView(ListView):
context["want_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.
"""
if self.request.headers.get("X-Requested-With") == "XMLHttpRequest":
from django.shortcuts import render
+
return render(self.request, "trades/_search_results.html", context)
else:
return super().render_to_response(context, **response_kwargs)
+
class TradeOfferDetailView(DetailView):
"""
Displays the details of a TradeOffer along with its active acceptances.
If the offer is still open and the current user is not its initiator,
an acceptance form is provided to create a new acceptance.
"""
+
model = TradeOffer
template_name = "trades/trade_offer_detail.html"
- #@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()
screenshot_mode = self.request.GET.get("screenshot_mode")
if screenshot_mode:
- context["show_friend_code"] = trade_offer.initiated_by.user.show_friend_code_on_link_previews
+ context["show_friend_code"] = (
+ trade_offer.initiated_by.user.show_friend_code_on_link_previews
+ )
context["screenshot_mode"] = screenshot_mode
-
+
# Calculate the number of cards in each category.
num_has = trade_offer.trade_offer_have_cards.count()
num_wants = trade_offer.trade_offer_want_cards.count()
@@ -315,14 +356,14 @@ class TradeOfferDetailView(DetailView):
# compute the height from the width.
image_width = base_width
image_height = int(round(image_width / aspect_ratio))
-
+
# Build the meta tags with the computed dimensions.
- title = f'Trade Offer from {trade_offer.initiated_by.in_game_name} ({trade_offer.initiated_by.friend_code})'
+ title = f"Trade Offer from {trade_offer.initiated_by.in_game_name} ({trade_offer.initiated_by.friend_code})"
context["meta"] = Meta(
title=title,
- description=f'Has: {", ".join([card.card.name for card in trade_offer.trade_offer_have_cards.all()])} • \nWants: {", ".join([card.card.name for card in trade_offer.trade_offer_want_cards.all()])}',
+ description=f"Has: {', '.join([card.card.name for card in trade_offer.trade_offer_have_cards.all()])} • \nWants: {', '.join([card.card.name for card in trade_offer.trade_offer_want_cards.all()])}",
image_object={
- "url": f'http://localhost:8000{reverse_lazy("trade_offer_png", kwargs={"pk": trade_offer.pk})}',
+ "url": f"http://localhost:8000{reverse_lazy('trade_offer_png', kwargs={'pk': trade_offer.pk})}",
"type": "image/png",
"width": image_width,
"height": image_height,
@@ -333,7 +374,7 @@ class TradeOfferDetailView(DetailView):
use_facebook=True,
use_schemaorg=True,
)
-
+
# Define terminal (closed) acceptance states based on our new system:
terminal_states = [
TradeAcceptance.AcceptanceState.THANKED_BY_INITIATOR,
@@ -342,32 +383,41 @@ class TradeOfferDetailView(DetailView):
TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR,
TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR,
]
-
+
# For example, if you want to separate active from terminal acceptances:
context["acceptances"] = trade_offer.acceptances.all()
-
+
# Option 1: Filter active acceptances using the queryset lookup.
- context["active_acceptances"] = trade_offer.acceptances.exclude(state__in=terminal_states)
-
+ context["active_acceptances"] = trade_offer.acceptances.exclude(
+ state__in=terminal_states
+ )
+
if self.request.user.is_authenticated:
user_friend_codes = self.request.user.friend_codes.all()
# Add context flag and deletion URL if the current user is the initiator
if trade_offer.initiated_by in user_friend_codes:
context["is_initiator"] = True
- context["delete_close_url"] = reverse_lazy("trade_offer_delete", kwargs={"pk": trade_offer.pk})
+ context["delete_close_url"] = reverse_lazy(
+ "trade_offer_delete", kwargs={"pk": trade_offer.pk}
+ )
else:
context["is_initiator"] = False
# Determine the user's default friend code (or fallback as needed).
- default_friend_code = self.request.user.default_friend_code or user_friend_codes.first()
-
+ default_friend_code = (
+ self.request.user.default_friend_code or user_friend_codes.first()
+ )
+
# If the current user is not the initiator and the offer is open, allow a new acceptance.
- if trade_offer.initiated_by not in user_friend_codes and not trade_offer.is_closed:
+ if (
+ trade_offer.initiated_by not in user_friend_codes
+ and not trade_offer.is_closed
+ ):
context["acceptance_form"] = TradeAcceptanceCreateForm(
trade_offer=trade_offer,
friend_codes=user_friend_codes,
- default_friend_code=default_friend_code
+ default_friend_code=default_friend_code,
)
else:
context["is_initiator"] = False
@@ -376,11 +426,15 @@ class TradeOfferDetailView(DetailView):
return context
-class TradeAcceptanceCreateView(LoginRequiredMixin, FriendCodeRequiredMixin, CreateView):
+
+class TradeAcceptanceCreateView(
+ LoginRequiredMixin, FriendCodeRequiredMixin, CreateView
+):
"""
View to create a new TradeAcceptance.
The URL should provide 'offer_pk' so that the proper TradeOffer can be identified.
"""
+
model = TradeAcceptance
form_class = TradeAcceptanceCreateForm
template_name = "trades/trade_acceptance_create.html"
@@ -390,16 +444,18 @@ class TradeAcceptanceCreateView(LoginRequiredMixin, FriendCodeRequiredMixin, Cre
return super().dispatch(request, *args, **kwargs)
def get_trade_offer(self):
- return TradeOffer.objects.get(pk=self.kwargs['offer_pk'])
+ return TradeOffer.objects.get(pk=self.kwargs["offer_pk"])
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
- if (self.trade_offer.initiated_by_id in
- self.request.user.friend_codes.values_list("id", flat=True) or
- self.trade_offer.is_closed):
+ if (
+ self.trade_offer.initiated_by_id
+ in self.request.user.friend_codes.values_list("id", flat=True)
+ or self.trade_offer.is_closed
+ ):
raise PermissionDenied("You cannot accept this trade offer.")
- kwargs['trade_offer'] = self.trade_offer
- kwargs['friend_codes'] = self.request.user.friend_codes.all()
+ kwargs["trade_offer"] = self.trade_offer
+ kwargs["friend_codes"] = self.request.user.friend_codes.all()
return kwargs
def get_context_data(self, **kwargs):
@@ -430,7 +486,13 @@ class TradeAcceptanceCreateView(LoginRequiredMixin, FriendCodeRequiredMixin, Cre
"acceptance_form": form,
"friend_codes": friend_codes,
"is_initiator": is_initiator,
- "delete_close_url": reverse_lazy("trade_offer_delete", kwargs={"pk": self.trade_offer.pk}) if is_initiator else None,
+ "delete_close_url": (
+ reverse_lazy(
+ "trade_offer_delete", kwargs={"pk": self.trade_offer.pk}
+ )
+ if is_initiator
+ else None
+ ),
}
# Render the detail page with the form errors
return render(self.request, "trades/trade_offer_detail.html", context)
@@ -439,11 +501,15 @@ class TradeAcceptanceCreateView(LoginRequiredMixin, FriendCodeRequiredMixin, Cre
def get_success_url(self):
return reverse_lazy("trade_acceptance_update", kwargs={"pk": self.object.pk})
-class TradeAcceptanceUpdateView(LoginRequiredMixin, FriendCodeRequiredMixin, UpdateView):
+
+class TradeAcceptanceUpdateView(
+ LoginRequiredMixin, FriendCodeRequiredMixin, UpdateView
+):
"""
View to update the state of an existing TradeAcceptance.
The allowed state transitions are provided via the form.
"""
+
model = TradeAcceptance
form_class = TradeAcceptanceTransitionForm
template_name = "trades/trade_acceptance_update.html"
@@ -451,8 +517,10 @@ class TradeAcceptanceUpdateView(LoginRequiredMixin, FriendCodeRequiredMixin, Upd
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
friend_codes = request.user.friend_codes.values_list("id", flat=True)
- if (self.object.accepted_by_id not in friend_codes and
- self.object.trade_offer.initiated_by_id not in friend_codes):
+ if (
+ self.object.accepted_by_id not in friend_codes
+ and self.object.trade_offer.initiated_by_id not in friend_codes
+ ):
raise PermissionDenied("You are not authorized to update this acceptance.")
return super().dispatch(request, *args, **kwargs)
@@ -481,6 +549,7 @@ class TradeAcceptanceUpdateView(LoginRequiredMixin, FriendCodeRequiredMixin, Upd
def get_success_url(self):
return reverse_lazy("trade_acceptance_update", kwargs={"pk": self.object.pk})
+
class TradeOfferPNGView(View):
"""
Generate a PNG screenshot of the rendered trade offer detail page using Playwright.
@@ -488,15 +557,17 @@ class TradeOfferPNGView(View):
runs at a time for a given TradeOffer. The generated PNG is then cached in the
TradeOffer model's `image` field (assumed to be an ImageField).
"""
+
def get_lock_key(self, trade_offer_id):
# Use the trade_offer_id as the lock key; adjust if needed.
return trade_offer_id
def get(self, request, *args, **kwargs):
- from django.shortcuts import get_object_or_404
- from django.http import HttpResponse
from django.core.files.base import ContentFile
- trade_offer = get_object_or_404(TradeOffer, pk=kwargs['pk'])
+ from django.http import HttpResponse
+ from django.shortcuts import get_object_or_404
+
+ trade_offer = get_object_or_404(TradeOffer, pk=kwargs["pk"])
# If the image is already generated and stored, serve it directly.
if trade_offer.image and not request.GET.get("debug"):
@@ -505,6 +576,7 @@ class TradeOfferPNGView(View):
# Acquire PostgreSQL advisory lock to prevent concurrent generation.
from django.db import connection
+
lock_key = self.get_lock_key(trade_offer.pk)
with connection.cursor() as cursor:
cursor.execute("SELECT pg_advisory_lock(%s)", [lock_key])
@@ -514,18 +586,22 @@ class TradeOfferPNGView(View):
if trade_offer.image and not request.GET.get("debug"):
trade_offer.image.open()
return HttpResponse(trade_offer.image.read(), content_type="image/png")
-
+
tag_context = render_trade_offer_png(
- {'request': request}, trade_offer, show_friend_code=trade_offer.initiated_by.user.show_friend_code_on_link_previews
+ {"request": request},
+ trade_offer,
+ show_friend_code=trade_offer.initiated_by.user.show_friend_code_on_link_previews,
)
- image_width = tag_context.get('image_width')
- image_height = tag_context.get('image_height')
+ image_width = tag_context.get("image_width")
+ image_height = tag_context.get("image_height")
if not image_width or not image_height:
- raise ValueError("Could not determine image dimensions from tag_context")
+ raise ValueError(
+ "Could not determine image dimensions from tag_context"
+ )
html = render_to_string(
"templatetags/trade_offer_png.html",
context=tag_context,
- request=request
+ request=request,
)
# if query string has "debug", render the HTML instead of the PNG
@@ -545,13 +621,20 @@ class TradeOfferPNGView(View):
"--disable-audio-output",
"--disable-webgl",
"--no-first-run",
- ]
+ ],
+ )
+ context_browser = browser.new_context(
+ viewport={"width": image_width, "height": image_height}
)
- context_browser = browser.new_context(viewport={"width": image_width, "height": image_height})
page = context_browser.new_page()
page.on("console", lambda msg: print(f"Console {msg.type}: {msg.text}"))
page.on("pageerror", lambda err: print(f"Page error: {err}"))
- page.on("requestfailed", lambda req: print(f"Failed to load: {req.url} - {req.failure.error_text}"))
+ page.on(
+ "requestfailed",
+ lambda req: print(
+ f"Failed to load: {req.url} - {req.failure.error_text}"
+ ),
+ )
page.set_content(html, wait_until="networkidle")
element = page.wait_for_selector(".trade-offer-card-screenshot")
screenshot_bytes = element.screenshot(type="png", omit_background=True)
@@ -567,11 +650,13 @@ class TradeOfferPNGView(View):
with connection.cursor() as cursor:
cursor.execute("SELECT pg_advisory_unlock(%s)", [lock_key])
+
class TradeOfferCreateConfirmView(LoginRequiredMixin, View):
"""
Processes a two-step create for TradeOffer; on confirmation,
commits the offer and shows form errors if any occur.
"""
+
def post(self, request, *args, **kwargs):
if "confirm" in request.POST:
return self._commit_offer(request)
@@ -605,17 +690,21 @@ class TradeOfferCreateConfirmView(LoginRequiredMixin, View):
}
# Supply additional context required by trade_offer_create.html.
from pkmntrade_club.cards.models import Card
+
context = {
"form": form,
"friend_codes": request.user.friend_codes.all(),
"selected_friend_code": (
- request.user.default_friend_code or request.user.friend_codes.first()
+ request.user.default_friend_code
+ or request.user.friend_codes.first()
),
"cards": Card.objects.all().order_by("name", "rarity_level"),
}
return render(request, "trades/trade_offer_create.html", context)
messages.success(request, "Trade offer created successfully!")
- return HttpResponseRedirect(reverse_lazy("trade_offer_detail", kwargs={"pk": trade_offer.pk}))
+ return HttpResponseRedirect(
+ reverse_lazy("trade_offer_detail", kwargs={"pk": trade_offer.pk})
+ )
else:
# When the form is not valid, update its initial data as well:
form.initial = {
@@ -624,16 +713,18 @@ class TradeOfferCreateConfirmView(LoginRequiredMixin, View):
"initiated_by": request.POST.get("initiated_by"),
}
from pkmntrade_club.cards.models import Card
+
context = {
"form": form,
"friend_codes": request.user.friend_codes.all(),
"selected_friend_code": (
- request.user.default_friend_code or request.user.friend_codes.first()
+ request.user.default_friend_code
+ or request.user.friend_codes.first()
),
"cards": Card.objects.all().order_by("name", "rarity_level"),
}
return render(request, "trades/trade_offer_create.html", context)
-
+
def _redirect_to_edit(self, request):
query_params = request.POST.copy()
query_params.pop("csrfmiddlewaretoken", None)
@@ -641,10 +732,11 @@ class TradeOfferCreateConfirmView(LoginRequiredMixin, View):
query_params.pop("confirm", None)
query_params.pop("preview", None)
from django.urls import reverse
+
base_url = reverse("trade_offer_create")
url_with_params = f"{base_url}?{query_params.urlencode()}"
return HttpResponseRedirect(url_with_params)
-
+
def _preview_offer(self, request):
form = TradeOfferCreateForm(request.POST)
form.fields["initiated_by"].queryset = request.user.friend_codes.all()
@@ -656,15 +748,16 @@ class TradeOfferCreateConfirmView(LoginRequiredMixin, View):
"initiated_by": request.POST.get("initiated_by"),
}
from pkmntrade_club.cards.models import Card
+
context = {
"form": form,
"friend_codes": request.user.friend_codes.all(),
- "selected_friend_code": request.user.default_friend_code or request.user.friend_codes.first(),
+ "selected_friend_code": request.user.default_friend_code
+ or request.user.friend_codes.first(),
"cards": Card.objects.all().order_by("name", "rarity_level"),
}
return render(request, "trades/trade_offer_create.html", context)
-
# Parse the card selections for "have" and "want" cards.
have_selections = self._parse_card_selections("have_cards")
want_selections = self._parse_card_selections("want_cards")