inital checkin
This commit is contained in:
0
users/__init__.py
Normal file
0
users/__init__.py
Normal file
3
users/admin.py
Normal file
3
users/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
5
users/apps.py
Normal file
5
users/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UsersConfig(AppConfig):
|
||||
name = 'users'
|
||||
47
users/forms.py
Normal file
47
users/forms.py
Normal 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'}),
|
||||
}
|
||||
57
users/migrations/0001_initial.py
Normal file
57
users/migrations/0001_initial.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
41
users/migrations/0002_address_paymentmethod.py
Normal file
41
users/migrations/0002_address_paymentmethod.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
users/migrations/__init__.py
Normal file
0
users/migrations/__init__.py
Normal file
62
users/models.py
Normal file
62
users/models.py
Normal 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
30
users/tests.py
Normal 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
15
users/urls.py
Normal 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
139
users/views.py
Normal 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
|
||||
})
|
||||
Reference in New Issue
Block a user