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',)
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 author_unicode(self):
131 return ", ".join(self.authors().values_list('name', flat=True))
134 def get_absolute_url(self):
135 return 'picture.views.picture_detail', [self.slug]
137 def get_initial(self):
139 return re.search(r'\w', self.title, re.U).group(0)
140 except AttributeError:
145 return type(self).objects.filter(sort_key__gt=self.sort_key)[0]
149 def get_previous(self):
151 return type(self).objects.filter(sort_key__lt=self.sort_key).order_by('-sort_key')[0]
156 def from_xml_file(cls, xml_file, image_file=None, image_store=None, overwrite=False):
158 Import xml and it's accompanying image file.
159 If image file is missing, it will be fetched by librarian.picture.ImageStore
160 which looks for an image file in the same directory the xml is, with extension matching
163 from sortify import sortify
164 from django.core.files import File
165 from librarian.picture import WLPicture, ImageStore
166 close_xml_file = False
167 close_image_file = False
169 if image_file is not None and not isinstance(image_file, File):
170 image_file = File(open(image_file))
171 close_image_file = True
173 if not isinstance(xml_file, File):
174 xml_file = File(open(xml_file))
175 close_xml_file = True
177 with transaction.atomic():
178 # use librarian to parse meta-data
179 if image_store is None:
180 image_store = ImageStore(picture_storage.path('images'))
181 picture_xml = WLPicture.from_file(xml_file, image_store=image_store)
183 picture, created = Picture.objects.get_or_create(slug=picture_xml.slug[:120])
184 if not created and not overwrite:
185 raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug)
187 picture.areas.all().delete()
188 picture.title = unicode(picture_xml.picture_info.title)
189 picture.extra_info = picture_xml.picture_info.to_dict()
191 picture_tags = set(catalogue.models.Tag.tags_from_info(picture_xml.picture_info))
195 area_data = {'themes': {}, 'things': {}}
197 # Treat all names in picture XML as in default language.
198 lang = settings.LANGUAGE_CODE
200 for part in picture_xml.partiter():
201 if picture_xml.frame:
202 c = picture_xml.frame[0]
203 part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']]
204 if part.get('object', None) is not None:
206 for objname in part['object'].split(','):
207 objname = objname.strip().capitalize()
208 tag, created = catalogue.models.Tag.objects.get_or_create(
209 slug=slughifi(objname), category='thing')
212 setattr(tag, 'name_%s' % lang, tag.name)
213 tag.sort_key = sortify(tag.name)
215 # thing_tags.add(tag)
216 area_data['things'][tag.slug] = {
218 'coords': part['coords'],
222 area = PictureArea.rectangle(picture, 'thing', part['coords'])
227 for motifs in part['themes']:
228 for motif in motifs.split(','):
229 tag, created = catalogue.models.Tag.objects.get_or_create(
230 slug=slughifi(motif), category='theme')
233 tag.sort_key = sortify(tag.name)
235 # motif_tags.add(tag)
237 area_data['themes'][tag.slug] = {
239 'coords': part['coords']
242 logging.debug("coords for theme: %s" % part['coords'])
243 area = PictureArea.rectangle(picture, 'theme', part['coords'])
245 area.tags = _tags.union(picture_tags)
247 picture.tags = picture_tags.union(motif_tags).union(thing_tags)
248 picture.areas_json = area_data
250 if image_file is not None:
253 img = picture_xml.image_file()
255 modified = cls.crop_to_frame(picture_xml, img)
256 modified = cls.add_source_note(picture_xml, modified)
258 picture.width, picture.height = modified.size
260 modified_file = StringIO()
261 modified.save(modified_file, format='JPEG', quality=95)
262 # FIXME: hardcoded extension - detect from DC format or orginal filename
263 picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
265 picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
267 tasks.generate_picture_html(picture.id)
277 def crop_to_frame(cls, wlpic, image_file):
278 img = Image.open(image_file)
279 if wlpic.frame is None or wlpic.frame == [[0, 0], [-1, -1]]:
281 img = img.crop(itertools.chain(*wlpic.frame))
285 def add_source_note(wlpic, img):
286 from PIL import ImageDraw, ImageFont
287 from librarian import get_resource
289 annotated = Image.new(img.mode, (img.size[0], img.size[1] + 40), (255, 255, 255))
290 annotated.paste(img, (0, 0))
291 annotation = Image.new('RGB', (img.size[0] * 3, 120), (255, 255, 255))
292 ImageDraw.Draw(annotation).text(
294 wlpic.picture_info.source_name,
296 font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
298 annotated.paste(annotation.resize((img.size[0], 40), Image.ANTIALIAS), (0, img.size[1]))
303 def picture_list(cls, filter=None):
304 """Generates a hierarchical listing of all pictures
305 Pictures are optionally filtered with a test function.
308 pics = cls.objects.all().order_by('sort_key').only('title', 'slug', 'image_file')
311 pics = pics.filter(filter).distinct()
313 pics_by_author = SortedDict()
315 for tag in catalogue.models.Tag.objects.filter(category='author'):
316 pics_by_author[tag] = []
318 for pic in pics.iterator():
319 authors = list(pic.authors().only('pk'))
321 for author in authors:
322 pics_by_author[author].append(pic)
326 return pics_by_author, orphans
330 if not hasattr(self, '_info'):
331 from librarian import dcparser
332 from librarian import picture
333 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
337 def pretty_title(self, html_links=False):
338 names = [(tag.name, tag.get_absolute_url()) for tag in self.authors().only('name', 'category', 'slug')]
339 names.append((self.title, self.get_absolute_url()))
342 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
344 names = [tag[0] for tag in names]
345 return ', '.join(names)
347 def related_themes(self):
348 return catalogue.models.Tag.objects.usage_for_queryset(
349 self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))
351 def flush_includes(self, languages=True):
354 if languages is True:
355 languages = [lc for (lc, _ln) in settings.LANGUAGES]
357 template % (self.pk, lang)
359 '/katalog/p/%d/short.%s.html',
360 '/katalog/p/%d/mini.%s.html',
362 for lang in languages