circle
", + "content": "Annotation number 4
", "resource_type": "dctypes:Text", - "motivation": "oa:commenting", + "motivation": "sc:painting", "format": "text/plain", "canvas": "7261fae2-a24e-4a1c-9743-516f6c4ea0c9", "language": "en", "owner": 1, "oa_annotation": { - "motivation": ["oa:commenting"], - "resource": [{ - "chars": "circle
", - "@type": "dctypes:Text", - "format": "text/html" - }], - "@type": "oa:Annotation", - "@id": "ca4f81ea-d37d-449c-9aca-0e329cefe340", - "@context": "http://iiif.io/api/presentation/2/context.json", - "on": [{ - "selector": { - "default": { - "value": "xywh=972,444,173,118", - "@type": "oa:FragmentSelector" - }, - "item": { - "value": "", - "@type": "oa:SvgSelector" + "on": [ + { + "full": "https://0.0.0.0:3000/iiif/readux:t9vft/canvas/fedora:emory:5622$0", + "@type": "oa:SpecificResource", + "within": { + "@id": "https://0.0.0.0:3000/iiif/v2/readux:t9vft/manifest", + "@type": "sc:Manifest" }, - "@type": "oa:Choice" - }, - "full": "https://readux-dev.org:3000/iiif/readux:st7r6/canvas/edora:emory:5622", - "within": { - "@id": "https://readux-dev.org:3000/iiif/v2/readux:st7r6/manifest", - "@type": "sc:Manifest" - }, - "@type": "oa:SpecificResource" - }] - }, - "svg": "", - "start_selector": null, - "end_selector": null, - "start_offset": null, - "end_offset": null - } -}, { - "model": "readux.userannotation", - "pk": "582a04c0-9ff3-4767-bdeb-ca2072a83598", - "fields": { - "x": 999, - "y": 153, - "w": 218, - "h": 240, - "order": 0, - "content": "freehand updated
\n", - "resource_type": "dctypes:Text", - "motivation": "oa:commenting", - "format": "text/plain", - "canvas": "7261fae2-a24e-4a1c-9743-516f6c4ea0c9", - "language": "en", - "owner": 2, - "oa_annotation": { - "motivation": "oa:commenting", - "resource": { - "chars": "
freehand updated
\n", - "language": "en", - "@type": "dctypes:Text", - "format": "text/html" - }, + "selector": { + "item": { + "@type": "oa:SvgSelector", + "value": "" + }, + "@type": "oa:Choice", + "default": { + "@type": "oa:FragmentSelector", + "value": "xywh=1694,1577,785,240" + } + } + } + ], + "@id": "0ace875c-033e-400a-a4a4-3cb857369239", "@type": "oa:Annotation", - "@id": "582a04c0-9ff3-4767-bdeb-ca2072a83598", - "annotatedBy": { - "name": "" - }, "@context": "http://iiif.io/api/presentation/2/context.json", - "on": [{ - "selector": { - "default": { - "value": "xywh=999,153,218,240", - "@type": "oa:FragmentSelector" - }, - "item": { - "value": "", - "@type": "oa:SvgSelector" - }, - "@type": "oa:Choice" - }, - "full": "https://readux-dev.org:3000/iiif/readux:st7r6/canvas/edora:emory:5622", - "within": { - "@id": "https://readux-dev.org:3000/iiif/v2/readux:st7r6/manifest", - "@type": "sc:Manifest" - }, - "@type": "oa:SpecificResource" - }] + "resource": [ + { + "@type": "dctypes:Text", + "chars": "
Annotation number 4
", + "format": "text/html" + } + ], + "stylesheet": { + "value": ".anno-0ace875c-033e-400a-a4a4-3cb857369239 { background: rgba(0, 128, 0, 0.5); }", + "type": "CssStylesheet" + }, + "motivation": [ + "oa:commenting" + ] }, - "svg": "", + "svg": "", "start_selector": null, "end_selector": null, "start_offset": null, "end_offset": null } -}, { - "model": "readux.userannotation", - "pk": "9ff1d308-976a-4402-98db-0c804c0355c8", - "fields": { - "x": 330, - "y": 590, - "w": 706, - "h": 88, - "order": 0, - "content": "Annotation has been updated again maybe, just wwmaybe...maybe.go awayd. i feel good? Still working?
", - "resource_type": "dctypes:Text", - "motivation": "oa:commenting", - "format": "text/plain", - "canvas": "7261fae2-a24e-4a1c-9743-516f6c4ea0c9", - "language": "en", - "owner": 1, - "oa_annotation": { - "motivation": "oa:commenting", - "resource": { - "chars": "Annotation has been updated again maybe, just wwmaybe...maybe.go awayd. i feel good? Still working?
", - "language": "en", - "@type": "dctypes:Text", - "format": "text/html" - }, - "@type": "oa:Annotation", - "@id": "9ff1d308-976a-4402-98db-0c804c0355c8", - "annotatedBy": { - "name": "" - }, - "@context": "http://iiif.io/api/presentation/2/context.json", - "on": { - "selector": { - "value": "xywh=330,590,706,88", - "item": { - "endSelector": { - "value": "//*[@id='2548a0c1-ff47-41f6-a5c9-11f7097d8662']", - "refinedBy": { - "end": 7, - "@type": "TextPositionSelector" - }, - "@type": "XPathSelector" - }, - "startSelector": { - "value": "//*[@id='28ca35f3-a22e-4f4f-b45f-d0f37261d532']", - "refinedBy": { - "start": 0, - "@type": "TextPositionSelector" - }, - "@type": "XPathSelector" - }, - "@type": "RangeSelector" - }, - "@type": "oa:FragmentSelector" - }, - "full": "https://readux-dev.org:3000/iiif/v2/readux:st7r6/canvas/edora:emory:5622", - "within": { - "@id": "https://readux-dev.org:3000/iiif/v2/readux:st7r6/manifest", - "@type": "sc:Manifest" - }, - "@type": "oa:SpecificResource" - } - }, - "svg": "", - "start_selector": "28ca35f3-a22e-4f4f-b45f-d0f37261d532", - "end_selector": "2548a0c1-ff47-41f6-a5c9-11f7097d8662", - "start_offset": 0, - "end_offset": 7 - } -}, { - "model": "readux.userannotation", - "pk": "d421f51b-1c11-4ae1-82d1-a0fe4d00dfbf", - "fields": { - "x": 319, - "y": 1539, - "w": 551, - "h": 29, - "order": 0, - "content": "partial works!? yes, yes it does, but will it stay?
", - "resource_type": "dctypes:Text", - "motivation": "oa:commenting", - "format": "text/plain", - "canvas": "7261fae2-a24e-4a1c-9743-516f6c4ea0c9", - "language": "en", - "owner": 2, - "oa_annotation": { - "motivation": "oa:commenting", - "resource": { - "chars": "partial works!? yes, yes it does, but will it stay?
", - "language": "en", - "@type": "dctypes:Text", - "format": "text/html" - }, - "@type": "oa:Annotation", - "@id": "d421f51b-1c11-4ae1-82d1-a0fe4d00dfbf", - "annotatedBy": { - "name": "" - }, - "@context": "http://iiif.io/api/presentation/2/context.json", - "on": { - "selector": { - "value": "xywh=319,1539,551,29", - "item": { - "endSelector": { - "value": "//*[@id='5add0a50-4de3-4892-a403-15ea310315b0']", - "refinedBy": { - "end": 4, - "@type": "TextPositionSelector" - }, - "@type": "XPathSelector" - }, - "startSelector": { - "value": "//*[@id='093c11ba-56cf-420d-90ee-4041f1070c7a']", - "refinedBy": { - "start": 7, - "@type": "TextPositionSelector" - }, - "@type": "XPathSelector" - }, - "@type": "RangeSelector" - }, - "@type": "oa:FragmentSelector" - }, - "full": "https://readux-dev.org:3000/iiif/v2/readux:st7r6/canvas/edora:emory:5622", - "within": { - "@id": "https://readux-dev.org:3000/iiif/v2/readux:st7r6/manifest", - "@type": "sc:Manifest" - }, - "@type": "oa:SpecificResource" - } - }, - "svg": "", - "start_selector": "093c11ba-56cf-420d-90ee-4041f1070c7a", - "end_selector": "5add0a50-4de3-4892-a403-15ea310315b0", - "start_offset": 7, - "end_offset": 4 - } -}] \ No newline at end of file +} +] \ No newline at end of file diff --git a/apps/readux/migrations/0004_userannotation_style.py b/apps/readux/migrations/0004_userannotation_style.py new file mode 100644 index 000000000..cd9f52f3b --- /dev/null +++ b/apps/readux/migrations/0004_userannotation_style.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.2 on 2019-12-16 14:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('readux', '0003_auto_20191002_2127'), + ] + + operations = [ + migrations.AddField( + model_name='userannotation', + name='style', + field=models.CharField(blank=True, max_length=1000, null=True), + ), + ] diff --git a/apps/readux/models.py b/apps/readux/models.py index e2858eaf3..22b5616af 100644 --- a/apps/readux/models.py +++ b/apps/readux/models.py @@ -4,6 +4,7 @@ from django.dispatch import receiver from apps.iiif.canvases.models import Canvas import json +import re class UserAnnotation(AbstractAnnotation): start_selector = models.ForeignKey(Annotation, on_delete=models.CASCADE, null=True, blank=True, related_name='start_selector', default=None) @@ -31,8 +32,8 @@ def parse_mirador_annotation(self): elif isinstance(self.oa_annotation['on'], dict): anno_on = self.oa_annotation['on'] - - self.canvas = Canvas.objects.get(pid=anno_on['full'].split('/')[-1]) + if self.canvas == None: + self.canvas = Canvas.objects.get(pid=anno_on['full'].split('/')[-1]) mirador_item = anno_on['selector']['item'] @@ -53,6 +54,11 @@ def parse_mirador_annotation(self): elif isinstance(self.oa_annotation['resource'], dict): self.content = self.oa_annotation['resource']['chars'] self.resource_type = self.oa_annotation['resource']['@type'] + + # Replace the ID given by Mirador with the Readux given ID + if ('stylesheet' in self.oa_annotation): + uuid_pattern = re.compile(r'[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}') + self.style = uuid_pattern.sub(str(self.id), self.oa_annotation['stylesheet']['value']) def __is_text_annotation(self): return all([ diff --git a/apps/readux/test_export.py b/apps/readux/test_export.py new file mode 100644 index 000000000..9dda6fbd4 --- /dev/null +++ b/apps/readux/test_export.py @@ -0,0 +1,131 @@ +from django.test import TestCase, Client +from django.test import RequestFactory +from django.conf import settings +from django.contrib.auth import get_user_model +from django.urls import reverse +from apps.iiif.manifests.models import Manifest +from apps.iiif.manifests.views import ManifestExport, JekyllExport +from apps.iiif.canvases.models import Canvas +from apps.iiif.manifests.export import IiifManifestExport, JekyllSiteExport +from iiif_prezi.loader import ManifestReader +import io +import json +import logging +import os +import re +import tempfile +import zipfile + +User = get_user_model() + +class ManifestExportTests(TestCase): + fixtures = ['users.json', 'kollections.json', 'manifests.json', 'canvases.json', 'annotations.json', 'userannotation.json'] + + def setUp(self): + self.user = get_user_model().objects.get(pk=1) + self.factory = RequestFactory() + self.client = Client() + self.volume = Manifest.objects.get(pk='464d82f6-6ae5-4503-9afc-8e3cdd92a3f1') + self.start_canvas = self.volume.canvas_set.filter(is_starting_page=True).first() + self.default_start_canvas = self.volume.canvas_set.filter(is_starting_page=False).first() + self.assumed_label = ' Descrizione del Palazzo Apostolico Vaticano ' + self.assumed_pid = 'readux:st7r6' + self.manifest_export_view = ManifestExport.as_view() + self.jekyll_export_view = JekyllExport.as_view() + + def test_zip_creation(self): + zip = IiifManifestExport.get_zip(self.volume, 'v2', owners=[self.user.id]) + assert isinstance(zip, bytes) + # unzip the file somewhere + tmpdir = tempfile.mkdtemp(prefix='tmp-rdx-export-') + iiif_zip = zipfile.ZipFile(io.BytesIO(zip), "r") + iiif_zip.extractall(tmpdir) + manifest_path = os.path.join(tmpdir, 'manifest.json') + with open(manifest_path) as json_file: + manifest = json.load(json_file) + + ocr_annotation_list_id = manifest['sequences'][0]['canvases'][0]['otherContent'][0]['@id'] + ocr_annotation_list_path = os.path.join(tmpdir, re.sub('\W','_', ocr_annotation_list_id) + ".json") + assert os.path.exists(ocr_annotation_list_path) == 1 + + with open(ocr_annotation_list_path) as json_file: + ocr_annotation_list = json.load(json_file) + assert ocr_annotation_list['@id'] == ocr_annotation_list_id + + comment_annotation_list_id = manifest['sequences'][0]['canvases'][0]['otherContent'][1]['@id'] + comment_annotation_list_path = os.path.join(tmpdir, re.sub('\W','_', comment_annotation_list_id) + ".json") + assert os.path.exists(comment_annotation_list_path) == 1 + + with open(comment_annotation_list_path) as json_file: + comment_annotation_list = json.load(json_file) + assert comment_annotation_list['@id'] == comment_annotation_list_id + + def test_jekyll_site_export(self): + j = JekyllSiteExport(self.volume, 'v2', owners=[self.user.id]) + zip = j.get_zip() + tempdir = j.generate_website() + web_zip = j.website_zip() + # j.import_iiif_jekyll(j.manifest, j.jekyll_site_dir) + assert isinstance(zip, tempfile._TemporaryFileWrapper) + assert "%s_annotated_site_" % (str(self.volume.pk)) in zip.name + assert zip.name.endswith('.zip') + assert isinstance(web_zip, tempfile._TemporaryFileWrapper) + assert "%s_annotated_site_" % (str(self.volume.pk)) in web_zip.name + assert web_zip.name.endswith('.zip') + assert 'tmp-rdx-export' in tempdir + assert tempdir.endswith('/export') + tmpdir = tempfile.mkdtemp(prefix='tmp-rdx-export-') + jekyll_zip = zipfile.ZipFile(zip, "r") + jekyll_zip.extractall(tmpdir) + jekyll_dir = os.listdir(tmpdir)[0] + jekyll_path = os.path.join(tmpdir, jekyll_dir) + # verify the iiif export is embedded + iiif_path = os.path.join(jekyll_path, 'iiif_export') + manifest_path = os.path.join(iiif_path, 'manifest.json') + assert os.path.exists(manifest_path) + # verify page count is correct + assert len(os.listdir(os.path.join(jekyll_path, '_volume_pages'))) == 2 + # verify ocr annotation count is correct + with open(os.path.join(jekyll_path, '_volume_pages', '0000.html')) as page_file: + contents = page_file.read() + assert contents.count('ocr-line') == 6 + # verify user annotation count is correct + assert len(os.listdir(os.path.join(jekyll_path, '_annotations'))) == 1 + + + def test_manifest_export(self): + kwargs = { 'pid': self.volume.pid, 'version': 'v2' } + url = reverse('ManifestExport', kwargs=kwargs) + request = self.factory.post(url, kwargs=kwargs) + request.user = self.user + response = self.manifest_export_view(request, pid=self.volume.pid, version='v2') + assert isinstance(response.getvalue(), bytes) + + # Things I want to test: + # * Unzip the IIIF zip file + # * Verify the directory layout is correct + # * Open the manifest.json file + # * Verify that the otherContent annotation list matches the annotationlist filename + # * Verify that the annotationList filename matches the @id within the annotation + # * Verify the contents of the annotationList match the OCR (or the commenting annotation) + + + def test_jekyll_export_exclude_download(self): + kwargs = { 'pid': self.volume.pid, 'version': 'v2' } + url = reverse('JekyllExport', kwargs=kwargs) + kwargs['deep_zoom'] = 'exclude' + kwargs['mode'] = 'download' + request = self.factory.post(url, data=kwargs) + request.user = self.user + response = self.jekyll_export_view(request, pid=self.volume.pid, version='v2', content_type="application/x-www-form-urlencoded") + assert isinstance(response.getvalue(), bytes) + + def test_jekyll_export_include_download(self): + kwargs = { 'pid': self.volume.pid, 'version': 'v2' } + url = reverse('JekyllExport', kwargs=kwargs) + kwargs['deep_zoom'] = 'include' + kwargs['mode'] = 'download' + request = self.factory.post(url, data=kwargs) + request.user = self.user + response = self.jekyll_export_view(request, pid=self.volume.pid, version='v2', content_type="application/x-www-form-urlencoded") + assert isinstance(response.getvalue(), bytes) diff --git a/apps/readux/tests.py b/apps/readux/tests.py index 4b780df53..ffaf02de6 100644 --- a/apps/readux/tests.py +++ b/apps/readux/tests.py @@ -13,6 +13,7 @@ from .models import UserAnnotation from apps.readux.views import VolumesList, VolumeDetail, CollectionDetail, CollectionDetail, Collection, ExportOptions from urllib.parse import urlencode +from cssutils import parseString import json import re @@ -68,6 +69,10 @@ class AnnotationTests(TestCase): "format": "text/html", "@type": "dctypes:Text" }], + "stylesheet": { + "value": ".anno-049e4a47-1d9e-4d52-8d30-fb9047d34481 { background: rgba(0, 128, 0, 0.5); }", + "type": "CssStylesheet" + }, "on": [{ "full": "https://readux-dev.org:3000/iiif/readux:st7r6/canvas/fedora:emory:5622", "@type": "oa:SpecificResource", @@ -104,7 +109,7 @@ class AnnotationTests(TestCase): } def setUp(self): - fixtures = ['kollections.json', 'manifests.json', 'canvases.json', 'annotations.json'] + # fixtures = ['kollections.json', 'manifests.json', 'canvases.json', 'annotations.json'] self.user_a = get_user_model().objects.get(pk=1) self.user_b = get_user_model().objects.get(pk=2) self.factory = RequestFactory() @@ -368,3 +373,28 @@ def test_motivation_is_commeting_by_default(self): for anno in UserAnnotation.objects.all(): print(anno.motivation) assert anno.motivation == 'oa:commenting' + + def test_style_attribute_adds_id_to_class_selector(self): + self.create_user_annotations(1, self.user_a) + anno = UserAnnotation.objects.all().first() + assert str(anno.id) in anno.style + + def test_style_attribute_is_valid_css(self): + self.create_user_annotations(1, self.user_a) + anno = UserAnnotation.objects.all().first() + style = parseString(anno.style) + assert style.cssRules[0].valid + + def test_stylesheet_is_serialized(self): + self.create_user_annotations(1, self.user_a) + kwargs = {'username': self.user_a.username, 'volume': self.manifest.pid, 'canvas': self.canvas.pid} + url = reverse('user_annotations', kwargs=kwargs) + request = self.factory.get(url) + request.user = self.user_a + response = self.view(request, username=self.user_a.username, volume=self.manifest.pid, canvas=self.canvas.pid) + annotation = self.load_anno(response)[0] + assert 'stylesheet' in annotation + assert 'value' in annotation['stylesheet'] + assert 'type' in annotation['stylesheet'] + assert annotation['@id'] in annotation['stylesheet']['value'] + assert annotation['stylesheet']['type'] == 'CssStylesheet' \ No newline at end of file diff --git a/apps/readux/urls.py b/apps/readux/urls.py index 9faca3c12..dbfeca578 100644 --- a/apps/readux/urls.py +++ b/apps/readux/urls.py @@ -16,5 +16,6 @@ path('volume/