ソースを参照

[beers] Add beer scrobbling

Colin Powell 8 ヶ月 前
コミット
08b48371bc

+ 25 - 0
vrobbler/apps/beers/admin.py

@@ -0,0 +1,25 @@
+from django.contrib import admin
+
+from beers.models import Beer
+
+from scrobbles.admin import ScrobbleInline
+
+
+class BeerInline(admin.TabularInline):
+    model = Beer
+    extra = 0
+
+
+@admin.register(Beer)
+class BeerAdmin(admin.ModelAdmin):
+    date_hierarchy = "created"
+    list_display = (
+        "uuid",
+        "title",
+        "style",
+    )
+    ordering = ("-created",)
+    search_fields = ("title",)
+    inlines = [
+        ScrobbleInline,
+    ]

+ 5 - 0
vrobbler/apps/beers/apps.py

@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class BeersConfig(AppConfig):
+    name = "beers"

+ 133 - 0
vrobbler/apps/beers/migrations/0001_initial.py

@@ -0,0 +1,133 @@
+# Generated by Django 4.2.16 on 2024-10-22 21:26
+
+from django.db import migrations, models
+import django_extensions.db.fields
+import taggit.managers
+import uuid
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        ("scrobbles", "0065_alter_scrobble_log"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="BeerProducer",
+            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"
+                    ),
+                ),
+                ("description", models.TextField(blank=True, null=True)),
+                (
+                    "location",
+                    models.CharField(blank=True, max_length=255, null=True),
+                ),
+            ],
+            options={
+                "get_latest_by": "modified",
+                "abstract": False,
+            },
+        ),
+        migrations.CreateModel(
+            name="Beer",
+            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),
+                ),
+                (
+                    "run_time_seconds",
+                    models.IntegerField(blank=True, null=True),
+                ),
+                (
+                    "run_time_ticks",
+                    models.PositiveBigIntegerField(blank=True, null=True),
+                ),
+                ("description", models.TextField(blank=True, null=True)),
+                ("ibu", models.SmallIntegerField(blank=True, null=True)),
+                ("abv", models.FloatField(blank=True, null=True)),
+                (
+                    "style",
+                    models.CharField(blank=True, max_length=100, null=True),
+                ),
+                ("non_alcoholic", models.BooleanField(default=False)),
+                (
+                    "beeradvocate_id",
+                    models.CharField(blank=True, max_length=255, null=True),
+                ),
+                (
+                    "beeradvocate_score",
+                    models.SmallIntegerField(blank=True, null=True),
+                ),
+                (
+                    "untappd_id",
+                    models.CharField(blank=True, max_length=255, null=True),
+                ),
+                (
+                    "genre",
+                    taggit.managers.TaggableManager(
+                        blank=True,
+                        help_text="A comma-separated list of tags.",
+                        through="scrobbles.ObjectWithGenres",
+                        to="scrobbles.Genre",
+                        verbose_name="Tags",
+                    ),
+                ),
+            ],
+            options={
+                "abstract": False,
+            },
+        ),
+    ]

+ 0 - 0
vrobbler/apps/beers/migrations/__init__.py


+ 41 - 0
vrobbler/apps/beers/models.py

@@ -0,0 +1,41 @@
+from django.apps import apps
+from django.db import models
+from django.urls import reverse
+from django_extensions.db.models import TimeStampedModel
+from scrobbles.dataclasses import BeerLogData
+from scrobbles.mixins import ScrobblableMixin
+
+BNULL = {"blank": True, "null": True}
+
+
+class BeerProducer(TimeStampedModel):
+    description = models.TextField(**BNULL)
+    location = models.CharField(max_length=255, **BNULL)
+
+
+class Beer(ScrobblableMixin):
+    description = models.TextField(**BNULL)
+    ibu = models.SmallIntegerField(**BNULL)
+    abv = models.FloatField(**BNULL)
+    style = models.CharField(max_length=100, **BNULL)
+    non_alcoholic = models.BooleanField(default=False)
+    beeradvocate_id = models.CharField(max_length=255, **BNULL)
+    beeradvocate_score = models.SmallIntegerField(**BNULL)
+    untappd_id = models.CharField(max_length=255, **BNULL)
+
+    def get_absolute_url(self):
+        return reverse("beers:beer_detail", kwargs={"slug": self.uuid})
+
+    @property
+    def logdata_cls(self):
+        return BeerLogData
+
+    @classmethod
+    def find_or_create(cls, title: str) -> "Beer":
+        return cls.objects.filter(title=title).first()
+
+    def scrobbles(self, user_id):
+        Scrobble = apps.get_model("scrobbles", "Scrobble")
+        return Scrobble.objects.filter(
+            user_id=user_id, life_event=self
+        ).order_by("-timestamp")

