diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index e6054d3..4c3e7ee 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -13,7 +13,7 @@ jobs: strategy: max-parallel: 2 matrix: - python-version: ['3.10', '3.11', '3.12'] + python-version: ['3.12', '3.13'] steps: - uses: actions/checkout@v3 diff --git a/ram/bookshelf/admin.py b/ram/bookshelf/admin.py index 5615472..c44a239 100644 --- a/ram/bookshelf/admin.py +++ b/ram/bookshelf/admin.py @@ -1,35 +1,58 @@ from django.contrib import admin from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin -from bookshelf.models import BookProperty, BookImage, Book, Author, Publisher +from bookshelf.models import ( + BaseBookProperty, + BaseBookImage, + BaseBookDocument, + Book, + Author, + Publisher, + Catalog, +) class BookImageInline(SortableInlineAdminMixin, admin.TabularInline): - model = BookImage + model = BaseBookImage min_num = 0 extra = 0 readonly_fields = ("image_thumbnail",) classes = ["collapse"] + verbose_name = "Image" + + +class BookDocInline(admin.TabularInline): + model = BaseBookDocument + min_num = 0 + extra = 0 + classes = ["collapse"] class BookPropertyInline(admin.TabularInline): - model = BookProperty + model = BaseBookProperty min_num = 0 extra = 0 autocomplete_fields = ("property",) + verbose_name = "Property" + verbose_name_plural = "Properties" @admin.register(Book) class BookAdmin(SortableAdminBase, admin.ModelAdmin): - inlines = (BookImageInline, BookPropertyInline,) + inlines = ( + BookPropertyInline, + BookImageInline, + BookDocInline, + ) list_display = ( "title", - "published", "get_authors", "get_publisher", "publication_year", - "number_of_pages" + "number_of_pages", + "published", ) + autocomplete_fields = ("authors", "publisher") readonly_fields = ("creation_time", "updated_time") search_fields = ("title", "publisher__name", "authors__last_name") list_filter = ("publisher__name", "authors") @@ -77,7 +100,10 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin): @admin.register(Author) class AuthorAdmin(admin.ModelAdmin): - search_fields = ("first_name", "last_name",) + search_fields = ( + "first_name", + "last_name", + ) list_filter = ("last_name",) @@ -85,3 +111,58 @@ class AuthorAdmin(admin.ModelAdmin): class PublisherAdmin(admin.ModelAdmin): list_display = ("name", "country") search_fields = ("name",) + + +@admin.register(Catalog) +class CatalogAdmin(SortableAdminBase, admin.ModelAdmin): + inlines = ( + BookPropertyInline, + BookImageInline, + BookDocInline, + ) + list_display = ( + "manufacturer", + "years", + "get_scales", + "published", + ) + autocomplete_fields = ("manufacturer",) + readonly_fields = ("creation_time", "updated_time") + search_fields = ("manufacturer__name", "years", "scales__scale") + list_filter = ("manufacturer__name", "publication_year", "scales__scale") + + fieldsets = ( + ( + None, + { + "fields": ( + "published", + "manufacturer", + "years", + "scales", + "ISBN", + "language", + "number_of_pages", + "publication_year", + "description", + "purchase_date", + "notes", + "tags", + ) + }, + ), + ( + "Audit", + { + "classes": ("collapse",), + "fields": ( + "creation_time", + "updated_time", + ), + }, + ), + ) + + @admin.display(description="Scales") + def get_scales(self, obj): + return "/".join(s.scale for s in obj.scales.all()) diff --git a/ram/bookshelf/migrations/0009_alter_bookimage_image.py b/ram/bookshelf/migrations/0009_alter_bookimage_image.py index 0a50733..df3e9e8 100644 --- a/ram/bookshelf/migrations/0009_alter_bookimage_image.py +++ b/ram/bookshelf/migrations/0009_alter_bookimage_image.py @@ -12,7 +12,7 @@ from django.conf import settings def move_images(apps, schema_editor): sys.stdout.write("\n Processing files. Please await...") - for r in bookshelf.models.BookImage.objects.all(): + for r in bookshelf.models.BaseBookImage.objects.all(): fname = os.path.basename(r.image.path) new_image = bookshelf.models.book_image_upload(r, fname) new_path = os.path.join(settings.MEDIA_ROOT, new_image) @@ -31,19 +31,21 @@ class Migration(migrations.Migration): ("bookshelf", "0008_alter_author_options_alter_publisher_options"), ] + # Migration is stale and shouldn't be used since model hes been heavily + # modified since then. Leaving it here for reference. operations = [ - migrations.AlterField( - model_name="bookimage", - name="image", - field=models.ImageField( - blank=True, - null=True, - storage=ram.utils.DeduplicatedStorage, - upload_to=bookshelf.models.book_image_upload, - ), - ), - migrations.RunPython( - move_images, - reverse_code=migrations.RunPython.noop - ), + # migrations.AlterField( + # model_name="bookimage", + # name="image", + # field=models.ImageField( + # blank=True, + # null=True, + # storage=ram.utils.DeduplicatedStorage, + # upload_to=bookshelf.models.book_image_upload, + # ), + # ), + # migrations.RunPython( + # move_images, + # reverse_code=migrations.RunPython.noop + # ), ] diff --git a/ram/bookshelf/migrations/0016_basebook_book_catalogue.py b/ram/bookshelf/migrations/0016_basebook_book_catalogue.py new file mode 100644 index 0000000..5e44540 --- /dev/null +++ b/ram/bookshelf/migrations/0016_basebook_book_catalogue.py @@ -0,0 +1,141 @@ +# Generated by Django 5.1.2 on 2024-11-27 16:35 + +import django.db.models.deletion +from django.db import migrations, models + + +def basebook_to_book(apps, schema_editor): + basebook = apps.get_model("bookshelf", "BaseBook") + book = apps.get_model("bookshelf", "Book") + for row in basebook.objects.all(): + b = book.objects.create( + basebook_ptr=row, + title=row.old_title, + publisher=row.old_publisher, + ) + b.authors.set(row.old_authors.all()) + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookshelf", "0015_alter_book_authors"), + ("metadata", "0019_alter_scale_gauge"), + ] + + operations = [ + migrations.AlterModelOptions( + name="Book", + options={"ordering": ["creation_time"]}, + ), + migrations.RenameModel( + old_name="BookImage", + new_name="BaseBookImage", + ), + migrations.RenameModel( + old_name="BookProperty", + new_name="BaseBookProperty", + ), + migrations.RenameModel( + old_name="Book", + new_name="BaseBook", + ), + migrations.RenameField( + model_name="basebook", + old_name="title", + new_name="old_title", + ), + migrations.RenameField( + model_name="basebook", + old_name="authors", + new_name="old_authors", + ), + migrations.RenameField( + model_name="basebook", + old_name="publisher", + new_name="old_publisher", + ), + migrations.AlterModelOptions( + name="basebookimage", + options={"ordering": ["order"], "verbose_name_plural": "Images"}, + ), + migrations.CreateModel( + name="Book", + fields=[ + ( + "basebook_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="bookshelf.basebook", + ), + ), + ("title", models.CharField(max_length=200)), + ( + "authors", + models.ManyToManyField( + blank=True, + to="bookshelf.author" + ), + ), + ( + "publisher", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="bookshelf.publisher" + ), + ), + ], + options={ + "ordering": ["title"], + }, + ), + migrations.RunPython( + basebook_to_book, + reverse_code=migrations.RunPython.noop + ), + migrations.RemoveField( + model_name="basebook", + name="old_title", + ), + migrations.RemoveField( + model_name="basebook", + name="old_authors", + ), + migrations.RemoveField( + model_name="basebook", + name="old_publisher", + ), + migrations.CreateModel( + name="Catalog", + fields=[ + ( + "basebook_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="bookshelf.basebook", + ), + ), + ("years", models.CharField(max_length=12)), + ( + "manufacturer", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="metadata.manufacturer", + ), + ), + ("scales", models.ManyToManyField(to="metadata.scale")), + ], + options={ + "ordering": ["manufacturer", "publication_year"], + }, + bases=("bookshelf.basebook",), + ), + ] diff --git a/ram/bookshelf/migrations/0017_alter_basebook_options_basebookdocument.py b/ram/bookshelf/migrations/0017_alter_basebook_options_basebookdocument.py new file mode 100644 index 0000000..b05aa6e --- /dev/null +++ b/ram/bookshelf/migrations/0017_alter_basebook_options_basebookdocument.py @@ -0,0 +1,52 @@ +# Generated by Django 5.1.2 on 2024-12-22 20:38 + +import django.db.models.deletion +import ram.utils +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookshelf", "0016_basebook_book_catalogue"), + ] + + operations = [ + migrations.AlterModelOptions( + name="basebook", + options={}, + ), + migrations.CreateModel( + name="BaseBookDocument", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("description", models.CharField(blank=True, max_length=128)), + ( + "file", + models.FileField( + storage=ram.utils.DeduplicatedStorage(), upload_to="files/" + ), + ), + ("private", models.BooleanField(default=False)), + ( + "book", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="document", + to="bookshelf.basebook", + ), + ), + ], + options={ + "unique_together": {("book", "file")}, + }, + ), + ] diff --git a/ram/bookshelf/migrations/0018_alter_basebookdocument_options.py b/ram/bookshelf/migrations/0018_alter_basebookdocument_options.py new file mode 100644 index 0000000..5e3adff --- /dev/null +++ b/ram/bookshelf/migrations/0018_alter_basebookdocument_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.4 on 2024-12-22 20:44 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookshelf", "0017_alter_basebook_options_basebookdocument"), + ] + + operations = [ + migrations.AlterModelOptions( + name="basebookdocument", + options={"verbose_name_plural": "Documents"}, + ), + ] diff --git a/ram/bookshelf/models.py b/ram/bookshelf/models.py index 46e036b..b5c0e9a 100644 --- a/ram/bookshelf/models.py +++ b/ram/bookshelf/models.py @@ -9,7 +9,8 @@ from tinymce import models as tinymce from metadata.models import Tag from ram.utils import DeduplicatedStorage -from ram.models import BaseModel, Image, PropertyInstance +from ram.models import BaseModel, Image, Document, PropertyInstance +from metadata.models import Scale, Manufacturer class Publisher(models.Model): @@ -38,10 +39,7 @@ class Author(models.Model): return f"{self.last_name} {self.first_name[0]}." -class Book(BaseModel): - title = models.CharField(max_length=200) - authors = models.ManyToManyField(Author, blank=True) - publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE) +class BaseBook(BaseModel): ISBN = models.CharField(max_length=17, blank=True) # 13 + dashes language = models.CharField( max_length=7, @@ -56,18 +54,6 @@ class Book(BaseModel): Tag, related_name="bookshelf", blank=True ) - class Meta: - ordering = ["title"] - - def __str__(self): - return self.title - - def publisher_name(self): - return self.publisher.name - - def get_absolute_url(self): - return reverse("book", kwargs={"uuid": self.uuid}) - def delete(self, *args, **kwargs): shutil.rmtree( os.path.join( @@ -75,7 +61,7 @@ class Book(BaseModel): ), ignore_errors=True ) - super(Book, self).delete(*args, **kwargs) + super(BaseBook, self).delete(*args, **kwargs) def book_image_upload(instance, filename): @@ -87,9 +73,9 @@ def book_image_upload(instance, filename): ) -class BookImage(Image): +class BaseBookImage(Image): book = models.ForeignKey( - Book, on_delete=models.CASCADE, related_name="image" + BaseBook, on_delete=models.CASCADE, related_name="image" ) image = models.ImageField( upload_to=book_image_upload, @@ -97,11 +83,68 @@ class BookImage(Image): ) -class BookProperty(PropertyInstance): +class BaseBookDocument(Document): book = models.ForeignKey( - Book, + BaseBook, on_delete=models.CASCADE, related_name="document" + ) + + class Meta: + verbose_name_plural = "Documents" + unique_together = ("book", "file") + + +class BaseBookProperty(PropertyInstance): + book = models.ForeignKey( + BaseBook, on_delete=models.CASCADE, null=False, blank=False, related_name="property", ) + + +class Book(BaseBook): + title = models.CharField(max_length=200) + authors = models.ManyToManyField(Author, blank=True) + publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE) + + class Meta: + ordering = ["title"] + + def __str__(self): + return self.title + + def publisher_name(self): + return self.publisher.name + + def get_absolute_url(self): + return reverse( + "bookshelf_item", + kwargs={"selector": "book", "uuid": self.uuid} + ) + + +class Catalog(BaseBook): + manufacturer = models.ForeignKey( + Manufacturer, + on_delete=models.CASCADE, + ) + years = models.CharField(max_length=12) + scales = models.ManyToManyField(Scale) + + class Meta: + ordering = ["manufacturer", "publication_year"] + + def __str__(self): + scales = self.get_scales + return "%s %s %s" % (self.manufacturer.name, self.years, scales) + + def get_absolute_url(self): + return reverse( + "bookshelf_item", + kwargs={"selector": "catalog", "uuid": self.uuid} + ) + + @property + def get_scales(self): + return "/".join([s.scale for s in self.scales.all()]) diff --git a/ram/portal/templates/base.html b/ram/portal/templates/base.html index 9d64635..a860f83 100644 --- a/ram/portal/templates/base.html +++ b/ram/portal/templates/base.html @@ -180,7 +180,7 @@
Catalog | + {% elif d.type == "book" %}Book | + {% endif %}||
---|---|---|---|
Manufacturer | +{{ d.item.manufacturer }} | +||
Scales | +{{ d.item.get_scales }} | +||
Authors | @@ -32,6 +47,7 @@ | Publisher | {{ d.item.publisher }} |
Language | {{ d.item.get_language_display }} | @@ -48,7 +64,7 @@