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.
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 django.template.loader import render_to_string
14 from django.utils.safestring import mark_safe
15 from django.core.cache import caches
16 from catalogue.utils import split_tags
17 from fnpdjango.utils.text.slughifi import slughifi
18 from picture import tasks
19 from StringIO import StringIO
26 from django.utils.translation import get_language, ugettext_lazy as _
27 from newtagging import managers
31 permanent_cache = caches['permanent']
33 picture_storage = FileSystemStorage(location=path.join(
34 settings.MEDIA_ROOT, 'pictures'),
35 base_url=settings.MEDIA_URL + "pictures/")
38 class PictureArea(models.Model):
39 picture = models.ForeignKey('picture.Picture', related_name='areas')
40 area = jsonfield.JSONField(_('area'), default={}, editable=False)
41 kind = models.CharField(_('kind'), max_length=10, blank=False,
42 null=False, db_index=True,
43 choices=(('thing', _('thing')),
44 ('theme', _('theme'))))
46 objects = models.Manager()
47 tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
48 tags = managers.TagDescriptor(catalogue.models.Tag)
49 tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
52 def rectangle(cls, picture, kind, coords):
59 def reset_short_html(self):
63 cache_key = "PictureArea.short_html/%d/%s"
64 for lang, langname in settings.LANGUAGES:
65 permanent_cache.delete(cache_key % (self.id, lang))
70 cache_key = "PictureArea.short_html/%d/%s" % (self.id, get_language())
71 short_html = permanent_cache.get(cache_key)
75 if short_html is not None:
76 return mark_safe(short_html)
78 theme = self.tags.filter(category='theme')
79 theme = theme and theme[0] or None
80 thing = self.tags.filter(category='thing')
81 thing = thing and thing[0] or None
83 short_html = unicode(render_to_string(
84 'picture/picturearea_short.html', locals()))
86 permanent_cache.set(cache_key, short_html)
87 return mark_safe(short_html)
90 class Picture(models.Model):
95 title = models.CharField(_('title'), max_length=120)
96 slug = models.SlugField(_('slug'), max_length=120, db_index=True, unique=True)
97 sort_key = models.CharField(_('sort key'), max_length=120, db_index=True, editable=False)
98 sort_key_author = models.CharField(_('sort key by author'), max_length=120, db_index=True, editable=False, default=u'')
99 created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
100 changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True)
101 xml_file = models.FileField('xml_file', upload_to="xml", storage=picture_storage)
102 image_file = ImageField(_('image_file'), upload_to="images", storage=picture_storage)
103 html_file = models.FileField('html_file', upload_to="html", storage=picture_storage)
104 areas_json = jsonfield.JSONField(_('picture areas JSON'), default={}, editable=False)
105 extra_info = jsonfield.JSONField(_('extra information'), default={})
106 culturepl_link = models.CharField(blank=True, max_length=240)
107 wiki_link = models.CharField(blank=True, max_length=240)
109 _related_info = jsonfield.JSONField(blank=True, null=True, editable=False)
111 width = models.IntegerField(null=True)
112 height = models.IntegerField(null=True)
114 objects = models.Manager()
115 tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
116 tags = managers.TagDescriptor(catalogue.models.Tag)
117 tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
119 class AlreadyExists(Exception):
123 ordering = ('sort_key',)
125 verbose_name = _('picture')
126 verbose_name_plural = _('pictures')
128 def save(self, force_insert=False, force_update=False, reset_short_html=True, **kwargs):
129 from sortify import sortify
131 self.sort_key = sortify(self.title)
133 ret = super(Picture, self).save(force_insert, force_update)
136 self.reset_short_html()
140 def __unicode__(self):
144 def get_absolute_url(self):
145 return ('picture.views.picture_detail', [self.slug])
148 def from_xml_file(cls, xml_file, image_file=None, image_store=None, overwrite=False):
150 Import xml and it's accompanying image file.
151 If image file is missing, it will be fetched by librarian.picture.ImageStore
152 which looks for an image file in the same directory the xml is, with extension matching
155 from sortify import sortify
156 from django.core.files import File
157 from librarian.picture import WLPicture, ImageStore
158 close_xml_file = False
159 close_image_file = False
162 if image_file is not None and not isinstance(image_file, File):
163 image_file = File(open(image_file))
164 close_image_file = True
166 if not isinstance(xml_file, File):
167 xml_file = File(open(xml_file))
168 close_xml_file = True
171 # use librarian to parse meta-data
172 if image_store is None:
173 image_store = ImageStore(picture_storage.path('images'))
174 picture_xml = WLPicture.from_file(xml_file, image_store=image_store)
176 picture, created = Picture.objects.get_or_create(slug=picture_xml.slug)
177 if not created and not overwrite:
178 raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug)
180 picture.areas.all().delete()
181 picture.title = unicode(picture_xml.picture_info.title)
182 picture.extra_info = picture_xml.picture_info.to_dict()
184 picture_tags = set(catalogue.models.Tag.tags_from_info(picture_xml.picture_info))
188 area_data = {'themes':{}, 'things':{}}
190 for part in picture_xml.partiter():
191 if picture_xml.frame:
192 c = picture_xml.frame[0]
193 part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']]
194 if part.get('object', None) is not None:
195 objname = part['object']
196 tag, created = catalogue.models.Tag.objects.get_or_create(slug=slughifi(objname), category='thing')
199 tag.sort_key = sortify(tag.name)
202 area_data['things'][tag.slug] = {
203 'object': part['object'],
204 'coords': part['coords'],
206 area = PictureArea.rectangle(picture, 'thing', part['coords'])
213 for motif in part['themes']:
214 tag, created = catalogue.models.Tag.objects.get_or_create(slug=slughifi(motif), category='theme')
217 tag.sort_key = sortify(tag.name)
221 area_data['themes'][tag.slug] = {
223 'coords': part['coords']
226 logging.debug("coords for theme: %s" % part['coords'])
227 area = PictureArea.rectangle(picture, 'theme', part['coords'])
229 area.tags = _tags.union(picture_tags)
231 picture.tags = picture_tags.union(motif_tags).union(thing_tags)
232 picture.areas_json = area_data
234 if image_file is not None:
237 img = picture_xml.image_file()
239 modified = cls.crop_to_frame(picture_xml, img)
240 modified = cls.add_source_note(picture_xml, modified)
242 picture.width, picture.height = modified.size
244 modified_file = StringIO()
245 modified.save(modified_file, format='png', quality=95)
246 # FIXME: hardcoded extension - detect from DC format or orginal filename
247 picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
249 picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
251 tasks.generate_picture_html(picture.id)
253 except Exception, ex:
254 logging.exception("Exception during import, rolling back")
255 transaction.rollback()
269 def crop_to_frame(cls, wlpic, image_file):
270 img = Image.open(image_file)
271 if wlpic.frame is None:
273 img = img.crop(itertools.chain(*wlpic.frame))
277 def add_source_note(wlpic, img):
278 from PIL import ImageDraw, ImageFont
279 from librarian import get_resource
281 annotated = Image.new(img.mode,
282 (img.size[0], img.size[1] + 40),
285 annotated.paste(img, (0, 0))
286 annotation = Image.new(img.mode, (3000, 120), (255, 255, 255))
287 ImageDraw.Draw(annotation).text(
289 wlpic.picture_info.source_name,
291 font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
293 annotated.paste(annotation.resize((1000, 40), Image.ANTIALIAS), (0, img.size[1]))
297 def picture_list(cls, filter=None):
298 """Generates a hierarchical listing of all pictures
299 Pictures are optionally filtered with a test function.
302 pics = cls.objects.all().order_by('sort_key')\
303 .only('title', 'slug', 'image_file')
306 pics = pics.filter(filter).distinct()
308 pics_by_author = SortedDict()
310 for tag in catalogue.models.Tag.objects.filter(category='author'):
311 pics_by_author[tag] = []
313 for pic in pics.iterator():
314 authors = list(pic.tags.filter(category='author'))
316 for author in authors:
317 pics_by_author[author].append(pic)
321 return pics_by_author, orphans
325 if not hasattr(self, '_info'):
326 from librarian import dcparser
327 from librarian import picture
328 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
332 def reset_short_html(self):
336 type(self).objects.filter(pk=self.pk).update(_related_info=None)
337 for area in self.areas.all().iterator():
338 area.reset_short_html()
341 author = self.tags.filter(category='author')[0].sort_key
344 type(self).objects.filter(pk=self.pk).update(sort_key_author=author)
346 cache_key = "Picture.short_html/%d/%s"
347 for lang, langname in settings.LANGUAGES:
348 permanent_cache.delete(cache_key % (self.id, lang))
350 def short_html(self):
352 cache_key = "Picture.short_html/%d/%s" % (self.id, get_language())
353 short_html = get_cache('permanent').get(cache_key)
357 if short_html is not None:
358 return mark_safe(short_html)
360 tags = self.tags.filter(category__in=('author', 'kind', 'epoch', 'genre'))
361 tags = split_tags(tags)
363 short_html = unicode(render_to_string(
364 'picture/picture_short.html',
365 {'picture': self, 'tags': tags}))
368 get_cache('permanent').set(cache_key, short_html)
369 return mark_safe(short_html)
371 def pretty_title(self, html_links=False):
373 # TODO Add translations (related_tag_info)
375 catalogue.models.Tag.create_url('author', tag.slug))
376 for tag in self.tags.filter(category='author')]
377 names.append((self.title, self.get_absolute_url()))
380 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
382 names = [tag[0] for tag in names]
383 return ', '.join(names)
385 def related_info(self):
386 """Keeps info about related objects (tags) in cache field."""
387 if self._related_info is not None:
388 return self._related_info
392 tags = self.tags.filter(category__in=(
393 'author', 'kind', 'genre', 'epoch'))
394 tags = split_tags(tags)
395 for category in tags:
397 for tag in tags[category]:
398 tag_info = {'slug': tag.slug, 'name': tag.name}
399 for lc, ln in settings.LANGUAGES:
400 tag_name = getattr(tag, "name_%s" % lc)
402 tag_info["name_%s" % lc] = tag_name
404 rel['tags'][category] = cat
408 type(self).objects.filter(pk=self.pk).update(_related_info=rel)
411 # copied from book.py, figure out
412 def related_themes(self):
413 # self.theme_counter hides a computation, so a line below actually makes sense
414 theme_counter = self.theme_counter
415 picture_themes = list(catalogue.models.Tag.objects.filter(pk__in=theme_counter.keys()))
416 for tag in picture_themes:
417 tag.count = theme_counter[tag.pk]
418 return picture_themes
420 def reset_tag_counter(self):
424 cache_key = "Picture.tag_counter/%d" % self.id
425 permanent_cache.delete(cache_key)
427 self.parent.reset_tag_counter()
430 def tag_counter(self):
432 cache_key = "Picture.tag_counter/%d" % self.id
433 tags = permanent_cache.get(cache_key)
439 # do we need to do this? there are no children here.
440 for tag in self.tags.exclude(category__in=('book', 'theme', 'thing', 'set')).order_by().iterator():
444 permanent_cache.set(cache_key, tags)
447 def reset_theme_counter(self):
451 cache_key = "Picture.theme_counter/%d" % self.id
452 permanent_cache.delete(cache_key)
455 def theme_counter(self):
457 cache_key = "Picture.theme_counter/%d" % self.id
458 tags = permanent_cache.get(cache_key)
464 for area in PictureArea.objects.filter(picture=self).order_by().iterator():
465 for tag in area.tags.filter(category__in=('theme', 'thing')).order_by().iterator():
466 tags[tag.pk] = tags.get(tag.pk, 0) + 1
469 permanent_cache.set(cache_key, tags)