Merge remote-tracking branch 'zawadzki/new-design'
[wolnelektury.git] / src / picture / models.py
index 5aabe3c..3a05ad8 100644 (file)
@@ -1,22 +1,24 @@
-# -*- coding: utf-8 -*-
 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from django.db import models, transaction
 import catalogue.models
 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from django.db import models, transaction
 import catalogue.models
-from django.db.models import permalink
 from sorl.thumbnail import ImageField
 from django.conf import settings
 from django.contrib.contenttypes.fields import GenericRelation
 from django.core.files.storage import FileSystemStorage
 from sorl.thumbnail import ImageField
 from django.conf import settings
 from django.contrib.contenttypes.fields import GenericRelation
 from django.core.files.storage import FileSystemStorage
-from django.utils.datastructures import SortedDict
-from fnpdjango.utils.text.slughifi import slughifi
-from ssify import flush_ssi_includes
+from django.urls import reverse
+from slugify import slugify
+
+from catalogue.models.tag import prefetched_relations
+from catalogue.utils import split_tags
 from picture import tasks
 from picture import tasks
-from StringIO import StringIO
-import jsonfield
+from wolnelektury.utils import cached_render, clear_cached_renders
+from io import BytesIO
 import itertools
 import itertools
+import json
 import logging
 import logging
+import re
 
 from PIL import Image
 
 
 from PIL import Image
 
