From db255ed9d242c56b010061fbdef8de9b696869d4 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Thu, 14 Jul 2022 11:28:28 +0200 Subject: [PATCH 01/16] Legimi button! --- src/depot/legimi.py | 4 +- src/depot/management/commands/depot.py | 10 ++++ ...02_legimibookpublish_legimichunkpublish.py | 38 ++++++++++++++ src/depot/models.py | 52 +++++++++++++++++++ src/depot/urls.py | 11 ++++ src/depot/views.py | 18 ++++++- src/documents/models/book.py | 3 ++ .../templates/documents/book_detail.html | 29 +++++++++-- src/redakcja/urls.py | 1 + 9 files changed, 158 insertions(+), 8 deletions(-) create mode 100644 src/depot/management/commands/depot.py create mode 100644 src/depot/migrations/0002_legimibookpublish_legimichunkpublish.py create mode 100644 src/depot/urls.py diff --git a/src/depot/legimi.py b/src/depot/legimi.py index 99f70624..1810ec57 100644 --- a/src/depot/legimi.py +++ b/src/depot/legimi.py @@ -175,8 +175,8 @@ class Legimi: # success: true # model.Url - def send_book(self, book): - wlbook = book.wldocument(librarian2=True) + def send_book(self, book, changes=None): + wlbook = book.wldocument(librarian2=True, changes=changes) meta = wlbook.meta cover = LabelMarquiseCover(meta, width=1200).output_file() diff --git a/src/depot/management/commands/depot.py b/src/depot/management/commands/depot.py new file mode 100644 index 00000000..4b8b99af --- /dev/null +++ b/src/depot/management/commands/depot.py @@ -0,0 +1,10 @@ +from django.core.management.base import BaseCommand +from depot.models import LegimiBookPublish + + +class Command(BaseCommand): + def handle(self, **options): + for p in LegimiBookPublish.objects.filter(status=0).order_by('created_at'): + print(p, p.book.slug, p.created_at) + p.publish() + diff --git a/src/depot/migrations/0002_legimibookpublish_legimichunkpublish.py b/src/depot/migrations/0002_legimibookpublish_legimichunkpublish.py new file mode 100644 index 00000000..38266060 --- /dev/null +++ b/src/depot/migrations/0002_legimibookpublish_legimichunkpublish.py @@ -0,0 +1,38 @@ +# Generated by Django 3.2.12 on 2022-07-14 10:36 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('documents', '0008_book_legimi_id'), + ('depot', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='LegimiBookPublish', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField()), + ('started_at', models.DateTimeField(blank=True, null=True)), + ('finished_at', models.DateTimeField(blank=True, null=True)), + ('status', models.PositiveSmallIntegerField(choices=[(0, 'queued'), (10, 'running'), (100, 'done'), (110, 'error')], default=0)), + ('error', models.TextField(blank=True)), + ('book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='documents.book')), + ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='LegimiChunkPublish', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('book_publish', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='depot.legimibookpublish')), + ('change', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='documents.chunkchange')), + ], + ), + ] diff --git a/src/depot/models.py b/src/depot/models.py index e3b6dfd8..b68b5365 100644 --- a/src/depot/models.py +++ b/src/depot/models.py @@ -1,11 +1,15 @@ import json import os import tempfile +import traceback import zipfile from datetime import datetime +from django.conf import settings from django.db import models +from django.utils.timezone import now from librarian.cover import make_cover from librarian.builders import EpubBuilder, MobiBuilder +from .legimi import legimi class Package(models.Model): @@ -115,3 +119,51 @@ class Package(models.Model): fname, output.get_bytes() ) + + +class LegimiBookPublish(models.Model): + book = models.ForeignKey('documents.Book', models.CASCADE) + user = models.ForeignKey(settings.AUTH_USER_MODEL, models.SET_NULL, null=True) + created_at = models.DateTimeField() + started_at = models.DateTimeField(null=True, blank=True) + finished_at = models.DateTimeField(null=True, blank=True) + status = models.PositiveSmallIntegerField(choices=[ + (0, 'queued'), + (10, 'running'), + (100, 'done'), + (110, 'error'), + ], default=0) + error = models.TextField(blank=True) + + @classmethod + def create_for(cls, book, user): + book.assert_publishable() + changes = book.get_current_changes(publishable=True) + me = cls.objects.create(book=book, user=user, created_at=now()) + for change in changes: + me.legimichunkpublish_set.create(change=change) + return me + + def publish(self): + self.status = 10 + self.started_at = now() + self.save(update_fields=['status', 'started_at']) + try: + changes = [ + p.change for p in + self.legimichunkpublish_set.order_by('change__chunk__number') + ] + legimi.send_book(self.book, changes=changes) + except Exception: + self.status = 110 + self.error = traceback.format_exc() + else: + self.status = 100 + self.error = '' + self.finished_at = now() + self.save(update_fields=['status', 'finished_at', 'error']) + + +class LegimiChunkPublish(models.Model): + book_publish = models.ForeignKey(LegimiBookPublish, models.CASCADE) + change = models.ForeignKey('documents.ChunkChange', models.CASCADE) diff --git a/src/depot/urls.py b/src/depot/urls.py new file mode 100644 index 00000000..98045158 --- /dev/null +++ b/src/depot/urls.py @@ -0,0 +1,11 @@ +from django.urls import path +from . import views + + +urlpatterns = [ + path( + 'legimi-publish//', + views.LegimiPublishView.as_view(), + name='depot_legimi_publish' + ) +] diff --git a/src/depot/views.py b/src/depot/views.py index 91ea44a2..5e468b67 100644 --- a/src/depot/views.py +++ b/src/depot/views.py @@ -1,3 +1,17 @@ -from django.shortcuts import render +from django.contrib.auth.mixins import PermissionRequiredMixin +from django.shortcuts import get_object_or_404, redirect +from django.views import View +from documents.models import Book +from . import models -# Create your views here. + +class LegimiPublishView(PermissionRequiredMixin, View): + permission_required = 'depot.add_legimibookpublish' + + def post(self, request, book_id): + book = get_object_or_404(Book, pk=book_id) + try: + publish = models.LegimiBookPublish.create_for(book, request.user) + except AssertionError: + pass + return redirect(book.get_absolute_url()) diff --git a/src/documents/models/book.py b/src/documents/models/book.py index 41e0bcd6..1580f74a 100644 --- a/src/documents/models/book.py +++ b/src/documents/models/book.py @@ -278,6 +278,9 @@ class Book(models.Model): except IndexError: return None + def last_legimi_publish(self): + return self.legimibookpublish_set.order_by('-created_at').first() + def assert_publishable(self): assert self.chunk_set.exists(), _('No chunks in the book.') try: diff --git a/src/documents/templates/documents/book_detail.html b/src/documents/templates/documents/book_detail.html index 77377542..f89e1e76 100644 --- a/src/documents/templates/documents/book_detail.html +++ b/src/documents/templates/documents/book_detail.html @@ -134,9 +134,27 @@ {{ publish_options_form.as_p }} + {% trans "Publish" %} - + + + {% if perms.depot.add_legimibookpublish %} +
+
+ {% csrf_token %} + + {% with llp=book.last_legimi_publish %} + {% if llp %} + {{ llp.created_at }} → + {{ llp.started_at }} → + {{ llp.finished_at }} + ({{ llp.get_status_display }}) + + {% endif %} + {% endwith %} +
+ {% endif %} + {% else %} {% trans "Log in to publish." %} {% endif %} @@ -145,11 +163,14 @@ {% endif %} - + + + + {% if doc %}
@@ -169,7 +190,7 @@ - {% with stats=book.wldocument.get_statistics %} + {% with stats=doc.get_statistics %} {% include 'documents/book_stats.html' with book=book stats=stats depth=0 %} {% endwith %} diff --git a/src/redakcja/urls.py b/src/redakcja/urls.py index 6e607305..2f36af18 100644 --- a/src/redakcja/urls.py +++ b/src/redakcja/urls.py @@ -26,6 +26,7 @@ urlpatterns = [ url(r'^editor/', include('wiki.urls')), url(r'^images/', include('wiki_img.urls')), url(r'^cover/', include('cover.urls')), + url(r'^depot/', include('depot.urls')), url(r'^wlxml/', include('wlxml.urls')), path('api/', include('redakcja.api.urls')), -- 2.20.1 From 467b414abdf8b789bef9a71a69a497c64ec19b3f Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 15 Jul 2022 11:17:32 +0200 Subject: [PATCH 02/16] Fix for Legimi. --- requirements/requirements.txt | 2 +- src/depot/legimi.py | 21 +++++++++++---------- src/depot/models.py | 1 + 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index d95985b9..cf952650 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -10,7 +10,7 @@ python-slugify python-docx==0.8.10 Wikidata==0.6.1 -librarian==2.4.5 +librarian==2.4.6 ## Django Django==3.2.14 diff --git a/src/depot/legimi.py b/src/depot/legimi.py index 1810ec57..86ad6e52 100644 --- a/src/depot/legimi.py +++ b/src/depot/legimi.py @@ -168,20 +168,21 @@ class Legimi: "url": model['Url'], } -# name=files[] -# filename -# content-type -# response: json -# success: true -# model.Url - def send_book(self, book, changes=None): wlbook = book.wldocument(librarian2=True, changes=changes) meta = wlbook.meta cover = LabelMarquiseCover(meta, width=1200).output_file() - epub_file = EpubBuilder(cover=MarquiseCover, fundraising=fundraising).build(wlbook).get_file() - mobi_file = MobiBuilder(cover=MarquiseCover, fundraising=fundraising).build(wlbook).get_file() + epub_file = EpubBuilder( + cover=MarquiseCover, + fundraising=fundraising, + base_url='file://' + book.gallery_path() + '/' + ).build(wlbook).get_file() + mobi_file = MobiBuilder( + cover=MarquiseCover, + fundraising=fundraising, + base_url='file://' + book.gallery_path() + '/' + ).build(wlbook).get_file() book_data = { "Title": meta.title, @@ -196,7 +197,7 @@ class Legimi: } if meta.isbn_html: isbn = meta.isbn_html - if isbn.upper().startswith('ISBN '): + if isbn.upper().startswith(('ISBN ', 'ISBN-')): isbn = isbn[5:] isbn = isbn.strip() book_data['Isbn'] = isbn diff --git a/src/depot/models.py b/src/depot/models.py index b68b5365..23b6d9f3 100644 --- a/src/depot/models.py +++ b/src/depot/models.py @@ -154,6 +154,7 @@ class LegimiBookPublish(models.Model): self.legimichunkpublish_set.order_by('change__chunk__number') ] legimi.send_book(self.book, changes=changes) + legimi.edit_sale(self.book) except Exception: self.status = 110 self.error = traceback.format_exc() -- 2.20.1 From ed7cd4d7271acd67bf366e4d320f9c49f07a50ea Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 18 Jul 2022 13:39:39 +0200 Subject: [PATCH 03/16] Use meta types in editor. --- requirements/requirements.txt | 2 +- src/catalogue/urls.py | 4 ++ src/catalogue/views.py | 23 ++++++++ .../static/js/wiki/view_properties.js | 13 ++++- src/wlxml/views.py | 54 ++++++++++++++----- 5 files changed, 81 insertions(+), 15 deletions(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index cf952650..5341b624 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -10,7 +10,7 @@ python-slugify python-docx==0.8.10 Wikidata==0.6.1 -librarian==2.4.6 +librarian==2.4.7 ## Django Django==3.2.14 diff --git a/src/catalogue/urls.py b/src/catalogue/urls.py index 10ef6d00..51bb6601 100644 --- a/src/catalogue/urls.py +++ b/src/catalogue/urls.py @@ -9,4 +9,8 @@ urlpatterns = [ path("", views.CatalogueView.as_view(), name="catalogue"), path("author//", views.AuthorView.as_view(), name="catalogue_author"), path("book//", views.BookView.as_view(), name="catalogue_book"), + + path('terms/epoch/', views.EpochTerms.as_view()), + path('terms/kind/', views.KindTerms.as_view()), + path('terms/genre/', views.GenreTerms.as_view()), ] diff --git a/src/catalogue/views.py b/src/catalogue/views.py index 792ea700..e68c4a28 100644 --- a/src/catalogue/views.py +++ b/src/catalogue/views.py @@ -5,6 +5,9 @@ from django.db.models import Prefetch from django.views.generic import DetailView, TemplateView from . import models import documents.models +from rest_framework.generics import ListAPIView +from rest_framework.filters import SearchFilter +from rest_framework import serializers class CatalogueView(TemplateView): @@ -33,3 +36,23 @@ class AuthorView(TemplateView): class BookView(DetailView): model = models.Book + + +class TermSearchFilter(SearchFilter): + search_param = 'term' + + +class Terms(ListAPIView): + filter_backends = [TermSearchFilter] + search_fields = ['name'] + + class serializer_class(serializers.Serializer): + label = serializers.CharField(source='name') + + +class EpochTerms(Terms): + queryset = models.Epoch.objects.all() +class KindTerms(Terms): + queryset = models.Kind.objects.all() +class GenreTerms(Terms): + queryset = models.Genre.objects.all() diff --git a/src/redakcja/static/js/wiki/view_properties.js b/src/redakcja/static/js/wiki/view_properties.js index a7caf10a..3495197c 100644 --- a/src/redakcja/static/js/wiki/view_properties.js +++ b/src/redakcja/static/js/wiki/view_properties.js @@ -243,7 +243,18 @@ pp.appendTo(ig); } - let $aninput = $(""); + let $aninput; + if (field.value_type.widget == 'select') { + $aninput = $(""); + if (field.value_type.autocomplete) { + $aninput.autocomplete(field.value_type.autocomplete); + } + } $aninput.data('edited', $(element)) $aninput.val( $(element).text() diff --git a/src/wlxml/views.py b/src/wlxml/views.py index b951b3c6..87780b79 100644 --- a/src/wlxml/views.py +++ b/src/wlxml/views.py @@ -6,6 +6,8 @@ from . import models from librarian.dcparser import BookInfo from librarian.document import WLDocument from librarian.builders import StandaloneHtmlBuilder +from librarian.meta.types.text import LegimiCategory, Epoch, Kind, Genre, Audience +from depot.legimi import legimi class XslView(TemplateView): @@ -42,21 +44,47 @@ class TagView(DetailView): slug_field = 'name' +VALUE_TYPES = { + LegimiCategory: { + 'widget': 'select', + 'options': list(legimi.CATEGORIES.keys()), + }, + Epoch: { + 'autocomplete': { + 'source': '/catalogue/terms/epoch/', + } + }, + Kind: { + 'autocomplete': { + 'source': '/catalogue/terms/kind/', + } + }, + Genre: { + 'autocomplete': { + 'source': '/catalogue/terms/genre/', + } + }, +} + + class MetaTagsView(View): def get(self, request): - return HttpResponse( - 'let META_FIELDS = ' + json.dumps([ - { - 'name': f.name, - 'required': f.required, - 'multiple': f.multiple, - 'uri': f.uri, - 'value_type': { - 'hasLanguage': f.value_type.has_language, - 'name': f.value_type.__name__, - } + fields = [] + for f in BookInfo.FIELDS: + d = { + 'name': f.name, + 'required': f.required, + 'multiple': f.multiple, + 'uri': f.uri, + 'value_type': { + 'hasLanguage': f.value_type.has_language, + 'name': f.value_type.__name__, } - for f in BookInfo.FIELDS - ]), + } + d['value_type'].update(VALUE_TYPES.get(f.value_type, {})) + fields.append(d) + + return HttpResponse( + 'let META_FIELDS = ' + json.dumps(fields), content_type='text/javascript') -- 2.20.1 From 360d9d37f72b1c6e2edb83c3a0218e55578134f3 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 22 Jul 2022 12:56:13 +0200 Subject: [PATCH 04/16] Metadata editing: auto-add record and more suggestions. --- src/catalogue/models.py | 8 +++ src/catalogue/urls.py | 5 ++ src/catalogue/views.py | 30 +++++++++ .../static/js/wiki/view_properties.js | 21 +++++- src/redakcja/static/js/wiki/wikiapi.js | 1 + src/wiki/templates/wiki/document_details.html | 15 +++-- src/wlxml/views.py | 66 ++++++++++++++++++- 7 files changed, 136 insertions(+), 10 deletions(-) diff --git a/src/catalogue/models.py b/src/catalogue/models.py index 75726a47..b0d3697b 100644 --- a/src/catalogue/models.py +++ b/src/catalogue/models.py @@ -63,6 +63,10 @@ class Author(WikidataMixin, models.Model): def get_absolute_url(self): return reverse("catalogue_author", args=[self.slug]) + @property + def name(self): + return f"{self.last_name}, {self.first_name}" + @property def pd_year(self): if self.year_of_death: @@ -160,6 +164,10 @@ class Book(WikidataMixin, models.Model): def get_absolute_url(self): return reverse("catalogue_book", args=[self.slug]) + + @property + def wluri(self): + return f'https://wolnelektury.pl/katalog/lektura/{self.slug}/' def authors_str(self): return ", ".join(str(author) for author in self.authors.all()) diff --git a/src/catalogue/urls.py b/src/catalogue/urls.py index 51bb6601..2b4301bf 100644 --- a/src/catalogue/urls.py +++ b/src/catalogue/urls.py @@ -13,4 +13,9 @@ urlpatterns = [ path('terms/epoch/', views.EpochTerms.as_view()), path('terms/kind/', views.KindTerms.as_view()), path('terms/genre/', views.GenreTerms.as_view()), + path('terms/wluri/', views.WLURITerms.as_view()), + path('terms/book_title/', views.BookTitleTerms.as_view()), + path('terms/author/', views.AuthorTerms.as_view()), + + path('terms/editor/', views.EditorTerms.as_view()), ] diff --git a/src/catalogue/views.py b/src/catalogue/views.py index e68c4a28..6d4a2249 100644 --- a/src/catalogue/views.py +++ b/src/catalogue/views.py @@ -2,6 +2,7 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # from django.db.models import Prefetch +from django.contrib.auth.models import User from django.views.generic import DetailView, TemplateView from . import models import documents.models @@ -56,3 +57,32 @@ class KindTerms(Terms): queryset = models.Kind.objects.all() class GenreTerms(Terms): queryset = models.Genre.objects.all() + +class AuthorTerms(Terms): + search_fields = ['first_name', 'last_name'] + queryset = models.Author.objects.all() + +class EditorTerms(Terms): + search_fields = ['first_name', 'last_name', 'username'] + queryset = User.objects.all() + + class serializer_class(serializers.Serializer): + label = serializers.SerializerMethodField() + + def get_label(self, obj): + return f'{obj.last_name}, {obj.first_name}' + +class BookTitleTerms(Terms): + queryset = models.Book.objects.all() + search_fields = ['title', 'slug'] + + class serializer_class(serializers.Serializer): + label = serializers.CharField(source='title') + +class WLURITerms(Terms): + queryset = models.Book.objects.all() + search_fields = ['title', 'slug'] + + class serializer_class(serializers.Serializer): + label = serializers.CharField(source='wluri') + diff --git a/src/redakcja/static/js/wiki/view_properties.js b/src/redakcja/static/js/wiki/view_properties.js index 3495197c..1fff64fe 100644 --- a/src/redakcja/static/js/wiki/view_properties.js +++ b/src/redakcja/static/js/wiki/view_properties.js @@ -119,9 +119,24 @@ if (field.value_type.hasLanguage) { span.attr('x-a-xml-lang', 'pl'); } - span.appendTo( - $("> [x-node='RDF'] > [x-node='Description']", self.$edited) - ); + + rdf = $("> [x-node='RDF']", self.$edited); + if (!rdf.length) { + rdf = $(""); + self.$edited.prepend(rdf); + self.$edited.prepend('\n '); + + } + rdfdesc = $("> [x-node='Description']", rdf); + if (!rdfdesc.length) { + rdfdesc = $(""); + rdf.prepend(rdfdesc); + rdf.prepend('\n '); + rdfdesc.append('\n '); + rdf.append('\n '); + } + span.appendTo(rdfdesc); + rdfdesc.append('\n '); self.displayMetaProperty($fg); diff --git a/src/redakcja/static/js/wiki/wikiapi.js b/src/redakcja/static/js/wiki/wikiapi.js index 4e3cd2c6..b9d36bea 100644 --- a/src/redakcja/static/js/wiki/wikiapi.js +++ b/src/redakcja/static/js/wiki/wikiapi.js @@ -69,6 +69,7 @@ this.galleryLink = $("*[data-key='gallery']", meta).text(); this.galleryStart = parseInt($("*[data-key='gallery-start']", meta).text()); + this.fullUri = $("*[data-key='full-uri']", meta).text(); var diff = $("*[data-key='diff']", meta).text(); if (diff) { diff --git a/src/wiki/templates/wiki/document_details.html b/src/wiki/templates/wiki/document_details.html index 4455e4d6..f3efc62e 100644 --- a/src/wiki/templates/wiki/document_details.html +++ b/src/wiki/templates/wiki/document_details.html @@ -49,9 +49,14 @@ {% endblock %} {% block dialogs %} - {% include "wiki/save_dialog.html" %} - {% include "wiki/revert_dialog.html" %} - {% if can_pubmark %} - {% include "wiki/pubmark_dialog.html" %} - {% endif %} + {% include "wiki/save_dialog.html" %} + {% include "wiki/revert_dialog.html" %} + {% if can_pubmark %} + {% include "wiki/pubmark_dialog.html" %} + {% endif %} +{% endblock %} + + +{% block meta-extra %} + {{ chunk.book.correct_about }} {% endblock %} diff --git a/src/wlxml/views.py b/src/wlxml/views.py index 87780b79..281ced5d 100644 --- a/src/wlxml/views.py +++ b/src/wlxml/views.py @@ -6,6 +6,7 @@ from . import models from librarian.dcparser import BookInfo from librarian.document import WLDocument from librarian.builders import StandaloneHtmlBuilder +from librarian.meta.types.wluri import WLURI from librarian.meta.types.text import LegimiCategory, Epoch, Kind, Genre, Audience from depot.legimi import legimi @@ -64,9 +65,62 @@ VALUE_TYPES = { 'source': '/catalogue/terms/genre/', } }, + WLURI: { + "autocomplete": { + "source": "/catalogue/terms/wluri/", + } + }, + "authors": { + "autocomplete": { + "source": "/catalogue/terms/author/", + } + }, + "translators": { + "autocomplete": { + "source": "/catalogue/terms/author/", + } + }, + "editors": { + "autocomplete": { + "source": "/catalogue/terms/editor/", + } + }, + "technical_editors": { + "autocomplete": { + "source": "/catalogue/terms/editor/", + } + }, + "type": { + "autocomplete": { + "source": ["text"] + } + }, + "title": { + "autocomplete": { + "source": "/catalogue/terms/book_title/", + } + }, + + "language": { + 'widget': 'select', + 'options': [ + 'pol', + 'eng', + 'fre', + 'ger', + 'lit', + ], + }, + "publisher": { + "autocomplete": { + "source": ["Fundacja Nowoczesna Polska"] + } + }, + } - + + class MetaTagsView(View): def get(self, request): fields = [] @@ -81,7 +135,15 @@ class MetaTagsView(View): 'name': f.value_type.__name__, } } - d['value_type'].update(VALUE_TYPES.get(f.value_type, {})) + d['value_type'].update( + VALUE_TYPES.get( + f.value_type, + VALUE_TYPES.get( + f.name, + {} + ) + ) + ) fields.append(d) return HttpResponse( -- 2.20.1 From 4fae06ea1b9fec1a45482ac043aa049ebce3f465 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 25 Jul 2022 12:05:52 +0200 Subject: [PATCH 05/16] Update for Django 4. --- requirements/requirements.txt | 24 ++++----- src/apiclient/urls.py | 10 ++-- src/cover/forms.py | 12 ++--- src/cover/models.py | 2 +- src/cover/urls.py | 17 +++--- src/documents/forms.py | 2 +- src/documents/models/book.py | 2 +- src/documents/models/chunk.py | 2 +- src/documents/models/image.py | 2 +- src/documents/models/project.py | 2 +- src/documents/models/publish_log.py | 2 +- src/documents/templatetags/book_list.py | 2 +- src/documents/templatetags/documents.py | 2 +- src/documents/templatetags/wall.py | 2 +- src/documents/urls.py | 70 ++++++++++++------------- src/documents/views.py | 5 +- src/dvcs/models.py | 2 +- src/dvcs/signals.py | 2 +- src/email_mangler/templatetags/email.py | 2 +- src/redakcja/urls.py | 31 ++++++----- src/toolbar/admin.py | 2 +- src/toolbar/models.py | 2 +- src/wiki/forms.py | 2 +- src/wiki/models.py | 2 +- src/wiki/urls.py | 48 +++++++---------- src/wiki/views.py | 2 +- src/wiki_img/forms.py | 2 +- src/wiki_img/urls.py | 16 +++--- src/wiki_img/views.py | 2 +- 29 files changed, 129 insertions(+), 144 deletions(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 5341b624..2f146c98 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -7,25 +7,25 @@ Pillow oauth2 httplib2 # oauth2 dependency python-slugify -python-docx==0.8.10 +python-docx==0.8.11 Wikidata==0.6.1 -librarian==2.4.7 +librarian==2.4.8 ## Django -Django==3.2.14 -fnpdjango==0.5 -django-pipeline==2.0.7 +Django==4.0.6 +fnpdjango==0.6 +django-pipeline==2.0.8 django-cas-ng==4.3.0 sorl-thumbnail==12.8.0 -fnp-django-pagination==2.2.4 +fnp-django-pagination==2.2.5 django-gravatar2==1.4.4 -django-extensions==3.1.3 -django-bootstrap4==3.0.1 +django-extensions==3.2.0 +django-bootstrap4==22.1 libsasscompiler==0.1.9 -django-debug-toolbar==3.2.4 -django-admin-numeric-filter==0.1.6 +django-debug-toolbar==3.5.0 +django-admin-numeric-filter @ git+https://github.com/lukasvinclav/django-admin-numeric-filter.git@83853dfcc8d354c7aea8020a075bbd83dc8ca171 djangorestframework==3.13.1 -django-filter==21.1 +django-filter==22.1 -sentry-sdk==0.12.2 +sentry-sdk==1.8.0 diff --git a/src/apiclient/urls.py b/src/apiclient/urls.py index 580a2c1b..f0613804 100644 --- a/src/apiclient/urls.py +++ b/src/apiclient/urls.py @@ -1,13 +1,13 @@ # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.conf.urls import url +from django.urls import path from . import views urlpatterns = [ - url(r'^oauth/$', views.oauth, name='apiclient_oauth'), - url(r'^oauth_callback/$', views.oauth_callback, name='apiclient_oauth_callback'), - url(r'^oauth-beta/$', views.oauth, kwargs={'beta': True}, name='apiclient_beta_oauth'), - url(r'^oauth_callback-beta/$', views.oauth_callback, kwargs={'beta': True}, name='apiclient_beta_callback'), + path('oauth/', views.oauth, name='apiclient_oauth'), + path('oauth_callback/', views.oauth_callback, name='apiclient_oauth_callback'), + path('oauth-beta/', views.oauth, kwargs={'beta': True}, name='apiclient_beta_oauth'), + path('oauth_callback-beta/', views.oauth_callback, kwargs={'beta': True}, name='apiclient_beta_callback'), ] diff --git a/src/cover/forms.py b/src/cover/forms.py index ee82a5af..7a855940 100644 --- a/src/cover/forms.py +++ b/src/cover/forms.py @@ -5,7 +5,7 @@ from io import BytesIO from django import forms from django.conf import settings -from django.utils.translation import ugettext_lazy as _, ugettext +from django.utils.translation import gettext_lazy as _, gettext from cover.models import Image from django.utils.safestring import mark_safe from PIL import Image as PILImage @@ -36,7 +36,7 @@ class ImageAddForm(forms.ModelForm): pass else: raise forms.ValidationError(mark_safe( - ugettext('Image already in repository.') + gettext('Image already in repository.') % {'url': img.get_absolute_url()})) return cl @@ -46,7 +46,7 @@ class ImageAddForm(forms.ModelForm): same_source = Image.objects.filter(source_url=source_url) if same_source: raise forms.ValidationError(mark_safe( - ugettext('Image already in repository.') + gettext('Image already in repository.') % {'url': same_source.first().get_absolute_url()})) return source_url @@ -60,7 +60,7 @@ class ImageAddForm(forms.ModelForm): download_url = cleaned_data.get('download_url', None) uploaded_file = cleaned_data.get('file', None) if not download_url and not uploaded_file: - raise forms.ValidationError(ugettext('No image specified')) + raise forms.ValidationError(gettext('No image specified')) if download_url: image_data = URLOpener().open(download_url).read() width, height = PILImage.open(BytesIO(image_data)).size @@ -68,7 +68,7 @@ class ImageAddForm(forms.ModelForm): width, height = PILImage.open(uploaded_file.file).size min_width, min_height = settings.MIN_COVER_SIZE if width < min_width or height < min_height: - raise forms.ValidationError(ugettext('Image too small: %sx%s, minimal dimensions %sx%s') % + raise forms.ValidationError(gettext('Image too small: %sx%s, minimal dimensions %sx%s') % (width, height, min_width, min_height)) return cleaned_data @@ -85,7 +85,7 @@ class ImageEditForm(forms.ModelForm): width, height = PILImage.open(uploaded_file.file).size min_width, min_height = settings.MIN_COVER_SIZE if width < min_width or height < min_height: - raise forms.ValidationError(ugettext('Image too small: %sx%s, minimal dimensions %sx%s') % + raise forms.ValidationError(gettext('Image too small: %sx%s, minimal dimensions %sx%s') % (width, height, min_width, min_height)) diff --git a/src/cover/models.py b/src/cover/models.py index 33c5e738..04c2a6bc 100644 --- a/src/cover/models.py +++ b/src/cover/models.py @@ -8,7 +8,7 @@ from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver from django.urls import reverse -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.contrib.sites.models import Site from PIL import Image as PILImage from cover.utils import URLOpener diff --git a/src/cover/urls.py b/src/cover/urls.py index 82788351..20cd7d60 100644 --- a/src/cover/urls.py +++ b/src/cover/urls.py @@ -1,22 +1,21 @@ # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.conf.urls import url from django.urls import path from . import views urlpatterns = [ - url(r'^preview/$', views.preview_from_xml, name='cover_preview'), - url(r'^preview/(?P[^/]+)/$', views.preview, name='cover_preview'), - url(r'^preview/(?P[^/]+)/(?P[^/]+)/$', + path('preview/', views.preview_from_xml, name='cover_preview'), + path('preview//', views.preview, name='cover_preview'), + path('preview///', views.preview, name='cover_preview'), - url(r'^preview/(?P[^/]+)/(?P[^/]+)/(?P\d+)/$', + path('preview////', views.preview, name='cover_preview'), - url(r'^image/$', views.image_list, name='cover_image_list'), - url(r'^image/(?P\d+)/?$', views.image, name='cover_image'), - url(r'^image/(?P\d+)/file/', views.image_file, name='cover_file'), - url(r'^add_image/$', views.add_image, name='cover_add_image'), + path('image/', views.image_list, name='cover_image_list'), + path('image//', views.image, name='cover_image'), + path('image//file/', views.image_file, name='cover_file'), + path('add_image/', views.add_image, name='cover_add_image'), path('quick-import//', views.quick_import, name='cover_quick_import'), ] diff --git a/src/documents/forms.py b/src/documents/forms.py index 418ae4ac..6598e75c 100644 --- a/src/documents/forms.py +++ b/src/documents/forms.py @@ -3,7 +3,7 @@ # from django.db.models import Count from django import forms -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.conf import settings from slugify import slugify from .constants import MASTERS diff --git a/src/documents/models/book.py b/src/documents/models/book.py index 1580f74a..23d3344b 100644 --- a/src/documents/models/book.py +++ b/src/documents/models/book.py @@ -6,7 +6,7 @@ from django.contrib.sites.models import Site from django.db import connection, models, transaction from django.template.loader import render_to_string from django.urls import reverse -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.conf import settings from slugify import slugify diff --git a/src/documents/models/chunk.py b/src/documents/models/chunk.py index 2698b58f..4d8f6d23 100644 --- a/src/documents/models/chunk.py +++ b/src/documents/models/chunk.py @@ -6,7 +6,7 @@ from django.db import models from django.db.utils import IntegrityError from django.template.loader import render_to_string from django.urls import reverse -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from documents.helpers import cached_in_field from documents.managers import VisibleManager from dvcs import models as dvcs_models diff --git a/src/documents/models/image.py b/src/documents/models/image.py index b05c9288..fd3d8c41 100644 --- a/src/documents/models/image.py +++ b/src/documents/models/image.py @@ -6,7 +6,7 @@ from django.contrib.sites.models import Site from django.db import models from django.template.loader import render_to_string from django.urls import reverse -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from documents.helpers import cached_in_field from documents.models import Project from dvcs import models as dvcs_models diff --git a/src/documents/models/project.py b/src/documents/models/project.py index f0ac752f..cc78ba58 100644 --- a/src/documents/models/project.py +++ b/src/documents/models/project.py @@ -2,7 +2,7 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ class Project(models.Model): diff --git a/src/documents/models/publish_log.py b/src/documents/models/publish_log.py index 5322bb62..2073a954 100644 --- a/src/documents/models/publish_log.py +++ b/src/documents/models/publish_log.py @@ -3,7 +3,7 @@ # from django.contrib.auth.models import User from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from documents.models import Chunk, Image diff --git a/src/documents/templatetags/book_list.py b/src/documents/templatetags/book_list.py index 88120d20..2261f5d6 100644 --- a/src/documents/templatetags/book_list.py +++ b/src/documents/templatetags/book_list.py @@ -4,7 +4,7 @@ import re from django.db.models import Q, Count, F, Max from django import template -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.contrib.auth.models import User from documents.models import Book, Chunk, Image, Project diff --git a/src/documents/templatetags/documents.py b/src/documents/templatetags/documents.py index 5c3b1eb8..b0a34bc9 100644 --- a/src/documents/templatetags/documents.py +++ b/src/documents/templatetags/documents.py @@ -3,7 +3,7 @@ # from django.urls import reverse from django import template -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ register = template.Library() diff --git a/src/documents/templatetags/wall.py b/src/documents/templatetags/wall.py index 4a6795be..486a2d6a 100644 --- a/src/documents/templatetags/wall.py +++ b/src/documents/templatetags/wall.py @@ -5,7 +5,7 @@ from datetime import timedelta from django.db.models import Q from django.urls import reverse from django import template -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from documents.models import Chunk, BookPublishRecord, Image, ImagePublishRecord diff --git a/src/documents/urls.py b/src/documents/urls.py index 602d4f22..1a1f1855 100644 --- a/src/documents/urls.py +++ b/src/documents/urls.py @@ -1,7 +1,7 @@ # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.conf.urls import url +from django.urls import path, re_path from django.contrib.auth.decorators import permission_required from django.views.generic import RedirectView from .feeds import PublishTrackFeed @@ -9,58 +9,56 @@ from . import views urlpatterns = [ - url(r'^$', RedirectView.as_view(url='catalogue/', permanent=False)), + path('', RedirectView.as_view(url='catalogue/', permanent=False)), - url(r'^images/$', views.image_list, name='documents_image_list'), - url(r'^image/(?P[^/]+)/$', views.image, name="documents_image"), - url(r'^image/(?P[^/]+)/publish$', views.publish_image, + path('images/', views.image_list, name='documents_image_list'), + path('image//', views.image, name="documents_image"), + path('image//publish', views.publish_image, name="documents_publish_image"), - url(r'^catalogue/$', views.document_list, name='documents_document_list'), - url(r'^user/$', views.my, name='documents_user'), - url(r'^user/(?P[^/]+)/$', views.user, name='documents_user'), - url(r'^users/$', views.users, name='documents_users'), - url(r'^activity/$', views.activity, name='documents_activity'), - url(r'^activity/(?P\d{4}-\d{2}-\d{2})/$', + path('catalogue/', views.document_list, name='documents_document_list'), + path('user/', views.my, name='documents_user'), + path('user//', views.user, name='documents_user'), + path('users/', views.users, name='documents_users'), + path('activity/', views.activity, name='documents_activity'), + re_path(r'^activity/(?P\d{4}-\d{2}-\d{2})/$', views.activity, name='documents_activity'), - url(r'^upload/$', - views.upload, name='documents_upload'), + path('upload/', views.upload, name='documents_upload'), - url(r'^create/(?P[^/]*)/', - views.create_missing, name='documents_create_missing'), - url(r'^create/', + path('create//', views.create_missing, name='documents_create_missing'), + path('create/', views.create_missing, name='documents_create_missing'), - url(r'^book/(?P[^/]+)/publish$', views.publish, name="documents_publish"), + path('book//publish', views.publish, name="documents_publish"), - url(r'^book/(?P[^/]+)/$', views.book, name="documents_book"), - url(r'^book/(?P[^/]+)/gallery/$', + path('book//', views.book, name="documents_book"), + path('book//gallery/', permission_required('documents.change_book')(views.GalleryView.as_view()), name="documents_book_gallery"), - url(r'^book/(?P[^/]+)/xml$', views.book_xml, name="documents_book_xml"), - url(r'^book/dc/(?P[^/]+)/xml$', views.book_xml_dc, name="documents_book_xml_dc"), - url(r'^book/(?P[^/]+)/txt$', views.book_txt, name="documents_book_txt"), - url(r'^book/(?P[^/]+)/html$', views.book_html, name="documents_book_html"), - url(r'^book/(?P[^/]+)/epub$', views.book_epub, name="documents_book_epub"), - url(r'^book/(?P[^/]+)/mobi$', views.book_mobi, name="documents_book_mobi"), - url(r'^book/(?P[^/]+)/pdf$', views.book_pdf, name="documents_book_pdf"), - url(r'^book/(?P[^/]+)/pdf-mobile$', views.book_pdf, kwargs={'mobile': True}, name="documents_book_pdf_mobile"), + path('book//xml', views.book_xml, name="documents_book_xml"), + path('book/dc//xml', views.book_xml_dc, name="documents_book_xml_dc"), + path('book//txt', views.book_txt, name="documents_book_txt"), + path('book//html', views.book_html, name="documents_book_html"), + path('book//epub', views.book_epub, name="documents_book_epub"), + path('book//mobi', views.book_mobi, name="documents_book_mobi"), + path('book//pdf', views.book_pdf, name="documents_book_pdf"), + path('book//pdf-mobile', views.book_pdf, kwargs={'mobile': True}, name="documents_book_pdf_mobile"), - url(r'^chunk_add/(?P[^/]+)/(?P[^/]+)/$', + path('chunk_add///', views.chunk_add, name="documents_chunk_add"), - url(r'^chunk_edit/(?P[^/]+)/(?P[^/]+)/$', + path('chunk_edit///', views.chunk_edit, name="documents_chunk_edit"), - url(r'^book_append/(?P[^/]+)/$', + path('book_append//', views.book_append, name="documents_book_append"), - url(r'^chunk_mass_edit', + path('chunk_mass_edit', views.chunk_mass_edit, name='documents_chunk_mass_edit'), - url(r'^image_mass_edit', + path('image_mass_edit', views.image_mass_edit, name='documents_image_mass_edit'), - url(r'^track/(?P[^/]*)/$', PublishTrackFeed()), - url(r'^active/$', views.active_users_list, name='active_users_list'), + path('track//', PublishTrackFeed()), + path('active/', views.active_users_list, name='active_users_list'), - url(r'^mark-final/$', views.mark_final, name='mark_final'), - url(r'^mark-final-completed/$', views.mark_final_completed, name='mark_final_completed'), + path('mark-final/', views.mark_final, name='mark_final'), + path('mark-final-completed/', views.mark_final_completed, name='mark_final_completed'), ] diff --git a/src/documents/views.py b/src/documents/views.py index 38e69c1b..6c9f29e3 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -5,7 +5,7 @@ from collections import defaultdict from datetime import datetime, date, timedelta import logging import os -from urllib.parse import unquote, urlsplit, urlunsplit +from urllib.parse import quote_plus, unquote, urlsplit, urlunsplit from django.conf import settings from django.contrib import auth @@ -19,8 +19,7 @@ from django.http import Http404, HttpResponse, HttpResponseForbidden from django.http.response import HttpResponseRedirect from django.shortcuts import get_object_or_404, render from django.utils.encoding import iri_to_uri -from django.utils.http import urlquote_plus -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.views.decorators.http import require_POST from django_cas_ng.decorators import user_passes_test diff --git a/src/dvcs/models.py b/src/dvcs/models.py index 978f0720..5d465caf 100644 --- a/src/dvcs/models.py +++ b/src/dvcs/models.py @@ -11,7 +11,7 @@ from django.core.files.storage import FileSystemStorage from django.db import models, transaction from django.db.models.base import ModelBase from django.utils.text import format_lazy -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ import merge3 from django.conf import settings diff --git a/src/dvcs/signals.py b/src/dvcs/signals.py index 14f37f40..d129cf97 100644 --- a/src/dvcs/signals.py +++ b/src/dvcs/signals.py @@ -4,4 +4,4 @@ from django.dispatch import Signal post_commit = Signal() -post_publishable = Signal(providing_args=['publishable']) +post_publishable = Signal() diff --git a/src/email_mangler/templatetags/email.py b/src/email_mangler/templatetags/email.py index dbf29396..c0503ec5 100644 --- a/src/email_mangler/templatetags/email.py +++ b/src/email_mangler/templatetags/email.py @@ -4,7 +4,7 @@ import codecs from django.utils.html import escape from django.utils.safestring import mark_safe -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django import template register = template.Library() diff --git a/src/redakcja/urls.py b/src/redakcja/urls.py index 2f36af18..e4078851 100644 --- a/src/redakcja/urls.py +++ b/src/redakcja/urls.py @@ -1,7 +1,6 @@ # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.conf.urls import url from django.urls import include, path from django.contrib import admin from django.conf import settings @@ -17,17 +16,17 @@ urlpatterns = [ #url(r'^admin/logout/$', django_cas_ng.views.logout, name='logout'), # Admin panel - url(r'^admin/doc/', include('django.contrib.admindocs.urls')), - url(r'^admin/', admin.site.urls), + path('admin/doc/', include('django.contrib.admindocs.urls')), + path('admin/', admin.site.urls), path('catalogue/', include('catalogue.urls')), - url(r'^$', RedirectView.as_view(url='/documents/', permanent=False)), - url(r'^documents/', include('documents.urls')), - url(r'^apiclient/', include('apiclient.urls')), - url(r'^editor/', include('wiki.urls')), - url(r'^images/', include('wiki_img.urls')), - url(r'^cover/', include('cover.urls')), - url(r'^depot/', include('depot.urls')), - url(r'^wlxml/', include('wlxml.urls')), + path('', RedirectView.as_view(url='/documents/', permanent=False)), + path('documents/', include('documents.urls')), + path('apiclient/', include('apiclient.urls')), + path('editor/', include('wiki.urls')), + path('images/', include('wiki_img.urls')), + path('cover/', include('cover.urls')), + path('depot/', include('depot.urls')), + path('wlxml/', include('wlxml.urls')), path('api/', include('redakcja.api.urls')), ] @@ -35,14 +34,14 @@ urlpatterns = [ if settings.CAS_SERVER_URL: urlpatterns += [ - url(r'^accounts/login/$', django_cas_ng.views.LoginView.as_view(), name='cas_ng_login'), - url(r'^accounts/logout/$', django_cas_ng.views.LogoutView.as_view(), name='logout'), + path('accounts/login/', django_cas_ng.views.LoginView.as_view(), name='cas_ng_login'), + path('accounts/logout/', django_cas_ng.views.LogoutView.as_view(), name='logout'), ] else: import django.contrib.auth.views urlpatterns += [ - url(r'^accounts/login/$', django.contrib.auth.views.LoginView.as_view(), name='cas_ng_login'), - url(r'^accounts/', include('django.contrib.auth.urls')), + path('accounts/login/', django.contrib.auth.views.LoginView.as_view(), name='cas_ng_login'), + path('accounts/', include('django.contrib.auth.urls')), ] @@ -51,4 +50,4 @@ if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) import debug_toolbar - urlpatterns += [url(r'^__debug__/', include(debug_toolbar.urls))] + urlpatterns += [path('__debug__/', include(debug_toolbar.urls))] diff --git a/src/toolbar/admin.py b/src/toolbar/admin.py index 1fa695b2..cb6de45c 100644 --- a/src/toolbar/admin.py +++ b/src/toolbar/admin.py @@ -2,7 +2,7 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # from django.contrib import admin -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django import forms import json diff --git a/src/toolbar/models.py b/src/toolbar/models.py index 02567fab..a221e04b 100644 --- a/src/toolbar/models.py +++ b/src/toolbar/models.py @@ -2,7 +2,7 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ class ButtonGroup(models.Model): diff --git a/src/wiki/forms.py b/src/wiki/forms.py index 084ae46d..46194677 100644 --- a/src/wiki/forms.py +++ b/src/wiki/forms.py @@ -2,7 +2,7 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # from django import forms -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from documents.models import Chunk diff --git a/src/wiki/models.py b/src/wiki/models.py index 456e2195..b3cb9d8e 100644 --- a/src/wiki/models.py +++ b/src/wiki/models.py @@ -2,7 +2,7 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ import logging logger = logging.getLogger("fnp.wiki") diff --git a/src/wiki/urls.py b/src/wiki/urls.py index f1a45087..f2fa3f5d 100644 --- a/src/wiki/urls.py +++ b/src/wiki/urls.py @@ -1,37 +1,27 @@ # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.conf.urls import url +from django.urls import path from . import views urlpatterns = [ - url(r'^edit/(?P[^/]+)/(?:(?P[^/]+)/)?$', - views.editor, name="wiki_editor"), - - url(r'^readonly/(?P[^/]+)/(?:(?P[^/]+)/)?$', - views.editor_readonly, name="wiki_editor_readonly"), - - url(r'^gallery/(?P[^/]+)/$', - views.gallery, name="wiki_gallery"), - - url(r'^history/(?P\d+)/$', - views.history, name="wiki_history"), - - url(r'^rev/(?P\d+)/$', - views.revision, name="wiki_revision"), - - url(r'^text/(?P\d+)/$', - views.text, name="wiki_text"), - - url(r'^revert/(?P\d+)/$', - views.revert, name='wiki_revert'), - - url(r'^diff/(?P\d+)/$', views.diff, name="wiki_diff"), - url(r'^pubmark/(?P\d+)/$', views.pubmark, name="wiki_pubmark"), - - url(r'^themes$', views.themes, name="themes"), - - url(r'^back/$', views.back), - url(r'^editor-user-area/$', views.editor_user_area), + path('edit///', views.editor, name="wiki_editor"), + path('edit//', views.editor, name="wiki_editor"), + + path('readonly///', + views.editor_readonly, name="wiki_editor_readonly"), + path('readonly//', + views.editor_readonly, name="wiki_editor_readonly"), + + path('gallery//', views.gallery, name="wiki_gallery"), + path('history//', views.history, name="wiki_history"), + path('rev//', views.revision, name="wiki_revision"), + path('text//', views.text, name="wiki_text"), + path('revert//', views.revert, name='wiki_revert'), + path('diff//', views.diff, name="wiki_diff"), + path('pubmark//', views.pubmark, name="wiki_pubmark"), + path('themes', views.themes, name="themes"), + path('back/', views.back), + path('editor-user-area/', views.editor_user_area), ] diff --git a/src/wiki/views.py b/src/wiki/views.py index 7bcea51f..cf67abbd 100644 --- a/src/wiki/views.py +++ b/src/wiki/views.py @@ -14,7 +14,7 @@ from django.http import Http404, HttpResponseForbidden from django.middleware.gzip import GZipMiddleware from django.utils.decorators import decorator_from_middleware from django.utils.formats import localize -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views.decorators.http import require_POST, require_GET from django.shortcuts import get_object_or_404, render diff --git a/src/wiki_img/forms.py b/src/wiki_img/forms.py index 43b1a253..49d7845d 100644 --- a/src/wiki_img/forms.py +++ b/src/wiki_img/forms.py @@ -2,7 +2,7 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # from django import forms -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from wiki.forms import DocumentTextSaveForm from documents.models import Image diff --git a/src/wiki_img/urls.py b/src/wiki_img/urls.py index 82244841..0c9e8482 100644 --- a/src/wiki_img/urls.py +++ b/src/wiki_img/urls.py @@ -1,26 +1,26 @@ # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.conf.urls import url +from django.urls import path from . import views urlpatterns = [ - url(r'^edit/(?P[^/]+)/$', + path('edit//', views.editor, name="wiki_img_editor"), - url(r'^readonly/(?P[^/]+)/$', + path('readonly//', views.editor_readonly, name="wiki_img_editor_readonly"), - url(r'^text/(?P\d+)/$', + path('text//', views.text, name="wiki_img_text"), - url(r'^history/(?P\d+)/$', + path('history//', views.history, name="wiki_img_history"), - url(r'^revert/(?P\d+)/$', + path('revert//', views.revert, name='wiki_img_revert'), - url(r'^diff/(?P\d+)/$', views.diff, name="wiki_img_diff"), - url(r'^pubmark/(?P\d+)/$', views.pubmark, name="wiki_img_pubmark"), + path('diff//', views.diff, name="wiki_img_diff"), + path('pubmark//', views.pubmark, name="wiki_img_pubmark"), ] diff --git a/src/wiki_img/views.py b/src/wiki_img/views.py index 7fc5c700..a3135286 100644 --- a/src/wiki_img/views.py +++ b/src/wiki_img/views.py @@ -15,7 +15,7 @@ from django.shortcuts import get_object_or_404, render from django.views.decorators.http import require_GET, require_POST from django.conf import settings from django.utils.formats import localize -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from documents.models import Image from wiki import forms -- 2.20.1 From 1986ab5301eff65a3071430ce52158f9104285d0 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 25 Jul 2022 13:08:22 +0200 Subject: [PATCH 06/16] Fix existing tests. --- src/cover/tests.py | 4 ++-- src/documents/tests/test_publish.py | 22 ++++++++++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/cover/tests.py b/src/cover/tests.py index 838b9bfe..a852f25b 100644 --- a/src/cover/tests.py +++ b/src/cover/tests.py @@ -2,12 +2,12 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # from django.test import TestCase -from cover.forms import FlickrForm +from cover.forms import ImportForm class FlickrTests(TestCase): def test_flickr(self): - form = FlickrForm({"source_url": "https://www.flickr.com/photos/rczajka/6941928577/in/photostream"}) + form = ImportForm({"source_url": "https://www.flickr.com/photos/rczajka/6941928577/in/photostream"}) self.assertTrue(form.is_valid()) self.assertEqual(form.cleaned_data['source_url'], "https://www.flickr.com/photos/rczajka/6941928577/") self.assertEqual(form.cleaned_data['author'], "Radek Czajka@Flickr") diff --git a/src/documents/tests/test_publish.py b/src/documents/tests/test_publish.py index c5813891..4e6592a8 100644 --- a/src/documents/tests/test_publish.py +++ b/src/documents/tests/test_publish.py @@ -26,7 +26,16 @@ class PublishTests(TestCase): def test_publish(self, api_call): self.book[0].head.set_publishable(True) self.book.publish(self.user) - api_call.assert_called_with(self.user, 'books/', {"book_xml": self.text1, "days": 0}, beta=False) + api_call.assert_called_with( + self.user, + 'books/', + { + "book_xml": self.text1, + "days": 0, + "hidden": False + }, + beta=False + ) @patch('apiclient.api_call') def test_publish_multiple(self, api_call): @@ -35,4 +44,13 @@ class PublishTests(TestCase): self.book[1].commit(get_fixture('chunk2.xml')) self.book[1].head.set_publishable(True) self.book.publish(self.user) - api_call.assert_called_with(self.user, 'books/', {"book_xml": get_fixture('expected.xml'), "days": 0}, beta=False) + api_call.assert_called_with( + self.user, + 'books/', + { + "book_xml": get_fixture('expected.xml'), + "days": 0, + "hidden": False + }, + beta=False + ) -- 2.20.1 From 81f5e7445d649ead05b4d3d0a15b742444cd8b06 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 29 Jul 2022 11:51:34 +0200 Subject: [PATCH 07/16] Tests. --- Makefile | 8 +- src/alerts/views.py | 3 - .../management/commands/refresh_covers.py | 43 ---- src/cover/models.py | 2 +- src/cover/tests.py | 223 +++++++++++++++++- src/cover/tests/angelus-novus.jpeg | Bin 0 -> 23365 bytes src/cover/tests/book.xml | 16 ++ src/cover/utils.py | 12 - src/cover/views.py | 24 +- src/documents/management/commands/fixdc.py | 52 ---- src/redakcja/settings/__init__.py | 3 + src/redakcja/settings/test.py | 13 + src/redakcja/settings/test_full.py | 5 + 13 files changed, 270 insertions(+), 134 deletions(-) delete mode 100644 src/alerts/views.py delete mode 100644 src/cover/management/commands/refresh_covers.py create mode 100644 src/cover/tests/angelus-novus.jpeg create mode 100644 src/cover/tests/book.xml delete mode 100644 src/documents/management/commands/fixdc.py create mode 100644 src/redakcja/settings/test_full.py diff --git a/Makefile b/Makefile index 304f5db7..34f331ad 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,13 @@ deploy: src/redakcja/localsettings.py .ONESHELL: test: cd src - coverage run --branch --source='.' ./manage.py test --settings=redakcja.settings.test; true + ./manage.py test --settings=redakcja.settings.test + + +.ONESHELL: +test_full: + cd src + coverage run --branch --source='.' ./manage.py test --settings=redakcja.settings.test_full; true coverage html -d ../htmlcov.new rm -rf ../htmlcov mv ../htmlcov.new ../htmlcov diff --git a/src/alerts/views.py b/src/alerts/views.py deleted file mode 100644 index 91ea44a2..00000000 --- a/src/alerts/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. diff --git a/src/cover/management/commands/refresh_covers.py b/src/cover/management/commands/refresh_covers.py deleted file mode 100644 index 57325633..00000000 --- a/src/cover/management/commands/refresh_covers.py +++ /dev/null @@ -1,43 +0,0 @@ -# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later. -# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. -# -import urllib2 as urllib -from django.core.files.base import ContentFile -from django.core.management import BaseCommand - -from cover.models import Image -from cover.utils import get_flickr_data, URLOpener, FlickrError - - -class Command(BaseCommand): - def add_arguments(self, parser): - parser.add_argument('--from', dest='from_id', type=int, default=1) - - def handle(self, *args, **options): - from_id = options.get('from_id', 1) - images = Image.objects.filter(id__gte=from_id).exclude(book=None).order_by('id') - images = images.filter(source_url__contains='flickr.com').exclude(download_url__endswith='_o.jpg') - for image in images: - print(image.id) - try: - flickr_data = get_flickr_data(image.source_url) - print(flickr_data) - except FlickrError as e: - print('Flickr analysis failed: %s' % e) - else: - flickr_url = flickr_data['download_url'] - if flickr_url != image.download_url: - same_url = Image.objects.filter(download_url=flickr_url) - if same_url: - print('Download url already present in image %s' % same_url.get().id) - continue - try: - t = URLOpener().open(flickr_url).read() - except urllib.URLError: - print('Broken download url') - except IOError: - print('Connection failed') - else: - image.download_url = flickr_url - image.file.save(image.file.name, ContentFile(t)) - image.save() diff --git a/src/cover/models.py b/src/cover/models.py index 04c2a6bc..87ac0369 100644 --- a/src/cover/models.py +++ b/src/cover/models.py @@ -73,7 +73,7 @@ class Image(models.Model): img, save=False ) - super().save(**kwargs) + super().save(update_fields=['use_file']) def get_absolute_url(self): return reverse('cover_image', args=[self.id]) diff --git a/src/cover/tests.py b/src/cover/tests.py index a852f25b..4755991e 100644 --- a/src/cover/tests.py +++ b/src/cover/tests.py @@ -1,17 +1,224 @@ # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.test import TestCase +import re +from django.core.files.uploadedfile import SimpleUploadedFile +from django.conf import settings +from django.contrib.auth.models import User +from django.test import TestCase, override_settings +from unittest import skipUnless +from unittest.mock import patch +from documents.models import Book from cover.forms import ImportForm +from cover.models import Image +IMAGE_PATH = __file__.rsplit('/', 1)[0] + '/tests/angelus-novus.jpeg' + +with open(__file__.rsplit('/', 1)[0] + '/tests/book.xml') as f: + SAMPLE_XML = f.read() + + +@skipUnless(settings.TEST_INTEGRATION, 'Skip integration tests') class FlickrTests(TestCase): + def assertEqualWithRe(self, dict1, dict2): + self.assertEqual(len(dict1), len(dict2)) + for k, v in dict2.items(): + if isinstance(v, re.Pattern): + self.assertRegex(dict1[k], v) + else: + self.assertEqual(dict1[k], v) + def test_flickr(self): - form = ImportForm({"source_url": "https://www.flickr.com/photos/rczajka/6941928577/in/photostream"}) + form = ImportForm({ + "source_url": "https://www.flickr.com/photos/rczajka/6941928577/in/photostream" + }) + self.assertTrue(form.is_valid()) + self.assertEqualWithRe( + form.cleaned_data, + { + 'source_url': "https://www.flickr.com/photos/rczajka/6941928577/", + 'author': "Radek Czajka@Flickr", + 'title': "Pirate Stańczyk", + 'license_name': "CC BY 2.0", + 'license_url': "https://creativecommons.org/licenses/by/2.0/", + 'download_url': re.compile(r'\.staticflickr\.com'), + } + ) + + def test_wikimedia_fal(self): + form = ImportForm({ + "source_url": "https://commons.wikimedia.org/wiki/File:Valdai_IverskyMon_asv2018_img47.jpg" + }) + self.assertTrue(form.is_valid()) + self.assertEqual( + form.cleaned_data, + { + 'title': 'Valdai IverskyMon asv2018 img47', + 'author': 'A.Savin', + 'source_url': 'https://commons.wikimedia.org/wiki/File:Valdai_IverskyMon_asv2018_img47.jpg', + 'download_url': 'https://upload.wikimedia.org/wikipedia/commons/4/43/Valdai_IverskyMon_asv2018_img47.jpg', + 'license_url': 'http://artlibre.org/licence/lal/en', + 'license_name': 'FAL' + } + ) + + def test_wikimedia_public_domain(self): + form = ImportForm({ + "source_url": 'https://commons.wikimedia.org/wiki/File:Pymonenko_A_boy_in_a_straw_hat.jpg' + }) self.assertTrue(form.is_valid()) - self.assertEqual(form.cleaned_data['source_url'], "https://www.flickr.com/photos/rczajka/6941928577/") - self.assertEqual(form.cleaned_data['author'], "Radek Czajka@Flickr") - self.assertEqual(form.cleaned_data['title'], u"Pirate Stańczyk") - self.assertEqual(form.cleaned_data['license_name'], "CC BY 2.0") - self.assertEqual(form.cleaned_data['license_url'], "https://creativecommons.org/licenses/by/2.0/") - self.assertTrue('.staticflickr.com' in form.cleaned_data['download_url']) + self.assertEqual( + form.cleaned_data, + { + 'title': 'Chłopiec w słomkowym kapeluszu', + 'author': 'Mykola Pymonenko', + 'source_url': 'https://commons.wikimedia.org/wiki/File:Pymonenko_A_boy_in_a_straw_hat.jpg', + 'download_url': 'https://upload.wikimedia.org/wikipedia/commons/9/9b/Pymonenko_A_boy_in_a_straw_hat.jpg', + 'license_url': 'https://pl.wikipedia.org/wiki/Domena_publiczna', + 'license_name': 'domena publiczna' + } + ) + + def test_mnw(self): + form = ImportForm({ + "source_url": 'https://cyfrowe.mnw.art.pl/pl/katalog/511078' + }) + self.assertTrue(form.is_valid()) + self.assertEqualWithRe( + form.cleaned_data, + { + 'title': 'Chłopka (Baba ukraińska)', + 'author': 'Krzyżanowski, Konrad (1872-1922)', + 'source_url': 'https://cyfrowe.mnw.art.pl/pl/katalog/511078', + 'download_url': re.compile(r'https://cyfrowe-cdn\.mnw\.art\.pl/.*\.jpg'), + 'license_url': 'https://pl.wikipedia.org/wiki/Domena_publiczna', + 'license_name': 'domena publiczna' + } + ) + + def test_quick_import(self): + user = User.objects.create(username='test', is_superuser=True) + self.client.force_login(user) + + book = Book.create(slug='test', text=SAMPLE_XML, creator=user) + + self.client.post( + '/cover/quick-import/1/', + { + 'url': 'https://cyfrowe.mnw.art.pl/pl/katalog/511078' + } + ) + + self.assertEqual(Image.objects.all().count(), 1) + self.assertEqual(book[0].history().count(), 2) + self.assertIn( + 'Chłopka (Baba ukraińska), Krzyżanowski, Konrad (1872-1922), domena publiczna', + book.materialize() + ) + + +class CoverPreviewTest(TestCase): + @classmethod + def setUpTestData(cls): + cls.book = Book.create(slug='test', text='', creator=None) + + def test_preview_from_bad_xml(self): + response = self.client.post('/cover/preview/', data={"xml": ''}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b'/media/static/img/sample_cover.png') + + @patch('cover.views.make_cover') + def test_preview_from_minimal_xml(self, make_cover): + response = self.client.post('/cover/preview/', data={"xml": SAMPLE_XML}) + self.assertEqual(response.status_code, 200) + self.assertIn(b'/media/dynamic/cover/preview/', response.content) + + def test_bad_book(self): + response = self.client.get('/cover/preview/test/') + self.assertEqual(response.status_code, 302) + + @patch('cover.views.make_cover') + def test_good_book(self, make_cover): + self.book[0].commit(text=SAMPLE_XML) + + response = self.client.get('/cover/preview/test/1/3/') + self.assertEqual(response.status_code, 404) + + response = self.client.get('/cover/preview/test/1/') + self.assertEqual(response.status_code, 200) + self.assertNotIn('Content-Disposition', response) + + response = self.client.get('/cover/preview/test/1/2/?download&width=100') + self.assertEqual(response.status_code, 200) + self.assertIn('attachment', response['Content-Disposition']) + + +class TestAddCover(TestCase): + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create(username='test', is_superuser=True) + + def test_add_image(self): + self.client.force_login(self.user) + + response = self.client.get('/cover/add_image/') + self.assertEqual(response.status_code, 200) + + with open(IMAGE_PATH, 'rb') as image_file: + response = self.client.post( + '/cover/add_image/', + data={ + 'title': 'Angelus Novus', + 'author': 'Paul Klee', + 'license_name': 'domena publiczna', + 'license_url': '', + 'file': image_file, + } + ) + self.assertEqual(Image.objects.all().count(), 1) + +class TestCover(TestCase): + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create(username='test', is_superuser=True) + with open(IMAGE_PATH, 'rb') as f: + cls.img = Image.objects.create( + title='Angelus Novus', + author='Paul Klee', + license_name='domena publiczna', + license_url='', + file=SimpleUploadedFile( + 'angelus-novus.jpg', + f.read(), + content_type='image/jpeg' + ) + ) + + def test_image_list(self): + response = self.client.get('/cover/image/') + self.assertEqual(len(response.context['object_list']), 1) + + def test_image(self): + response = self.client.get('/cover/image/1/') + self.assertEqual(response.context['object'].title, 'Angelus Novus') + + def test_edit_image(self): + self.client.force_login(self.user) + response = self.client.post('/cover/image/1/', { + 'author': 'author', + 'title': 'changed title', + 'license_name': 'domena', + 'cut_top': 1, + 'cut_bottom': 1, + 'cut_left': 1, + 'cut_right': 1, + }) + + response = self.client.get('/cover/image/1/') + self.assertEqual(response.context['object'].title, 'changed title') + + + def test_image_file(self): + response = self.client.get('/cover/image/1/file/') + self.assertRegex(response['Location'], r'^/media/dynamic/cover/image/') diff --git a/src/cover/tests/angelus-novus.jpeg b/src/cover/tests/angelus-novus.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..fd0394fb342880b9fef27f1ad6d2055b06061ae2 GIT binary patch literal 23365 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 literal 0 HcmV?d00001 diff --git a/src/cover/tests/book.xml b/src/cover/tests/book.xml new file mode 100644 index 00000000..acdba432 --- /dev/null +++ b/src/cover/tests/book.xml @@ -0,0 +1,16 @@ + + + + Tytuł utworu + Utworu, Autor + 2022-07-26 + Wolne Lektury + pol + slug + domena publiczna + + + + diff --git a/src/cover/utils.py b/src/cover/utils.py index fcda91fd..56fb24a7 100644 --- a/src/cover/utils.py +++ b/src/cover/utils.py @@ -76,14 +76,6 @@ def get_flickr_data(url): def get_wikimedia_data(url): - """ - >>> get_wikimedia_data('https://commons.wikimedia.org/wiki/File:Valdai_IverskyMon_asv2018_img47.jpg') - {'title': 'Valdai IverskyMon asv2018 img47', 'author': 'A.Savin', 'source_url': 'https://commons.wikimedia.org/wiki/File:Valdai_IverskyMon_asv2018_img47.jpg', 'download_url': 'https://upload.wikimedia.org/wikipedia/commons/4/43/Valdai_IverskyMon_asv2018_img47.jpg', 'license_url': 'http://artlibre.org/licence/lal/en', 'license_name': 'FAL'} - - >>> get_wikimedia_data('https://commons.wikimedia.org/wiki/File:Pymonenko_A_boy_in_a_straw_hat.jpg') - {'title': 'Chłopiec w słomkowym kapeluszu', 'author': 'Mykola Pymonenko', 'source_url': 'https://commons.wikimedia.org/wiki/File:Pymonenko_A_boy_in_a_straw_hat.jpg', 'download_url': 'https://upload.wikimedia.org/wikipedia/commons/9/9b/Pymonenko_A_boy_in_a_straw_hat.jpg', 'license_url': 'https://pl.wikipedia.org/wiki/Domena_publiczna', 'license_name': 'domena publiczna'} - - """ file_name = url.rsplit('/', 1)[-1].rsplit(':', 1)[-1] d = json.loads(URLOpener().open('https://commons.wikimedia.org/w/api.php?action=query&titles=File:{}&prop=imageinfo&iiprop=url|user|extmetadata&iimetadataversion=latest&format=json'.format(file_name)).read().decode('utf-8')) @@ -118,10 +110,6 @@ def get_wikimedia_data(url): def get_mnw_data(url): - """ - >>> get_mnw_data('https://cyfrowe.mnw.art.pl/pl/katalog/511078') - {'title': 'Chłopka (Baba ukraińska)', 'author': 'Krzyżanowski, Konrad (1872-1922)', 'source_url': 'https://cyfrowe.mnw.art.pl/pl/katalog/511078', 'download_url': 'https://cyfrowe-cdn.mnw.art.pl/upload/multimedia/a0/68/a0681c60f203d907d9c45050d245c921.jpg', 'license_url': 'https://pl.wikipedia.org/wiki/Domena_publiczna', 'license_name': 'domena publiczna'} - """ nr = url.rsplit('/', 1)[-1] d = list( csv.DictReader( diff --git a/src/cover/views.py b/src/cover/views.py index 660909d5..638f1c06 100644 --- a/src/cover/views.py +++ b/src/cover/views.py @@ -1,7 +1,10 @@ # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # +from hashlib import sha1 +from os import makedirs 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 @@ -10,6 +13,8 @@ from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from lxml import etree from librarian import RDFNS, DCNS +from librarian.cover import make_cover +from librarian.dcparser import BookInfo from documents.helpers import active_tab from documents.models import Book, Chunk from cover.models import Image @@ -26,10 +31,6 @@ def preview(request, book, chunk=None, rev=None): If chunk and rev number are given, use version from given revision. If rev is not given, use publishable version. """ - from PIL import Image - from librarian.cover import make_cover - from librarian.dcparser import BookInfo - chunk = Chunk.get(book, chunk) if rev is not None: try: @@ -44,7 +45,8 @@ def preview(request, book, chunk=None, rev=None): try: info = BookInfo.from_bytes(xml) - except: + except Exception as e: + print(e) return HttpResponseRedirect(os.path.join(settings.STATIC_URL, "img/sample_cover.png")) width = request.GET.get('width') width = int(width) if width else None @@ -70,17 +72,11 @@ def preview(request, book, chunk=None, rev=None): @csrf_exempt @require_POST def preview_from_xml(request): - from hashlib import sha1 - from PIL import Image - from os import makedirs - from lxml import etree - from librarian.cover import make_cover - from librarian.dcparser import BookInfo - xml = request.POST['xml'] try: info = BookInfo.from_bytes(xml.encode('utf-8')) - except: + except Exception as e: + print(e) return HttpResponse(os.path.join(settings.STATIC_URL, "img/sample_cover.png")) coverid = sha1(etree.tostring(info.to_etree())).hexdigest() cover = make_cover(info) @@ -91,7 +87,7 @@ def preview_from_xml(request): except OSError: pass fname = os.path.join(cover_dir, "%s.%s" % (coverid, cover.ext())) - img = cover.image().resize(PREVIEW_SIZE, Image.ANTIALIAS) + img = cover.image().resize(PREVIEW_SIZE, PIL.Image.ANTIALIAS) img.save(os.path.join(settings.MEDIA_ROOT, fname)) return HttpResponse(os.path.join(settings.MEDIA_URL, fname)) diff --git a/src/documents/management/commands/fixdc.py b/src/documents/management/commands/fixdc.py deleted file mode 100644 index 3f4a848e..00000000 --- a/src/documents/management/commands/fixdc.py +++ /dev/null @@ -1,52 +0,0 @@ -# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later. -# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. -# -from librarian import RDFNS, WLURI, ValidationError -from librarian.dcparser import BookInfo -from documents.management import XmlUpdater -from documents.management.commands import XmlUpdaterCommand - - -class FixDC(XmlUpdater): - commit_desc = "auto-fixing DC" - retain_publishable = True - only_first_chunk = True - - def fix_wluri(elem, change, verbose): - try: - WLURI.strict(elem.text) - except ValidationError: - correct_field = str(WLURI.from_slug( - WLURI(elem.text.strip()).slug)) - try: - WLURI.strict(correct_field) - except ValidationError: - # Can't make a valid WLURI out of it, leave as is. - return False - if verbose: - print("Changing %s from %s to %s" % ( - elem.tag, elem.text, correct_field - )) - elem.text = correct_field - return True - for field in BookInfo.FIELDS: - if field.validator == WLURI: - XmlUpdater.fixes_elements('.//' + field.uri)(fix_wluri) - - @XmlUpdater.fixes_elements(".//" + RDFNS("Description")) - def fix_rdfabout(elem, change, verbose): - correct_about = change.tree.book.correct_about() - attr_name = RDFNS("about") - current_about = elem.get(attr_name) - if current_about != correct_about: - if verbose: - print("Changing rdf:about from %s to %s" % ( - current_about, correct_about - )) - elem.set(attr_name, correct_about) - return True - - -class Command(XmlUpdaterCommand): - updater = FixDC - help = 'Fixes obvious errors in DC: rdf:about and WLURI format.' diff --git a/src/redakcja/settings/__init__.py b/src/redakcja/settings/__init__.py index 1325743e..3286f073 100644 --- a/src/redakcja/settings/__init__.py +++ b/src/redakcja/settings/__init__.py @@ -290,6 +290,9 @@ REST_FRAMEWORK = { } +TEST_INTEGRATION = False + + try: SENTRY_DSN except NameError: diff --git a/src/redakcja/settings/test.py b/src/redakcja/settings/test.py index 4a80142f..dba64b6b 100644 --- a/src/redakcja/settings/test.py +++ b/src/redakcja/settings/test.py @@ -26,3 +26,16 @@ SECRET_KEY = "not-so-secret" LITERARY_DIRECTOR_USERNAME = 'Kaowiec' + +MIN_COVER_SIZE = (1, 1) + + +class DisableMigrations(object): + + def __contains__(self, item): + return True + + def __getitem__(self, item): + return None + +MIGRATION_MODULES = DisableMigrations() diff --git a/src/redakcja/settings/test_full.py b/src/redakcja/settings/test_full.py new file mode 100644 index 00000000..90d243d3 --- /dev/null +++ b/src/redakcja/settings/test_full.py @@ -0,0 +1,5 @@ +from .test import * + +TEST_INTEGRATION = True + +MIGRATION_MODULES = {} -- 2.20.1 From 17d853e607e5db956defac483a76459eb654811c Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 19 Sep 2022 10:15:30 +0200 Subject: [PATCH 08/16] ad hoc changes --- src/catalogue/admin.py | 18 +++++++++++++- .../documents/active_users_list.html | 2 ++ src/documents/urls.py | 1 + src/documents/views.py | 24 +++++++++++++++---- src/wlxml/views.py | 3 ++- 5 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/catalogue/admin.py b/src/catalogue/admin.py index 2cab170c..54c232f8 100644 --- a/src/catalogue/admin.py +++ b/src/catalogue/admin.py @@ -25,6 +25,7 @@ class AuthorAdmin(WikidataAdminMixin, admin.ModelAdmin): "slug", ] list_filter = ["year_of_death", "priority", "collections", "status", "gender", "nationality"] + list_per_page = 10000000 search_fields = ["first_name", "last_name", "wikidata"] prepopulated_fields = {"slug": ("first_name", "last_name")} autocomplete_fields = ["collections"] @@ -71,6 +72,15 @@ class CoverLicenseFilter(LicenseFilter): license_name_field = 'document_book__dc_cover_image__license_name' +def add_title(base_class, suffix): + class TitledCategoryFilter(base_class): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.title += suffix + return TitledCategoryFilter + + + class BookAdmin(WikidataAdminMixin, NumericFilterModelAdmin): list_display = [ "smart_title", @@ -95,16 +105,22 @@ class BookAdmin(WikidataAdminMixin, NumericFilterModelAdmin): ("pd_year", RangeNumericFilter), "collections", "collections__category", + ("authors__collections", add_title(admin.RelatedFieldListFilter, ' autora')), + ("authors__collections__category", add_title(admin.RelatedFieldListFilter, ' autora')), + ("translators__collections", add_title(admin.RelatedFieldListFilter, ' tłumacza')), + ("translators__collections__category", add_title(admin.RelatedFieldListFilter, ' tłumacza')), "epochs", "kinds", "genres", "priority", "authors__gender", "authors__nationality", "translators__gender", "translators__nationality", "document_book__chunk__stage", - "document_book__chunk__user", + #"document_book__chunk__user", LicenseFilter, CoverLicenseFilter, ] + list_per_page = 1000000 + readonly_fields = ["wikidata_link", "estimated_costs", "documents_book_link"] actions = [export_as_csv_action()] fieldsets = [ diff --git a/src/documents/templates/documents/active_users_list.html b/src/documents/templates/documents/active_users_list.html index ef9532fc..f58febf2 100644 --- a/src/documents/templates/documents/active_users_list.html +++ b/src/documents/templates/documents/active_users_list.html @@ -11,6 +11,8 @@ {% trans "Users active in the year" %} {{ year }} +

Pobierz jako CSV

+
    {% for email, names, count in users %}
  • {% for name in names %}{{ name }}, {% endfor %}{{ email }} ({{ count }})
  • diff --git a/src/documents/urls.py b/src/documents/urls.py index 1a1f1855..71ce10a0 100644 --- a/src/documents/urls.py +++ b/src/documents/urls.py @@ -58,6 +58,7 @@ urlpatterns = [ path('track//', PublishTrackFeed()), path('active/', views.active_users_list, name='active_users_list'), + path('active.csv', views.active_users_list, kwargs={'csv': True}, name='active_users_csv'), path('mark-final/', views.mark_final, name='mark_final'), path('mark-final-completed/', views.mark_final_completed, name='mark_final_completed'), diff --git a/src/documents/views.py b/src/documents/views.py index 6c9f29e3..ea222361 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -634,7 +634,7 @@ class GalleryView(UploadView): return "%s%s/" % (settings.IMAGE_DIR, self.object.gallery) -def active_users_list(request): +def active_users_list(request, csv=False): year = int(request.GET.get('y', date.today().year)) by_user = defaultdict(lambda: 0) by_email = defaultdict(lambda: 0) @@ -659,10 +659,24 @@ def active_users_list(request): for email, count in by_email.items(): active_users.append((email, names_by_email[email], count)) active_users.sort(key=lambda x: -x[2]) - return render(request, 'documents/active_users_list.html', { - 'users': active_users, - 'year': year, - }) + if csv: + return http.HttpResponse( + '\n'.join(( + ','.join( + (str(x[2]), x[0], ','.join(x[1])) + ) + for x in active_users + )), + content_type='text/csv', + headers={ + 'Content-Disposition': f'attachment; filename=redakcja-{year}.csv', + } + ) + else: + return render(request, 'documents/active_users_list.html', { + 'users': active_users, + 'year': year, + }) @user_passes_test(lambda u: u.is_superuser) diff --git a/src/wlxml/views.py b/src/wlxml/views.py index 281ced5d..edf8a920 100644 --- a/src/wlxml/views.py +++ b/src/wlxml/views.py @@ -48,7 +48,7 @@ class TagView(DetailView): VALUE_TYPES = { LegimiCategory: { 'widget': 'select', - 'options': list(legimi.CATEGORIES.keys()), + 'options': [''] + list(legimi.CATEGORIES.keys()), }, Epoch: { 'autocomplete': { @@ -104,6 +104,7 @@ VALUE_TYPES = { "language": { 'widget': 'select', 'options': [ + '', 'pol', 'eng', 'fre', -- 2.20.1 From 022b1747124fb6b6314c4eccf98d5bf89dff5d25 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 19 Sep 2022 12:40:27 +0200 Subject: [PATCH 09/16] Catalogue improvements. --- src/catalogue/admin.py | 59 +++++++++++++++++-- ...of_birth_author_place_of_death_and_more.py | 28 +++++++++ ...2_book_free_license_book_polona_missing.py | 23 ++++++++ src/catalogue/models.py | 11 +++- 4 files changed, 114 insertions(+), 7 deletions(-) create mode 100644 src/catalogue/migrations/0031_author_place_of_birth_author_place_of_death_and_more.py create mode 100644 src/catalogue/migrations/0032_book_free_license_book_polona_missing.py diff --git a/src/catalogue/admin.py b/src/catalogue/admin.py index 54c232f8..8d641545 100644 --- a/src/catalogue/admin.py +++ b/src/catalogue/admin.py @@ -2,7 +2,7 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # from django.contrib import admin -from django.utils.html import escape +from django.utils.html import escape, format_html from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from admin_numeric_filter.admin import RangeNumericFilter, NumericFilterModelAdmin @@ -24,7 +24,17 @@ class AuthorAdmin(WikidataAdminMixin, admin.ModelAdmin): "wikidata_link", "slug", ] - list_filter = ["year_of_death", "priority", "collections", "status", "gender", "nationality"] + list_display_links = [ + "first_name", "last_name" + ] + list_filter = [ + ("year_of_death", RangeNumericFilter), + "priority", + "collections", + "status", + "gender", + "nationality", + ] list_per_page = 10000000 search_fields = ["first_name", "last_name", "wikidata"] prepopulated_fields = {"slug": ("first_name", "last_name")} @@ -114,15 +124,41 @@ class BookAdmin(WikidataAdminMixin, NumericFilterModelAdmin): "authors__gender", "authors__nationality", "translators__gender", "translators__nationality", "document_book__chunk__stage", - #"document_book__chunk__user", LicenseFilter, CoverLicenseFilter, + 'free_license', + 'polona_missing', ] list_per_page = 1000000 - readonly_fields = ["wikidata_link", "estimated_costs", "documents_book_link"] - actions = [export_as_csv_action()] + readonly_fields = [ + "wikidata_link", + "estimated_costs", + "documents_book_link", + "scans_source_link", + ] + actions = [export_as_csv_action( + fields=[ + "id", + "wikidata", + "slug", + "title", + "authors_str", # authors? + "translators_str", # translators? + "language", + "based_on", + "scans_source", + "text_source", + "notes", + "priority", + "pd_year", + "gazeta_link", + "estimated_chars", + "estimated_verses", + "estimate_source" + ] + )] fieldsets = [ (None, {"fields": [("wikidata", "wikidata_link")]}), ( @@ -153,7 +189,8 @@ class BookAdmin(WikidataAdminMixin, NumericFilterModelAdmin): _("Plan"), { "fields": [ - "scans_source", + ("free_license", "polona_missing"), + ("scans_source", "scans_source_link"), "text_source", "priority", "collections", @@ -199,6 +236,16 @@ class BookAdmin(WikidataAdminMixin, NumericFilterModelAdmin): return mark_safe(''.format(book.get_absolute_url(), book.slug)) documents_book_link.short_description = _('Book') + def scans_source_link(self, obj): + if obj.scans_source: + return format_html( + '{url}', + url=obj.scans_source, + ) + else: + return "" + scans_source_link.short_description = _('scans source') + admin.site.register(models.Book, BookAdmin) diff --git a/src/catalogue/migrations/0031_author_place_of_birth_author_place_of_death_and_more.py b/src/catalogue/migrations/0031_author_place_of_birth_author_place_of_death_and_more.py new file mode 100644 index 00000000..b1ecc81c --- /dev/null +++ b/src/catalogue/migrations/0031_author_place_of_birth_author_place_of_death_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.0.6 on 2022-09-19 12:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalogue', '0030_auto_20210706_1408'), + ] + + operations = [ + migrations.AddField( + model_name='author', + name='place_of_birth', + field=models.CharField(blank=True, max_length=255, verbose_name='place of birth'), + ), + migrations.AddField( + model_name='author', + name='place_of_death', + field=models.CharField(blank=True, max_length=255, verbose_name='place of death'), + ), + migrations.AddField( + model_name='author', + name='year_of_birth', + field=models.SmallIntegerField(blank=True, null=True, verbose_name='year of birth'), + ), + ] diff --git a/src/catalogue/migrations/0032_book_free_license_book_polona_missing.py b/src/catalogue/migrations/0032_book_free_license_book_polona_missing.py new file mode 100644 index 00000000..dd9a6fa6 --- /dev/null +++ b/src/catalogue/migrations/0032_book_free_license_book_polona_missing.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.6 on 2022-09-19 12:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalogue', '0031_author_place_of_birth_author_place_of_death_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='book', + name='free_license', + field=models.BooleanField(default=False, verbose_name='free license'), + ), + migrations.AddField( + model_name='book', + name='polona_missing', + field=models.BooleanField(default=False, verbose_name='missing on Polona'), + ), + ] diff --git a/src/catalogue/models.py b/src/catalogue/models.py index b0d3697b..4568af10 100644 --- a/src/catalogue/models.py +++ b/src/catalogue/models.py @@ -19,7 +19,10 @@ class Author(WikidataMixin, models.Model): gender = models.CharField(_("gender"), max_length=255, blank=True) nationality = models.CharField(_("nationality"), max_length=255, blank=True) + year_of_birth = models.SmallIntegerField(_("year of birth"), null=True, blank=True) + place_of_birth = models.CharField(_('place of birth'), max_length=255, blank=True) year_of_death = models.SmallIntegerField(_("year of death"), null=True, blank=True) + place_of_death = models.CharField(_('place of death'), max_length=255, blank=True) status = models.PositiveSmallIntegerField( _("status"), null=True, @@ -58,7 +61,10 @@ class Author(WikidataMixin, models.Model): notes = "description" def __str__(self): - return f"{self.first_name} {self.last_name}" + name = f"{self.first_name} {self.last_name}" + if self.year_of_death is not None: + name += f' (zm. {self.year_of_death})' + return name def get_absolute_url(self): return reverse("catalogue_author", args=[self.slug]) @@ -139,6 +145,9 @@ class Book(WikidataMixin, models.Model): estimated_verses = models.IntegerField(_("estimated number of verses"), null=True, blank=True) estimate_source = models.CharField(_("source of estimates"), max_length=2048, blank=True) + free_license = models.BooleanField(_('free license'), default=False) + polona_missing = models.BooleanField(_('missing on Polona'), default=False) + class Meta: ordering = ("title",) verbose_name = _('book') -- 2.20.1 From 631e1c32e1c23b24eda0c747f6bbd30cb394b791 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 19 Sep 2022 15:47:02 +0200 Subject: [PATCH 10/16] More author data in catalogue. --- requirements/notes | 4 + requirements/requirements.txt | 2 + src/catalogue/admin.py | 13 +- src/catalogue/locale/pl/LC_MESSAGES/django.mo | Bin 3327 -> 3607 bytes src/catalogue/locale/pl/LC_MESSAGES/django.po | 171 +++++++++++------- ...iption_pl_author_first_name_de_and_more.py | 88 +++++++++ src/catalogue/migrations/0034_notablebook.py | 27 +++ src/catalogue/models.py | 8 +- src/catalogue/translation.py | 13 ++ src/redakcja/settings/__init__.py | 10 + 10 files changed, 266 insertions(+), 70 deletions(-) create mode 100644 requirements/notes create mode 100644 src/catalogue/migrations/0033_author_description_pl_author_first_name_de_and_more.py create mode 100644 src/catalogue/migrations/0034_notablebook.py create mode 100644 src/catalogue/translation.py diff --git a/requirements/notes b/requirements/notes new file mode 100644 index 00000000..1795c502 --- /dev/null +++ b/requirements/notes @@ -0,0 +1,4 @@ + +Django 4 needs 3.8 +3.9 is fine +3.10 not compatible with lxml diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 2f146c98..eba62b92 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -27,5 +27,7 @@ django-debug-toolbar==3.5.0 django-admin-numeric-filter @ git+https://github.com/lukasvinclav/django-admin-numeric-filter.git@83853dfcc8d354c7aea8020a075bbd83dc8ca171 djangorestframework==3.13.1 django-filter==22.1 +django-modeltranslation==0.18.4 +django-admin-ordering==0.15 sentry-sdk==1.8.0 diff --git a/src/catalogue/admin.py b/src/catalogue/admin.py index 8d641545..47152ebb 100644 --- a/src/catalogue/admin.py +++ b/src/catalogue/admin.py @@ -6,13 +6,21 @@ from django.utils.html import escape, format_html from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from admin_numeric_filter.admin import RangeNumericFilter, NumericFilterModelAdmin +from admin_ordering.admin import OrderableAdmin from fnpdjango.actions import export_as_csv_action +from modeltranslation.admin import TabbedTranslationAdmin from . import models import documents.models from .wikidata import WikidataAdminMixin -class AuthorAdmin(WikidataAdminMixin, admin.ModelAdmin): +class NotableBookInline(OrderableAdmin, admin.TabularInline): + model = models.NotableBook + raw_id_fields = ['book'] + ordering_field_hide_input = True + + +class AuthorAdmin(WikidataAdminMixin, TabbedTranslationAdmin): list_display = [ "first_name", "last_name", @@ -39,6 +47,9 @@ class AuthorAdmin(WikidataAdminMixin, admin.ModelAdmin): search_fields = ["first_name", "last_name", "wikidata"] prepopulated_fields = {"slug": ("first_name", "last_name")} autocomplete_fields = ["collections"] + inlines = [ + NotableBookInline, + ] admin.site.register(models.Author, AuthorAdmin) diff --git a/src/catalogue/locale/pl/LC_MESSAGES/django.mo b/src/catalogue/locale/pl/LC_MESSAGES/django.mo index 3c0c51d56546f913a9af22427ffc9bac05af110b..3dbf04bec5bdaef81f356104f7704cb34ebdbb8f 100644 GIT binary patch delta 1822 zcmY+ETWDNW6oz*+lVmbub7^dhP2*T=jcpTWk|vroY8%rBqYn~96zRjsbkdw=GDl`6 z6!S7*s8EHV9;1a+5S98+FO=q`R;1XDf_D&Ntcf$t! z9`gs`I{aO57aWHg|1`YTxsrR14<7d-F9lqNjqp`i4^KcIcgpL`IhW)^0Y)HW-6-sW_e1S`8Y>leNG_<7KG0zeD!sYRE#2#yEcsY@tDkv_mb>1;^nI^AACdI}A1PS*Q|+P=Sv^ za&WIfjXMD~?xgjfHl8znZu~Yz{WbAN8d~86(@{V{{*R``_*^_^0=$K6gW=# zv``z|05?Ll_d#8{Vb}~GgsNO=pLHC7Z8SUy6<`r+;F9^rjc-FuTrvLxV;L%u&y1^( z$Ng;n6{x#XLs_^gm85{~zjM84JGv7k(Kd8D8bS)9BiM=lYs2Q~Co1b$gByVNpj(lC z#{H;js-#SnRCVst`h#l0#?ZY;|5ANO=cj)}*>2R0ZbCBcR8{Cfs@5iSGunt`w;=7d zYIpOo!<@Zv#J=0Ke_fEO>CAPZ?n0Z9&N_i)DRf8WYwfia=+f>%x|}*+nU0P7`#;-) zcN^m0-HjstdMQzNIxI%IVteL_(?Q;i2Ek1Dd}N{V;k=hCWW8dL57*bd5Pn=Y5u3_q zGKs98&g2SVd;RW~S-(*5a|aSZF7Z&14RT)iNc}`-)=Os+!Bk?>&ljiN6IFMA#w$*T z$Lo(oDjnh0hNRB?QintSl-PP{PTy) z@6UK)Z{yKuvHaBBte2k8gyqJiFc%wJcPPl_ytTa22fgrQ?184qyf>4ev67ab3IB{e nx_Q>m94wI9Tt3)8pUL^2tG1Mv`O2sLa3a1K{kJ>(F23a-r{T{c delta 1556 zcmY+EO>9h26vuC?(;1zaqUxix+G6OaVP-m`s~RqE&^4^a`Tf=1$J1x3##z-P%GbJ_qUth zZ`=)o%bwFa&Se{rgbkpF$P*0xHj2i+`~A7pOeHp%&yP znfw}Cw7%qj{$}}7+b8Zt{hX04rN*rY{IowKPJ5;q>Uzz{b{7+DYe1*#Y3sSQy=lsMl)KSjO z(<)&EYJwP?4O1511XX!2oB=bi77jvX4ha#BH zrDXd4W$TJ4w2ju+KY~qianyyPNY-9Nq2;&>wt`kKV#%7Uqd%mJwCs#}-+T?;g32S_ z^~zJt1G^6F+@H-2We4{*wG8Zvy8i-uhN51lzt_9q@A5wSw|aenW8UXLW_mo9NW|jF zrbKd0I@KCajQ3B?lub#lX!9OawRvU1bKd3PnVEyR?15s^Vo%x9p$*<}sK=2 && n%10<=4) && (n" -"%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n" -"%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" -"X-Generator: Poedit 2.3\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && " +"(n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && " +"n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" +"X-Generator: Poedit 3.0.1\n" -#: catalogue/admin.py:62 +#: catalogue/admin.py:176 msgid "Identification" msgstr "Identyfikacja" -#: catalogue/admin.py:76 +#: catalogue/admin.py:190 msgid "Features" msgstr "Cechy" -#: catalogue/admin.py:86 +#: catalogue/admin.py:200 msgid "Plan" msgstr "Plan" +#: catalogue/admin.py:242 +#, fuzzy +#| msgid "title" +msgid "Title" +msgstr "tytuł" + +#: catalogue/admin.py:248 +msgid "Book" +msgstr "książka" + +#: catalogue/admin.py:258 catalogue/models.py:139 +msgid "scans source" +msgstr "źródło skanów" + #: catalogue/models.py:15 msgid "first name" msgstr "imię" @@ -56,200 +70,217 @@ msgid "nationality" msgstr "narodowość" #: catalogue/models.py:23 +msgid "year of birth" +msgstr "rok urodzenia" + +#: catalogue/models.py:24 +msgid "place of birth" +msgstr "miejsce urodzenia" + +#: catalogue/models.py:25 msgid "year of death" msgstr "rok śmierci" -#: catalogue/models.py:25 +#: catalogue/models.py:26 +msgid "place of death" +msgstr "miejsce śmierci" + +#: catalogue/models.py:28 msgid "status" msgstr "status" -#: catalogue/models.py:29 +#: catalogue/models.py:32 msgid "Alive" msgstr "Å»yje" -#: catalogue/models.py:30 +#: catalogue/models.py:33 msgid "Dead" msgstr "Zmarły" -#: catalogue/models.py:31 +#: catalogue/models.py:34 msgid "Long dead" msgstr "Dawno zmarły" -#: catalogue/models.py:32 +#: catalogue/models.py:35 msgid "Unknown" msgstr "Nieznany" -#: catalogue/models.py:35 catalogue/models.py:126 +#: catalogue/models.py:38 catalogue/models.py:141 catalogue/models.py:207 +#: catalogue/models.py:225 msgid "notes" msgstr "notatki" -#: catalogue/models.py:36 catalogue/models.py:132 +#: catalogue/models.py:39 catalogue/models.py:147 msgid "gazeta link" msgstr "link do bazy gazety" -#: catalogue/models.py:37 +#: catalogue/models.py:40 msgid "culture.pl link" msgstr "link do bazy culture.pl" -#: catalogue/models.py:39 +#: catalogue/models.py:42 msgid "description" msgstr "opis" -#: catalogue/models.py:40 -msgid "description (de)" -msgstr "opis (de)" - -#: catalogue/models.py:41 -msgid "description (lt)" -msgstr "opis (lt)" - -#: catalogue/models.py:44 catalogue/models.py:128 catalogue/models.py:253 +#: catalogue/models.py:47 catalogue/models.py:143 catalogue/models.py:275 msgid "priority" msgstr "priorytet" -#: catalogue/models.py:45 catalogue/models.py:129 +#: catalogue/models.py:48 catalogue/models.py:144 msgid "Low" msgstr "Niski" -#: catalogue/models.py:45 catalogue/models.py:129 +#: catalogue/models.py:48 catalogue/models.py:144 msgid "Medium" msgstr "Średni" -#: catalogue/models.py:45 catalogue/models.py:129 +#: catalogue/models.py:48 catalogue/models.py:144 msgid "High" msgstr "Wysoki" -#: catalogue/models.py:47 catalogue/models.py:133 catalogue/models.py:208 -#: catalogue/models.py:260 +#: catalogue/models.py:50 catalogue/models.py:148 catalogue/models.py:230 +#: catalogue/models.py:282 msgid "collections" msgstr "kolekcje" -#: catalogue/models.py:50 +#: catalogue/models.py:53 msgid "author" msgstr "autor" -#: catalogue/models.py:51 catalogue/models.py:107 +#: catalogue/models.py:54 catalogue/models.py:122 msgid "authors" msgstr "autorzy" -#: catalogue/models.py:78 catalogue/models.py:185 catalogue/models.py:201 -#: catalogue/models.py:233 +#: catalogue/models.py:93 catalogue/models.py:205 catalogue/models.py:222 +#: catalogue/models.py:255 msgid "name" msgstr "nazwa" -#: catalogue/models.py:89 +#: catalogue/models.py:104 msgid "epoch" msgstr "epoka" -#: catalogue/models.py:90 catalogue/models.py:115 catalogue/models.py:257 +#: catalogue/models.py:105 catalogue/models.py:130 catalogue/models.py:279 msgid "epochs" msgstr "epoki" -#: catalogue/models.py:95 +#: catalogue/models.py:110 msgid "genre" msgstr "gatunek" -#: catalogue/models.py:96 catalogue/models.py:117 catalogue/models.py:259 +#: catalogue/models.py:111 catalogue/models.py:132 catalogue/models.py:281 msgid "genres" msgstr "gatunki" -#: catalogue/models.py:101 +#: catalogue/models.py:116 msgid "kind" msgstr "rodzaj" -#: catalogue/models.py:102 catalogue/models.py:116 catalogue/models.py:258 +#: catalogue/models.py:117 catalogue/models.py:131 catalogue/models.py:280 msgid "kinds" msgstr "rodzaje" -#: catalogue/models.py:113 +#: catalogue/models.py:128 msgid "translators" msgstr "tłumacze" -#: catalogue/models.py:118 +#: catalogue/models.py:133 msgid "title" msgstr "tytuł" -#: catalogue/models.py:119 +#: catalogue/models.py:134 msgid "language" msgstr "język" -#: catalogue/models.py:122 +#: catalogue/models.py:137 msgid "based on" msgstr "oparte na" -#: catalogue/models.py:124 -msgid "scans source" -msgstr "źródło skanów" - -#: catalogue/models.py:125 +#: catalogue/models.py:140 msgid "text source" msgstr "źródło tekstu" -#: catalogue/models.py:131 +#: catalogue/models.py:146 msgid "year of entry into PD" msgstr "rok wstąpienia do DP" -#: catalogue/models.py:135 +#: catalogue/models.py:150 msgid "estimated number of characters" msgstr "szacowana liczba znaków" -#: catalogue/models.py:136 +#: catalogue/models.py:151 msgid "estimated number of verses" msgstr "szacowana liczba wersów" -#: catalogue/models.py:137 +#: catalogue/models.py:152 msgid "source of estimates" msgstr "źródło szacunków" -#: catalogue/models.py:143 +#: catalogue/models.py:154 +msgid "free license" +msgstr "wolna licencja" + +#: catalogue/models.py:155 +msgid "missing on Polona" +msgstr "brak na Polonie" + +#: catalogue/models.py:159 msgid "book" msgstr "książka" -#: catalogue/models.py:144 +#: catalogue/models.py:160 msgid "books" msgstr "książki" -#: catalogue/models.py:186 +#: catalogue/models.py:190 +msgid "Author" +msgstr "autor" + +#: catalogue/models.py:195 +msgid "Translator" +msgstr "tłumacze" + +#: catalogue/models.py:206 msgid "parent" msgstr "rodzic" -#: catalogue/models.py:190 +#: catalogue/models.py:211 msgid "collection category" msgstr "kategoria kolekcji" -#: catalogue/models.py:191 +#: catalogue/models.py:212 msgid "collection categories" msgstr "kategorie kolekcji" -#: catalogue/models.py:203 +#: catalogue/models.py:224 msgid "category" msgstr "kategoria" -#: catalogue/models.py:207 +#: catalogue/models.py:229 msgid "collection" msgstr "kolekcja" -#: catalogue/models.py:237 catalogue/models.py:256 +#: catalogue/models.py:259 catalogue/models.py:278 msgid "work type" msgstr "rodzaj pracy" -#: catalogue/models.py:238 +#: catalogue/models.py:260 msgid "work types" msgstr "rodzaje prac" -#: catalogue/models.py:254 +#: catalogue/models.py:276 msgid "per normalized page" msgstr "za stronę maszynopisu" -#: catalogue/models.py:255 +#: catalogue/models.py:277 msgid "per verse" msgstr "za wers" -#: catalogue/models.py:264 +#: catalogue/models.py:286 msgid "work rate" msgstr "stawka" -#: catalogue/models.py:265 +#: catalogue/models.py:287 msgid "work rates" msgstr "stawki" @@ -279,3 +310,9 @@ msgstr[3] "" #: catalogue/wikidata.py:18 msgid "If you have a Wikidata ID, like \"Q1337\", enter it and save." msgstr "Jeśli masz identyfikator Wikidanych , jak „Q1337”, wklej go i zapisz." + +#~ msgid "description (de)" +#~ msgstr "opis (de)" + +#~ msgid "description (lt)" +#~ msgstr "opis (lt)" diff --git a/src/catalogue/migrations/0033_author_description_pl_author_first_name_de_and_more.py b/src/catalogue/migrations/0033_author_description_pl_author_first_name_de_and_more.py new file mode 100644 index 00000000..46db716b --- /dev/null +++ b/src/catalogue/migrations/0033_author_description_pl_author_first_name_de_and_more.py @@ -0,0 +1,88 @@ +# Generated by Django 4.0.6 on 2022-09-19 15:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalogue', '0032_book_free_license_book_polona_missing'), + ] + + operations = [ + migrations.AddField( + model_name='author', + name='description_pl', + field=models.TextField(blank=True, null=True, verbose_name='description'), + ), + migrations.AddField( + model_name='author', + name='first_name_de', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='first name'), + ), + migrations.AddField( + model_name='author', + name='first_name_lt', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='first name'), + ), + migrations.AddField( + model_name='author', + name='first_name_pl', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='first name'), + ), + migrations.AddField( + model_name='author', + name='last_name_de', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='last name'), + ), + migrations.AddField( + model_name='author', + name='last_name_lt', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='last name'), + ), + migrations.AddField( + model_name='author', + name='last_name_pl', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='last name'), + ), + migrations.AddField( + model_name='author', + name='place_of_birth_de', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='place of birth'), + ), + migrations.AddField( + model_name='author', + name='place_of_birth_lt', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='place of birth'), + ), + migrations.AddField( + model_name='author', + name='place_of_birth_pl', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='place of birth'), + ), + migrations.AddField( + model_name='author', + name='place_of_death_de', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='place of death'), + ), + migrations.AddField( + model_name='author', + name='place_of_death_lt', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='place of death'), + ), + migrations.AddField( + model_name='author', + name='place_of_death_pl', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='place of death'), + ), + migrations.AlterField( + model_name='author', + name='description_de', + field=models.TextField(blank=True, null=True, verbose_name='description'), + ), + migrations.AlterField( + model_name='author', + name='description_lt', + field=models.TextField(blank=True, null=True, verbose_name='description'), + ), + ] diff --git a/src/catalogue/migrations/0034_notablebook.py b/src/catalogue/migrations/0034_notablebook.py new file mode 100644 index 00000000..611b5e1b --- /dev/null +++ b/src/catalogue/migrations/0034_notablebook.py @@ -0,0 +1,27 @@ +# Generated by Django 4.0.6 on 2022-09-19 15:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalogue', '0033_author_description_pl_author_first_name_de_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='NotableBook', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ordering', models.PositiveIntegerField(default=0, verbose_name='ordering')), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='catalogue.author')), + ('book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='catalogue.book')), + ], + options={ + 'ordering': ['ordering'], + 'abstract': False, + }, + ), + ] diff --git a/src/catalogue/models.py b/src/catalogue/models.py index 4568af10..d0698808 100644 --- a/src/catalogue/models.py +++ b/src/catalogue/models.py @@ -4,6 +4,7 @@ from django.apps import apps from django.db import models from django.urls import reverse from django.utils.translation import gettext_lazy as _ +from admin_ordering.models import OrderableModel from wikidata.client import Client from .constants import WIKIDATA from .wikidata import WikidataMixin @@ -39,8 +40,6 @@ class Author(WikidataMixin, models.Model): culturepl_link = models.CharField(_("culture.pl link"), max_length=255, blank=True) description = models.TextField(_("description"), blank=True) - description_de = models.TextField(_("description (de)"), blank=True) - description_lt = models.TextField(_("description (lt)"), blank=True) priority = models.PositiveSmallIntegerField( _("priority"), @@ -83,6 +82,11 @@ class Author(WikidataMixin, models.Model): return None +class NotableBook(OrderableModel): + author = models.ForeignKey(Author, models.CASCADE) + book = models.ForeignKey('Book', models.CASCADE) + + class Category(WikidataMixin, models.Model): name = models.CharField(_("name"), max_length=255) slug = models.SlugField(max_length=255, unique=True) diff --git a/src/catalogue/translation.py b/src/catalogue/translation.py new file mode 100644 index 00000000..0ee80711 --- /dev/null +++ b/src/catalogue/translation.py @@ -0,0 +1,13 @@ +from modeltranslation.translator import register, TranslationOptions +from . import models + + +@register(models.Author) +class AuthorTranslationOptions(TranslationOptions): + fields = ( + 'first_name', + 'last_name', + 'place_of_birth', + 'place_of_death', + 'description', + ) diff --git a/src/redakcja/settings/__init__.py b/src/redakcja/settings/__init__.py index 3286f073..ecebe40e 100644 --- a/src/redakcja/settings/__init__.py +++ b/src/redakcja/settings/__init__.py @@ -69,6 +69,8 @@ if CAS_SERVER_URL: ROOT_URLCONF = 'redakcja.urls' INSTALLED_APPS = ( + 'modeltranslation', + 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', @@ -89,6 +91,7 @@ INSTALLED_APPS = ( 'bootstrap4', 'rest_framework', 'django_filters', + 'admin_ordering', 'redakcja.api', 'catalogue', @@ -290,6 +293,13 @@ REST_FRAMEWORK = { } +LANGUAGES = [ + ('pl', 'polski'), + ('de', 'Deutsch'), + ('lt', 'lietuvių'), +] + + TEST_INTEGRATION = False -- 2.20.1 From 66f7c2655c3bbc544569b6b5111b50b14cb9336b Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 20 Sep 2022 14:15:43 +0200 Subject: [PATCH 11/16] Fix after upgrading django-filter. --- src/documents/api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/documents/api/views.py b/src/documents/api/views.py index a2f3529d..36e886c2 100644 --- a/src/documents/api/views.py +++ b/src/documents/api/views.py @@ -23,7 +23,7 @@ class BookDetail(RetrieveAPIView): class ChunkList(ListAPIView): queryset = models.Chunk.objects.all() serializer_class = serializers.ChunkSerializer - filter_fields = ['user', 'stage'] + filterset_fields = ['user', 'stage'] search_fields = ['book__title'] def get_queryset(self): -- 2.20.1 From fe3dc36ec67496aa078963997e86ea8cef65645d Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Thu, 22 Sep 2022 14:23:11 +0200 Subject: [PATCH 12/16] fixes --- src/documents/views.py | 6 +++--- src/fileupload/views.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/documents/views.py b/src/documents/views.py index ea222361..a5b99710 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -99,7 +99,7 @@ def activity(request, isodate=None): @never_cache def logout_then_redirect(request): auth.logout(request) - return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?=')) + return http.HttpResponseRedirect(quote_plus(request.GET.get('next', '/'), safe='/?=')) @permission_required('documents.add_book') @@ -444,7 +444,7 @@ def chunk_edit(request, slug, chunk): form.save() go_next = request.GET.get('next', None) if go_next: - go_next = urlquote_plus(unquote(iri_to_uri(go_next)), safe='/?=&') + go_next = quote_plus(unquote(iri_to_uri(go_next)), safe='/?=&') else: go_next = doc.book.get_absolute_url() return http.HttpResponseRedirect(go_next) @@ -455,7 +455,7 @@ def chunk_edit(request, slug, chunk): if referer: parts = urlsplit(referer) parts = ['', ''] + list(parts[2:]) - go_next = urlquote_plus(urlunsplit(parts)) + go_next = quote_plus(urlunsplit(parts)) else: go_next = '' diff --git a/src/fileupload/views.py b/src/fileupload/views.py index 56a198ed..23fbc3bb 100644 --- a/src/fileupload/views.py +++ b/src/fileupload/views.py @@ -98,7 +98,7 @@ class UploadView(FormView): return super(UploadView, self).dispatch(request, *args, **kwargs) def get(self, request, *args, **kwargs): - if request.is_ajax(): + if request.headers.get('x-requested-with') == 'XMLHttpRequest': files = [] path = self.get_safe_path() if os.path.isdir(path): -- 2.20.1 From 8161763bf98b7f396ecc349ccd4670b8de592f40 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Thu, 22 Sep 2022 16:03:22 +0200 Subject: [PATCH 13/16] More data in catalogue --- src/catalogue/admin.py | 36 +++- src/catalogue/constants.py | 3 + src/catalogue/locale/pl/LC_MESSAGES/django.mo | Bin 3607 -> 4068 bytes src/catalogue/locale/pl/LC_MESSAGES/django.po | 162 +++++++++++------- ...e_remove_author_place_of_birth_and_more.py | 83 +++++++++ ...or_place_of_birth_author_place_of_death.py | 24 +++ ...s_author_year_of_birth_inexact_and_more.py | 27 +++ src/catalogue/models.py | 45 ++++- src/catalogue/translation.py | 10 +- src/catalogue/wikidata.py | 45 +++-- 10 files changed, 344 insertions(+), 91 deletions(-) create mode 100644 src/catalogue/migrations/0035_place_remove_author_place_of_birth_and_more.py create mode 100644 src/catalogue/migrations/0036_author_place_of_birth_author_place_of_death.py create mode 100644 src/catalogue/migrations/0037_alter_place_options_author_year_of_birth_inexact_and_more.py diff --git a/src/catalogue/admin.py b/src/catalogue/admin.py index 47152ebb..213e9ca7 100644 --- a/src/catalogue/admin.py +++ b/src/catalogue/admin.py @@ -16,7 +16,7 @@ from .wikidata import WikidataAdminMixin class NotableBookInline(OrderableAdmin, admin.TabularInline): model = models.NotableBook - raw_id_fields = ['book'] + autocomplete_fields = ['book'] ordering_field_hide_input = True @@ -45,8 +45,35 @@ class AuthorAdmin(WikidataAdminMixin, TabbedTranslationAdmin): ] list_per_page = 10000000 search_fields = ["first_name", "last_name", "wikidata"] + readonly_fields = ["wikidata_link"] + + fieldsets = [ + (None, {"fields": [("wikidata", "wikidata_link")]}), + ( + _("Identification"), + { + "fields": [ + ("first_name", "last_name"), + "slug", + "gender", + "nationality", + ("date_of_birth", "year_of_birth", "year_of_birth_inexact", "year_of_birth_range", "place_of_birth"), + ("date_of_death", "year_of_death", "year_of_death_inexact", "year_of_death_range", "place_of_death"), + "description", + "status", + "collections", + "priority", + + "notes", + "gazeta_link", + "culturepl_link", + ] + }, + ), + ] + prepopulated_fields = {"slug": ("first_name", "last_name")} - autocomplete_fields = ["collections"] + autocomplete_fields = ["collections", "place_of_birth", "place_of_death"] inlines = [ NotableBookInline, ] @@ -316,3 +343,8 @@ class WorkTypeAdmin(admin.ModelAdmin): admin.site.register(models.WorkType, WorkTypeAdmin) + + +@admin.register(models.Place) +class PlaceAdmin(WikidataAdminMixin, TabbedTranslationAdmin): + search_fields = ['name'] diff --git a/src/catalogue/constants.py b/src/catalogue/constants.py index 9134e2d3..0f961b9d 100644 --- a/src/catalogue/constants.py +++ b/src/catalogue/constants.py @@ -2,10 +2,13 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # class WIKIDATA: + PLACE_OF_BIRTH = 'P19' + PLACE_OF_DEATH = 'P20' GENDER = "P21" AUTHOR = "P50" CREATOR = "P170" LANGUAGE = "P407" + DATE_OF_BIRTH = "P569" DATE_OF_DEATH = "P570" LAST_NAME = "P734" GIVEN_NAME = "P735" diff --git a/src/catalogue/locale/pl/LC_MESSAGES/django.mo b/src/catalogue/locale/pl/LC_MESSAGES/django.mo index 3dbf04bec5bdaef81f356104f7704cb34ebdbb8f..39239f881d2b50e2719f557197b2a02ac0521480 100644 GIT binary patch literal 4068 zcmZvdU2I%O6~|9$3&jnk4TY9c!ZeW7PQ32!Iwr{`PMXAV>&8x;$iWptZO8YH*W-J4 zW_9PTH+NN})KVx<6_qGz8a3h}^Z}_4RVxvozyn_O0jUxnLRE!|$O9D;-%=$IQpNw= zJL}!3x!N)9RRn2JpLqjGx#`&A@mK9*9$=QvjPr*2E>mt zG(7GZuYVh4Ki>g)-bIk-UH0}XAkTjl{497C{0#UucsqCv{51G?@KfN2o?Eae_umKd zdOJb(I|^puc{y)MY} ze+6<}e+%-wcR*hMk08h6FCdTqH^}4v2uL0*3c z$m4eT{)*T4gS_vDLB8i1kiU=e(W(dHjQ3-{*N4Mj%2%=fE$5 z-}3e=AlJLsK+e0jL8RU0v(vv-z_Nemb%!A}$T0}Vou7vxpNDW9#vxqiIF^IYJ}7)H zemIV&@;B5KPre&IFF*SW&f)&LiR%VnSyYh^O?Q^;j=yn;fM3^ zQOF_4S0Nma9T0vVf*|Cy8&ZNyL-?se<{*5R$03hExL!{}z6|L-0xu(wlaO5yu8|y1 zzMJ0j1^@CT@BzqSuO9(9w~j*ijydnRw)UPe|1t~CK=9W@d%TTy3ylj{^m zM`FFE=t$9Z)Y^!S$yCO+-cmHBWJuFWrmaLddQ{g}XeLx9)yq1NskVk@mPKY;Vnwbg zA;qGObcoqvW@=Q#I#Oc$>B{){-tD6Tvs5Bvb8o6?gB8kKN6MwTfr+6o zt;P~UV3$R(A`=-P9yjh@LkG6EtP__CBO8jA6Q!_+KpCf~E;|Y#&bL%oCR7Ol2|BoS zq>Va<)xm4$U!h2wFmD_ovh|j%E4=g;jxFCK?!Td()3^?6#Jr8Ik(4{;Mhu5)Bv-|7 zobnr=QpP&X$k-Gfn=--mQWK{$Hff-%gY!1o72ERjP&1YRbZ$8YMG!aDB8s_+^%89p zUfu=B0O4#a!PEjgw--}KE=6uSO4YfvH!RI_C*|B3qa1k(IcnP^5(%!N|K^Qp*7Qbx zbCfBW-&3*2>$Fjrg>0l3J0i(YPni1mG_sbRZ*F!P3+xWu9nL~ zv$)q%|Es~wS%2lF1tl8;dQCLrp}Cp4<9+9qvGUL{i?7sR4TPa4II0ifO0+qUr>s-y z|4_5(0trVWL%m?GPs&B&0KYpdk(-Y_e>6($;pa1 zEXtxLhRu#j`Osul?A$5%_2A^55q{6@<%5-~c>3wQqkO1>p2MPoN#+5}?+rRMS?*7+ zjPQ8B=tSSXzi)>(tm@stU$JZz%Z`s67<#<)h%yR^Vv`y^29%{#jE|MaDntFsIaoz) z^2^uTgI%mr_ovya;(CRlk<%L&x^G63P86k;zTS{YcRizHDp<*AF>^N3MQP2JbtHpT zNsp=SrC5uGbRD6&=;l3G&R1EJ1uJ4ytjb7SJAXMZv)9hQJSy5ztX4(c3N1RasajSdyJg^vf^TS}9tI4LizU`CRQ@ zYG{=N8k^FnqfLwOa;wR8WMJFUaE%N)OH$w{5nswjI@;RA-8=~o W?I|}L*>>s1g{J1T;>4Spr~d;0w6KE! delta 1652 zcmY+ETWpL$6vwBvYIj@Pi`A;Gw(eE8ZnZ@fRT|NF15JoTBK-b!ns74z`F&@;nKLtI=KJ229FHVl=J`4t z)`%i#MY?k_xXnj}eI4oCcsK|tE-S-1ip!%?mqHkT#pahmKmKg~tJtDtti1u9@0%z}rZ0;Qn(UxXTe+5B#(h4onbZKws`huZmL zmkNMqad-2uy6KVnBP4L+`2o?7SWN$8w zRfvI1&R-pKY0yFnpe87WO>ma^d!YL5hZ=YYD#adD;1iHFxU*3GI-&YqwfEPJca8nV z=b7YR1K-e)2R|8qLAB?Q#{yUgHDMHLf*NBT9EaavTyO2WppIrA)Dawn+TdxZg`GEE zO4^HVYv?imF4O`ZK;71-P!qp}WYK*zeuEVEn@WMRNS`JO!+cl-)m{m8>1tsPTn&}E zd=yg$i&Os$+-w7mQs{1G~+?W$cAo$V1}*q`3Fye}lR!X{3d#GPEe5`|sQw zG!xB76{sA|KnszA=m@INzgBCGK2cfWU$|cGbt#fX(cGb@ztuFLOS=r|a_W3#IyUa_e>MegI^yr{8W6u;QdHLL-OT9nwvRgE z75UpETjPmMPSsAgEtY869c$Ul!_Hl$iCC-m!XNim2F80^0v%p{Fyt)`uJK0~mPV^< vtE#V^4A!T6ud_~g9i!X4@=%GlHk9!CLJ{vvDCO@>wZ=E^XpVdR*#&<9M@Esg diff --git a/src/catalogue/locale/pl/LC_MESSAGES/django.po b/src/catalogue/locale/pl/LC_MESSAGES/django.po index ad827e5e..044f9605 100644 --- a/src/catalogue/locale/pl/LC_MESSAGES/django.po +++ b/src/catalogue/locale/pl/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2022-09-19 15:45+0200\n" +"PO-Revision-Date: 2022-09-22 15:57+0200\n" "Last-Translator: \n" "Language-Team: \n" "Language: pl\n" @@ -19,29 +19,27 @@ msgstr "" "n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" "X-Generator: Poedit 3.0.1\n" -#: catalogue/admin.py:176 +#: catalogue/admin.py:53 catalogue/admin.py:203 msgid "Identification" msgstr "Identyfikacja" -#: catalogue/admin.py:190 +#: catalogue/admin.py:217 msgid "Features" msgstr "Cechy" -#: catalogue/admin.py:200 +#: catalogue/admin.py:227 msgid "Plan" msgstr "Plan" -#: catalogue/admin.py:242 -#, fuzzy -#| msgid "title" +#: catalogue/admin.py:269 msgid "Title" msgstr "tytuł" -#: catalogue/admin.py:248 +#: catalogue/admin.py:275 msgid "Book" msgstr "książka" -#: catalogue/admin.py:258 catalogue/models.py:139 +#: catalogue/admin.py:285 catalogue/models.py:160 msgid "scans source" msgstr "źródło skanów" @@ -73,217 +71,253 @@ msgstr "narodowość" msgid "year of birth" msgstr "rok urodzenia" -#: catalogue/models.py:24 +#: catalogue/models.py:24 catalogue/models.py:33 +msgid "inexact" +msgstr "niedokładny" + +#: catalogue/models.py:25 +msgid "year of birth, range end" +msgstr "rok urodzenia, koniec zakresu" + +#: catalogue/models.py:26 +msgid "date_of_birth" +msgstr "data urodzenia" + +#: catalogue/models.py:29 msgid "place of birth" msgstr "miejsce urodzenia" -#: catalogue/models.py:25 +#: catalogue/models.py:32 msgid "year of death" msgstr "rok śmierci" -#: catalogue/models.py:26 +#: catalogue/models.py:34 +msgid "year of death, range end" +msgstr "rok śmierci, koniec zakresu" + +#: catalogue/models.py:35 +msgid "date_of_death" +msgstr "data śmierci" + +#: catalogue/models.py:38 msgid "place of death" msgstr "miejsce śmierci" -#: catalogue/models.py:28 +#: catalogue/models.py:42 msgid "status" msgstr "status" -#: catalogue/models.py:32 +#: catalogue/models.py:46 msgid "Alive" msgstr "Å»yje" -#: catalogue/models.py:33 +#: catalogue/models.py:47 msgid "Dead" msgstr "Zmarły" -#: catalogue/models.py:34 +#: catalogue/models.py:48 msgid "Long dead" msgstr "Dawno zmarły" -#: catalogue/models.py:35 +#: catalogue/models.py:49 msgid "Unknown" msgstr "Nieznany" -#: catalogue/models.py:38 catalogue/models.py:141 catalogue/models.py:207 -#: catalogue/models.py:225 +#: catalogue/models.py:52 catalogue/models.py:162 catalogue/models.py:228 +#: catalogue/models.py:246 msgid "notes" msgstr "notatki" -#: catalogue/models.py:39 catalogue/models.py:147 +#: catalogue/models.py:53 catalogue/models.py:168 msgid "gazeta link" msgstr "link do bazy gazety" -#: catalogue/models.py:40 +#: catalogue/models.py:54 msgid "culture.pl link" msgstr "link do bazy culture.pl" -#: catalogue/models.py:42 +#: catalogue/models.py:56 msgid "description" msgstr "opis" -#: catalogue/models.py:47 catalogue/models.py:143 catalogue/models.py:275 +#: catalogue/models.py:59 catalogue/models.py:164 catalogue/models.py:296 msgid "priority" msgstr "priorytet" -#: catalogue/models.py:48 catalogue/models.py:144 +#: catalogue/models.py:60 catalogue/models.py:165 msgid "Low" msgstr "Niski" -#: catalogue/models.py:48 catalogue/models.py:144 +#: catalogue/models.py:60 catalogue/models.py:165 msgid "Medium" msgstr "Średni" -#: catalogue/models.py:48 catalogue/models.py:144 +#: catalogue/models.py:60 catalogue/models.py:165 msgid "High" msgstr "Wysoki" -#: catalogue/models.py:50 catalogue/models.py:148 catalogue/models.py:230 -#: catalogue/models.py:282 +#: catalogue/models.py:62 catalogue/models.py:169 catalogue/models.py:251 +#: catalogue/models.py:303 msgid "collections" msgstr "kolekcje" -#: catalogue/models.py:53 +#: catalogue/models.py:65 msgid "author" msgstr "autor" -#: catalogue/models.py:54 catalogue/models.py:122 +#: catalogue/models.py:66 catalogue/models.py:143 msgid "authors" msgstr "autorzy" -#: catalogue/models.py:93 catalogue/models.py:205 catalogue/models.py:222 -#: catalogue/models.py:255 +#: catalogue/models.py:114 catalogue/models.py:226 catalogue/models.py:243 +#: catalogue/models.py:276 catalogue/models.py:329 msgid "name" msgstr "nazwa" -#: catalogue/models.py:104 +#: catalogue/models.py:125 msgid "epoch" msgstr "epoka" -#: catalogue/models.py:105 catalogue/models.py:130 catalogue/models.py:279 +#: catalogue/models.py:126 catalogue/models.py:151 catalogue/models.py:300 msgid "epochs" msgstr "epoki" -#: catalogue/models.py:110 +#: catalogue/models.py:131 msgid "genre" msgstr "gatunek" -#: catalogue/models.py:111 catalogue/models.py:132 catalogue/models.py:281 +#: catalogue/models.py:132 catalogue/models.py:153 catalogue/models.py:302 msgid "genres" msgstr "gatunki" -#: catalogue/models.py:116 +#: catalogue/models.py:137 msgid "kind" msgstr "rodzaj" -#: catalogue/models.py:117 catalogue/models.py:131 catalogue/models.py:280 +#: catalogue/models.py:138 catalogue/models.py:152 catalogue/models.py:301 msgid "kinds" msgstr "rodzaje" -#: catalogue/models.py:128 +#: catalogue/models.py:149 msgid "translators" msgstr "tłumacze" -#: catalogue/models.py:133 +#: catalogue/models.py:154 msgid "title" msgstr "tytuł" -#: catalogue/models.py:134 +#: catalogue/models.py:155 msgid "language" msgstr "język" -#: catalogue/models.py:137 +#: catalogue/models.py:158 msgid "based on" msgstr "oparte na" -#: catalogue/models.py:140 +#: catalogue/models.py:161 msgid "text source" msgstr "źródło tekstu" -#: catalogue/models.py:146 +#: catalogue/models.py:167 msgid "year of entry into PD" msgstr "rok wstąpienia do DP" -#: catalogue/models.py:150 +#: catalogue/models.py:171 msgid "estimated number of characters" msgstr "szacowana liczba znaków" -#: catalogue/models.py:151 +#: catalogue/models.py:172 msgid "estimated number of verses" msgstr "szacowana liczba wersów" -#: catalogue/models.py:152 +#: catalogue/models.py:173 msgid "source of estimates" msgstr "źródło szacunków" -#: catalogue/models.py:154 +#: catalogue/models.py:175 msgid "free license" msgstr "wolna licencja" -#: catalogue/models.py:155 +#: catalogue/models.py:176 msgid "missing on Polona" msgstr "brak na Polonie" -#: catalogue/models.py:159 +#: catalogue/models.py:180 msgid "book" msgstr "książka" -#: catalogue/models.py:160 +#: catalogue/models.py:181 msgid "books" msgstr "książki" -#: catalogue/models.py:190 +#: catalogue/models.py:211 msgid "Author" msgstr "autor" -#: catalogue/models.py:195 +#: catalogue/models.py:216 msgid "Translator" msgstr "tłumacze" -#: catalogue/models.py:206 +#: catalogue/models.py:227 msgid "parent" msgstr "rodzic" -#: catalogue/models.py:211 +#: catalogue/models.py:232 msgid "collection category" msgstr "kategoria kolekcji" -#: catalogue/models.py:212 +#: catalogue/models.py:233 msgid "collection categories" msgstr "kategorie kolekcji" -#: catalogue/models.py:224 +#: catalogue/models.py:245 msgid "category" msgstr "kategoria" -#: catalogue/models.py:229 +#: catalogue/models.py:250 msgid "collection" msgstr "kolekcja" -#: catalogue/models.py:259 catalogue/models.py:278 +#: catalogue/models.py:280 catalogue/models.py:299 msgid "work type" msgstr "rodzaj pracy" -#: catalogue/models.py:260 +#: catalogue/models.py:281 msgid "work types" msgstr "rodzaje prac" -#: catalogue/models.py:276 +#: catalogue/models.py:297 msgid "per normalized page" msgstr "za stronę maszynopisu" -#: catalogue/models.py:277 +#: catalogue/models.py:298 msgid "per verse" msgstr "za wers" -#: catalogue/models.py:286 +#: catalogue/models.py:307 msgid "work rate" msgstr "stawka" -#: catalogue/models.py:287 +#: catalogue/models.py:308 msgid "work rates" msgstr "stawki" +#: catalogue/models.py:330 +msgid "locative" +msgstr "miejscownik" + +#: catalogue/models.py:330 +msgid "in…" +msgstr "w…" + +#: catalogue/models.py:333 +msgid "place" +msgstr "miejsce" + +#: catalogue/models.py:334 +msgid "places" +msgstr "miejsca" + #: catalogue/templates/catalogue/author_detail.html:7 #: catalogue/templates/catalogue/author_detail.html:13 #: catalogue/templates/catalogue/book_detail.html:7 diff --git a/src/catalogue/migrations/0035_place_remove_author_place_of_birth_and_more.py b/src/catalogue/migrations/0035_place_remove_author_place_of_birth_and_more.py new file mode 100644 index 00000000..339af8f6 --- /dev/null +++ b/src/catalogue/migrations/0035_place_remove_author_place_of_birth_and_more.py @@ -0,0 +1,83 @@ +# Generated by Django 4.0.6 on 2022-09-22 14:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalogue', '0034_notablebook'), + ] + + operations = [ + migrations.CreateModel( + name='Place', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('wikidata', models.CharField(blank=True, help_text='If you have a Wikidata ID, like "Q1337", enter it and save.', max_length=255)), + ('name', models.CharField(blank=True, max_length=255, verbose_name='name')), + ('name_pl', models.CharField(blank=True, max_length=255, null=True, verbose_name='name')), + ('name_de', models.CharField(blank=True, max_length=255, null=True, verbose_name='name')), + ('name_lt', models.CharField(blank=True, max_length=255, null=True, verbose_name='name')), + ('locative', models.CharField(blank=True, help_text='in…', max_length=255, verbose_name='locative')), + ('locative_pl', models.CharField(blank=True, help_text='in…', max_length=255, null=True, verbose_name='locative')), + ('locative_de', models.CharField(blank=True, help_text='in…', max_length=255, null=True, verbose_name='locative')), + ('locative_lt', models.CharField(blank=True, help_text='in…', max_length=255, null=True, verbose_name='locative')), + ], + options={ + 'abstract': False, + }, + ), + migrations.RemoveField( + model_name='author', + name='place_of_birth', + ), + migrations.RemoveField( + model_name='author', + name='place_of_birth_de', + ), + migrations.RemoveField( + model_name='author', + name='place_of_birth_lt', + ), + migrations.RemoveField( + model_name='author', + name='place_of_birth_pl', + ), + migrations.RemoveField( + model_name='author', + name='place_of_death', + ), + migrations.RemoveField( + model_name='author', + name='place_of_death_de', + ), + migrations.RemoveField( + model_name='author', + name='place_of_death_lt', + ), + migrations.RemoveField( + model_name='author', + name='place_of_death_pl', + ), + migrations.AddField( + model_name='author', + name='date_of_birth', + field=models.DateField(blank=True, null=True, verbose_name='date_of_birth'), + ), + migrations.AddField( + model_name='author', + name='date_of_death', + field=models.DateField(blank=True, null=True, verbose_name='date_of_death'), + ), + migrations.AddField( + model_name='author', + name='year_of_birth_range', + field=models.SmallIntegerField(blank=True, null=True, verbose_name='year of birth, range end'), + ), + migrations.AddField( + model_name='author', + name='year_of_death_range', + field=models.SmallIntegerField(blank=True, null=True, verbose_name='year of death, range end'), + ), + ] diff --git a/src/catalogue/migrations/0036_author_place_of_birth_author_place_of_death.py b/src/catalogue/migrations/0036_author_place_of_birth_author_place_of_death.py new file mode 100644 index 00000000..b72e4ba0 --- /dev/null +++ b/src/catalogue/migrations/0036_author_place_of_birth_author_place_of_death.py @@ -0,0 +1,24 @@ +# Generated by Django 4.0.6 on 2022-09-22 14:57 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalogue', '0035_place_remove_author_place_of_birth_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='author', + name='place_of_birth', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='authors_born', to='catalogue.place', verbose_name='place of birth'), + ), + migrations.AddField( + model_name='author', + name='place_of_death', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='authors_died', to='catalogue.place', verbose_name='place of death'), + ), + ] diff --git a/src/catalogue/migrations/0037_alter_place_options_author_year_of_birth_inexact_and_more.py b/src/catalogue/migrations/0037_alter_place_options_author_year_of_birth_inexact_and_more.py new file mode 100644 index 00000000..e5c5629b --- /dev/null +++ b/src/catalogue/migrations/0037_alter_place_options_author_year_of_birth_inexact_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 4.0.6 on 2022-09-22 15:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalogue', '0036_author_place_of_birth_author_place_of_death'), + ] + + operations = [ + migrations.AlterModelOptions( + name='place', + options={'verbose_name': 'place', 'verbose_name_plural': 'places'}, + ), + migrations.AddField( + model_name='author', + name='year_of_birth_inexact', + field=models.BooleanField(default=False, verbose_name='inexact'), + ), + migrations.AddField( + model_name='author', + name='year_of_death_inexact', + field=models.BooleanField(default=False, verbose_name='inexact'), + ), + ] diff --git a/src/catalogue/models.py b/src/catalogue/models.py index d0698808..d671425d 100644 --- a/src/catalogue/models.py +++ b/src/catalogue/models.py @@ -21,9 +21,23 @@ class Author(WikidataMixin, models.Model): gender = models.CharField(_("gender"), max_length=255, blank=True) nationality = models.CharField(_("nationality"), max_length=255, blank=True) year_of_birth = models.SmallIntegerField(_("year of birth"), null=True, blank=True) - place_of_birth = models.CharField(_('place of birth'), max_length=255, blank=True) + year_of_birth_inexact = models.BooleanField(_("inexact"), default=False) + year_of_birth_range = models.SmallIntegerField(_("year of birth, range end"), null=True, blank=True) + date_of_birth = models.DateField(_("date_of_birth"), null=True, blank=True) + place_of_birth = models.ForeignKey( + 'Place', models.PROTECT, null=True, blank=True, + verbose_name=_('place of birth'), + related_name='authors_born' + ) year_of_death = models.SmallIntegerField(_("year of death"), null=True, blank=True) - place_of_death = models.CharField(_('place of death'), max_length=255, blank=True) + year_of_death_inexact = models.BooleanField(_("inexact"), default=False) + year_of_death_range = models.SmallIntegerField(_("year of death, range end"), null=True, blank=True) + date_of_death = models.DateField(_("date_of_death"), null=True, blank=True) + place_of_death = models.ForeignKey( + 'Place', models.PROTECT, null=True, blank=True, + verbose_name=_('place of death'), + related_name='authors_died' + ) status = models.PositiveSmallIntegerField( _("status"), null=True, @@ -55,10 +69,19 @@ class Author(WikidataMixin, models.Model): class Wikidata: first_name = WIKIDATA.GIVEN_NAME last_name = WIKIDATA.LAST_NAME + date_of_birth = WIKIDATA.DATE_OF_BIRTH + year_of_birth = WIKIDATA.DATE_OF_BIRTH + place_of_birth = WIKIDATA.PLACE_OF_BIRTH + date_of_death = WIKIDATA.DATE_OF_DEATH year_of_death = WIKIDATA.DATE_OF_DEATH + place_of_death = WIKIDATA.PLACE_OF_DEATH gender = WIKIDATA.GENDER notes = "description" + def _supplement(obj): + if not obj.first_name and not obj.last_name: + yield 'first_name', 'label' + def __str__(self): name = f"{self.first_name} {self.last_name}" if self.year_of_death is not None: @@ -169,10 +192,10 @@ class Book(WikidataMixin, models.Model): txt = self.title astr = self.authors_str() if astr: - txt = f"{astr} – {txt}" + txt = f"{txt}, {astr}" tstr = self.translators_str() if tstr: - txt = f"{txt} (tłum. {tstr})" + txt = f"{txt}, tłum. {tstr}" return txt def get_absolute_url(self): @@ -301,3 +324,17 @@ class WorkRate(models.Model): if book.estimated_chars: return (decimal.Decimal(book.estimated_chars) / 1800 * self.per_normpage).quantize(decimal.Decimal('1.00'), rounding=decimal.ROUND_HALF_UP) + +class Place(WikidataMixin, models.Model): + name = models.CharField(_('name'), max_length=255, blank=True) + locative = models.CharField(_('locative'), max_length=255, blank=True, help_text=_('in…')) + + class Meta: + verbose_name = _('place') + verbose_name_plural = _('places') + + class Wikidata: + name = 'label' + + def __str__(self): + return self.name diff --git a/src/catalogue/translation.py b/src/catalogue/translation.py index 0ee80711..9637e55d 100644 --- a/src/catalogue/translation.py +++ b/src/catalogue/translation.py @@ -7,7 +7,13 @@ class AuthorTranslationOptions(TranslationOptions): fields = ( 'first_name', 'last_name', - 'place_of_birth', - 'place_of_death', 'description', ) + + +@register(models.Place) +class PlaceTranslationOptions(TranslationOptions): + fields = ( + 'name', + 'locative', + ) diff --git a/src/catalogue/wikidata.py b/src/catalogue/wikidata.py index 4fc8bbd8..610245d5 100644 --- a/src/catalogue/wikidata.py +++ b/src/catalogue/wikidata.py @@ -21,6 +21,28 @@ class WikidataMixin(models.Model): class Meta: abstract = True + def wikidata_populate(self, client, entity, attname, wd): + model_field = self._meta.get_field(attname) + if isinstance(model_field, models.ManyToManyField): + if getattr(self, attname).all().exists(): + return + else: + if getattr(self, attname): + return + + wdvalue = None + if wd == "description": + wdvalue = entity.description.get("pl", str(entity.description)) + elif wd == "label": + wdvalue = entity.label.get("pl", str(entity.label)) + else: + try: + wdvalue = entity.get(client.get(wd)) + except DatavalueError: + pass + + self.set_field_from_wikidata(attname, wdvalue) + def save(self, **kwargs): super().save() if self.wikidata and hasattr(self, "Wikidata"): @@ -33,26 +55,11 @@ class WikidataMixin(models.Model): continue wd = getattr(Wikidata, attname) - model_field = self._meta.get_field(attname) - if isinstance(model_field, models.ManyToManyField): - if getattr(self, attname).all().exists(): - continue - else: - if getattr(self, attname): - continue - - wdvalue = None - if wd == "description": - wdvalue = entity.description.get("pl", str(entity.description)) - elif wd == "label": - wdvalue = entity.label.get("pl", str(entity.label)) - else: - try: - wdvalue = entity.get(client.get(wd)) - except DatavalueError: - pass + self.wikidata_populate(client, entity, attname, wd) + if hasattr(Wikidata, '_supplement'): + for attname, wd in Wikidata._supplement(self): + self.wikidata_populate(client, entity, attname, wd) - self.set_field_from_wikidata(attname, wdvalue) kwargs.update(force_insert=False, force_update=True) super().save(**kwargs) -- 2.20.1 From 0251adf97488c11e936a714976261febfb8c5268 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 23 Sep 2022 15:40:17 +0200 Subject: [PATCH 14/16] Catalogue: wikidata suggestions --- src/catalogue/models.py | 10 +- .../static/catalogue/wikidata_admin.css | 23 +++++ .../static/catalogue/wikidata_admin.js | 61 ++++++++++++ src/catalogue/urls.py | 2 + src/catalogue/views.py | 49 +++++++++ src/catalogue/wikidata.py | 99 +++++++++++++------ 6 files changed, 210 insertions(+), 34 deletions(-) create mode 100644 src/catalogue/static/catalogue/wikidata_admin.css create mode 100644 src/catalogue/static/catalogue/wikidata_admin.js diff --git a/src/catalogue/models.py b/src/catalogue/models.py index d671425d..b974de14 100644 --- a/src/catalogue/models.py +++ b/src/catalogue/models.py @@ -7,10 +7,10 @@ from django.utils.translation import gettext_lazy as _ from admin_ordering.models import OrderableModel from wikidata.client import Client from .constants import WIKIDATA -from .wikidata import WikidataMixin +from .wikidata import WikidataModel -class Author(WikidataMixin, models.Model): +class Author(WikidataModel): slug = models.SlugField(max_length=255, null=True, blank=True, unique=True) first_name = models.CharField(_("first name"), max_length=255, blank=True) last_name = models.CharField(_("last name"), max_length=255, blank=True) @@ -110,7 +110,7 @@ class NotableBook(OrderableModel): book = models.ForeignKey('Book', models.CASCADE) -class Category(WikidataMixin, models.Model): +class Category(WikidataModel): name = models.CharField(_("name"), max_length=255) slug = models.SlugField(max_length=255, unique=True) @@ -138,7 +138,7 @@ class Kind(Category): verbose_name_plural = _('kinds') -class Book(WikidataMixin, models.Model): +class Book(WikidataModel): slug = models.SlugField(max_length=255, blank=True, null=True, unique=True) authors = models.ManyToManyField(Author, blank=True, verbose_name=_("authors")) translators = models.ManyToManyField( @@ -325,7 +325,7 @@ class WorkRate(models.Model): return (decimal.Decimal(book.estimated_chars) / 1800 * self.per_normpage).quantize(decimal.Decimal('1.00'), rounding=decimal.ROUND_HALF_UP) -class Place(WikidataMixin, models.Model): +class Place(WikidataModel): name = models.CharField(_('name'), max_length=255, blank=True) locative = models.CharField(_('locative'), max_length=255, blank=True, help_text=_('in…')) diff --git a/src/catalogue/static/catalogue/wikidata_admin.css b/src/catalogue/static/catalogue/wikidata_admin.css new file mode 100644 index 00000000..92ac69d9 --- /dev/null +++ b/src/catalogue/static/catalogue/wikidata_admin.css @@ -0,0 +1,23 @@ +.wikidata-hint { + background-image: url('https://www.wikidata.org/static/favicon/wikidata.ico'); + background-repeat: no-repeat; + background-position: 2px 50%; + background-size: 16px auto; + padding: 2px 2px 2px 20px; + cursor: pointer; + color: black; + background-color: white; + border-radius: 10px; +} + +#id_wikidata { + transition: .2s background-position; + background-image: url('https://www.wikidata.org/static/favicon/wikidata.ico'); + background-size: 64px 64px; + background-repeat: no-repeat; + background-position: -64px 50%; +} +#id_wikidata.wikidata-processing { + background-position: 100% 50%; + transition: 10s background-position; +} diff --git a/src/catalogue/static/catalogue/wikidata_admin.js b/src/catalogue/static/catalogue/wikidata_admin.js new file mode 100644 index 00000000..2ba5a1c9 --- /dev/null +++ b/src/catalogue/static/catalogue/wikidata_admin.js @@ -0,0 +1,61 @@ +(function($) { + $(function() { + + let model = $('body').attr('class').match(/model-([^\s]*)/)[1]; + $("#id_wikidata").each(show_wikidata_hints).on('change', show_wikidata_hints); + + function show_wikidata_hints() { + $(".wikidata-hint").remove(); + $wdinput = $(this); + let qid = $wdinput.val(); + $wdinput.addClass('wikidata-processing'); + $.ajax( + '/catalogue/wikidata/' + model + '/' + qid, + { + success: function(result) { + for (att in result) { + let val = result[att]; + let $input = $("#id_" + att); + if (val && val != $input.val()) { + let el = $(''); + if (val.wd) { + el.on('click', function() { + set_value_from_wikidata_id( + $input, val.model, val.wd, + function() { + $(this).remove(); + } + ); + }); + el.text(val.label); + } else { + el.on('click', function() { + $input.val(val); + $(this).remove(); + }); + el.text(val); + } + $input.parent().append(el); + } + }; + + $wdinput.removeClass('wikidata-processing'); + }, + } + ); + } + + function set_value_from_wikidata_id($input, model, wikidata_id, callback) { + $.post({ + url: '/catalogue/wikidata/' + model + '/' + wikidata_id, + data: { + csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val(), + }, + success: function(result) { + $input.val(result.id); + callback(); + }, + }) + } + }); +})(jQuery); diff --git a/src/catalogue/urls.py b/src/catalogue/urls.py index 2b4301bf..d4fd6b6a 100644 --- a/src/catalogue/urls.py +++ b/src/catalogue/urls.py @@ -18,4 +18,6 @@ urlpatterns = [ path('terms/author/', views.AuthorTerms.as_view()), path('terms/editor/', views.EditorTerms.as_view()), + + path('wikidata//', views.WikidataView.as_view()), ] diff --git a/src/catalogue/views.py b/src/catalogue/views.py index 6d4a2249..65b29808 100644 --- a/src/catalogue/views.py +++ b/src/catalogue/views.py @@ -1,13 +1,19 @@ # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # +from django.apps import apps from django.db.models import Prefetch +from django.http import Http404 +from django.utils.formats import localize_input from django.contrib.auth.models import User from django.views.generic import DetailView, TemplateView from . import models import documents.models from rest_framework.generics import ListAPIView from rest_framework.filters import SearchFilter +from rest_framework.permissions import IsAdminUser +from rest_framework.response import Response +from rest_framework.views import APIView from rest_framework import serializers @@ -86,3 +92,46 @@ class WLURITerms(Terms): class serializer_class(serializers.Serializer): label = serializers.CharField(source='wluri') + +class WikidataView(APIView): + permission_classes = [IsAdminUser] + + def get_object(self, model, qid, save): + try: + Model = apps.get_model('catalogue', model) + except LookupError: + raise Http404 + if not issubclass(Model, models.WikidataModel): + raise Http404 + + obj = Model.objects.filter(wikidata=qid).first() + if obj is None: + obj = Model(wikidata=qid) + if not obj.pk and save: + obj.save() + else: + obj.wikidata_populate(save=False) + d = { + "id": obj.pk, + } + for attname in dir(Model.Wikidata): + if attname.startswith("_"): + continue + for fieldname, lang in obj.wikidata_fields_for_attribute(attname): + d[fieldname] = getattr(obj, fieldname) + + if isinstance(d[fieldname], models.WikidataModel): + d[attname] = { + "model": type(d[fieldname])._meta.model_name, + "wd": d[fieldname].wikidata, + "label": str(d[fieldname]) or d[fieldname]._wikidata_label, + } + else: + d[fieldname] = localize_input(d[fieldname]) + return Response(d) + + def get(self, request, model, qid): + return self.get_object(model, qid, save=False) + + def post(self, request, model, qid): + return self.get_object(model, qid, save=True) diff --git a/src/catalogue/wikidata.py b/src/catalogue/wikidata.py index 610245d5..b5f3e94f 100644 --- a/src/catalogue/wikidata.py +++ b/src/catalogue/wikidata.py @@ -2,6 +2,7 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # from datetime import date +from django.conf import settings from django.db import models from django.db.models.signals import m2m_changed from django.utils.html import format_html @@ -9,9 +10,11 @@ from django.utils.translation import gettext_lazy as _ from django.dispatch import receiver from wikidata.client import Client from wikidata.datavalue import DatavalueError +from modeltranslation.translator import translator +from modeltranslation.settings import AVAILABLE_LANGUAGES -class WikidataMixin(models.Model): +class WikidataModel(models.Model): wikidata = models.CharField( max_length=255, blank=True, @@ -20,8 +23,8 @@ class WikidataMixin(models.Model): class Meta: abstract = True - - def wikidata_populate(self, client, entity, attname, wd): + + def wikidata_populate_field(self, client, entity, attname, wd, save, lang): model_field = self._meta.get_field(attname) if isinstance(model_field, models.ManyToManyField): if getattr(self, attname).all().exists(): @@ -32,53 +35,87 @@ class WikidataMixin(models.Model): wdvalue = None if wd == "description": - wdvalue = entity.description.get("pl", str(entity.description)) + wdvalue = entity.description.get(lang, str(entity.description)) elif wd == "label": - wdvalue = entity.label.get("pl", str(entity.label)) + wdvalue = entity.label.get(lang, str(entity.label)) else: try: + # TODO: lang? wdvalue = entity.get(client.get(wd)) except DatavalueError: pass - self.set_field_from_wikidata(attname, wdvalue) - + self.set_field_from_wikidata(attname, wdvalue, save=save) + + def wikidata_populate(self, save=True): + Wikidata = type(self).Wikidata + client = Client() + # Probably should getlist + entity = client.get(self.wikidata) + for attname in dir(Wikidata): + if attname.startswith("_"): + continue + wd = getattr(Wikidata, attname) + + self.wikidata_populate_attribute(client, entity, attname, wd, save=save) + if hasattr(Wikidata, '_supplement'): + for attname, wd in Wikidata._supplement(self): + self.wikidata_populate_attribute(client, entity, attname, wd, save=save) + + def wikidata_fields_for_attribute(self, attname): + field = getattr(type(self), attname) + if type(self) in translator._registry: + opts = translator.get_options_for_model(type(self)) + if attname in opts.fields: + tfields = opts.fields[attname] + for tf in tfields: + yield tf.name, tf.language + return + + yield attname, settings.LANGUAGE_CODE + + def wikidata_populate_attribute(self, client, entity, attname, wd, save): + for fieldname, lang in self.wikidata_fields_for_attribute(attname): + self.wikidata_populate_field(client, entity, fieldname, wd, save, lang) + def save(self, **kwargs): - super().save() - if self.wikidata and hasattr(self, "Wikidata"): - Wikidata = type(self).Wikidata - client = Client() - # Probably should getlist - entity = client.get(self.wikidata) - for attname in dir(Wikidata): - if attname.startswith("_"): - continue - wd = getattr(Wikidata, attname) - - self.wikidata_populate(client, entity, attname, wd) - if hasattr(Wikidata, '_supplement'): - for attname, wd in Wikidata._supplement(self): - self.wikidata_populate(client, entity, attname, wd) + am_new = self.pk is None + super().save() + if am_new and self.wikidata and hasattr(self, "Wikidata"): + self.wikidata_populate() kwargs.update(force_insert=False, force_update=True) super().save(**kwargs) - def set_field_from_wikidata(self, attname, wdvalue): + def set_field_from_wikidata(self, attname, wdvalue, save, language='pl'): if not wdvalue: return # Find out what this model field is model_field = self._meta.get_field(attname) if isinstance(model_field, models.ForeignKey): rel_model = model_field.related_model - if issubclass(rel_model, WikidataMixin): - # welp, we can try and find by WD identifier. - wdvalue, created = rel_model.objects.get_or_create(wikidata=wdvalue.id) + if issubclass(rel_model, WikidataModel): + label = wdvalue.label.get(language, str(wdvalue.label)) + try: + wdvalue = rel_model.objects.get(wikidata=wdvalue.id) + except rel_model.DoesNotExist: + wdvalue = rel_model(wikidata=wdvalue.id) + if save: + wdvalue.save() + wdvalue._wikidata_label = label setattr(self, attname, wdvalue) elif isinstance(model_field, models.ManyToManyField): rel_model = model_field.related_model - if issubclass(rel_model, WikidataMixin): - wdvalue, created = rel_model.objects.get_or_create(wikidata=wdvalue.id) + if issubclass(rel_model, WikidataModel): + label = wdvalue.label.get(language, str(wdvalue.label)) + try: + wdvalue = rel_model.objects.get(wikidata=wdvalue.id) + except rel_model.DoesNotExist: + wdvalue = rel_model(wikidata=wdvalue.id) + if save: + wdvalue.save() + wdvalue._wikidata_label = label getattr(self, attname).set([wdvalue]) else: # How to get original title? @@ -86,7 +123,7 @@ class WikidataMixin(models.Model): if isinstance(model_field, models.IntegerField): wdvalue = wdvalue.year elif not isinstance(wdvalue, str): - wdvalue = wdvalue.label.get("pl", str(wdvalue.label)) + wdvalue = wdvalue.label.get(language, str(wdvalue.label)) setattr(self, attname, wdvalue) def wikidata_link(self): @@ -102,6 +139,10 @@ class WikidataMixin(models.Model): class WikidataAdminMixin: + class Media: + css = {"screen": ("catalogue/wikidata_admin.css",)} + js = ("catalogue/wikidata_admin.js",) + def save_related(self, request, form, formsets, change): super().save_related(request, form, formsets, change) form.instance.save() -- 2.20.1 From 24ae804e7a88a17dcefd0e671eb08e548de14ae8 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 23 Sep 2022 15:41:53 +0200 Subject: [PATCH 15/16] Cover quick import fix. --- src/cover/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cover/views.py b/src/cover/views.py index 638f1c06..25663a09 100644 --- a/src/cover/views.py +++ b/src/cover/views.py @@ -158,7 +158,7 @@ def quick_import(request, pk): if url.startswith('%s://%s/' % ( request.scheme, request.get_host())): - cover_id = url.rsplit('/', 1)[-1] + cover_id = url.rstrip('/').rsplit('/', 1)[-1] cover = Image.objects.get(pk=cover_id) else: data = get_import_data(url) -- 2.20.1 From 94e92455b60f276f2a1a5b55de5e6b060687f396 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 23 Sep 2022 15:47:57 +0200 Subject: [PATCH 16/16] fix --- src/catalogue/static/catalogue/wikidata_admin.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/catalogue/static/catalogue/wikidata_admin.js b/src/catalogue/static/catalogue/wikidata_admin.js index 2ba5a1c9..a8aa51ee 100644 --- a/src/catalogue/static/catalogue/wikidata_admin.js +++ b/src/catalogue/static/catalogue/wikidata_admin.js @@ -8,6 +8,7 @@ $(".wikidata-hint").remove(); $wdinput = $(this); let qid = $wdinput.val(); + if (!qid) return; $wdinput.addClass('wikidata-processing'); $.ajax( '/catalogue/wikidata/' + model + '/' + qid, @@ -39,6 +40,8 @@ } }; + }, + complete: function() { $wdinput.removeClass('wikidata-processing'); }, } -- 2.20.1