From 939f115b73778ebcd99e99abc04e94dbe16d17ff Mon Sep 17 00:00:00 2001 From: Marcin Koziej Date: Wed, 4 Dec 2013 19:45:09 +0100 Subject: [PATCH] viewer basically implemented. going to wire information/links on the site --- .../management/commands/importbooks.py | 14 +- apps/catalogue/models/tag.py | 1 + apps/picture/migrations/0001_initial.py | 46 +++ .../0002_auto__add_field_picture_areas.py | 38 +++ apps/picture/migrations/__init__.py | 0 apps/picture/models.py | 94 ++++-- apps/picture/tasks.py | 25 ++ .../templates/picture/picture_viewer.html | 22 +- apps/picture/views.py | 5 +- .../static/css/master.book.css | 4 +- .../static/css/master.picture.css | 80 ++++- apps/wolnelektury_core/static/js/picture.js | 318 +++++++++++------- wolnelektury/settings/static.py | 2 +- 13 files changed, 490 insertions(+), 159 deletions(-) create mode 100644 apps/picture/migrations/0001_initial.py create mode 100644 apps/picture/migrations/0002_auto__add_field_picture_areas.py create mode 100644 apps/picture/migrations/__init__.py create mode 100644 apps/picture/tasks.py diff --git a/apps/catalogue/management/commands/importbooks.py b/apps/catalogue/management/commands/importbooks.py index 323755e58..2a44d5aaf 100644 --- a/apps/catalogue/management/commands/importbooks.py +++ b/apps/catalogue/management/commands/importbooks.py @@ -11,7 +11,7 @@ from django.core.management.base import BaseCommand from django.core.management.color import color_style from django.core.files import File from catalogue.utils import trim_query_log - +from librarian.picture import ImageStore from wolnelektury_core.management.profile import profile from catalogue.models import Book @@ -60,8 +60,16 @@ class Command(BaseCommand): print "Importing %s.%s" % (file_base, ebook_format) book.save() - def import_picture(self, file_path, options): - picture = Picture.from_xml_file(file_path, overwrite=options.get('force')) + def import_picture(self, file_path, options, continue_on_error=True): + try: + image_store = ImageStore(os.path.dirname(file_path)) + picture = Picture.from_xml_file(file_path, image_store=image_store, overwrite=options.get('force')) + except Exception, ex: + if continue_on_error: + print "%s: %s" % (file_path, ex) + return + else: + raise ex return picture # @profile diff --git a/apps/catalogue/models/tag.py b/apps/catalogue/models/tag.py index b0c75fbb4..a6f395cee 100644 --- a/apps/catalogue/models/tag.py +++ b/apps/catalogue/models/tag.py @@ -19,6 +19,7 @@ TAG_CATEGORIES = ( ('theme', _('theme')), ('set', _('set')), ('book', _('book')), + ('thing', _('thing')), # things shown on pictures ) diff --git a/apps/picture/migrations/0001_initial.py b/apps/picture/migrations/0001_initial.py new file mode 100644 index 000000000..f61a3de74 --- /dev/null +++ b/apps/picture/migrations/0001_initial.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Picture' + db.create_table(u'picture_picture', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('title', self.gf('django.db.models.fields.CharField')(max_length=120)), + ('slug', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=120)), + ('sort_key', self.gf('django.db.models.fields.CharField')(max_length=120, db_index=True)), + ('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, db_index=True, blank=True)), + ('changed_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)), + ('xml_file', self.gf('django.db.models.fields.files.FileField')(max_length=100)), + ('image_file', self.gf('sorl.thumbnail.fields.ImageField')(max_length=100)), + ('html_file', self.gf('django.db.models.fields.files.FileField')(max_length=100)), + )) + db.send_create_signal(u'picture', ['Picture']) + + + def backwards(self, orm): + # Deleting model 'Picture' + db.delete_table(u'picture_picture') + + + models = { + u'picture.picture': { + 'Meta': {'ordering': "('sort_key',)", 'object_name': 'Picture'}, + 'changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'html_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image_file': ('sorl.thumbnail.fields.ImageField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '120'}), + 'sort_key': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '120'}), + 'xml_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}) + } + } + + complete_apps = ['picture'] \ No newline at end of file diff --git a/apps/picture/migrations/0002_auto__add_field_picture_areas.py b/apps/picture/migrations/0002_auto__add_field_picture_areas.py new file mode 100644 index 000000000..44ad804de --- /dev/null +++ b/apps/picture/migrations/0002_auto__add_field_picture_areas.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Picture.areas' + db.add_column(u'picture_picture', 'areas', + self.gf('jsonfield.fields.JSONField')(default='{}'), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Picture.areas' + db.delete_column(u'picture_picture', 'areas') + + + models = { + u'picture.picture': { + 'Meta': {'ordering': "('sort_key',)", 'object_name': 'Picture'}, + 'areas': ('jsonfield.fields.JSONField', [], {'default': "'{}'"}), + 'changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'html_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image_file': ('sorl.thumbnail.fields.ImageField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '120'}), + 'sort_key': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '120'}), + 'xml_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}) + } + } + + complete_apps = ['picture'] \ No newline at end of file diff --git a/apps/picture/migrations/__init__.py b/apps/picture/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/picture/models.py b/apps/picture/models.py index dc92c43d6..cd91a3f8a 100644 --- a/apps/picture/models.py +++ b/apps/picture/models.py @@ -1,4 +1,4 @@ -from django.db import models +from django.db import models, transaction import catalogue.models from django.db.models import permalink from sorl.thumbnail import ImageField @@ -10,13 +10,21 @@ from django.core.cache import get_cache from catalogue.utils import split_tags from django.utils.safestring import mark_safe from fnpdjango.utils.text.slughifi import slughifi +from picture import tasks +from StringIO import StringIO +import jsonfield +import itertools + +from PIL import Image from django.utils.translation import ugettext_lazy as _ from newtagging import managers from os import path -picture_storage = FileSystemStorage(location=path.join(settings.MEDIA_ROOT, 'pictures'), base_url=settings.MEDIA_URL + "pictures/") +picture_storage = FileSystemStorage(location=path.join( + settings.MEDIA_ROOT, 'pictures'), + base_url=settings.MEDIA_URL + "pictures/") class Picture(models.Model): @@ -31,6 +39,9 @@ class Picture(models.Model): 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 = jsonfield.JSONField(_('picture areas'), default='{}', editable=False) + objects = models.Manager() tagged = managers.ModelTaggedItemManager(catalogue.models.Tag) tags = managers.TagDescriptor(catalogue.models.Tag) @@ -64,7 +75,7 @@ class Picture(models.Model): return ('picture.views.picture_detail', [self.slug]) @classmethod - def from_xml_file(cls, xml_file, image_file=None, overwrite=False): + def from_xml_file(cls, xml_file, image_file=None, image_store=None, overwrite=False): """ Import xml and it's accompanying image file. If image file is missing, it will be fetched by librarian.picture.ImageStore @@ -76,10 +87,7 @@ class Picture(models.Model): from librarian.picture import WLPicture, ImageStore close_xml_file = False close_image_file = False - # class SimpleImageStore(object): - # def path(self_, slug, mime_type): - # """Returns the image file. Ignores slug ad mime_type.""" - # return image_file + if image_file is not None and not isinstance(image_file, File): image_file = File(open(image_file)) @@ -88,12 +96,12 @@ class Picture(models.Model): if not isinstance(xml_file, File): xml_file = File(open(xml_file)) close_xml_file = True - + try: # use librarian to parse meta-data - picture_xml = WLPicture.from_file(xml_file, - image_store=ImageStore(picture_storage.path('images'))) - # image_store=SimpleImageStore + if image_store is None: + image_store = ImageStore(picture_storage.path('images')) + picture_xml = WLPicture.from_file(xml_file, image_store=image_store) picture, created = Picture.objects.get_or_create(slug=picture_xml.slug) if not created and not overwrite: @@ -102,35 +110,80 @@ class Picture(models.Model): picture.title = picture_xml.picture_info.title motif_tags = set() + thing_tags = set() + area_data = {'themes':{}, 'things':{}} + for part in picture_xml.partiter(): - for motif in part['themes']: - tag, created = catalogue.models.Tag.objects.get_or_create(slug=slughifi(motif), category='theme') + if picture_xml.frame: + c = picture_xml.frame[0] + part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']] + if part.get('object', None) is not None: + objname = part['object'] + tag, created = catalogue.models.Tag.objects.get_or_create(slug=slughifi(objname), category='thing') if created: - tag.name = motif + tag.name = objname tag.sort_key = sortify(tag.name) tag.save() - motif_tags.add(tag) + thing_tags.add(tag) + area_data['things'][tag.slug] = { + 'object': part['object'], + 'coords': part['coords'], + } + else: + for motif in part['themes']: + tag, created = catalogue.models.Tag.objects.get_or_create(slug=slughifi(motif), category='theme') + if created: + tag.name = motif + tag.sort_key = sortify(tag.name) + tag.save() + motif_tags.add(tag) + area_data['themes'][tag.slug] = { + 'theme': motif, + 'coords': part['coords'] + } picture.tags = catalogue.models.Tag.tags_from_info(picture_xml.picture_info) + \ - list(motif_tags) + list(motif_tags) + list(thing_tags) + picture.areas = area_data if image_file is not None: img = image_file else: img = picture_xml.image_file() - # FIXME: hardcoded extension - picture.image_file.save(path.basename(picture_xml.image_path), File(img)) + modified = cls.crop_to_frame(picture_xml, img) + # FIXME: hardcoded extension - detect from DC format or orginal filename + picture.image_file.save(path.basename(picture_xml.image_path), File(modified)) picture.xml_file.save("%s.xml" % picture.slug, File(xml_file)) picture.save() + tasks.generate_picture_html(picture.id) + + except Exception, ex: + print "Rolling back a transaction" + transaction.rollback() + raise ex + finally: if close_xml_file: xml_file.close() if close_image_file: image_file.close() + + transaction.commit() + return picture + @classmethod + def crop_to_frame(cls, wlpic, image_file): + if wlpic.frame is None: + return image_file + img = Image.open(image_file) + img = img.crop(itertools.chain(*wlpic.frame)) + contents = StringIO() + img.save(contents, format='png', quality=95) + return contents + @classmethod def picture_list(cls, filter=None): """Generates a hierarchical listing of all pictures @@ -187,8 +240,9 @@ class Picture(models.Model): tags = self.tags.filter(category__in=('author', 'kind', 'epoch', 'genre')) tags = split_tags(tags) - short_html = unicode(render_to_string('picture/picture_short.html', - {'picture': self, 'tags': tags})) + short_html = unicode(render_to_string( + 'picture/picture_short.html', + {'picture': self, 'tags': tags})) if self.id: get_cache('permanent').set(cache_key, short_html) diff --git a/apps/picture/tasks.py b/apps/picture/tasks.py new file mode 100644 index 000000000..b823f26ea --- /dev/null +++ b/apps/picture/tasks.py @@ -0,0 +1,25 @@ +# -*- 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 datetime import datetime +from traceback import print_exc +from celery.task import task +from django.conf import settings +import picture.models +from django.core.files.base import ContentFile +from django.template.loader import render_to_string +import librarian.picture + + +@task +def generate_picture_html(picture_id): + pic = picture.models.Picture.objects.get(pk=picture_id) + + + html_text = unicode(render_to_string('picture/picture_info.html', { + 'things': pic.areas['things'], + 'themes': pic.areas['themes'], + })) + pic.html_file.save("%s.html" % pic.slug, ContentFile(html_text)) + diff --git a/apps/picture/templates/picture/picture_viewer.html b/apps/picture/templates/picture/picture_viewer.html index 46772dbc2..df0160cd1 100644 --- a/apps/picture/templates/picture/picture_viewer.html +++ b/apps/picture/templates/picture/picture_viewer.html @@ -1,6 +1,7 @@ {% load i18n %} {% load static from staticfiles %} {% load chunks compressed catalogue_tags %} +{% load thumbnail %} @@ -19,8 +20,8 @@