Files
vrobbler/vrobbler/apps/drinks/management/commands/populate_demo_data.py

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"
)
)