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