inital checkin
This commit is contained in:
0
store/__init__.py
Normal file
0
store/__init__.py
Normal file
3
store/admin.py
Normal file
3
store/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
5
store/apps.py
Normal file
5
store/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class StoreConfig(AppConfig):
|
||||
name = 'store'
|
||||
0
store/management/__init__.py
Normal file
0
store/management/__init__.py
Normal file
0
store/management/commands/__init__.py
Normal file
0
store/management/commands/__init__.py
Normal file
58
store/management/commands/create_test_packs.py
Normal file
58
store/management/commands/create_test_packs.py
Normal 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'))
|
||||
139
store/management/commands/populate_db.py
Normal file
139
store/management/commands/populate_db.py
Normal 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")
|
||||
90
store/migrations/0001_initial.py
Normal file
90
store/migrations/0001_initial.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
58
store/migrations/0002_initial.py
Normal file
58
store/migrations/0002_initial.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
29
store/migrations/0005_vaultitem.py
Normal file
29
store/migrations/0005_vaultitem.py
Normal 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')},
|
||||
},
|
||||
),
|
||||
]
|
||||
0
store/migrations/__init__.py
Normal file
0
store/migrations/__init__.py
Normal file
152
store/models.py
Normal file
152
store/models.py
Normal 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
3
store/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
23
store/urls.py
Normal file
23
store/urls.py
Normal 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
133
store/utils.py
Normal 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
282
store/views.py
Normal 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})
|
||||
|
||||
Reference in New Issue
Block a user