From: Radek Czajka Date: Tue, 25 Jul 2023 10:57:27 +0000 (+0200) Subject: Add audience and thema data to catalogue. Also: nicer cover view. X-Git-Url: https://git.mdrn.pl/redakcja.git/commitdiff_plain/87bd94a94dc6cc3dbf47db2bdd17761b3f400138?ds=sidebyside;hp=e96eab03b81641de7b74f22610bb7cd3a2db8432 Add audience and thema data to catalogue. Also: nicer cover view. --- diff --git a/src/catalogue/admin.py b/src/catalogue/admin.py index 77f1078c..614450eb 100644 --- a/src/catalogue/admin.py +++ b/src/catalogue/admin.py @@ -556,6 +556,14 @@ class PlaceAdmin(WikidataAdminMixin, TabbedTranslationAdmin): @admin.register(models.Thema) class ThemaAdmin(admin.ModelAdmin): - list_display = ['code', 'name', 'usable', 'hidden'] - list_filter = ['usable', 'hidden'] - search_fields = ['code', 'name', 'description'] + list_display = ['code', 'name', 'usable', 'hidden', 'woblink_category'] + list_filter = ['usable', 'usable_as_main', 'hidden'] + search_fields = ['code', 'name', 'description', 'public_description'] + prepopulated_fields = {"slug": ["name"]} + + +@admin.register(models.Audience) +class ThemaAdmin(admin.ModelAdmin): + list_display = ['code', 'name', 'thema'] + search_fields = ['code', 'name', 'description', 'thema'] + prepopulated_fields = {"slug": ["name"]} diff --git a/src/catalogue/migrations/0048_audience_alter_thema_options_thema_plural_thema_slug_and_more.py b/src/catalogue/migrations/0048_audience_alter_thema_options_thema_plural_thema_slug_and_more.py new file mode 100644 index 00000000..e9f255a4 --- /dev/null +++ b/src/catalogue/migrations/0048_audience_alter_thema_options_thema_plural_thema_slug_and_more.py @@ -0,0 +1,102 @@ +# Generated by Django 4.1.9 on 2023-07-25 12:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("catalogue", "0047_author_woblink"), + ] + + operations = [ + migrations.CreateModel( + name="Audience", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "code", + models.CharField( + help_text="Techniczny identifyikator. W miarę możliwości nie należy zmieniać.", + max_length=128, + unique=True, + ), + ), + ( + "name", + models.CharField( + help_text="W formie: „dla … (kogo?)”", max_length=1024 + ), + ), + ( + "slug", + models.SlugField( + blank=True, + help_text="Element adresu na WL, w postaci: /dla/slug/. Można zmieniać.", + max_length=255, + null=True, + unique=True, + ), + ), + ("description", models.TextField(blank=True)), + ( + "thema", + models.CharField( + blank=True, + help_text="Odpowiadający kwalifikator Thema.", + max_length=32, + ), + ), + ], + options={ + "ordering": ("code",), + }, + ), + migrations.AlterModelOptions( + name="thema", + options={"ordering": ("code",), "verbose_name_plural": "Thema"}, + ), + migrations.AddField( + model_name="thema", + name="plural", + field=models.CharField( + blank=True, max_length=255, verbose_name="liczba mnoga" + ), + ), + migrations.AddField( + model_name="thema", + name="slug", + field=models.SlugField( + blank=True, + help_text="Element adresu na WL, w postaci: /tag/slug/. Można zmieniać.", + max_length=255, + null=True, + unique=True, + ), + ), + migrations.AddField( + model_name="thema", + name="usable_as_main", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="thema", + name="woblink_category", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name="genre", + name="plural", + field=models.CharField( + blank=True, max_length=255, verbose_name="liczba mnoga" + ), + ), + ] diff --git a/src/catalogue/migrations/0049_thema_public_description.py b/src/catalogue/migrations/0049_thema_public_description.py new file mode 100644 index 00000000..9ccde991 --- /dev/null +++ b/src/catalogue/migrations/0049_thema_public_description.py @@ -0,0 +1,21 @@ +# Generated by Django 4.1.9 on 2023-07-25 12:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "catalogue", + "0048_audience_alter_thema_options_thema_plural_thema_slug_and_more", + ), + ] + + operations = [ + migrations.AddField( + model_name="thema", + name="public_description", + field=models.TextField(blank=True), + ), + ] diff --git a/src/catalogue/models.py b/src/catalogue/models.py index d2befb90..85757a65 100644 --- a/src/catalogue/models.py +++ b/src/catalogue/models.py @@ -217,7 +217,6 @@ class Epoch(Category): class Genre(Category): plural = models.CharField( 'liczba mnoga', max_length=255, blank=True, - help_text='dotyczy gatunków' ) is_epoch_specific = models.BooleanField( default=False, @@ -564,9 +563,43 @@ class BookMonthlyStats(models.Model): class Thema(models.Model): code = models.CharField(max_length=128, unique=True) name = models.CharField(max_length=1024) + slug = models.SlugField( + max_length=255, null=True, blank=True, unique=True, + help_text='Element adresu na WL, w postaci: /tag/slug/. Można zmieniać.' + ) + plural = models.CharField( + 'liczba mnoga', max_length=255, blank=True, + ) description = models.TextField(blank=True) + public_description = models.TextField(blank=True) usable = models.BooleanField() + usable_as_main = models.BooleanField(default=False) hidden = models.BooleanField(default=False) + woblink_category = models.IntegerField(null=True, blank=True) + + class Meta: + ordering = ('code',) + verbose_name_plural = 'Thema' + + +class Audience(models.Model): + code = models.CharField( + max_length=128, unique=True, + help_text='Techniczny identifyikator. W miarę możliwości nie należy zmieniać.' + ) + name = models.CharField( + max_length=1024, + help_text='W formie: „dla … (kogo?)”' + ) + slug = models.SlugField( + max_length=255, null=True, blank=True, unique=True, + help_text='Element adresu na WL, w postaci: /dla/slug/. Można zmieniać.' + ) + description = models.TextField(blank=True) + thema = models.CharField( + max_length=32, blank=True, + help_text='Odpowiadający kwalifikator Thema.' + ) class Meta: ordering = ('code',) diff --git a/src/cover/forms.py b/src/cover/forms.py index 7a855940..aad80c4d 100644 --- a/src/cover/forms.py +++ b/src/cover/forms.py @@ -77,7 +77,7 @@ class ImageEditForm(forms.ModelForm): """Form used for editing a Book.""" class Meta: model = Image - exclude = ['download_url'] + exclude = ['download_url', 'use_file', 'example',] def clean(self): cleaned_data = super(ImageEditForm, self).clean() diff --git a/src/cover/migrations/0006_image_example.py b/src/cover/migrations/0006_image_example.py new file mode 100644 index 00000000..2ae53cb4 --- /dev/null +++ b/src/cover/migrations/0006_image_example.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.9 on 2023-07-24 16:33 + +import cover.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("cover", "0005_alter_image_download_url"), + ] + + operations = [ + migrations.AddField( + model_name="image", + name="example", + field=models.ImageField( + default="", + editable=False, + storage=cover.models.OverwriteStorage(), + upload_to="cover/example", + ), + preserve_default=False, + ), + ] diff --git a/src/cover/models.py b/src/cover/models.py index 87ac0369..d7662227 100644 --- a/src/cover/models.py +++ b/src/cover/models.py @@ -11,6 +11,9 @@ from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django.contrib.sites.models import Site from PIL import Image as PILImage +from librarian.dcparser import BookInfo +from librarian.meta.types.person import Person +from librarian.cover import make_cover from cover.utils import URLOpener @@ -39,6 +42,13 @@ class Image(models.Model): editable=True, verbose_name=_('file for use') ) + + example = models.ImageField( + upload_to='cover/example', + storage=OverwriteStorage(), + editable=False, + ) + cut_top = models.IntegerField(default=0, ) cut_bottom = models.IntegerField(default=0) cut_left = models.IntegerField(default=0) @@ -73,7 +83,28 @@ class Image(models.Model): img, save=False ) + super().save(update_fields=['use_file']) + + self.example.save( + "%d.jpg" % self.pk, + ContentFile(self.build_example().get_bytes()), + save=False + ) + super().save(update_fields=['example']) + + + def build_example(self): + class A: + pass + info = A() + info.authors = [] + info.translators = [] + info.cover_class = None + info.cover_box_position = None + info.title = '?' + info.cover_url = 'file://' + self.use_file.path + return make_cover(info, width=200).output_file() def get_absolute_url(self): return reverse('cover_image', args=[self.id]) @@ -81,6 +112,41 @@ class Image(models.Model): def get_full_url(self): return "http://%s%s" % (Site.objects.get_current().domain, self.get_absolute_url()) + def cut_percentages(self): + img = PILImage.open(self.file) + max_w, max_h = 600, 600 + w, h = img.size + scale = min(max_w / w, max_h / h) + ws, hs = round(w * scale), round(h * scale) + + return { + 'left': 100 * self.cut_left / w, + 'right': 100 * self.cut_right / w, + 'top': 100 * self.cut_top / h, + 'bottom': 100 * self.cut_bottom / h, + 'width': ws, + 'height': hs, + 'th': f'{ws}x{hs}', + } + + @property + def etag(self): + return f'{self.cut_top}.{self.cut_bottom}.{self.cut_left}.{self.cut_right}' + + @property + def attribution(self): + pieces = [] + if self.title: + pieces.append(self.title) + if self.author: + pieces.append(self.author) + if self.license_name: + pieces.append(self.license_name) + if self.source_url: + pieces.append(self.source_url.split('/')[2]) + return ', '.join(pieces) + + @receiver(post_save, sender=Image) def download_image(sender, instance, **kwargs): diff --git a/src/cover/templates/cover/image_detail.html b/src/cover/templates/cover/image_detail.html index 4b8f73f7..2d43f9e0 100644 --- a/src/cover/templates/cover/image_detail.html +++ b/src/cover/templates/cover/image_detail.html @@ -1,53 +1,94 @@ {% extends "documents/base.html" %} -{% load i18n %} +{% load i18n l10n %} {% load thumbnail %} {% load build_absolute_uri from fnp_common %} +{% load bootstrap4 %} {% block titleextra %}{% trans "Cover image" %}{% endblock %} {% block content %} -

