75ea4c37d53039fd1fab1a0d5be83a7d555569f4
[wolnelektury.git] / apps / picture / models.py
1 # -*- coding: utf-8 -*-
2 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
3 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
4 #
5 from django.db import models, transaction
6 import catalogue.models
7 from django.db.models import permalink
8 from sorl.thumbnail import ImageField
9 from django.conf import settings
10 from django.contrib.contenttypes.fields import GenericRelation
11 from django.core.files.storage import FileSystemStorage
12 from django.utils.datastructures import SortedDict
13 from django.template.loader import render_to_string
14 from django.utils.safestring import mark_safe
15 from django.core.cache import caches
16 from catalogue.utils import split_tags
17 from fnpdjango.utils.text.slughifi import slughifi
18 from picture import tasks
19 from StringIO import StringIO
20 import jsonfield
21 import itertools
22 import logging
23
24 from PIL import Image
25
26 from django.utils.translation import get_language, ugettext_lazy as _
27 from newtagging import managers
28 from os import path
29
30
31 permanent_cache = caches['permanent']
32
33 picture_storage = FileSystemStorage(location=path.join(
34         settings.MEDIA_ROOT, 'pictures'),
35         base_url=settings.MEDIA_URL + "pictures/")
36
37
38 class PictureArea(models.Model):
39     picture = models.ForeignKey('picture.Picture', related_name='areas')
40     area = jsonfield.JSONField(_('area'), default={}, editable=False)
41     kind = models.CharField(_('kind'), max_length=10, blank=False,
42                            null=False, db_index=True,
43                            choices=(('thing', _('thing')),
44                                     ('theme', _('theme'))))
45
46     objects     = models.Manager()
47     tagged      = managers.ModelTaggedItemManager(catalogue.models.Tag)
48     tags        = managers.TagDescriptor(catalogue.models.Tag)
49     tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
50
51     @classmethod
52     def rectangle(cls, picture, kind, coords):
53         pa = PictureArea()
54         pa.picture = picture
55         pa.kind = kind
56         pa.area = coords
57         return pa
58
59     def reset_short_html(self):
60         if self.id is None:
61             return
62
63         cache_key = "PictureArea.short_html/%d/%s"
64         for lang, langname in settings.LANGUAGES:
65             permanent_cache.delete(cache_key % (self.id, lang))
66
67
68     def short_html(self):
69         if self.id:
70             cache_key = "PictureArea.short_html/%d/%s" % (self.id, get_language())
71             short_html = permanent_cache.get(cache_key)
72         else:
73             short_html = None
74
75         if short_html is not None:
76             return mark_safe(short_html)
77         else:
78             theme = self.tags.filter(category='theme')
79             theme = theme and theme[0] or None
80             thing = self.tags.filter(category='thing')
81             thing = thing and thing[0] or None
82             area = self
83             short_html = unicode(render_to_string(
84                     'picture/picturearea_short.html', locals()))
85             if self.id:
86                 permanent_cache.set(cache_key, short_html)
87             return mark_safe(short_html)
88
89
90 class Picture(models.Model):
91     """
92     Picture resource.
93
94     """
95     title       = models.CharField(_('title'), max_length=120)
96     slug        = models.SlugField(_('slug'), max_length=120, db_index=True, unique=True)
97     sort_key    = models.CharField(_('sort key'), max_length=120, db_index=True, editable=False)
98     sort_key_author = models.CharField(_('sort key by author'), max_length=120, db_index=True, editable=False, default=u'')
99     created_at  = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
100     changed_at  = models.DateTimeField(_('creation date'), auto_now=True, db_index=True)
101     xml_file    = models.FileField('xml_file', upload_to="xml", storage=picture_storage)
102     image_file  = ImageField(_('image_file'), upload_to="images", storage=picture_storage)
103     html_file   = models.FileField('html_file', upload_to="html", storage=picture_storage)
104     areas_json       = jsonfield.JSONField(_('picture areas JSON'), default={}, editable=False)
105     extra_info    = jsonfield.JSONField(_('extra information'), default={})
106     culturepl_link   = models.CharField(blank=True, max_length=240)
107     wiki_link     = models.CharField(blank=True, max_length=240)
108
109     width       = models.IntegerField(null=True)
110     height      = models.IntegerField(null=True)
111
112     objects     = models.Manager()
113     tagged      = managers.ModelTaggedItemManager(catalogue.models.Tag)
114     tags        = managers.TagDescriptor(catalogue.models.Tag)
115     tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
116
117     class AlreadyExists(Exception):
118         pass
119
120     class Meta:
121         ordering = ('sort_key',)
122
123         verbose_name = _('picture')
124         verbose_name_plural = _('pictures')
125
126     def save(self, force_insert=False, force_update=False, reset_short_html=True, **kwargs):
127         from sortify import sortify
128
129         self.sort_key = sortify(self.title)
130
131         ret = super(Picture, self).save(force_insert, force_update)
132
133         if reset_short_html:
134             self.reset_short_html()
135
136         return ret
137
138     def __unicode__(self):
139         return self.title
140
141     @permalink
142     def get_absolute_url(self):
143         return ('picture.views.picture_detail', [self.slug])
144
145     @classmethod
146     def from_xml_file(cls, xml_file, image_file=None, image_store=None, overwrite=False):
147         """
148         Import xml and it's accompanying image file.
149         If image file is missing, it will be fetched by librarian.picture.ImageStore
150         which looks for an image file in the same directory the xml is, with extension matching
151         its mime type.
152         """
153         from sortify import sortify
154         from django.core.files import File
155         from librarian.picture import WLPicture, ImageStore
156         close_xml_file = False
157         close_image_file = False
158
159
160         if image_file is not None and not isinstance(image_file, File):
161             image_file = File(open(image_file))
162             close_image_file = True
163
164         if not isinstance(xml_file, File):
165             xml_file = File(open(xml_file))
166             close_xml_file = True
167
168         try:
169             # use librarian to parse meta-data
170             if image_store is None:
171                 image_store = ImageStore(picture_storage.path('images'))
172             picture_xml = WLPicture.from_file(xml_file, image_store=image_store)
173
174             picture, created = Picture.objects.get_or_create(slug=picture_xml.slug)
175             if not created and not overwrite:
176                 raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug)
177
178             picture.areas.all().delete()
179             picture.title = unicode(picture_xml.picture_info.title)
180             picture.extra_info = picture_xml.picture_info.to_dict()
181
182             picture_tags = set(catalogue.models.Tag.tags_from_info(picture_xml.picture_info))
183             motif_tags = set()
184             thing_tags = set()
185
186             area_data = {'themes':{}, 'things':{}}
187
188             for part in picture_xml.partiter():
189                 if picture_xml.frame:
190                     c = picture_xml.frame[0]
191                     part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']]
192                 if part.get('object', None) is not None:
193                     objname = part['object']
194                     tag, created = catalogue.models.Tag.objects.get_or_create(slug=slughifi(objname), category='thing')
195                     if created:
196                         tag.name = objname
197                         tag.sort_key = sortify(tag.name)
198                         tag.save()
199                     #thing_tags.add(tag)
200                     area_data['things'][tag.slug] = {
201                         'object': part['object'],
202                         'coords': part['coords'],
203                         }
204                     area = PictureArea.rectangle(picture, 'thing', part['coords'])
205                     area.save()
206                     _tags = set()
207                     _tags.add(tag)
208                     area.tags = _tags
209                 else:
210                     _tags = set()
211                     for motif in part['themes']:
212                         tag, created = catalogue.models.Tag.objects.get_or_create(slug=slughifi(motif), category='theme')
213                         if created:
214                             tag.name = motif
215                             tag.sort_key = sortify(tag.name)
216                             tag.save()
217                         #motif_tags.add(tag)
218                         _tags.add(tag)
219                         area_data['themes'][tag.slug] = {
220                             'theme': motif,
221                             'coords': part['coords']
222                             }
223
224                     logging.debug("coords for theme: %s" % part['coords'])
225                     area = PictureArea.rectangle(picture, 'theme', part['coords'])
226                     area.save()
227                     area.tags = _tags.union(picture_tags)
228
229             picture.tags = picture_tags.union(motif_tags).union(thing_tags)
230             picture.areas_json = area_data
231
232             if image_file is not None:
233                 img = image_file
234             else:
235                 img = picture_xml.image_file()
236
237             modified = cls.crop_to_frame(picture_xml, img)
238             modified = cls.add_source_note(picture_xml, modified)
239
240             picture.width, picture.height = modified.size
241
242             modified_file = StringIO()
243             modified.save(modified_file, format='png', quality=95)
244             # FIXME: hardcoded extension - detect from DC format or orginal filename
245             picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
246
247             picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
248             picture.save()
249             tasks.generate_picture_html(picture.id)
250
251         except Exception, ex:
252             logging.exception("Exception during import, rolling back")
253             transaction.rollback()
254             raise ex
255
256         finally:
257             if close_xml_file:
258                 xml_file.close()
259             if close_image_file:
260                 image_file.close()
261
262         transaction.commit()
263
264         return picture
265
266     @classmethod
267     def crop_to_frame(cls, wlpic, image_file):
268         img = Image.open(image_file)
269         if wlpic.frame is None:
270             return img
271         img = img.crop(itertools.chain(*wlpic.frame))
272         return img
273
274     @staticmethod
275     def add_source_note(wlpic, img):
276         from PIL import ImageDraw, ImageFont
277         from librarian import get_resource
278
279         annotated = Image.new(img.mode,
280                 (img.size[0], img.size[1] + 40),
281                 (255, 255, 255)
282             )
283         annotated.paste(img, (0, 0))
284         annotation = Image.new(img.mode, (3000, 120), (255, 255, 255))
285         ImageDraw.Draw(annotation).text(
286             (30, 15),
287             wlpic.picture_info.source_name,
288             (0, 0, 0),
289             font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
290         )
291         annotated.paste(annotation.resize((1000, 40), Image.ANTIALIAS), (0, img.size[1]))
292         return annotated
293
294     @classmethod
295     def picture_list(cls, filter=None):
296         """Generates a hierarchical listing of all pictures
297         Pictures are optionally filtered with a test function.
298         """
299
300         pics = cls.objects.all().order_by('sort_key')\
301             .only('title', 'slug', 'image_file')
302
303         if filter:
304             pics = pics.filter(filter).distinct()
305
306         pics_by_author = SortedDict()
307         orphans = []
308         for tag in catalogue.models.Tag.objects.filter(category='author'):
309             pics_by_author[tag] = []
310
311         for pic in pics.iterator():
312             authors = list(pic.tags.filter(category='author'))
313             if authors:
314                 for author in authors:
315                     pics_by_author[author].append(pic)
316             else:
317                 orphans.append(pic)
318
319         return pics_by_author, orphans
320
321     @property
322     def info(self):
323         if not hasattr(self, '_info'):
324             from librarian import dcparser
325             from librarian import picture
326             info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
327             self._info = info
328         return self._info
329
330     def reset_short_html(self):
331         if self.id is None:
332             return
333
334         for area in self.areas.all().iterator():
335             area.reset_short_html()
336
337         try:
338             author = self.tags.filter(category='author')[0].sort_key
339         except IndexError:
340             author = u''
341         type(self).objects.filter(pk=self.pk).update(sort_key_author=author)
342
343         cache_key = "Picture.short_html/%d/%s"
344         for lang, langname in settings.LANGUAGES:
345             permanent_cache.delete(cache_key % (self.id, lang))
346
347     def short_html(self):
348         if self.id:
349             cache_key = "Picture.short_html/%d/%s" % (self.id, get_language())
350             short_html = permanent_cache.get(cache_key)
351         else:
352             short_html = None
353
354         if short_html is not None:
355             return mark_safe(short_html)
356         else:
357             tags = self.tags.filter(category__in=('author', 'kind', 'epoch', 'genre'))
358             tags = split_tags(tags)
359
360             short_html = unicode(render_to_string(
361                     'picture/picture_short.html',
362                     {'picture': self, 'tags': tags}))
363
364             if self.id:
365                 permanent_cache.set(cache_key, short_html)
366             return mark_safe(short_html)
367
368     def pretty_title(self, html_links=False):
369         picture = self
370         names = [(tag.name,
371                   catalogue.models.Tag.create_url('author', tag.slug))
372                  for tag in self.tags.filter(category='author')]
373         names.append((self.title, self.get_absolute_url()))
374
375         if html_links:
376             names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
377         else:
378             names = [tag[0] for tag in names]
379         return ', '.join(names)
380
381     # copied from book.py, figure out
382     def related_themes(self):
383         return catalogue.models.Tag.usage_for_queryset(
384             self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))