Add audience and thema data to catalogue. Also: nicer cover view.
authorRadek Czajka <rczajka@rczajka.pl>
Tue, 25 Jul 2023 10:57:27 +0000 (12:57 +0200)
committerRadek Czajka <rczajka@rczajka.pl>
Tue, 25 Jul 2023 10:57:27 +0000 (12:57 +0200)
src/catalogue/admin.py
src/catalogue/migrations/0048_audience_alter_thema_options_thema_plural_thema_slug_and_more.py [new file with mode: 0644]
src/catalogue/migrations/0049_thema_public_description.py [new file with mode: 0644]
src/catalogue/models.py
src/cover/forms.py
src/cover/migrations/0006_image_example.py [new file with mode: 0644]
src/cover/models.py
src/cover/templates/cover/image_detail.html
src/cover/views.py
src/documents/models/listeners.py
src/wlxml/views.py

index 77f1078..614450e 100644 (file)
@@ -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 (file)
index 0000000..e9f255a
--- /dev/null
@@ -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 (file)
index 0000000..9ccde99
--- /dev/null
@@ -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),
+        ),
+    ]
index d2befb9..85757a6 100644 (file)
@@ -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',)
index 7a85594..aad80c4 100644 (file)
@@ -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 (file)
index 0000000..2ae53cb
--- /dev/null
@@ -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,
+        ),
+    ]
index 87ac036..d766222 100644 (file)
@@ -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):
index 4b8f73f..2d43f9e 100644 (file)
@@ -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 %}
-<h1>{% trans "Cover image" %}</h1>
+  <h1>{% trans "Cover image" %}</h1>
+  <div class="row">
+    <div class="col-md-5">
 
-<div style="float: right; margin-bottom:1em;">
 
-<a href="{{ object.use_file.url }}"><img style="max-width: 565px; max-height: 833px"
-        src="{{ object.use_file.url }}?{{ object.cut_top }}.{{ object.cut_bottom }}.{{ object.cut_left }}.{{ object.cut_right }}" />
-    </a>
-<br/><a href="{{ object.source_url }}">{{ object.title }}</a> by {{ object.author }},
-    {% if object.license_url %}<a href="{{ object.license_url }}">{% endif %}
-        {{ object.license_name }}
-    {% if object.license_url %}</a>{% endif %}
-</div>
 
 
-{% if editable %}
-    <form method="post" enctype="multipart/form-data">
-    {% csrf_token %}
-{% endif %}
-<table class='editable'><tbody>
-    {{ form.as_table }}
-    {% if editable %}
-        <tr><td></td><td><button type="submit">{% trans "Change" %}</button></td></tr>
-    {% endif %}
-</tbody></table>
-{% if editable %}</form>{% endif %}
-
-
-<h2>{% trans "Used in:" %}</h2>
-{% if object.book_set %}
-<ul>
-    {% for book in object.book_set.all %}
-        <li><a href="{{ book.get_absolute_url }}">{{ book }}</a></li>
-    {% endfor %}
-</ul>
-{% else %}
-    <p>{% trans "None" %}</p>
-{% endif %}
-
-
-<textarea style="width:100%" rows="5">
+
+      {% if editable %}
+        <form method="post" enctype="multipart/form-data">
+          {% csrf_token %}
+      {% endif %}
+      <table class='editable'><tbody>
+        {% bootstrap_form form %}
+        {% if editable %}
+          {% buttons %}
+          <button type="submit" class="btn btn-primary">{% trans "Change" %}</button>
+      {% endbuttons %}
+        {% endif %}
+      </tbody></table>
+      {% if editable %}</form>{% endif %}
+    </div>
+
+
+    <div class="col-md-7">
+      {% with perc=object.cut_percentages %}
+        <a href="{{ object.use_file.url }}" style="position: relative; display: block; width: {{ perc.width }}px; height: {{ perc.height }}px;">
+          {% thumbnail object.file perc.th as th %}
+          <img src="{{ th.url }}" />
+    {% endthumbnail %}
+    {% localize off %}
+    <div style="background: black; opacity: .5; display: block; position: absolute; top:0; left: 0; right: 0; height: {{ perc.top }}%"></div>
+    <div style="background: black; opacity: .5; display: block; position: absolute; bottom:0; left: 0; right: 0; height: {{ perc.bottom }}%"></div>
+    <div style="background: black; opacity: .5; display: block; position: absolute; top: {{ perc.top }}%; bottom: {{ perc.bottom }}%; left: 0; width: {{ perc.left }}%"></div>
+    <div style="background: black; opacity: .5; display: block; position: absolute; top: {{ perc.top }}%; bottom: {{ perc.bottom }}%; right: 0; width: {{ perc.right }}%"></div>
+      {% endlocalize %}
+      {% endwith %}
+        </a>
+        <br/><a href="{{ object.source_url }}">{{ object.title }}</a> by {{ object.author }},
+        {% if object.license_url %}<a href="{{ object.license_url }}">{% endif %}
+          {{ object.license_name }}
+          {% if object.license_url %}</a>{% endif %}
+
+          {% if object.example %}
+            <div class="mt-4">
+              <img src="{{ object.example.url }}?{{ object.etag }}">
+            </div>
+          {% endif %}
+    </div>
+  </div>
+
+
+  <textarea style="width:100%" rows="6" class="form-control mt-4" disabled>
 &lt;dc:relation.coverImage.url xmlns:dc="http://purl.org/dc/elements/1.1/">{{ object.use_file.url|build_absolute_uri:request }}&lt;/dc:relation.coverImage.url>
