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=255)
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)
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)
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 for part in picture_xml.partiter():
174 if picture_xml.frame:
175 c = picture_xml.frame[0]
176 part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']]
177 if part.get('object', None) is not None:
179 for objname in part['object'].split(','):
180 objname = objname.strip()
181 tag, created = catalogue.models.Tag.objects.get_or_create(slug=slughifi(objname), category='thing')
184 tag.sort_key = sortify(tag.name)
187 area_data['things'][tag.slug] = {
188 'object': part['object'],
189 'coords': part['coords'],
193 area = PictureArea.rectangle(picture, 'thing', part['coords'])
198 for motifs in part['themes']:
199 for motif in motifs.split(','):
200 tag, created = catalogue.models.Tag.objects.get_or_create(slug=slughifi(motif), category='theme')
203 tag.sort_key = sortify(tag.name)
207 area_data['themes'][tag.slug] = {
209 'coords': part['coords']
212 logging.debug("coords for theme: %s" % part['coords'])
213 area = PictureArea.rectangle(picture, 'theme', part['coords'])
215 area.tags = _tags.union(picture_tags)
217 picture.tags = picture_tags.union(motif_tags).union(thing_tags)
218 picture.areas_json = area_data
220 if image_file is not None:
223 img = picture_xml.image_file()
225 modified = cls.crop_to_frame(picture_xml, img)
226 modified = cls.add_source_note(picture_xml, modified)
228 picture.width, picture.height = modified.size
230 modified_file = StringIO()
231 modified.save(modified_file, format='png', quality=95)
232 # FIXME: hardcoded extension - detect from DC format or orginal filename
233 picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
235 picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
237 tasks.generate_picture_html(picture.id)
239 except Exception, ex:
240 logging.exception("Exception during import, rolling back")
241 transaction.rollback()
255 def crop_to_frame(cls, wlpic, image_file):
256 img = Image.open(image_file)
257 if wlpic.frame is None:
259 img = img.crop(itertools.chain(*wlpic.frame))
263 def add_source_note(wlpic, img):
264 from PIL import ImageDraw, ImageFont
265 from librarian import get_resource
267 annotated = Image.new(img.mode,
268 (img.size[0], img.size[1] + 40),
271 annotated.paste(img, (0, 0))
272 annotation = Image.new(img.mode, (3000, 120), (255, 255, 255))
273 ImageDraw.Draw(annotation).text(
275 wlpic.picture_info.source_name,
277 font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
279 annotated.paste(annotation.resize((1000, 40), Image.ANTIALIAS), (0, img.size[1]))
283 def picture_list(cls, filter=None):
284 """Generates a hierarchical listing of all pictures
285 Pictures are optionally filtered with a test function.
288 pics = cls.objects.all().order_by('sort_key')\
289 .only('title', 'slug', 'image_file')
292 pics = pics.filter(filter).distinct()
294 pics_by_author = SortedDict()
296 for tag in catalogue.models.Tag.objects.filter(category='author'):
297 pics_by_author[tag] = []
299 for pic in pics.iterator():
300 authors = list(pic.tags.filter(category='author'))
302 for author in authors:
303 pics_by_author[author].append(pic)
307 return pics_by_author, orphans
311 if not hasattr(self, '_info'):
312 from librarian import dcparser
313 from librarian import picture
314 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
318 def pretty_title(self, html_links=False):
320 names = [(tag.name, tag.get_absolute_url())
321 for tag in self.tags.filter(category='author')]
322 names.append((self.title, self.get_absolute_url()))
325 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
327 names = [tag[0] for tag in names]
328 return ', '.join(names)
330 def related_themes(self):
331 return catalogue.models.Tag.objects.usage_for_queryset(
332 self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))
334 def flush_includes(self, languages=True):
337 if languages is True:
338 languages = [lc for (lc, _ln) in settings.LANGUAGES]
340 template % (self.pk, lang)
342 '/katalog/p/%d/short.%s.html',
344 for lang in languages