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 picture import tasks
18 from StringIO import StringIO
26 from django.utils.translation import ugettext_lazy as _
27 from newtagging import managers
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(
40 _('kind'), max_length=10, blank=False, null=False, db_index=True,
41 choices=(('thing', _('thing')), ('theme', _('theme'))))
43 objects = models.Manager()
44 tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
45 tags = managers.TagDescriptor(catalogue.models.Tag)
46 tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
48 short_html_url_name = 'picture_area_short'
51 def rectangle(cls, picture, kind, coords):
58 def flush_includes(self, languages=True):
62 languages = [lc for (lc, _ln) in settings.LANGUAGES]
64 template % (self.pk, lang)
66 '/katalog/pa/%d/short.%s.html',
72 class Picture(models.Model):
77 title = models.CharField(_('title'), max_length=32767)
78 slug = models.SlugField(_('slug'), max_length=120, db_index=True, unique=True)
79 sort_key = models.CharField(_('sort key'), max_length=120, db_index=True, editable=False)
80 sort_key_author = models.CharField(
81 _('sort key by author'), max_length=120, db_index=True, editable=False, default=u'')
82 created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
83 changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True)
84 xml_file = models.FileField(_('xml file'), upload_to="xml", storage=picture_storage)
85 image_file = ImageField(_('image file'), upload_to="images", storage=picture_storage)
86 html_file = models.FileField(_('html file'), upload_to="html", storage=picture_storage)
87 areas_json = jsonfield.JSONField(_('picture areas JSON'), default={}, editable=False)
88 extra_info = jsonfield.JSONField(_('extra information'), default={})
89 culturepl_link = models.CharField(blank=True, max_length=240)
90 wiki_link = models.CharField(blank=True, max_length=240)
92 width = models.IntegerField(null=True)
93 height = models.IntegerField(null=True)
95 objects = models.Manager()
96 tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
97 tags = managers.TagDescriptor(catalogue.models.Tag)
98 tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
100 short_html_url_name = 'picture_short'
102 class AlreadyExists(Exception):
106 ordering = ('sort_key_author', 'sort_key')
108 verbose_name = _('picture')
109 verbose_name_plural = _('pictures')
111 def save(self, force_insert=False, force_update=False, **kwargs):
112 from sortify import sortify
114 self.sort_key = sortify(self.title)[:120]
117 author = self.authors().first().sort_key
118 except AttributeError:
120 self.sort_key_author = author
122 ret = super(Picture, self).save(force_insert, force_update)
126 def __unicode__(self):
130 return self.tags.filter(category='author')
132 def tag_unicode(self, category):
133 relations = prefetched_relations(self, category)
135 return ', '.join(rel.tag.name for rel in relations)
137 return ', '.join(self.tags.filter(category=category).values_list('name', flat=True))
139 def author_unicode(self):
140 return self.tag_unicode('author')
143 def get_absolute_url(self):
144 return 'picture.views.picture_detail', [self.slug]
146 def get_initial(self):
148 return re.search(r'\w', self.title, re.U).group(0)
149 except AttributeError:
154 return type(self).objects.filter(sort_key__gt=self.sort_key)[0]
158 def get_previous(self):
160 return type(self).objects.filter(sort_key__lt=self.sort_key).order_by('-sort_key')[0]
165 def from_xml_file(cls, xml_file, image_file=None, image_store=None, overwrite=False):
167 Import xml and it's accompanying image file.
168 If image file is missing, it will be fetched by librarian.picture.ImageStore
169 which looks for an image file in the same directory the xml is, with extension matching
172 from sortify import sortify
173 from django.core.files import File
174 from librarian.picture import WLPicture, ImageStore
175 close_xml_file = False
176 close_image_file = False
178 if image_file is not None and not isinstance(image_file, File):
179 image_file = File(open(image_file))
180 close_image_file = True
182 if not isinstance(xml_file, File):
183 xml_file = File(open(xml_file))
184 close_xml_file = True
186 with transaction.atomic():
187 # use librarian to parse meta-data
188 if image_store is None:
189 image_store = ImageStore(picture_storage.path('images'))
190 picture_xml = WLPicture.from_file(xml_file, image_store=image_store)
192 picture, created = Picture.objects.get_or_create(slug=picture_xml.slug[:120])
193 if not created and not overwrite:
194 raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug)
196 picture.areas.all().delete()
197 picture.title = unicode(picture_xml.picture_info.title)
198 picture.extra_info = picture_xml.picture_info.to_dict()
200 picture_tags = set(catalogue.models.Tag.tags_from_info(picture_xml.picture_info))
201 for tag in picture_tags:
202 if not tag.for_pictures:
203 tag.for_pictures = True
206 area_data = {'themes': {}, 'things': {}}
208 # Treat all names in picture XML as in default language.
209 lang = settings.LANGUAGE_CODE
211 for part in picture_xml.partiter():
212 if picture_xml.frame:
213 c = picture_xml.frame[0]
214 part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']]
215 if part.get('object', None) is not None:
217 for objname in part['object'].split(','):
218 objname = objname.strip()
219 assert objname, 'Empty object name'
220 # str.capitalize() is wrong, because it also lowers letters
221 objname = objname[0].upper() + objname[1:]
222 tag, created = catalogue.models.Tag.objects.get_or_create(
223 slug=slughifi(objname), category='thing')
226 setattr(tag, 'name_%s' % lang, tag.name)
227 tag.sort_key = sortify(tag.name)
228 tag.for_pictures = True
230 area_data['things'][tag.slug] = {
232 'coords': part['coords'],
236 if not tag.for_pictures:
237 tag.for_pictures = True
239 area = PictureArea.rectangle(picture, 'thing', part['coords'])
241 # WTF thing area does not inherit tags from picture and theme area does, is it intentional?
245 for motifs in part['themes']:
246 for motif in motifs.split(','):
247 tag, created = catalogue.models.Tag.objects.get_or_create(
248 slug=slughifi(motif), category='theme')
251 tag.sort_key = sortify(tag.name)
252 tag.for_pictures = True
254 # motif_tags.add(tag)
256 if not tag.for_pictures:
257 tag.for_pictures = True
259 area_data['themes'][tag.slug] = {
261 'coords': part['coords']
264 logging.debug("coords for theme: %s" % part['coords'])
265 area = PictureArea.rectangle(picture, 'theme', part['coords'])
267 area.tags = _tags.union(picture_tags)
269 picture.tags = picture_tags
270 picture.areas_json = area_data
272 if image_file is not None:
275 img = picture_xml.image_file()
277 modified = cls.crop_to_frame(picture_xml, img)
278 modified = cls.add_source_note(picture_xml, modified)
280 picture.width, picture.height = modified.size
282 modified_file = StringIO()
283 modified.save(modified_file, format='JPEG', quality=95)
284 # FIXME: hardcoded extension - detect from DC format or orginal filename
285 picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
287 picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
289 tasks.generate_picture_html(picture.id)
299 def crop_to_frame(cls, wlpic, image_file):
300 img = Image.open(image_file)
301 if wlpic.frame is None or wlpic.frame == [[0, 0], [-1, -1]]:
303 img = img.crop(itertools.chain(*wlpic.frame))
307 def add_source_note(wlpic, img):
308 from PIL import ImageDraw, ImageFont
309 from librarian import get_resource
311 annotated = Image.new(img.mode, (img.size[0], img.size[1] + 40), (255, 255, 255))
312 annotated.paste(img, (0, 0))
313 annotation = Image.new('RGB', (img.size[0] * 3, 120), (255, 255, 255))
314 ImageDraw.Draw(annotation).text(
316 wlpic.picture_info.source_name,
318 font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
320 annotated.paste(annotation.resize((img.size[0], 40), Image.ANTIALIAS), (0, img.size[1]))
325 def picture_list(cls, filter=None):
326 """Generates a hierarchical listing of all pictures
327 Pictures are optionally filtered with a test function.
330 pics = cls.objects.all().order_by('sort_key').only('title', 'slug', 'image_file')
333 pics = pics.filter(filter).distinct()
335 pics_by_author = SortedDict()
337 for tag in catalogue.models.Tag.objects.filter(category='author'):
338 pics_by_author[tag] = []
340 for pic in pics.iterator():
341 authors = list(pic.authors().only('pk'))
343 for author in authors:
344 pics_by_author[author].append(pic)
348 return pics_by_author, orphans
352 if not hasattr(self, '_info'):
353 from librarian import dcparser
354 from librarian import picture
355 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
359 def pretty_title(self, html_links=False):
360 names = [(tag.name, tag.get_absolute_url()) for tag in self.authors().only('name', 'category', 'slug')]
361 names.append((self.title, self.get_absolute_url()))
364 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
366 names = [tag[0] for tag in names]
367 return ', '.join(names)
369 def related_themes(self):
370 return catalogue.models.Tag.objects.usage_for_queryset(
371 self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))
373 def flush_includes(self, languages=True):
376 if languages is True:
377 languages = [lc for (lc, _ln) in settings.LANGUAGES]
379 template % (self.pk, lang)
381 '/katalog/p/%d/short.%s.html',
382 '/katalog/p/%d/mini.%s.html',
384 for lang in languages