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
store/__init__.py Normal file
View File

3
store/admin.py Normal file
View File

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

5
store/apps.py Normal file
View File

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

View File

View File

View File

@@ -0,0 +1,58 @@
from django.core.management.base import BaseCommand
from store.models import Game, Set, Card, PackListing, VirtualPack
import random
class Command(BaseCommand):
help = 'Creates test packs for MTG, Lorcana, and Pokemon'
def handle(self, *args, **kwargs):
games_data = [
{'name': 'Magic: The Gathering', 'slug': 'mtg', 'pack_price': 4.99},
{'name': 'Disney Lorcana', 'slug': 'lorcana', 'pack_price': 5.99},
{'name': 'Pokémon', 'slug': 'pokemon', 'pack_price': 3.99},
]
for game_info in games_data:
# Lookup by slug since it's unique
game, _ = Game.objects.get_or_create(
slug=game_info['slug'],
defaults={'name': game_info['name']}
)
# Ensure at least one set exists
set_obj, _ = Set.objects.get_or_create(
game=game,
name=f"{game.name} Base Set",
defaults={'code': 'BASE'}
)
# Ensure cards exist
if not Card.objects.filter(set__game=game).exists():
self.stdout.write(f"Creating dummy cards for {game.name}...")
for i in range(20):
Card.objects.create(
set=set_obj,
name=f"{game.name} Card {i+1}",
rarity='Common',
collector_number=str(i+1)
)
# Create Pack Listing
listing, _ = PackListing.objects.get_or_create(
game=game,
name=f"{game.name} Booster Pack",
defaults={'price': game_info['pack_price']}
)
self.stdout.write(f"Generating packs for {listing.name}...")
all_game_cards = list(Card.objects.filter(set__game=game))
# Create 10 packs
for _ in range(10):
pack = VirtualPack.objects.create(listing=listing)
# Add 5 random cards
pack_cards = random.sample(all_game_cards, min(len(all_game_cards), 5))
pack.cards.set(pack_cards)
self.stdout.write(self.style.SUCCESS('Successfully created test packs'))

View File

