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             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',
 
 343                 '/katalog/p/%d/mini.%s.html',
 
 345             for lang in languages