From f3fc58e2c07ed3e45be490be344ba35472b92a4a Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Sat, 10 Aug 2024 22:12:24 -0400 Subject: [PATCH] [moods] Add moods --- data/moods.json | 3 + vrobbler/apps/boardgames/models.py | 6 +- vrobbler/apps/lifeevents/models.py | 6 +- vrobbler/apps/moods/admin.py | 23 ++++++ vrobbler/apps/moods/apps.py | 5 ++ .../apps/moods/migrations/0001_initial.py | 79 +++++++++++++++++++ vrobbler/apps/moods/migrations/__init__.py | 0 vrobbler/apps/moods/models.py | 28 +++++++ vrobbler/apps/moods/urls.py | 14 ++++ vrobbler/apps/moods/views.py | 12 +++ vrobbler/apps/scrobbles/dataclasses.py | 47 ++++++----- ...robble_mood_alter_scrobble_log_and_more.py | 57 +++++++++++++ vrobbler/apps/scrobbles/models.py | 46 ++++------- vrobbler/apps/scrobbles/views.py | 33 +++++++- vrobbler/settings.py | 1 + vrobbler/urls.py | 2 + 16 files changed, 304 insertions(+), 58 deletions(-) create mode 100644 data/moods.json create mode 100644 vrobbler/apps/moods/admin.py create mode 100644 vrobbler/apps/moods/apps.py create mode 100644 vrobbler/apps/moods/migrations/0001_initial.py create mode 100644 vrobbler/apps/moods/migrations/__init__.py create mode 100644 vrobbler/apps/moods/models.py create mode 100644 vrobbler/apps/moods/urls.py create mode 100644 vrobbler/apps/moods/views.py create mode 100644 vrobbler/apps/scrobbles/migrations/0057_scrobble_mood_alter_scrobble_log_and_more.py diff --git a/data/moods.json b/data/moods.json new file mode 100644 index 0000000..1b6962c --- /dev/null +++ b/data/moods.json @@ -0,0 +1,3 @@ +Eagerly running all tasks +Connected to sqlite@db.sqlite3 +[{"model": "moods.mood", "pk": 1, "fields": {"created": "2024-08-10T20:06:25.694Z", "modified": "2024-08-10T20:06:25.694Z", "uuid": "5191fa4b-67a3-4b94-b8a5-de3e6863914d", "title": "Anxious", "run_time_seconds": null, "run_time_ticks": null, "description": "Anxiety is an emotion which is characterized by an unpleasant state of inner turmoil and includes feelings of dread over anticipated events. Anxiety is different from fear in that fear is defined as the emotional response to a present threat, whereas anxiety is the anticipation of a future one. (Wikipedia)"}}, {"model": "moods.mood", "pk": 2, "fields": {"created": "2024-08-10T20:06:55.418Z", "modified": "2024-08-10T20:06:55.418Z", "uuid": "2c431aba-5103-444c-ad91-4fec2405a363", "title": "Fear", "run_time_seconds": null, "run_time_ticks": null, "description": "Fear is an intensely unpleasant primal emotion in response to perceiving or recognizing a danger or threat. Fear causes psychological changes that may produce behavioral reactions such as mounting an aggressive response or fleeing the threat. (Wikipedia)"}}, {"model": "moods.mood", "pk": 3, "fields": {"created": "2024-08-10T20:07:14.035Z", "modified": "2024-08-10T20:07:14.035Z", "uuid": "696b2bf2-9ae6-43b4-8b3d-67f03c9dc48b", "title": "Sadness", "run_time_seconds": null, "run_time_ticks": null, "description": "Sadness is an emotional pain associated with, or characterized by, feelings of disadvantage, loss, despair, grief, helplessness, disappointment and sorrow. An individual experiencing sadness may become quiet or lethargic, and withdraw themselves from others. (Wikipedia)"}}, {"model": "moods.mood", "pk": 4, "fields": {"created": "2024-08-10T20:07:38.437Z", "modified": "2024-08-10T20:07:38.437Z", "uuid": "550beff0-d673-4bb5-a87b-80c6f4a9a62a", "title": "Anger", "run_time_seconds": null, "run_time_ticks": null, "description": "Anger, also known as wrath or rage, is an intense emotional state involving a strong uncomfortable and non-cooperative response to a perceived provocation, hurt, or threat. (Wikipedia)"}}, {"model": "moods.mood", "pk": 5, "fields": {"created": "2024-08-10T20:08:09.423Z", "modified": "2024-08-10T20:08:09.423Z", "uuid": "3b210bc1-7981-4d55-8cf3-e07b46cb15ea", "title": "Surprise", "run_time_seconds": null, "run_time_ticks": null, "description": "Surprise is an instantaneous, instinctual reaction to something you didn't expect. (Wikipedia)"}}, {"model": "moods.mood", "pk": 6, "fields": {"created": "2024-08-10T20:09:01.056Z", "modified": "2024-08-10T20:09:01.056Z", "uuid": "865dd78e-66e6-44bb-8257-d8dc1fe5c19c", "title": "Disgust", "run_time_seconds": null, "run_time_ticks": null, "description": "Disgust is an emotional response of rejection or revulsion to something potentially contagious or something considered offensive, distasteful or unpleasant. (Wikipedia)"}}, {"model": "moods.mood", "pk": 8, "fields": {"created": "2024-08-10T20:11:02.145Z", "modified": "2024-08-10T20:11:02.145Z", "uuid": "89e82483-c52f-4855-9bb0-cb246a0b684b", "title": "Joy", "run_time_seconds": null, "run_time_ticks": null, "description": "Joy is the state of being that allows one to experience feelings of intense, long-lasting happiness and satisfaction of life. It is closely related to, and often evoked by, well-being, success, or good fortune. Happiness, pleasure, and gratitude are closely related to joy but are not identical to it. (Wikipedia)"}}, {"model": "moods.mood", "pk": 9, "fields": {"created": "2024-08-10T20:13:38.852Z", "modified": "2024-08-10T20:13:38.852Z", "uuid": "a0d80c03-969a-4b80-a4b6-2beb9d91687d", "title": "Admiration", "run_time_seconds": null, "run_time_ticks": null, "description": "Admiration is a social emotion felt by observing people of competence, talent, or skill exceeding standards. Admiration facilitates social learning in groups. Admiration motivates self-improvement through learning from role-models."}}, {"model": "moods.mood", "pk": 10, "fields": {"created": "2024-08-10T20:14:06.617Z", "modified": "2024-08-10T20:14:06.617Z", "uuid": "5382f548-2a5a-4a08-b447-6470d41c1729", "title": "Adoration", "run_time_seconds": null, "run_time_ticks": null, "description": "Adoration is a feeling of great admiration and love for someone or something. She needs and wants to be loved with overwhelming passion and adoration. Synonyms: love, honour, worship, worshipping More Synonyms of adoration."}}, {"model": "moods.mood", "pk": 11, "fields": {"created": "2024-08-10T20:14:25.964Z", "modified": "2024-08-10T20:14:25.964Z", "uuid": "e36ed925-37d8-4856-ba7c-bfce617ecab2", "title": "Aesthetic Appreciation", "run_time_seconds": null, "run_time_ticks": null, "description": "Aesthetic appreciation or good taste is the ability to appreciate objective beauty. Aesthetic taste is influenced by many factors, such as personal or cultural background, familiarity, context, and expertise. Access to aesthetically interesting and valuable objects and environments can improve our well-being."}}, {"model": "moods.mood", "pk": 12, "fields": {"created": "2024-08-10T20:14:58.926Z", "modified": "2024-08-10T20:14:58.926Z", "uuid": "4d8c6a20-0041-4793-94ae-1d3dc12c2e95", "title": "Amusement", "run_time_seconds": null, "run_time_ticks": null, "description": "Amusement is the state of experiencing humorous and entertaining events or situations while the person or animal actively maintains the experience, and is associated with enjoyment, happiness, laughter and pleasure. It is an emotion with positive valence and high physiological arousal."}}, {"model": "moods.mood", "pk": 13, "fields": {"created": "2024-08-10T20:17:32.001Z", "modified": "2024-08-10T20:17:32.001Z", "uuid": "460e072a-6d5b-481a-8343-cec66b9f4baa", "title": "Awe", "run_time_seconds": null, "run_time_ticks": null, "description": "Awe is a complex emotion that occurs when we experience or witness something wonderous, vast, terrifying, inspiring, amazing or mind-blowing. Awe can be triggered by experiences as diverse as walking through an untamed natural landscape, viewing a highly complex piece of art or architecture, having a spiritual or religious experience, or witnessing a seemingly impossible athletic feat."}}, {"model": "moods.mood", "pk": 14, "fields": {"created": "2024-08-10T20:17:52.494Z", "modified": "2024-08-10T20:17:52.494Z", "uuid": "cbeefe6b-0a6e-4f91-8b1f-a272901265ff", "title": "Awkwardness", "run_time_seconds": null, "run_time_ticks": null, "description": "Embarrassment or awkwardness is an emotional state that is associated with mild to severe levels of discomfort, and which is usually experienced when someone commits (or thinks of) a socially unacceptable or frowned-upon act that is witnessed by or revealed to others."}}, {"model": "moods.mood", "pk": 15, "fields": {"created": "2024-08-10T20:18:40.932Z", "modified": "2024-08-10T20:18:40.933Z", "uuid": "1c27b52e-aae4-4631-9bf4-21953776ef8b", "title": "Boredom", "run_time_seconds": null, "run_time_ticks": null, "description": "In conventional usage, boredom, ennui, or tedium is an emotion characterized by uninterest in one's surrounding, often caused by a lack of distractions or occupations. Although, \"There is no universally accepted definition of boredom. But whatever it is, researchers argue, it is not simply another name for depression or apathy. It seems to be a specific mental state that people find unpleasant—a lack of stimulation that leaves them craving relief, with a host of behavioral, medical and social consequences.\""}}, {"model": "moods.mood", "pk": 16, "fields": {"created": "2024-08-10T20:19:04.661Z", "modified": "2024-08-10T20:19:04.661Z", "uuid": "6a0ea707-05d6-41a5-9e2f-92b7a9c15cdc", "title": "Calmness", "run_time_seconds": null, "run_time_ticks": null, "description": "Calmness is the mental state of peace of mind, being free from agitation, excitement, or disturbance. It also refers being in a state of serenity, tranquillity, or peace. Calmness can most easily occur for the average person during a state of relaxation, but it can also be found during much more alert and aware states."}}, {"model": "moods.mood", "pk": 17, "fields": {"created": "2024-08-10T20:19:26.216Z", "modified": "2024-08-10T20:19:26.216Z", "uuid": "c4e5e2b2-9070-4203-857e-18b82cd62b92", "title": "Confusion", "run_time_seconds": null, "run_time_ticks": null, "description": "The feeling when you get information that does not make sense to you, leaving you uncertain what to do with it."}}, {"model": "moods.mood", "pk": 18, "fields": {"created": "2024-08-10T20:20:32.294Z", "modified": "2024-08-10T20:20:32.294Z", "uuid": "0bffe27f-6514-4478-b9b5-0b099ea39e28", "title": "Craving/Desire", "run_time_seconds": null, "run_time_ticks": null, "description": "Desires are states of mind that are expressed by terms like \"wanting\", \"wishing\", \"longing\" or \"craving\". A great variety of features is commonly associated with desires. They are seen as propositional attitudes towards conceivable states of affairs. They aim to change the world by representing how the world should be, unlike beliefs, which aim to represent how the world actually is."}}, {"model": "moods.mood", "pk": 19, "fields": {"created": "2024-08-10T20:21:00.225Z", "modified": "2024-08-10T20:21:00.225Z", "uuid": "2f213c97-369e-4763-ac87-7d74dc6801ae", "title": "Empathetic pain", "run_time_seconds": null, "run_time_ticks": null, "description": "Pain empathy is a specific variety of empathy that involves recognizing and understanding another person's pain. Empathy is the mental ability that allows one person to understand another person's mental and emotional state and how to effectively respond to that person."}}, {"model": "moods.mood", "pk": 20, "fields": {"created": "2024-08-10T20:21:37.372Z", "modified": "2024-08-10T20:21:37.372Z", "uuid": "4beb8a85-16e0-4df9-b569-d29205e0fb4f", "title": "Entrancement", "run_time_seconds": null, "run_time_ticks": null, "description": "Definitions of entrancement. noun. a feeling of delight at being filled with wonder and enchantment."}}, {"model": "moods.mood", "pk": 21, "fields": {"created": "2024-08-10T20:22:24.010Z", "modified": "2024-08-10T20:22:24.010Z", "uuid": "8f8c26e2-86cd-4c69-89ed-4ce8a4ec5029", "title": "Excitement", "run_time_seconds": null, "run_time_ticks": null, "description": "Excitement is a feeling or situation full of activity, joy, exhilaration, or upheaval. One thing about excitement — it sure isn't boring. There are a few types of excitement, but they're all exciting — they get your attention. If you can't wait for your birthday, you're feeling a happy kind of excitement."}}, {"model": "moods.mood", "pk": 22, "fields": {"created": "2024-08-10T20:22:41.137Z", "modified": "2024-08-10T20:22:41.137Z", "uuid": "1056d48e-8615-4caf-b876-97e6e7324086", "title": "Horror", "run_time_seconds": null, "run_time_ticks": null, "description": "Horror is the feeling of dread and anticipation that usually occurs before something frightening is seen, heard, or otherwise experienced. It is also the feeling one gets after coming to an awful realization or experiencing a hideous revelation."}}, {"model": "moods.mood", "pk": 23, "fields": {"created": "2024-08-10T20:23:10.897Z", "modified": "2024-08-10T20:23:10.897Z", "uuid": "42c1f073-3292-4cfd-8a3e-39073f6a13c1", "title": "Interest", "run_time_seconds": null, "run_time_ticks": null, "description": "Interest is a feeling or emotion that causes attention to focus on an object, event, or process. In contemporary psychology of interest,[1] the term is used as a general concept that may encompass other more specific psychological terms, such as curiosity and to a much lesser degree surprise."}}, {"model": "moods.mood", "pk": 24, "fields": {"created": "2024-08-10T20:23:38.584Z", "modified": "2024-08-10T20:23:38.584Z", "uuid": "8a4aa6df-db88-4a85-a6b8-975ca6c588ef", "title": "Nostalgia", "run_time_seconds": null, "run_time_ticks": null, "description": "Nostalgia is triggered by something reminding an individual of an event or item from their past. The resulting emotion can vary from happiness to sorrow. The term \"feeling nostalgic\" is more commonly used to describe pleasurable emotions associated with, or a longing to return to, a particular period of time."}}, {"model": "moods.mood", "pk": 25, "fields": {"created": "2024-08-10T20:24:19.272Z", "modified": "2024-08-10T20:24:19.272Z", "uuid": "fbe92946-988e-4e33-8eaa-f060a5a89d7f", "title": "Relief", "run_time_seconds": null, "run_time_ticks": null, "description": "Relief is a positive emotion experienced when something unpleasant, painful or distressing has not happened or has come to an end.\r\n\r\nOften accompanied with a sigh, which signals emotional transition, relief is universally recognized, and judged as a fundamental emotion."}}, {"model": "moods.mood", "pk": 26, "fields": {"created": "2024-08-10T20:24:52.182Z", "modified": "2024-08-10T20:24:52.182Z", "uuid": "0044a42d-b75d-497d-b887-43b014db9850", "title": "Romance", "run_time_seconds": null, "run_time_ticks": null, "description": "Romance or romantic love is a feeling of love for, or a strong attraction towards another person,and the courtship behaviors undertaken by an individual to express those overall feelings and resultant emotions."}}, {"model": "moods.mood", "pk": 27, "fields": {"created": "2024-08-10T20:25:39.705Z", "modified": "2024-08-10T20:25:39.705Z", "uuid": "185e903f-f958-4230-9604-2ce74d9adf40", "title": "Satisfaction", "run_time_seconds": null, "run_time_ticks": null, "description": "Satisfaction is the pleasure that you feel when you do something or get something that you wanted or needed to do or get."}}] \ No newline at end of file diff --git a/vrobbler/apps/boardgames/models.py b/vrobbler/apps/boardgames/models.py index 8738a9e..6b7eb20 100644 --- a/vrobbler/apps/boardgames/models.py +++ b/vrobbler/apps/boardgames/models.py @@ -12,8 +12,8 @@ from django.urls import reverse from django_extensions.db.models import TimeStampedModel from imagekit.models import ImageSpecField from imagekit.processors import ResizeToFit +from scrobbles.dataclasses import BoardGameLogData from scrobbles.mixins import ScrobblableMixin -from vrobbler.apps.boardgames.bgg import lookup_boardgame_id_from_bgg logger = logging.getLogger(__name__) BNULL = {"blank": True, "null": True} @@ -96,6 +96,10 @@ class BoardGame(ScrobblableMixin): "boardgames:boardgame_detail", kwargs={"slug": self.uuid} ) + @property + def logdata_cls(self): + return BoardGameLogData + def primary_image_url(self) -> str: url = "" if self.cover: diff --git a/vrobbler/apps/lifeevents/models.py b/vrobbler/apps/lifeevents/models.py index b826c48..d711b6c 100644 --- a/vrobbler/apps/lifeevents/models.py +++ b/vrobbler/apps/lifeevents/models.py @@ -2,7 +2,7 @@ from django.apps import apps from django.db import models from django.urls import reverse import pendulum -from scrobbles.dataclasses import LifeEventMetadata +from scrobbles.dataclasses import LifeEventLogData from scrobbles.mixins import ScrobblableMixin BNULL = {"blank": True, "null": True} @@ -22,8 +22,8 @@ class LifeEvent(ScrobblableMixin): ) @property - def metadata_class(self): - return LifeEventMetadata + def logdata_cls(self): + return LifeEventLogData @classmethod def find_or_create(cls, title: str) -> "LifeEvent": diff --git a/vrobbler/apps/moods/admin.py b/vrobbler/apps/moods/admin.py new file mode 100644 index 0000000..42fca46 --- /dev/null +++ b/vrobbler/apps/moods/admin.py @@ -0,0 +1,23 @@ +from django.contrib import admin + +from moods.models import Mood +from scrobbles.admin import ScrobbleInline + + +class MoodInline(admin.TabularInline): + model = Mood + extra = 0 + + +@admin.register(Mood) +class MoodAdmin(admin.ModelAdmin): + date_hierarchy = "created" + list_display = ( + "uuid", + "title", + ) + ordering = ("-created",) + search_fields = ("title",) + inlines = [ + ScrobbleInline, + ] diff --git a/vrobbler/apps/moods/apps.py b/vrobbler/apps/moods/apps.py new file mode 100644 index 0000000..375da96 --- /dev/null +++ b/vrobbler/apps/moods/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class MoodsConfig(AppConfig): + name = "moods" diff --git a/vrobbler/apps/moods/migrations/0001_initial.py b/vrobbler/apps/moods/migrations/0001_initial.py new file mode 100644 index 0000000..202fd4f --- /dev/null +++ b/vrobbler/apps/moods/migrations/0001_initial.py @@ -0,0 +1,79 @@ +# Generated by Django 4.2.13 on 2024-08-10 20:01 + +from django.db import migrations, models +import django_extensions.db.fields +import taggit.managers +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("scrobbles", "0056_scrobble_life_event_alter_scrobble_media_type"), + ] + + operations = [ + migrations.CreateModel( + name="Mood", + 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)), + ( + "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/moods/migrations/__init__.py b/vrobbler/apps/moods/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vrobbler/apps/moods/models.py b/vrobbler/apps/moods/models.py new file mode 100644 index 0000000..390c094 --- /dev/null +++ b/vrobbler/apps/moods/models.py @@ -0,0 +1,28 @@ +import logging +from uuid import uuid4 + +from django.contrib.auth import get_user_model +from django.db import models +from django.urls import reverse +from scrobbles.mixins import ScrobblableMixin +from vrobbler.apps.scrobbles.dataclasses import MoodLogData + +logger = logging.getLogger(__name__) +BNULL = {"blank": True, "null": True} +User = get_user_model() + + +class Mood(ScrobblableMixin): + description = models.TextField(**BNULL) + + def __str__(self): + if self.title: + return self.title + return str(self.uuid) + + def get_absolute_url(self): + return reverse("moods:mood-detail", kwargs={"slug": self.uuid}) + + @property + def logdata_cls(self): + return MoodLogData diff --git a/vrobbler/apps/moods/urls.py b/vrobbler/apps/moods/urls.py new file mode 100644 index 0000000..452f079 --- /dev/null +++ b/vrobbler/apps/moods/urls.py @@ -0,0 +1,14 @@ +from django.urls import path +from moods import views + +app_name = "moods" + + +urlpatterns = [ + path("mood/", views.MoodListView.as_view(), name="mood-list"), + path( + "mood//", + views.MoodDetailView.as_view(), + name="mood-detail", + ), +] diff --git a/vrobbler/apps/moods/views.py b/vrobbler/apps/moods/views.py new file mode 100644 index 0000000..1d78fd8 --- /dev/null +++ b/vrobbler/apps/moods/views.py @@ -0,0 +1,12 @@ +from django.db.models import Count +from django.views import generic +from moods.models import Mood +from scrobbles.views import ScrobbleableListView, ScrobbleableDetailView + + +class MoodListView(ScrobbleableListView): + model = Mood + + +class MoodDetailView(ScrobbleableDetailView): + model = Mood diff --git a/vrobbler/apps/scrobbles/dataclasses.py b/vrobbler/apps/scrobbles/dataclasses.py index b17b475..07c6322 100644 --- a/vrobbler/apps/scrobbles/dataclasses.py +++ b/vrobbler/apps/scrobbles/dataclasses.py @@ -1,3 +1,4 @@ +from functools import cached_property import inspect import json from dataclasses import asdict, dataclass @@ -10,17 +11,17 @@ from locations.models import GeoLocation User = get_user_model() -class ScrobbleMetadataEncoder(json.JSONEncoder): +class ScrobbleLogDataEncoder(json.JSONEncoder): def default(self, o): return o.__dict__ -class ScrobbleMetadataDecoder(json.JSONDecoder): +class ScrobbleLogDataDecoder(json.JSONDecoder): def default(self, o): return o.__dict__ -class JSONMetadata(JSONWizard): +class JSONDataclass(JSONWizard): @property def asdict(self): return asdict(self) @@ -29,19 +30,9 @@ class JSONMetadata(JSONWizard): def json(self): return json.dumps(self.asdict) - @classmethod - def from_dict(cls, env): - return cls( - **{ - k: v - for k, v in env.items() - if k in inspect.signature(cls).parameters - } - ) - @dataclass -class BoardGameScore(JSONMetadata): +class BoardGameScoreLogData(JSONDataclass): user_id: Optional[int] = None name: Optional[str] = None bgg_username: Optional[str] = None @@ -53,19 +44,22 @@ class BoardGameScore(JSONMetadata): @dataclass -class BoardGameMetadata(JSONMetadata): - players: Optional[list[BoardGameScore]] = None +class BoardGameLogData(JSONDataclass): + players: Optional[list[BoardGameScoreLogData]] = None + location: Optional[str] = None + geo_location_id: Optional[int] = None difficulty: Optional[int] = None solo: Optional[bool] = None two_handed: Optional[bool] = None - location: Optional[str] = None - def geo_location(self): - return GeoLocation.objects.filter(id=self.geo_location_id).first() + @cached_property + def geo_location(self) -> Optional[GeoLocation]: + if self.geo_location_id: + return GeoLocation.objects.filter(id=self.geo_location_id).first() @dataclass -class BookPageMetadata(JSONMetadata): +class BookPageLogData(JSONDataclass): page_number: Optional[int] = None end_ts: Optional[int] = None start_ts: Optional[int] = None @@ -73,14 +67,14 @@ class BookPageMetadata(JSONMetadata): @dataclass -class BookMetadata(JSONMetadata): +class BookLogData(JSONDataclass): koreader_hash: Optional[str] pages_read: Optional[int] - page_data: Optional[list[BookPageMetadata]] + page_data: Optional[list[BookPageLogData]] @dataclass -class LifeEventMetadata(JSONMetadata): +class LifeEventLogData(JSONDataclass): participant_user_ids: Optional[list[int]] = None participant_names: Optional[list[str]] = None location: Optional[str] = None @@ -108,7 +102,12 @@ class LifeEventMetadata(JSONMetadata): @dataclass -class VideoMetadata(JSONMetadata): +class MoodLogData(JSONDataclass): + reasons: Optional[str] + + +@dataclass +class VideoMetadata(JSONDataclass): title: str video_type: str run_time_seconds: int diff --git a/vrobbler/apps/scrobbles/migrations/0057_scrobble_mood_alter_scrobble_log_and_more.py b/vrobbler/apps/scrobbles/migrations/0057_scrobble_mood_alter_scrobble_log_and_more.py new file mode 100644 index 0000000..3232908 --- /dev/null +++ b/vrobbler/apps/scrobbles/migrations/0057_scrobble_mood_alter_scrobble_log_and_more.py @@ -0,0 +1,57 @@ +# Generated by Django 4.2.13 on 2024-08-10 20:01 + +from django.db import migrations, models +import django.db.models.deletion +import scrobbles.dataclasses + + +class Migration(migrations.Migration): + + dependencies = [ + ("moods", "0001_initial"), + ("scrobbles", "0056_scrobble_life_event_alter_scrobble_media_type"), + ] + + operations = [ + migrations.AddField( + model_name="scrobble", + name="mood", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + to="moods.mood", + ), + ), + migrations.AlterField( + model_name="scrobble", + name="log", + field=models.JSONField( + blank=True, + decoder=scrobbles.dataclasses.ScrobbleLogDataDecoder, + encoder=scrobbles.dataclasses.ScrobbleLogDataEncoder, + null=True, + ), + ), + 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"), + ("WebPage", "Web Page"), + ("LifeEvent", "Life event"), + ("Mood", "Mood"), + ], + default="Video", + max_length=14, + ), + ), + ] diff --git a/vrobbler/apps/scrobbles/models.py b/vrobbler/apps/scrobbles/models.py index ae9d084..d38b34d 100644 --- a/vrobbler/apps/scrobbles/models.py +++ b/vrobbler/apps/scrobbles/models.py @@ -1,4 +1,3 @@ -import pytz import calendar import datetime import logging @@ -7,6 +6,7 @@ from typing import Iterable, Optional from uuid import uuid4 import pendulum +import pytz from boardgames.models import BoardGame from books.koreader import process_koreader_sqlite_file from books.models import Book @@ -19,7 +19,9 @@ from django.utils.functional import cached_property from django_extensions.db.models import TimeStampedModel from imagekit.models import ImageSpecField from imagekit.processors import ResizeToFit +from lifeevents.models import LifeEvent from locations.models import GeoLocation +from moods.models import Mood from music.lastfm import LastFM from music.models import Artist, Track from podcasts.models import PodcastEpisode @@ -31,6 +33,7 @@ from profiles.utils import ( start_of_month, start_of_week, ) +from scrobbles import dataclasses as logdata from scrobbles.constants import LONG_PLAY_MEDIA from scrobbles.stats import build_charts from scrobbles.utils import media_class_to_foreign_key @@ -38,17 +41,7 @@ from sports.models import SportEvent from videogames import retroarch from videogames.models import VideoGame from videos.models import Series, Video -from scrobbles.dataclasses import ( - BoardGameMetadata, - BookMetadata, - JSONMetadata, - LifeEventMetadata, - ScrobbleMetadataDecoder, - ScrobbleMetadataEncoder, - VideoMetadata, -) from webpages.models import WebPage -from lifeevents.models import LifeEvent from vrobbler.apps.scrobbles.constants import MEDIA_END_PADDING_SECONDS @@ -492,6 +485,7 @@ class Scrobble(TimeStampedModel): GEO_LOCATION = "GeoLocation", "GeoLocation" WEBPAGE = "WebPage", "Web Page" LIFE_EVENT = "LifeEvent", "Life event" + MOOD = "Mood", "Mood" uuid = models.UUIDField(editable=False, **BNULL) video = models.ForeignKey(Video, on_delete=models.DO_NOTHING, **BNULL) @@ -516,6 +510,7 @@ class Scrobble(TimeStampedModel): life_event = models.ForeignKey( LifeEvent, on_delete=models.DO_NOTHING, **BNULL ) + mood = models.ForeignKey(Mood, on_delete=models.DO_NOTHING, **BNULL) media_type = models.CharField( max_length=14, choices=MediaType.choices, default=MediaType.VIDEO ) @@ -537,8 +532,8 @@ class Scrobble(TimeStampedModel): source = models.CharField(max_length=255, **BNULL) log = models.JSONField( **BNULL, - encoder=ScrobbleMetadataEncoder, - decoder=ScrobbleMetadataDecoder, + encoder=logdata.ScrobbleLogDataEncoder, + decoder=logdata.ScrobbleLogDataDecoder, ) timezone = models.CharField(max_length=50, **BNULL) @@ -582,7 +577,8 @@ class Scrobble(TimeStampedModel): # Microseconds mess up Django's filtering, and we don't need be that specific if self.timestamp: self.timestamp = self.timestamp.replace(microsecond=0) - self.media_type = self.MediaType(self.media_obj.__class__.__name__) + if self.media_obj: + self.media_type = self.MediaType(self.media_obj.__class__.__name__) return super(Scrobble, self).save(*args, **kwargs) @@ -608,25 +604,15 @@ class Scrobble(TimeStampedModel): ) @property - def metadata(self) -> Optional[JSONMetadata]: - metadata_cls = None - if self.media_type == self.MediaType.LIFE_EVENT: - metadata_cls = LifeEventMetadata - if self.media_type == self.MediaType.BOARD_GAME: - metadata_cls = BoardGameMetadata - if self.media_type == self.MediaType.VIDEO: - metadata_cls = VideoMetadata - if self.media_type == self.MediaType.BOOK: - metadata_cls = BookMetadata - - if not metadata_cls: + def logdata(self) -> dict: + if not self.media_obj.logdata_cls: logger.warn( - f"Media type has no metadata class", + f"Media type has no log data class", extra={"media_type": self.media_type, "scrobble_id": self.id}, ) - return None + return {} - return metadata_cls.from_dict(self.log) + return self.media_obj.logdata_cls.from_dict(self.log) def redirect_url(self, user_id) -> str: user = User.objects.filter(id=user_id).first() @@ -864,6 +850,8 @@ class Scrobble(TimeStampedModel): media_obj = self.web_page if self.life_event: media_obj = self.life_event + if self.mood: + media_obj = self.mood return media_obj def __str__(self): diff --git a/vrobbler/apps/scrobbles/views.py b/vrobbler/apps/scrobbles/views.py index 2f58e09..55af9cc 100644 --- a/vrobbler/apps/scrobbles/views.py +++ b/vrobbler/apps/scrobbles/views.py @@ -8,7 +8,7 @@ from django.apps import apps from django.conf import settings from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin -from django.db.models import Q +from django.db.models import Count, Q from django.db.models.query import QuerySet from django.http import FileResponse, HttpResponseRedirect, JsonResponse from django.urls import reverse_lazy @@ -60,6 +60,37 @@ from scrobbles.utils import ( logger = logging.getLogger(__name__) +class ScrobbleableListView(ListView): + model = None + paginate_by = 20 + + def get_queryset(self): + queryset = super().get_queryset() + if not self.request.user.is_anonymous: + queryset = queryset.annotate( + scrobble_count=Count("scrobble"), + filter=Q(user=self.request.user), + ).order_by("-scrobble_count") + else: + queryset = queryset.annotate( + scrobble_count=Count("scrobble") + ).order_by("-scrobble_count") + + +class ScrobbleableDetailView(DetailView): + model = None + slug_field = "uuid" + + def get_context_data(self, **kwargs): + context_data = super().get_context_data(**kwargs) + context_data["scrobbles"] = list() + if not self.request.user.is_anonymous: + context_data["scrobbles"] = self.object.scrobbles( + self.request.user + ) + return context_data + + class RecentScrobbleList(ListView): model = Scrobble diff --git a/vrobbler/settings.py b/vrobbler/settings.py index 8bdce1b..245157b 100644 --- a/vrobbler/settings.py +++ b/vrobbler/settings.py @@ -125,6 +125,7 @@ INSTALLED_APPS = [ "locations", "webpages", "lifeevents", + "moods", "mathfilters", "rest_framework", "allauth", diff --git a/vrobbler/urls.py b/vrobbler/urls.py index 7efa1af..d3b087d 100644 --- a/vrobbler/urls.py +++ b/vrobbler/urls.py @@ -13,6 +13,7 @@ from vrobbler.apps.boardgames import urls as boardgame_urls from vrobbler.apps.locations import urls as locations_urls from vrobbler.apps.lifeevents import urls as lifeevents_urls from vrobbler.apps.webpages import urls as webpages_urls +from vrobbler.apps.moods import urls as moods_urls from vrobbler.apps.music.api.views import ( AlbumViewSet, ArtistViewSet, @@ -73,6 +74,7 @@ urlpatterns = [ path("", include(webpages_urls, namespace="webpages")), path("", include(podcast_urls, namespace="podcasts")), path("", include(lifeevents_urls, namespace="life-events")), + path("", include(moods_urls, namespace="moods")), path("", include(scrobble_urls, namespace="scrobbles")), path( "", scrobbles_views.RecentScrobbleList.as_view(), name="vrobbler-home"