@@ -0,0 +1,139 @@
import random
import requests
from django.core.management.base import BaseCommand
from django.utils.text import slugify
from django.contrib.auth import get_user_model
from store.models import Game, Set, Card, CardListing
from faker import Faker
User = get_user_model()
fake = Faker()
class Command(BaseCommand):
help = 'Populate database with MTG data from Scryfall and fake data for other games'
def handle(self, *args, **options):
self.stdout.write('Starting database population...')
# Create Games
mtg, _ = Game.objects.get_or_create(name='Magic: The Gathering', slug='mtg')
pokemon, _ = Game.objects.get_or_create(name='Pokemon TCG', slug='pokemon')
lorcana, _ = Game.objects.get_or_create(name='Disney Lorcana', slug='lorcana')
# Populate MTG (Real Data)
self.populate_mtg(mtg)
# Populate Pokemon (Fake Data)
self.populate_fake_game(pokemon, 'Pokemon')
# Populate Lorcana (Fake Data)
self.populate_fake_game(lorcana, 'Lorcana')
# Create Superuser
if not User.objects.filter(username='admin').exists():
User.objects.create_superuser('admin', 'admin@example.com', 'admin')
self.stdout.write(self.style.SUCCESS('Created superuser: admin/admin'))
# Create Demo Users
self.create_demo_users()
self.stdout.write(self.style.SUCCESS('Database populated successfully!'))
def populate_mtg(self, game):
self.stdout.write('Fetching MTG data from Scryfall...')
# Get a few sets
sets_api = "https://api.scryfall.com/sets"
try:
resp = requests.get(sets_api).json()
# Pick top 3 recent expansion sets
target_sets = [s for s in resp['data'] if s['set_type'] == 'expansion'][:3]
for s_data in target_sets:
set_obj, created = Set.objects.get_or_create(
game=game,
name=s_data['name'],
code=s_data['code'],
defaults={'release_date': s_data.get('released_at')}
)
if created:
self.stdout.write(f"Created set: {set_obj.name}")
# Fetch cards for this set
cards_url = s_data['search_uri']
cards_resp = requests.get(cards_url).json()
for card_data in cards_resp.get('data', [])[:20]: # Limit to 20 cards per set to be fast
if 'image_uris' in card_data:
image = card_data['image_uris'].get('normal')
elif 'card_faces' in card_data and 'image_uris' in card_data['card_faces'][0]:
image = card_data['card_faces'][0]['image_uris'].get('normal')
else:
continue
card, _ = Card.objects.get_or_create(
set=set_obj,
name=card_data['name'],
defaults={
'rarity': card_data['rarity'].capitalize(),
'image_url': image,
'scryfall_id': card_data['id'],
'tcgplayer_id': card_data.get('tcgplayer_id'),
'collector_number': card_data['collector_number']
}
)
# Create Listings
self.create_listings_for_card(card)
except Exception as e:
self.stdout.write(self.style.ERROR(f"Failed to fetch MTG data: {e}"))
def populate_fake_game(self, game, prefix):
self.stdout.write(f'Generating data for {game.name}...')
for i in range(3): # 3 Sets
set_name = f"{prefix} Set {i+1}"
set_obj, _ = Set.objects.get_or_create(
game=game,
name=set_name,
code=f"{prefix[:3].upper()}{i+1}",
defaults={'release_date': fake.date_between(start_date='-2y', end_date='today')}
)
for j in range(15): # 15 Cards per set
card, _ = Card.objects.get_or_create(
set=set_obj,
name=f"{prefix} Monster {fake.word().capitalize()}",
defaults={
'rarity': random.choice(['Common', 'Uncommon', 'Rare', 'Ultra Rare']),
'image_url': f"https://placehold.co/400x600?text={prefix}+{j}",
'collector_number': str(j+1)
}
)
self.create_listings_for_card(card)
def create_listings_for_card(self, card):
# Create 1-5 listings per card with different conditions
for _ in range(random.randint(1, 4)):
condition = random.choice(['NM', 'LP', 'MP', 'HP'])
price = round(random.uniform(0.50, 100.00), 2)
CardListing.objects.create(
card=card,
condition=condition,
price=price,
quantity=random.randint(1, 20),
market_price=price, # Simplified
is_foil=random.choice([True, False])
)
def create_demo_users(self):
# Create a Pro user
if not User.objects.filter(username='prouser').exists():
u = User.objects.create_user('prouser', 'pro@example.com', 'password')
u.profile.is_pro = True
u.profile.save()
self.stdout.write("Created prouser/password")
# Create a Basic user
if not User.objects.filter(username='basicuser').exists():
User.objects.create_user('basicuser', 'basic@example.com', 'password')
self.stdout.write("Created basicuser/password")

View File

@@ -0,0 +1,90 @@
# Generated by Django 6.0.1 on 2026-01-19 13:38
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Card',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('rarity', models.CharField(blank=True, max_length=50)),
('image_url', models.URLField(blank=True, max_length=500)),
('scryfall_id', models.CharField(blank=True, max_length=100, null=True)),
('tcgplayer_id', models.CharField(blank=True, max_length=100, null=True)),
('collector_number', models.CharField(blank=True, max_length=50)),
],
),
migrations.CreateModel(
name='Cart',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='CartItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.PositiveIntegerField(default=1)),
],
),
migrations.CreateModel(
name='Game',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(unique=True)),
],
),
migrations.CreateModel(
name='Order',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('status', models.CharField(choices=[('pending', 'Pending'), ('paid', 'Paid'), ('shipped', 'Shipped'), ('cancelled', 'Cancelled')], default='pending', max_length=20)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('total_price', models.DecimalField(decimal_places=2, default=0, max_digits=12)),
('stripe_payment_intent', models.CharField(blank=True, max_length=100)),
('shipping_address', models.TextField(blank=True)),
],
),
migrations.CreateModel(
name='OrderItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('price_at_purchase', models.DecimalField(decimal_places=2, max_digits=10)),
('quantity', models.PositiveIntegerField(default=1)),
],
),
migrations.CreateModel(
name='Set',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('code', models.CharField(blank=True, max_length=20)),
('release_date', models.DateField(blank=True, null=True)),
],
),
migrations.CreateModel(
name='CardListing',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('condition', models.CharField(choices=[('NM', 'Near Mint'), ('LP', 'Lightly Played'), ('MP', 'Moderately Played'), ('HP', 'Heavily Played')], default='NM', max_length=2)),
('price', models.DecimalField(decimal_places=2, max_digits=10)),
('quantity', models.PositiveIntegerField(default=0)),
('market_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
('is_foil', models.BooleanField(default=False)),
('card', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='listings', to='store.card')),
],
),
]

