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
15 from picture import tasks
16 from StringIO import StringIO
24 from django.utils.translation import ugettext_lazy as _
25 from newtagging import managers
29 picture_storage = FileSystemStorage(location=path.join(
30 settings.MEDIA_ROOT, 'pictures'),
31 base_url=settings.MEDIA_URL + "pictures/")
34 class PictureArea(models.Model):
35 picture = models.ForeignKey('picture.Picture', related_name='areas')
36 area = jsonfield.JSONField(_('area'), default={}, editable=False)
37 kind = models.CharField(
38 _('kind'), max_length=10, blank=False, null=False, db_index=True,
39 choices=(('thing', _('thing')), ('theme', _('theme'))))
41 objects = models.Manager()
42 tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
43 tags = managers.TagDescriptor(catalogue.models.Tag)
44 tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
46 short_html_url_name = 'picture_area_short'
49 def rectangle(cls, picture, kind, coords):
56 def flush_includes(self, languages=True):
60 languages = [lc for (lc, _ln) in settings.LANGUAGES]
62 template % (self.pk, lang)
64 '/katalog/pa/%d/short.%s.html',
70 class Picture(models.Model):
75 title = models.CharField(_('title'), max_length=32767)
76 slug = models.SlugField(_('slug'), max_length=120, db_index=True, unique=True)
77 sort_key = models.CharField(_('sort key'), max_length=120, db_index=True, editable=False)
78 sort_key_author = models.CharField(
79 _('sort key by author'), max_length=120, db_index=True, editable=False, default=u'')
80 created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
81 changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True)
82 xml_file = models.FileField(_('xml file'), upload_to="xml", storage=picture_storage)
83 image_file = ImageField(_('image file'), upload_to="images", storage=picture_storage)
84 html_file = models.FileField(_('html file'), upload_to="html", storage=picture_storage)
85 areas_json = jsonfield.JSONField(_('picture areas JSON'), default={}, editable=False)
86 extra_info = jsonfield.JSONField(_('extra information'), default={})
87 culturepl_link = models.CharField(blank=True, max_length=240)
88 wiki_link = models.CharField(blank=True, max_length=240)
90 width = models.IntegerField(null=True)
91 height = models.IntegerField(null=True)
93 objects = models.Manager()
94 tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
95 tags = managers.TagDescriptor(catalogue.models.Tag)
96 tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
98 short_html_url_name = 'picture_short'
100 class AlreadyExists(Exception):
104 ordering = ('sort_key_author', 'sort_key')
106 verbose_name = _('picture')
107 verbose_name_plural = _('pictures')
109 def save(self, force_insert=False, force_update=False, **kwargs):
110 from sortify import sortify
112 self.sort_key = sortify(self.title)[:120]
115 author = self.authors().first().sort_key
116 except AttributeError:
118 self.sort_key_author = author
120 ret = super(Picture, self).save(force_insert, force_update)
124 def __unicode__(self):
128 return self.tags.filter(category='author')
130 def tag_unicode(self, category):
131 return ", ".join(self.tags.filter(category=category).values_list('name', flat=True))
133 def author_unicode(self):
134 return self.tag_unicode('author')
137 def get_absolute_url(self):
138 return 'picture.views.picture_detail', [self.slug]
140 def get_initial(self):
142 return re.search(r'\w', self.title, re.U).group(0)
143 except AttributeError:
148 return type(self).objects.filter(sort_key__gt=self.sort_key)[0]
152 def get_previous(self):
154 return type(self).objects.filter(sort_key__lt=self.sort_key).order_by('-sort_key')[0]
159 def from_xml_file(cls, xml_file, image_file=None, image_store=None, overwrite=False):
161 Import xml and it's accompanying image file.
162 If image file is missing, it will be fetched by librarian.picture.ImageStore
163 which looks for an image file in the same directory the xml is, with extension matching
166 from sortify import sortify
167 from django.core.files import File
168 from librarian.picture import WLPicture, ImageStore
169 close_xml_file = False
170 close_image_file = False
172 if image_file is not None and not isinstance(image_file, File):
173 image_file = File(open(image_file))
174 close_image_file = True
176 if not isinstance(xml_file, File):
177 xml_file = File(open(xml_file))
178 close_xml_file = True
180 with transaction.atomic():
181 # use librarian to parse meta-data
182 if image_store is None:
183 image_store = ImageStore(picture_storage.path('images'))
184 picture_xml = WLPicture.from_file(xml_file, image_store=image_store)
186 picture, created = Picture.objects.get_or_create(slug=picture_xml.slug[:120])
187 if not created and not overwrite:
188 raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug)
190 picture.areas.all().delete()
191 picture.title = unicode(picture_xml.picture_info.title)
192 picture.extra_info = picture_xml.picture_info.to_dict()
194 picture_tags = set(catalogue.models.Tag.tags_from_info(picture_xml.picture_info))
198 area_data = {'themes': {}, 'things': {}}
200 # Treat all names in picture XML as in default language.
201 lang = settings.LANGUAGE_CODE
203 for part in picture_xml.partiter():
204 if picture_xml.frame:
205 c = picture_xml.frame[0]
206 part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']]
207 if part.get('object', None) is not None:
209 for objname in part['object'].split(','):
210 objname = objname.strip().capitalize()
211 tag, created = catalogue.models.Tag.objects.get_or_create(
212 slug=slughifi(objname), category='thing')
215 setattr(tag, 'name_%s' % lang, tag.name)
216 tag.sort_key = sortify(tag.name)
218 # thing_tags.add(tag)
219 area_data['things'][tag.slug] = {
221 'coords': part['coords'],
225 area = PictureArea.rectangle(picture, 'thing', part['coords'])
230 for motifs in part['themes']:
231 for motif in motifs.split(','):
232 tag, created = catalogue.models.Tag.objects.get_or_create(
233 slug=slughifi(motif), category='theme')
236 tag.sort_key = sortify(tag.name)
238 # motif_tags.add(tag)
240 area_data['themes'][tag.slug] = {
242 'coords': part['coords']
245 logging.debug("coords for theme: %s" % part['coords'])
246 area = PictureArea.rectangle(picture, 'theme', part['coords'])
248 area.tags = _tags.union(picture_tags)
250 picture.tags = picture_tags.union(motif_tags).union(thing_tags)
251 picture.areas_json = area_data
253 if image_file is not None:
256 img = picture_xml.image_file()
258 modified = cls.crop_to_frame(picture_xml, img)
259 modified = cls.add_source_note(picture_xml, modified)
261 picture.width, picture.height = modified.size
263 modified_file = StringIO()
264 modified.save(modified_file, format='JPEG', quality=95)
265 # FIXME: hardcoded extension - detect from DC format or orginal filename
266 picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
268 picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
270 tasks.generate_picture_html(picture.id)
280 def crop_to_frame(cls, wlpic, image_file):
281 img = Image.open(image_file)
282 if wlpic.frame is None or wlpic.frame == [[0, 0], [-1, -1]]:
284 img = img.crop(itertools.chain(*wlpic.frame))
288 def add_source_note(wlpic, img):
289 from PIL import ImageDraw, ImageFont
290 from librarian import get_resource
292 annotated = Image.new(img.mode, (img.size[0], img.size[1] + 40), (255, 255, 255))
293 annotated.paste(img, (0, 0))
294 annotation = Image.new('RGB', (img.size[0] * 3, 120), (255, 255, 255))
295 ImageDraw.Draw(annotation).text(
297 wlpic.picture_info.source_name,
299 font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
301 annotated.paste(annotation.resize((img.size[0], 40), Image.ANTIALIAS), (0, img.size[1]))
306 def picture_list(cls, filter=None):
307 """Generates a hierarchical listing of all pictures
308 Pictures are optionally filtered with a test function.
311 pics = cls.objects.all().order_by('sort_key').only('title', 'slug', 'image_file')
314 pics = pics.filter(filter).distinct()
316 pics_by_author = SortedDict()
318 for tag in catalogue.models.Tag.objects.filter(category='author'):
319 pics_by_author[tag] = []
321 for pic in pics.iterator():
322 authors = list(pic.authors().only('pk'))
324 for author in authors:
325 pics_by_author[author].append(pic)
329 return pics_by_author, orphans
333 if not hasattr(self, '_info'):
334 from librarian import dcparser
335 from librarian import picture
336 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
340 def pretty_title(self, html_links=False):
341 names = [(tag.name, tag.get_absolute_url()) for tag in self.authors().only('name', 'category', 'slug')]
342 names.append((self.title, self.get_absolute_url()))
345 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
347 names = [tag[0] for tag in names]
348 return ', '.join(names)
350 def related_themes(self):
351 return catalogue.models.Tag.objects.usage_for_queryset(
352 self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))
354 def flush_includes(self, languages=True):
357 if languages is True:
358 languages = [lc for (lc, _ln) in settings.LANGUAGES]
360 template % (self.pk, lang)
362 '/katalog/p/%d/short.%s.html',
363 '/katalog/p/%d/mini.%s.html',
365 for lang in languages