From 08b48371bc78de05952ee805d0d5eb93d0158cba Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Tue, 22 Oct 2024 17:26:55 -0400 Subject: [PATCH] [beers] Add beer scrobbling --- vrobbler/apps/beers/admin.py | 25 ++++ vrobbler/apps/beers/apps.py | 5 + .../apps/beers/migrations/0001_initial.py | 133 ++++++++++++++++++ vrobbler/apps/beers/migrations/__init__.py | 0 vrobbler/apps/beers/models.py | 41 ++++++ vrobbler/apps/beers/urls.py | 14 ++ vrobbler/apps/beers/views.py | 11 ++ vrobbler/apps/scrobbles/constants.py | 1 + vrobbler/apps/scrobbles/dataclasses.py | 10 ++ ...scrobble_beer_alter_scrobble_media_type.py | 50 +++++++ vrobbler/apps/scrobbles/models.py | 8 +- vrobbler/settings-testing.py | 1 + vrobbler/settings.py | 1 + vrobbler/templates/beers/beer_detail.html | 68 +++++++++ vrobbler/templates/beers/beer_list.html | 23 +++ vrobbler/urls.py | 2 + 16 files changed, 392 insertions(+), 1 deletion(-) create mode 100644 vrobbler/apps/beers/admin.py create mode 100644 vrobbler/apps/beers/apps.py create mode 100644 vrobbler/apps/beers/migrations/0001_initial.py create mode 100644 vrobbler/apps/beers/migrations/__init__.py create mode 100644 vrobbler/apps/beers/models.py create mode 100644 vrobbler/apps/beers/urls.py create mode 100644 vrobbler/apps/beers/views.py create mode 100644 vrobbler/apps/scrobbles/migrations/0066_scrobble_beer_alter_scrobble_media_type.py create mode 100644 vrobbler/templates/beers/beer_detail.html create mode 100644 vrobbler/templates/beers/beer_list.html diff --git a/vrobbler/apps/beers/admin.py b/vrobbler/apps/beers/admin.py new file mode 100644 index 0000000..9073a21 --- /dev/null +++ b/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, + ] diff --git a/vrobbler/apps/beers/apps.py b/vrobbler/apps/beers/apps.py new file mode 100644 index 0000000..ea0615e --- /dev/null +++ b/vrobbler/apps/beers/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class BeersConfig(AppConfig): + name = "beers" diff --git a/vrobbler/apps/beers/migrations/0001_initial.py b/vrobbler/apps/beers/migrations/0001_initial.py new file mode 100644 index 0000000..dd613c3 --- /dev/null +++ b/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, + }, + ), + ] diff --git a/vrobbler/apps/beers/migrations/__init__.py b/vrobbler/apps/beers/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vrobbler/apps/beers/models.py b/vrobbler/apps/beers/models.py new file mode 100644 index 0000000..7e06a5d --- /dev/null +++ b/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") diff --git a/vrobbler/apps/beers/urls.py b/vrobbler/apps/beers/urls.py new file mode 100644 index 0000000..4d19aa2 --- /dev/null +++ b/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//", + views.BeerDetailView.as_view(), + name="beer_detail", + ), +] diff --git a/vrobbler/apps/beers/views.py b/vrobbler/apps/beers/views.py new file mode 100644 index 0000000..22a443a --- /dev/null +++ b/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 diff --git a/vrobbler/apps/scrobbles/constants.py b/vrobbler/apps/scrobbles/constants.py index 0d473cb..fe6477b 100644 --- a/vrobbler/apps/scrobbles/constants.py +++ b/vrobbler/apps/scrobbles/constants.py @@ -17,6 +17,7 @@ PLAY_AGAIN_MEDIA = { "moods": "Mood", "bricksets": "BrickSet", "trails": "Trail", + "beers": "Beer", } MEDIA_END_PADDING_SECONDS = { diff --git a/vrobbler/apps/scrobbles/dataclasses.py b/vrobbler/apps/scrobbles/dataclasses.py index 5e91cbc..5bfd701 100644 --- a/vrobbler/apps/scrobbles/dataclasses.py +++ b/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 diff --git a/vrobbler/apps/scrobbles/migrations/0066_scrobble_beer_alter_scrobble_media_type.py b/vrobbler/apps/scrobbles/migrations/0066_scrobble_beer_alter_scrobble_media_type.py new file mode 100644 index 0000000..39b2cfe --- /dev/null +++ b/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, + ), + ), + ] diff --git a/vrobbler/apps/scrobbles/models.py b/vrobbler/apps/scrobbles/models.py index 86eb8b3..e86962a 100644 --- a/vrobbler/apps/scrobbles/models.py +++ b/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: diff --git a/vrobbler/settings-testing.py b/vrobbler/settings-testing.py index c1d41e7..62c5a4f 100644 --- a/vrobbler/settings-testing.py +++ b/vrobbler/settings-testing.py @@ -113,6 +113,7 @@ INSTALLED_APPS = [ "webpages", "tasks", "trails", + "beers", "lifeevents", "moods", "mathfilters", diff --git a/vrobbler/settings.py b/vrobbler/settings.py index 83b335e..15fdb10 100644 --- a/vrobbler/settings.py +++ b/vrobbler/settings.py @@ -130,6 +130,7 @@ INSTALLED_APPS = [ "webpages", "tasks", "trails", + "beers", "lifeevents", "moods", "mathfilters", diff --git a/vrobbler/templates/beers/beer_detail.html b/vrobbler/templates/beers/beer_detail.html new file mode 100644 index 0000000..0627c6f --- /dev/null +++ b/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 %} + +{% endblock %} + +{% block lists %} + +
+
+ {% if object.description%} +

{{object.description|safe|linebreaks|truncatewords:160}}

+
+ {% endif %} +

+ +

+
+
+
+

{{object.scrobble_set.count}} scrobbles

+

+ Drink again +

+
+
+
+

Last scrobbles

+
+ + + + + + + + {% for scrobble in object.scrobble_set.all|dictsortreversed:"timestamp" %} + + + + {% endfor %} + +
Date
{{scrobble.timestamp}}
+
+
+
+{% endblock %} diff --git a/vrobbler/templates/beers/beer_list.html b/vrobbler/templates/beers/beer_list.html new file mode 100644 index 0000000..cee1b31 --- /dev/null +++ b/vrobbler/templates/beers/beer_list.html @@ -0,0 +1,23 @@ +{% extends "base_list.html" %} + +{% block title %}Beers{% endblock %} + +{% block head_extra %} + +{% endblock %} + +{% block lists %} +
+ +
+
+ {% include "_scrobblable_list.html" %} +
+
+
+{% endblock %} diff --git a/vrobbler/urls.py b/vrobbler/urls.py index 4ac9988..18a33df 100644 --- a/vrobbler/urls.py +++ b/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")),