fa602e66c851b66191370e0dbd296b29e6f48b2f
[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.core.cache import get_cache
10 from catalogue.utils import split_tags
11 from django.utils.safestring import mark_safe
12 from fnpdjango.utils.text.slughifi import slughifi
13 from picture import tasks
14 from StringIO import StringIO
15 import jsonfield
16 import itertools
17
18 from PIL import Image
19
20 from django.utils.translation import ugettext_lazy as _
21 from newtagging import managers
22 from os import path
23
24
25 picture_storage = FileSystemStorage(location=path.join(
26         settings.MEDIA_ROOT, 'pictures'),
27         base_url=settings.MEDIA_URL + "pictures/")
28
29
30 class Picture(models.Model):
31     """
32     Picture resource.
33
34     """
35     title       = models.CharField(_('title'), max_length=120)
36     slug        = models.SlugField(_('slug'), max_length=120, db_index=True, unique=True)
37     sort_key    = models.CharField(_('sort key'), max_length=120, db_index=True, editable=False)
38     created_at  = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
39     changed_at  = models.DateTimeField(_('creation date'), auto_now=True, db_index=True)
40     xml_file    = models.FileField('xml_file', upload_to="xml", storage=picture_storage)
41     image_file  = ImageField(_('image_file'), upload_to="images", storage=picture_storage)
42     html_file   = models.FileField('html_file', upload_to="html", storage=picture_storage)
43     areas       = jsonfield.JSONField(_('picture areas'), default={}, editable=False)
44
45     objects     = models.Manager()
46     tagged      = managers.ModelTaggedItemManager(catalogue.models.Tag)
47     tags        = managers.TagDescriptor(catalogue.models.Tag)
48
49     class AlreadyExists(Exception):
50         pass
51
52     class Meta:
53         ordering = ('sort_key',)
54
55         verbose_name = _('picture')
56         verbose_name_plural = _('pictures')
57
58     def save(self, force_insert=False, force_update=False, reset_short_html=True, **kwargs):
59         from sortify import sortify
60
61         self.sort_key = sortify(self.title)
62
63         ret = super(Picture, self).save(force_insert, force_update)
64
65         if reset_short_html:
66             self.reset_short_html()
67
68         return ret
69
70     def __unicode__(self):
71         return self.title
72
73     @permalink
74     def get_absolute_url(self):
75         return ('picture.views.picture_detail', [self.slug])
76
77     @classmethod
78     def from_xml_file(cls, xml_file, image_file=None, image_store=None, overwrite=False):
79         """
80         Import xml and it's accompanying image file.
81         If image file is missing, it will be fetched by librarian.picture.ImageStore
82         which looks for an image file in the same directory the xml is, with extension matching
83         its mime type.
84         """
85         from sortify import sortify
86         from django.core.files import File
87         from librarian.picture import WLPicture, ImageStore
88         close_xml_file = False
89         close_image_file = False
90
91
92         if image_file is not None and not isinstance(image_file, File):
93             image_file = File(open(image_file))
94             close_image_file = True
95
96         if not isinstance(xml_file, File):
97             xml_file = File(open(xml_file))
98             close_xml_file = True
99         
100         try:
101             # use librarian to parse meta-data
102             if image_store is None:
103                 image_store = ImageStore(picture_storage.path('images'))
104             picture_xml = WLPicture.from_file(xml_file, image_store=image_store)
105
106             picture, created = Picture.objects.get_or_create(slug=picture_xml.slug)
107             if not created and not overwrite:
108                 raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug)
109
110             picture.title = picture_xml.picture_info.title
111
112             motif_tags = set()
113             thing_tags = set()
114             area_data = {'themes':{}, 'things':{}}
115
116             for part in picture_xml.partiter():
117                 if picture_xml.frame:
118                     c = picture_xml.frame[0]
119                     part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']]
120                 if part.get('object', None) is not None:
121                     objname = part['object']
122                     tag, created = catalogue.models.Tag.objects.get_or_create(slug=slughifi(objname), category='thing')
123                     if created:
124                         tag.name = objname
125                         tag.sort_key = sortify(tag.name)
126                         tag.save()
127                     thing_tags.add(tag)
128                     area_data['things'][tag.slug] = {
129                         'object': part['object'],
130                         'coords': part['coords'],
131                         }
132                 else:
133                     for motif in part['themes']:
134                         tag, created = catalogue.models.Tag.objects.get_or_create(slug=slughifi(motif), category='theme')
135                         if created:
136                             tag.name = motif
137                             tag.sort_key = sortify(tag.name)
138                             tag.save()
139                         motif_tags.add(tag)
140                         area_data['themes'][tag.slug] = {
141                             'theme': motif,
142                             'coords': part['coords']
143                             }
144
145             picture.tags = catalogue.models.Tag.tags_from_info(picture_xml.picture_info) + \
146                 list(motif_tags) + list(thing_tags)
147             picture.areas = area_data
148
149             if image_file is not None:
150                 img = image_file
151             else:
152                 img = picture_xml.image_file()
153
154             modified = cls.crop_to_frame(picture_xml, img)
155             # FIXME: hardcoded extension - detect from DC format or orginal filename
156             picture.image_file.save(path.basename(picture_xml.image_path), File(modified))
157
158             picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
159             picture.save()
160             tasks.generate_picture_html(picture.id)
161
162         except Exception, ex:
163             print "Rolling back a transaction"
164             transaction.rollback()
165             raise ex
166
167         finally:
168             if close_xml_file:
169                 xml_file.close()
170             if close_image_file:
171                 image_file.close()
172
173         transaction.commit()
174
175         return picture
176
177     @classmethod
178     def crop_to_frame(cls, wlpic, image_file):
179         if wlpic.frame is None:
180             return image_file
181         img = Image.open(image_file)
182         img = img.crop(itertools.chain(*wlpic.frame))
183         contents = StringIO()
184         img.save(contents, format='png', quality=95)
185         return contents
186
187     @classmethod
188     def picture_list(cls, filter=None):
189         """Generates a hierarchical listing of all pictures
190         Pictures are optionally filtered with a test function.
191         """
192
193         pics = cls.objects.all().order_by('sort_key')\
194             .only('title', 'slug', 'image_file')
195
196         if filter:
197             pics = pics.filter(filter).distinct()
198
199         pics_by_author = SortedDict()
200         orphans = []
201         for tag in catalogue.models.Tag.objects.filter(category='author'):
202             pics_by_author[tag] = []
203
204         for pic in pics.iterator():
205             authors = list(pic.tags.filter(category='author'))
206             if authors:
207                 for author in authors:
208                     pics_by_author[author].append(pic)
209             else:
210                 orphans.append(pic)
211
212         return pics_by_author, orphans
213
214     @property
215     def info(self):
216         if not hasattr(self, '_info'):
217             from librarian import dcparser
218             from librarian import picture
219             info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
220             self._info = info
221         return self._info
222
223     def reset_short_html(self):
224         if self.id is None:
225             return
226
227         cache_key = "Picture.short_html/%d" % (self.id)
228         get_cache('permanent').delete(cache_key)
229
230     def short_html(self):
231         if self.id:
232             cache_key = "Picture.short_html/%d" % (self.id)
233             short_html = get_cache('permanent').get(cache_key)
234         else:
235             short_html = None
236
237         if short_html is not None:
238             return mark_safe(short_html)
239         else:
240             tags = self.tags.filter(category__in=('author', 'kind', 'epoch', 'genre'))
241             tags = split_tags(tags)
242
243             short_html = unicode(render_to_string(
244                     'picture/picture_short.html',
245                     {'picture': self, 'tags': tags}))
246
247             if self.id:
248                 get_cache('permanent').set(cache_key, short_html)
249             return mark_safe(short_html)