diff --git a/ram/bookshelf/admin.py b/ram/bookshelf/admin.py index 4fea47f..d24f6eb 100644 --- a/ram/bookshelf/admin.py +++ b/ram/bookshelf/admin.py @@ -76,7 +76,7 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin): autocomplete_fields = ("authors", "publisher", "shop") readonly_fields = ("invoices", "creation_time", "updated_time") search_fields = ("title", "publisher__name", "authors__last_name") - list_filter = ("publisher__name", "authors") + list_filter = ("publisher__name", "authors", "published") fieldsets = ( ( @@ -239,7 +239,12 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin): autocomplete_fields = ("manufacturer",) readonly_fields = ("invoices", "creation_time", "updated_time") search_fields = ("manufacturer__name", "years", "scales__scale") - list_filter = ("manufacturer__name", "publication_year", "scales__scale") + list_filter = ( + "manufacturer__name", + "publication_year", + "scales__scale", + "published", + ) fieldsets = ( ( @@ -358,8 +363,8 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin): actions = [publish, unpublish, download_csv] -@admin.register(Issue) -class MagazineIssueAdmin(admin.ModelAdmin): +@admin.register(MagazineIssue) +class MagazineIssueAdmin(SortableAdminBase, admin.ModelAdmin): inlines = ( BookPropertyInline, BookImageInline, @@ -370,10 +375,8 @@ class MagazineIssueAdmin(admin.ModelAdmin): "issue_number", "published", ) - # autocomplete_fields = ("publisher",) - # readonly_fields = ("creation_time", "updated_time") - # search_fields = ("title", "publisher__name") - # list_filter = ("publisher__name", "language") + autocomplete_fields = ("shop",) + readonly_fields = ("magazine", "creation_time", "updated_time") def get_model_perms(self, request): """ @@ -381,14 +384,106 @@ class MagazineIssueAdmin(admin.ModelAdmin): """ return {} + fieldsets = ( + ( + None, + { + "fields": ( + "published", + "magazine", + "issue_number", + "publication_year", + "publication_month", + "ISBN", + "language", + "number_of_pages", + "description", + "tags", + ) + }, + ), + ( + "Purchase data", + { + "classes": ("collapse",), + "fields": ( + "shop", + "purchase_date", + "price", + ), + }, + ), + ( + "Notes", + {"classes": ("collapse",), "fields": ("notes",)}, + ), + ( + "Audit", + { + "classes": ("collapse",), + "fields": ( + "creation_time", + "updated_time", + ), + }, + ), + ) actions = [publish, unpublish] +class MagazineIssueInline(admin.StackedInline): + model = MagazineIssue + min_num = 0 + extra = 0 + autocomplete_fields = ("shop",) + show_change_link = True + fieldsets = ( + ( + None, + { + "fields": ( + "published", + "issue_number", + "publication_year", + "publication_month", + ) + }, + ), + ( + "Additional info", + { + "classes": ("collapse",), + "fields": ( + "language", + "number_of_pages", + "ISBN", + "tags", + ), + }, + ), + ( + "Purchase data", + { + "classes": ("collapse",), + "fields": ( + "shop", + "purchase_date", + "price", + ), + }, + ), + ) + + class Media: + js = ('admin/js/magazine_issue_defaults.js',) + + @admin.register(Magazine) -class MagazineAdmin(admin.ModelAdmin): +class MagazineAdmin(SortableAdminBase, admin.ModelAdmin): inlines = ( MagazineIssueInline, ) + list_display = ( "__str__", "publisher", @@ -397,6 +492,37 @@ class MagazineAdmin(admin.ModelAdmin): autocomplete_fields = ("publisher",) readonly_fields = ("creation_time", "updated_time") search_fields = ("name", "publisher__name") - list_filter = ("publisher__name", "language") + list_filter = ("publisher__name", "published") + fieldsets = ( + ( + None, + { + "fields": ( + "published", + "name", + "publisher", + "ISBN", + "language", + "description", + "image", + "tags", + ) + }, + ), + ( + "Notes", + {"classes": ("collapse",), "fields": ("notes",)}, + ), + ( + "Audit", + { + "classes": ("collapse",), + "fields": ( + "creation_time", + "updated_time", + ), + }, + ), + ) actions = [publish, unpublish] diff --git a/ram/bookshelf/migrations/0024_issue_magazine_magazineissue.py b/ram/bookshelf/migrations/0025_magazine_magazineissue.py similarity index 86% rename from ram/bookshelf/migrations/0024_issue_magazine_magazineissue.py rename to ram/bookshelf/migrations/0025_magazine_magazineissue.py index eb4ca91..eaf801d 100644 --- a/ram/bookshelf/migrations/0024_issue_magazine_magazineissue.py +++ b/ram/bookshelf/migrations/0025_magazine_magazineissue.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.8 on 2025-11-13 23:01 +# Generated by Django 6.0 on 2025-12-08 17:47 import bookshelf.models import django.db.models.deletion @@ -11,32 +11,11 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ("bookshelf", "0023_delete_basebookdocument"), + ("bookshelf", "0024_alter_basebook_language"), ("metadata", "0025_alter_company_options_alter_manufacturer_options_and_more"), ] operations = [ - migrations.CreateModel( - name="Issue", - 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", - ), - ), - ("issue_number", models.CharField(max_length=100)), - ], - options={ - "abstract": False, - }, - bases=("bookshelf.basebook",), - ), migrations.CreateModel( name="Magazine", fields=[ @@ -55,12 +34,13 @@ class Migration(migrations.Migration): ("updated_time", models.DateTimeField(auto_now=True)), ("published", models.BooleanField(default=True)), ("name", models.CharField(max_length=200)), + ("ISBN", models.CharField(blank=True, max_length=17)), ( "image", models.ImageField( blank=True, storage=ram.utils.DeduplicatedStorage, - upload_to=bookshelf.models.magazine_image_upload, + upload_to=bookshelf.models.book_image_upload, ), ), ( @@ -108,6 +88,7 @@ class Migration(migrations.Migration): ("hi", "Hindi"), ("hr", "Croatian"), ("hsb", "Upper Sorbian"), + ("ht", "Haitian Creole"), ("hu", "Hungarian"), ("hy", "Armenian"), ("ia", "Interlingua"), @@ -193,34 +174,51 @@ class Migration(migrations.Migration): name="MagazineIssue", fields=[ ( - "id", - models.BigAutoField( + "basebook_ptr", + models.OneToOneField( auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, primary_key=True, serialize=False, - verbose_name="ID", + to="bookshelf.basebook", ), ), + ("issue_number", models.CharField(max_length=100)), ( - "issue", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="magazine_issue", - to="bookshelf.issue", + "publication_month", + models.SmallIntegerField( + blank=True, + choices=[ + (1, "January"), + (2, "February"), + (3, "March"), + (4, "April"), + (5, "May"), + (6, "June"), + (7, "July"), + (8, "August"), + (9, "September"), + (10, "October"), + (11, "November"), + (12, "December"), + ], + null=True, ), ), ( "magazine", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, - related_name="magazine_issue", + related_name="issue", to="bookshelf.magazine", ), ), ], options={ - "ordering": ["magazine", "issue"], - "unique_together": {("magazine", "issue")}, + "ordering": ["magazine", "issue_number"], + "unique_together": {("magazine", "issue_number")}, }, + bases=("bookshelf.basebook",), ), ] diff --git a/ram/bookshelf/models.py b/ram/bookshelf/models.py index c45308b..7c38845 100644 --- a/ram/bookshelf/models.py +++ b/ram/bookshelf/models.py @@ -4,6 +4,7 @@ from django.db import models from django.conf import settings from django.urls import reverse from django.utils.dates import MONTHS +from django.core.exceptions import ValidationError from django_countries.fields import CountryField from ram.utils import DeduplicatedStorage @@ -212,4 +213,11 @@ class MagazineIssue(BaseBook): ordering = ["magazine", "issue_number"] def __str__(self): - return f"{self.magazine.name} - {self.issue.issue_number}" + return f"{self.magazine.name} - {self.issue_number}" + + def clean(self): + if self.magazine.published is False and self.published is True: + raise ValidationError( + "Cannot set an issue as published if the magazine is not " + "published." + ) diff --git a/ram/bookshelf/static/admin/js/magazine_issue_defaults.js b/ram/bookshelf/static/admin/js/magazine_issue_defaults.js new file mode 100644 index 0000000..a400ca7 --- /dev/null +++ b/ram/bookshelf/static/admin/js/magazine_issue_defaults.js @@ -0,0 +1,16 @@ +document.addEventListener('formset:added', function(event) { + const newForm = event.target; // the new inline form element + + const defaultLanguage = document.querySelector('#id_language').value; + const defaultStatus = document.querySelector('#id_published').checked; + + const languageInput = newForm.querySelector('select[name$="language"]'); + const statusInput = newForm.querySelector('input[name$="published"]'); + + if (languageInput) { + languageInput.value = defaultLanguage; + } + if (statusInput) { + statusInput.checked = defaultStatus; + } +}); diff --git a/ram/repository/migrations/0004_magazineissuedocument.py b/ram/repository/migrations/0004_magazineissuedocument.py new file mode 100644 index 0000000..702c0a5 --- /dev/null +++ b/ram/repository/migrations/0004_magazineissuedocument.py @@ -0,0 +1,65 @@ +# Generated by Django 6.0 on 2025-12-08 17:47 + +import django.db.models.deletion +import ram.utils +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookshelf", "0025_magazine_magazineissue"), + ( + "repository", + "0003_alter_bookdocument_file_alter_catalogdocument_file_and_more", + ), + ] + + operations = [ + migrations.CreateModel( + name="MagazineIssueDocument", + 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/" + ), + ), + ("creation_time", models.DateTimeField(auto_now_add=True)), + ("updated_time", models.DateTimeField(auto_now=True)), + ( + "private", + models.BooleanField( + default=False, + help_text="Document will be visible only to logged users", + ), + ), + ( + "issue", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="document", + to="bookshelf.magazineissue", + ), + ), + ], + options={ + "verbose_name_plural": "Magazines documents", + "constraints": [ + models.UniqueConstraint( + fields=("issue", "file"), name="unique_issue_file" + ) + ], + }, + ), + ] diff --git a/ram/repository/models.py b/ram/repository/models.py index 5be856c..08e25d2 100644 --- a/ram/repository/models.py +++ b/ram/repository/models.py @@ -5,7 +5,7 @@ from tinymce import models as tinymce from ram.models import PrivateDocument from metadata.models import Decoder, Shop, Tag from roster.models import RollingStock -from bookshelf.models import Book, Catalog, Issue +from bookshelf.models import Book, Catalog, MagazineIssue class GenericDocument(PrivateDocument): @@ -78,7 +78,7 @@ class CatalogDocument(PrivateDocument): class MagazineIssueDocument(PrivateDocument): issue = models.ForeignKey( - Issue, on_delete=models.CASCADE, related_name="document" + MagazineIssue, on_delete=models.CASCADE, related_name="document" ) class Meta: