Catalogue: wikidata suggestions
[redakcja.git] / src / catalogue / wikidata.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 datetime import date
5 from django.conf import settings
6 from django.db import models
7 from django.db.models.signals import m2m_changed
8 from django.utils.html import format_html
9 from django.utils.translation import gettext_lazy as _
10 from django.dispatch import receiver
11 from wikidata.client import Client
12 from wikidata.datavalue import DatavalueError
13 from modeltranslation.translator import translator
14 from modeltranslation.settings import AVAILABLE_LANGUAGES
15
16
17 class WikidataModel(models.Model):
18     wikidata = models.CharField(
19         max_length=255,
20         blank=True,
21         help_text=_('If you have a Wikidata ID, like "Q1337", enter it and save.'),
22     )
23
24     class Meta:
25         abstract = True
26         
27     def wikidata_populate_field(self, client, entity, attname, wd, save, lang):
28         model_field = self._meta.get_field(attname)
29         if isinstance(model_field, models.ManyToManyField):
30             if getattr(self, attname).all().exists():
31                 return
32         else:
33             if getattr(self, attname):
34                 return
35
36         wdvalue = None
37         if wd == "description":
38             wdvalue = entity.description.get(lang, str(entity.description))
39         elif wd == "label":
40             wdvalue = entity.label.get(lang, str(entity.label))
41         else:
42             try:
43                 # TODO: lang?
44                 wdvalue = entity.get(client.get(wd))
45             except DatavalueError:
46                 pass
47
48         self.set_field_from_wikidata(attname, wdvalue, save=save)
49
50     def wikidata_populate(self, save=True):
51         Wikidata = type(self).Wikidata
52         client = Client()
53         # Probably should getlist
54         entity = client.get(self.wikidata)
55         for attname in dir(Wikidata):
56             if attname.startswith("_"):
57                 continue
58             wd = getattr(Wikidata, attname)
59
60             self.wikidata_populate_attribute(client, entity, attname, wd, save=save)
61         if hasattr(Wikidata, '_supplement'):
62             for attname, wd in Wikidata._supplement(self):
63                 self.wikidata_populate_attribute(client, entity, attname, wd, save=save)
64
65     def wikidata_fields_for_attribute(self, attname):
66         field = getattr(type(self), attname)
67         if type(self) in translator._registry:
68             opts = translator.get_options_for_model(type(self))
69             if attname in opts.fields:
70                 tfields = opts.fields[attname]
71                 for tf in tfields:
72                     yield tf.name, tf.language
73                 return
74
75         yield attname, settings.LANGUAGE_CODE
76
77     def wikidata_populate_attribute(self, client, entity, attname, wd, save):
78         for fieldname, lang in self.wikidata_fields_for_attribute(attname):
79             self.wikidata_populate_field(client, entity, fieldname, wd, save, lang)
80                 
81     def save(self, **kwargs):
82         am_new = self.pk is None
83
84         super().save()
85         if am_new and self.wikidata and hasattr(self, "Wikidata"):
86             self.wikidata_populate()
87
88         kwargs.update(force_insert=False, force_update=True)
89         super().save(**kwargs)
90
91     def set_field_from_wikidata(self, attname, wdvalue, save, language='pl'):
92         if not wdvalue:
93             return
94         # Find out what this model field is
95         model_field = self._meta.get_field(attname)
96         if isinstance(model_field, models.ForeignKey):
97             rel_model = model_field.related_model
98             if issubclass(rel_model, WikidataModel):
99                 label = wdvalue.label.get(language, str(wdvalue.label))
100                 try:
101                     wdvalue = rel_model.objects.get(wikidata=wdvalue.id)
102                 except rel_model.DoesNotExist:
103                     wdvalue = rel_model(wikidata=wdvalue.id)
104                     if save:
105                         wdvalue.save()
106                 wdvalue._wikidata_label = label
107                 setattr(self, attname, wdvalue)
108         elif isinstance(model_field, models.ManyToManyField):
109             rel_model = model_field.related_model
110             if issubclass(rel_model, WikidataModel):
111                 label = wdvalue.label.get(language, str(wdvalue.label))
112                 try:
113                     wdvalue = rel_model.objects.get(wikidata=wdvalue.id)
114                 except rel_model.DoesNotExist:
115                     wdvalue = rel_model(wikidata=wdvalue.id)
116                     if save:
117                         wdvalue.save()
118                 wdvalue._wikidata_label = label
119                 getattr(self, attname).set([wdvalue])
120         else:
121             # How to get original title?
122             if isinstance(wdvalue, date):
123                 if isinstance(model_field, models.IntegerField):
124                     wdvalue = wdvalue.year
125             elif not isinstance(wdvalue, str):
126                 wdvalue = wdvalue.label.get(language, str(wdvalue.label))
127             setattr(self, attname, wdvalue)
128
129     def wikidata_link(self):
130         if self.wikidata:
131             return format_html(
132                 '<a href="https://www.wikidata.org/wiki/{wd}" target="_blank">{wd}</a>',
133                 wd=self.wikidata,
134             )
135         else:
136             return ""
137
138     wikidata_link.admin_order_field = "wikidata"
139
140
141 class WikidataAdminMixin:
142     class Media:
143         css = {"screen": ("catalogue/wikidata_admin.css",)}
144         js = ("catalogue/wikidata_admin.js",)
145
146     def save_related(self, request, form, formsets, change):
147         super().save_related(request, form, formsets, change)
148         form.instance.save()