View File

@@ -0,0 +1,58 @@
# Generated by Django 6.0.1 on 2026-01-19 13:38
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('store', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='cart',
name='user',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='cart', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='cartitem',
name='cart',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='store.cart'),
),
migrations.AddField(
model_name='cartitem',
name='listing',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='store.cardlisting'),
),
migrations.AddField(
model_name='order',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='orderitem',
name='listing',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='store.cardlisting'),
),
migrations.AddField(
model_name='orderitem',
name='order',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='store.order'),
),
migrations.AddField(
model_name='set',
name='game',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sets', to='store.game'),
),
migrations.AddField(
model_name='card',
name='set',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cards', to='store.set'),
),
]

View File

@@ -0,0 +1,40 @@
# Generated by Django 6.0.1 on 2026-01-19 16:22
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0002_initial'),
]
operations = [
migrations.AddField(
model_name='cart',
name='insurance',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='order',
name='insurance_purchased',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='order',
name='proxy_service',
field=models.BooleanField(default=False),
),
migrations.CreateModel(
name='Bounty',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('target_price', models.DecimalField(decimal_places=2, max_digits=10)),
('quantity_wanted', models.PositiveIntegerField(default=1)),
('is_active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('card', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bounties', to='store.card')),
],
),
]

View File

@@ -0,0 +1,57 @@
# Generated by Django 6.0.1 on 2026-01-19 18:44
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0003_cart_insurance_order_insurance_purchased_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='cartitem',
name='listing',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='store.cardlisting'),
),
migrations.AlterField(
model_name='orderitem',
name='listing',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='store.cardlisting'),
),
migrations.CreateModel(
name='PackListing',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('price', models.DecimalField(decimal_places=2, max_digits=10)),
('image_url', models.URLField(blank=True, max_length=500)),
('game', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pack_listings', to='store.game')),
],
),
migrations.AddField(
model_name='cartitem',
name='pack_listing',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='store.packlisting'),
),
migrations.AddField(
model_name='orderitem',
name='pack_listing',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='store.packlisting'),
),
migrations.CreateModel(
name='VirtualPack',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('status', models.CharField(choices=[('sealed', 'Sealed'), ('opened', 'Opened')], default='sealed', max_length=10)),
('created_at', models.DateTimeField(auto_now_add=True)),
('cards', models.ManyToManyField(related_name='packs', to='store.card')),
('listing', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='packs', to='store.packlisting')),
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='packs', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 6.0.1 on 2026-01-19 19:14
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0004_alter_cartitem_listing_alter_orderitem_listing_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='VaultItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.PositiveIntegerField(default=1)),
('added_at', models.DateTimeField(auto_now_add=True)),
('card', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vault_items', to='store.card')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vault_items', to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('user', 'card')},
},
),
]

View File

152
store/models.py Normal file
View File

