Fix caret positioning outside editable.
[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 from django.contrib import admin
5 from django.db.models import Min
6 from django.utils.html import escape, format_html
7 from django.utils.safestring import mark_safe
8 from django.utils.translation import gettext_lazy as _
9 from admin_numeric_filter.admin import RangeNumericFilter, NumericFilterModelAdmin, RangeNumericForm
10 from admin_ordering.admin import OrderableAdmin
11 from fnpdjango.actions import export_as_csv_action
12 from modeltranslation.admin import TabbedTranslationAdmin
13 from . import models
14 import documents.models
15 from .wikidata import WikidataAdminMixin
16
17
18 class NotableBookInline(OrderableAdmin, admin.TabularInline):
19     model = models.NotableBook
20     autocomplete_fields = ['book']
21     ordering_field_hide_input = True
22
23
24 class AuthorAdmin(WikidataAdminMixin, TabbedTranslationAdmin):
25     list_display = [
26         "first_name",
27         "last_name",
28         "status",
29         "year_of_death",
30         "gender",
31         "nationality",
32         "priority",
33         "wikidata_link",
34         "slug",
35     ]
36     list_display_links = [
37         "first_name", "last_name"
38     ]
39     list_filter = [
40         ("year_of_death", RangeNumericFilter),
41         "priority",
42         "collections",
43         "status",
44         "gender",
45         "nationality",
46         "place_of_birth",
47         "place_of_death",
48         ("genitive", admin.EmptyFieldListFilter)
49     ]
50     list_per_page = 10000000
51     search_fields = ["first_name", "last_name", "wikidata"]
52     readonly_fields = ["wikidata_link", "description_preview"]
53
54     fieldsets = [
55         (None, {"fields": [("wikidata", "wikidata_link")]}),
56         (
57             _("Identification"),
58             {
59                 "fields": [
60                     ("first_name", "last_name"),
61                     "slug",
62                     "genitive",
63                     "gender",
64                     "nationality",
65                     (
66                         "date_of_birth",
67                         "year_of_birth",
68                         "year_of_birth_inexact",
69                         "year_of_birth_range",
70                         "century_of_birth",
71                         "place_of_birth"
72                     ),
73                     (
74                         "date_of_death",
75                         "year_of_death",
76                         "year_of_death_inexact",
77                         "year_of_death_range",
78                         "century_of_death",
79                         "place_of_death"
80                     ),
81                     ("description", "description_preview"),
82                     "status",
83                     "collections",
84                     "priority",
85                     
86                     "notes",
87                     "gazeta_link",
88                     "culturepl_link",
89                     "plwiki",
90                     "photo", "photo_source", "photo_attribution",
91                 ]
92             },
93         ),
94     ]
95     
96     prepopulated_fields = {"slug": ("first_name", "last_name")}
97     autocomplete_fields = ["collections", "place_of_birth", "place_of_death"]
98     inlines = [
99         NotableBookInline,
100     ]
101
102     def description_preview(self, obj):
103         return obj.generate_description()
104
105
106 admin.site.register(models.Author, AuthorAdmin)
107
108
109 class LicenseFilter(admin.SimpleListFilter):
110     title = 'Licencja'
111     parameter_name = 'book_license'
112     license_url_field = 'document_book__dc__license'
113     license_name_field = 'document_book__dc__license_description'
114
115     def lookups(self, requesrt, model_admin):
116         return [
117             ('cc', 'CC'),
118             ('fal', 'FAL'),
119             ('pd', 'domena publiczna'),
120         ]
121
122     def queryset(self, request, queryset):
123         v = self.value()
124         if v == 'cc': 
125             return queryset.filter(**{
126                 self.license_url_field + '__icontains': 'creativecommons.org'
127             })
128         elif v == 'fal':
129             return queryset.filter(**{
130                 self.license_url_field + '__icontains': 'artlibre.org'
131             })
132         elif v == 'pd':
133             return queryset.filter(**{
134                 self.license_name_field + '__icontains': 'domena publiczna'
135             })
136         else:
137             return queryset
138
139
140 class CoverLicenseFilter(LicenseFilter):
141     title = 'Licencja okładki'
142     parameter_name = 'cover_license'
143     license_url_field = 'document_book__dc_cover_image__license_url'
144     license_name_field = 'document_book__dc_cover_image__license_name'
145
146
147 def add_title(base_class, suffix):
148     class TitledCategoryFilter(base_class):
149         def __init__(self, *args, **kwargs):
150             super().__init__(*args, **kwargs)
151             self.title += suffix
152     return TitledCategoryFilter
153
154
155 class FirstPublicationYearFilter(admin.ListFilter):
156     title = 'Rok pierwszej publikacji'
157     parameter_name = 'first_publication_year'
158     template = 'admin/filter_numeric_range.html'
159
160     def __init__(self, request, params, *args, **kwargs):
161         super().__init__(request, params, *args, **kwargs)
162
163         self.request = request
164
165         if self.parameter_name + '_from' in params:
166             value = params.pop(self.parameter_name + '_from')
167             self.used_parameters[self.parameter_name + '_from'] = value
168
169         if self.parameter_name + '_to' in params:
170             value = params.pop(self.parameter_name + '_to')
171             self.used_parameters[self.parameter_name + '_to'] = value
172
173     def has_output(self):
174         return True
175
176     def queryset(self, request, queryset):
177         filters = {}
178
179         value_from = self.used_parameters.get(self.parameter_name + '_from', None)
180         if value_from is not None and value_from != '':
181             filters.update({
182                 self.parameter_name + '__gte': self.used_parameters.get(self.parameter_name + '_from', None),
183             })
184
185         value_to = self.used_parameters.get(self.parameter_name + '_to', None)
186         if value_to is not None and value_to != '':
187             filters.update({
188                 self.parameter_name + '__lte': self.used_parameters.get(self.parameter_name + '_to', None),
189             })
190
191         return queryset.filter(**filters)
192
193     def choices(self, changelist):
194         return ({
195             'request': self.request,
196             'parameter_name': self.parameter_name,
197             'form': RangeNumericForm(name=self.parameter_name, data={
198                 self.parameter_name + '_from': self.used_parameters.get(self.parameter_name + '_from', None),
199                 self.parameter_name + '_to': self.used_parameters.get(self.parameter_name + '_to', None),
200             }),
201         }, )
202
203     def expected_parameters(self):
204         return [
205             '{}_from'.format(self.parameter_name),
206             '{}_to'.format(self.parameter_name),
207         ]
208
209
210 class BookAdmin(WikidataAdminMixin, NumericFilterModelAdmin):
211     list_display = [
212         "smart_title",
213         "authors_str",
214         "translators_str",
215         "language",
216         "pd_year",
217         "priority",
218         "wikidata_link",
219     ]
220     search_fields = [
221         "title", "wikidata",
222         "authors__first_name", "authors__last_name",
223         "translators__first_name", "translators__last_name",
224         "scans_source", "text_source", "notes", "estimate_source",
225     ]
226     autocomplete_fields = ["authors", "translators", "based_on", "collections", "epochs", "genres", "kinds"]
227     prepopulated_fields = {"slug": ("title",)}
228     list_filter = [
229         "language",
230         "based_on__language",
231         ("pd_year", RangeNumericFilter),
232         "collections",
233         "collections__category",
234         ("authors__collections", add_title(admin.RelatedFieldListFilter, ' autora')),
235         ("authors__collections__category", add_title(admin.RelatedFieldListFilter, ' autora')),
236         ("translators__collections", add_title(admin.RelatedFieldListFilter, ' tłumacza')), 
237         ("translators__collections__category", add_title(admin.RelatedFieldListFilter, ' tłumacza')),
238         "epochs", "kinds", "genres",
239         "priority",
240         "authors__gender", "authors__nationality",
241         "translators__gender", "translators__nationality",
242
243         ("authors__place_of_birth", add_title(admin.RelatedFieldListFilter, ' autora')),
244         ("authors__place_of_death", add_title(admin.RelatedFieldListFilter, ' autora')),
245         ("translators__place_of_birth", add_title(admin.RelatedFieldListFilter, ' tłumacza')),
246         ("translators__place_of_death", add_title(admin.RelatedFieldListFilter, ' tłumacza')),
247
248         "document_book__chunk__stage",
249
250         LicenseFilter,
251         CoverLicenseFilter,
252         'free_license',
253         'polona_missing',
254
255         FirstPublicationYearFilter,
256     ]
257     list_per_page = 1000000
258
259     readonly_fields = [
260         "wikidata_link",
261         "estimated_costs",
262         "documents_book_link",
263         "scans_source_link",
264         "monthly_views_page",
265         "monthly_views_reader",
266     ]
267     actions = [export_as_csv_action(
268         fields=[
269             "id",
270             "wikidata",
271             "slug",
272             "title",
273             "authors_first_names",
274             "authors_last_names",
275             "translators_first_names",
276             "translators_last_names",
277             "language",
278             "based_on",
279             "scans_source",
280             "text_source",
281             "notes",
282             "priority",
283             "pd_year",
284             "gazeta_link",
285             "estimated_chars",
286             "estimated_verses",
287             "estimate_source",
288
289             "document_book__project",
290             "audience",
291             "first_publication_year",
292
293             "monthly_views_page",
294             "monthly_views_reader",
295
296             # content stats
297             "chars",
298             "chars_with_fn",
299             "words",
300             "words_with_fn",
301             "verses",
302             "chars_out_verse",
303             "verses_with_fn",
304             "chars_out_verse_with_fn",
305         ]
306     )]
307     fieldsets = [
308         (None, {"fields": [("wikidata", "wikidata_link")]}),
309         (
310             _("Identification"),
311             {
312                 "fields": [
313                     "title",
314                     ("slug", 'documents_book_link'),
315                     "authors",
316                     "translators",
317                     "language",
318                     "based_on",
319                     "original_year",
320                     "pd_year",
321                 ]
322             },
323         ),
324         (
325             _("Features"),
326             {
327                 "fields": [
328                     "epochs",
329                     "genres",
330                     "kinds",
331                 ]
332             },
333         ),
334         (
335             _("Plan"),
336             {
337                 "fields": [
338                     ("free_license", "polona_missing"),
339                     ("scans_source", "scans_source_link"),
340                     "text_source",
341                     "priority",
342                     "collections",
343                     "notes",
344                     ("estimated_chars", "estimated_verses", "estimate_source"),
345                     "estimated_costs",
346                     ("monthly_views_page", "monthly_views_reader"),
347                 ]
348             },
349         ),
350     ]
351
352     def get_queryset(self, request):
353         qs = super().get_queryset(request)
354         if request.resolver_match.view_name.endswith("changelist"):
355             qs = qs.prefetch_related("authors", "translators")
356             qs = qs.annotate(first_publication_year=Min('document_book__publish_log__timestamp__year'))
357         return qs
358
359     def estimated_costs(self, obj):
360         return "\n".join(
361             "{}: {} zł".format(
362                 work_type.name,
363                 cost or '—'
364             )
365             for work_type, cost in obj.get_estimated_costs().items()
366         )
367
368     def smart_title(self, obj):
369         if obj.title:
370             return obj.title
371         if obj.notes:
372             n = obj.notes
373             if len(n) > 100:
374                 n = n[:100] + '…'
375             return mark_safe(
376                 '<em><small>' + escape(n) + '</small></em>'
377             )
378         return '---'
379     smart_title.short_description = _('Title')
380     smart_title.admin_order_field = 'title'
381
382     def documents_book_link(self, obj):
383         for book in obj.document_books.all():
384             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))
385     documents_book_link.short_description = _('Book')
386
387     def scans_source_link(self, obj):
388         if obj.scans_source:
389             return format_html(
390                 '<a href="{url}" target="_blank">{url}</a>',
391                 url=obj.scans_source,
392             )
393         else:
394             return ""
395     scans_source_link.short_description = _('scans source')
396
397
398 admin.site.register(models.Book, BookAdmin)
399
400
401 admin.site.register(models.CollectionCategory)
402
403
404 class AuthorInline(admin.TabularInline):
405     model = models.Author.collections.through
406     autocomplete_fields = ["author"]
407
408
409 class BookInline(admin.TabularInline):
410     model = models.Book.collections.through
411     autocomplete_fields = ["book"]
412
413
414 class CollectionAdmin(admin.ModelAdmin):
415     list_display = ["name"]
416     autocomplete_fields = []
417     prepopulated_fields = {"slug": ("name",)}
418     search_fields = ["name"]
419     fields = ['name', 'slug', 'category', 'description', 'notes', 'estimated_costs']
420     readonly_fields = ['estimated_costs']
421     inlines = [AuthorInline, BookInline]
422
423     def estimated_costs(self, obj):
424         return "\n".join(
425             "{}: {} zł".format(
426                 work_type.name,
427                 cost or '—'
428             )
429             for work_type, cost in obj.get_estimated_costs().items()
430         )
431
432
433 admin.site.register(models.Collection, CollectionAdmin)
434
435
436
437 class CategoryAdmin(admin.ModelAdmin):
438     search_fields = ["name"]
439
440     def has_description(self, obj):
441         return bool(obj.description)
442     has_description.boolean = True
443     has_description.short_description = 'opis'
444
445
446 @admin.register(models.Epoch)
447 class EpochAdmin(CategoryAdmin):
448     list_display = [
449         'name',
450         'adjective_feminine_singular',
451         'adjective_nonmasculine_plural',
452         'has_description',
453     ]
454
455
456 @admin.register(models.Genre)
457 class GenreAdmin(CategoryAdmin):
458     list_display = [
459         'name',
460         'plural',
461         'is_epoch_specific',
462         'has_description',
463     ]
464
465
466 @admin.register(models.Kind)
467 class KindAdmin(CategoryAdmin):
468     list_display = [
469         'name',
470         'collective_noun',
471         'has_description',
472     ]
473
474
475
476 class WorkRateInline(admin.TabularInline):
477     model = models.WorkRate
478     autocomplete_fields = ['kinds', 'genres', 'epochs', 'collections']
479
480
481 class WorkTypeAdmin(admin.ModelAdmin):
482     inlines = [WorkRateInline]
483
484 admin.site.register(models.WorkType, WorkTypeAdmin)
485
486
487
488 @admin.register(models.Place)
489 class PlaceAdmin(WikidataAdminMixin, TabbedTranslationAdmin):
490     search_fields = ['name']
491
492
493 @admin.register(models.Thema)
494 class ThemaAdmin(admin.ModelAdmin):
495     list_display = ['code', 'name', 'usable', 'hidden']
496     list_filter = ['usable', 'hidden']
497     search_fields = ['code', 'name', 'description']