[puzzles] Add puzzle model and hooks

This commit is contained in:
2025-05-10 23:36:03 -04:00
parent d066a98282
commit dd71bdd38c
16 changed files with 453 additions and 8 deletions

View File

@ -0,0 +1,28 @@
from puzzles.models import Puzzle, PuzzleManufacturer
from django.contrib import admin
from scrobbles.admin import ScrobbleInline
class PuzzleInline(admin.TabularInline):
model = Puzzle
extra = 0
@admin.register(PuzzleManufacturer)
class PuzzleManufacturerAdmin(admin.ModelAdmin):
date_hierarchy = "created"
search_fields = ("name",)
@admin.register(Puzzle)
class PuzzleAdmin(admin.ModelAdmin):
date_hierarchy = "created"
list_display = (
"uuid",
"title",
)
ordering = ("-created",)
search_fields = ("title",)
inlines = [
ScrobbleInline,
]

View File

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

View File

@ -0,0 +1,163 @@
# Generated by Django 4.2.19 on 2025-05-11 03:21
from django.db import migrations, models
import django.db.models.deletion
import django_extensions.db.fields
import taggit.managers
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
("scrobbles", "0068_scrobble_paper_alter_scrobble_media_type"),
]
operations = [
migrations.CreateModel(
name="PuzzleManufacturer",
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,
),
),
("name", models.CharField(max_length=255)),
(
"ipdb_id",
models.CharField(blank=True, max_length=200, null=True),
),
("description", models.TextField(blank=True, null=True)),
],
options={
"get_latest_by": "modified",
"abstract": False,
},
),
migrations.CreateModel(
name="Puzzle",
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(default=900)),
(
"run_time_ticks",
models.PositiveBigIntegerField(blank=True, null=True),
),
("description", models.TextField(blank=True, null=True)),
(
"orientation",
models.CharField(blank=True, max_length=50, null=True),
),
(
"dimensions_in_inches",
models.CharField(blank=True, max_length=10, null=True),
),
("publish_year", models.IntegerField(blank=True, null=True)),
(
"material",
models.CharField(blank=True, max_length=50, null=True),
),
(
"cut_style",
models.CharField(blank=True, max_length=100, null=True),
),
("pieces_count", models.IntegerField(blank=True, null=True)),
(
"igdb_id",
models.CharField(blank=True, max_length=255, null=True),
),
(
"barcode",
models.CharField(blank=True, max_length=13, null=True),
),
(
"igdb_image",
models.ImageField(
blank=True, null=True, upload_to="puzzles/igdb/"
),
),
(
"genre",
taggit.managers.TaggableManager(
blank=True,
help_text="A comma-separated list of tags.",
through="scrobbles.ObjectWithGenres",
to="scrobbles.Genre",
verbose_name="Tags",
),
),
(
"manufacturer",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
to="puzzles.puzzlemanufacturer",
),
),
],
options={
"abstract": False,
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.19 on 2025-05-11 03:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("puzzles", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="puzzle",
name="dimensions_in_inches",
field=models.CharField(blank=True, max_length=30, null=True),
),
]

View File

@ -0,0 +1,108 @@
from uuid import uuid4
from puzzles.sources import ipdb
from django.apps import apps
from django.db import models
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 PuzzleLogData
from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin
BNULL = {"blank": True, "null": True}
class PuzzleManufacturer(TimeStampedModel):
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
name = models.CharField(max_length=255)
ipdb_id = models.CharField(max_length=200, **BNULL)
description = models.TextField(**BNULL)
def __str__(self) -> str:
return str(self.name)
class Puzzle(ScrobblableMixin):
description = models.TextField(**BNULL)
orientation = models.CharField(max_length=50, **BNULL)
dimensions_in_inches = models.CharField(max_length=30, **BNULL)
publish_year = models.IntegerField(**BNULL)
material = models.CharField(max_length=50, **BNULL)
cut_style = models.CharField(max_length=100, **BNULL)
pieces_count = models.IntegerField(**BNULL)
igdb_id = models.CharField(max_length=255, **BNULL)
barcode = models.CharField(max_length=13, **BNULL)
igdb_image = models.ImageField(upload_to="puzzles/igdb/", **BNULL)
igdb_image_small = ImageSpecField(
source="igdb_image",
processors=[ResizeToFit(100, 100)],
format="JPEG",
options={"quality": 60},
)
igdb_image_medium = ImageSpecField(
source="igdb_image",
processors=[ResizeToFit(300, 300)],
format="JPEG",
options={"quality": 75},
)
manufacturer = models.ForeignKey(
PuzzleManufacturer, on_delete=models.DO_NOTHING, **BNULL
)
def get_absolute_url(self) -> str:
return reverse("puzzles:puzzle_detail", kwargs={"slug": self.uuid})
def __str__(self):
return f"{self.title} ({self.pieces_count}) by {self.manufacturer}"
@property
def subtitle(self):
return self.manufacturer.name
@property
def strings(self) -> ScrobblableConstants:
return ScrobblableConstants(verb="Solving", tags="puzzle")
@property
def igdb_link(self) -> str:
link = ""
if self.igdb_id:
link = f"https://www.ipdb.plus/IPDb/puzzle.php?id={self.igdb_id}"
return link
@property
def primary_image_url(self) -> str:
url = ""
if self.ipdb_image:
url = self.ipdb_image.url
return url
@property
def logdata_cls(self):
return PuzzleLogData
@classmethod
def find_or_create(cls, ipdb_id: str) -> "Puzzle":
puzzle = cls.objects.filter(ipdb_id=ipdb_id).first()
if not puzzle:
puzzle_dict = ipdb.get_puzzle_from_ipdb_id(ipdb_id)
manufacturer_name = puzzle_dict.pop("manufacturer")
manufacturer, _created = PuzzleManufacturer.objects.get_or_create(
name=manufacturer_name
)
ipdb_dict["manufacturer_id"] = manufacturer.id
genres = puzzle_dict.pop("genres")
puzzle = Puzzle.objects.create(**puzzle_dict)
if genres:
puzzle.genre.add(*genres)
return puzzle
def scrobbles(self, user_id):
Scrobble = apps.get_model("scrobbles", "Scrobble")
return Scrobble.objects.filter(user_id=user_id, puzzle=self).order_by(
"-timestamp"
)

View File

@ -0,0 +1,5 @@
#!/usr/bin/env python3
def get_puzzle_from_ipdb_id(ipdb_id: str) -> dict:
return {}

View File

@ -0,0 +1,14 @@
from django.urls import path
from puzzles import views
app_name = "puzzles"
urlpatterns = [
path("puzzles/", views.PuzzleListView.as_view(), name="puzzle_list"),
path(
"puzzles/<slug:slug>/",
views.PuzzleDetailView.as_view(),
name="puzzle_detail",
),
]

View File

@ -0,0 +1,11 @@
from puzzles.models import Puzzle
from scrobbles.views import ScrobbleableListView, ScrobbleableDetailView
class PuzzleListView(ScrobbleableListView):
model = Puzzle
class PuzzleDetailView(ScrobbleableDetailView):
model = Puzzle