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))
204 area_data = {'themes': {}, 'things': {}}
206 # Treat all names in picture XML as in default language.
207 lang = settings.LANGUAGE_CODE
209 for part in picture_xml.partiter():
210 if picture_xml.frame:
211 c = picture_xml.frame[0]
212 part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']]
213 if part.get('object', None) is not None:
215 for objname in part['object'].split(','):
216 objname = objname.strip().capitalize()
217 tag, created = catalogue.models.Tag.objects.get_or_create(
218 slug=slughifi(objname), category='thing')
221 setattr(tag, 'name_%s' % lang, tag.name)
222 tag.sort_key = sortify(tag.name)
224 # thing_tags.add(tag)
225 area_data['things'][tag.slug] = {
227 'coords': part['coords'],
231 area = PictureArea.rectangle(picture, 'thing', part['coords'])
236 for motifs in part['themes']:
237 for motif in motifs.split(','):
238 tag, created = catalogue.models.Tag.objects.get_or_create(
239 slug=slughifi(motif), category='theme')
242 tag.sort_key = sortify(tag.name)
244 # motif_tags.add(tag)
246 area_data['themes'][tag.slug] = {
248 'coords': part['coords']
251 logging.debug("coords for theme: %s" % part['coords'])
252 area = PictureArea.rectangle(picture, 'theme', part['coords'])
254 area.tags = _tags.union(picture_tags)
256 picture.tags = picture_tags.union(motif_tags).union(thing_tags)
257 picture.areas_json = area_data
259 if image_file is not None:
262 img = picture_xml.image_file()
264 modified = cls.crop_to_frame(picture_xml, img)
265 modified = cls.add_source_note(picture_xml, modified)
267 picture.width, picture.height = modified.size
269 modified_file = StringIO()
270 modified.save(modified_file, format='JPEG', quality=95)
271 # FIXME: hardcoded extension - detect from DC format or orginal filename
272 picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
274 picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
276 tasks.generate_picture_html(picture.id)
286 def crop_to_frame(cls, wlpic, image_file):
287 img = Image.open(image_file)
288 if wlpic.frame is None or wlpic.frame == [[0, 0], [-1, -1]]:
290 img = img.crop(itertools.chain(*wlpic.frame))
294 def add_source_note(wlpic, img):
295 from PIL import ImageDraw, ImageFont
296 from librarian import get_resource
298 annotated = Image.new(img.mode, (img.size[0], img.size[1] + 40), (255, 255, 255))
299 annotated.paste(img, (0, 0))
300 annotation = Image.new('RGB', (img.size[0] * 3, 120), (255, 255, 255))
301 ImageDraw.Draw(annotation).text(
303 wlpic.picture_info.source_name,
305 font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
307 annotated.paste(annotation.resize((img.size[0], 40), Image.ANTIALIAS), (0, img.size[1]))
312 def picture_list(cls, filter=None):
313 """Generates a hierarchical listing of all pictures
314 Pictures are optionally filtered with a test function.
317 pics = cls.objects.all().order_by('sort_key').only('title', 'slug', 'image_file')
320 pics = pics.filter(filter).distinct()
322 pics_by_author = SortedDict()
324 for tag in catalogue.models.Tag.objects.filter(category='author'):
325 pics_by_author[tag] = []
327 for pic in pics.iterator():
328 authors = list(pic.authors().only('pk'))
330 for author in authors:
331 pics_by_author[author].append(pic)
335 return pics_by_author, orphans
339 if not hasattr(self, '_info'):
340 from librarian import dcparser
341 from librarian import picture
342 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
346 def pretty_title(self, html_links=False):
347 names = [(tag.name, tag.get_absolute_url()) for tag in self.authors().only('name', 'category', 'slug')]
348 names.append((self.title, self.get_absolute_url()))
351 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
353 names = [tag[0] for tag in names]
354 return ', '.join(names)
356 def related_themes(self):
357 return catalogue.models.Tag.objects.usage_for_queryset(
358 self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))
360 def flush_includes(self, languages=True):
363 if languages is True:
364 languages = [lc for (lc, _ln) in settings.LANGUAGES]
366 template % (self.pk, lang)
368 '/katalog/p/%d/short.%s.html',
369 '/katalog/p/%d/mini.%s.html',
371 for lang in languages