@@ -31,16 +33,15 @@ picture_storage = FileSystemStorage(location=path.join(
 
 
 class PictureArea(models.Model):
 
 
 class PictureArea(models.Model):
-    picture = models.ForeignKey('picture.Picture', related_name='areas')
-    area = jsonfield.JSONField(_('area'), default={}, editable=False)
-    kind = models.CharField(_('kind'), max_length=10, blank=False,
-                           null=False, db_index=True,
-                           choices=(('thing', _('thing')),
-                                    ('theme', _('theme'))))
-
-    objects     = models.Manager()
-    tagged      = managers.ModelTaggedItemManager(catalogue.models.Tag)
-    tags        = managers.TagDescriptor(catalogue.models.Tag)
+    picture = models.ForeignKey('picture.Picture', models.CASCADE, related_name='areas')
+    area = models.TextField(_('area'), default='{}', editable=False)
+    kind = models.CharField(
+        _('kind'), max_length=10, blank=False, null=False, db_index=True,
+        choices=(('thing', _('thing')), ('theme', _('theme'))))
+
+    objects = models.Manager()
+    tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
+    tags = managers.TagDescriptor(catalogue.models.Tag)
     tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
 
     short_html_url_name = 'picture_area_short'
     tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
 
     short_html_url_name = 'picture_area_short'
@@ -50,21 +51,24 @@ class PictureArea(models.Model):
         pa = PictureArea()
         pa.picture = picture
         pa.kind = kind
         pa = PictureArea()
         pa.picture = picture
         pa.kind = kind
-        pa.area = coords
+        pa.area = json.dumps(coords)
         return pa
 
         return pa
 
-    def flush_includes(self, languages=True):
-        if not languages:
-            return
-        if languages is True:
-            languages = [lc for (lc, _ln) in settings.LANGUAGES]
-        flush_ssi_includes([
-            template % (self.pk, lang)
-            for template in [
-                '/katalog/pa/%d/short.%s.html',
-                ]
-            for lang in languages
-            ])
+    def get_area_json(self):
+        return json.loads(self.area)
+
+    @cached_render('picture/picturearea_short.html')
+    def midi_box(self):
+        themes = self.tags.filter(category='theme')
+        things = self.tags.filter(category='thing')
+        return {
+            'area': self,
+            'theme': themes[0] if themes else None,
+            'thing': things[0] if things else None,
+        }
+
+    def clear_cache(self):
+        clear_cached_renders(self.midi_box)
 
 
 class Picture(models.Model):
 
 
 class Picture(models.Model):
@@ -72,26 +76,27 @@ class Picture(models.Model):
     Picture resource.
 
     """
     Picture resource.
 
     """
-    title       = models.CharField(_('title'), max_length=32767)
-    slug        = models.SlugField(_('slug'), max_length=120, db_index=True, unique=True)
-    sort_key    = models.CharField(_('sort key'), max_length=120, db_index=True, editable=False)
-    sort_key_author = models.CharField(_('sort key by author'), max_length=120, db_index=True, editable=False, default=u'')
-    created_at  = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
-    changed_at  = models.DateTimeField(_('creation date'), auto_now=True, db_index=True)
-    xml_file    = models.FileField('xml_file', upload_to="xml", storage=picture_storage)
-    image_file  = ImageField(_('image_file'), upload_to="images", storage=picture_storage)
-    html_file   = models.FileField('html_file', upload_to="html", storage=picture_storage)
-    areas_json       = jsonfield.JSONField(_('picture areas JSON'), default={}, editable=False)
-    extra_info    = jsonfield.JSONField(_('extra information'), default={})
-    culturepl_link   = models.CharField(blank=True, max_length=240)
-    wiki_link     = models.CharField(blank=True, max_length=240)
-
-    width       = models.IntegerField(null=True)
-    height      = models.IntegerField(null=True)
-
-    objects     = models.Manager()
-    tagged      = managers.ModelTaggedItemManager(catalogue.models.Tag)
-    tags        = managers.TagDescriptor(catalogue.models.Tag)
+    title = models.CharField(_('title'), max_length=32767)
+    slug = models.SlugField(_('slug'), max_length=120, db_index=True, unique=True)
+    sort_key = models.CharField(_('sort key'), max_length=120, db_index=True, editable=False)
+    sort_key_author = models.CharField(
+        _('sort key by author'), max_length=120, db_index=True, editable=False, default='')
+    created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
+    changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True)
+    xml_file = models.FileField(_('xml file'), upload_to="xml", storage=picture_storage)
+    image_file = ImageField(_('image file'), upload_to="images", storage=picture_storage)
+    html_file = models.FileField(_('html file'), upload_to="html", storage=picture_storage)
+    areas_json = models.TextField(_('picture areas JSON'), default='{}', editable=False)
+    extra_info = models.TextField(_('extra information'), default='{}')
+    culturepl_link = models.CharField(blank=True, max_length=240)
+    wiki_link = models.CharField(blank=True, max_length=240)
+
+    width = models.IntegerField(null=True)
+    height = models.IntegerField(null=True)
+
+    objects = models.Manager()
+    tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
+    tags = managers.TagDescriptor(catalogue.models.Tag)
     tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
 
     short_html_url_name = 'picture_short'
     tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
 
     short_html_url_name = 'picture_short'
@@ -100,35 +105,71 @@ class Picture(models.Model):
         pass
 
     class Meta:
         pass
 
     class Meta:
-        ordering = ('sort_key',)
+        ordering = ('sort_key_author', 'sort_key')
 
         verbose_name = _('picture')
         verbose_name_plural = _('pictures')
 
 
         verbose_name = _('picture')
         verbose_name_plural = _('pictures')
 
+    def get_extra_info_json(self):
+        return json.loads(self.extra_info or '{}')
+
     def save(self, force_insert=False, force_update=False, **kwargs):
         from sortify import sortify
 
         self.sort_key = sortify(self.title)[:120]
 
         try:
     def save(self, force_insert=False, force_update=False, **kwargs):
         from sortify import sortify
 
         self.sort_key = sortify(self.title)[:120]
 
         try:
-            author = self.tags.filter(category='author')[0].sort_key
-        except IndexError:
-            author = u''
+            author = self.authors().first().sort_key
+        except AttributeError:
+            author = ''
         self.sort_key_author = author
 
         ret = super(Picture, self).save(force_insert, force_update)
 
         return ret
 
         self.sort_key_author = author
 
         ret = super(Picture, self).save(force_insert, force_update)
 
         return ret
 
-    def __unicode__(self):
+    def __str__(self):
         return self.title
 
         return self.title
 
-    @permalink
+    def authors(self):
+        return self.tags.filter(category='author')
+
+    def tag_unicode(self, category):
+        relations = prefetched_relations(self, category)
+        if relations:
+            return ', '.join(rel.tag.name for rel in relations)
+        else:
+            return ', '.join(self.tags.filter(category=category).values_list('name', flat=True))
+
+    def author_unicode(self):
+        return self.tag_unicode('author')
+
+    def tags_by_category(self):
+        return split_tags(self.tags)
+
     def get_absolute_url(self):
     def get_absolute_url(self):
-        return ('picture.views.picture_detail', [self.slug])
+        return reverse('picture_detail', args=[self.slug])
+
+    def get_initial(self):
+        try:
+            return re.search(r'\w', self.title, re.U).group(0)
+        except AttributeError:
+            return ''
+
+    def get_next(self):
+        try:
+            return type(self).objects.filter(sort_key__gt=self.sort_key)[0]
+        except IndexError:
+            return None
+
+    def get_previous(self):
+        try:
+            return type(self).objects.filter(sort_key__lt=self.sort_key).order_by('-sort_key')[0]
+        except IndexError:
+            return None
 
     @classmethod
 
     @classmethod
-    def from_xml_file(cls, xml_file, image_file=None, image_store=None, overwrite=False):
+    def from_xml_file(cls, xml_file, image_file=None, image_store=None, overwrite=False, search_index=True):
         """
         Import xml and it's accompanying image file.
         If image file is missing, it will be fetched by librarian.picture.ImageStore
         """
         Import xml and it's accompanying image file.
         If image file is missing, it will be fetched by librarian.picture.ImageStore
@@ -141,9 +182,8 @@ class Picture(models.Model):
         close_xml_file = False
         close_image_file = False
 
         close_xml_file = False
         close_image_file = False
 
-
         if image_file is not None and not isinstance(image_file, File):
         if image_file is not None and not isinstance(image_file, File):
-            image_file = File(open(image_file))
+            image_file = File(open(image_file, 'rb'))
             close_image_file = True
 
         if not isinstance(xml_file, File):
             close_image_file = True
 
         if not isinstance(xml_file, File):
@@ -161,14 +201,16 @@ class Picture(models.Model):
                 raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug)
 
             picture.areas.all().delete()
                 raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug)
 
             picture.areas.all().delete()
