[drinks] Add Drink as a scrobbleable media
This commit is contained in:
@ -5,6 +5,7 @@ from drinks.models import (
|
||||
BeerStyle,
|
||||
Coffee,
|
||||
CoffeeRoaster,
|
||||
Drink,
|
||||
Wine,
|
||||
WineGrape,
|
||||
WineProducer,
|
||||
@ -97,3 +98,17 @@ class CoffeeAdmin(admin.ModelAdmin):
|
||||
inlines = [
|
||||
ScrobbleInline,
|
||||
]
|
||||
|
||||
|
||||
@admin.register(Drink)
|
||||
class DrinkAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = "created"
|
||||
list_display = (
|
||||
"uuid",
|
||||
"title",
|
||||
)
|
||||
ordering = ("-created",)
|
||||
search_fields = ("title",)
|
||||
inlines = [
|
||||
ScrobbleInline,
|
||||
]
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
# Generated by Django 4.2.29 on 2026-07-13 18:26
|
||||
|
||||
from django.db import migrations, models
|
||||
import django_extensions.db.fields
|
||||
import taggit.managers
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("scrobbles", "0101_favoritemedia_coffee_favoritemedia_wine_and_more"),
|
||||
("taggit", "0004_alter_taggeditem_content_type_alter_taggeditem_tag"),
|
||||
("drinks", "0009_coffeeroaster_winegrape_wineproducer_wineregion_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="beerproducer",
|
||||
options={},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="beerstyle",
|
||||
options={},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="wine",
|
||||
name="non_alcoholic",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Drink",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"created",
|
||||
django_extensions.db.fields.CreationDateTimeField(
|
||||
auto_now_add=True, verbose_name="created"
|
||||
),
|
||||
),
|
||||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, verbose_name="modified"
|
||||
),
|
||||
),
|
||||
(
|
||||
"uuid",
|
||||
models.UUIDField(
|
||||
blank=True, default=uuid.uuid4, editable=False, null=True
|
||||
),
|
||||
),
|
||||
("title", models.CharField(blank=True, max_length=255, null=True)),
|
||||
("base_run_time_seconds", models.IntegerField(blank=True, null=True)),
|
||||
("calories", models.PositiveIntegerField(blank=True, null=True)),
|
||||
("is_alcoholic", models.BooleanField(default=False)),
|
||||
(
|
||||
"genre",
|
||||
taggit.managers.TaggableManager(
|
||||
blank=True,
|
||||
help_text="A comma-separated list of tags.",
|
||||
through="scrobbles.ObjectWithGenres",
|
||||
to="scrobbles.Genre",
|
||||
verbose_name="Genre",
|
||||
),
|
||||
),
|
||||
(
|
||||
"tags",
|
||||
taggit.managers.TaggableManager(
|
||||
blank=True,
|
||||
help_text="A comma-separated list of tags.",
|
||||
through="taggit.TaggedItem",
|
||||
to="taggit.Tag",
|
||||
verbose_name="Tags",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"db_table": "drinks_drink",
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -75,7 +75,7 @@ class DrinkLogData(BaseLogData):
|
||||
@property
|
||||
def size_oz(self) -> Optional[float]:
|
||||
if self.size_ml:
|
||||
return round(self.size_ml / ML_PER_OZ, 1)
|
||||
return round(float(self.size_ml) / ML_PER_OZ, 1)
|
||||
return None
|
||||
|
||||
@property
|
||||
@ -146,16 +146,27 @@ class CoffeeLogData(DrinkLogData):
|
||||
|
||||
class Drink(ScrobblableMixin):
|
||||
calories = models.PositiveIntegerField(**BNULL)
|
||||
is_alcoholic = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
db_table = "drinks_drink"
|
||||
|
||||
def __str__(self):
|
||||
return self.title or "Drink"
|
||||
|
||||
@classmethod
|
||||
def find_or_create_water(cls) -> "Drink":
|
||||
water, _ = cls.objects.get_or_create(
|
||||
title="Water", defaults={"calories": 0, "base_run_time_seconds": 120}
|
||||
)
|
||||
return water
|
||||
|
||||
def get_absolute_url(self) -> str:
|
||||
return reverse("drinks:drink_detail", kwargs={"slug": self.uuid})
|
||||
|
||||
@property
|
||||
def is_alcoholic(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
# --- Beer models ---
|
||||
def logdata_cls(self):
|
||||
return DrinkLogData
|
||||
|
||||
|
||||
class BeerStyle(TimeStampedModel):
|
||||
@ -188,11 +199,12 @@ class BeerProducer(TimeStampedModel):
|
||||
db_table = "beers_beerproducer"
|
||||
|
||||
|
||||
class Beer(Drink):
|
||||
class Beer(ScrobblableMixin):
|
||||
description = models.TextField(**BNULL)
|
||||
ibu = models.SmallIntegerField(**BNULL)
|
||||
abv = models.FloatField(**BNULL)
|
||||
styles = models.ManyToManyField(BeerStyle, related_name="styles")
|
||||
calories = models.PositiveIntegerField(**BNULL)
|
||||
non_alcoholic = models.BooleanField(default=False)
|
||||
beeradvocate_id = models.CharField(max_length=255, **BNULL)
|
||||
beeradvocate_score = models.SmallIntegerField(**BNULL)
|
||||
@ -322,8 +334,10 @@ class WineProducer(TimeStampedModel):
|
||||
return self.name
|
||||
|
||||
|
||||
class Wine(Drink):
|
||||
class Wine(ScrobblableMixin):
|
||||
description = models.TextField(**BNULL)
|
||||
calories = models.PositiveIntegerField(**BNULL)
|
||||
non_alcoholic = models.BooleanField(default=False)
|
||||
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)
|
||||
@ -476,8 +490,9 @@ class CoffeeRoaster(TimeStampedModel):
|
||||
return self.name
|
||||
|
||||
|
||||
class Coffee(Drink):
|
||||
class Coffee(ScrobblableMixin):
|
||||
description = models.TextField(**BNULL)
|
||||
calories = models.PositiveIntegerField(**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)
|
||||
|
||||
@ -23,4 +23,15 @@ urlpatterns = [
|
||||
views.CoffeeDetailView.as_view(),
|
||||
name="coffee_detail",
|
||||
),
|
||||
path("drinks/", views.DrinkListView.as_view(), name="drink_list"),
|
||||
path(
|
||||
"drinks/<slug:slug>/",
|
||||
views.DrinkDetailView.as_view(),
|
||||
name="drink_detail",
|
||||
),
|
||||
path(
|
||||
"quick-water/",
|
||||
views.QuickWaterScrobbleView.as_view(),
|
||||
name="quick_water_scrobble",
|
||||
),
|
||||
]
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
from drinks.models import Beer, Coffee, Wine
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views import View
|
||||
from drinks.models import Beer, Coffee, Drink, Wine
|
||||
from scrobbles.views import ScrobbleableDetailView, ScrobbleableListView
|
||||
|
||||
|
||||
@ -24,3 +27,32 @@ class CoffeeListView(ScrobbleableListView):
|
||||
|
||||
class CoffeeDetailView(ScrobbleableDetailView):
|
||||
model = Coffee
|
||||
|
||||
|
||||
class DrinkListView(ScrobbleableListView):
|
||||
model = Drink
|
||||
|
||||
|
||||
class DrinkDetailView(ScrobbleableDetailView):
|
||||
model = Drink
|
||||
|
||||
|
||||
class QuickWaterScrobbleView(View):
|
||||
def post(self, request, *args, **kwargs):
|
||||
if not request.user.is_authenticated:
|
||||
messages.error(request, "You must be logged in to scrobble water.")
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
from scrobbles.scrobblers import manual_scrobble_water
|
||||
|
||||
scrobble = manual_scrobble_water(user_id=request.user.id)
|
||||
|
||||
if scrobble:
|
||||
messages.success(request, "Water scrobbled!")
|
||||
return HttpResponseRedirect(scrobble.redirect_url(request.user.id))
|
||||
|
||||
messages.error(request, "Failed to scrobble water.")
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return self.post(request, *args, **kwargs)
|
||||
|
||||
@ -27,6 +27,7 @@ AUTO_FINISH_MEDIA = {
|
||||
"tracks": "Track",
|
||||
"videos": "Video",
|
||||
"moods": "Mood",
|
||||
"drinks": "Drink",
|
||||
}
|
||||
|
||||
PLAY_AGAIN_MEDIA = {
|
||||
@ -40,6 +41,9 @@ PLAY_AGAIN_MEDIA = {
|
||||
"locations": "GeoLocation",
|
||||
"trails": "Trail",
|
||||
"drinks": "Beer",
|
||||
"drinks": "Wine",
|
||||
"drinks": "Coffee",
|
||||
"drinks": "Drink",
|
||||
"puzzles": "Puzzle",
|
||||
"foods": "Food",
|
||||
"tasks": "Task",
|
||||
@ -52,7 +56,7 @@ PLAY_AGAIN_MEDIA = {
|
||||
"discgolf": "DiscGolfCourse",
|
||||
}
|
||||
|
||||
DRINK_MODELS = ["Beer", "Wine", "Coffee"]
|
||||
DRINK_MODELS = ["Beer", "Wine", "Coffee", "Drink"]
|
||||
|
||||
MEDIA_END_PADDING_SECONDS = {
|
||||
"Video": 3600, # 60 min
|
||||
@ -93,6 +97,7 @@ MANUAL_SCROBBLE_FNS = {
|
||||
"-l": "manual_scrobble_brickset",
|
||||
"-c": "manual_scrobble_book",
|
||||
"-f": "manual_scrobble_food",
|
||||
"-d": "manual_scrobble_drink",
|
||||
"-h": "manual_scrobble_twitch_channel",
|
||||
"-dg": "manual_scrobble_discgolf",
|
||||
"-pp": "manual_scrobble_paper",
|
||||
|
||||
@ -0,0 +1,102 @@
|
||||
# Generated by Django 4.2.29 on 2026-07-13 18:31
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("drinks", "0010_alter_beerproducer_options_alter_beerstyle_options_and_more"),
|
||||
("scrobbles", "0101_favoritemedia_coffee_favoritemedia_wine_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="favoritemedia",
|
||||
name="drink",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="drinks.drink",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="scrobble",
|
||||
name="drink",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
to="drinks.drink",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="favoritemedia",
|
||||
name="media_type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("Video", "Video"),
|
||||
("Track", "Track"),
|
||||
("PodcastEpisode", "Podcast episode"),
|
||||
("SportEvent", "Sport event"),
|
||||
("Book", "Book"),
|
||||
("Paper", "Paper"),
|
||||
("VideoGame", "Video game"),
|
||||
("BoardGame", "Board game"),
|
||||
("GeoLocation", "GeoLocation"),
|
||||
("Trail", "Trail"),
|
||||
("Beer", "Beer"),
|
||||
("Wine", "Wine"),
|
||||
("Coffee", "Coffee"),
|
||||
("Drink", "Drink"),
|
||||
("Puzzle", "Puzzle"),
|
||||
("Food", "Food"),
|
||||
("Task", "Task"),
|
||||
("WebPage", "Web Page"),
|
||||
("LifeEvent", "Life event"),
|
||||
("Mood", "Mood"),
|
||||
("BrickSet", "Brick set"),
|
||||
("Channel", "Channel"),
|
||||
("BirdingLocation", "Birding location"),
|
||||
("DiscGolfCourse", "Disc golf"),
|
||||
],
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="scrobble",
|
||||
name="media_type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("Video", "Video"),
|
||||
("Track", "Track"),
|
||||
("PodcastEpisode", "Podcast episode"),
|
||||
("SportEvent", "Sport event"),
|
||||
("Book", "Book"),
|
||||
("Paper", "Paper"),
|
||||
("VideoGame", "Video game"),
|
||||
("BoardGame", "Board game"),
|
||||
("GeoLocation", "GeoLocation"),
|
||||
("Trail", "Trail"),
|
||||
("Beer", "Beer"),
|
||||
("Wine", "Wine"),
|
||||
("Coffee", "Coffee"),
|
||||
("Drink", "Drink"),
|
||||
("Puzzle", "Puzzle"),
|
||||
("Food", "Food"),
|
||||
("Task", "Task"),
|
||||
("WebPage", "Web Page"),
|
||||
("LifeEvent", "Life event"),
|
||||
("Mood", "Mood"),
|
||||
("BrickSet", "Brick set"),
|
||||
("Channel", "Channel"),
|
||||
("BirdingLocation", "Birding location"),
|
||||
("DiscGolfCourse", "Disc golf"),
|
||||
],
|
||||
default="Video",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -9,7 +9,7 @@ from zoneinfo import ZoneInfo
|
||||
|
||||
import pendulum
|
||||
import pytz
|
||||
from drinks.models import Beer, Wine, Coffee
|
||||
from drinks.models import Beer, Wine, Coffee, Drink
|
||||
from birds.models import BirdingLocation
|
||||
from discgolf.models import DiscGolfCourse
|
||||
from boardgames.models import BoardGame
|
||||
@ -686,6 +686,7 @@ TYPE_FK_PREFETCHES: dict[str, tuple[str, ...]] = {
|
||||
"Beer": ("beer",),
|
||||
"Wine": ("wine",),
|
||||
"Coffee": ("coffee",),
|
||||
"Drink": ("drink",),
|
||||
"Puzzle": ("puzzle",),
|
||||
"Food": ("food",),
|
||||
"Task": ("task",),
|
||||
@ -716,6 +717,7 @@ class ScrobbleQuerySet(models.QuerySet):
|
||||
"beer",
|
||||
"wine",
|
||||
"coffee",
|
||||
"drink",
|
||||
"puzzle",
|
||||
"food",
|
||||
"trail",
|
||||
@ -769,6 +771,7 @@ class Scrobble(TimeStampedModel):
|
||||
BEER = "Beer", "Beer"
|
||||
WINE = "Wine", "Wine"
|
||||
COFFEE = "Coffee", "Coffee"
|
||||
DRINK = "Drink", "Drink"
|
||||
PUZZLE = "Puzzle", "Puzzle"
|
||||
FOOD = "Food", "Food"
|
||||
TASK = "Task", "Task"
|
||||
@ -800,6 +803,7 @@ class Scrobble(TimeStampedModel):
|
||||
beer = models.ForeignKey(Beer, on_delete=models.DO_NOTHING, **BNULL)
|
||||
wine = models.ForeignKey(Wine, on_delete=models.DO_NOTHING, **BNULL)
|
||||
coffee = models.ForeignKey(Coffee, on_delete=models.DO_NOTHING, **BNULL)
|
||||
drink = models.ForeignKey(Drink, on_delete=models.DO_NOTHING, **BNULL)
|
||||
puzzle = models.ForeignKey(Puzzle, on_delete=models.DO_NOTHING, **BNULL)
|
||||
food = models.ForeignKey(Food, on_delete=models.DO_NOTHING, **BNULL)
|
||||
trail = models.ForeignKey(Trail, on_delete=models.DO_NOTHING, **BNULL)
|
||||
@ -1382,6 +1386,8 @@ class Scrobble(TimeStampedModel):
|
||||
media_obj = self.wine
|
||||
if self.coffee:
|
||||
media_obj = self.coffee
|
||||
if self.drink:
|
||||
media_obj = self.drink
|
||||
if self.puzzle:
|
||||
media_obj = self.puzzle
|
||||
if self.task:
|
||||
@ -1945,6 +1951,7 @@ class FavoriteMedia(TimeStampedModel):
|
||||
beer = models.ForeignKey(Beer, on_delete=models.CASCADE, **BNULL)
|
||||
wine = models.ForeignKey(Wine, on_delete=models.CASCADE, **BNULL)
|
||||
coffee = models.ForeignKey(Coffee, on_delete=models.CASCADE, **BNULL)
|
||||
drink = models.ForeignKey(Drink, on_delete=models.CASCADE, **BNULL)
|
||||
puzzle = models.ForeignKey(Puzzle, on_delete=models.CASCADE, **BNULL)
|
||||
food = models.ForeignKey(Food, on_delete=models.CASCADE, **BNULL)
|
||||
trail = models.ForeignKey(Trail, on_delete=models.CASCADE, **BNULL)
|
||||
@ -2003,6 +2010,8 @@ class FavoriteMedia(TimeStampedModel):
|
||||
media_obj = self.wine
|
||||
if self.coffee:
|
||||
media_obj = self.coffee
|
||||
if self.drink:
|
||||
media_obj = self.drink
|
||||
if self.puzzle:
|
||||
media_obj = self.puzzle
|
||||
if self.task:
|
||||
@ -2037,6 +2046,7 @@ class FavoriteMedia(TimeStampedModel):
|
||||
"Beer": "beer",
|
||||
"Wine": "wine",
|
||||
"Coffee": "coffee",
|
||||
"Drink": "drink",
|
||||
"Puzzle": "puzzle",
|
||||
"Food": "food",
|
||||
"Trail": "trail",
|
||||
|
||||
@ -15,7 +15,7 @@ from bricksets.models import BrickSet
|
||||
from dateutil.parser import parse
|
||||
from discgolf.models import DiscGolfCourse
|
||||
from django.utils import timezone
|
||||
from drinks.models import Beer, Coffee, Wine
|
||||
from drinks.models import Beer, Coffee, Wine, Drink, DrinkFormat
|
||||
from foods.models import Food
|
||||
from foods.sources.rscraper import RecipeScraperService
|
||||
from locations.constants import LOCATION_PROVIDERS
|
||||
@ -1350,6 +1350,37 @@ def manual_scrobble_coffee(
|
||||
return Scrobble.create_or_update(coffee, user_id, scrobble_dict)
|
||||
|
||||
|
||||
def manual_scrobble_water(
|
||||
user_id: int = None,
|
||||
source: str = "Vrobbler",
|
||||
action: Optional[str] = None,
|
||||
):
|
||||
water = Drink.find_or_create_water()
|
||||
|
||||
if not water:
|
||||
logger.error("Could not find or create Water instance")
|
||||
return
|
||||
|
||||
scrobble_dict = {
|
||||
"user_id": user_id,
|
||||
"timestamp": timezone.now(),
|
||||
"playback_position_seconds": 0,
|
||||
"source": source,
|
||||
"log": {"format": DrinkFormat.BY_THE_GLASS, "size_ml": 250},
|
||||
}
|
||||
logger.info(
|
||||
"[vrobbler-scrobble] water scrobble request received",
|
||||
extra={
|
||||
"water_id": water.id,
|
||||
"user_id": user_id,
|
||||
"scrobble_dict": scrobble_dict,
|
||||
"media_type": Scrobble.MediaType.DRINK,
|
||||
},
|
||||
)
|
||||
|
||||
return Scrobble.create_or_update(water, user_id, scrobble_dict)
|
||||
|
||||
|
||||
def manual_scrobble_puzzle(
|
||||
ipdb_id: str,
|
||||
user_id: int,
|
||||
@ -1461,6 +1492,50 @@ def manual_scrobble_food(
|
||||
return Scrobble.create_or_update(food, user_id, scrobble_dict)
|
||||
|
||||
|
||||
def manual_scrobble_drink(
|
||||
item_id: str,
|
||||
user_id: int,
|
||||
source: str = "Vrobbler",
|
||||
action: Optional[str] = None,
|
||||
):
|
||||
match = re.match(r"(.+?)\s+-\s+(\d+)", item_id)
|
||||
if not match:
|
||||
logger.info(
|
||||
"[manual_scrobble_drink] invalid format, expected 'Drink Name - calories'",
|
||||
extra={"item_id": item_id, "user_id": user_id},
|
||||
)
|
||||
return
|
||||
|
||||
drink_name = match.group(1).strip()
|
||||
calories = int(match.group(2))
|
||||
|
||||
drink = Drink.objects.filter(title=drink_name).first()
|
||||
if not drink:
|
||||
drink = Drink.objects.create(
|
||||
title=drink_name,
|
||||
calories=calories,
|
||||
)
|
||||
|
||||
scrobble_dict = {
|
||||
"user_id": user_id,
|
||||
"timestamp": timezone.now(),
|
||||
"playback_position_seconds": 0,
|
||||
"source": source,
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"[scrobblers] manual drink scrobble request received",
|
||||
extra={
|
||||
"drink_id": drink.id,
|
||||
"user_id": user_id,
|
||||
"scrobble_dict": scrobble_dict,
|
||||
"media_type": Scrobble.MediaType.DRINK,
|
||||
},
|
||||
)
|
||||
|
||||
return Scrobble.create_or_update(drink, user_id, scrobble_dict)
|
||||
|
||||
|
||||
def manual_scrobble_discgolf(
|
||||
item_id: str,
|
||||
user_id: int,
|
||||
|
||||
@ -1164,6 +1164,7 @@ def toggle_favorite(request, media_type, object_id):
|
||||
"Beer": ("drinks", "Beer"),
|
||||
"Wine": ("drinks", "Wine"),
|
||||
"Coffee": ("drinks", "Coffee"),
|
||||
"Water": ("drinks", "Water"),
|
||||
"Puzzle": ("puzzles", "Puzzle"),
|
||||
"Food": ("foods", "Food"),
|
||||
"Trail": ("trails", "Trail"),
|
||||
|
||||
@ -280,6 +280,12 @@
|
||||
<div class="nav-item text-nowrap">
|
||||
<a class="nav-link px-3" href="{% url "moods:checkin" %}">Check-in</a>
|
||||
</div>
|
||||
<div class="nav-item text-nowrap">
|
||||
<form method="post" action="{% url "drinks:quick_water_scrobble" %}" style="display:inline;">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="nav-link px-3 btn btn-link" style="text-decoration:none;">Drink water</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="nav-item text-nowrap">
|
||||
<a class="nav-link px-3" href="{% url "profiles:profile_settings" %}">Settings</a>
|
||||
</div>
|
||||
|
||||
80
vrobbler/templates/drinks/drink_detail.html
Normal file
80
vrobbler/templates/drinks/drink_detail.html
Normal file
@ -0,0 +1,80 @@
|
||||
{% extends "base_list.html" %}
|
||||
{% load mathfilters %}
|
||||
{% load static %}
|
||||
{% load naturalduration %}
|
||||
|
||||
{% block title %}{{object.title}}{% endblock %}
|
||||
|
||||
{% block head_extra %}
|
||||
<style>
|
||||
.cover img {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.cover {
|
||||
float: left;
|
||||
width: 252px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.summary {
|
||||
float: left;
|
||||
width: 600px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block lists %}
|
||||
|
||||
<div class="row">
|
||||
<div class="summary">
|
||||
{% if object.description%}
|
||||
<p>{{object.description|safe|linebreaks|truncatewords:160}}</p>
|
||||
<hr />
|
||||
{% endif %}
|
||||
<p style="float:right;">
|
||||
<a href="{{object.untappd_link}}"><img src="{% static "images/untappd-logo.png" %}" width=35></a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>{{scrobbles.count}} scrobbles</p>
|
||||
<p>
|
||||
<a href="{{object.start_url}}">Drink again</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<h3>Last scrobbles</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col">Format</th>
|
||||
<th scope="col">Size</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for scrobble in scrobbles.all|dictsortreversed:"timestamp" %}
|
||||
<tr>
|
||||
<td><a href="{{scrobble.get_absolute_url}}">{{scrobble.local_timestamp}}</a></td>
|
||||
<td>{{ scrobble.logdata.format_display }}</td>
|
||||
<td>
|
||||
{% if scrobble.logdata.size_ml %}
|
||||
{% if request.user.profile.volume_unit == "imperial" %}
|
||||
{{ scrobble.logdata.size_oz }} oz
|
||||
{% else %}
|
||||
{{ scrobble.logdata.size_ml }} mL
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
23
vrobbler/templates/drinks/drink_list.html
Normal file
23
vrobbler/templates/drinks/drink_list.html
Normal file
@ -0,0 +1,23 @@
|
||||
{% extends "base_list.html" %}
|
||||
|
||||
{% block title %}Drinks{% endblock %}
|
||||
|
||||
{% block head_extra %}
|
||||
<style>
|
||||
dl { width: 210px; float:left; margin-right: 10px; }
|
||||
dt a { color:white; text-decoration: none; font-size:smaller; }
|
||||
img { height:200px; width: 200px; object-fit: cover; }
|
||||
dd .right { float:right; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block lists %}
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md">
|
||||
<div class="table-responsive">
|
||||
{% include "_scrobblable_list.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
79
vrobbler/templates/drinks/water_detail.html
Normal file
79
vrobbler/templates/drinks/water_detail.html
Normal file
@ -0,0 +1,79 @@
|
||||
{% extends "base_list.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{object.title}}{% endblock %}
|
||||
|
||||
{% block head_extra %}
|
||||
<style>
|
||||
.cover img {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.cover {
|
||||
float: left;
|
||||
width: 252px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.summary {
|
||||
float: left;
|
||||
width: 600px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block lists %}
|
||||
|
||||
<div class="row">
|
||||
<div class="summary">
|
||||
{% if object.description%}
|
||||
<p>{{object.description|safe|linebreaks|truncatewords:160}}</p>
|
||||
<hr />
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>{{scrobbles.count}} scrobbles</p>
|
||||
<p>
|
||||
<a href="{{object.start_url}}">Drink again</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<h3>Last scrobbles</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col">Format</th>
|
||||
<th scope="col">Size</th>
|
||||
<th scope="col">Temperature</th>
|
||||
<th scope="col">Sparkling</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for scrobble in scrobbles.all|dictsortreversed:"timestamp" %}
|
||||
<tr>
|
||||
<td><a href="{{scrobble.get_absolute_url}}">{{scrobble.local_timestamp}}</a></td>
|
||||
<td>{{ scrobble.logdata.format_display }}</td>
|
||||
<td>
|
||||
{% if scrobble.logdata.size_ml %}
|
||||
{% if request.user.profile.volume_unit == "imperial" %}
|
||||
{{ scrobble.logdata.size_oz }} oz
|
||||
{% else %}
|
||||
{{ scrobble.logdata.size_ml }} mL
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ scrobble.logdata.temperature|title }}</td>
|
||||
<td>{% if scrobble.logdata.carbonated %}Yes{% elif scrobble.logdata.carbonated == False %}No{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -93,6 +93,8 @@
|
||||
<a href="{% url 'drinks:wine_detail' scrobble.wine.uuid %}">{{ scrobble.wine.title }}</a>
|
||||
{% elif scrobble.coffee %}
|
||||
<a href="{% url 'drinks:coffee_detail' scrobble.coffee.uuid %}">{{ scrobble.coffee.title }}</a>
|
||||
{% elif scrobble.water %}
|
||||
<a href="{% url 'drinks:water_detail' scrobble.water.uuid %}">{{ scrobble.water.title }}</a>
|
||||
{% elif scrobble.web_page %}
|
||||
<a href="{% url 'webpages:webpage_detail' scrobble.web_page.uuid %}">{{ scrobble.web_page.title }}</a>
|
||||
{% elif scrobble.podcast_episode %}
|
||||
|
||||
Reference in New Issue
Block a user