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
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)
243 except Exception, ex:
244 logging.exception("Exception during import, rolling back")
245 transaction.rollback()
259 def crop_to_frame(cls, wlpic, image_file):
260 img = Image.open(image_file)
261 if wlpic.frame is None or wlpic.frame == [[0, 0], [-1, -1]]:
263 img = img.crop(itertools.chain(*wlpic.frame))
267 def add_source_note(wlpic, img):
268 from PIL import ImageDraw, ImageFont
269 from librarian import get_resource
271 annotated = Image.new(img.mode,
272 (img.size[0], img.size[1] + 40),
275 annotated.paste(img, (0, 0))
276 annotation = Image.new('RGB', (img.size[0] * 3, 120), (255, 255, 255))
277 ImageDraw.Draw(annotation).text(
279 wlpic.picture_info.source_name,
281 font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
283 annotated.paste(annotation.resize((img.size[0], 40), Image.ANTIALIAS), (0, img.size[1]))
287 def picture_list(cls, filter=None):
288 """Generates a hierarchical listing of all pictures
289 Pictures are optionally filtered with a test function.
292 pics = cls.objects.all().order_by('sort_key')\
293 .only('title', 'slug', 'image_file')
296 pics = pics.filter(filter).distinct()
298 pics_by_author = SortedDict()
300 for tag in catalogue.models.Tag.objects.filter(category='author'):
301 pics_by_author[tag] = []
303 for pic in pics.iterator():
304 authors = list(pic.tags.filter(category='author'))
306 for author in authors:
307 pics_by_author[author].append(pic)
311 return pics_by_author, orphans
315 if not hasattr(self, '_info'):
316 from librarian import dcparser
317 from librarian import picture
318 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
322 def pretty_title(self, html_links=False):
324 names = [(tag.name, tag.get_absolute_url())
325 for tag in self.tags.filter(category='author')]
326 names.append((self.title, self.get_absolute_url()))
329 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
331 names = [tag[0] for tag in names]
332 return ', '.join(names)
334 def related_themes(self):
335 return catalogue.models.Tag.objects.usage_for_queryset(
336 self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))
338 def flush_includes(self, languages=True):
341 if languages is True:
342 languages = [lc for (lc, _ln) in settings.LANGUAGES]
344 template % (self.pk, lang)
346 '/katalog/p/%d/short.%s.html',
347 '/katalog/p/%d/mini.%s.html',
349 for lang in languages