-            picture.title = unicode(picture_xml.picture_info.title)
-            picture.extra_info = picture_xml.picture_info.to_dict()
+            picture.title = str(picture_xml.picture_info.title)
+            picture.extra_info = json.dumps(picture_xml.picture_info.to_dict())
 
             picture_tags = set(catalogue.models.Tag.tags_from_info(picture_xml.picture_info))
 
             picture_tags = set(catalogue.models.Tag.tags_from_info(picture_xml.picture_info))
-            motif_tags = set()
-            thing_tags = set()
+            for tag in picture_tags:
+                if not tag.for_pictures:
+                    tag.for_pictures = True
+                    tag.save()
 
 
-            area_data = {'themes':{}, 'things':{}}
+            area_data = {'themes': {}, 'things': {}}
 
             # Treat all names in picture XML as in default language.
             lang = settings.LANGUAGE_CODE
 
             # Treat all names in picture XML as in default language.
             lang = settings.LANGUAGE_CODE
@@ -180,34 +222,47 @@ class Picture(models.Model):
                 if part.get('object', None) is not None:
                     _tags = set()
                     for objname in part['object'].split(','):
                 if part.get('object', None) is not None:
                     _tags = set()
                     for objname in part['object'].split(','):
-                        objname = objname.strip().capitalize()
-                        tag, created = catalogue.models.Tag.objects.get_or_create(slug=slughifi(objname), category='thing')
+                        objname = objname.strip()
+                        assert objname, 'Empty object name'
+                        # str.capitalize() is wrong, because it also lowers letters
+                        objname = objname[0].upper() + objname[1:]
+                        tag, created = catalogue.models.Tag.objects.get_or_create(
+                            slug=slugify(objname), category='thing')
                         if created:
                             tag.name = objname
                             setattr(tag, 'name_%s' % lang, tag.name)
                             tag.sort_key = sortify(tag.name)
                         if created:
                             tag.name = objname
                             setattr(tag, 'name_%s' % lang, tag.name)
                             tag.sort_key = sortify(tag.name)
