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))
202 area_data = {'themes': {}, 'things': {}}
204 # Treat all names in picture XML as in default language.
205 lang = settings.LANGUAGE_CODE
207 for part in picture_xml.partiter():
208 if picture_xml.frame:
209 c = picture_xml.frame[0]
210 part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']]
211 if part.get('object', None) is not None:
213 for objname in part['object'].split(','):
214 objname = objname.strip()
215 assert objname, 'Empty object name'
216 # str.capitalize() is wrong, because it also lowers letters
217 objname = objname[0].upper() + objname[1:]
218 tag, created = catalogue.models.Tag.objects.get_or_create(
219 slug=slughifi(objname), category='thing')
222 setattr(tag, 'name_%s' % lang, tag.name)
223 tag.sort_key = sortify(tag.name)
225 # thing_tags.add(tag)
226 area_data['things'][tag.slug] = {
228 'coords': part['coords'],
232 area = PictureArea.rectangle(picture, 'thing', part['coords'])
237 for motifs in part['themes']:
238 for motif in motifs.split(','):
239 tag, created = catalogue.models.Tag.objects.get_or_create(
240 slug=slughifi(motif), category='theme')
243 tag.sort_key = sortify(tag.name)
245 # motif_tags.add(tag)
247 area_data['themes'][tag.slug] = {
249 'coords': part['coords']
252 logging.debug("coords for theme: %s" % part['coords'])
253 area = PictureArea.rectangle(picture, 'theme', part['coords'])
255 area.tags = _tags.union(picture_tags)
257 picture.tags = picture_tags
258 picture.areas_json = area_data
260 if image_file is not None:
263 img = picture_xml.image_file()
265 modified = cls.crop_to_frame(picture_xml, img)
266 modified = cls.add_source_note(picture_xml, modified)
268 picture.width, picture.height = modified.size
270 modified_file = StringIO()
271 modified.save(modified_file, format='JPEG', quality=95)
272 # FIXME: hardcoded extension - detect from DC format or orginal filename
273 picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
275 picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
277 tasks.generate_picture_html(picture.id)
287 def crop_to_frame(cls, wlpic, image_file):
288 img = Image.open(image_file)
289 if wlpic.frame is None or wlpic.frame == [[0, 0], [-1, -1]]:
291 img = img.crop(itertools.chain(*wlpic.frame))
295 def add_source_note(wlpic, img):
296 from PIL import ImageDraw, ImageFont
297 from librarian import get_resource
299 annotated = Image.new(img.mode, (img.size[0], img.size[1] + 40), (255, 255, 255))
300 annotated.paste(img, (0, 0))
301 annotation = Image.new('RGB', (img.size[0] * 3, 120), (255, 255, 255))
302 ImageDraw.Draw(annotation).text(
304 wlpic.picture_info.source_name,
306 font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
308 annotated.paste(annotation.resize((img.size[0], 40), Image.ANTIALIAS), (0, img.size[1]))
313 def picture_list(cls, filter=None):
314 """Generates a hierarchical listing of all pictures
315 Pictures are optionally filtered with a test function.
318 pics = cls.objects.all().order_by('sort_key').only('title', 'slug', 'image_file')
321 pics = pics.filter(filter).distinct()
323 pics_by_author = SortedDict()
325 for tag in catalogue.models.Tag.objects.filter(category='author'):
326 pics_by_author[tag] = []
328 for pic in pics.iterator():
329 authors = list(pic.authors().only('pk'))
331 for author in authors:
332 pics_by_author[author].append(pic)
336 return pics_by_author, orphans
340 if not hasattr(self, '_info'):
341 from librarian import dcparser
342 from librarian import picture
343 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
347 def pretty_title(self, html_links=False):
348 names = [(tag.name, tag.get_absolute_url()) for tag in self.authors().only('name', 'category', 'slug')]
349 names.append((self.title, self.get_absolute_url()))
352 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
354 names = [tag[0] for tag in names]
355 return ', '.join(names)
357 def related_themes(self):
358 return catalogue.models.Tag.objects.usage_for_queryset(
359 self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))
361 def flush_includes(self, languages=True):
364 if languages is True:
365 languages = [lc for (lc, _ln) in settings.LANGUAGES]
367 template % (self.pk, lang)
369 '/katalog/p/%d/short.%s.html',
370 '/katalog/p/%d/mini.%s.html',
372 for lang in languages