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.core.files.storage import FileSystemStorage
11 from django.utils.datastructures import SortedDict
12 from django.template.loader import render_to_string
13 from django.utils.safestring import mark_safe
14 from django.core.cache import get_cache
15 from catalogue.utils import split_tags
16 from fnpdjango.utils.text.slughifi import slughifi
17 from picture import tasks
18 from StringIO import StringIO
25 from django.utils.translation import get_language, ugettext_lazy as _
26 from newtagging import managers
30 permanent_cache = get_cache('permanent')
32 picture_storage = FileSystemStorage(location=path.join(
33 settings.MEDIA_ROOT, 'pictures'),
34 base_url=settings.MEDIA_URL + "pictures/")
37 class PictureArea(models.Model):
38 picture = models.ForeignKey('picture.Picture', related_name='areas')
39 area = jsonfield.JSONField(_('area'), default={}, editable=False)
40 kind = models.CharField(_('kind'), max_length=10, blank=False,
41 null=False, db_index=True,
42 choices=(('thing', _('thing')),
43 ('theme', _('theme'))))
45 objects = models.Manager()
46 tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
47 tags = managers.TagDescriptor(catalogue.models.Tag)
50 def rectangle(cls, picture, kind, coords):
57 def reset_short_html(self):
61 cache_key = "PictureArea.short_html/%d/%s"
62 for lang, langname in settings.LANGUAGES:
63 permanent_cache.delete(cache_key % (self.id, lang))
68 cache_key = "PictureArea.short_html/%d/%s" % (self.id, get_language())
69 short_html = permanent_cache.get(cache_key)
73 if short_html is not None:
74 return mark_safe(short_html)
76 theme = self.tags.filter(category='theme')
77 theme = theme and theme[0] or None
78 thing = self.tags.filter(category='thing')
79 thing = thing and thing[0] or None
81 short_html = unicode(render_to_string(
82 'picture/picturearea_short.html', locals()))
84 permanent_cache.set(cache_key, short_html)
85 return mark_safe(short_html)
88 class Picture(models.Model):
93 title = models.CharField(_('title'), max_length=120)
94 slug = models.SlugField(_('slug'), max_length=120, db_index=True, unique=True)
95 sort_key = models.CharField(_('sort key'), max_length=120, db_index=True, editable=False)
96 sort_key_author = models.CharField(_('sort key by author'), max_length=120, db_index=True, editable=False, default=u'')
97 created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
98 changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True)
99 xml_file = models.FileField('xml_file', upload_to="xml", storage=picture_storage)
100 image_file = ImageField(_('image_file'), upload_to="images", storage=picture_storage)
101 html_file = models.FileField('html_file', upload_to="html", storage=picture_storage)
102 areas_json = jsonfield.JSONField(_('picture areas JSON'), default={}, editable=False)
103 extra_info = jsonfield.JSONField(_('extra information'), default={})
104 culturepl_link = models.CharField(blank=True, max_length=240)
105 wiki_link = models.CharField(blank=True, max_length=240)
107 _related_info = jsonfield.JSONField(blank=True, null=True, editable=False)
109 width = models.IntegerField(null=True)
110 height = models.IntegerField(null=True)
112 objects = models.Manager()
113 tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
114 tags = managers.TagDescriptor(catalogue.models.Tag)
116 class AlreadyExists(Exception):
120 ordering = ('sort_key',)
122 verbose_name = _('picture')
123 verbose_name_plural = _('pictures')
125 def save(self, force_insert=False, force_update=False, reset_short_html=True, **kwargs):
126 from sortify import sortify
128 self.sort_key = sortify(self.title)
130 ret = super(Picture, self).save(force_insert, force_update)
133 self.reset_short_html()
137 def __unicode__(self):
141 def get_absolute_url(self):
142 return ('picture.views.picture_detail', [self.slug])
145 def from_xml_file(cls, xml_file, image_file=None, image_store=None, overwrite=False):
147 Import xml and it's accompanying image file.
148 If image file is missing, it will be fetched by librarian.picture.ImageStore
149 which looks for an image file in the same directory the xml is, with extension matching
152 from sortify import sortify
153 from django.core.files import File
154 from librarian.picture import WLPicture, ImageStore
155 close_xml_file = False
156 close_image_file = False
159 if image_file is not None and not isinstance(image_file, File):
160 image_file = File(open(image_file))
161 close_image_file = True
163 if not isinstance(xml_file, File):
164 xml_file = File(open(xml_file))
165 close_xml_file = True
168 # use librarian to parse meta-data
169 if image_store is None:
170 image_store = ImageStore(picture_storage.path('images'))
171 picture_xml = WLPicture.from_file(xml_file, image_store=image_store)
173 picture, created = Picture.objects.get_or_create(slug=picture_xml.slug)
174 if not created and not overwrite:
175 raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug)
177 picture.areas.all().delete()
178 picture.title = unicode(picture_xml.picture_info.title)
179 picture.extra_info = picture_xml.picture_info.to_dict()
181 picture_tags = set(catalogue.models.Tag.tags_from_info(picture_xml.picture_info))
185 area_data = {'themes':{}, 'things':{}}
187 for part in picture_xml.partiter():
188 if picture_xml.frame:
189 c = picture_xml.frame[0]
190 part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']]
191 if part.get('object', None) is not None:
192 objname = part['object']
193 tag, created = catalogue.models.Tag.objects.get_or_create(slug=slughifi(objname), category='thing')
196 tag.sort_key = sortify(tag.name)
199 area_data['things'][tag.slug] = {
200 'object': part['object'],
201 'coords': part['coords'],
203 area = PictureArea.rectangle(picture, 'thing', part['coords'])
210 for motif in part['themes']:
211 tag, created = catalogue.models.Tag.objects.get_or_create(slug=slughifi(motif), category='theme')
214 tag.sort_key = sortify(tag.name)
218 area_data['themes'][tag.slug] = {
220 'coords': part['coords']
223 logging.debug("coords for theme: %s" % part['coords'])
224 area = PictureArea.rectangle(picture, 'theme', part['coords'])
226 area.tags = _tags.union(picture_tags)
228 picture.tags = picture_tags.union(motif_tags).union(thing_tags)
229 picture.areas_json = area_data
231 if image_file is not None:
234 img = picture_xml.image_file()
236 modified = cls.crop_to_frame(picture_xml, img)
237 modified = cls.add_source_note(picture_xml, modified)
239 picture.width, picture.height = modified.size
241 modified_file = StringIO()
242 modified.save(modified_file, format='png', quality=95)
243 # FIXME: hardcoded extension - detect from DC format or orginal filename
244 picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
246 picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
248 tasks.generate_picture_html(picture.id)
250 except Exception, ex:
251 logging.exception("Exception during import, rolling back")
252 transaction.rollback()
266 def crop_to_frame(cls, wlpic, image_file):
267 img = Image.open(image_file)
268 if wlpic.frame is None:
270 img = img.crop(itertools.chain(*wlpic.frame))
274 def add_source_note(wlpic, img):
275 from PIL import ImageDraw, ImageFont
276 from librarian import get_resource
278 annotated = Image.new(img.mode,
279 (img.size[0], img.size[1] + 40),
282 annotated.paste(img, (0, 0))
283 annotation = Image.new(img.mode, (3000, 120), (255, 255, 255))
284 ImageDraw.Draw(annotation).text(
286 wlpic.picture_info.source_name,
288 font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
290 annotated.paste(annotation.resize((1000, 40), Image.ANTIALIAS), (0, img.size[1]))
294 def picture_list(cls, filter=None):
295 """Generates a hierarchical listing of all pictures
296 Pictures are optionally filtered with a test function.
299 pics = cls.objects.all().order_by('sort_key')\
300 .only('title', 'slug', 'image_file')
303 pics = pics.filter(filter).distinct()
305 pics_by_author = SortedDict()
307 for tag in catalogue.models.Tag.objects.filter(category='author'):
308 pics_by_author[tag] = []
310 for pic in pics.iterator():
311 authors = list(pic.tags.filter(category='author'))
313 for author in authors:
314 pics_by_author[author].append(pic)
318 return pics_by_author, orphans
322 if not hasattr(self, '_info'):
323 from librarian import dcparser
324 from librarian import picture
325 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
329 def reset_short_html(self):
333 type(self).objects.filter(pk=self.pk).update(_related_info=None)
334 for area in self.areas.all().iterator():
335 area.reset_short_html()
338 author = self.tags.filter(category='author')[0].sort_key
341 type(self).objects.filter(pk=self.pk).update(sort_key_author=author)
343 cache_key = "Picture.short_html/%d/%s"
344 for lang, langname in settings.LANGUAGES:
345 permanent_cache.delete(cache_key % (self.id, lang))
347 def short_html(self):
349 cache_key = "Picture.short_html/%d/%s" % (self.id, get_language())
350 short_html = get_cache('permanent').get(cache_key)
354 if short_html is not None:
355 return mark_safe(short_html)
357 tags = self.tags.filter(category__in=('author', 'kind', 'epoch', 'genre'))
358 tags = split_tags(tags)
360 short_html = unicode(render_to_string(
361 'picture/picture_short.html',
362 {'picture': self, 'tags': tags}))
365 get_cache('permanent').set(cache_key, short_html)
366 return mark_safe(short_html)
368 def pretty_title(self, html_links=False):
370 # TODO Add translations (related_tag_info)
372 catalogue.models.Tag.create_url('author', tag.slug))
373 for tag in self.tags.filter(category='author')]
374 names.append((self.title, self.get_absolute_url()))
377 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
379 names = [tag[0] for tag in names]
380 return ', '.join(names)
382 def related_info(self):
383 """Keeps info about related objects (tags) in cache field."""
384 if self._related_info is not None:
385 return self._related_info
389 tags = self.tags.filter(category__in=(
390 'author', 'kind', 'genre', 'epoch'))
391 tags = split_tags(tags)
392 for category in tags:
394 for tag in tags[category]:
395 tag_info = {'slug': tag.slug, 'name': tag.name}
396 for lc, ln in settings.LANGUAGES:
397 tag_name = getattr(tag, "name_%s" % lc)
399 tag_info["name_%s" % lc] = tag_name
401 rel['tags'][category] = cat
405 type(self).objects.filter(pk=self.pk).update(_related_info=rel)
408 # copied from book.py, figure out
409 def related_themes(self):
410 # self.theme_counter hides a computation, so a line below actually makes sense
411 theme_counter = self.theme_counter
412 picture_themes = list(catalogue.models.Tag.objects.filter(pk__in=theme_counter.keys()))
413 for tag in picture_themes:
414 tag.count = theme_counter[tag.pk]
415 return picture_themes
417 def reset_tag_counter(self):
421 cache_key = "Picture.tag_counter/%d" % self.id
422 permanent_cache.delete(cache_key)
424 self.parent.reset_tag_counter()
427 def tag_counter(self):
429 cache_key = "Picture.tag_counter/%d" % self.id
430 tags = permanent_cache.get(cache_key)
436 # do we need to do this? there are no children here.
437 for tag in self.tags.exclude(category__in=('book', 'theme', 'thing', 'set')).order_by().iterator():
441 permanent_cache.set(cache_key, tags)
444 def reset_theme_counter(self):
448 cache_key = "Picture.theme_counter/%d" % self.id
449 permanent_cache.delete(cache_key)
452 def theme_counter(self):
454 cache_key = "Picture.theme_counter/%d" % self.id
455 tags = permanent_cache.get(cache_key)
461 for area in PictureArea.objects.filter(picture=self).order_by().iterator():
462 for tag in area.tags.filter(category__in=('theme', 'thing')).order_by().iterator():
463 tags[tag.pk] = tags.get(tag.pk, 0) + 1
466 permanent_cache.set(cache_key, tags)