[beers] Add beer scrobbling

This commit is contained in:
2024-10-22 17:26:55 -04:00
parent d3d5b088cd
commit 08b48371bc
16 changed files with 392 additions and 1 deletions

View File

@ -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,
]

View File

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

View File

@ -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,
},
),
]

View File

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

View File

@ -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",
),
]

View File

@ -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

View File

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

View File

@ -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

View File

@ -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,
),
),
]

View File

@ -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:

View File

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

View File

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

View File

@ -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 %}

View File

@ -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 %}

View File

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