1 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
4 from django.db import models, transaction
5 import catalogue.models
6 from sorl.thumbnail import ImageField
7 from django.conf import settings
8 from django.contrib.contenttypes.fields import GenericRelation
9 from django.core.files.storage import FileSystemStorage
10 from django.urls import reverse
11 from slugify import slugify
12 from ssify import flush_ssi_includes
14 from catalogue.models.tag import prefetched_relations
15 from catalogue.utils import split_tags
16 from picture import tasks
17 from io import BytesIO
25 from django.utils.translation import ugettext_lazy as _
26 from newtagging import managers
30 picture_storage = FileSystemStorage(location=path.join(
31 settings.MEDIA_ROOT, 'pictures'),
32 base_url=settings.MEDIA_URL + "pictures/")
35 class PictureArea(models.Model):
36 picture = models.ForeignKey('picture.Picture', models.CASCADE, related_name='areas')
37 area = jsonfield.JSONField(_('area'), default={}, editable=False)
38 kind = models.CharField(
39 _('kind'), max_length=10, blank=False, null=False, db_index=True,
40 choices=(('thing', _('thing')), ('theme', _('theme'))))
42 objects = models.Manager()
43 tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
44 tags = managers.TagDescriptor(catalogue.models.Tag)
45 tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
47 short_html_url_name = 'picture_area_short'
50 def rectangle(cls, picture, kind, coords):
57 def flush_includes(self, languages=True):
61 languages = [lc for (lc, _ln) in settings.LANGUAGES]
63 template % (self.pk, lang)
65 '/katalog/pa/%d/short.%s.html',
71 class Picture(models.Model):
76 title = models.CharField(_('title'), max_length=32767)
77 slug = models.SlugField(_('slug'), max_length=120, db_index=True, unique=True)
78 sort_key = models.CharField(_('sort key'), max_length=120, db_index=True, editable=False)
79 sort_key_author = models.CharField(
80 _('sort key by author'), max_length=120, db_index=True, editable=False, default=u'')
81 created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
82 changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True)
83 xml_file = models.FileField(_('xml file'), upload_to="xml", storage=picture_storage)
84 image_file = ImageField(_('image file'), upload_to="images", storage=picture_storage)
85 html_file = models.FileField(_('html file'), upload_to="html", storage=picture_storage)
86 areas_json = jsonfield.JSONField(_('picture areas JSON'), default={}, editable=False)
87 extra_info = jsonfield.JSONField(_('extra information'), default={})
88 culturepl_link = models.CharField(blank=True, max_length=240)
89 wiki_link = models.CharField(blank=True, max_length=240)
91 width = models.IntegerField(null=True)
92 height = models.IntegerField(null=True)
94 objects = models.Manager()
95 tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
96 tags = managers.TagDescriptor(catalogue.models.Tag)
97 tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
99 short_html_url_name = 'picture_short'
101 class AlreadyExists(Exception):
105 ordering = ('sort_key_author', 'sort_key')
107 verbose_name = _('picture')
108 verbose_name_plural = _('pictures')
110 def save(self, force_insert=False, force_update=False, **kwargs):
111 from sortify import sortify
113 self.sort_key = sortify(self.title)[:120]
116 author = self.authors().first().sort_key
117 except AttributeError:
119 self.sort_key_author = author
121 ret = super(Picture, self).save(force_insert, force_update)
129 return self.tags.filter(category='author')
131 def tag_unicode(self, category):
132 relations = prefetched_relations(self, category)
134 return ', '.join(rel.tag.name for rel in relations)
136 return ', '.join(self.tags.filter(category=category).values_list('name', flat=True))
138 def author_unicode(self):
139 return self.tag_unicode('author')
141 def tags_by_category(self):
142 return split_tags(self.tags)
144 def get_absolute_url(self):
145 return reverse('picture_detail', args=[self.slug])
147 def get_initial(self):
149 return re.search(r'\w', self.title, re.U).group(0)
150 except AttributeError:
155 return type(self).objects.filter(sort_key__gt=self.sort_key)[0]
159 def get_previous(self):
161 return type(self).objects.filter(sort_key__lt=self.sort_key).order_by('-sort_key')[0]
166 def from_xml_file(cls, xml_file, image_file=None, image_store=None, overwrite=False, search_index=True):
168 Import xml and it's accompanying image file.
169 If image file is missing, it will be fetched by librarian.picture.ImageStore
170 which looks for an image file in the same directory the xml is, with extension matching
173 from sortify import sortify
174 from django.core.files import File
175 from librarian.picture import WLPicture, ImageStore
176 close_xml_file = False
177 close_image_file = False
179 if image_file is not None and not isinstance(image_file, File):
180 image_file = File(open(image_file, 'rb'))
181 close_image_file = True
183 if not isinstance(xml_file, File):
184 xml_file = File(open(xml_file))
185 close_xml_file = True
187 with transaction.atomic():
188 # use librarian to parse meta-data
189 if image_store is None:
190 image_store = ImageStore(picture_storage.path('images'))
191 picture_xml = WLPicture.from_file(xml_file, image_store=image_store)
193 picture, created = Picture.objects.get_or_create(slug=picture_xml.slug[:120])
194 if not created and not overwrite:
195 raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug)
197 picture.areas.all().delete()
198 picture.title = str(picture_xml.picture_info.title)
199 picture.extra_info = picture_xml.picture_info.to_dict()
201 picture_tags = set(catalogue.models.Tag.tags_from_info(picture_xml.picture_info))
202 for tag in picture_tags:
203 if not tag.for_pictures:
204 tag.for_pictures = True
207 area_data = {'themes': {}, 'things': {}}
209 # Treat all names in picture XML as in default language.
210 lang = settings.LANGUAGE_CODE
212 for part in picture_xml.partiter():
213 if picture_xml.frame:
214 c = picture_xml.frame[0]
215 part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']]
216 if part.get('object', None) is not None:
218 for objname in part['object'].split(','):
219 objname = objname.strip()
220 assert objname, 'Empty object name'
221 # str.capitalize() is wrong, because it also lowers letters
222 objname = objname[0].upper() + objname[1:]
223 tag, created = catalogue.models.Tag.objects.get_or_create(
224 slug=slugify(objname), category='thing')
227 setattr(tag, 'name_%s' % lang, tag.name)
228 tag.sort_key = sortify(tag.name)
229 tag.for_pictures = True
231 area_data['things'][tag.slug] = {
233 'coords': part['coords'],
237 if not tag.for_pictures:
238 tag.for_pictures = True
240 area = PictureArea.rectangle(picture, 'thing', part['coords'])
242 # WTF thing area does not inherit tags from picture and theme area does, is it intentional?
246 for motifs in part['themes']:
247 for motif in motifs.split(','):
248 tag, created = catalogue.models.Tag.objects.get_or_create(
249 slug=slugify(motif), category='theme')
252 tag.sort_key = sortify(tag.name)
253 tag.for_pictures = True
255 # motif_tags.add(tag)
257 if not tag.for_pictures:
258 tag.for_pictures = True
260 area_data['themes'][tag.slug] = {
262 'coords': part['coords']
265 logging.debug("coords for theme: %s" % part['coords'])
266 area = PictureArea.rectangle(picture, 'theme', part['coords'])
268 area.tags = _tags.union(picture_tags)
270 picture.tags = picture_tags
271 picture.areas_json = area_data
273 if image_file is not None:
276 img = picture_xml.image_file()
278 modified = cls.crop_to_frame(picture_xml, img)
279 modified = cls.add_source_note(picture_xml, modified)
281 picture.width, picture.height = modified.size
283 modified_file = BytesIO()
284 modified.save(modified_file, format='JPEG', quality=95)
285 # FIXME: hardcoded extension - detect from DC format or orginal filename
286 picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
288 picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
290 tasks.generate_picture_html(picture.id)
291 if not settings.NO_SEARCH_INDEX and search_index:
292 tasks.index_picture.delay(picture.id, picture_info=picture_xml.picture_info)
302 def crop_to_frame(cls, wlpic, image_file):
303 img = Image.open(image_file)
304 if wlpic.frame is None or wlpic.frame == [[0, 0], [-1, -1]]:
306 img = img.crop(itertools.chain(*wlpic.frame))
310 def add_source_note(wlpic, img):
311 from PIL import ImageDraw, ImageFont
312 from librarian import get_resource
314 annotated = Image.new(img.mode, (img.size[0], img.size[1] + 40), (255, 255, 255))
315 annotated.paste(img, (0, 0))
316 annotation = Image.new('RGB', (img.size[0] * 3, 120), (255, 255, 255))
317 ImageDraw.Draw(annotation).text(
319 wlpic.picture_info.source_name,
321 font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
323 annotated.paste(annotation.resize((img.size[0], 40), Image.ANTIALIAS), (0, img.size[1]))
328 if not hasattr(self, '_info'):
329 from librarian import dcparser
330 from librarian import picture
331 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
335 def pretty_title(self, html_links=False):
336 names = [(tag.name, tag.get_absolute_url()) for tag in self.authors().only('name', 'category', 'slug')]
337 names.append((self.title, self.get_absolute_url()))
340 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
342 names = [tag[0] for tag in names]
343 return ', '.join(names)
345 def related_themes(self):
346 return catalogue.models.Tag.objects.usage_for_queryset(
347 self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))
349 def flush_includes(self, languages=True):
352 if languages is True:
353 languages = [lc for (lc, _ln) in settings.LANGUAGES]
355 template % (self.pk, lang)
357 '/katalog/p/%d/short.%s.html',
358 '/katalog/p/%d/mini.%s.html',
360 for lang in languages
363 def search_index(self, picture_info=None, index=None, index_tags=True, commit=True):
365 from search.index import Index
368 index.index_picture(self, picture_info)
373 except Exception as e:
374 index.index.rollback()