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=120)
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:
178 objname = part['object']
179 tag, created = catalogue.models.Tag.objects.get_or_create(slug=slughifi(objname), category='thing')
182 tag.sort_key = sortify(tag.name)
185 area_data['things'][tag.slug] = {
186 'object': part['object'],
187 'coords': part['coords'],
189 area = PictureArea.rectangle(picture, 'thing', part['coords'])
196 for motif in part['themes']:
197 tag, created = catalogue.models.Tag.objects.get_or_create(slug=slughifi(motif), category='theme')
200 tag.sort_key = sortify(tag.name)
204 area_data['themes'][tag.slug] = {
206 'coords': part['coords']
209 logging.debug("coords for theme: %s" % part['coords'])
210 area = PictureArea.rectangle(picture, 'theme', part['coords'])
212 area.tags = _tags.union(picture_tags)
214 picture.tags = picture_tags.union(motif_tags).union(thing_tags)
215 picture.areas_json = area_data
217 if image_file is not None:
220 img = picture_xml.image_file()
222 modified = cls.crop_to_frame(picture_xml, img)
223 modified = cls.add_source_note(picture_xml, modified)
225 picture.width, picture.height = modified.size
227 modified_file = StringIO()
228 modified.save(modified_file, format='png', quality=95)
229 # FIXME: hardcoded extension - detect from DC format or orginal filename
230 picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
232 picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
234 tasks.generate_picture_html(picture.id)
236 except Exception, ex:
237 logging.exception("Exception during import, rolling back")
238 transaction.rollback()
252 def crop_to_frame(cls, wlpic, image_file):
253 img = Image.open(image_file)
254 if wlpic.frame is None:
256 img = img.crop(itertools.chain(*wlpic.frame))
260 def add_source_note(wlpic, img):
261 from PIL import ImageDraw, ImageFont
262 from librarian import get_resource
264 annotated = Image.new(img.mode,
265 (img.size[0], img.size[1] + 40),
268 annotated.paste(img, (0, 0))
269 annotation = Image.new(img.mode, (3000, 120), (255, 255, 255))
270 ImageDraw.Draw(annotation).text(
272 wlpic.picture_info.source_name,
274 font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
276 annotated.paste(annotation.resize((1000, 40), Image.ANTIALIAS), (0, img.size[1]))
280 def picture_list(cls, filter=None):
281 """Generates a hierarchical listing of all pictures
282 Pictures are optionally filtered with a test function.
285 pics = cls.objects.all().order_by('sort_key')\
286 .only('title', 'slug', 'image_file')
289 pics = pics.filter(filter).distinct()
291 pics_by_author = SortedDict()
293 for tag in catalogue.models.Tag.objects.filter(category='author'):
294 pics_by_author[tag] = []
296 for pic in pics.iterator():
297 authors = list(pic.tags.filter(category='author'))
299 for author in authors:
300 pics_by_author[author].append(pic)
304 return pics_by_author, orphans
308 if not hasattr(self, '_info'):
309 from librarian import dcparser
310 from librarian import picture
311 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
315 def pretty_title(self, html_links=False):
317 names = [(tag.name, tag.get_absolute_url())
318 for tag in self.tags.filter(category='author')]
319 names.append((self.title, self.get_absolute_url()))
322 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
324 names = [tag[0] for tag in names]
325 return ', '.join(names)
327 def related_themes(self):
328 return catalogue.models.Tag.objects.usage_for_queryset(
329 self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))
331 def flush_includes(self, languages=True):
334 if languages is True:
335 languages = [lc for (lc, _ln) in settings.LANGUAGES]
337 template % (self.pk, lang)
339 '/katalog/p/%d/short.%s.html',
341 for lang in languages