Files
vrobbler/vrobbler/apps/drinks/models.py

576 lines
17 KiB
Python

from dataclasses import dataclass
from typing import Optional
from uuid import uuid4
import requests
from django.apps import apps
from django.core.files.base import ContentFile
from django.db import models
from django.urls import reverse
from django_extensions.db.models import TimeStampedModel
from drinks.cellartracker import get_wine_from_cellartracker_id
from drinks.roastdb import get_coffee_from_slug, search_coffee_on_roastdb
from drinks.untappd import get_beer_from_untappd_id
from drinks.vivino import get_wine_from_vivino_id
from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFit
from scrobbles.dataclasses import BaseLogData
from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin
BNULL = {"blank": True, "null": True}
class WineType(models.TextChoices):
RED = "red", "Red"
WHITE = "white", "White"
ROSE = "rosé", "Rosé"
SPARKLING = "sparkling", "Sparkling"
DESSERT = "dessert", "Dessert"
FORTIFIED = "fortified", "Fortified"
ORANGE = "orange", "Orange"
class DrinkFormat(models.TextChoices):
ALUMINUM_CAN = "aluminum_can", "Aluminum Can"
GLASS_BOTTLE = "glass_bottle", "Glass Bottle"
PLASTIC_BOTTLE = "plastic_bottle", "Plastic Bottle"
METAL_BOTTLE = "metal_bottle", "Metal Bottle"
KEG = "keg", "Keg"
BY_THE_GLASS = "by_the_glass", "By the Glass"
DRAFT = "draft", "Draft"
GROWLER = "growler", "Growler"
BOX = "box", "Box"
class RoastLevel(models.TextChoices):
LIGHT = "light", "Light"
MEDIUM = "medium", "Medium"
DARK = "dark", "Dark"
class BrewingMethod(models.TextChoices):
POUROVER = "pourover", "Pour-over"
DRIP = "drip", "Drip"
FRENCH_PRESS = "french_press", "French Press"
ESPRESSO = "espresso", "Espresso"
AEROPRESS = "aeropress", "AeroPress"
KEURIG = "keurig", "Keurig"
NESCAPRESSO = "nespresso", "Nespresso"
COLD_BREW = "cold_brew", "Cold Brew"
MOKA_POT = "moka_pot", "Moka Pot"
SIPHON = "siphon", "Siphon"
TURKISH = "turkish", "Turkish"
CHEMEX = "chemex", "Chemex"
ML_PER_OZ = 29.5735
@dataclass
class DrinkLogData(BaseLogData):
rating: Optional[str] = None
format: Optional[str] = None
size_ml: Optional[int] = None
@property
def size_oz(self) -> Optional[float]:
if self.size_ml:
return round(self.size_ml / ML_PER_OZ, 1)
return None
@property
def format_display(self) -> str:
if self.format:
return DrinkFormat(self.format).label
return ""
@classmethod
def override_fields(cls) -> dict:
from django import forms
return {
"format": forms.ChoiceField(
choices=[("", "---------")] + list(DrinkFormat.choices),
required=False,
),
}
@dataclass
class BeerLogData(DrinkLogData):
pass
@dataclass
class WineLogData(DrinkLogData):
pass
@dataclass
class CoffeeLogData(DrinkLogData):
roast_level: Optional[str] = None
brewing_method: Optional[str] = None
single_origin_or_blend: Optional[str] = None
@property
def roast_level_display(self) -> str:
if self.roast_level:
return RoastLevel(self.roast_level).label
return ""
@property
def brewing_method_display(self) -> str:
if self.brewing_method:
return BrewingMethod(self.brewing_method).label
return ""
@classmethod
def override_fields(cls) -> dict:
from django import forms
fields = super().override_fields()
fields.update(
{
"roast_level": forms.ChoiceField(
choices=[("", "---------")] + list(RoastLevel.choices),
required=False,
),
"brewing_method": forms.ChoiceField(
choices=[("", "---------")] + list(BrewingMethod.choices),
required=False,
),
}
)
return fields
class Drink(ScrobblableMixin):
calories = models.PositiveIntegerField(**BNULL)
class Meta:
abstract = True
@property
def is_alcoholic(self) -> bool:
return True
# --- Beer models ---
class BeerStyle(TimeStampedModel):
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
name = models.CharField(max_length=255)
description = models.TextField(**BNULL)
def __str__(self):
return self.name
class Meta:
db_table = "beers_beerstyle"
class BeerProducer(TimeStampedModel):
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
name = models.CharField(max_length=255)
description = models.TextField(**BNULL)
location = models.CharField(max_length=255, **BNULL)
beeradvocate_id = models.CharField(max_length=255, **BNULL)
untappd_id = models.CharField(max_length=255, **BNULL)
def find_or_create(cls, title: str) -> "BeerProducer":
return cls.objects.filter(title=title).first()
def __str__(self):
return self.name
class Meta:
db_table = "beers_beerproducer"
class Beer(Drink):
description = models.TextField(**BNULL)
ibu = models.SmallIntegerField(**BNULL)
abv = models.FloatField(**BNULL)
styles = models.ManyToManyField(BeerStyle, related_name="styles")
non_alcoholic = models.BooleanField(default=False)
beeradvocate_id = models.CharField(max_length=255, **BNULL)
beeradvocate_score = models.SmallIntegerField(**BNULL)
untappd_image = models.ImageField(upload_to="drinks/untappd/", **BNULL)
untappd_image_small = ImageSpecField(
source="untappd_image",
processors=[ResizeToFit(100, 100)],
format="JPEG",
options={"quality": 60},
)
untappd_image_medium = ImageSpecField(
source="untappd_image",
processors=[ResizeToFit(300, 300)],
format="JPEG",
options={"quality": 75},
)
untappd_id = models.CharField(max_length=255, **BNULL)
untappd_rating = models.FloatField(**BNULL)
producer = models.ForeignKey(BeerProducer, on_delete=models.DO_NOTHING, **BNULL)
def get_absolute_url(self) -> str:
return reverse("drinks:beer_detail", kwargs={"slug": self.uuid})
def __str__(self):
return f"{self.title} by {self.producer}"
@property
def subtitle(self):
return self.producer.name
@property
def strings(self) -> ScrobblableConstants:
return ScrobblableConstants(verb="Drinking", tags="beer")
@property
def beeradvocate_link(self) -> str:
link = ""
if self.producer and self.beeradvocate_id:
if self.beeradvocate_id:
link = f"https://www.beeradvocate.com/beer/profile/{self.producer.beeradvocate_id}/{self.beeradvocate_id}/"
return link
@property
def untappd_link(self) -> str:
link = ""
if self.untappd_id:
link = f"https://www.untappd.com/beer/{self.untappd_id}/"
return link
@property
def primary_image_url(self) -> str:
url = ""
if self.untappd_image:
url = self.untappd_image.url
return url
@property
def logdata_cls(self):
return BeerLogData
@classmethod
def find_or_create(cls, untappd_id: str) -> "Beer":
beer = cls.objects.filter(untappd_id=untappd_id).first()
if not beer:
beer_dict = get_beer_from_untappd_id(untappd_id)
producer_dict = {}
style_ids = []
for key in list(beer_dict.keys()):
if "producer__" in key:
pkey = key.replace("producer__", "")
producer_dict[pkey] = beer_dict.pop(key)
if "styles" in key:
for style in beer_dict.pop("styles"):
style_inst, created = BeerStyle.objects.get_or_create(
name=style
)
style_ids.append(style_inst.id)
producer, _created = BeerProducer.objects.get_or_create(**producer_dict)
beer_dict["producer_id"] = producer.id
beer = Beer.objects.create(**beer_dict)
for style_id in style_ids:
beer.styles.add(style_id)
return beer
def scrobbles(self, user_id):
Scrobble = apps.get_model("scrobbles", "Scrobble")
return Scrobble.objects.filter(user_id=user_id, beer=self).order_by(
"-timestamp"
)
class Meta:
db_table = "beers_beer"
# --- Wine models ---
class WineGrape(TimeStampedModel):
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
name = models.CharField(max_length=255)
def __str__(self):
return self.name
class WineRegion(TimeStampedModel):
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
name = models.CharField(max_length=255)
country = models.CharField(max_length=255, **BNULL)
def __str__(self):
return self.name
class WineProducer(TimeStampedModel):
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
name = models.CharField(max_length=255)
description = models.TextField(**BNULL)
location = models.CharField(max_length=255, **BNULL)
vivino_id = models.CharField(max_length=255, **BNULL)
cellartracker_id = models.CharField(max_length=255, **BNULL)
def __str__(self):
return self.name
class Wine(Drink):
description = models.TextField(**BNULL)
wine_type = models.CharField(max_length=20, choices=WineType.choices, **BNULL)
grapes = models.ManyToManyField(WineGrape, related_name="wines", blank=True)
region = models.ForeignKey(WineRegion, on_delete=models.DO_NOTHING, **BNULL)
producer = models.ForeignKey(WineProducer, on_delete=models.DO_NOTHING, **BNULL)
vintage = models.CharField(max_length=255, **BNULL)
vivino_id = models.CharField(max_length=255, **BNULL)
vivino_rating = models.FloatField(**BNULL)
vivino_image = models.ImageField(upload_to="drinks/vivino/", **BNULL)
vivino_image_small = ImageSpecField(
source="vivino_image",
processors=[ResizeToFit(100, 100)],
format="JPEG",
options={"quality": 60},
)
vivino_image_medium = ImageSpecField(
source="vivino_image",
processors=[ResizeToFit(300, 300)],
format="JPEG",
options={"quality": 75},
)
cellartracker_id = models.CharField(max_length=255, **BNULL)
cellartracker_rating = models.FloatField(**BNULL)
cellartracker_image = models.ImageField(upload_to="drinks/cellartracker/", **BNULL)
def get_absolute_url(self) -> str:
return reverse("drinks:wine_detail", kwargs={"slug": self.uuid})
def __str__(self):
return f"{self.title} by {self.producer}"
@property
def subtitle(self):
return self.producer.name
@property
def strings(self) -> ScrobblableConstants:
return ScrobblableConstants(verb="Drinking", tags="wine")
@property
def vivino_link(self) -> str:
if self.vivino_id:
return f"https://www.vivino.com/{self.vivino_id}"
return ""
@property
def cellartracker_link(self) -> str:
if self.cellartracker_id:
return (
f"https://www.cellartracker.com/wine.asp?iWine={self.cellartracker_id}"
)
return ""
@property
def primary_image_url(self) -> str:
if self.vivino_image:
return self.vivino_image.url
if self.cellartracker_image:
return self.cellartracker_image.url
return ""
@property
def logdata_cls(self):
return WineLogData
def save_image_from_url(self, url):
headers = {"User-Agent": "Vrobbler 0.11.12"}
r = requests.get(url, headers=headers)
if r.status_code == 200:
fname = f"{self.title}_{self.uuid}.jpg"
self.vivino_image.save(fname, ContentFile(r.content), save=True)
@classmethod
def find_or_create(
cls, vivino_id: str = None, cellartracker_id: str = None
) -> "Wine":
wine = None
if vivino_id:
wine = cls.objects.filter(vivino_id=vivino_id).first()
if not wine and cellartracker_id:
wine = cls.objects.filter(cellartracker_id=cellartracker_id).first()
if wine:
return wine
wine_dict = None
if vivino_id:
wine_dict = get_wine_from_vivino_id(vivino_id)
elif cellartracker_id:
wine_dict = get_wine_from_cellartracker_id(cellartracker_id)
if not wine_dict or not wine_dict.get("title"):
return None
producer_name = wine_dict.pop("producer__name", "")
region_name = wine_dict.pop("region", "")
grape_names = wine_dict.pop("grapes", [])
image_url = wine_dict.pop("image_url", "")
producer = None
if producer_name:
producer, _ = WineProducer.objects.get_or_create(name=producer_name)
region = None
if region_name:
region, _ = WineRegion.objects.get_or_create(name=region_name)
wine = cls.objects.create(
title=wine_dict.get("title", ""),
description=wine_dict.get("description", ""),
wine_type=wine_dict.get("wine_type", ""),
vintage=wine_dict.get("vintage", ""),
vivino_id=wine_dict.get("vivino_id", vivino_id),
vivino_rating=wine_dict.get("vivino_rating"),
cellartracker_id=wine_dict.get("cellartracker_id", cellartracker_id),
cellartracker_rating=wine_dict.get("cellartracker_rating"),
producer=producer,
region=region,
)
for grape_name in grape_names:
grape, _ = WineGrape.objects.get_or_create(name=grape_name)
wine.grapes.add(grape)
if image_url:
try:
wine.save_image_from_url(image_url)
except Exception:
pass
return wine
def scrobbles(self, user_id):
Scrobble = apps.get_model("scrobbles", "Scrobble")
return Scrobble.objects.filter(user_id=user_id, wine=self).order_by(
"-timestamp"
)
# --- Coffee models ---
class CoffeeRoaster(TimeStampedModel):
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
name = models.CharField(max_length=255)
description = models.TextField(**BNULL)
location = models.CharField(max_length=255, **BNULL)
roastdb_id = models.CharField(max_length=255, **BNULL)
def __str__(self):
return self.name
class Coffee(Drink):
description = models.TextField(**BNULL)
origin = models.CharField(max_length=255, **BNULL)
roaster = models.ForeignKey(CoffeeRoaster, on_delete=models.DO_NOTHING, **BNULL)
roastdb_id = models.CharField(max_length=255, **BNULL)
roastdb_image = models.ImageField(upload_to="drinks/roastdb/", **BNULL)
roastdb_image_small = ImageSpecField(
source="roastdb_image",
processors=[ResizeToFit(100, 100)],
format="JPEG",
options={"quality": 60},
)
roastdb_image_medium = ImageSpecField(
source="roastdb_image",
processors=[ResizeToFit(300, 300)],
format="JPEG",
options={"quality": 75},
)
def get_absolute_url(self) -> str:
return reverse("drinks:coffee_detail", kwargs={"slug": self.uuid})
def __str__(self):
return f"{self.title} by {self.roaster}"
@property
def subtitle(self):
return self.roaster.name
@property
def strings(self) -> ScrobblableConstants:
return ScrobblableConstants(verb="Drinking", tags="coffee")
@property
def primary_image_url(self) -> str:
if self.roastdb_image:
return self.roastdb_image.url
return ""
@property
def logdata_cls(self):
return CoffeeLogData
def save_image_from_url(self, url):
headers = {"User-Agent": "Vrobbler 0.11.12"}
r = requests.get(url, headers=headers)
if r.status_code == 200:
fname = f"{self.title}_{self.uuid}.jpg"
self.roastdb_image.save(fname, ContentFile(r.content), save=True)
@classmethod
def find_or_create(cls, roastdb_id: str = None, slug: str = None) -> "Coffee":
coffee = None
if roastdb_id:
coffee = cls.objects.filter(roastdb_id=roastdb_id).first()
if not coffee and slug:
coffee = cls.objects.filter(roastdb_id=slug).first()
if coffee:
return coffee
coffee_dict = None
if slug:
coffee_dict = get_coffee_from_slug(slug)
elif roastdb_id:
coffee_dict = get_coffee_from_slug(roastdb_id)
if not coffee_dict or not coffee_dict.get("title"):
return None
roaster_name = coffee_dict.pop("roaster__name", "")
image_url = coffee_dict.pop("image_url", "")
roaster = None
if roaster_name:
roaster, _ = CoffeeRoaster.objects.get_or_create(name=roaster_name)
coffee = cls.objects.create(
title=coffee_dict.get("title", ""),
description=coffee_dict.get("description", ""),
origin=coffee_dict.get("origin", ""),
roastdb_id=slug or roastdb_id,
roaster=roaster,
)
if image_url:
try:
coffee.save_image_from_url(image_url)
except Exception:
pass
return coffee
def scrobbles(self, user_id):
Scrobble = apps.get_model("scrobbles", "Scrobble")
return Scrobble.objects.filter(user_id=user_id, coffee=self).order_by(
"-timestamp"
)