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 slugify import slugify
13 from ssify import flush_ssi_includes
15 from catalogue.models.tag import prefetched_relations
16 from catalogue.utils import split_tags
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')
142 def tags_by_category(self):
143 return split_tags(self.tags)
146 def get_absolute_url(self):
147 return 'picture.views.picture_detail', [self.slug]
149 def get_initial(self):
151 return re.search(r'\w', self.title, re.U).group(0)
152 except AttributeError:
157 return type(self).objects.filter(sort_key__gt=self.sort_key)[0]
161 def get_previous(self):
163 return type(self).objects.filter(sort_key__lt=self.sort_key).order_by('-sort_key')[0]
168 def from_xml_file(cls, xml_file, image_file=None, image_store=None, overwrite=False, search_index=True):
170 Import xml and it's accompanying image file.
171 If image file is missing, it will be fetched by librarian.picture.ImageStore
172 which looks for an image file in the same directory the xml is, with extension matching
175 from sortify import sortify
176 from django.core.files import File
177 from librarian.picture import WLPicture, ImageStore
178 close_xml_file = False
179 close_image_file = False
181 if image_file is not None and not isinstance(image_file, File):
182 image_file = File(open(image_file))
183 close_image_file = True
185 if not isinstance(xml_file, File):
186 xml_file = File(open(xml_file))
187 close_xml_file = True
189 with transaction.atomic():
190 # use librarian to parse meta-data
191 if image_store is None:
192 image_store = ImageStore(picture_storage.path('images'))
193 picture_xml = WLPicture.from_file(xml_file, image_store=image_store)
195 picture, created = Picture.objects.get_or_create(slug=picture_xml.slug[:120])
196 if not created and not overwrite:
197 raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug)
199 picture.areas.all().delete()
200 picture.title = unicode(picture_xml.picture_info.title)
201 picture.extra_info = picture_xml.picture_info.to_dict()
203 picture_tags = set(catalogue.models.Tag.tags_from_info(picture_xml.picture_info))
204 for tag in picture_tags:
205 if not tag.for_pictures:
206 tag.for_pictures = True
209 area_data = {'themes': {}, 'things': {}}
211 # Treat all names in picture XML as in default language.
212 lang = settings.LANGUAGE_CODE
214 for part in picture_xml.partiter():
215 if picture_xml.frame:
216 c = picture_xml.frame[0]
217 part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']]
218 if part.get('object', None) is not None:
220 for objname in part['object'].split(','):
221 objname = objname.strip()
222 assert objname, 'Empty object name'
223 # str.capitalize() is wrong, because it also lowers letters
224 objname = objname[0].upper() + objname[1:]
225 tag, created = catalogue.models.Tag.objects.get_or_create(
226 slug=slugify(objname), category='thing')
229 setattr(tag, 'name_%s' % lang, tag.name)
230 tag.sort_key = sortify(tag.name)
231 tag.for_pictures = True
233 area_data['things'][tag.slug] = {
235 'coords': part['coords'],
239 if not tag.for_pictures:
240 tag.for_pictures = True
242 area = PictureArea.rectangle(picture, 'thing', part['coords'])
244 # WTF thing area does not inherit tags from picture and theme area does, is it intentional?
248 for motifs in part['themes']:
249 for motif in motifs.split(','):
250 tag, created = catalogue.models.Tag.objects.get_or_create(
251 slug=slugify(motif), category='theme')
254 tag.sort_key = sortify(tag.name)
255 tag.for_pictures = True
257 # motif_tags.add(tag)
259 if not tag.for_pictures:
260 tag.for_pictures = True
262 area_data['themes'][tag.slug] = {
264 'coords': part['coords']
267 logging.debug("coords for theme: %s" % part['coords'])
268 area = PictureArea.rectangle(picture, 'theme', part['coords'])
270 area.tags = _tags.union(picture_tags)
272 picture.tags = picture_tags
273 picture.areas_json = area_data
275 if image_file is not None:
278 img = picture_xml.image_file()
280 modified = cls.crop_to_frame(picture_xml, img)
281 modified = cls.add_source_note(picture_xml, modified)
283 picture.width, picture.height = modified.size
285 modified_file = StringIO()
286 modified.save(modified_file, format='JPEG', quality=95)
287 # FIXME: hardcoded extension - detect from DC format or orginal filename
288 picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
290 picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
292 tasks.generate_picture_html(picture.id)
293 if not settings.NO_SEARCH_INDEX and search_index:
294 tasks.index_picture.delay(picture.id, picture_info=picture_xml.picture_info)
304 def crop_to_frame(cls, wlpic, image_file):
305 img = Image.open(image_file)
306 if wlpic.frame is None or wlpic.frame == [[0, 0], [-1, -1]]:
308 img = img.crop(itertools.chain(*wlpic.frame))
312 def add_source_note(wlpic, img):
313 from PIL import ImageDraw, ImageFont
314 from librarian import get_resource
316 annotated = Image.new(img.mode, (img.size[0], img.size[1] + 40), (255, 255, 255))
317 annotated.paste(img, (0, 0))
318 annotation = Image.new('RGB', (img.size[0] * 3, 120), (255, 255, 255))
319 ImageDraw.Draw(annotation).text(
321 wlpic.picture_info.source_name,
323 font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
325 annotated.paste(annotation.resize((img.size[0], 40), Image.ANTIALIAS), (0, img.size[1]))
330 if not hasattr(self, '_info'):
331 from librarian import dcparser
332 from librarian import picture
333 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
337 def pretty_title(self, html_links=False):
338 names = [(tag.name, tag.get_absolute_url()) for tag in self.authors().only('name', 'category', 'slug')]
339 names.append((self.title, self.get_absolute_url()))
342 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
344 names = [tag[0] for tag in names]
345 return ', '.join(names)
347 def related_themes(self):
348 return catalogue.models.Tag.objects.usage_for_queryset(
349 self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))
351 def flush_includes(self, languages=True):
354 if languages is True:
355 languages = [lc for (lc, _ln) in settings.LANGUAGES]
357 template % (self.pk, lang)
359 '/katalog/p/%d/short.%s.html',
360 '/katalog/p/%d/mini.%s.html',
362 for lang in languages
365 def search_index(self, picture_info=None, index=None, index_tags=True, commit=True):
367 from search.index import Index
370 index.index_picture(self, picture_info)
376 index.index.rollback()