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.tags.filter(category='author')[0].sort_key
118 self.sort_key_author = author
120 ret = super(Picture, self).save(force_insert, force_update)
124 def __unicode__(self):
127 def author_str(self):
128 return ", ".join(str(t) for t in self.tags.filter(category='author'))
131 def get_absolute_url(self):
132 return 'picture.views.picture_detail', [self.slug]
134 def get_initial(self):
136 return re.search(r'\w', self.title, re.U).group(0)
137 except AttributeError:
142 return type(self).objects.filter(sort_key__gt=self.sort_key)[0]
146 def get_previous(self):
148 return type(self).objects.filter(sort_key__lt=self.sort_key).order_by('-sort_key')[0]
153 def from_xml_file(cls, xml_file, image_file=None, image_store=None, overwrite=False):
155 Import xml and it's accompanying image file.
156 If image file is missing, it will be fetched by librarian.picture.ImageStore
157 which looks for an image file in the same directory the xml is, with extension matching
160 from sortify import sortify
161 from django.core.files import File
162 from librarian.picture import WLPicture, ImageStore
163 close_xml_file = False
164 close_image_file = False
166 if image_file is not None and not isinstance(image_file, File):
167 image_file = File(open(image_file))
168 close_image_file = True
170 if not isinstance(xml_file, File):
171 xml_file = File(open(xml_file))
172 close_xml_file = True
174 with transaction.atomic():
175 # use librarian to parse meta-data
176 if image_store is None:
177 image_store = ImageStore(picture_storage.path('images'))
178 picture_xml = WLPicture.from_file(xml_file, image_store=image_store)
180 picture, created = Picture.objects.get_or_create(slug=picture_xml.slug[:120])
181 if not created and not overwrite:
182 raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug)
184 picture.areas.all().delete()
185 picture.title = unicode(picture_xml.picture_info.title)
186 picture.extra_info = picture_xml.picture_info.to_dict()
188 picture_tags = set(catalogue.models.Tag.tags_from_info(picture_xml.picture_info))
192 area_data = {'themes': {}, 'things': {}}
194 # Treat all names in picture XML as in default language.
195 lang = settings.LANGUAGE_CODE
197 for part in picture_xml.partiter():
198 if picture_xml.frame:
199 c = picture_xml.frame[0]
200 part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']]
201 if part.get('object', None) is not None:
203 for objname in part['object'].split(','):
204 objname = objname.strip().capitalize()
205 tag, created = catalogue.models.Tag.objects.get_or_create(
206 slug=slughifi(objname), category='thing')
209 setattr(tag, 'name_%s' % lang, tag.name)
210 tag.sort_key = sortify(tag.name)
212 # thing_tags.add(tag)
213 area_data['things'][tag.slug] = {
215 'coords': part['coords'],
219 area = PictureArea.rectangle(picture, 'thing', part['coords'])
224 for motifs in part['themes']:
225 for motif in motifs.split(','):
226 tag, created = catalogue.models.Tag.objects.get_or_create(
227 slug=slughifi(motif), category='theme')
230 tag.sort_key = sortify(tag.name)
232 # motif_tags.add(tag)
234 area_data['themes'][tag.slug] = {
236 'coords': part['coords']
239 logging.debug("coords for theme: %s" % part['coords'])
240 area = PictureArea.rectangle(picture, 'theme', part['coords'])
242 area.tags = _tags.union(picture_tags)
244 picture.tags = picture_tags.union(motif_tags).union(thing_tags)
245 picture.areas_json = area_data
247 if image_file is not None:
250 img = picture_xml.image_file()
252 modified = cls.crop_to_frame(picture_xml, img)
253 modified = cls.add_source_note(picture_xml, modified)
255 picture.width, picture.height = modified.size
257 modified_file = StringIO()
258 modified.save(modified_file, format='JPEG', quality=95)
259 # FIXME: hardcoded extension - detect from DC format or orginal filename
260 picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
262 picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
264 tasks.generate_picture_html(picture.id)
274 def crop_to_frame(cls, wlpic, image_file):
275 img = Image.open(image_file)
276 if wlpic.frame is None or wlpic.frame == [[0, 0], [-1, -1]]:
278 img = img.crop(itertools.chain(*wlpic.frame))
282 def add_source_note(wlpic, img):
283 from PIL import ImageDraw, ImageFont
284 from librarian import get_resource
286 annotated = Image.new(img.mode, (img.size[0], img.size[1] + 40), (255, 255, 255))
287 annotated.paste(img, (0, 0))
288 annotation = Image.new('RGB', (img.size[0] * 3, 120), (255, 255, 255))
289 ImageDraw.Draw(annotation).text(
291 wlpic.picture_info.source_name,
293 font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
295 annotated.paste(annotation.resize((img.size[0], 40), Image.ANTIALIAS), (0, img.size[1]))
300 def picture_list(cls, filter=None):
301 """Generates a hierarchical listing of all pictures
302 Pictures are optionally filtered with a test function.
305 pics = cls.objects.all().order_by('sort_key').only('title', 'slug', 'image_file')
308 pics = pics.filter(filter).distinct()
310 pics_by_author = SortedDict()
312 for tag in catalogue.models.Tag.objects.filter(category='author'):
313 pics_by_author[tag] = []
315 for pic in pics.iterator():
316 authors = list(pic.tags.filter(category='author'))
318 for author in authors:
319 pics_by_author[author].append(pic)
323 return pics_by_author, orphans
327 if not hasattr(self, '_info'):
328 from librarian import dcparser
329 from librarian import picture
330 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
334 def pretty_title(self, html_links=False):
336 names = [(tag.name, tag.get_absolute_url())
337 for tag in self.tags.filter(category='author')]
338 names.append((self.title, self.get_absolute_url()))
341 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
343 names = [tag[0] for tag in names]
344 return ', '.join(names)
346 def related_themes(self):
347 return catalogue.models.Tag.objects.usage_for_queryset(
348 self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))
350 def flush_includes(self, languages=True):
353 if languages is True:
354 languages = [lc for (lc, _ln) in settings.LANGUAGES]
356 template % (self.pk, lang)
358 '/katalog/p/%d/short.%s.html',
359 '/katalog/p/%d/mini.%s.html',
361 for lang in languages