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(_('kind'), max_length=10, blank=False,
38 null=False, db_index=True,
39 choices=(('thing', _('thing')),
40 ('theme', _('theme'))))
42 objects = models.Manager()
43 tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
44 tags = managers.TagDescriptor(catalogue.models.Tag)
45 tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
47 short_html_url_name = 'picture_area_short'
50 def rectangle(cls, picture, kind, coords):
57 def flush_includes(self, languages=True):
61 languages = [lc for (lc, _ln) in settings.LANGUAGES]
63 template % (self.pk, lang)
65 '/katalog/pa/%d/short.%s.html',
71 class Picture(models.Model):
76 title = models.CharField(_('title'), max_length=32767)
77 slug = models.SlugField(_('slug'), max_length=120, db_index=True, unique=True)
78 sort_key = models.CharField(_('sort key'), max_length=120, db_index=True, editable=False)
79 sort_key_author = models.CharField(_('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
167 if image_file is not None and not isinstance(image_file, File):
168 image_file = File(open(image_file))
169 close_image_file = True
171 if not isinstance(xml_file, File):
172 xml_file = File(open(xml_file))
173 close_xml_file = True
175 with transaction.atomic():
176 # use librarian to parse meta-data
177 if image_store is None:
178 image_store = ImageStore(picture_storage.path('images'))
179 picture_xml = WLPicture.from_file(xml_file, image_store=image_store)
181 picture, created = Picture.objects.get_or_create(slug=picture_xml.slug[:120])
182 if not created and not overwrite:
183 raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug)
185 picture.areas.all().delete()
186 picture.title = unicode(picture_xml.picture_info.title)
187 picture.extra_info = picture_xml.picture_info.to_dict()
189 picture_tags = set(catalogue.models.Tag.tags_from_info(picture_xml.picture_info))
193 area_data = {'themes':{}, 'things':{}}
195 # Treat all names in picture XML as in default language.
196 lang = settings.LANGUAGE_CODE
198 for part in picture_xml.partiter():
199 if picture_xml.frame:
200 c = picture_xml.frame[0]
201 part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']]
202 if part.get('object', None) is not None:
204 for objname in part['object'].split(','):
205 objname = objname.strip().capitalize()
206 tag, created = catalogue.models.Tag.objects.get_or_create(slug=slughifi(objname), category='thing')
209 setattr(tag, 'name_%s' % lang, tag.name)
210 tag.sort_key = sortify(tag.name)
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(slug=slughifi(motif), category='theme')
229 tag.sort_key = sortify(tag.name)
233 area_data['themes'][tag.slug] = {
235 'coords': part['coords']
238 logging.debug("coords for theme: %s" % part['coords'])
239 area = PictureArea.rectangle(picture, 'theme', part['coords'])
241 area.tags = _tags.union(picture_tags)
243 picture.tags = picture_tags.union(motif_tags).union(thing_tags)
244 picture.areas_json = area_data
246 if image_file is not None:
249 img = picture_xml.image_file()
251 modified = cls.crop_to_frame(picture_xml, img)
252 modified = cls.add_source_note(picture_xml, modified)
254 picture.width, picture.height = modified.size
256 modified_file = StringIO()
257 modified.save(modified_file, format='JPEG', quality=95)
258 # FIXME: hardcoded extension - detect from DC format or orginal filename
259 picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
261 picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
263 tasks.generate_picture_html(picture.id)
273 def crop_to_frame(cls, wlpic, image_file):
274 img = Image.open(image_file)
275 if wlpic.frame is None or wlpic.frame == [[0, 0], [-1, -1]]:
277 img = img.crop(itertools.chain(*wlpic.frame))
281 def add_source_note(wlpic, img):
282 from PIL import ImageDraw, ImageFont
283 from librarian import get_resource
285 annotated = Image.new(img.mode,
286 (img.size[0], img.size[1] + 40),
289 annotated.paste(img, (0, 0))
290 annotation = Image.new('RGB', (img.size[0] * 3, 120), (255, 255, 255))
291 ImageDraw.Draw(annotation).text(
293 wlpic.picture_info.source_name,
295 font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
297 annotated.paste(annotation.resize((img.size[0], 40), Image.ANTIALIAS), (0, img.size[1]))
301 def picture_list(cls, filter=None):
302 """Generates a hierarchical listing of all pictures
303 Pictures are optionally filtered with a test function.
306 pics = cls.objects.all().order_by('sort_key')\
307 .only('title', 'slug', 'image_file')
310 pics = pics.filter(filter).distinct()
312 pics_by_author = SortedDict()
314 for tag in catalogue.models.Tag.objects.filter(category='author'):
315 pics_by_author[tag] = []
317 for pic in pics.iterator():
318 authors = list(pic.tags.filter(category='author'))
320 for author in authors:
321 pics_by_author[author].append(pic)
325 return pics_by_author, orphans
329 if not hasattr(self, '_info'):
330 from librarian import dcparser
331 from librarian import picture
332 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
336 def pretty_title(self, html_links=False):
338 names = [(tag.name, tag.get_absolute_url())
339 for tag in self.tags.filter(category='author')]
340 names.append((self.title, self.get_absolute_url()))
343 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
345 names = [tag[0] for tag in names]
346 return ', '.join(names)
348 def related_themes(self):
349 return catalogue.models.Tag.objects.usage_for_queryset(
350 self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))
352 def flush_includes(self, languages=True):
355 if languages is True:
356 languages = [lc for (lc, _ln) in settings.LANGUAGES]
358 template % (self.pk, lang)
360 '/katalog/p/%d/short.%s.html',
361 '/katalog/p/%d/mini.%s.html',
363 for lang in languages