+                            tag.for_pictures = True
                             tag.save()
                             tag.save()
-                        #thing_tags.add(tag)
                         area_data['things'][tag.slug] = {
                             'object': objname,
                             'coords': part['coords'],
                             }
 
                         _tags.add(tag)
                         area_data['things'][tag.slug] = {
                             'object': objname,
                             'coords': part['coords'],
                             }
 
                         _tags.add(tag)
+                        if not tag.for_pictures:
+                            tag.for_pictures = True
+                            tag.save()
                     area = PictureArea.rectangle(picture, 'thing', part['coords'])
                     area.save()
                     area = PictureArea.rectangle(picture, 'thing', part['coords'])
                     area.save()
+                    # WTF thing area does not inherit tags from picture and theme area does, is it intentional?
                     area.tags = _tags
                 else:
                     _tags = set()
                     for motifs in part['themes']:
                         for motif in motifs.split(','):
                     area.tags = _tags
                 else:
                     _tags = set()
                     for motifs in part['themes']:
                         for motif in motifs.split(','):
-                            tag, created = catalogue.models.Tag.objects.get_or_create(slug=slughifi(motif), category='theme')
+                            tag, created = catalogue.models.Tag.objects.get_or_create(
+                                slug=slugify(motif), category='theme')
                             if created:
                                 tag.name = motif
                                 tag.sort_key = sortify(tag.name)
                             if created:
                                 tag.name = motif
                                 tag.sort_key = sortify(tag.name)
+                                tag.for_pictures = True
                                 tag.save()
                                 tag.save()
-                            #motif_tags.add(tag)
+                            # motif_tags.add(tag)
                             _tags.add(tag)
                             _tags.add(tag)
