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
19 from sorl.thumbnail import get_thumbnail, default
20 from .engine import CustomCroppingEngine
24 from django.utils.translation import get_language, ugettext_lazy as _
25 from newtagging import managers
29 permanent_cache = get_cache('permanent')
31 picture_storage = FileSystemStorage(location=path.join(
32 settings.MEDIA_ROOT, 'pictures'),
33 base_url=settings.MEDIA_URL + "pictures/")
36 class PictureArea(models.Model):
37 picture = models.ForeignKey('picture.Picture', related_name='areas')
38 area = jsonfield.JSONField(_('area'), default={}, editable=False)
39 kind = models.CharField(_('kind'), max_length=10, blank=False,
40 null=False, db_index=True,
41 choices=(('thing', _('thing')),
42 ('theme', _('theme'))))
44 objects = models.Manager()
45 tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
46 tags = managers.TagDescriptor(catalogue.models.Tag)
49 def rectangle(cls, picture, kind, coords):
56 def reset_short_html(self):
60 cache_key = "PictureArea.short_html/%d/%s"
61 for lang, langname in settings.LANGUAGES:
62 permanent_cache.delete(cache_key % (self.id, lang))
67 cache_key = "PictureArea.short_html/%d/%s" % (self.id, get_language())
68 short_html = permanent_cache.get(cache_key)
72 if short_html is not None:
73 return mark_safe(short_html)
75 theme = self.tags.filter(category='theme')
76 theme = theme and theme[0] or None
77 thing = self.tags.filter(category='thing')
78 thing = thing and thing[0] or None
80 short_html = unicode(render_to_string(
81 'picture/picturearea_short.html', locals()))
83 permanent_cache.set(cache_key, short_html)
84 return mark_safe(short_html)
87 class Picture(models.Model):
92 title = models.CharField(_('title'), max_length=120)
93 slug = models.SlugField(_('slug'), max_length=120, db_index=True, unique=True)
94 sort_key = models.CharField(_('sort key'), max_length=120, db_index=True, editable=False)
95 sort_key_author = models.CharField(_('sort key by author'), max_length=120, db_index=True, editable=False, default=u'')
96 created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
97 changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True)
98 xml_file = models.FileField('xml_file', upload_to="xml", storage=picture_storage)
99 image_file = ImageField(_('image_file'), upload_to="images", storage=picture_storage)
100 html_file = models.FileField('html_file', upload_to="html", storage=picture_storage)
101 areas_json = jsonfield.JSONField(_('picture areas JSON'), default={}, editable=False)
102 extra_info = jsonfield.JSONField(_('extra information'), default={})
103 culturepl_link = models.CharField(blank=True, max_length=240)
104 wiki_link = models.CharField(blank=True, max_length=240)
106 _related_info = jsonfield.JSONField(blank=True, null=True, editable=False)
108 width = models.IntegerField(null=True)
109 height = models.IntegerField(null=True)
111 objects = models.Manager()
112 tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
113 tags = managers.TagDescriptor(catalogue.models.Tag)
115 class AlreadyExists(Exception):
119 ordering = ('sort_key',)
121 verbose_name = _('picture')
122 verbose_name_plural = _('pictures')
124 def save(self, force_insert=False, force_update=False, reset_short_html=True, **kwargs):
125 from sortify import sortify
127 self.sort_key = sortify(self.title)
129 ret = super(Picture, self).save(force_insert, force_update)
132 self.reset_short_html()
136 def __unicode__(self):
140 def get_absolute_url(self):
141 return ('picture.views.picture_detail', [self.slug])
144 def from_xml_file(cls, xml_file, image_file=None, image_store=None, overwrite=False):
146 Import xml and it's accompanying image file.
147 If image file is missing, it will be fetched by librarian.picture.ImageStore
148 which looks for an image file in the same directory the xml is, with extension matching
151 from sortify import sortify
152 from django.core.files import File
153 from librarian.picture import WLPicture, ImageStore
154 close_xml_file = False
155 close_image_file = False
158 if image_file is not None and not isinstance(image_file, File):
159 image_file = File(open(image_file))
160 close_image_file = True
162 if not isinstance(xml_file, File):
163 xml_file = File(open(xml_file))
164 close_xml_file = True
167 # use librarian to parse meta-data
168 if image_store is None:
169 image_store = ImageStore(picture_storage.path('images'))
170 picture_xml = WLPicture.from_file(xml_file, image_store=image_store)
172 picture, created = Picture.objects.get_or_create(slug=picture_xml.slug)
173 if not created and not overwrite:
174 raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug)
176 picture.areas.all().delete()
177 picture.title = unicode(picture_xml.picture_info.title)
178 picture.extra_info = picture_xml.picture_info.to_dict()
180 picture_tags = set(catalogue.models.Tag.tags_from_info(picture_xml.picture_info))
184 area_data = {'themes':{}, 'things':{}}
186 for part in picture_xml.partiter():
187 if picture_xml.frame:
188 c = picture_xml.frame[0]
189 part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']]
190 if part.get('object', None) is not None:
191 objname = part['object']
192 tag, created = catalogue.models.Tag.objects.get_or_create(slug=slughifi(objname), category='thing')
195 tag.sort_key = sortify(tag.name)
198 area_data['things'][tag.slug] = {
199 'object': part['object'],
200 'coords': part['coords'],
202 area = PictureArea.rectangle(picture, 'thing', part['coords'])
209 for motif in part['themes']:
210 tag, created = catalogue.models.Tag.objects.get_or_create(slug=slughifi(motif), category='theme')
213 tag.sort_key = sortify(tag.name)
217 area_data['themes'][tag.slug] = {
219 'coords': part['coords']
222 logging.debug("coords for theme: %s" % part['coords'])
223 area = PictureArea.rectangle(picture, 'theme', part['coords'])
225 area.tags = _tags.union(picture_tags)
227 picture.tags = picture_tags.union(motif_tags).union(thing_tags)
228 picture.areas_json = area_data
230 if image_file is not None:
233 img = picture_xml.image_file()
235 modified = cls.crop_to_frame(picture_xml, img)
236 picture.width, picture.height = modified.size
238 modified_file = StringIO()
239 modified.save(modified_file, format='png', quality=95)
240 # FIXME: hardcoded extension - detect from DC format or orginal filename
241 picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
243 picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
245 tasks.generate_picture_html(picture.id)
247 except Exception, ex:
248 logging.exception("Exception during import, rolling back")
249 transaction.rollback()
263 def crop_to_frame(cls, wlpic, image_file):
264 img = Image.open(image_file)
265 if wlpic.frame is None:
267 img = img.crop(itertools.chain(*wlpic.frame))
271 def picture_list(cls, filter=None):
272 """Generates a hierarchical listing of all pictures
273 Pictures are optionally filtered with a test function.
276 pics = cls.objects.all().order_by('sort_key')\
277 .only('title', 'slug', 'image_file')
280 pics = pics.filter(filter).distinct()
282 pics_by_author = SortedDict()
284 for tag in catalogue.models.Tag.objects.filter(category='author'):
285 pics_by_author[tag] = []
287 for pic in pics.iterator():
288 authors = list(pic.tags.filter(category='author'))
290 for author in authors:
291 pics_by_author[author].append(pic)
295 return pics_by_author, orphans
299 if not hasattr(self, '_info'):
300 from librarian import dcparser
301 from librarian import picture
302 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
306 def reset_short_html(self):
310 type(self).objects.filter(pk=self.pk).update(_related_info=None)
311 for area in self.areas.all().iterator():
312 area.reset_short_html()
315 author = self.tags.filter(category='author')[0].sort_key
318 type(self).objects.filter(pk=self.pk).update(sort_key_author=author)
320 cache_key = "Picture.short_html/%d/%s"
321 for lang, langname in settings.LANGUAGES:
322 permanent_cache.delete(cache_key % (self.id, lang))
324 def short_html(self):
326 cache_key = "Picture.short_html/%d/%s" % (self.id, get_language())
327 short_html = get_cache('permanent').get(cache_key)
331 if short_html is not None:
332 return mark_safe(short_html)
334 tags = self.tags.filter(category__in=('author', 'kind', 'epoch', 'genre'))
335 tags = split_tags(tags)
337 short_html = unicode(render_to_string(
338 'picture/picture_short.html',
339 {'picture': self, 'tags': tags}))
342 get_cache('permanent').set(cache_key, short_html)
343 return mark_safe(short_html)
345 def pretty_title(self, html_links=False):
347 # TODO Add translations (related_tag_info)
349 catalogue.models.Tag.create_url('author', tag.slug))
350 for tag in self.tags.filter(category='author')]
351 names.append((self.title, self.get_absolute_url()))
354 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
356 names = [tag[0] for tag in names]
357 return ', '.join(names)
359 def related_info(self):
360 """Keeps info about related objects (tags) in cache field."""
361 if self._related_info is not None:
362 return self._related_info
366 tags = self.tags.filter(category__in=(
367 'author', 'kind', 'genre', 'epoch'))
368 tags = split_tags(tags)
369 for category in tags:
371 for tag in tags[category]:
372 tag_info = {'slug': tag.slug, 'name': tag.name}
373 for lc, ln in settings.LANGUAGES:
374 tag_name = getattr(tag, "name_%s" % lc)
376 tag_info["name_%s" % lc] = tag_name
378 rel['tags'][category] = cat
382 type(self).objects.filter(pk=self.pk).update(_related_info=rel)
385 # copied from book.py, figure out
386 def related_themes(self):
387 # self.theme_counter hides a computation, so a line below actually makes sense
388 theme_counter = self.theme_counter
389 picture_themes = list(catalogue.models.Tag.objects.filter(pk__in=theme_counter.keys()))
390 for tag in picture_themes:
391 tag.count = theme_counter[tag.pk]
392 return picture_themes
394 def reset_tag_counter(self):
398 cache_key = "Picture.tag_counter/%d" % self.id
399 permanent_cache.delete(cache_key)
401 self.parent.reset_tag_counter()
404 def tag_counter(self):
406 cache_key = "Picture.tag_counter/%d" % self.id
407 tags = permanent_cache.get(cache_key)
413 # do we need to do this? there are no children here.
414 for tag in self.tags.exclude(category__in=('book', 'theme', 'thing', 'set')).order_by().iterator():
418 permanent_cache.set(cache_key, tags)
421 def reset_theme_counter(self):
425 cache_key = "Picture.theme_counter/%d" % self.id
426 permanent_cache.delete(cache_key)
429 def theme_counter(self):
431 cache_key = "Picture.theme_counter/%d" % self.id
432 tags = permanent_cache.get(cache_key)
438 for area in PictureArea.objects.filter(picture=self).order_by().iterator():
439 for tag in area.tags.filter(category__in=('theme','thing')).order_by().iterator():
440 tags[tag.pk] = tags.get(tag.pk, 0) + 1
443 permanent_cache.set(cache_key, tags)