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