a035cc8e8fa0a9e61f27aff5ded2978d89461289
[redakcja.git] / src / catalogue / admin.py
1 # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
3 #
4 import json
5 from django.contrib import admin
6 from django.db.models import Min
7 from django import forms
8 from django.urls import reverse
9 from django.utils.html import escape, format_html
10 from django.utils.safestring import mark_safe
11 from django.utils.translation import gettext_lazy as _
12 from admin_numeric_filter.admin import RangeNumericFilter, NumericFilterModelAdmin, RangeNumericForm
13 from admin_ordering.admin import OrderableAdmin
14 from fnpdjango.actions import export_as_csv_action
15 from modeltranslation.admin import TabbedTranslationAdmin
16 from . import models
17 import documents.models
18 import sources.models
19 from .wikidata import WikidataAdminMixin
20
21
22 class NotableBookInline(OrderableAdmin, admin.TabularInline):
23     model = models.NotableBook
24     autocomplete_fields = ['book']
25     ordering_field_hide_input = True
26
27
28 class WoblinkCatalogueWidget(forms.Select):
29     class Media:
30         js = (
31             "admin/js/vendor/jquery/jquery.min.js",
32             "admin/js/vendor/select2/select2.full.min.js",
33             "admin/js/vendor/select2/i18n/pl.js",
34             "catalogue/woblink_admin.js",
35             "admin/js/jquery.init.js",
36             "admin/js/autocomplete.js",
37         )
38         css = {
39             "screen": (
40                 "admin/css/vendor/select2/select2.min.css",
41                 "admin/css/autocomplete.css",
42             ),
43         }
44
45     def __init__(self):
46         self.attrs = {}
47         self.choices = []
48         self.field = None
49
50     def get_url(self):
51         return reverse('catalogue_woblink_autocomplete', args=[self.category])
52
53     def build_attrs(self, base_attrs, extra_attrs=None):
54         attrs = super().build_attrs(base_attrs, extra_attrs=extra_attrs)
55         attrs.setdefault("class", "")
56         attrs.update(
57             {
58                 "data-ajax--cache": "true",
59                 "data-ajax--delay": 250,
60                 "data-ajax--type": "GET",
61                 "data-ajax--url": self.get_url(),
62                 "data-app-label": '',
63                 "data-model-name": '',
64                 "data-field-name": '',
65                 "data-theme": "admin-autocomplete",
66                 "data-allow-clear": json.dumps(not self.is_required),
67
68                 "data-placeholder": "", # Chyba że znaleziony?
69                 "lang": "pl",
70                 "class": attrs["class"]
71                 + (" " if attrs["class"] else "")
72                 + "admin-autocomplete admin-woblink",
73             }
74         )
75         return attrs
76
77     def optgroups(self, name, value, attrs=None):
78         """ Add synthetic option for keeping the current value. """
79         return [(None, [
80             self.create_option(
81                 name,
82                 v,
83                 '(bez zmian)',
84                 selected=True,
85                 index=index,
86                 attrs=attrs,
87             )
88             for index, v in enumerate(value)
89         ], 0)]
90
91 class WoblinkAuthorWidget(WoblinkCatalogueWidget):
92     category = 'author'
93
94 class AuthorForm(forms.ModelForm):
95     class Meta:
96         model = models.Author
97         fields = '__all__'
98         widgets = {
99             'woblink': WoblinkAuthorWidget,
100         }
101
102 class AuthorAdmin(WikidataAdminMixin, TabbedTranslationAdmin):
103     form = AuthorForm
104     list_display = [
105         "first_name",
106         "last_name",
107         "status",
108         "year_of_death",
109         "gender",
110         "nationality",
111         "priority",
112         "wikidata_link",
113         "woblink_link",
114         "slug",
115     ]
116     list_display_links = [
117         "first_name", "last_name"
118     ]
119     list_filter = [
120         ("year_of_death", RangeNumericFilter),
121         "priority",
122         "collections",
123         "status",
124         "gender",
125         "nationality",
126         "place_of_birth",
127         "place_of_death",
128         ("genitive", admin.EmptyFieldListFilter)
129     ]
130     list_per_page = 10000000
131     search_fields = ["first_name", "last_name", "wikidata"]
132     readonly_fields = ["wikidata_link", "description_preview", "woblink_link"]
133     prepopulated_fields = {"slug": ("first_name", "last_name")}
134     autocomplete_fields = ["collections", "place_of_birth", "place_of_death"]
135     inlines = [
136         NotableBookInline,
137     ]
138
139     fieldsets = [
140         (None, {
141             "fields": [
142                 ("wikidata", "wikidata_link"),
143                 ("woblink", "woblink_link"),
144             ]
145         }),
146         (
147             _("Identification"),
148             {
149                 "fields": [
150                     ("first_name", "last_name"),
151                     "slug",
152                     "genitive",
153                     "gender",
154                     "nationality",
155                     (
156                         "date_of_birth",
157                         "year_of_birth",
158                         "year_of_birth_inexact",
159                         "year_of_birth_range",
160                         "century_of_birth",
161                         "place_of_birth"
162                     ),
163                     (
164                         "date_of_death",
165                         "year_of_death",
166                         "year_of_death_inexact",
167                         "year_of_death_range",
168                         "century_of_death",
169                         "place_of_death"
170                     ),
171                     ("description", "description_preview"),
172                     "status",
173                     "collections",
174                     "priority",
175                     
176                     "notes",
177                     "gazeta_link",
178                     "culturepl_link",
179                     "plwiki",
180                     "photo", "photo_source", "photo_attribution",
181                 ]
182             },
183         ),
184     ]
185
186     def description_preview(self, obj):
187         return obj.generate_description()
188
189     def woblink_link(self, obj):
190         if obj.woblink:
191             return format_html(
192                 '<a href="https://woblink.com/autor/{slug}-{w}" target="_blank">{w}</a>',
193                 w=obj.woblink,
194                 slug=obj.slug,
195             )
196         else:
197             return ""
198     woblink_link.admin_order_field = "woblink"
199
200
201 admin.site.register(models.Author, AuthorAdmin)
202
203
204 class LicenseFilter(admin.SimpleListFilter):
205     title = 'Licencja'
206     parameter_name = 'book_license'
207     license_url_field = 'document_book__dc__license'
208     license_name_field = 'document_book__dc__license_description'
209
210     def lookups(self, requesrt, model_admin):
211         return [
212             ('cc', 'CC'),
213             ('fal', 'FAL'),
214             ('pd', 'domena publiczna'),
215         ]
216
217     def queryset(self, request, queryset):
218         v = self.value()
219         if v == 'cc': 
220             return queryset.filter(**{
221                 self.license_url_field + '__icontains': 'creativecommons.org'
222             })
223         elif v == 'fal':
224             return queryset.filter(**{
225                 self.license_url_field + '__icontains': 'artlibre.org'
226             })
227         elif v == 'pd':
228             return queryset.filter(**{
229                 self.license_name_field + '__icontains': 'domena publiczna'
230             })
231         else:
232             return queryset
233
234
235 class CoverLicenseFilter(LicenseFilter):
236     title = 'Licencja okładki'
237     parameter_name = 'cover_license'
238     license_url_field = 'document_book__dc_cover_image__license_url'
239     license_name_field = 'document_book__dc_cover_image__license_name'
240
241
242 def add_title(base_class, suffix):
243     class TitledCategoryFilter(base_class):
244         def __init__(self, *args, **kwargs):
245             super().__init__(*args, **kwargs)
246             self.title += suffix
247     return TitledCategoryFilter
248
249
250 class FirstPublicationYearFilter(admin.ListFilter):
251     title = 'Rok pierwszej publikacji'
252     parameter_name = 'first_publication_year'
253     template = 'admin/filter_numeric_range.html'
254
255     def __init__(self, request, params, *args, **kwargs):
256         super().__init__(request, params, *args, **kwargs)
257
258         self.request = request
259
260         if self.parameter_name + '_from' in params:
261             value = params.pop(self.parameter_name + '_from')
262             self.used_parameters[self.parameter_name + '_from'] = value
263
264         if self.parameter_name + '_to' in params:
265             value = params.pop(self.parameter_name + '_to')
266             self.used_parameters[self.parameter_name + '_to'] = value
267
268     def has_output(self):
269         return True
270
271     def queryset(self, request, queryset):
272         filters = {}
273
274         value_from = self.used_parameters.get(self.parameter_name + '_from', None)
275         if value_from is not None and value_from != '':
276             filters.update({
277                 self.parameter_name + '__gte': self.used_parameters.get(self.parameter_name + '_from', None),
278             })
279
280         value_to = self.used_parameters.get(self.parameter_name + '_to', None)
281         if value_to is not None and value_to != '':
282             filters.update({
283                 self.parameter_name + '__lte': self.used_parameters.get(self.parameter_name + '_to', None),
284             })
285
286         return queryset.filter(**filters)
287
288     def choices(self, changelist):
289         return ({
290             'request': self.request,
291             'parameter_name': self.parameter_name,
292             'form': RangeNumericForm(name=self.parameter_name, data={
293                 self.parameter_name + '_from': self.used_parameters.get(self.parameter_name + '_from', None),
294                 self.parameter_name + '_to': self.used_parameters.get(self.parameter_name + '_to', None),
295             }),
296         }, )
297
298     def expected_parameters(self):
299         return [
300             '{}_from'.format(self.parameter_name),
301             '{}_to'.format(self.parameter_name),
302         ]
303
304
305 class SourcesInline(admin.TabularInline):
306     model = sources.models.BookSource
307     extra = 1
308
309
310 class BookAdmin(WikidataAdminMixin, NumericFilterModelAdmin):
311     inlines = [SourcesInline]
312     list_display = [
313         "smart_title",
314         "authors_str",
315         "translators_str",
316         "language",
317         "pd_year",
318         "priority",
319         "wikidata_link",
320     ]
321     search_fields = [
322         "title", "wikidata",
323         "authors__first_name", "authors__last_name",
324         "translators__first_name", "translators__last_name",
325         "scans_source", "text_source", "notes", "estimate_source",
326     ]
327     autocomplete_fields = ["authors", "translators", "based_on", "epochs", "genres", "kinds"]
328     filter_horizontal = ['collections']
329     prepopulated_fields = {"slug": ("title",)}
330     list_filter = [
331         "language",
332         "based_on__language",
333         ("pd_year", RangeNumericFilter),
334         "collections",
335         "collections__category",
336         ("authors__collections", add_title(admin.RelatedFieldListFilter, ' autora')),
337         ("authors__collections__category", add_title(admin.RelatedFieldListFilter, ' autora')),
338         ("translators__collections", add_title(admin.RelatedFieldListFilter, ' tłumacza')), 
339         ("translators__collections__category", add_title(admin.RelatedFieldListFilter, ' tłumacza')),
340         "epochs", "kinds", "genres",
341         "priority",
342         "authors__gender", "authors__nationality",
343         "translators__gender", "translators__nationality",
344
345         ("authors__place_of_birth", add_title(admin.RelatedFieldListFilter, ' autora')),
346         ("authors__place_of_death", add_title(admin.RelatedFieldListFilter, ' autora')),
347         ("translators__place_of_birth", add_title(admin.RelatedFieldListFilter, ' tłumacza')),
348         ("translators__place_of_death", add_title(admin.RelatedFieldListFilter, ' tłumacza')),
349
350         "document_book__chunk__stage",
351
352         LicenseFilter,
353         CoverLicenseFilter,
354         'free_license',
355         'polona_missing',
356
357         FirstPublicationYearFilter,
358     ]
359     list_per_page = 1000000
360
361     readonly_fields = [
362         "wikidata_link",
363         "estimated_costs",
364         "documents_book_link",
365         "scans_source_link",
366         "monthly_views_page",
367         "monthly_views_reader",
368     ]
369     actions = [export_as_csv_action(
370         fields=[
371             "id",
372             "wikidata",
373             "slug",
374             "title",
375             "authors_first_names",
376             "authors_last_names",
377             "translators_first_names",
378             "translators_last_names",
379             "language",
380             "based_on",
381             "scans_source",
382             "text_source",
383             "notes",
384             "priority",
385             "pd_year",
386             "gazeta_link",
387             "estimated_chars",
388             "estimated_verses",
389             "estimate_source",
390
391             "document_book__project",
392             "audience",
393             "first_publication_year",
394
395             "monthly_views_page",
396             "monthly_views_reader",
397
398             # content stats
399             "chars",
400             "chars_with_fn",
401             "words",
402             "words_with_fn",
403             "verses",
404             "chars_out_verse",
405             "verses_with_fn",
406             "chars_out_verse_with_fn",
407         ]
408     )]
409     fieldsets = [
410         (None, {"fields": [("wikidata", "wikidata_link")]}),
411         (
412             _("Identification"),
413             {
414                 "fields": [
415                     "title",
416                     ("slug", 'documents_book_link'),
417                     "authors",
418                     "translators",
419                     "language",
420                     "based_on",
421                     "original_year",
422                     "pd_year",
423                 ]
424             },
425         ),
426         (
427             _("Features"),
428             {
429                 "fields": [
430                     "epochs",
431                     "genres",
432                     "kinds",
433                 ]
434             },
435         ),
436         (
437             _("Plan"),
438             {
439                 "fields": [
440                     ("free_license", "polona_missing"),
441                     ("scans_source", "scans_source_link"),
442                     "text_source",
443                     "priority",
444                     "collections",
445                     "notes",
446                     ("estimated_chars", "estimated_verses", "estimate_source"),
447                     "estimated_costs",
448                     ("monthly_views_page", "monthly_views_reader"),
449                 ]
450             },
451         ),
452     ]
453
454     def get_queryset(self, request):
455         qs = super().get_queryset(request)
456         if request.resolver_match.view_name.endswith("changelist"):
457             qs = qs.prefetch_related("authors", "translators")
458             qs = qs.annotate(first_publication_year=Min('document_book__publish_log__timestamp__year'))
459         return qs
460
461     def estimated_costs(self, obj):
462         return "\n".join(
463             "{}: {} zł".format(
464                 work_type.name,
465                 cost or '—'
466             )
467             for work_type, cost in obj.get_estimated_costs().items()
468         )
469
470     def smart_title(self, obj):
471         if obj.title:
472             return obj.title
473         if obj.notes:
474             n = obj.notes
475             if len(n) > 100:
476                 n = n[:100] + '…'
477             return mark_safe(
478                 '<em><small>' + escape(n) + '</small></em>'
479             )
480         return '---'
481     smart_title.short_description = _('Title')
482     smart_title.admin_order_field = 'title'
483
484     def documents_book_link(self, obj):
485         for book in obj.document_books.all():
486             return mark_safe('<a style="position: absolute" href="{}"><img height="100" width="70" src="/cover/preview/{}/?height=100&width=70"></a>'.format(book.get_absolute_url(), book.slug))
487     documents_book_link.short_description = _('Book')
488
489     def scans_source_link(self, obj):
490         if obj.scans_source:
491             return format_html(
492                 '<a href="{url}" target="_blank">{url}</a>',
493                 url=obj.scans_source,
494             )
495         else:
496             return ""
497     scans_source_link.short_description = _('scans source')
498
499
500 admin.site.register(models.Book, BookAdmin)
501
502
503 admin.site.register(models.CollectionCategory)
504
505
506 class AuthorInline(admin.TabularInline):
507     model = models.Author.collections.through
508     autocomplete_fields = ["author"]
509
510
511 class BookInline(admin.TabularInline):
512     model = models.Book.collections.through
513     autocomplete_fields = ["book"]
514
515
516 class CollectionAdmin(admin.ModelAdmin):
517     list_display = ["name"]
518     autocomplete_fields = []
519     prepopulated_fields = {"slug": ("name",)}
520     search_fields = ["name"]
521     fields = ['name', 'slug', 'category', 'description', 'notes', 'estimated_costs']
522     readonly_fields = ['estimated_costs']
523     inlines = [AuthorInline, BookInline]
524
525     def estimated_costs(self, obj):
526         return "\n".join(
527             "{}: {} zł".format(
528                 work_type.name,
529                 cost or '—'
530             )
531             for work_type, cost in obj.get_estimated_costs().items()
532         )
533
534
535 admin.site.register(models.Collection, CollectionAdmin)
536
537
538
539 class CategoryAdmin(admin.ModelAdmin):
540     search_fields = ["name"]
541
542     def has_description(self, obj):
543         return bool(obj.description)
544     has_description.boolean = True
545     has_description.short_description = 'opis'
546
547
548 @admin.register(models.Epoch)
549 class EpochAdmin(CategoryAdmin):
550     list_display = [
551         'name',
552         'adjective_feminine_singular',
553         'adjective_nonmasculine_plural',
554         'has_description',
555     ]
556
557
558 @admin.register(models.Genre)
559 class GenreAdmin(CategoryAdmin):
560     list_display = [
561         'name',
562         'plural',
563         'is_epoch_specific',
564         'has_description',
565     ]
566
567
568 @admin.register(models.Kind)
569 class KindAdmin(CategoryAdmin):
570     list_display = [
571         'name',
572         'collective_noun',
573         'has_description',
574     ]
575
576
577
578 class WorkRateInline(admin.TabularInline):
579     model = models.WorkRate
580     autocomplete_fields = ['kinds', 'genres', 'epochs', 'collections']
581
582
583 class WorkTypeAdmin(admin.ModelAdmin):
584     inlines = [WorkRateInline]
585
586 admin.site.register(models.WorkType, WorkTypeAdmin)
587
588
589
590 @admin.register(models.Place)
591 class PlaceAdmin(WikidataAdminMixin, TabbedTranslationAdmin):
592     search_fields = ['name']
593
594
595 @admin.register(models.Thema)
596 class ThemaAdmin(admin.ModelAdmin):
597     list_display = ['code', 'name', 'usable', 'hidden', 'woblink_category']
598     list_filter = ['usable', 'usable_as_main', 'hidden']
599     search_fields = ['code', 'name', 'description', 'public_description']
600     prepopulated_fields = {"slug": ["name"]}
601
602
603
604 class WoblinkSeriesWidget(WoblinkCatalogueWidget):
605     category = 'series'
606
607 class AudienceForm(forms.ModelForm):
608     class Meta:
609         model = models.Audience
610         fields = '__all__'
611         widgets = {
612             'woblink': WoblinkSeriesWidget,
613         }
614
615 @admin.register(models.Audience)
616 class AudienceAdmin(admin.ModelAdmin):
617     form = AudienceForm
618     list_display = ['code', 'name', 'thema', 'woblink']
619     search_fields = ['code', 'name', 'description', 'thema', 'woblink']
620     prepopulated_fields = {"slug": ["name"]}
621     fields = ['code', 'name', 'slug', 'description', 'thema', ('woblink', 'woblink_id')]
622     readonly_fields = ['woblink_id']
623
624     def woblink_id(self, obj):
625         return obj.woblink or ''