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