From b8572c1701165b3190f91d9da40b27de7a92ce94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Sun, 1 Oct 2023 21:35:14 +0200 Subject: [PATCH 01/10] Rename SKU to 'Item number' --- ram/portal/templates/cards.html | 4 ++-- ram/portal/templates/rollingstock.html | 8 ++++---- ram/portal/views.py | 2 +- ram/roster/admin.py | 10 +++++----- ...0018_rename_sku_rollingstock_item_number.py | 18 ++++++++++++++++++ ram/roster/models.py | 2 +- 6 files changed, 31 insertions(+), 13 deletions(-) create mode 100644 ram/roster/migrations/0018_rename_sku_rollingstock_item_number.py diff --git a/ram/portal/templates/cards.html b/ram/portal/templates/cards.html index 1ba79c5..4c106d6 100644 --- a/ram/portal/templates/cards.html +++ b/ram/portal/templates/cards.html @@ -68,8 +68,8 @@ {{ d.scale }} - SKU - {{ d.sku }} + Item number + {{ d.item_number }} diff --git a/ram/portal/templates/rollingstock.html b/ram/portal/templates/rollingstock.html index 5329dca..fd8827a 100644 --- a/ram/portal/templates/rollingstock.html +++ b/ram/portal/templates/rollingstock.html @@ -105,8 +105,8 @@ {{ rolling_stock.scale }} - SKU - {{ rolling_stock.sku }} + Item number + {{ rolling_stock.item_number }} @@ -155,8 +155,8 @@ {{ rolling_stock.scale }} - SKU - {{ rolling_stock.sku }} + Item number + {{ rolling_stock.item_number }} Era diff --git a/ram/portal/views.py b/ram/portal/views.py index 26cfd2b..b7f56df 100644 --- a/ram/portal/views.py +++ b/ram/portal/views.py @@ -80,7 +80,7 @@ class SearchRoster(View): | Q(rolling_class__description__icontains=s) | Q(rolling_class__type__type__icontains=s) | Q(road_number__icontains=s) - | Q(sku=s) + | Q(item_number=s) | Q(rolling_class__company__name__icontains=s) | Q(rolling_class__company__country__icontains=s) | Q(manufacturer__name__icontains=s) diff --git a/ram/roster/admin.py b/ram/roster/admin.py index 3343c9a..6322f75 100644 --- a/ram/roster/admin.py +++ b/ram/roster/admin.py @@ -68,7 +68,7 @@ class RollingStockDocumentAdmin(admin.ModelAdmin): ) search_fields = ( "rolling_stock__rolling_class__identifier", - "rolling_stock__sku", + "rolling_stock__item_number", "description", "file", ) @@ -89,7 +89,7 @@ class RollingJournalDocumentAdmin(admin.ModelAdmin): search_fields = ( "rolling_stock__rolling_class__identifier", "rolling_stock__road_number", - "rolling_stock__sku", + "rolling_stock__item_number", "log", ) @@ -108,7 +108,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin): "address", "manufacturer", "scale", - "sku", + "item_number", "company", "country", ) @@ -125,7 +125,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin): "manufacturer__name", "road_number", "address", - "sku", + "item_number", ) save_as = True @@ -138,7 +138,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin): "road_number", "scale", "manufacturer", - "sku", + "item_number", "era", "production_year", "purchase_date", diff --git a/ram/roster/migrations/0018_rename_sku_rollingstock_item_number.py b/ram/roster/migrations/0018_rename_sku_rollingstock_item_number.py new file mode 100644 index 0000000..45feed6 --- /dev/null +++ b/ram/roster/migrations/0018_rename_sku_rollingstock_item_number.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.5 on 2023-10-01 19:32 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("roster", "0017_remove_rollingstockimage_is_thumbnail"), + ] + + operations = [ + migrations.RenameField( + model_name="rollingstock", + old_name="sku", + new_name="item_number", + ), + ] diff --git a/ram/roster/models.py b/ram/roster/models.py index 8b99b9c..e2d336e 100644 --- a/ram/roster/models.py +++ b/ram/roster/models.py @@ -82,7 +82,7 @@ class RollingStock(models.Model): limit_choices_to={"category": "model"}, ) scale = models.ForeignKey(Scale, on_delete=models.CASCADE) - sku = models.CharField(max_length=32, blank=True) + item_number = models.CharField(max_length=32, blank=True) decoder_interface = models.PositiveSmallIntegerField( choices=settings.DECODER_INTERFACES, null=True, blank=True ) From 968ebeb0b6397720a77b8f0634b62c94c6e96413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Mon, 2 Oct 2023 00:02:24 +0200 Subject: [PATCH 02/10] First bookshelf implementation --- ram/bookshelf/__init__.py | 0 ram/bookshelf/admin.py | 43 ++++++ ram/bookshelf/apps.py | 6 + ram/bookshelf/migrations/0001_initial.py | 119 +++++++++++++++ ...language_book_numbers_of_pages_and_more.py | 142 ++++++++++++++++++ ram/bookshelf/migrations/__init__.py | 0 ram/bookshelf/models.py | 80 ++++++++++ ram/bookshelf/tests.py | 3 + ram/bookshelf/views.py | 3 + ram/ram/__init__.py | 2 +- ram/ram/settings.py | 1 + 11 files changed, 398 insertions(+), 1 deletion(-) create mode 100644 ram/bookshelf/__init__.py create mode 100644 ram/bookshelf/admin.py create mode 100644 ram/bookshelf/apps.py create mode 100644 ram/bookshelf/migrations/0001_initial.py create mode 100644 ram/bookshelf/migrations/0002_book_language_book_numbers_of_pages_and_more.py create mode 100644 ram/bookshelf/migrations/__init__.py create mode 100644 ram/bookshelf/models.py create mode 100644 ram/bookshelf/tests.py create mode 100644 ram/bookshelf/views.py diff --git a/ram/bookshelf/__init__.py b/ram/bookshelf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ram/bookshelf/admin.py b/ram/bookshelf/admin.py new file mode 100644 index 0000000..71deb73 --- /dev/null +++ b/ram/bookshelf/admin.py @@ -0,0 +1,43 @@ +from django.contrib import admin + +from bookshelf.models import BookProperty, Book, Author, Publisher + + +class BookPropertyInline(admin.TabularInline): + model = BookProperty + min_num = 0 + extra = 0 + + +@admin.register(Book) +class BookAdmin(admin.ModelAdmin): + inlines = (BookPropertyInline,) + list_display = ( + "title", + "get_authors", + "get_publisher", + "publication_year", + "numbers_of_pages" + ) + search_fields = ("title",) + list_filter = ("publisher__name",) + + @admin.display(description="Publisher") + def get_publisher(self, obj): + return obj.publisher.name + + @admin.display(description="Authors") + def get_authors(self, obj): + return ", ".join(a.short_name() for a in obj.authors.all()) + + +@admin.register(Author) +class AuthorAdmin(admin.ModelAdmin): + search_fields = ("first_name", "last_name",) + list_filter = ("last_name",) + + +@admin.register(Publisher) +class PublisherAdmin(admin.ModelAdmin): + list_display = ("name", "country") + search_fields = ("name",) diff --git a/ram/bookshelf/apps.py b/ram/bookshelf/apps.py new file mode 100644 index 0000000..49fa81a --- /dev/null +++ b/ram/bookshelf/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class BookshelfConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "bookshelf" diff --git a/ram/bookshelf/migrations/0001_initial.py b/ram/bookshelf/migrations/0001_initial.py new file mode 100644 index 0000000..61c8b35 --- /dev/null +++ b/ram/bookshelf/migrations/0001_initial.py @@ -0,0 +1,119 @@ +# Generated by Django 4.2.5 on 2023-10-01 20:16 + +import ckeditor_uploader.fields +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("metadata", "0012_alter_decoder_manufacturer_decoderdocument"), + ] + + operations = [ + migrations.CreateModel( + name="Author", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("first_name", models.CharField(max_length=100)), + ("last_name", models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name="Book", + fields=[ + ( + "uuid", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("title", models.CharField(max_length=200)), + ("ISBN", models.CharField(max_length=13, unique=True)), + ("publication_year", models.SmallIntegerField(blank=True, null=True)), + ("purchase_date", models.DateField(blank=True, null=True)), + ("notes", ckeditor_uploader.fields.RichTextUploadingField(blank=True)), + ("creation_time", models.DateTimeField(auto_now_add=True)), + ("updated_time", models.DateTimeField(auto_now=True)), + ("authors", models.ManyToManyField(to="bookshelf.author")), + ], + ), + migrations.CreateModel( + name="Publisher", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=200)), + ("website", models.URLField()), + ], + ), + migrations.CreateModel( + name="BookProperty", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("value", models.CharField(max_length=256)), + ( + "book", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="property", + to="bookshelf.book", + ), + ), + ( + "property", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="metadata.property", + ), + ), + ], + options={ + "verbose_name_plural": "Properties", + }, + ), + migrations.AddField( + model_name="book", + name="publisher", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="bookshelf.publisher" + ), + ), + migrations.AddField( + model_name="book", + name="tags", + field=models.ManyToManyField( + blank=True, related_name="bookshelf", to="metadata.tag" + ), + ), + ] diff --git a/ram/bookshelf/migrations/0002_book_language_book_numbers_of_pages_and_more.py b/ram/bookshelf/migrations/0002_book_language_book_numbers_of_pages_and_more.py new file mode 100644 index 0000000..ed2fb25 --- /dev/null +++ b/ram/bookshelf/migrations/0002_book_language_book_numbers_of_pages_and_more.py @@ -0,0 +1,142 @@ +# Generated by Django 4.2.5 on 2023-10-01 21:33 + +from django.db import migrations, models +import django_countries.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookshelf", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="book", + name="language", + field=models.CharField( + choices=[ + ("af", "Afrikaans"), + ("ar", "Arabic"), + ("ar-dz", "Algerian Arabic"), + ("ast", "Asturian"), + ("az", "Azerbaijani"), + ("bg", "Bulgarian"), + ("be", "Belarusian"), + ("bn", "Bengali"), + ("br", "Breton"), + ("bs", "Bosnian"), + ("ca", "Catalan"), + ("ckb", "Central Kurdish (Sorani)"), + ("cs", "Czech"), + ("cy", "Welsh"), + ("da", "Danish"), + ("de", "German"), + ("dsb", "Lower Sorbian"), + ("el", "Greek"), + ("en", "English"), + ("en-au", "Australian English"), + ("en-gb", "British English"), + ("eo", "Esperanto"), + ("es", "Spanish"), + ("es-ar", "Argentinian Spanish"), + ("es-co", "Colombian Spanish"), + ("es-mx", "Mexican Spanish"), + ("es-ni", "Nicaraguan Spanish"), + ("es-ve", "Venezuelan Spanish"), + ("et", "Estonian"), + ("eu", "Basque"), + ("fa", "Persian"), + ("fi", "Finnish"), + ("fr", "French"), + ("fy", "Frisian"), + ("ga", "Irish"), + ("gd", "Scottish Gaelic"), + ("gl", "Galician"), + ("he", "Hebrew"), + ("hi", "Hindi"), + ("hr", "Croatian"), + ("hsb", "Upper Sorbian"), + ("hu", "Hungarian"), + ("hy", "Armenian"), + ("ia", "Interlingua"), + ("id", "Indonesian"), + ("ig", "Igbo"), + ("io", "Ido"), + ("is", "Icelandic"), + ("it", "Italian"), + ("ja", "Japanese"), + ("ka", "Georgian"), + ("kab", "Kabyle"), + ("kk", "Kazakh"), + ("km", "Khmer"), + ("kn", "Kannada"), + ("ko", "Korean"), + ("ky", "Kyrgyz"), + ("lb", "Luxembourgish"), + ("lt", "Lithuanian"), + ("lv", "Latvian"), + ("mk", "Macedonian"), + ("ml", "Malayalam"), + ("mn", "Mongolian"), + ("mr", "Marathi"), + ("ms", "Malay"), + ("my", "Burmese"), + ("nb", "Norwegian Bokmål"), + ("ne", "Nepali"), + ("nl", "Dutch"), + ("nn", "Norwegian Nynorsk"), + ("os", "Ossetic"), + ("pa", "Punjabi"), + ("pl", "Polish"), + ("pt", "Portuguese"), + ("pt-br", "Brazilian Portuguese"), + ("ro", "Romanian"), + ("ru", "Russian"), + ("sk", "Slovak"), + ("sl", "Slovenian"), + ("sq", "Albanian"), + ("sr", "Serbian"), + ("sr-latn", "Serbian Latin"), + ("sv", "Swedish"), + ("sw", "Swahili"), + ("ta", "Tamil"), + ("te", "Telugu"), + ("tg", "Tajik"), + ("th", "Thai"), + ("tk", "Turkmen"), + ("tr", "Turkish"), + ("tt", "Tatar"), + ("udm", "Udmurt"), + ("uk", "Ukrainian"), + ("ur", "Urdu"), + ("uz", "Uzbek"), + ("vi", "Vietnamese"), + ("zh-hans", "Simplified Chinese"), + ("zh-hant", "Traditional Chinese"), + ], + default="en", + max_length=7, + ), + ), + migrations.AddField( + model_name="book", + name="numbers_of_pages", + field=models.SmallIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="publisher", + name="country", + field=django_countries.fields.CountryField(blank=True, max_length=2), + ), + migrations.AlterField( + model_name="book", + name="ISBN", + field=models.CharField(blank=True, max_length=13), + ), + migrations.AlterField( + model_name="publisher", + name="website", + field=models.URLField(blank=True), + ), + ] diff --git a/ram/bookshelf/migrations/__init__.py b/ram/bookshelf/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ram/bookshelf/models.py b/ram/bookshelf/models.py new file mode 100644 index 0000000..d151fdf --- /dev/null +++ b/ram/bookshelf/models.py @@ -0,0 +1,80 @@ +from uuid import uuid4 +from django.db import models +from django.conf import settings +from django_countries.fields import CountryField + +from ckeditor_uploader.fields import RichTextUploadingField + +from metadata.models import ( + Property, + Tag, +) + + +class Publisher(models.Model): + name = models.CharField(max_length=200) + country = CountryField(blank=True) + website = models.URLField(blank=True) + + def __str__(self): + return self.name + + +class Author(models.Model): + first_name = models.CharField(max_length=100) + last_name = models.CharField(max_length=100) + + def __str__(self): + return f"{self.last_name}, {self.first_name}" + + def short_name(self): + return f"{self.last_name} {self.first_name[0]}." + + +class Book(models.Model): + uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False) + title = models.CharField(max_length=200) + authors = models.ManyToManyField(Author) + publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE) + ISBN = models.CharField(max_length=13, blank=True) + language = models.CharField( + max_length=7, + choices=settings.LANGUAGES, + default='en' + ) + numbers_of_pages = models.SmallIntegerField(null=True, blank=True) + publication_year = models.SmallIntegerField(null=True, blank=True) + purchase_date = models.DateField(null=True, blank=True) + tags = models.ManyToManyField( + Tag, related_name="bookshelf", blank=True + ) + notes = RichTextUploadingField(blank=True) + creation_time = models.DateTimeField(auto_now_add=True) + updated_time = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.title + + def publisher_name(self): + return self.publisher.name + + # def get_absolute_url(self): + # return reverse("rolling_stock", kwargs={"uuid": self.uuid}) + + +class BookProperty(models.Model): + book = models.ForeignKey( + Book, + on_delete=models.CASCADE, + null=False, + blank=False, + related_name="property", + ) + property = models.ForeignKey(Property, on_delete=models.CASCADE) + value = models.CharField(max_length=256) + + def __str__(self): + return self.property.name + + class Meta: + verbose_name_plural = "Properties" diff --git a/ram/bookshelf/tests.py b/ram/bookshelf/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/ram/bookshelf/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/ram/bookshelf/views.py b/ram/bookshelf/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/ram/bookshelf/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/ram/ram/__init__.py b/ram/ram/__init__.py index d83f9cb..6754000 100644 --- a/ram/ram/__init__.py +++ b/ram/ram/__init__.py @@ -1,4 +1,4 @@ from ram.utils import git_suffix -__version__ = "0.4.3" +__version__ = "0.5.0" __version__ += git_suffix(__file__) diff --git a/ram/ram/settings.py b/ram/ram/settings.py index de654ce..8d2c340 100644 --- a/ram/ram/settings.py +++ b/ram/ram/settings.py @@ -55,6 +55,7 @@ INSTALLED_APPS = [ "metadata", "roster", "consist", + "bookshelf", ] MIDDLEWARE = [ From 3f905877e7ca9c581f835e52fe15af9fcdfede4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Mon, 2 Oct 2023 22:19:04 +0200 Subject: [PATCH 03/10] Extend the bookshelf implementation --- ram/bookshelf/admin.py | 19 +++++--- ram/bookshelf/migrations/0003_bookimage.py | 51 ++++++++++++++++++++++ ram/bookshelf/models.py | 29 ++++++------ ram/bookshelf/serializers.py | 26 +++++++++++ ram/bookshelf/urls.py | 7 +++ ram/bookshelf/views.py | 19 +++++++- ram/ram/models.py | 36 ++++++++++++++- ram/ram/urls.py | 1 + ram/roster/models.py | 43 +++--------------- 9 files changed, 172 insertions(+), 59 deletions(-) create mode 100644 ram/bookshelf/migrations/0003_bookimage.py create mode 100644 ram/bookshelf/serializers.py create mode 100644 ram/bookshelf/urls.py diff --git a/ram/bookshelf/admin.py b/ram/bookshelf/admin.py index 71deb73..34027e0 100644 --- a/ram/bookshelf/admin.py +++ b/ram/bookshelf/admin.py @@ -1,6 +1,15 @@ from django.contrib import admin +from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin -from bookshelf.models import BookProperty, Book, Author, Publisher +from bookshelf.models import BookProperty, BookImage, Book, Author, Publisher + + +class BookImageInline(SortableInlineAdminMixin, admin.TabularInline): + model = BookImage + min_num = 0 + extra = 0 + readonly_fields = ("image_thumbnail",) + classes = ["collapse"] class BookPropertyInline(admin.TabularInline): @@ -10,8 +19,8 @@ class BookPropertyInline(admin.TabularInline): @admin.register(Book) -class BookAdmin(admin.ModelAdmin): - inlines = (BookPropertyInline,) +class BookAdmin(SortableAdminBase, admin.ModelAdmin): + inlines = (BookImageInline, BookPropertyInline,) list_display = ( "title", "get_authors", @@ -19,8 +28,8 @@ class BookAdmin(admin.ModelAdmin): "publication_year", "numbers_of_pages" ) - search_fields = ("title",) - list_filter = ("publisher__name",) + search_fields = ("title", "publisher__name", "authors__last_name") + list_filter = ("publisher__name", "authors") @admin.display(description="Publisher") def get_publisher(self, obj): diff --git a/ram/bookshelf/migrations/0003_bookimage.py b/ram/bookshelf/migrations/0003_bookimage.py new file mode 100644 index 0000000..b272b79 --- /dev/null +++ b/ram/bookshelf/migrations/0003_bookimage.py @@ -0,0 +1,51 @@ +# Generated by Django 4.2.5 on 2023-10-02 10:36 + +from django.db import migrations, models +import django.db.models.deletion +import ram.utils + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookshelf", "0002_book_language_book_numbers_of_pages_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="BookImage", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("order", models.PositiveIntegerField(default=0)), + ( + "image", + models.ImageField( + blank=True, + null=True, + storage=ram.utils.DeduplicatedStorage, + upload_to="images/books/", + ), + ), + ( + "book", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="image", + to="bookshelf.book", + ), + ), + ], + options={ + "ordering": ["order"], + "abstract": False, + }, + ), + ] diff --git a/ram/bookshelf/models.py b/ram/bookshelf/models.py index d151fdf..c170be8 100644 --- a/ram/bookshelf/models.py +++ b/ram/bookshelf/models.py @@ -5,10 +5,9 @@ from django_countries.fields import CountryField from ckeditor_uploader.fields import RichTextUploadingField -from metadata.models import ( - Property, - Tag, -) +from metadata.models import Tag +from ram.utils import DeduplicatedStorage +from ram.models import Image, PropertyInstance class Publisher(models.Model): @@ -62,7 +61,19 @@ class Book(models.Model): # return reverse("rolling_stock", kwargs={"uuid": self.uuid}) -class BookProperty(models.Model): +class BookImage(Image): + book = models.ForeignKey( + Book, on_delete=models.CASCADE, related_name="image" + ) + image = models.ImageField( + upload_to="images/books/", # FIXME, find a better way to replace this + storage=DeduplicatedStorage, + null=True, + blank=True + ) + + +class BookProperty(PropertyInstance): book = models.ForeignKey( Book, on_delete=models.CASCADE, @@ -70,11 +81,3 @@ class BookProperty(models.Model): blank=False, related_name="property", ) - property = models.ForeignKey(Property, on_delete=models.CASCADE) - value = models.CharField(max_length=256) - - def __str__(self): - return self.property.name - - class Meta: - verbose_name_plural = "Properties" diff --git a/ram/bookshelf/serializers.py b/ram/bookshelf/serializers.py new file mode 100644 index 0000000..d331a9c --- /dev/null +++ b/ram/bookshelf/serializers.py @@ -0,0 +1,26 @@ +from rest_framework import serializers +from bookshelf.models import Book, Author, Publisher +from metadata.serializers import TagSerializer + + +class AuthorSerializer(serializers.ModelSerializer): + class Meta: + model = Author + fields = "__all__" + + +class PublisherSerializer(serializers.ModelSerializer): + class Meta: + model = Publisher + fields = "__all__" + + +class BookSerializer(serializers.ModelSerializer): + authors = AuthorSerializer(many=True) + publisher = PublisherSerializer() + tags = TagSerializer(many=True) + + class Meta: + model = Book + fields = "__all__" + read_only_fields = ("creation_time", "updated_time") diff --git a/ram/bookshelf/urls.py b/ram/bookshelf/urls.py new file mode 100644 index 0000000..c9479af --- /dev/null +++ b/ram/bookshelf/urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from bookshelf.views import BookList, BookGet + +urlpatterns = [ + path("book/list", BookList.as_view()), + path("book/get/", BookGet.as_view()), +] diff --git a/ram/bookshelf/views.py b/ram/bookshelf/views.py index 91ea44a..57268b7 100644 --- a/ram/bookshelf/views.py +++ b/ram/bookshelf/views.py @@ -1,3 +1,18 @@ -from django.shortcuts import render +from rest_framework.generics import ListAPIView, RetrieveAPIView +from rest_framework.schemas.openapi import AutoSchema -# Create your views here. +from bookshelf.models import Book +from bookshelf.serializers import BookSerializer + + +class BookList(ListAPIView): + queryset = Book.objects.all() + serializer_class = BookSerializer + + +class BookGet(RetrieveAPIView): + queryset = Book.objects.all() + serializer_class = BookSerializer + lookup_field = "uuid" + + schema = AutoSchema(operation_id_base="retrieveBookByUUID") diff --git a/ram/ram/models.py b/ram/ram/models.py index b370728..59749ad 100644 --- a/ram/ram/models.py +++ b/ram/ram/models.py @@ -3,7 +3,7 @@ import os from django.db import models from django.utils.safestring import mark_safe -from ram.utils import DeduplicatedStorage +from ram.utils import DeduplicatedStorage, get_image_preview class Document(models.Model): @@ -28,3 +28,37 @@ class Document(models.Model): return mark_safe( 'Link'.format(self.file.url) ) + + +class Image(models.Model): + order = models.PositiveIntegerField(default=0, blank=False, null=False) + image = models.ImageField( + upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True + ) + + def image_thumbnail(self): + return get_image_preview(self.image.url) + + image_thumbnail.short_description = "Preview" + + def __str__(self): + return "{0}".format(os.path.basename(self.image.name)) + + class Meta: + abstract = True + ordering = ["order"] + + +class PropertyInstance(models.Model): + property = models.ForeignKey( + "metadata.Property", # To avoid circular dependencies + on_delete=models.CASCADE + ) + value = models.CharField(max_length=256) + + def __str__(self): + return self.property.name + + class Meta: + abstract = True + verbose_name_plural = "Properties" diff --git a/ram/ram/urls.py b/ram/ram/urls.py index 12114b6..7760faf 100644 --- a/ram/ram/urls.py +++ b/ram/ram/urls.py @@ -28,6 +28,7 @@ urlpatterns = [ path("api/v1/consist/", include("consist.urls")), path("api/v1/roster/", include("roster.urls")), path("api/v1/dcc/", include("driver.urls")), + path("api/v1/bookshelf/", include("bookshelf.urls")), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) if settings.DEBUG: diff --git a/ram/roster/models.py b/ram/roster/models.py index e2d336e..1573c4c 100644 --- a/ram/roster/models.py +++ b/ram/roster/models.py @@ -1,4 +1,3 @@ -import os import re from uuid import uuid4 from django.db import models @@ -8,10 +7,9 @@ from django.dispatch import receiver from ckeditor_uploader.fields import RichTextUploadingField -from ram.utils import DeduplicatedStorage, get_image_preview -from ram.models import Document +from ram.models import Document, Image, PropertyInstance +from ram.utils import get_image_preview from metadata.models import ( - Property, Scale, Manufacturer, Decoder, @@ -43,7 +41,7 @@ class RollingClass(models.Model): return "{0} {1}".format(self.company, self.identifier) -class RollingClassProperty(models.Model): +class RollingClassProperty(PropertyInstance): rolling_class = models.ForeignKey( RollingClass, on_delete=models.CASCADE, @@ -52,14 +50,6 @@ class RollingClassProperty(models.Model): related_name="property", verbose_name="Class", ) - property = models.ForeignKey(Property, on_delete=models.CASCADE) - value = models.CharField(max_length=256) - - def __str__(self): - return self.property.name - - class Meta: - verbose_name_plural = "Properties" class RollingStock(models.Model): @@ -136,28 +126,13 @@ class RollingStockDocument(Document): unique_together = ("rolling_stock", "file") -class RollingStockImage(models.Model): - order = models.PositiveIntegerField(default=0, blank=False, null=False) +class RollingStockImage(Image): rolling_stock = models.ForeignKey( RollingStock, on_delete=models.CASCADE, related_name="image" ) - image = models.ImageField( - upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True - ) - - def image_thumbnail(self): - return get_image_preview(self.image.url) - - image_thumbnail.short_description = "Preview" - - def __str__(self): - return "{0}".format(os.path.basename(self.image.name)) - - class Meta: - ordering = ["order"] -class RollingStockProperty(models.Model): +class RollingStockProperty(PropertyInstance): rolling_stock = models.ForeignKey( RollingStock, on_delete=models.CASCADE, @@ -165,14 +140,6 @@ class RollingStockProperty(models.Model): null=False, blank=False, ) - property = models.ForeignKey(Property, on_delete=models.CASCADE) - value = models.CharField(max_length=256) - - def __str__(self): - return self.property.name - - class Meta: - verbose_name_plural = "Properties" class RollingStockJournal(models.Model): From 996ddd67ea2c4757cffb8ad6d14f06e86af1e792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Mon, 2 Oct 2023 22:58:15 +0200 Subject: [PATCH 04/10] Web bookshelf first draft --- ram/bookshelf/admin.py | 2 +- ...e_numbers_of_pages_book_number_of_pages.py | 18 +++ ram/bookshelf/models.py | 5 +- ram/portal/templates/books.html | 104 ++++++++++++++++++ ram/portal/urls.py | 3 + ram/portal/views.py | 8 ++ 6 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 ram/bookshelf/migrations/0004_rename_numbers_of_pages_book_number_of_pages.py create mode 100644 ram/portal/templates/books.html diff --git a/ram/bookshelf/admin.py b/ram/bookshelf/admin.py index 34027e0..1393380 100644 --- a/ram/bookshelf/admin.py +++ b/ram/bookshelf/admin.py @@ -26,7 +26,7 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin): "get_authors", "get_publisher", "publication_year", - "numbers_of_pages" + "number_of_pages" ) search_fields = ("title", "publisher__name", "authors__last_name") list_filter = ("publisher__name", "authors") diff --git a/ram/bookshelf/migrations/0004_rename_numbers_of_pages_book_number_of_pages.py b/ram/bookshelf/migrations/0004_rename_numbers_of_pages_book_number_of_pages.py new file mode 100644 index 0000000..fed0bab --- /dev/null +++ b/ram/bookshelf/migrations/0004_rename_numbers_of_pages_book_number_of_pages.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.5 on 2023-10-02 20:38 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookshelf", "0003_bookimage"), + ] + + operations = [ + migrations.RenameField( + model_name="book", + old_name="numbers_of_pages", + new_name="number_of_pages", + ), + ] diff --git a/ram/bookshelf/models.py b/ram/bookshelf/models.py index c170be8..107dcd7 100644 --- a/ram/bookshelf/models.py +++ b/ram/bookshelf/models.py @@ -1,6 +1,7 @@ from uuid import uuid4 from django.db import models from django.conf import settings +from django.urls import reverse from django_countries.fields import CountryField from ckeditor_uploader.fields import RichTextUploadingField @@ -41,7 +42,7 @@ class Book(models.Model): choices=settings.LANGUAGES, default='en' ) - numbers_of_pages = models.SmallIntegerField(null=True, blank=True) + number_of_pages = models.SmallIntegerField(null=True, blank=True) publication_year = models.SmallIntegerField(null=True, blank=True) purchase_date = models.DateField(null=True, blank=True) tags = models.ManyToManyField( @@ -58,7 +59,7 @@ class Book(models.Model): return self.publisher.name # def get_absolute_url(self): - # return reverse("rolling_stock", kwargs={"uuid": self.uuid}) + # return reverse("books", kwargs={"uuid": self.uuid}) class BookImage(Image): diff --git a/ram/portal/templates/books.html b/ram/portal/templates/books.html new file mode 100644 index 0000000..8b332a9 --- /dev/null +++ b/ram/portal/templates/books.html @@ -0,0 +1,104 @@ +{% extends "cards.html" %} + + {% block cards %} + {% for d in data %} +
+
+ {% if d.image.all %} + {# FIXME #} + {% for r in d.image.all %} + {% if forloop.first %}Card image cap{% endif %} + {% endfor %} + {# FIXME #} + {% endif %} +
+

+ {{ d }} + {# #} +

+ {% if d.tags.all %} +

Tags: + {% for t in d.tags.all %} + {{ t.name }}{# new line is required #} + {% endfor %} +

+ {% endif %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Book
Authors +
    {% for a in d.authors.all %}
  • {{ a }}
  • {% endfor %}
+
Publisher{{ d.publisher }}
Language{{ d.get_language_display }}
Pages{{ d.number_of_pages }}
Year{{ d.publication_year }}
+
+ {# FIXME Show all data #} + {% if request.user.is_staff %}Edit{% endif %} +
+
+
+
+ {% endfor %} + {% endblock %} + {% block pagination %} + {% if data.has_other_pages %} + + {% endif %} + {% endblock %} diff --git a/ram/portal/urls.py b/ram/portal/urls.py index 73b2355..fcc07a9 100644 --- a/ram/portal/urls.py +++ b/ram/portal/urls.py @@ -12,6 +12,7 @@ from portal.views import ( Manufacturers, Scales, Types, + Books, SearchRoster, ) @@ -54,6 +55,8 @@ urlpatterns = [ path("scales/", Types.as_view(), name="scales_pagination"), path("types", Types.as_view(), name="types"), path("types/", Types.as_view(), name="types_pagination"), + path("books", Books.as_view(), name="books"), + path("books/", Books.as_view(), name="books_pagination"), path( "search", SearchRoster.as_view(http_method_names=["post"]), diff --git a/ram/portal/views.py b/ram/portal/views.py index b7f56df..759a130 100644 --- a/ram/portal/views.py +++ b/ram/portal/views.py @@ -14,6 +14,7 @@ from portal.utils import get_site_conf from portal.models import Flatpage from roster.models import RollingStock from consist.models import Consist +from bookshelf.models import Book from metadata.models import Company, Manufacturer, Scale, RollingStockType, Tag @@ -340,6 +341,13 @@ class Types(GetData): self.data = RollingStockType.objects.all() +class Books(GetData): + def __init__(self): + self.title = "Books" + self.template = "books.html" + self.data = Book.objects.all() + + class GetFlatpage(View): def get(self, request, flatpage): try: From 98c696b2d934fe637bea55ad9b5f2cb782f759a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Mon, 2 Oct 2023 23:01:43 +0200 Subject: [PATCH 05/10] Add Books in the main menu --- ram/portal/templates/base.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ram/portal/templates/base.html b/ram/portal/templates/base.html index 572a3e5..0667dea 100644 --- a/ram/portal/templates/base.html +++ b/ram/portal/templates/base.html @@ -172,6 +172,9 @@ {% show_menu %} + {% include 'includes/search.html' %} From 22bee7d95d1973c4ad863140457c0caebe8732f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Mon, 2 Oct 2023 23:16:54 +0200 Subject: [PATCH 06/10] Show booshelf menu --- ram/portal/templates/base.html | 6 ++---- ram/portal/templates/bookshelf_menu.html | 10 ++++++++++ ram/portal/templates/flatpage_menu.html | 4 ++-- ram/portal/templatetags/show_menu.py | 12 +++++++++--- 4 files changed, 23 insertions(+), 9 deletions(-) create mode 100644 ram/portal/templates/bookshelf_menu.html diff --git a/ram/portal/templates/base.html b/ram/portal/templates/base.html index 0667dea..cf47f37 100644 --- a/ram/portal/templates/base.html +++ b/ram/portal/templates/base.html @@ -171,10 +171,8 @@
  • Real
  • - {% show_menu %} - + {% show_flatpage_menu %} + {% show_bookshelf_menu %} {% include 'includes/search.html' %} diff --git a/ram/portal/templates/bookshelf_menu.html b/ram/portal/templates/bookshelf_menu.html new file mode 100644 index 0000000..6d209a8 --- /dev/null +++ b/ram/portal/templates/bookshelf_menu.html @@ -0,0 +1,10 @@ + {% if bookshelf_menu %} + + {% endif %} diff --git a/ram/portal/templates/flatpage_menu.html b/ram/portal/templates/flatpage_menu.html index 193fa08..af5fc49 100644 --- a/ram/portal/templates/flatpage_menu.html +++ b/ram/portal/templates/flatpage_menu.html @@ -1,10 +1,10 @@ - {% if menu %} + {% if flatpage_menu %}