{% trans "Cover image" %}

+

{% trans "Cover image" %}

+
+
-
- - -
{{ object.title }} by {{ object.author }}, - {% if object.license_url %}{% endif %} - {{ object.license_name }} - {% if object.license_url %}{% endif %} -
-{% if editable %} -
- {% csrf_token %} -{% endif %} - - {{ form.as_table }} - {% if editable %} - - {% endif %} -
-{% if editable %}
{% endif %} - - -

{% trans "Used in:" %}

-{% if object.book_set %} -
    - {% for book in object.book_set.all %} -
  • {{ book }}
  • - {% endfor %} -
-{% else %} -

{% trans "None" %}

-{% endif %} - - - +<dc:relation.coverImage.attribution xmlns:dc="http://purl.org/dc/elements/1.1/">{{ object.attribution }}</dc:relation.coverImage.attribution> +<dc:relation.coverImage.source xmlns:dc="http://purl.org/dc/elements/1.1/">{{ object.get_full_url }}</dc:relation.coverImage.source> + +
+
+

{% trans "Used in:" %}

+
+
+ {% if object.book_set.exists %} +
    + {% for book in object.book_set.all %} +
  • + + {{ book }} + +
  • +
  • + + {{ book }} + +
  • + {% endfor %} +
+ {% else %} +