+                            if not tag.for_pictures:
+                                tag.for_pictures = True
+                                tag.save()
                             area_data['themes'][tag.slug] = {
                                 'theme': motif,
                                 'coords': part['coords']
                             area_data['themes'][tag.slug] = {
                                 'theme': motif,
                                 'coords': part['coords']
@@ -218,8 +273,8 @@ class Picture(models.Model):
                     area.save()
                     area.tags = _tags.union(picture_tags)
 
                     area.save()
                     area.tags = _tags.union(picture_tags)
 
-            picture.tags = picture_tags.union(motif_tags).union(thing_tags)
-            picture.areas_json = area_data
+            picture.tags = picture_tags
+            picture.areas_json = json.dumps(area_data)
 
             if image_file is not None:
                 img = image_file
 
             if image_file is not None:
                 img = image_file
@@ -231,7 +286,7 @@ class Picture(models.Model):
 
             picture.width, picture.height = modified.size
 
 
             picture.width, picture.height = modified.size
 
-            modified_file = StringIO()
+            modified_file = BytesIO()
             modified.save(modified_file, format='JPEG', quality=95)
             # FIXME: hardcoded extension - detect from DC format or orginal filename
             picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
             modified.save(modified_file, format='JPEG', quality=95)
             # FIXME: hardcoded extension - detect from DC format or orginal filename
             picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
@@ -239,6 +294,8 @@ class Picture(models.Model):
             picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
             picture.save()
             tasks.generate_picture_html(picture.id)
             picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
             picture.save()
             tasks.generate_picture_html(picture.id)
+            if not settings.NO_SEARCH_INDEX and search_index:
+                tasks.index_picture.delay(picture.id, picture_info=picture_xml.picture_info)
 
         if close_xml_file:
             xml_file.close()
 
         if close_xml_file:
             xml_file.close()
@@ -260,10 +317,7 @@ class Picture(models.Model):
         from PIL import ImageDraw, ImageFont
         from librarian import get_resource
 
         from PIL import ImageDraw, ImageFont
         from librarian import get_resource
 
-        annotated = Image.new(img.mode,
-                (img.size[0], img.size[1] + 40),
-                (255, 255, 255)
-            )
+        annotated = Image.new(img.mode, (img.size[0], img.size[1] + 40), (255, 255, 255))
         annotated.paste(img, (0, 0))
         annotation = Image.new('RGB', (img.size[0] * 3, 120), (255, 255, 255))
         ImageDraw.Draw(annotation).text(
         annotated.paste(img, (0, 0))
         annotation = Image.new('RGB', (img.size[0] * 3, 120), (255, 255, 255))
         ImageDraw.Draw(annotation).text(
@@ -275,33 +329,6 @@ class Picture(models.Model):
         annotated.paste(annotation.resize((img.size[0], 40), Image.ANTIALIAS), (0, img.size[1]))
         return annotated
 
         annotated.paste(annotation.resize((img.size[0], 40), Image.ANTIALIAS), (0, img.size[1]))
         return annotated
 
-    @classmethod
-    def picture_list(cls, filter=None):
-        """Generates a hierarchical listing of all pictures
-        Pictures are optionally filtered with a test function.
-        """
-
-        pics = cls.objects.all().order_by('sort_key')\
-            .only('title', 'slug', 'image_file')
-
-        if filter:
-            pics = pics.filter(filter).distinct()
-
-        pics_by_author = SortedDict()
-        orphans = []
-        for tag in catalogue.models.Tag.objects.filter(category='author'):
-            pics_by_author[tag] = []
-
-        for pic in pics.iterator():
-            authors = list(pic.tags.filter(category='author'))
-            if authors:
-                for author in authors:
-                    pics_by_author[author].append(pic)
-            else:
-                orphans.append(pic)
-
-        return pics_by_author, orphans
-
     @property
     def info(self):
         if not hasattr(self, '_info'):
     @property
     def info(self):
         if not hasattr(self, '_info'):
@@ -312,9 +339,7 @@ class Picture(models.Model):
         return self._info
 
     def pretty_title(self, html_links=False):
         return self._info
 
     def pretty_title(self, html_links=False):
-        picture = self
-        names = [(tag.name, tag.get_absolute_url())
-                 for tag in self.tags.filter(category='author')]
+        names = [(tag.name, tag.get_absolute_url()) for tag in self.authors().only('name', 'category', 'slug')]
         names.append((self.title, self.get_absolute_url()))
 
         if html_links:
         names.append((self.title, self.get_absolute_url()))
 
         if html_links:
@@ -323,20 +348,36 @@ class Picture(models.Model):
             names = [tag[0] for tag in names]
         return ', '.join(names)
 
             names = [tag[0] for tag in names]
         return ', '.join(names)
 
+    @cached_render('picture/picture_mini_box.html')
+    def mini_box(self):
+        return {
+            'picture': self,
+        }
+
+    @cached_render('picture/picture_short.html')
+    def midi_box(self):
+        return {
+            'picture': self,
+        }
+
     def related_themes(self):
         return catalogue.models.Tag.objects.usage_for_queryset(
             self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))
 
     def related_themes(self):
         return catalogue.models.Tag.objects.usage_for_queryset(
             self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))
 
-    def flush_includes(self, languages=True):
-        if not languages:
-            return
-        if languages is True:
-            languages = [lc for (lc, _ln) in settings.LANGUAGES]
-        flush_ssi_includes([
-            template % (self.pk, lang)
-            for template in [
-                '/katalog/p/%d/short.%s.html',
-                '/katalog/p/%d/mini.%s.html',
-                ]
-            for lang in languages
-            ])
+    def clear_cache(self):
+        clear_cached_renders(self.mini_box)
+        clear_cached_renders(self.midi_box)
+
+    def search_index(self, picture_info=None, index=None, index_tags=True, commit=True):
+        if index is None:
+            from search.index import Index
+            index = Index()
+        try:
+            index.index_picture(self, picture_info)
+            if index_tags:
+                index.index_tags()
+            if commit:
+                index.index.commit()
+        except Exception as e:
+            index.index.rollback()
+            raise e