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 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)
115 tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
117 class AlreadyExists(Exception):
121 ordering = ('sort_key',)
123 verbose_name = _('picture')
124 verbose_name_plural = _('pictures')
126 def save(self, force_insert=False, force_update=False, reset_short_html=True, **kwargs):
127 from sortify import sortify
129 self.sort_key = sortify(self.title)
131 ret = super(Picture, self).save(force_insert, force_update)
134 self.reset_short_html()
138 def __unicode__(self):
142 def get_absolute_url(self):
143 return ('picture.views.picture_detail', [self.slug])
146 def from_xml_file(cls, xml_file, image_file=None, image_store=None, overwrite=False):
148 Import xml and it's accompanying image file.
149 If image file is missing, it will be fetched by librarian.picture.ImageStore
150 which looks for an image file in the same directory the xml is, with extension matching
153 from sortify import sortify
154 from django.core.files import File
155 from librarian.picture import WLPicture, ImageStore
156 close_xml_file = False
157 close_image_file = False
160 if image_file is not None and not isinstance(image_file, File):
161 image_file = File(open(image_file))
162 close_image_file = True
164 if not isinstance(xml_file, File):
165 xml_file = File(open(xml_file))
166 close_xml_file = True
169 # use librarian to parse meta-data
170 if image_store is None:
171 image_store = ImageStore(picture_storage.path('images'))
172 picture_xml = WLPicture.from_file(xml_file, image_store=image_store)
174 picture, created = Picture.objects.get_or_create(slug=picture_xml.slug)
175 if not created and not overwrite:
176 raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug)
178 picture.areas.all().delete()
179 picture.title = unicode(picture_xml.picture_info.title)
180 picture.extra_info = picture_xml.picture_info.to_dict()
182 picture_tags = set(catalogue.models.Tag.tags_from_info(picture_xml.picture_info))
186 area_data = {'themes':{}, 'things':{}}
188 for part in picture_xml.partiter():
189 if picture_xml.frame:
190 c = picture_xml.frame[0]
191 part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']]
192 if part.get('object', None) is not None:
193 objname = part['object']
194 tag, created = catalogue.models.Tag.objects.get_or_create(slug=slughifi(objname), category='thing')
197 tag.sort_key = sortify(tag.name)
200 area_data['things'][tag.slug] = {
201 'object': part['object'],
202 'coords': part['coords'],
204 area = PictureArea.rectangle(picture, 'thing', part['coords'])
211 for motif in part['themes']:
212 tag, created = catalogue.models.Tag.objects.get_or_create(slug=slughifi(motif), category='theme')
215 tag.sort_key = sortify(tag.name)
219 area_data['themes'][tag.slug] = {
221 'coords': part['coords']
224 logging.debug("coords for theme: %s" % part['coords'])
225 area = PictureArea.rectangle(picture, 'theme', part['coords'])
227 area.tags = _tags.union(picture_tags)
229 picture.tags = picture_tags.union(motif_tags).union(thing_tags)
230 picture.areas_json = area_data
232 if image_file is not None:
235 img = picture_xml.image_file()
237 modified = cls.crop_to_frame(picture_xml, img)
238 modified = cls.add_source_note(picture_xml, modified)
240 picture.width, picture.height = modified.size
242 modified_file = StringIO()
243 modified.save(modified_file, format='png', quality=95)
244 # FIXME: hardcoded extension - detect from DC format or orginal filename
245 picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
247 picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
249 tasks.generate_picture_html(picture.id)
251 except Exception, ex:
252 logging.exception("Exception during import, rolling back")
253 transaction.rollback()
267 def crop_to_frame(cls, wlpic, image_file):
268 img = Image.open(image_file)
269 if wlpic.frame is None:
271 img = img.crop(itertools.chain(*wlpic.frame))
275 def add_source_note(wlpic, img):
276 from PIL import ImageDraw, ImageFont
277 from librarian import get_resource
279 annotated = Image.new(img.mode,
280 (img.size[0], img.size[1] + 40),
283 annotated.paste(img, (0, 0))
284 annotation = Image.new(img.mode, (3000, 120), (255, 255, 255))
285 ImageDraw.Draw(annotation).text(
287 wlpic.picture_info.source_name,
289 font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
291 annotated.paste(annotation.resize((1000, 40), Image.ANTIALIAS), (0, img.size[1]))
295 def picture_list(cls, filter=None):
296 """Generates a hierarchical listing of all pictures
297 Pictures are optionally filtered with a test function.
300 pics = cls.objects.all().order_by('sort_key')\
301 .only('title', 'slug', 'image_file')
304 pics = pics.filter(filter).distinct()
306 pics_by_author = SortedDict()
308 for tag in catalogue.models.Tag.objects.filter(category='author'):
309 pics_by_author[tag] = []
311 for pic in pics.iterator():
312 authors = list(pic.tags.filter(category='author'))
314 for author in authors:
315 pics_by_author[author].append(pic)
319 return pics_by_author, orphans
323 if not hasattr(self, '_info'):
324 from librarian import dcparser
325 from librarian import picture
326 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
330 def reset_short_html(self):
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 = permanent_cache.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 permanent_cache.set(cache_key, short_html)
366 return mark_safe(short_html)
368 def pretty_title(self, html_links=False):
371 catalogue.models.Tag.create_url('author', tag.slug))
372 for tag in self.tags.filter(category='author')]
373 names.append((self.title, self.get_absolute_url()))
376 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
378 names = [tag[0] for tag in names]
379 return ', '.join(names)
381 # copied from book.py, figure out
382 def related_themes(self):
383 return catalogue.models.Tag.usage_for_queryset(
384 self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))