{% trans "None" %}

+ {% endif %} +
+
+ + {% endblock %} diff --git a/src/cover/views.py b/src/cover/views.py index d00123cc..82bc911f 100644 --- a/src/cover/views.py +++ b/src/cover/views.py @@ -7,7 +7,7 @@ import os.path import PIL.Image from django.conf import settings from django.contrib.auth.decorators import permission_required -from django.http import HttpResponse, HttpResponseRedirect, Http404 +from django.http import HttpResponse, HttpResponseRedirect, Http404, JsonResponse from django.shortcuts import get_object_or_404, render from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST @@ -103,6 +103,17 @@ def preview_from_xml(request): def image(request, pk): img = get_object_or_404(Image, pk=pk) + if not request.accepts('text/html') and request.accepts('application/json') or request.GET.get('format') == 'json': + return JsonResponse({ + 'attribution': img.attribution, + 'cut_left': img.cut_left, + 'cut_right': img.cut_right, + 'cut_top': img.cut_top, + 'cut_bottom': img.cut_bottom, + 'file': img.file.url, + 'use_file': img.use_file.url, + }) + if request.user.has_perm('cover.change_image'): if request.method == "POST": form = forms.ImageEditForm(request.POST, request.FILES, instance=img) diff --git a/src/documents/models/listeners.py b/src/documents/models/listeners.py index d306e4e9..1a022fb2 100644 --- a/src/documents/models/listeners.py +++ b/src/documents/models/listeners.py @@ -3,6 +3,7 @@ # from django.contrib.auth.models import User from django.db import models +import cover.models from documents.models import (Book, Chunk, Image, BookPublishRecord, ImagePublishRecord) from documents.signals import post_publish @@ -53,3 +54,9 @@ def listener_create(sender, instance, created, **kwargs): instance.chunk_set.create(number=1, slug='1') models.signals.post_save.connect(listener_create, sender=Book) + +def cover_changed(sender, instance, created, **kwargs): + for book in instance.book_set.all(): + book.build_cover() +models.signals.post_save.connect(cover_changed, sender=cover.models.Image) + diff --git a/src/wlxml/views.py b/src/wlxml/views.py index ebc837dc..f1f5dae3 100644 --- a/src/wlxml/views.py +++ b/src/wlxml/views.py @@ -119,7 +119,7 @@ VALUE_TYPES = { }, "publisher": { "autocomplete": { - "source": ["Fundacja Nowoczesna Polska"] + "source": ["Fundacja Wolne Lektury"] } },