inital checkin

This commit is contained in:
2026-01-20 05:22:38 -06:00
parent 9784e14c77
commit c43603bfb5
75 changed files with 4327 additions and 0 deletions

0
users/__init__.py Normal file
View File

3
users/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
users/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
name = 'users'

47
users/forms.py Normal file
View File

@@ -0,0 +1,47 @@
from django import forms
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import User, Profile, Address, PaymentMethod
class CustomUserCreationForm(UserCreationForm):
class Meta:
model = User
fields = ('username', 'email')
class CustomUserChangeForm(UserChangeForm):
class Meta:
model = User
fields = ('username', 'email')
class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
fields = ('theme_preference',)
widgets = {
'theme_preference': forms.Select(attrs={'class': 'form-control'})
}
class AddressForm(forms.ModelForm):
class Meta:
model = Address
fields = ('name', 'street', 'city', 'state', 'zip_code', 'address_type', 'is_default')
widgets = {
'name': forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'Full Name'}),
'street': forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'Street Address'}),
'city': forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'City'}),
'state': forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'State'}),
'zip_code': forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'ZIP Code'}),
'address_type': forms.Select(attrs={'class': 'form-select'}),
'is_default': forms.CheckboxInput(attrs={'class': 'form-checkbox'}),
}
class PaymentMethodForm(forms.ModelForm):
class Meta:
model = PaymentMethod
fields = ('brand', 'last4', 'exp_month', 'exp_year', 'is_default')
widgets = {
'brand': forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'Card Brand (e.g. Visa)'}),
'last4': forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'Last 4 Digits', 'maxlength': '4'}),
'exp_month': forms.NumberInput(attrs={'class': 'form-input', 'placeholder': 'Exp Month', 'min': '1', 'max': '12'}),
'exp_year': forms.NumberInput(attrs={'class': 'form-input', 'placeholder': 'Exp Year', 'min': '2024'}),
'is_default': forms.CheckboxInput(attrs={'class': 'form-checkbox'}),
}

View File

