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 fnpdjango.utils.text.slughifi import slughifi
14 from ssify import flush_ssi_includes
16 from catalogue.models.tag import prefetched_relations
17 from catalogue.utils import split_tags
18 from picture import tasks
19 from StringIO import StringIO
27 from django.utils.translation import ugettext_lazy as _
28 from newtagging import managers
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(
41 _('kind'), max_length=10, blank=False, null=False, db_index=True,
42 choices=(('thing', _('thing')), ('theme', _('theme'))))
44 objects = models.Manager()
45 tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
46 tags = managers.TagDescriptor(catalogue.models.Tag)
47 tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
49 short_html_url_name = 'picture_area_short'
52 def rectangle(cls, picture, kind, coords):
59 def flush_includes(self, languages=True):
63 languages = [lc for (lc, _ln) in settings.LANGUAGES]
65 template % (self.pk, lang)
67 '/katalog/pa/%d/short.%s.html',
73 class Picture(models.Model):
78 title = models.CharField(_('title'), max_length=32767)
79 slug = models.SlugField(_('slug'), max_length=120, db_index=True, unique=True)
80 sort_key = models.CharField(_('sort key'), max_length=120, db_index=True, editable=False)
81 sort_key_author = models.CharField(
82 _('sort key by author'), max_length=120, db_index=True, editable=False, default=u'')
83 created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
84 changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True)
85 xml_file = models.FileField(_('xml file'), upload_to="xml", storage=picture_storage)
86 image_file = ImageField(_('image file'), upload_to="images", storage=picture_storage)
87 html_file = models.FileField(_('html file'), upload_to="html", storage=picture_storage)
88 areas_json = jsonfield.JSONField(_('picture areas JSON'), default={}, editable=False)
89 extra_info = jsonfield.JSONField(_('extra information'), default={})
90 culturepl_link = models.CharField(blank=True, max_length=240)
91 wiki_link = models.CharField(blank=True, max_length=240)
93 width = models.IntegerField(null=True)
94 height = models.IntegerField(null=True)
96 objects = models.Manager()
97 tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
98 tags = managers.TagDescriptor(catalogue.models.Tag)
99 tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
101 short_html_url_name = 'picture_short'
103 class AlreadyExists(Exception):
107 ordering = ('sort_key_author', 'sort_key')
109 verbose_name = _('picture')
110 verbose_name_plural = _('pictures')
112 def save(self, force_insert=False, force_update=False, **kwargs):
113 from sortify import sortify
115 self.sort_key = sortify(self.title)[:120]
118 author = self.authors().first().sort_key
119 except AttributeError:
121 self.sort_key_author = author
123 ret = super(Picture, self).save(force_insert, force_update)
127 def __unicode__(self):
131 return self.tags.filter(category='author')
133 def tag_unicode(self, category):
134 relations = prefetched_relations(self, category)
136 return ', '.join(rel.tag.name for rel in relations)
138 return ', '.join(self.tags.filter(category=category).values_list('name', flat=True))
140 def author_unicode(self):
141 return self.tag_unicode('author')
143 def tags_by_category(self):
144 return split_tags(self.tags)
147 def get_absolute_url(self):
148 return 'picture.views.picture_detail', [self.slug]
150 def get_initial(self):
152 return re.search(r'\w', self.title, re.U).group(0)
153 except AttributeError:
158 return type(self).objects.filter(sort_key__gt=self.sort_key)[0]
162 def get_previous(self):
164 return type(self).objects.filter(sort_key__lt=self.sort_key).order_by('-sort_key')[0]
169 def from_xml_file(cls, xml_file, image_file=None, image_store=None, overwrite=False):
171 Import xml and it's accompanying image file.
172 If image file is missing, it will be fetched by librarian.picture.ImageStore
173 which looks for an image file in the same directory the xml is, with extension matching
176 from sortify import sortify
177 from django.core.files import File
178 from librarian.picture import WLPicture, ImageStore
179 close_xml_file = False
180 close_image_file = False
182 if image_file is not None and not isinstance(image_file, File):
183 image_file = File(open(image_file))
184 close_image_file = True
186 if not isinstance(xml_file, File):
187 xml_file = File(open(xml_file))
188 close_xml_file = True
190 with transaction.atomic():
191 # use librarian to parse meta-data
192 if image_store is None:
193 image_store = ImageStore(picture_storage.path('images'))
194 picture_xml = WLPicture.from_file(xml_file, image_store=image_store)
196 picture, created = Picture.objects.get_or_create(slug=picture_xml.slug[:120])
197 if not created and not overwrite:
198 raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug)
200 picture.areas.all().delete()
201 picture.title = unicode(picture_xml.picture_info.title)
202 picture.extra_info = picture_xml.picture_info.to_dict()
204 picture_tags = set(catalogue.models.Tag.tags_from_info(picture_xml.picture_info))
205 for tag in picture_tags:
206 if not tag.for_pictures:
207 tag.for_pictures = True
210 area_data = {'themes': {}, 'things': {}}
212 # Treat all names in picture XML as in default language.
213 lang = settings.LANGUAGE_CODE
215 for part in picture_xml.partiter():
216 if picture_xml.frame:
217 c = picture_xml.frame[0]
218 part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']]
219 if part.get('object', None) is not None:
221 for objname in part['object'].split(','):
222 objname = objname.strip()
223 assert objname, 'Empty object name'
224 # str.capitalize() is wrong, because it also lowers letters
225 objname = objname[0].upper() + objname[1:]
226 tag, created = catalogue.models.Tag.objects.get_or_create(
227 slug=slughifi(objname), category='thing')
230 setattr(tag, 'name_%s' % lang, tag.name)
231 tag.sort_key = sortify(tag.name)
232 tag.for_pictures = True
234 area_data['things'][tag.slug] = {
236 'coords': part['coords'],
240 if not tag.for_pictures:
241 tag.for_pictures = True
243 area = PictureArea.rectangle(picture, 'thing', part['coords'])
245 # WTF thing area does not inherit tags from picture and theme area does, is it intentional?
249 for motifs in part['themes']:
250 for motif in motifs.split(','):
251 tag, created = catalogue.models.Tag.objects.get_or_create(
252 slug=slughifi(motif), category='theme')
255 tag.sort_key = sortify(tag.name)
256 tag.for_pictures = True
258 # motif_tags.add(tag)
260 if not tag.for_pictures:
261 tag.for_pictures = True
263 area_data['themes'][tag.slug] = {
265 'coords': part['coords']
268 logging.debug("coords for theme: %s" % part['coords'])
269 area = PictureArea.rectangle(picture, 'theme', part['coords'])
271 area.tags = _tags.union(picture_tags)
273 picture.tags = picture_tags
274 picture.areas_json = area_data
276 if image_file is not None:
279 img = picture_xml.image_file()
281 modified = cls.crop_to_frame(picture_xml, img)
282 modified = cls.add_source_note(picture_xml, modified)
284 picture.width, picture.height = modified.size
286 modified_file = StringIO()
287 modified.save(modified_file, format='JPEG', quality=95)
288 # FIXME: hardcoded extension - detect from DC format or orginal filename
289 picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
291 picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
293 tasks.generate_picture_html(picture.id)
303 def crop_to_frame(cls, wlpic, image_file):
304 img = Image.open(image_file)
305 if wlpic.frame is None or wlpic.frame == [[0, 0], [-1, -1]]:
307 img = img.crop(itertools.chain(*wlpic.frame))
311 def add_source_note(wlpic, img):
312 from PIL import ImageDraw, ImageFont
313 from librarian import get_resource
315 annotated = Image.new(img.mode, (img.size[0], img.size[1] + 40), (255, 255, 255))
316 annotated.paste(img, (0, 0))
317 annotation = Image.new('RGB', (img.size[0] * 3, 120), (255, 255, 255))
318 ImageDraw.Draw(annotation).text(
320 wlpic.picture_info.source_name,
322 font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
324 annotated.paste(annotation.resize((img.size[0], 40), Image.ANTIALIAS), (0, img.size[1]))
329 def picture_list(cls, filter=None):
330 """Generates a hierarchical listing of all pictures
331 Pictures are optionally filtered with a test function.
334 pics = cls.objects.all().order_by('sort_key').only('title', 'slug', 'image_file')
337 pics = pics.filter(filter).distinct()
339 pics_by_author = SortedDict()
341 for tag in catalogue.models.Tag.objects.filter(category='author'):
342 pics_by_author[tag] = []
344 for pic in pics.iterator():
345 authors = list(pic.authors().only('pk'))
347 for author in authors:
348 pics_by_author[author].append(pic)
352 return pics_by_author, orphans
356 if not hasattr(self, '_info'):
357 from librarian import dcparser
358 from librarian import picture
359 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
363 def pretty_title(self, html_links=False):
364 names = [(tag.name, tag.get_absolute_url()) for tag in self.authors().only('name', 'category', 'slug')]
365 names.append((self.title, self.get_absolute_url()))
368 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
370 names = [tag[0] for tag in names]
371 return ', '.join(names)
373 def related_themes(self):
374 return catalogue.models.Tag.objects.usage_for_queryset(
375 self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))
377 def flush_includes(self, languages=True):
380 if languages is True:
381 languages = [lc for (lc, _ln) in settings.LANGUAGES]
383 template % (self.pk, lang)
385 '/katalog/p/%d/short.%s.html',
386 '/katalog/p/%d/mini.%s.html',
388 for lang in languages