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 modified = cls.add_source_note(picture_xml, modified)
238 picture.width, picture.height = modified.size
240 modified_file = StringIO()
241 modified.save(modified_file, format='png', quality=95)
242 # FIXME: hardcoded extension - detect from DC format or orginal filename
243 picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
245 picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
247 tasks.generate_picture_html(picture.id)
249 except Exception, ex:
250 logging.exception("Exception during import, rolling back")
251 transaction.rollback()
265 def crop_to_frame(cls, wlpic, image_file):
266 img = Image.open(image_file)
267 if wlpic.frame is None:
269 img = img.crop(itertools.chain(*wlpic.frame))
273 def add_source_note(wlpic, img):
274 from PIL import ImageDraw, ImageFont
275 from librarian import get_resource
277 annotated = Image.new(img.mode,
278 (img.size[0], img.size[1] + 40),
281 annotated.paste(img, (0, 0))
282 annotation = Image.new(img.mode, (3000, 120), (255, 255, 255))
283 ImageDraw.Draw(annotation).text(
285 wlpic.picture_info.source_name,
287 font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
289 annotated.paste(annotation.resize((1000, 40), Image.ANTIALIAS), (0, img.size[1]))
293 def picture_list(cls, filter=None):
294 """Generates a hierarchical listing of all pictures
295 Pictures are optionally filtered with a test function.
298 pics = cls.objects.all().order_by('sort_key')\
299 .only('title', 'slug', 'image_file')
302 pics = pics.filter(filter).distinct()
304 pics_by_author = SortedDict()
306 for tag in catalogue.models.Tag.objects.filter(category='author'):
307 pics_by_author[tag] = []
309 for pic in pics.iterator():
310 authors = list(pic.tags.filter(category='author'))
312 for author in authors:
313 pics_by_author[author].append(pic)
317 return pics_by_author, orphans
321 if not hasattr(self, '_info'):
322 from librarian import dcparser
323 from librarian import picture
324 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
328 def reset_short_html(self):
332 type(self).objects.filter(pk=self.pk).update(_related_info=None)
333 for area in self.areas.all().iterator():
334 area.reset_short_html()
337 author = self.tags.filter(category='author')[0].sort_key
340 type(self).objects.filter(pk=self.pk).update(sort_key_author=author)
342 cache_key = "Picture.short_html/%d/%s"
343 for lang, langname in settings.LANGUAGES:
344 permanent_cache.delete(cache_key % (self.id, lang))
346 def short_html(self):
348 cache_key = "Picture.short_html/%d/%s" % (self.id, get_language())
349 short_html = get_cache('permanent').get(cache_key)
353 if short_html is not None:
354 return mark_safe(short_html)
356 tags = self.tags.filter(category__in=('author', 'kind', 'epoch', 'genre'))
357 tags = split_tags(tags)
359 short_html = unicode(render_to_string(
360 'picture/picture_short.html',
361 {'picture': self, 'tags': tags}))
364 get_cache('permanent').set(cache_key, short_html)
365 return mark_safe(short_html)
367 def pretty_title(self, html_links=False):
369 # TODO Add translations (related_tag_info)
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 def related_info(self):
382 """Keeps info about related objects (tags) in cache field."""
383 if self._related_info is not None:
384 return self._related_info
388 tags = self.tags.filter(category__in=(
389 'author', 'kind', 'genre', 'epoch'))
390 tags = split_tags(tags)
391 for category in tags:
393 for tag in tags[category]:
394 tag_info = {'slug': tag.slug, 'name': tag.name}
395 for lc, ln in settings.LANGUAGES:
396 tag_name = getattr(tag, "name_%s" % lc)
398 tag_info["name_%s" % lc] = tag_name
400 rel['tags'][category] = cat
404 type(self).objects.filter(pk=self.pk).update(_related_info=rel)
407 # copied from book.py, figure out
408 def related_themes(self):
409 # self.theme_counter hides a computation, so a line below actually makes sense
410 theme_counter = self.theme_counter
411 picture_themes = list(catalogue.models.Tag.objects.filter(pk__in=theme_counter.keys()))
412 for tag in picture_themes:
413 tag.count = theme_counter[tag.pk]
414 return picture_themes
416 def reset_tag_counter(self):
420 cache_key = "Picture.tag_counter/%d" % self.id
421 permanent_cache.delete(cache_key)
423 self.parent.reset_tag_counter()
426 def tag_counter(self):
428 cache_key = "Picture.tag_counter/%d" % self.id
429 tags = permanent_cache.get(cache_key)
435 # do we need to do this? there are no children here.
436 for tag in self.tags.exclude(category__in=('book', 'theme', 'thing', 'set')).order_by().iterator():
440 permanent_cache.set(cache_key, tags)
443 def reset_theme_counter(self):
447 cache_key = "Picture.theme_counter/%d" % self.id
448 permanent_cache.delete(cache_key)
451 def theme_counter(self):
453 cache_key = "Picture.theme_counter/%d" % self.id
454 tags = permanent_cache.get(cache_key)
460 for area in PictureArea.objects.filter(picture=self).order_by().iterator():
461 for tag in area.tags.filter(category__in=('theme','thing')).order_by().iterator():
462 tags[tag.pk] = tags.get(tag.pk, 0) + 1
465 permanent_cache.set(cache_key, tags)