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..1393380 --- /dev/null +++ b/ram/bookshelf/admin.py @@ -0,0 +1,52 @@ +from django.contrib import admin +from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin + +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): + model = BookProperty + min_num = 0 + extra = 0 + + +@admin.register(Book) +class BookAdmin(SortableAdminBase, admin.ModelAdmin): + inlines = (BookImageInline, BookPropertyInline,) + list_display = ( + "title", + "get_authors", + "get_publisher", + "publication_year", + "number_of_pages" + ) + search_fields = ("title", "publisher__name", "authors__last_name") + list_filter = ("publisher__name", "authors") + + @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/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/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/migrations/0005_alter_book_options.py b/ram/bookshelf/migrations/0005_alter_book_options.py new file mode 100644 index 0000000..6c493cf --- /dev/null +++ b/ram/bookshelf/migrations/0005_alter_book_options.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.5 on 2023-10-03 19:57 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookshelf", "0004_rename_numbers_of_pages_book_number_of_pages"), + ] + + operations = [ + migrations.AlterModelOptions( + name="book", + options={ + "ordering": ["authors__last_name", "title"], + "verbose_name_plural": "Rolling stock", + }, + ), + ] 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..721ad90 --- /dev/null +++ b/ram/bookshelf/models.py @@ -0,0 +1,88 @@ +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 + +from metadata.models import Tag +from ram.utils import DeduplicatedStorage +from ram.models import Image, PropertyInstance + + +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' + ) + 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( + 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) + + class Meta: + ordering = ["authors__last_name", "title"] + verbose_name_plural = "Rolling stock" + + 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}) + + +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, + null=False, + blank=False, + related_name="property", + ) 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/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/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 new file mode 100644 index 0000000..57268b7 --- /dev/null +++ b/ram/bookshelf/views.py @@ -0,0 +1,18 @@ +from rest_framework.generics import ListAPIView, RetrieveAPIView +from rest_framework.schemas.openapi import AutoSchema + +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/portal/templates/base.html b/ram/portal/templates/base.html index 572a3e5..dfb0d26 100644 --- a/ram/portal/templates/base.html +++ b/ram/portal/templates/base.html @@ -171,7 +171,8 @@
  • Real
  • - {% show_menu %} + {% show_flatpages_menu %} + {% show_bookshelf_menu %} {% include 'includes/search.html' %} @@ -191,7 +192,7 @@
    {% block carousel %} {% endblock %} - + {% block cards_layout %} {% endblock %}
    diff --git a/ram/portal/templates/bookshelf/book.html b/ram/portal/templates/bookshelf/book.html new file mode 100644 index 0000000..5731e69 --- /dev/null +++ b/ram/portal/templates/bookshelf/book.html @@ -0,0 +1,125 @@ +{% extends 'base.html' %} + + {% block header %} + {% if book.tags.all %} +

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

    + {% endif %} + Updated {{ book.updated_time | date:"M d, Y H:i" }} + {% endblock %} + {% block carousel %} +
    + + {% endblock %} + {% block cards %} + {% endblock %} + {% block extra_content %} +
    +
    +
    + + +
    + {% if request.user.is_staff %}Edit{% endif %} +
    +
    +
    +
    + {% endblock %} diff --git a/ram/portal/templates/bookshelf/books.html b/ram/portal/templates/bookshelf/books.html new file mode 100644 index 0000000..c2cf53b --- /dev/null +++ b/ram/portal/templates/bookshelf/books.html @@ -0,0 +1,104 @@ +{% extends "cards.html" %} + + {% block cards %} + {% for d in data %} +
    +
    + {% if d.image.all %} + + {% for r in d.image.all %} + {% if forloop.first %}Card image cap{% endif %} + {% endfor %} + + {% 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|default:"-" }}
    Year{{ d.publication_year|default:"-" }}
    +
    + 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/templates/bookshelf/bookshelf_menu.html b/ram/portal/templates/bookshelf/bookshelf_menu.html new file mode 100644 index 0000000..9c67e19 --- /dev/null +++ b/ram/portal/templates/bookshelf/bookshelf_menu.html @@ -0,0 +1,10 @@ + {% if bookshelf_menu %} + + {% endif %} 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/companies.html b/ram/portal/templates/companies.html index 31e3cfc..4b52fe2 100644 --- a/ram/portal/templates/companies.html +++ b/ram/portal/templates/companies.html @@ -56,7 +56,7 @@