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
23 from django.utils.translation import ugettext_lazy as _
24 from newtagging import managers
28 picture_storage = FileSystemStorage(location=path.join(
29 settings.MEDIA_ROOT, 'pictures'),
30 base_url=settings.MEDIA_URL + "pictures/")
33 class PictureArea(models.Model):
34 picture = models.ForeignKey('picture.Picture', related_name='areas')
35 area = jsonfield.JSONField(_('area'), default={}, editable=False)
36 kind = models.CharField(_('kind'), max_length=10, blank=False,
37 null=False, db_index=True,
38 choices=(('thing', _('thing')),
39 ('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(_('sort key by author'), max_length=120, db_index=True, editable=False, default=u'')
79 created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
80 changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True)
81 xml_file = models.FileField('xml_file', upload_to="xml", storage=picture_storage)
82 image_file = ImageField(_('image_file'), upload_to="images", storage=picture_storage)
83 html_file = models.FileField('html_file', upload_to="html", storage=picture_storage)
84 areas_json = jsonfield.JSONField(_('picture areas JSON'), default={}, editable=False)
85 extra_info = jsonfield.JSONField(_('extra information'), default={})
86 culturepl_link = models.CharField(blank=True, max_length=240)
87 wiki_link = models.CharField(blank=True, max_length=240)
89 width = models.IntegerField(null=True)
90 height = models.IntegerField(null=True)
92 objects = models.Manager()
93 tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
94 tags = managers.TagDescriptor(catalogue.models.Tag)
95 tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
97 short_html_url_name = 'picture_short'
99 class AlreadyExists(Exception):
103 ordering = ('sort_key',)
105 verbose_name = _('picture')
106 verbose_name_plural = _('pictures')
108 def save(self, force_insert=False, force_update=False, **kwargs):
109 from sortify import sortify
111 self.sort_key = sortify(self.title)[:120]
114 author = self.tags.filter(category='author')[0].sort_key
117 self.sort_key_author = author
119 ret = super(Picture, self).save(force_insert, force_update)
123 def __unicode__(self):
127 def get_absolute_url(self):
128 return ('picture.views.picture_detail', [self.slug])
131 def from_xml_file(cls, xml_file, image_file=None, image_store=None, overwrite=False):
133 Import xml and it's accompanying image file.
134 If image file is missing, it will be fetched by librarian.picture.ImageStore
135 which looks for an image file in the same directory the xml is, with extension matching
138 from sortify import sortify
139 from django.core.files import File
140 from librarian.picture import WLPicture, ImageStore
141 close_xml_file = False
142 close_image_file = False
145 if image_file is not None and not isinstance(image_file, File):
146 image_file = File(open(image_file))
147 close_image_file = True
149 if not isinstance(xml_file, File):
150 xml_file = File(open(xml_file))
151 close_xml_file = True
153 with transaction.atomic():
154 # use librarian to parse meta-data
155 if image_store is None:
156 image_store = ImageStore(picture_storage.path('images'))
157 picture_xml = WLPicture.from_file(xml_file, image_store=image_store)
159 picture, created = Picture.objects.get_or_create(slug=picture_xml.slug[:120])
160 if not created and not overwrite:
161 raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug)
163 picture.areas.all().delete()
164 picture.title = unicode(picture_xml.picture_info.title)
165 picture.extra_info = picture_xml.picture_info.to_dict()
167 picture_tags = set(catalogue.models.Tag.tags_from_info(picture_xml.picture_info))
171 area_data = {'themes':{}, 'things':{}}
173 # Treat all names in picture XML as in default language.
174 lang = settings.LANGUAGE_CODE
176 for part in picture_xml.partiter():
177 if picture_xml.frame:
178 c = picture_xml.frame[0]
179 part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']]
180 if part.get('object', None) is not None:
182 for objname in part['object'].split(','):
183 objname = objname.strip().capitalize()
184 tag, created = catalogue.models.Tag.objects.get_or_create(slug=slughifi(objname), category='thing')
187 setattr(tag, 'name_%s' % lang, tag.name)
188 tag.sort_key = sortify(tag.name)
191 area_data['things'][tag.slug] = {
193 'coords': part['coords'],
197 area = PictureArea.rectangle(picture, 'thing', part['coords'])
202 for motifs in part['themes']:
203 for motif in motifs.split(','):
204 tag, created = catalogue.models.Tag.objects.get_or_create(slug=slughifi(motif), category='theme')
207 tag.sort_key = sortify(tag.name)
211 area_data['themes'][tag.slug] = {
213 'coords': part['coords']
216 logging.debug("coords for theme: %s" % part['coords'])
217 area = PictureArea.rectangle(picture, 'theme', part['coords'])
219 area.tags = _tags.union(picture_tags)
221 picture.tags = picture_tags.union(motif_tags).union(thing_tags)
222 picture.areas_json = area_data
224 if image_file is not None:
227 img = picture_xml.image_file()
229 modified = cls.crop_to_frame(picture_xml, img)
230 modified = cls.add_source_note(picture_xml, modified)
232 picture.width, picture.height = modified.size
234 modified_file = StringIO()
235 modified.save(modified_file, format='JPEG', quality=95)
236 # FIXME: hardcoded extension - detect from DC format or orginal filename
237 picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
239 picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
241 tasks.generate_picture_html(picture.id)
251 def crop_to_frame(cls, wlpic, image_file):
252 img = Image.open(image_file)
253 if wlpic.frame is None or wlpic.frame == [[0, 0], [-1, -1]]:
255 img = img.crop(itertools.chain(*wlpic.frame))
259 def add_source_note(wlpic, img):
260 from PIL import ImageDraw, ImageFont
261 from librarian import get_resource
263 annotated = Image.new(img.mode,
264 (img.size[0], img.size[1] + 40),
267 annotated.paste(img, (0, 0))
268 annotation = Image.new('RGB', (img.size[0] * 3, 120), (255, 255, 255))
269 ImageDraw.Draw(annotation).text(
271 wlpic.picture_info.source_name,
273 font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
275 annotated.paste(annotation.resize((img.size[0], 40), Image.ANTIALIAS), (0, img.size[1]))
279 def picture_list(cls, filter=None):
280 """Generates a hierarchical listing of all pictures
281 Pictures are optionally filtered with a test function.
284 pics = cls.objects.all().order_by('sort_key')\
285 .only('title', 'slug', 'image_file')
288 pics = pics.filter(filter).distinct()
290 pics_by_author = SortedDict()
292 for tag in catalogue.models.Tag.objects.filter(category='author'):
293 pics_by_author[tag] = []
295 for pic in pics.iterator():
296 authors = list(pic.tags.filter(category='author'))
298 for author in authors:
299 pics_by_author[author].append(pic)
303 return pics_by_author, orphans
307 if not hasattr(self, '_info'):
308 from librarian import dcparser
309 from librarian import picture
310 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
314 def pretty_title(self, html_links=False):
316 names = [(tag.name, tag.get_absolute_url())
317 for tag in self.tags.filter(category='author')]
318 names.append((self.title, self.get_absolute_url()))
321 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
323 names = [tag[0] for tag in names]
324 return ', '.join(names)
326 def related_themes(self):
327 return catalogue.models.Tag.objects.usage_for_queryset(
328 self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))
330 def flush_includes(self, languages=True):
333 if languages is True:
334 languages = [lc for (lc, _ln) in settings.LANGUAGES]
336 template % (self.pk, lang)
338 '/katalog/p/%d/short.%s.html',
339 '/katalog/p/%d/mini.%s.html',
341 for lang in languages