@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"]}
--- /dev/null
+# 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"
+ ),
+ ),
+ ]
--- /dev/null
+# 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),
+ ),
+ ]
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,
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',)
"""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()
--- /dev/null
+# 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,
+ ),
+ ]
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
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)
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])
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):
{% 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>
<dc:relation.coverImage.url xmlns:dc="http://purl.org/dc/elements/1.1/">{{ object.use_file.url|build_absolute_uri:request }}</dc:relation.coverImage.url>
-<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 }}</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>
-</textarea>
+<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></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 %}
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
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)
#
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
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)
+
},
"publisher": {
"autocomplete": {
- "source": ["Fundacja Nowoczesna Polska"]
+ "source": ["Fundacja Wolne Lektury"]
}
},