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