-&lt;dc:relation.coverImage.attribution xmlns:dc="http://purl.org/dc/elements/1.1/">{% if object.title %}{{ object.title }}, {% endif %}{% if object.author %}{{ object.author }}, {% endif %}{{ object.license_name }}&lt;/dc:relation.coverImage.attribution>
-&lt;dc:relation.coverImage.source xmlns:dc="http://purl.org/dc/elements/1.1/">{{ object.get_full_url }}&lt;/dc:relation.coverImage.source>
-</textarea>
+&lt;dc:relation.coverImage.attribution xmlns:dc="http://purl.org/dc/elements/1.1/">{{ object.attribution }}&lt;/dc:relation.coverImage.attribution>
+&lt;dc:relation.coverImage.source xmlns:dc="http://purl.org/dc/elements/1.1/">{{ object.get_full_url }}&lt;/dc:relation.coverImage.source></textarea>
+
+  <div class="card mt-4">
+    <div class="card-header">
+      <h2>{% trans "Used in:" %}</h2>
+    </div>
+    <div class="card-body">
+      {% if object.book_set.exists %}
+        <ul style="list-style: none; padding: 0; display: flex; gap: 10px;">
+          {% for book in object.book_set.all %}
+            <li>
+              <a href="{{ book.get_absolute_url }}">
+                <img src="{{ book.cover.url }}?{{ object.etag }}" alt="{{ book }}">
+              </a>
+            </li>
+            <li>
+              <a href="{{ book.get_absolute_url }}" title="{{ book }}">
+                <img src="{{ book.cover.url }}?{{ object.etag }}" alt="{{ book }}">
+              </a>
+            </li>
+          {% endfor %}
+        </ul>
+      {% else %}
+        <p>{% trans "None" %}</p>
+      {% endif %}
+  </div>
+</div>
+
+
 {% endblock %}
index d00123c..82bc911 100644 (file)
@@ -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)
index d306e4e..1a022fb 100644 (file)
@@ -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)
+
index ebc837d..f1f5dae 100644 (file)
@@ -119,7 +119,7 @@ VALUE_TYPES = {
     },
     "publisher": {
         "autocomplete": {
-            "source": ["Fundacja Nowoczesna Polska"]
+            "source": ["Fundacja Wolne Lektury"]
         }
     },