@@ -0,0 +1,152 @@
from django.db import models
from django.conf import settings
class Game(models.Model):
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(unique=True)
def __str__(self):
return self.name
class Set(models.Model):
game = models.ForeignKey(Game, on_delete=models.CASCADE, related_name='sets')
name = models.CharField(max_length=200)
code = models.CharField(max_length=20, blank=True)
release_date = models.DateField(null=True, blank=True)
def __str__(self):
return f"{self.game.name} - {self.name}"
class Card(models.Model):
set = models.ForeignKey(Set, on_delete=models.CASCADE, related_name='cards')
name = models.CharField(max_length=200)
rarity = models.CharField(max_length=50, blank=True)
image_url = models.URLField(max_length=500, blank=True)
scryfall_id = models.CharField(max_length=100, blank=True, null=True)
tcgplayer_id = models.CharField(max_length=100, blank=True, null=True)
collector_number = models.CharField(max_length=50, blank=True)
def __str__(self):
return self.name
class CardListing(models.Model):
CONDITION_CHOICES = (
('NM', 'Near Mint'),
('LP', 'Lightly Played'),
('MP', 'Moderately Played'),
('HP', 'Heavily Played'),
)
card = models.ForeignKey(Card, on_delete=models.CASCADE, related_name='listings')
condition = models.CharField(max_length=2, choices=CONDITION_CHOICES, default='NM')
price = models.DecimalField(max_digits=10, decimal_places=2)
quantity = models.PositiveIntegerField(default=0)
market_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
is_foil = models.BooleanField(default=False)
def __str__(self):
return f"{self.card.name} ({self.condition}) - ${self.price}"
class PackListing(models.Model):
game = models.ForeignKey(Game, on_delete=models.CASCADE, related_name='pack_listings')
name = models.CharField(max_length=200)
price = models.DecimalField(max_digits=10, decimal_places=2)
image_url = models.URLField(max_length=500, blank=True)
def __str__(self):
return f"{self.name} - ${self.price}"
class VirtualPack(models.Model):
STATUS_CHOICES = (
('sealed', 'Sealed'),
('opened', 'Opened'),
)
listing = models.ForeignKey(PackListing, on_delete=models.CASCADE, related_name='packs')
cards = models.ManyToManyField(Card, related_name='packs')
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='sealed')
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='packs')
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.listing.name} ({self.get_status_display()})"
class Order(models.Model):
STATUS_CHOICES = (
('pending', 'Pending'),
('paid', 'Paid'),
('shipped', 'Shipped'),
('cancelled', 'Cancelled'),
)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='orders')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
total_price = models.DecimalField(max_digits=12, decimal_places=2, default=0)
stripe_payment_intent = models.CharField(max_length=100, blank=True)
shipping_address = models.TextField(blank=True)
insurance_purchased = models.BooleanField(default=False)
proxy_service = models.BooleanField(default=False)
def __str__(self):
return f"Order #{self.id} - {self.user.username}"
class OrderItem(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items')
listing = models.ForeignKey(CardListing, on_delete=models.SET_NULL, null=True, blank=True)
pack_listing = models.ForeignKey(PackListing, on_delete=models.SET_NULL, null=True, blank=True)
price_at_purchase = models.DecimalField(max_digits=10, decimal_places=2)
quantity = models.PositiveIntegerField(default=1)
def __str__(self):
if self.pack_listing:
return f"{self.quantity}x {self.pack_listing.name}"
return f"{self.quantity}x {self.listing.card.name if self.listing else 'Deleted Listing'}"
class Cart(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='cart', null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
insurance = models.BooleanField(default=False)
@property
def total_price(self):
base_total = sum(item.total_price for item in self.items.all())
if self.insurance:
return base_total + 5 # Flat fee
return base_total
class CartItem(models.Model):
cart = models.ForeignKey(Cart, on_delete=models.CASCADE, related_name='items')
listing = models.ForeignKey(CardListing, on_delete=models.CASCADE, null=True, blank=True)
pack_listing = models.ForeignKey(PackListing, on_delete=models.CASCADE, null=True, blank=True)
quantity = models.PositiveIntegerField(default=1)
@property
def total_price(self):
if self.listing:
return self.listing.price * self.quantity
if self.pack_listing:
return self.pack_listing.price * self.quantity
return 0
class Bounty(models.Model):
card = models.ForeignKey(Card, on_delete=models.CASCADE, related_name='bounties')
target_price = models.DecimalField(max_digits=10, decimal_places=2)
quantity_wanted = models.PositiveIntegerField(default=1)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"WANTED: {self.card.name} @ ${self.target_price}"
class VaultItem(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='vault_items')
card = models.ForeignKey(Card, on_delete=models.CASCADE, related_name='vault_items')
quantity = models.PositiveIntegerField(default=1)
added_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('user', 'card')
def __str__(self):
return f"{self.user.username}'s {self.card.name} ({self.quantity})"

3
store/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

23
store/urls.py Normal file
View File

@@ -0,0 +1,23 @@
from django.urls import path
from . import views
app_name = 'store'
urlpatterns = [
path('', views.card_list, name='card_list'), # Home page associated with 'card_list' view
path('home/', views.card_list, name='home'), # Explicit home alias for readability and templates using 'home' naming convention
path('card/<int:card_id>/', views.card_detail, name='card_detail'),
path('cart/', views.cart_view, name='cart'),
path('cart/add/<int:listing_id>/', views.add_to_cart, name='add_to_cart'),
path('cart/remove/<int:item_id>/', views.remove_from_cart, name='remove_from_cart'),
path('api/stock/<int:card_id>/', views.get_card_stock, name='get_card_stock'),
path('deck-buyer/', views.deck_buyer, name='deck_buyer'),
path('cart/insurance/', views.toggle_insurance, name='toggle_insurance'),
path('bounty-board/', views.bounty_board, name='bounty_board'),
path('packs/', views.pack_list, name='pack_list'),
path('cart/add-pack/<int:pack_listing_id>/', views.add_pack_to_cart, name='add_pack_to_cart'),
path('checkout/', views.checkout, name='checkout'),
path('my-packs/', views.my_packs, name='my_packs'),
path('packs/open/<int:pack_id>/', views.open_pack, name='open_pack'),
path('order/<int:order_id>/', views.order_detail, name='order_detail'),
]

133
store/utils.py Normal file
View File

@@ -0,0 +1,133 @@
import re
from .models import Card, CardListing, Order, OrderItem, VaultItem
from django.db.models import Min
def add_to_vault(user, card, quantity=1):
"""
Adds a card to the user's vault.
"""
vault_item, created = VaultItem.objects.get_or_create(user=user, card=card)
if not created:
vault_item.quantity += quantity
else:
vault_item.quantity = quantity
vault_item.save()
def parse_deck_list(deck_text):
"""
Parses a deck list string and returns a list of dictionaries with 'quantity' and 'name'.
Format expected: "4 Lightning Bolt" or "1x Lightning Bolt"
"""
lines = deck_text.strip().split('\n')
parsed_cards = []
for line in lines:
line = line.strip()
if not line:
continue
match = re.match(r'^(\d+)[xX]?\s+(.+)$', line)
if match:
quantity = int(match.group(1))
name = match.group(2).strip()
parsed_cards.append({'quantity': quantity, 'name': name})
else:
# Maybe just name? assume 1
parsed_cards.append({'quantity': 1, 'name': line})
return parsed_cards
def find_best_listings_for_deck(parsed_cards):
"""
Finds cheapest listings for the parsed cards.
Returns:
- found_items: list of {listing, quantity_needed, total_cost, card_name}
- missing_items: list of {name, quantity}
"""
found_items = []
missing_items = []
for item in parsed_cards:
name = item['name']
qty_needed = item['quantity']
# Find card (simple name match)
cards = Card.objects.filter(name__iexact=name)
if not cards.exists():
# Try contains
cards = Card.objects.filter(name__icontains=name)
if not cards.exists():
missing_items.append(item)
continue
# Find cheapest listing with stock
# We try to fill the quantity from multiple listings if needed
listings = CardListing.objects.filter(
card__in=cards,
quantity__gt=0
).order_by('price')
qty_remaining = qty_needed
for listing in listings:
if qty_remaining <= 0:
break
qty_to_take = min(listing.quantity, qty_remaining)
found_items.append({
'listing': listing,
'quantity': qty_to_take,
'card_name': listing.card.name,
'price': listing.price,
'total': listing.price * qty_to_take
})
qty_remaining -= qty_to_take
if qty_remaining > 0:
missing_items.append({'name': name, 'quantity': qty_remaining})
return found_items, missing_items
def get_user_collection(user):
"""
Returns a dict {card_name: quantity} of cards in user's vault.
"""
owned = {}
if not user.is_authenticated:
return owned
vault_items = VaultItem.objects.filter(user=user).select_related('card')
for item in vault_items:
owned[item.card.name] = item.quantity
return owned
def filter_deck_by_collection(parsed_cards, owned_cards):
"""
Subtracts owned quantities from parsed_cards.
Returns new list of parsed_cards.
"""
filtered = []
for item in parsed_cards:
name = item['name']
needed = item['quantity']
# Simple name match
owned_qty = 0
# Try exact match first
if name in owned_cards:
owned_qty = owned_cards[name]
else:
# Try case insensitive fallback
for key in owned_cards:
if key.lower() == name.lower():
owned_qty = owned_cards[key]
break
remaining = needed - owned_qty
if remaining > 0:
filtered.append({'name': name, 'quantity': remaining})
return filtered

282
store/views.py Normal file
View File

@@ -0,0 +1,282 @@
from django.shortcuts import render, get_object_or_404, redirect
from django.db.models import Q
from django.core.paginator import Paginator
from django.contrib.auth.decorators import login_required
from .models import Card, Game, Set, Cart, CartItem, CardListing, PackListing, VirtualPack, Order, OrderItem
import random
def card_list(request):
cards = Card.objects.all().select_related('set', 'set__game').prefetch_related('listings')
# Filtering
game_slug = request.GET.get('game')
if game_slug:
cards = cards.filter(set__game__slug=game_slug)
search_query = request.GET.get('q')
if search_query:
cards = cards.filter(name__icontains=search_query)
set_id = request.GET.get('set')
if set_id:
cards = cards.filter(set__id=set_id)
# Simple logic: only show cards that have listings or show all?
# Let's show all for browsing, but indicate stock.
paginator = Paginator(cards.order_by('name'), 24) # 24 cards per page
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
games = Game.objects.all()
# If a game is selected, getting its sets for the filter dropdown
sets = Set.objects.filter(game__slug=game_slug) if game_slug else Set.objects.all()[:50] # Limit default sets
return render(request, 'store/card_list.html', {
'page_obj': page_obj,
'games': games,
'sets': sets,
'current_game': game_slug,
'search_query': search_query,
})
def card_detail(request, card_id):
card = get_object_or_404(Card, id=card_id)
listings = card.listings.filter(quantity__gt=0).order_by('price')
return render(request, 'store/card_detail.html', {'card': card, 'listings': listings})
@login_required
def add_to_cart(request, listing_id):
listing = get_object_or_404(CardListing, id=listing_id)
cart, _ = Cart.objects.get_or_create(user=request.user)
cart_item, created = CartItem.objects.get_or_create(cart=cart, listing=listing)
if not created:
cart_item.quantity += 1
cart_item.save()
return redirect('store:cart')
@login_required
def cart_view(request):
try:
cart = request.user.cart
except Cart.DoesNotExist:
cart = None
return render(request, 'store/cart.html', {'cart': cart})
@login_required
def remove_from_cart(request, item_id):
if hasattr(request.user, 'cart'):
item = get_object_or_404(CartItem, id=item_id, cart=request.user.cart)
item.delete()
return redirect('store:cart')
@login_required
def toggle_insurance(request):
if hasattr(request.user, 'cart'):
cart = request.user.cart
cart.insurance = not cart.insurance
cart.save()
return redirect('store:cart')
from django.http import JsonResponse
def get_card_stock(request, card_id):
card = get_object_or_404(Card, id=card_id)
listings = card.listings.all()
stock_breakdown = {}
total_stock = 0
for listing in listings:
stock_breakdown[listing.get_condition_display()] = listing.quantity
total_stock += listing.quantity
return JsonResponse({
'card_id': card.id,
'total_stock': total_stock,
'breakdown': stock_breakdown
})
from .utils import parse_deck_list, find_best_listings_for_deck, get_user_collection, filter_deck_by_collection, add_to_vault
@login_required
def deck_buyer(request):
if request.method == 'POST':
action = request.POST.get('action')
if action == 'preview':
deck_text = request.POST.get('deck_text')
ignore_owned = request.POST.get('ignore_owned') == 'on'
parsed = parse_deck_list(deck_text)
if ignore_owned and request.user.is_authenticated:
owned = get_user_collection(request.user)
parsed = filter_deck_by_collection(parsed, owned)
found, missing = find_best_listings_for_deck(parsed)
total_cost = sum(item['total'] for item in found)
return render(request, 'store/deck_buyer.html', {
'found_items': found,
'missing_items': missing,
'deck_text': deck_text,
'total_cost': total_cost,
'preview': True,
'ignore_owned': ignore_owned
})
elif action == 'add_to_cart':
# Re-parse or rely on hidden fields?
# Re-parsing is safer/easier for now than passing complex data
deck_text = request.POST.get('deck_text')
parsed = parse_deck_list(deck_text)
found, _ = find_best_listings_for_deck(parsed)
cart, _ = Cart.objects.get_or_create(user=request.user)
count = 0
for item in found:
listing = item['listing']
qty = item['quantity']
# Check stock again? "Live stock"
if listing.quantity >= qty:
cart_item, created = CartItem.objects.get_or_create(cart=cart, listing=listing)
if not created:
cart_item.quantity += qty
else:
cart_item.quantity = qty
cart_item.save()
count += 1
return redirect('store:cart')
return render(request, 'store/deck_buyer.html')
from .models import Bounty
def bounty_board(request):
bounties = Bounty.objects.filter(is_active=True).select_related('card', 'card__set').order_by('-created_at')
return render(request, 'store/bounty_board.html', {'bounties': bounties})
def pack_list(request):
packs = PackListing.objects.all()
return render(request, 'store/pack_list.html', {'packs': packs})
@login_required
def add_pack_to_cart(request, pack_listing_id):
listing = get_object_or_404(PackListing, id=pack_listing_id)
cart, _ = Cart.objects.get_or_create(user=request.user)
cart_item, created = CartItem.objects.get_or_create(cart=cart, pack_listing=listing)
if not created:
cart_item.quantity += 1
cart_item.save()
return redirect('store:cart')
@login_required
def checkout(request):
try:
cart = request.user.cart
except Cart.DoesNotExist:
return redirect('store:cart')
if not cart.items.exists():
return redirect('store:cart')
# Create Order
order = Order.objects.create(
user=request.user,
status='paid',
total_price=cart.total_price
)
# Move items
for item in cart.items.all():
OrderItem.objects.create(
order=order,
listing=item.listing,
pack_listing=item.pack_listing,
price_at_purchase=item.listing.price if item.listing else item.pack_listing.price,
quantity=item.quantity
)
# Add single cards to vault
if item.listing:
add_to_vault(request.user, item.listing.card, item.quantity)
# If it's a pack, assign VirtualPacks to user
if item.pack_listing:
# Find available sealed packs
available_packs = list(VirtualPack.objects.filter(
listing=item.pack_listing,
owner__isnull=True,
status='sealed'
)[:item.quantity])
# If not enough, create more
if len(available_packs) < item.quantity:
needed = item.quantity - len(available_packs)
game = item.pack_listing.game
all_game_cards = list(Card.objects.filter(set__game=game))
if not all_game_cards:
# Fallback if no cards? Should not happen due to management command or basic setup
pass
for _ in range(needed):
pack = VirtualPack.objects.create(listing=item.pack_listing)
if all_game_cards:
pack.cards.set(random.sample(all_game_cards, min(len(all_game_cards), 5)))
available_packs.append(pack)
for pack in available_packs:
pack.owner = request.user
pack.save()
# Clear cart
cart.items.all().delete()
return redirect('store:my_packs')
@login_required
def my_packs(request):
packs = VirtualPack.objects.filter(owner=request.user, status='sealed').select_related('listing')
return render(request, 'store/my_packs.html', {'packs': packs})
@login_required
def open_pack(request, pack_id):
pack = get_object_or_404(VirtualPack, id=pack_id, owner=request.user)
if request.method == 'POST':
if pack.status == 'sealed':
pack.status = 'opened'
pack.save()
# Add cards to vault
for card in pack.cards.all():
add_to_vault(request.user, card)
data = {
'cards': [{
'name': c.name,
'image_url': c.image_url,
'rarity': c.rarity,
'set': c.set.name
} for c in pack.cards.all()]
}
return JsonResponse(data)
return render(request, 'store/open_pack.html', {'pack': pack})
@login_required
def order_detail(request, order_id):
order = get_object_or_404(Order, id=order_id)
# Security check: only allow viewing own orders (unless superuser)
if order.user != request.user and not request.user.is_superuser:
return redirect('users:profile')
return render(request, 'store/order_detail.html', {'order': order})