[puzzles] Add puzzle model and hooks
This commit is contained in:
28
vrobbler/apps/puzzles/admin.py
Normal file
28
vrobbler/apps/puzzles/admin.py
Normal 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,
|
||||
]
|
||||
5
vrobbler/apps/puzzles/apps.py
Normal file
5
vrobbler/apps/puzzles/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PuzzlesConfig(AppConfig):
|
||||
name = "puzzles"
|
||||
163
vrobbler/apps/puzzles/migrations/0001_initial.py
Normal file
163
vrobbler/apps/puzzles/migrations/0001_initial.py
Normal 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,
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
0
vrobbler/apps/puzzles/migrations/__init__.py
Normal file
0
vrobbler/apps/puzzles/migrations/__init__.py
Normal file
108
vrobbler/apps/puzzles/models.py
Normal file
108
vrobbler/apps/puzzles/models.py
Normal 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"
|
||||
)
|
||||
5
vrobbler/apps/puzzles/sources/ipdb.py
Normal file
5
vrobbler/apps/puzzles/sources/ipdb.py
Normal file
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
def get_puzzle_from_ipdb_id(ipdb_id: str) -> dict:
|
||||
return {}
|
||||
14
vrobbler/apps/puzzles/urls.py
Normal file
14
vrobbler/apps/puzzles/urls.py
Normal 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",
|
||||
),
|
||||
]
|
||||
11
vrobbler/apps/puzzles/views.py
Normal file
11
vrobbler/apps/puzzles/views.py
Normal 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
|
||||
Reference in New Issue
Block a user