508 lines
17 KiB
Python
508 lines
17 KiB
Python
import random
|
|
from datetime import timedelta
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.core.management.base import BaseCommand
|
|
from django.utils import timezone
|
|
from drinks.models import (
|
|
Beer,
|
|
BeerProducer,
|
|
BeerStyle,
|
|
BrewingMethod,
|
|
Coffee,
|
|
CoffeeRoaster,
|
|
DrinkFormat,
|
|
RoastLevel,
|
|
Wine,
|
|
WineGrape,
|
|
WineProducer,
|
|
WineRegion,
|
|
WineType,
|
|
)
|
|
from scrobbles.models import FavoriteMedia, Scrobble
|
|
|
|
User = get_user_model()
|
|
|
|
# Realistic demo data
|
|
|
|
BEER_PRODUCERS = [
|
|
("Trillium Brewing", "Canton, MA"),
|
|
("Tree House Brewing", "Charlton, MA"),
|
|
("The Alchemist", "Stowe, VT"),
|
|
("Hill Farmstead", "Greensboro, VT"),
|
|
("Other Half Brewing", "Brooklyn, NY"),
|
|
("Equilibrium Brewery", "Middletown, NY"),
|
|
("Monkish Brewing", "Torrance, CA"),
|
|
("Russian River Brewing", "Santa Rosa, CA"),
|
|
("Cellar Maker", "San Francisco, CA"),
|
|
("Definitive Brewing", "Portland, ME"),
|
|
]
|
|
|
|
BEER_STYLES = [
|
|
"New England IPA",
|
|
"Hazy Double IPA",
|
|
"West Coast IPA",
|
|
"Pale Ale",
|
|
"Stout",
|
|
"Imperial Stout",
|
|
"Porter",
|
|
"Saison",
|
|
"Pilsner",
|
|
"Lager",
|
|
"Gose",
|
|
"Sour Ale",
|
|
"Brown Ale",
|
|
"Barleywine",
|
|
"Wheat Ale",
|
|
]
|
|
|
|
BEERS = [
|
|
("Congress Street", "Trillium Brewing", ["New England IPA"], 6.8, 45),
|
|
("Julius", "Tree House Brewing", ["New England IPA"], 6.8, 50),
|
|
("Heady Topper", "The Alchemist", ["New England IPA"], 8.0, 70),
|
|
("Sip of Sunshine", "Hill Farmstead", ["New England IPA"], 8.0, 60),
|
|
("All Green Everything", "Other Half Brewing", ["Hazy Double IPA"], 10.0, 100),
|
|
("Moment of Clarity", "Equilibrium Brewery", ["New England IPA"], 8.5, 55),
|
|
("Tired Hands Collab", "Monkish Brewing", ["New England IPA"], 7.2, 40),
|
|
("Pliny the Elder", "Russian River Brewing", ["West Coast IPA"], 8.0, 100),
|
|
("Density", "Cellar Maker", ["Hazy Double IPA"], 8.2, 65),
|
|
("Bright Days Ahead", "Definitive Brewing", ["Pale Ale"], 5.5, 35),
|
|
("Bright", "Tree House Brewing", ["Pale Ale"], 5.2, 30),
|
|
("King Julius", "Tree House Brewing", ["Hazy Double IPA"], 9.0, 80),
|
|
("Very Hazy", "Other Half Brewing", ["New England IPA"], 7.5, 45),
|
|
("Galaxy Dry Hop", "Equilibrium Brewery", ["New England IPA"], 7.0, 50),
|
|
("DDH Foggier Window", "Monkish Brewing", ["Hazy Double IPA"], 8.0, 70),
|
|
]
|
|
|
|
WINE_PRODUCERS = [
|
|
("Domaine de la Romanée-Conti", "Burgundy, France"),
|
|
("Opus One", "Napa Valley, CA"),
|
|
("Penfolds", "South Australia"),
|
|
("Cloudy Bay", "Marlborough, New Zealand"),
|
|
("Antinori", "Tuscany, Italy"),
|
|
("Vega Sicilia", "Ribera del Duero, Spain"),
|
|
("Catena Zapata", "Mendoza, Argentina"),
|
|
("Concha y Toro", "Casablanca Valley, Chile"),
|
|
("Kim Crawford", "Hawke's Bay, New Zealand"),
|
|
("Meiomi", "Monterey County, CA"),
|
|
("Château d'Esclans", "Provence, France"),
|
|
("Veuve Clicquot", "Reims, France"),
|
|
("Giacomo Conterno", "Piemonte, Italy"),
|
|
("Château de Beaucastel", "Rhône, France"),
|
|
("Dr. Loosen", "Mosel, Germany"),
|
|
]
|
|
|
|
WINE_REGIONS = [
|
|
("Burgundy", "France"),
|
|
("Napa Valley", "USA"),
|
|
("Barossa Valley", "Australia"),
|
|
("Marlborough", "New Zealand"),
|
|
("Tuscany", "Italy"),
|
|
("Ribera del Duero", "Spain"),
|
|
("Mendoza", "Argentina"),
|
|
("Casablanca Valley", "Chile"),
|
|
("Hawke's Bay", "New Zealand"),
|
|
("Monterey County", "USA"),
|
|
("Willamette Valley", "USA"),
|
|
("Rioja", "Spain"),
|
|
("Piemonte", "Italy"),
|
|
("Champagne", "France"),
|
|
("Mosel", "Germany"),
|
|
("Provence", "France"),
|
|
("Rhône", "France"),
|
|
("Reims", "France"),
|
|
]
|
|
|
|
WINE_GRAPES = [
|
|
"Pinot Noir",
|
|
"Cabernet Sauvignon",
|
|
"Merlot",
|
|
"Syrah",
|
|
"Grenache",
|
|
"Chardonnay",
|
|
"Sauvignon Blanc",
|
|
"Riesling",
|
|
"Tempranillo",
|
|
"Malbec",
|
|
"Nebbiolo",
|
|
"Sangiovese",
|
|
"Zinfandel",
|
|
"Viognier",
|
|
"Mourvèdre",
|
|
]
|
|
|
|
WINES = [
|
|
(
|
|
"2021 Pinot Noir",
|
|
"Domaine de la Romanée-Conti",
|
|
"Burgundy",
|
|
"Pinot Noir",
|
|
"red",
|
|
"2021",
|
|
4.5,
|
|
),
|
|
(
|
|
"2019 Cabernet Sauvignon",
|
|
"Opus One",
|
|
"Napa Valley",
|
|
"Cabernet Sauvignon",
|
|
"red",
|
|
"2019",
|
|
4.8,
|
|
),
|
|
("2020 Shiraz", "Penfolds", "Barossa Valley", "Syrah", "red", "2020", 4.6),
|
|
(
|
|
"2022 Sauvignon Blanc",
|
|
"Cloudy Bay",
|
|
"Marlborough",
|
|
"Sauvignon Blanc",
|
|
"white",
|
|
"2022",
|
|
4.2,
|
|
),
|
|
("2020 Tignanello", "Antinori", "Tuscany", "Sangiovese", "red", "2020", 4.7),
|
|
(
|
|
"2018 Unico",
|
|
"Vega Sicilia",
|
|
"Ribera del Duero",
|
|
"Tempranillo",
|
|
"red",
|
|
"2018",
|
|
4.9,
|
|
),
|
|
("2021 Malbec", "Catena Zapata", "Mendoza", "Malbec", "red", "2021", 4.4),
|
|
(
|
|
"2022 Chardonnay",
|
|
"Concha y Toro",
|
|
"Casablanca Valley",
|
|
"Chardonnay",
|
|
"white",
|
|
"2022",
|
|
4.1,
|
|
),
|
|
(
|
|
"2021 Sauvignon Blanc",
|
|
"Kim Crawford",
|
|
"Hawke's Bay",
|
|
"Sauvignon Blanc",
|
|
"white",
|
|
"2021",
|
|
4.0,
|
|
),
|
|
("2020 Pinot Noir", "Meiomi", "Monterey County", "Pinot Noir", "red", "2020", 4.3),
|
|
("2021 Rosé", "Château d'Esclans", "Provence", "Grenache", "rosé", "2021", 4.2),
|
|
(
|
|
"2020 Champagne Brut",
|
|
"Veuve Clicquot",
|
|
"Champagne",
|
|
"Chardonnay",
|
|
"sparkling",
|
|
"NV",
|
|
4.5,
|
|
),
|
|
("2019 Barolo", "Giacomo Conterno", "Piemonte", "Nebbiolo", "red", "2019", 4.8),
|
|
(
|
|
"2020 Châteauneuf-du-Pape",
|
|
"Château de Beaucastel",
|
|
"Rhône",
|
|
"Grenache",
|
|
"red",
|
|
"2020",
|
|
4.6,
|
|
),
|
|
("2021 Riesling Spätlese", "Dr. Loosen", "Mosel", "Riesling", "white", "2021", 4.3),
|
|
]
|
|
|
|
COFFEE_ROASTERS = [
|
|
("Counter Culture", "Durham, NC"),
|
|
("Intelligentsia", "Chicago, IL"),
|
|
("Stumptown", "Portland, OR"),
|
|
("Blue Bottle", "Oakland, CA"),
|
|
(
|
|
"Onyx Coffee Lab",
|
|
"Rogers, AR",
|
|
),
|
|
("Verve", "Santa Cruz, CA"),
|
|
("Heart", "Portland, OR"),
|
|
("George Howell", "Boston, MA"),
|
|
("SEY", "Brooklyn, NY"),
|
|
("Black & White", "Raleigh, NC"),
|
|
]
|
|
|
|
COFFEES = [
|
|
("Hologram", "Counter Culture", "Ethiopia", "medium"),
|
|
("Big Trouble", "Counter Culture", "Colombia", "medium"),
|
|
("Black Cat Espresso", "Intelligentsia", "Brazil", "medium"),
|
|
("Hair Bender", "Intelligentsia", "Latin America", "dark"),
|
|
("Stumptown Hair Bender", "Stumptown", "Latin America", "medium"),
|
|
("Holler Mountain", "Stumptown", "Ethiopia", "medium"),
|
|
("Bella Donovan", "Blue Bottle", "Latin America", "medium"),
|
|
("Three Africas", "Blue Bottle", "Uganda", "light"),
|
|
("Monarch", "Onyx Coffee Lab", "Ethiopia", "light"),
|
|
("Geometry", "Onyx Coffee Lab", "Colombia", "medium"),
|
|
("Buena Vista", "Verve", "Guatemala", "medium"),
|
|
("Farmhouse", "Verve", "Costa Rica", "medium"),
|
|
("Decaf", "Heart", "Colombia", "medium"),
|
|
("Los Altos", "Heart", "Guatemala", "medium"),
|
|
("Hayes Valley Espresso", "George Howell", "Brazil", "medium"),
|
|
]
|
|
|
|
|
|
class Command(BaseCommand):
|
|
help = "Populate demo data for the drinks app with realistic scrobbles"
|
|
|
|
def add_arguments(self, parser):
|
|
parser.add_argument(
|
|
"--user",
|
|
type=str,
|
|
help="Username to create scrobbles for (default: creates 'demo')",
|
|
)
|
|
parser.add_argument(
|
|
"--days",
|
|
type=int,
|
|
default=30,
|
|
help="Number of days of history to generate (default: 30)",
|
|
)
|
|
parser.add_argument(
|
|
"--clear",
|
|
action="store_true",
|
|
help="Clear existing demo data before populating",
|
|
)
|
|
|
|
def handle(self, *args, **options):
|
|
username = options.get("user") or "demo"
|
|
days = options["days"]
|
|
clear = options["clear"]
|
|
|
|
user, _created = User.objects.get_or_create(
|
|
username=username,
|
|
defaults={"email": f"{username}@demo.local"},
|
|
)
|
|
if _created:
|
|
user.set_password("demo")
|
|
user.save()
|
|
self.stdout.write(self.style.SUCCESS(f"Created user '{username}'"))
|
|
else:
|
|
self.stdout.write(f"Using existing user '{username}'")
|
|
|
|
if clear:
|
|
self.stdout.write("Clearing existing demo drink scrobbles...")
|
|
Scrobble.objects.filter(
|
|
user=user,
|
|
media_type__in=["Beer", "Wine", "Coffee"],
|
|
).delete()
|
|
FavoriteMedia.objects.filter(user=user).delete()
|
|
self.stdout.write(self.style.SUCCESS("Cleared demo data"))
|
|
|
|
self.stdout.write("Creating beer styles...")
|
|
style_objects = {}
|
|
for style_name in BEER_STYLES:
|
|
style, _ = BeerStyle.objects.get_or_create(name=style_name)
|
|
style_objects[style_name] = style
|
|
|
|
self.stdout.write("Creating beer producers...")
|
|
producer_objects = {}
|
|
for name, location in BEER_PRODUCERS:
|
|
producer, _ = BeerProducer.objects.get_or_create(
|
|
name=name, defaults={"location": location}
|
|
)
|
|
producer_objects[name] = producer
|
|
|
|
self.stdout.write("Creating beers...")
|
|
beer_objects = []
|
|
for title, producer_name, styles, abv, ibu in BEERS:
|
|
beer, _ = Beer.objects.get_or_create(
|
|
untappd_id=title.lower().replace(" ", "-"),
|
|
defaults={
|
|
"title": title,
|
|
"abv": abv,
|
|
"ibu": ibu,
|
|
"producer_id": producer_objects[producer_name].id,
|
|
"description": f"A delicious {styles[0]} from {producer_name}.",
|
|
"base_run_time_seconds": 900,
|
|
"calories": int(abv * 30),
|
|
},
|
|
)
|
|
for style_name in styles:
|
|
beer.styles.add(style_objects[style_name])
|
|
beer_objects.append(beer)
|
|
|
|
self.stdout.write("Creating wine regions...")
|
|
region_objects = {}
|
|
for name, country in WINE_REGIONS:
|
|
region, _ = WineRegion.objects.get_or_create(
|
|
name=name, defaults={"country": country}
|
|
)
|
|
region_objects[name] = region
|
|
|
|
self.stdout.write("Creating wine producers...")
|
|
wine_producer_objects = {}
|
|
for name, location in WINE_PRODUCERS:
|
|
producer, _ = WineProducer.objects.get_or_create(
|
|
name=name, defaults={"location": location}
|
|
)
|
|
wine_producer_objects[name] = producer
|
|
|
|
self.stdout.write("Creating wine grapes...")
|
|
grape_objects = {}
|
|
for name in WINE_GRAPES:
|
|
grape, _ = WineGrape.objects.get_or_create(name=name)
|
|
grape_objects[name] = grape
|
|
|
|
self.stdout.write("Creating wines...")
|
|
wine_objects = []
|
|
for (
|
|
title,
|
|
producer_name,
|
|
region_name,
|
|
grape_name,
|
|
wine_type,
|
|
vintage,
|
|
rating,
|
|
) in WINES:
|
|
wine, _ = Wine.objects.get_or_create(
|
|
vivino_id=title.lower().replace(" ", "-"),
|
|
defaults={
|
|
"title": title,
|
|
"wine_type": wine_type,
|
|
"vintage": vintage,
|
|
"vivino_rating": rating,
|
|
"producer_id": wine_producer_objects[producer_name].id,
|
|
"region_id": region_objects[region_name].id,
|
|
"description": f"A fine {wine_type} from {producer_name}.",
|
|
"base_run_time_seconds": 3600,
|
|
"calories": 125,
|
|
},
|
|
)
|
|
wine.grapes.add(grape_objects[grape_name])
|
|
wine_objects.append(wine)
|
|
|
|
self.stdout.write("Creating coffee roasters...")
|
|
roaster_objects = {}
|
|
for name, location in COFFEE_ROASTERS:
|
|
roaster, _ = CoffeeRoaster.objects.get_or_create(
|
|
name=name, defaults={"location": location}
|
|
)
|
|
roaster_objects[name] = roaster
|
|
|
|
self.stdout.write("Creating coffees...")
|
|
coffee_objects = []
|
|
for title, roaster_name, origin, _roast_level in COFFEES:
|
|
coffee, _ = Coffee.objects.get_or_create(
|
|
roastdb_id=title.lower().replace(" ", "-"),
|
|
defaults={
|
|
"title": title,
|
|
"origin": origin,
|
|
"roaster_id": roaster_objects[roaster_name].id,
|
|
"description": f"Specialty coffee from {roaster_name}.",
|
|
"base_run_time_seconds": 1200,
|
|
"calories": 5,
|
|
},
|
|
)
|
|
coffee_objects.append(coffee)
|
|
|
|
self.stdout.write("Creating scrobbles...")
|
|
now = timezone.now()
|
|
scrobbles_created = 0
|
|
|
|
for day_offset in range(days):
|
|
day = now - timedelta(days=day_offset)
|
|
|
|
# Random number of drinks per day (1-5)
|
|
num_drinks = random.randint(1, 5)
|
|
for _ in range(num_drinks):
|
|
# Pick a random drink type weighted toward beers
|
|
drink_type = random.choices(
|
|
["beer", "wine", "coffee"], weights=[50, 25, 25]
|
|
)[0]
|
|
|
|
if drink_type == "beer":
|
|
media = random.choice(beer_objects)
|
|
media_type = Scrobble.MediaType.BEER
|
|
source = random.choice(["Untappd", "Vrobbler"])
|
|
log_data = {
|
|
"rating": str(random.choice([3.5, 4.0, 4.5, 5.0])),
|
|
"format": random.choice(
|
|
[
|
|
DrinkFormat.ALUMINUM_CAN,
|
|
DrinkFormat.GLASS_BOTTLE,
|
|
DrinkFormat.DRAFT,
|
|
]
|
|
),
|
|
"size_ml": random.choice([330, 355, 473, 500]),
|
|
}
|
|
elif drink_type == "wine":
|
|
media = random.choice(wine_objects)
|
|
media_type = Scrobble.MediaType.WINE
|
|
source = random.choice(["Vivino", "Vrobbler"])
|
|
log_data = {
|
|
"rating": str(random.choice([3.5, 4.0, 4.5, 5.0])),
|
|
"format": random.choice(
|
|
[
|
|
DrinkFormat.GLASS_BOTTLE,
|
|
DrinkFormat.BY_THE_GLASS,
|
|
DrinkFormat.BOX,
|
|
]
|
|
),
|
|
"size_ml": random.choice([150, 375, 750]),
|
|
}
|
|
else:
|
|
media = random.choice(coffee_objects)
|
|
media_type = Scrobble.MediaType.COFFEE
|
|
source = random.choice(["RoastDB", "Vrobbler"])
|
|
log_data = {
|
|
"rating": str(random.choice([3.5, 4.0, 4.5, 5.0])),
|
|
"format": random.choice(
|
|
[
|
|
DrinkFormat.GLASS_BOTTLE,
|
|
DrinkFormat.ALUMINUM_CAN,
|
|
DrinkFormat.BY_THE_GLASS,
|
|
]
|
|
),
|
|
"size_ml": random.choice([240, 355, 473]),
|
|
"roast_level": random.choice(RoastLevel.values),
|
|
"brewing_method": random.choice(BrewingMethod.values),
|
|
"single_origin_or_blend": random.choice(
|
|
["single origin", "blend"]
|
|
),
|
|
}
|
|
|
|
# Random time during the day
|
|
hour = random.randint(7, 22)
|
|
minute = random.randint(0, 59)
|
|
timestamp = day.replace(
|
|
hour=hour, minute=minute, second=0, microsecond=0
|
|
)
|
|
|
|
# Use the FK field name from the media class name
|
|
fk_field = f"{media.__class__.__name__[0].lower()}{media.__class__.__name__[1:]}"
|
|
# Simpler: just lowercase the class name
|
|
fk_field = media.__class__.__name__.lower()
|
|
|
|
scrobble = Scrobble.objects.create(
|
|
user=user,
|
|
media_type=media_type,
|
|
timestamp=timestamp,
|
|
playback_position_seconds=media.base_run_time_seconds or 900,
|
|
played_to_completion=True,
|
|
source=source,
|
|
log=log_data,
|
|
**{fk_field: media},
|
|
)
|
|
scrobbles_created += 1
|
|
|
|
# Some scrobbles get favorited
|
|
if random.random() < 0.15:
|
|
FavoriteMedia.toggle(media, user)
|
|
|
|
self.stdout.write(
|
|
self.style.SUCCESS(
|
|
f"\nDone! Created:\n"
|
|
f" {len(beer_objects)} beers\n"
|
|
f" {len(wine_objects)} wines\n"
|
|
f" {len(coffee_objects)} coffees\n"
|
|
f" {scrobbles_created} scrobbles over {days} days\n"
|
|
f" User: {username} / demo"
|
|
)
|
|
)
|