+ 14 - 0
vrobbler/apps/beers/urls.py

@@ -0,0 +1,14 @@
+from django.urls import path
+from beers import views
+
+app_name = "beers"
+
+
+urlpatterns = [
+    path("beers/", views.BeerListView.as_view(), name="beer_list"),
+    path(
+        "beers/<slug:slug>/",
+        views.BeerDetailView.as_view(),
+        name="beer_detail",
+    ),
+]

+ 11 - 0
vrobbler/apps/beers/views.py

@@ -0,0 +1,11 @@
+from beers.models import Beer
+
+from scrobbles.views import ScrobbleableListView, ScrobbleableDetailView
+
+
+class BeerListView(ScrobbleableListView):
+    model = Beer
+
+
+class BeerDetailView(ScrobbleableDetailView):
+    model = Beer

+ 1 - 0
vrobbler/apps/scrobbles/constants.py

@@ -17,6 +17,7 @@ PLAY_AGAIN_MEDIA = {
     "moods": "Mood",
     "bricksets": "BrickSet",
     "trails": "Trail",
+    "beers": "Beer",
 }
 
 MEDIA_END_PADDING_SECONDS = {

+ 10 - 0
vrobbler/apps/scrobbles/dataclasses.py

@@ -35,6 +35,7 @@ class JSONDataclass(JSONWizard):
 class ScrobbleLogData(JSONDataclass):
     description: Optional[str] = None
 
+
 class LongPlayLogData(JSONDataclass):
     serial_scrobble_id: Optional[int]
     long_play_complete: bool = False
@@ -193,3 +194,12 @@ class TrailLogData(WithOthersLogData):
     details: Optional[str] = None
     effort: Optional[str] = None
     difficulty: Optional[str] = None
+
+
+@dataclass
+class BeerLogData(WithOthersLogData):
+    with_user_ids: Optional[list[int]] = None
+    with_names_str: Optional[list[str]] = None
+    details: Optional[str] = None
+    rating: Optional[str] = None
+    notes: Optional[str] = None

+ 50 - 0
vrobbler/apps/scrobbles/migrations/0066_scrobble_beer_alter_scrobble_media_type.py

@@ -0,0 +1,50 @@
+# Generated by Django 4.2.16 on 2024-10-22 21:26
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("beers", "0001_initial"),
+        ("scrobbles", "0065_alter_scrobble_log"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="scrobble",
+            name="beer",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.DO_NOTHING,
+                to="beers.beer",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="scrobble",
+            name="media_type",
+            field=models.CharField(
+                choices=[
+                    ("Video", "Video"),
+                    ("Track", "Track"),
+                    ("PodcastEpisode", "Podcast episode"),
+                    ("SportEvent", "Sport event"),
+                    ("Book", "Book"),
+                    ("VideoGame", "Video game"),
+                    ("BoardGame", "Board game"),
+                    ("GeoLocation", "GeoLocation"),
+                    ("Trail", "Trail"),
+                    ("Beer", "Beer"),
+                    ("Task", "Task"),
+                    ("WebPage", "Web Page"),
+                    ("LifeEvent", "Life event"),
+                    ("Mood", "Mood"),
+                    ("BrickSet", "Brick set"),
+                ],
+                default="Video",
+                max_length=14,
+            ),
+        ),
+    ]

+ 7 - 1
vrobbler/apps/scrobbles/models.py

@@ -39,6 +39,7 @@ from scrobbles.stats import build_charts
 from scrobbles.utils import media_class_to_foreign_key
 from sports.models import SportEvent
 from tasks.models import Task