@@ -0,0 +1,57 @@
# Generated by Django 6.0.1 on 2026-01-19 13:38
import django.contrib.auth.models
import django.contrib.auth.validators
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='Profile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_pro', models.BooleanField(default=False)),
('theme_preference', models.CharField(choices=[('light', 'Light'), ('dark', 'Dark')], default='dark', max_length=10)),
('shipping_address', models.TextField(blank=True)),
('stripe_customer_id', models.CharField(blank=True, max_length=100, null=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -0,0 +1,41 @@
# Generated by Django 6.0.1 on 2026-01-20 11:08
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Address',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('street', models.CharField(max_length=200)),
('city', models.CharField(max_length=100)),
('state', models.CharField(max_length=100)),
('zip_code', models.CharField(max_length=20)),
('address_type', models.CharField(choices=[('shipping', 'Shipping'), ('billing', 'Billing')], default='shipping', max_length=20)),
('is_default', models.BooleanField(default=False)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='PaymentMethod',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('brand', models.CharField(max_length=50)),
('last4', models.CharField(max_length=4)),
('exp_month', models.PositiveIntegerField()),
('exp_year', models.PositiveIntegerField()),
('is_default', models.BooleanField(default=False)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payment_methods', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

62
users/models.py Normal file
View File

@@ -0,0 +1,62 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
class User(AbstractUser):
pass
class Profile(models.Model):
THEME_CHOICES = (
('light', 'Light'),
('dark', 'Dark'),
)
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
is_pro = models.BooleanField(default=False)
theme_preference = models.CharField(max_length=10, choices=THEME_CHOICES, default='dark')
# Keeping this simple text field for legacy/simple addressing, whilst adding robust Address model below
shipping_address = models.TextField(blank=True)
stripe_customer_id = models.CharField(max_length=100, blank=True, null=True)
def __str__(self):
return self.user.username
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
instance.profile.save()
class Address(models.Model):
ADDRESS_TYPE_CHOICES = (
('shipping', 'Shipping'),
('billing', 'Billing'),
)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='addresses')
name = models.CharField(max_length=100)
street = models.CharField(max_length=200)
city = models.CharField(max_length=100)
state = models.CharField(max_length=100)
zip_code = models.CharField(max_length=20)
address_type = models.CharField(max_length=20, choices=ADDRESS_TYPE_CHOICES, default='shipping')
is_default = models.BooleanField(default=False)
def __str__(self):
return f"{self.name} - {self.street}"
class PaymentMethod(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='payment_methods')
# For now, we'll store mock data. In a real app complexity is higher (PCI compliance etc)
brand = models.CharField(max_length=50) # Visa, Mastercard
last4 = models.CharField(max_length=4)
exp_month = models.PositiveIntegerField()
exp_year = models.PositiveIntegerField()
is_default = models.BooleanField(default=False)
def __str__(self):
return f"{self.brand} ending in {self.last4}"

30
users/tests.py Normal file
View File

@@ -0,0 +1,30 @@
from django.test import TestCase
from django.urls import reverse
from .models import User
class ProfileThemeTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(username='testuser', password='password')
self.client.login(username='testuser', password='password')
def test_theme_preference_update(self):
# Default should be dark
self.assertEqual(self.user.profile.theme_preference, 'dark')
# Check if form is in context
response = self.client.get(reverse('users:profile'))
self.assertEqual(response.status_code, 200)
self.assertIn('form', response.context)
# Update theme to light
response = self.client.post(reverse('users:profile'), {
'theme_preference': 'light'
})
# Should redirect back to profile
self.assertRedirects(response, reverse('users:profile'))
# Check if updated in DB
self.user.profile.refresh_from_db()
self.assertEqual(self.user.profile.theme_preference, 'light')

15
users/urls.py Normal file
View File

@@ -0,0 +1,15 @@
from django.urls import path
from . import views
app_name = 'users'
urlpatterns = [
path('register/', views.RegisterView.as_view(), name='register'),
path('profile/', views.profile_view, name='profile'),
path('vault/', views.vault_view, name='vault'),
path('upgrade/', views.upgrade_account_view, name='upgrade_account'),
path('profile/address/add/', views.add_address_view, name='add_address'),
path('profile/payment/add/', views.add_payment_method_view, name='add_payment_method'),
path('profile/address/delete/<int:pk>/', views.delete_address_view, name='delete_address'),
path('profile/payment/delete/<int:pk>/', views.delete_payment_method_view, name='delete_payment_method'),
]

139
users/views.py Normal file
View File

@@ -0,0 +1,139 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth import login
from .forms import CustomUserCreationForm, ProfileForm, AddressForm, PaymentMethodForm
from django.views.generic import CreateView
from django.urls import reverse_lazy
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from .models import User, Address, PaymentMethod
from store.models import VaultItem, Set, Card
class RegisterView(CreateView):
model = User
form_class = CustomUserCreationForm
template_name = 'users/register.html'
success_url = reverse_lazy('home')
def form_valid(self, form):
user = form.save()
login(self.request, user)
return redirect('home')
@login_required
def profile_view(request):
if request.method == 'POST':
form = ProfileForm(request.POST, instance=request.user.profile)
if form.is_valid():
form.save()
messages.success(request, 'Profile updated successfully.')
return redirect('users:profile')
else:
form = ProfileForm(instance=request.user.profile)
# Order filtering
orders = request.user.orders.all().order_by('-created_at')
date_query = request.GET.get('date')
if date_query:
orders = orders.filter(created_at__date=date_query)
addresses = request.user.addresses.all()
payment_methods = request.user.payment_methods.all()
return render(request, 'users/profile.html', {
'user': request.user,
'form': form,
'orders': orders,
'date_query': date_query,
'addresses': addresses,
'payment_methods': payment_methods,
})
@login_required
def upgrade_account_view(request):
if request.method == 'POST':
# Mock payment processing would go here
profile = request.user.profile
profile.is_pro = True
profile.save()
messages.success(request, 'Account upgraded to Pro!')
return redirect('users:profile')
return redirect('users:profile')
@login_required
def add_address_view(request):
if request.method == 'POST':
form = AddressForm(request.POST)
if form.is_valid():
address = form.save(commit=False)
address.user = request.user
if address.is_default:
# Unset previous default
Address.objects.filter(user=request.user, address_type=address.address_type, is_default=True).update(is_default=False)
address.save()
messages.success(request, 'Address added successfully.')
return redirect('users:profile')
else:
form = AddressForm()
return render(request, 'users/address_form.html', {'form': form})
@login_required
def delete_address_view(request, pk):
address = get_object_or_404(Address, pk=pk, user=request.user)
if request.method == 'POST':
address.delete()
messages.success(request, 'Address deleted.')
return redirect('users:profile')
@login_required
def add_payment_method_view(request):
if request.method == 'POST':
form = PaymentMethodForm(request.POST)
if form.is_valid():
pm = form.save(commit=False)
pm.user = request.user
if pm.is_default:
PaymentMethod.objects.filter(user=request.user, is_default=True).update(is_default=False)
pm.save()
messages.success(request, 'Payment method added successfully.')
return redirect('users:profile')
else:
form = PaymentMethodForm()
return render(request, 'users/payment_form.html', {'form': form})
@login_required
def delete_payment_method_view(request, pk):
pm = get_object_or_404(PaymentMethod, pk=pk, user=request.user)
if request.method == 'POST':
pm.delete()
messages.success(request, 'Payment method deleted.')
return redirect('users:profile')
@login_required
def vault_view(request):
vault_items = VaultItem.objects.filter(user=request.user).select_related('card', 'card__set').order_by('-added_at')
# Filtering
set_id = request.GET.get('set')
rarity = request.GET.get('rarity')
if set_id:
vault_items = vault_items.filter(card__set_id=set_id)
if rarity:
vault_items = vault_items.filter(card__rarity=rarity)
total_cards = sum(item.quantity for item in vault_items)
# Get options for filters
# We only want sets and rarities that are actually in the user's vault
user_card_ids = VaultItem.objects.filter(user=request.user).values_list('card_id', flat=True)
available_sets = Set.objects.filter(cards__id__in=user_card_ids).distinct()
available_rarities = Card.objects.filter(id__in=user_card_ids).values_list('rarity', flat=True).distinct()
return render(request, 'users/vault.html', {
'vault_items': vault_items,
'total_cards': total_cards,
'available_sets': available_sets,
'available_rarities': available_rarities,
'current_set': int(set_id) if set_id else None,
'current_rarity': rarity
})