+from beers.models import Beer
 from trails.models import Trail
 from videogames import retroarch
 from videogames.models import VideoGame
@@ -486,6 +487,7 @@ class Scrobble(TimeStampedModel):
         BOARD_GAME = "BoardGame", "Board game"
         GEO_LOCATION = "GeoLocation", "GeoLocation"
         TRAIL = "Trail", "Trail"
+        BEER = "Beer", "Beer"
         TASK = "Task", "Task"
         WEBPAGE = "WebPage", "Web Page"
         LIFE_EVENT = "LifeEvent", "Life event"
@@ -511,6 +513,7 @@ class Scrobble(TimeStampedModel):
     geo_location = models.ForeignKey(
         GeoLocation, on_delete=models.DO_NOTHING, **BNULL
     )
+    beer = models.ForeignKey(Beer, on_delete=models.DO_NOTHING, **BNULL)
     trail = models.ForeignKey(Trail, on_delete=models.DO_NOTHING, **BNULL)
     task = models.ForeignKey(Task, on_delete=models.DO_NOTHING, **BNULL)
     web_page = models.ForeignKey(WebPage, on_delete=models.DO_NOTHING, **BNULL)
@@ -574,8 +577,11 @@ class Scrobble(TimeStampedModel):
     @property
     def last_serial_scrobble(self) -> Optional["Scrobble"]:
         from scrobbles.models import Scrobble
+
         if self.logdata and self.logdata.serial_scrobble_id:
-            return Scrobble.objects.filter(id=self.logdata.serial_scrobble_id).first()
+            return Scrobble.objects.filter(
+                id=self.logdata.serial_scrobble_id
+            ).first()
 
     def save(self, *args, **kwargs):
         if not self.uuid:

+ 1 - 0
vrobbler/settings-testing.py

@@ -113,6 +113,7 @@ INSTALLED_APPS = [
     "webpages",
     "tasks",
     "trails",
+    "beers",
     "lifeevents",
     "moods",
     "mathfilters",

+ 1 - 0
vrobbler/settings.py

@@ -130,6 +130,7 @@ INSTALLED_APPS = [
     "webpages",
     "tasks",
     "trails",
+    "beers",
     "lifeevents",
     "moods",
     "mathfilters",

+ 68 - 0
vrobbler/templates/beers/beer_detail.html

@@ -0,0 +1,68 @@
+{% 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.beeradvocate_link}}"><img src="{% static "images/beeradvoate-logo.png" %}" width=35></a>
+        </p>
+    </div>
+</div>
+<div class="row">
+    <p>{{object.scrobble_set.count}} scrobbles</p>
+    <p>
+        <a href="{{object.get_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>
+                    </tr>
+                </thead>
+                <tbody>
+                    {% for scrobble in object.scrobble_set.all|dictsortreversed:"timestamp" %}
+                    <tr>
+                        <td>{{scrobble.timestamp}}</td>
+                    </tr>
+                    {% endfor %}
+                </tbody>
+            </table>
+        </div>
+    </div>
+</div>
+{% endblock %}

+ 23 - 0
vrobbler/templates/beers/beer_list.html

@@ -0,0 +1,23 @@
+{% extends "base_list.html" %}
+
+{% block title %}Beers{% 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 %}

+ 2 - 0
vrobbler/urls.py

@@ -37,6 +37,7 @@ from vrobbler.apps.sports.api.views import (
 )
 from vrobbler.apps.tasks import urls as tasks_urls
 from vrobbler.apps.trails import urls as trails_urls
+from vrobbler.apps.beers import urls as beers_urls
 from vrobbler.apps.videogames import urls as videogame_urls
 from vrobbler.apps.videos import urls as video_urls
 from vrobbler.apps.videos.api.views import SeriesViewSet, VideoViewSet
@@ -78,6 +79,7 @@ urlpatterns = [
     path("", include(sports_urls, namespace="sports")),
     path("", include(locations_urls, namespace="locations")),
     path("", include(trails_urls, namespace="trails")),
+    path("", include(beers_urls, namespace="beers")),
     path("", include(tasks_urls, namespace="tasks")),
     path("", include(webpages_urls, namespace="webpages")),
     path("", include(podcast_urls, namespace="podcasts")),