diff --git a/.coveragerc b/.coveragerc
index 199aac00..4ae91d22 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -13,4 +13,5 @@ exclude_lines =
from
import
logger
+ LOGGER
pragma: no cover
\ No newline at end of file
diff --git a/apps/iiif/annotations/admin.py b/apps/iiif/annotations/admin.py
index 2b71bea3..5ed1f5df 100644
--- a/apps/iiif/annotations/admin.py
+++ b/apps/iiif/annotations/admin.py
@@ -1,4 +1,4 @@
-"""Django admin module for `apps.iiif.annotations`"""
+"""Django admin module for :class:`apps.iiif.annotations`"""
from django.contrib import admin
from import_export import resources, fields
from import_export.admin import ImportExportModelAdmin
diff --git a/apps/iiif/annotations/apps.py b/apps/iiif/annotations/apps.py
index 956b3bf8..41bc0994 100644
--- a/apps/iiif/annotations/apps.py
+++ b/apps/iiif/annotations/apps.py
@@ -1,4 +1,4 @@
-"""Configuration for `apps.iiif.annotations`"""
+"""Configuration for :class:`apps.iiif.annotations`"""
from django.apps import AppConfig
class AnnotationsConfig(AppConfig):
diff --git a/apps/iiif/annotations/fixtures/annotations.json b/apps/iiif/annotations/fixtures/annotations.json
index f4cd5291..657fb06a 100644
--- a/apps/iiif/annotations/fixtures/annotations.json
+++ b/apps/iiif/annotations/fixtures/annotations.json
@@ -6,7 +6,7 @@
"y": 928,
"w": 22,
"h": 22,
- "order": 54,
+ "order": 1,
"content": "a",
"resource_type": "cnt:ContentAsText",
"motivation": "sc:painting",
@@ -147,4 +147,29 @@
},
"svg": ","
}
+},
+{
+ "model": "annotations.annotation",
+ "pk": "f846588c-1e1c-44d3-b1ce-20c0f6109dc5",
+ "fields": {
+ "x": 1146,
+ "y": 928,
+ "w": 22,
+ "h": 22,
+ "order": 1,
+ "content": ",",
+ "resource_type": "cnt:ContentAsText",
+ "motivation": "sc:painting",
+ "format": "text/plain",
+ "canvas": "a7f1bd69-766c-4dd4-ab66-f4051fdd4cff",
+ "language": "en",
+ "owner": null,
+ "oa_annotation": {
+ "annotatedBy": {
+ "name": "ocr"
+ },
+ "@id": "f846587c-1e1c-44d3-b1ce-20c0f7104dc5"
+ },
+ "svg": "stankonia"
+ }
}]
\ No newline at end of file
diff --git a/apps/iiif/annotations/models.py b/apps/iiif/annotations/models.py
index b465510b..ebe97f70 100644
--- a/apps/iiif/annotations/models.py
+++ b/apps/iiif/annotations/models.py
@@ -1,4 +1,4 @@
-"""Django models for `apps.iiif.annotations`"""
+"""Django models for :class:`apps.iiif.annotations`"""
from django.contrib.postgres.fields import JSONField
from django.db import models, IntegrityError
from django.conf import settings
diff --git a/apps/iiif/annotations/tests/tests.py b/apps/iiif/annotations/tests/tests.py
index b6cc4a28..241ac33e 100644
--- a/apps/iiif/annotations/tests/tests.py
+++ b/apps/iiif/annotations/tests/tests.py
@@ -1,5 +1,5 @@
# pylint: disable = missing-function-docstring, invalid-name, line-too-long
-"""Test cases for `apps.iiif.annotations`."""
+"""Test cases for :class:`apps.iiif.annotations`."""
from django.test import TestCase, Client
from django.test import RequestFactory
from django.conf import settings
diff --git a/apps/iiif/annotations/urls.py b/apps/iiif/annotations/urls.py
index 24432d2d..6d5aef7b 100644
--- a/apps/iiif/annotations/urls.py
+++ b/apps/iiif/annotations/urls.py
@@ -1,4 +1,4 @@
-"""Url patterns for `apps.iiif.annotations`"""
+"""Url patterns for :class:`apps.iiif.annotations`"""
from django.urls import path
from . import views
diff --git a/apps/iiif/annotations/views.py b/apps/iiif/annotations/views.py
index 93963622..9271940e 100644
--- a/apps/iiif/annotations/views.py
+++ b/apps/iiif/annotations/views.py
@@ -1,4 +1,4 @@
-"""Django views for `apps.iiif.annotations`"""
+"""Django views for :class:`apps.iiif.annotations`"""
import json
from django.views import View
from django.core.serializers import serialize
diff --git a/apps/iiif/canvases/management/commands/rebuild_ocr.py b/apps/iiif/canvases/management/commands/rebuild_ocr.py
index fce540d4..20048f9d 100644
--- a/apps/iiif/canvases/management/commands/rebuild_ocr.py
+++ b/apps/iiif/canvases/management/commands/rebuild_ocr.py
@@ -49,7 +49,7 @@ def handle(self, *args, **options):
)
elif options['canvas']:
try:
- canvas = Canvas.objects.get(pid=options['canvas'])
+ canvas = Canvas.objects.get(pid=options['canvas'])
self.__rebuild(canvas, options['testing'])
self.stdout.write(
@@ -107,7 +107,8 @@ def __rebuild(self, canvas, testing=False):
y=word['y'],
canvas=canvas,
owner=USER.objects.get(username='ocr'),
- resource_type=Annotation.OCR
+ resource_type=Annotation.OCR,
+ order=word_order
)
word_order += 1
anno.content = word['content']
diff --git a/apps/iiif/canvases/services.py b/apps/iiif/canvases/services.py
index c45761bb..008bec68 100644
--- a/apps/iiif/canvases/services.py
+++ b/apps/iiif/canvases/services.py
@@ -28,6 +28,44 @@ def get_fake_canvas_info(canvas):
response = fetch_url(canvas.service_id, timeout=settings.HTTP_REQUEST_TIMEOUT, format='json')
return response
+def get_fake_ocr():
+ return [
+ {
+ "h": 22,
+ "w": 22,
+ "x": 1146,
+ "y": 928,
+ "content": "Dope"
+ },
+ {
+ "h": 222,
+ "w": 222,
+ "x": 11462,
+ "y": 9282,
+ "content": ""
+ },
+ {
+ "h": 21,
+ "w": 21,
+ "x": 1141,
+ "y": 9281,
+ "content": "southernplayalisticadillacmuzik"
+ },
+ {
+ "h": 213,
+ "w": 213,
+ "x": 11413,
+ "y": 92813
+ },
+ {
+ "h": 214,
+ "w": 214,
+ "x": 11414,
+ "y": 92814,
+ "content": " "
+ }
+ ]
+
def get_ocr(canvas):
"""Function to determine method for fetching OCR for a canvas.
@@ -36,6 +74,8 @@ def get_ocr(canvas):
:return: List of dicts of parsed OCR data.
:rtype: list
"""
+ if 'fake.info' in canvas.IIIF_IMAGE_SERVER_BASE.IIIF_IMAGE_SERVER_BASE:
+ return get_fake_ocr()
if canvas.default_ocr == "line":
result = fetch_alto_ocr(canvas)
return add_alto_ocr(result)
@@ -178,8 +218,8 @@ def add_alto_ocr(result):
for zones in surface:
if 'zone' in zones.tag:
for line in zones:
- if line[-1].text is None:
- continue
+ # if line[-1].text is None:
+ # continue
ocr.append({
'content': line[-1].text,
'h': int(line.attrib['lry']) - int(line.attrib['uly']),
diff --git a/apps/iiif/canvases/tests/tests.py b/apps/iiif/canvases/tests/tests.py
index 983c6e47..11f940c0 100644
--- a/apps/iiif/canvases/tests/tests.py
+++ b/apps/iiif/canvases/tests/tests.py
@@ -1,9 +1,10 @@
"""
-Test cases for `apps.iiif.canvases`
+Test cases for :class:`apps.iiif.canvases`
"""
import json
from io import StringIO
import httpretty
+from bs4 import BeautifulSoup
from django.test import TestCase, Client
from django.urls import reverse
from django.core.management import call_command
@@ -29,7 +30,7 @@ def setUp(self):
def test_default_iiif_image_server_url(self):
i_server = IServer()
assert i_server.IIIF_IMAGE_SERVER_BASE == settings.IIIF_IMAGE_SERVER_BASE
-
+
def test_app_config(self):
assert CanvasesConfig.verbose_name == 'Canvases'
assert CanvasesConfig.name == 'apps.iiif.canvases'
@@ -94,7 +95,7 @@ def test_ia_ocr_creation(self):
def test_fedora_ocr_creation(self):
valid_fedora_positional_response = """523\t 116\t 151\t 45\tDistillery\r\n 704\t 117\t 148\t 52\tplaid,"\r\n""".encode('UTF-8-sig')
-
+
ocr = services.add_positional_ocr(self.canvas, valid_fedora_positional_response)
assert len(ocr) == 2
for word in ocr:
@@ -145,6 +146,9 @@ def test_line_by_line_from_alto(self):
assert ocr.x == 916
assert ocr.y == 0
+ for num, anno in enumerate(updated_canvas.annotation_set.all(), start=1):
+ assert anno.order == num
+
@httpretty.activate
def test_ocr_from_tsv(self):
tsv = """content\tx\ty\tw\th\nJordan\t459\t391\t89\t43\t\n\t453\t397\t397\t3\n \t1\t2\t3\t4\n"""
@@ -173,7 +177,7 @@ def test_from_bad_alto(self):
assert ocr is None
def test_canvas_detail(self):
- kwargs = { 'manifest': self.manifest.pid, 'pid': self.canvas.pid }
+ kwargs = {'manifest': self.manifest.pid, 'pid': self.canvas.pid}
url = reverse('RenderCanvasDetail', kwargs=kwargs)
response = self.client.get(url)
serialized_canvas = json.loads(response.content.decode('UTF-8-sig'))
@@ -211,7 +215,7 @@ def test_wide_image_crops(self):
assert canvas.thumbnail_crop_landscape == "%s/%s/pct:25,0,50,100/,250/0/default.jpg" % (canvas.IIIF_IMAGE_SERVER_BASE, pid)
assert canvas.thumbnail_crop_tallwide == "%s/%s/pct:5,5,90,90/250,/0/default.jpg" % (canvas.IIIF_IMAGE_SERVER_BASE, pid)
assert canvas.thumbnail_crop_volume == "%s/%s/pct:25,15,50,85/,600/0/default.jpg" % (canvas.IIIF_IMAGE_SERVER_BASE, pid)
-
+
def test_result_property(self):
assert self.canvas.result == "a retto , dio Quef\u00eca de'"
@@ -221,12 +225,18 @@ def test_get_image_info(self):
updated_canvas = Canvas.objects.get(pk=self.canvas.pk)
assert updated_canvas.image_info['height'] == 3000
assert updated_canvas.image_info['width'] == 3000
-
+
def test_command_output_rebuild_canvas(self):
out = StringIO()
call_command('rebuild_ocr', canvas=Canvas.objects.all().first().pid, stdout=out)
assert 'OCR rebuilt for canvas' in out.getvalue()
+ def test_command_output_rebuild_canvas_with_no_existing_annotations(self):
+ canvas = CanvasFactory.create(manifest=self.manifest)
+ out = StringIO()
+ call_command('rebuild_ocr', canvas=canvas.pid, stdout=out)
+ assert 'OCR rebuilt for canvas' in out.getvalue()
+
def test_command_output_rebuild_manifest(self):
out = StringIO()
call_command('rebuild_ocr', manifest=Manifest.objects.all().first().pid, stdout=out)
@@ -247,22 +257,69 @@ def test_command_output_rebuild_pid_not_given(self):
call_command('rebuild_ocr', stdout=out)
assert 'ERROR: your must provide a manifest or canvas pid' in out.getvalue()
- # def test_command_rebuild_ocr(self):
- # iiif_server = IServer.objects.get(IIIF_IMAGE_SERVER_BASE='https://images.readux.ecds.emory/')
- # self.canvas.IIIF_IMAGE_SERVER_BASE = iiif_server
- # self.canvas.save()
- # out = StringIO()
- # self.canvas.label = 'karl'
- # call_command('rebuild_ocr', canvas=self.canvas.pid, testing=True, stdout=out)
- # assert 'yup' in out.getvalue()
- # # ocr = canvas.annotation_set.all().first()
- # # assert ocr.h == 43
- # # assert ocr.w == 89
- # # assert ocr.x == 459
- # # assert ocr.y == 391
- # # assert 'Jordan' in ocr.content
- # # assert len(canvas.annotation_set.all()) == 1
-
+ def test_command_rebuild_ocr_canvas(self):
+ original_anno_count = self.canvas.annotation_set.all().count()
+ # Check the OCR attributes before rebuilding.
+ first_anno = self.canvas.annotation_set.all().first()
+ assert first_anno.h == 22
+ assert first_anno.w == 22
+ assert first_anno.x == 1146
+ assert first_anno.y == 928
+ original_span = BeautifulSoup(first_anno.content, 'html.parser')
+ assert 'Dope' not in original_span.string
+ assert original_span.span is not None
+ assert original_span.span.span is None
+ self.canvas.IIIF_IMAGE_SERVER_BASE = IServer.objects.get(
+ IIIF_IMAGE_SERVER_BASE='http://fake.info'
+ )
+ self.canvas.save()
+ out = StringIO()
+ call_command('rebuild_ocr', canvas=self.canvas.pid, testing=True, stdout=out)
+ assert 'OCR rebuilt for canvas' in out.getvalue()
+ ocr = self.canvas.annotation_set.all().first()
+ assert ocr.h == 22
+ assert ocr.w == 22
+ assert ocr.x == 1146
+ assert ocr.y == 928
+ new_span = BeautifulSoup(ocr.content, 'html.parser')
+ assert 'Dope' in new_span.string
+ assert original_span.string not in new_span.string
+ assert new_span.span is not None
+ assert new_span.span.span is None
+ assert len(self.canvas.annotation_set.all()) == original_anno_count + 1
+
+ def test_command_rebuild_ocr_manifest(self):
+ canvas = Canvas.objects.get(pk='a7f1bd69-766c-4dd4-ab66-f4051fdd4cff')
+ original_anno_count = canvas.annotation_set.all().count()
+ # Check the OCR attributes before rebuilding.
+ first_anno = canvas.annotation_set.all().first()
+ assert first_anno.h == 22
+ assert first_anno.w == 22
+ assert first_anno.x == 1146
+ assert first_anno.y == 928
+ original_span = BeautifulSoup(first_anno.content, 'html.parser')
+ assert 'southernplayalisticadillacmuzik' not in original_span.string
+ assert original_span.span is not None
+ assert original_span.span.span is None
+ canvas.IIIF_IMAGE_SERVER_BASE = IServer.objects.get(
+ IIIF_IMAGE_SERVER_BASE='http://fake.info'
+ )
+ canvas.save()
+ out = StringIO()
+ call_command('rebuild_ocr', manifest=canvas.manifest.pid, testing=True, stdout=out)
+ assert 'OCR rebuilt for manifest' in out.getvalue()
+ ocr = canvas.annotation_set.all().first()
+ assert ocr.h == 22
+ assert ocr.w == 22
+ assert ocr.x == 1146
+ assert ocr.y == 928
+ new_span = BeautifulSoup(ocr.content, 'html.parser')
+ assert 'Dope' in new_span.string
+ assert original_span.string not in new_span.string
+ assert new_span.span is not None
+ assert new_span.span.span is None
+ assert len(canvas.annotation_set.all()) == original_anno_count + 1
+
def test_no_alto_for_internet_archive(self):
iiif_server = IServer.objects.get(IIIF_IMAGE_SERVER_BASE='https://iiif.archivelab.org/iiif/')
canvas = CanvasFactory(IIIF_IMAGE_SERVER_BASE=iiif_server, manifest=self.canvas.manifest)
diff --git a/apps/iiif/canvases/urls.py b/apps/iiif/canvases/urls.py
index 37ee2b39..71c04620 100644
--- a/apps/iiif/canvases/urls.py
+++ b/apps/iiif/canvases/urls.py
@@ -1,5 +1,5 @@
"""
-URL patterns for `apps.iiif.canvases`
+URL patterns for :class:`apps.iiif.canvases`
"""
from django.urls import path
from .views import IIIFV2Detail, IIIFV2List
diff --git a/apps/iiif/canvases/views.py b/apps/iiif/canvases/views.py
index 0423f141..19bfe550 100644
--- a/apps/iiif/canvases/views.py
+++ b/apps/iiif/canvases/views.py
@@ -70,5 +70,6 @@ def get(self, request, *args, **kwargs): # pylint: disable = unused-argument
'canvas',
self.get_queryset()
)
- )
+ ),
+ safe=False
)
diff --git a/apps/iiif/kollections/tests/factories.py b/apps/iiif/kollections/tests/factories.py
index 6a1faf0b..193ec1bf 100644
--- a/apps/iiif/kollections/tests/factories.py
+++ b/apps/iiif/kollections/tests/factories.py
@@ -8,7 +8,7 @@
class CollectionFactory(DjangoModelFactory):
"""
- Factory for mocking `apps.iiif.kollections.models.Collection` objects.
+ Factory for mocking :class:`apps.iiif.kollections.models.Collection` objects.
"""
pid = str(random.randrange(2000, 5000))
label = Faker("name")
diff --git a/apps/iiif/kollections/tests/tests.py b/apps/iiif/kollections/tests/tests.py
index da1844de..ec0f4d0b 100644
--- a/apps/iiif/kollections/tests/tests.py
+++ b/apps/iiif/kollections/tests/tests.py
@@ -11,6 +11,7 @@
import config.settings.local as settings
from ..views import CollectionSitemap
from ..models import Collection
+from ..admin import CollectionAdmin, ManifestInline
from ...manifests.models import Manifest
class KollectionTests(TestCase):
@@ -168,3 +169,11 @@ def test_serialize_single_object(self):
collection = json.loads(serialize('kollection', [Collection.objects.all().first()]))
assert collection['@type'] == 'sc:Collection'
assert isinstance(collection, dict)
+
+ def test_collection_admin_inlines(self):
+ pid = Manifest.collections.through.objects.all().first().manifest.pid
+ admin_pid = CollectionAdmin.inlines[0].manifest_pid(
+ ManifestInline,
+ Manifest.collections.through.objects.all().first()
+ )
+ assert pid == admin_pid
diff --git a/apps/iiif/manifests/export.py b/apps/iiif/manifests/export.py
index 4a07ab1c..a83ddb3d 100644
--- a/apps/iiif/manifests/export.py
+++ b/apps/iiif/manifests/export.py
@@ -45,7 +45,7 @@ class IiifManifestExport:
:return: Return bytes containing the entire contents of the buffer.
:rtype: bytes
- """
+ """
@classmethod
def get_zip(self, manifest, version, owners=[]):
"""Generate zipfile of manifest.
@@ -58,7 +58,7 @@ def get_zip(self, manifest, version, owners=[]):
:type owners: list, optional
:return: Return bytes containing the entire contents of the buffer.
:rtype: bytes
- """
+ """
# zip_subdir = manifest.label
# zip_filename = "iiif_export.zip"
@@ -194,7 +194,7 @@ def __init__(self, manifest, version, page_one=None,
include_images=False, deep_zoom='hosted',
github_repo=None, owners=None, user=None):
"""Init JekyllSiteExport
-
+
:param manifest: Manifest to be exported
:type manifest: apps.iiif.manifests.models.Manifest
:param version: IIIF API version eg 'v2'
@@ -255,7 +255,7 @@ def notify_msg(self, msg):
# Why not just call `website_zip` directly?
def get_zip(self):
"""Get the zip file of the export.
-
+
:return: Exported site in zip file
:rtype: bytes
"""
@@ -513,7 +513,7 @@ def website_gitrepo(self):
# jekyll dir is *inside* the export directory;
# for the jekyll site to display correctly, we need to commit what
# is in the directory, not the directory itself
- jekyll_dir = self.edition_dir(export_dir)
+ jekyll_dir = self.edition_dir(export_dir)
# modify the jekyll config for relative url on github.io
config_file_path = os.path.join(jekyll_dir, '_config.yml')
@@ -576,8 +576,8 @@ def website_gitrepo(self):
# push local master to the gh-pages branch of the newly created repo,
# using the user's oauth token credentials
self.log_status('Pushing new content to GitHub')
- if self.is_testing is False:
- gitcmd.push([repo_url, 'master:gh-pages'])
+ if self.is_testing is False: # pragma: no cover
+ gitcmd.push([repo_url, 'master:gh-pages']) # pragma: no cover
# clean up temporary files after push to github
shutil.rmtree(export_dir)
@@ -621,7 +621,7 @@ def update_gitrepo(self):
repo.git.checkout('HEAD', b='gh-pages')
else:
repo = git.Repo.clone_from(auth_repo_url, tmpdir, branch='gh-pages')
- repo.remote().pull()
+ repo.remote().pull() # pragma: no cover
# create and switch to a new branch and switch to it; using datetime
# for uniqueness
git_branch_name = 'readux-update-%s' % \
@@ -677,7 +677,7 @@ def update_gitrepo(self):
self.import_iiif_jekyll(self.manifest, self.jekyll_site_dir)
# add any files that could be updated to the git index
- repo.index.add([
+ repo.index.add([ # pragma: no cover
'_config.yml', '_volume_pages/*', '_annotations/*',
'_data/tags.yml', 'tags/*', 'iiif_export/*'
])
@@ -697,7 +697,7 @@ def update_gitrepo(self):
if self.is_testing is False:
# push the update to a new branch on github
- repo.remotes.origin.push(
+ repo.remotes.origin.push( # pragma: no cover
'{b}s:{b}s'.format(b=git_branch_name)
)
# convert repo url to form needed to generate pull request
@@ -740,7 +740,7 @@ def github_export(self, user_email):
# making the request because the Head method is not implemented.
if self.is_testing is False and 'repo' not in self.github.oauth_scopes():
LOGGER.error('TODO: bad scope message')
- return None
+ return None # pragma: no cover
repo_url = None
ghpages_url = None
@@ -759,10 +759,10 @@ def github_export(self, user_email):
# update an existing github repository with new branch and
# a pull request
try:
- # TODO: How to highjack the request to
+ # TODO: How to highjack the request to
# https://58816:x-oauth-basic@github.com/zaphod/marx.git/ when testing.
if self.is_testing is False:
- pr_url = self.update_gitrepo()
+ pr_url = self.update_gitrepo() # pragma: no cover
else:
pr_url = 'https://github.com/{u}/{r}/pull/2'.format(
u=self.github_username,
diff --git a/apps/iiif/manifests/github.py b/apps/iiif/manifests/github.py
index 172768d3..337823fa 100644
--- a/apps/iiif/manifests/github.py
+++ b/apps/iiif/manifests/github.py
@@ -66,7 +66,7 @@ def connect_as_user(cls, user):
def oauth_scopes(self, test=False):
"""Get a list of scopes available for the current oauth token
-
+
:param test: Flag for if code is being executed for testing, defaults to False
:type test: bool, optional
:return: List of OAuth headers
@@ -77,7 +77,7 @@ def oauth_scopes(self, test=False):
if test:
response = self.session.get('%s/user' % self.url)
else:
- response = self.session.head('%s/user' % self.url)
+ response = self.session.head('%s/user' % self.url) # pragma: no cover
if response.status_code == requests.codes.ok:
return response.headers['x-oauth-scopes'].split(', ')
@@ -85,7 +85,7 @@ def oauth_scopes(self, test=False):
def create_repo(self, name, description=None, homepage=None):
"""Create a new user repository with the specified name.
-
+
:param name: Repo name
:type name: str
:param description: Repo description, defaults to None
@@ -110,7 +110,7 @@ def create_repo(self, name, description=None, homepage=None):
def list_repos(self, user):
"""Get a list of a repositories by person
-
+
:param user: GitHub username
:type user: str
:return: List of person's repositories.
@@ -190,4 +190,3 @@ def create_pull_request(self, repo, title, head, base, text=None):
pass
raise GithubApiException(error_message)
-
\ No newline at end of file
diff --git a/apps/iiif/serializers/annotation.py b/apps/iiif/serializers/annotation.py
index 06c3e4e6..828f7627 100644
--- a/apps/iiif/serializers/annotation.py
+++ b/apps/iiif/serializers/annotation.py
@@ -1,61 +1,35 @@
+# pylint: disable = attribute-defined-outside-init, too-few-public-methods
+"""Module for serializing IIIF Annotation"""
from django.core.serializers.base import SerializerDoesNotExist
-from django.core.serializers.json import Serializer as JSONSerializer
-from apps.readux.models import UserAnnotation
+from apps.iiif.serializers.base import Serializer as JSONSerializer
import config.settings.local as settings
class Serializer(JSONSerializer):
"""
- Serialize a :class:`apps.iiif.annotation.models.Annotation` object based on the IIIF Presentation API
+ Serialize a :class:`apps.iiif.annotation.models.Annotation`
+ object based on the IIIF Presentation API
IIIF V2 Annotation List https://iiif.io/api/presentation/2.1/#annotation-list
"""
def _init_options(self):
- """
- Initialize object with options
- """
super()._init_options()
- self.version = self.json_kwargs.pop('version', 'v2')
- self.is_list = self.json_kwargs.pop('is_list', False)
self.owners = self.json_kwargs.pop('owners', 0)
- def start_serialization(self):
- """
- Initialize the object and set the first character depending
- on if we are serailizing a single object or a list of objects.
- """
- self._init_options()
- if (self.is_list):
- self.stream.write('[')
- else:
- self.stream.write('')
-
- def end_serialization(self):
- """
- Set the last character depending on if we are serailizing a
- single object or a list of objects.
- """
- if (self.is_list):
- self.stream.write(']')
- else:
- self.stream.write('')
-
- def start_object(self, obj):
- super().start_object(obj)
-
def get_dump_object(self, obj):
"""
Serialize an :class:`apps.iiif.annotation.models.Annotation`
based on the IIIF presentation API
-
+
:param obj: Annotation to be serialized.
:type obj: :class:`apps.iiif.annotation.models.Annotation`
:return: Serialzed annotation.
:rtype: dict
- """
+ """
+ # TODO: Add more validation checks before trying to serialize.
if ((self.version == 'v2') or (self.version is None)):
name = 'OCR'
if obj.owner_id:
- name = obj.owner.username if "" == obj.owner.name else obj.owner.name
+ name = obj.owner.username if obj.owner.name == '' else obj.owner.name
data = {
"@context": "http://iiif.io/api/presentation/2/context.json",
"@id": str(obj.pk),
@@ -71,15 +45,29 @@ def get_dump_object(self, obj):
"language": obj.language
},
"on": {
- "full": "%s/iiif/%s/%s/canvas/%s" % (settings.HOSTNAME, self.version, obj.canvas.manifest.pid, obj.canvas.pid),
+ "full": '{h}/iiif/{v}/{m}/canvas/{c}'.format(
+ h=settings.HOSTNAME,
+ v=self.version,
+ m=obj.canvas.manifest.pid,
+ c=obj.canvas.pid
+ ),
"@type": "oa:SpecificResource",
"within": {
- "@id": "%s/iiif/%s/%s/manifest" % (settings.HOSTNAME, self.version, obj.canvas.manifest.pid),
+ "@id": '{h}/iiif/{v}/{c}/manifest'.format(
+ h=settings.HOSTNAME,
+ v=self.version,
+ c=obj.canvas.manifest.pid
+ ),
"@type": "sc:Manifest"
},
"selector": {
"@type": "oa:FragmentSelector",
- "value": "xywh=%s,%s,%s,%s" % (str(obj.x), str(obj.y), str(obj.w), str(obj.h))
+ "value": 'xywh={x},{y},{w},{h}'.format(
+ x=str(obj.x),
+ y=str(obj.y),
+ w=str(obj.w),
+ h=str(obj.h)
+ )
}
}
}
@@ -99,37 +87,39 @@ def get_dump_object(self, obj):
"@type": "oa:Tag",
"chars": tag.name
}
- data['resource'].append(wa_tag)
+ data['resource'].append(wa_tag) # pylint: disable= no-member
return data
+ return None
# TODO: write serializer for v3 of the IIIF Presentation API.
# elif (self.version == 'v3'):
# return None
- def handle_field(self, obj, field):
- super().handle_field(obj, field)
-
# TODO: is this needed?
@classmethod
- def __serialize_item(self, obj):
+ def __serialize_item(cls, obj):
return obj.item
-
+
@classmethod
- def __serialize_style(self, obj):
+ def __serialize_style(cls, obj):
"""
- Serialize the stylesheet data.
-
+ Private function to serialize the stylesheet data.
+
:param obj: Annotation to be serialized
:type obj: :class:`apps.iiif.annotation.models.Annotation`
:return: Stylesheet data compliant with the web annotation standard.
:rtype: dict
- """
+ """
return {
"type": "CssStylesheet",
"value": obj.style
}
class Deserializer:
+ """Deserialize IIIF Annotation
+
+ :raises SerializerDoesNotExist: Not yet implemented.
+ """
def __init__(self, *args, **kwargs):
- raise SerializerDoesNotExist("annotation is a serialization-only serializer")
\ No newline at end of file
+ raise SerializerDoesNotExist("annotation is a serialization-only serializer")
diff --git a/apps/iiif/serializers/annotation_list.py b/apps/iiif/serializers/annotation_list.py
index db8cdd15..ce512afc 100644
--- a/apps/iiif/serializers/annotation_list.py
+++ b/apps/iiif/serializers/annotation_list.py
@@ -1,12 +1,14 @@
+# pylint: disable = attribute-defined-outside-init, too-few-public-methods
+"""Module for serializing IIIF Annotation Lists"""
+import json
+from django.core.serializers import serialize
from django.core.serializers.base import SerializerDoesNotExist
-from django.core.serializers.json import Serializer as JSONSerializer
+from .base import Serializer as JSONSerializer
from django.contrib.auth import get_user_model
from django.db.models import Q
import config.settings.local as settings
-from django.core.serializers import serialize
-import json
-User = get_user_model()
+USER = get_user_model()
class Serializer(JSONSerializer):
"""
@@ -14,40 +16,36 @@ class Serializer(JSONSerializer):
"""
def _init_options(self):
super()._init_options()
- self.version = self.json_kwargs.pop('version', 'v2')
- self.is_list = self.json_kwargs.pop('is_list', False)
self.owners = self.json_kwargs.pop('owners', 0)
- def start_serialization(self):
- self._init_options()
- if (self.is_list):
- self.stream.write('[')
- else:
- self.stream.write('')
-
- def end_serialization(self):
- if (self.is_list):
- self.stream.write(']')
- else:
- self.stream.write('')
-
- def start_object(self, obj):
- super().start_object(obj)
-
def get_dump_object(self, obj):
- if ((self.version == 'v2') or (self.version is None)):
+ # TODO: Add more validation checks before trying to serialize.
+ if self.version == 'v2' or self.version is None:
data = {
"@context": "http://iiif.io/api/presentation/2/context.json",
- "@id": "%s/iiif/v2/%s/list/%s" % (settings.HOSTNAME, obj.manifest.pid, obj.pid),
+ "@id": '{h}/iiif/v2/{m}/list/{c}'.format(
+ h=settings.HOSTNAME,
+ m=obj.manifest.pid,
+ c=obj.pid
+ ),
"@type": "sc:AnnotationList",
- "resources": json.loads(serialize('annotation', obj.annotation_set.filter(Q(owner=User.objects.get(username='ocr')) | Q(owner__in=self.owners)), is_list=True))
+ "resources": json.loads(
+ serialize(
+ 'annotation',
+ obj.annotation_set.filter(
+ Q(owner=USER.objects.get(username='ocr')) |
+ Q(owner__in=self.owners)
+ ),
+ is_list=True)
+ )
}
return data
-
- def handle_field(self, obj, field):
- super().handle_field(obj, field)
-
+ return None
class Deserializer:
+ """Deserialize IIIF Annotation List
+
+ :raises SerializerDoesNotExist: Not yet implemented.
+ """
def __init__(self, *args, **kwargs):
- raise SerializerDoesNotExist("annotation_list is a serialization-only serializer")
\ No newline at end of file
+ raise SerializerDoesNotExist("annotation_list is a serialization-only serializer")
diff --git a/apps/iiif/serializers/base.py b/apps/iiif/serializers/base.py
new file mode 100644
index 00000000..1b088b13
--- /dev/null
+++ b/apps/iiif/serializers/base.py
@@ -0,0 +1,21 @@
+from django.core.serializers.json import Serializer as JSONSerializer
+
+class Serializer(JSONSerializer):
+ """Base Serializer Class"""
+ def _init_options(self):
+ super()._init_options()
+ self.version = self.json_kwargs.pop('version', 'v2')
+ self.is_list = self.json_kwargs.pop('is_list', False)
+
+ def start_serialization(self):
+ self._init_options()
+ if self.is_list:
+ self.stream.write('[')
+ else:
+ self.stream.write('')
+
+ def end_serialization(self):
+ if self.is_list:
+ self.stream.write(']')
+ else:
+ self.stream.write('')
\ No newline at end of file
diff --git a/apps/iiif/serializers/canvas.py b/apps/iiif/serializers/canvas.py
index 47142197..d39bba40 100644
--- a/apps/iiif/serializers/canvas.py
+++ b/apps/iiif/serializers/canvas.py
@@ -1,101 +1,35 @@
+# pylint: disable = attribute-defined-outside-init, too-few-public-methods
+"""Module for serializing IIIF Canvas"""
from django.core.serializers.base import SerializerDoesNotExist
-from django.core.serializers.json import Serializer as JSONSerializer
-from ...users.models import User
from django.urls import reverse
+from django.contrib.auth import get_user_model
import config.settings.local as settings
+from apps.iiif.serializers.base import Serializer as JSONSerializer
-
-"""
-V2
-{
- // Metadata about this canvas
- "@context": "http://iiif.io/api/presentation/2/context.json",
- "@id": "https://example.org/iiif/book1/canvas/p1",
- "@type": "sc:Canvas",
- "label": "p. 1",
- "height": 1000,
- "width": 750,
- "thumbnail" : {
- "@id" : "https://example.org/iiif/book1/canvas/p1/thumb.jpg",
- "@type": "dctypes:Image",
- "height": 200,
- "width": 150
- },
- "images": [
- {
- "@type": "oa:Annotation"
- // Link from Image to canvas should be included here, as below
- }
- ],
- "otherContent": [
- {
- // Reference to list of other Content resources, _not included directly_
- "@id": "https://example.org/iiif/book1/list/p1",
- "@type": "sc:AnnotationList"
- }
- ]
-
-}
-
-V3
-{
- // Metadata about this canvas
- "id": "https://example.org/iiif/book1/canvas/p1",
- "type": "Canvas",
- "label": { "@none": [ "p. 1" ] },
- "height": 1000,
- "width": 750,
-
- "items": [
- {
- "id": "https://example.org/iiif/book1/page/p1/1",
- "type": "AnnotationPage",
- "items": [
- // Content Annotations on the Canvas are included here
- ]
- }
- ]
-}
-"""
+USER = get_user_model()
class Serializer(JSONSerializer):
"""
Convert a queryset to IIIF Canvas
"""
- def _init_options(self):
- super()._init_options()
- self.version = self.json_kwargs.pop('version', 'v2')
- self.is_list = self.json_kwargs.pop('is_list', False)
-
- def start_serialization(self):
- self._init_options()
- if (self.is_list):
- self.stream.write('[')
- else:
- self.stream.write('')
-
- def end_serialization(self):
- if (self.is_list):
- self.stream.write(']')
- else:
- self.stream.write('')
-
- def start_object(self, obj):
- super().start_object(obj)
-
def get_dump_object(self, obj):
obj.label = str(obj.position)
if ((self.version == 'v2') or (self.version is None)):
- otherContent = [
- { "@id" : "%s/list/%s" % (obj.manifest.baseurl, obj.pid),
- "@type": "sc:AnnotationList",
- "label": "OCR Text" }
- ]
- for user in User.objects.filter(userannotation__canvas=obj).distinct():
+ otherContent = [ # pylint: disable=invalid-name
+ {
+ "@id": '{m}/list/{c}'.format(m=obj.manifest.baseurl, c=obj.pid),
+ "@type": "sc:AnnotationList",
+ "label": "OCR Text"
+ }
+ ]
+ for user in USER.objects.filter(userannotation__canvas=obj).distinct():
kwargs = {'username': user.username, 'volume': obj.manifest.pid, 'canvas': obj.pid}
- url = "{h}{k}".format(h=settings.HOSTNAME, k=reverse('user_annotations', kwargs=kwargs))
- user_endpoint = {
- "label": "Annotations by %s" % user.username,
+ url = "{h}{k}".format(
+ h=settings.HOSTNAME,
+ k=reverse('user_annotations', kwargs=kwargs)
+ )
+ user_endpoint = {
+ "label": 'Annotations by {u}'.format(u=user.username),
"@type": "sc:AnnotationList",
"@id": url
}
@@ -108,25 +42,25 @@ def get_dump_object(self, obj):
"height": obj.height,
"width": obj.width,
"images": [
- {
- "@context": "http://iiif.io/api/presentation/2/context.json",
- "@id": "%s" % (obj.anno_id),
- "@type": "oa:Annotation",
- "motivation": "sc:painting",
- "resource": {
- "@id": "%s/full/full/0/default.jpg" % (obj.service_id),
- "@type": "dctypes:Image",
- "format": "image/jpeg",
- "height": obj.height,
- "width": obj.width,
- "service": {
- "@context": "https://iiif.io/api/image/2/context.json",
- "@id": obj.service_id,
- "profile": "https://iiif.io/api/image/2/level2.json"
- }
- },
- "on": obj.identifier,
- }
+ {
+ "@context": "http://iiif.io/api/presentation/2/context.json",
+ "@id": str(obj.anno_id),
+ "@type": "oa:Annotation",
+ "motivation": "sc:painting",
+ "resource": {
+ "@id": '{id}/full/full/0/default.jpg'.format(id=obj.service_id),
+ "@type": "dctypes:Image",
+ "format": "image/jpeg",
+ "height": obj.height,
+ "width": obj.width,
+ "service": {
+ "@context": "https://iiif.io/api/image/2/context.json",
+ "@id": obj.service_id,
+ "profile": "https://iiif.io/api/image/2/level2.json"
+ }
+ },
+ "on": obj.identifier,
+ }
],
"thumbnail" : {
"@id" : obj.thumbnail,
@@ -136,11 +70,13 @@ def get_dump_object(self, obj):
"otherContent" : otherContent
}
return data
-
- def handle_field(self, obj, field):
- super().handle_field(obj, field)
-
+ # TODO: Should probably return a helpful error.
+ return None
class Deserializer:
+ """Deserialize IIIF Annotation List
+
+ :raises SerializerDoesNotExist: Not yet implemented.
+ """
def __init__(self, *args, **kwargs):
- raise SerializerDoesNotExist("canvas is a serialization-only serializer")
\ No newline at end of file
+ raise SerializerDoesNotExist("canvas is a serialization-only serializer")
diff --git a/apps/iiif/serializers/collection_manifest.py b/apps/iiif/serializers/collection_manifest.py
index a8ffc95a..c95e683b 100644
--- a/apps/iiif/serializers/collection_manifest.py
+++ b/apps/iiif/serializers/collection_manifest.py
@@ -1,44 +1,25 @@
+# pylint: disable = attribute-defined-outside-init, too-few-public-methods
+"""Module for serializing IIIF Collection Lists"""
from django.core.serializers.base import SerializerDoesNotExist
-from django.core.serializers.json import Serializer as JSONSerializer
import config.settings.local as settings
+from apps.iiif.serializers.base import Serializer as JSONSerializer
class Serializer(JSONSerializer):
- """
- """
- def _init_options(self):
- super()._init_options()
- self.version = self.json_kwargs.pop('version', 'v2')
- self.is_list = self.json_kwargs.pop('is_list', False)
-
- def start_serialization(self):
- self._init_options()
- if (self.is_list):
- self.stream.write('[')
- else:
- self.stream.write('')
-
- def end_serialization(self):
- if (self.is_list):
- self.stream.write(']')
- else:
- self.stream.write('')
-
- def start_object(self, obj):
- super().start_object(obj)
-
+ """IIIF Collection"""
def get_dump_object(self, obj):
if ((self.version == 'v2') or (self.version is None)):
- data = {
- "@id": "%s/iiif/%s/manifest" % (settings.HOSTNAME, obj.pid),
- "@type": "sc:Manifest",
- "label": obj.label,
+ data = {
+ "@id": '{h}/iiif/{p}/manifest'.format(h=settings.HOSTNAME, p=obj.pid),
+ "@type": "sc:Manifest",
+ "label": obj.label,
}
return data
-
- def handle_field(self, obj, field):
- super().handle_field(obj, field)
-
+ return None
class Deserializer:
+ """Deserialize IIIF Annotation List
+
+ :raises SerializerDoesNotExist: Not yet implemented.
+ """
def __init__(self, *args, **kwargs):
- raise SerializerDoesNotExist("collection_manifest is a serialization-only serializer")
\ No newline at end of file
+ raise SerializerDoesNotExist("collection_manifest is a serialization-only serializer")
diff --git a/apps/iiif/serializers/kollection.py b/apps/iiif/serializers/kollection.py
index 08edffc8..74ada99c 100644
--- a/apps/iiif/serializers/kollection.py
+++ b/apps/iiif/serializers/kollection.py
@@ -1,51 +1,44 @@
+# pylint: disable = attribute-defined-outside-init, too-few-public-methods
+"""Module for serializing IIIF Annotation Lists"""
+import json
from django.core.serializers.base import SerializerDoesNotExist
-from django.core.serializers.json import Serializer as JSONSerializer
-import config.settings.local as settings
from django.core.serializers import serialize
-import json
+import config.settings.local as settings
+from apps.iiif.serializers.base import Serializer as JSONSerializer
class Serializer(JSONSerializer):
"""
+ IIIF Collection
"""
- def _init_options(self):
- super()._init_options()
- self.version = self.json_kwargs.pop('version', 'v2')
- self.is_list = self.json_kwargs.pop('is_list', False)
-
- def start_serialization(self):
- self._init_options()
- if (self.is_list):
- self.stream.write('[')
- else:
- self.stream.write('')
-
- def end_serialization(self):
- if (self.is_list):
- self.stream.write(']')
- else:
- self.stream.write('')
-
- def start_object(self, obj):
- super().start_object(obj)
-
def get_dump_object(self, obj):
if ((self.version == 'v2') or (self.version is None)):
data = {
- "@context": "http://iiif.io/api/presentation/2/context.json",
- "@id": "%s/iiif/%s/%s/collection" % (settings.HOSTNAME, self.version, obj.pid),
- "@type": "sc:Collection",
- "label": obj.label,
- "viewingHint": "top",
- "description": obj.summary,
- "attribution": obj.attribution,
- "manifests": json.loads(serialize('collection_manifest', obj.manifests.all(), is_list=True))
+ "@context": "http://iiif.io/api/presentation/2/context.json",
+ "@id": '{h}/iiif/{v}/{p}/collection'.format(
+ h=settings.HOSTNAME,
+ v=self.version,
+ p=obj.pid
+ ),
+ "@type": "sc:Collection",
+ "label": obj.label,
+ "viewingHint": "top",
+ "description": obj.summary,
+ "attribution": obj.attribution,
+ "manifests": json.loads(
+ serialize(
+ 'collection_manifest',
+ obj.manifests.all(),
+ is_list=True
+ )
+ )
}
return data
-
- def handle_field(self, obj, field):
- super().handle_field(obj, field)
-
+ return None
class Deserializer:
+ """Deserialize IIIF Annotation List
+
+ :raises SerializerDoesNotExist: Not yet implemented.
+ """
def __init__(self, *args, **kwargs):
- raise SerializerDoesNotExist("kollection is a serialization-only serializer")
\ No newline at end of file
+ raise SerializerDoesNotExist("kollection is a serialization-only serializer")
diff --git a/apps/iiif/serializers/manifest.py b/apps/iiif/serializers/manifest.py
index b723e789..91aa2cb3 100644
--- a/apps/iiif/serializers/manifest.py
+++ b/apps/iiif/serializers/manifest.py
@@ -1,60 +1,11 @@
+# pylint: disable = attribute-defined-outside-init, too-few-public-methods
+"""Module for serializing IIIF Annotation Lists"""
import json
+from datetime import datetime
from django.core.serializers.base import SerializerDoesNotExist
-from django.core.serializers.json import Serializer as JSONSerializer
from django.core.serializers import serialize
from apps.iiif.canvases.models import Canvas
-"""
-V2
-{
- // Metadata about this canvas
- "@context": "http://iiif.io/api/presentation/2/context.json",
- "@id": 'https://example.org/iiif/%s/canvas/p1' % (obj.pid),
- "@type": "sc:Canvas",
- "label": "p. 1",
- "height": 1000,
- "width": 750,
- "thumbnail" : {
- "@id" : "https://example.org/iiif/book1/canvas/p1/thumb.jpg",
- "@type": "dctypes:Image",
- "height": 200,
- "width": 150
- },
- "images": [
- {
- "@type": "oa:Annotation"
- // Link from Image to canvas should be included here, as below
- }
- ],
- "otherContent": [
- {
- // Reference to list of other Content resources, _not included directly_
- "@id": "https://example.org/iiif/book1/list/p1",
- "@type": "sc:AnnotationList"
- }
- ]
-
-}
-
-V3
-{
- // Metadata about this canvas
- "id": "https://example.org/iiif/book1/canvas/p1",
- "type": "Canvas",
- "label": { "@none": [ "p. 1" ] },
- "height": 1000,
- "width": 750,
-
- "items": [
- {
- "id": "https://example.org/iiif/book1/page/p1/1",
- "type": "AnnotationPage",
- "items": [
- // Content Annotations on the Canvas are included here
- ]
- }
- ]
-}
-"""
+from apps.iiif.serializers.base import Serializer as JSONSerializer
class Serializer(JSONSerializer):
"""
@@ -62,9 +13,11 @@ class Serializer(JSONSerializer):
"""
def _init_options(self):
super()._init_options()
- self.version = self.json_kwargs.pop('version', 'v2')
- self.annotators = self.json_kwargs.pop('annotators')
- self.exportdate = self.json_kwargs.pop('exportdate')
+ self.annotators = self.json_kwargs.pop('annotators', 0)
+ # if 'exportdate' in self.json_kwargs:
+ self.exportdate = self.json_kwargs.pop('exportdate', datetime.utcnow())
+ # else:
+ # self.exportdate =
def start_serialization(self):
self._init_options()
@@ -73,97 +26,109 @@ def start_serialization(self):
def end_serialization(self):
self.stream.write('')
- def start_object(self, obj):
- super().start_object(obj)
-
def get_dump_object(self, obj):
- startpage = obj.canvas_set.all().filter(is_starting_page=True)
# TODO: Raise error if version is not v2 or v3
- if ((self.version == 'v2') or (self.version is None)):
- within = []
- for col in obj.collections.all():
- within.append(col.get_absolute_url())
- try:
- thumbnail = "%s/%s" % (obj.canvas_set.all().first().IIIF_IMAGE_SERVER_BASE, obj.canvas_set.all().get(is_starting_page=1).pid)
- except Canvas.MultipleObjectsReturned:
- thumbnail = "%s/%s" % (obj.canvas_set.all().first().IIIF_IMAGE_SERVER_BASE, obj.canvas_set.all().first().pid)
- data = {
- "@context": "http://iiif.io/api/presentation/2/context.json",
- "@id": "%s/manifest" % (obj.baseurl),
- "@type": "sc:Manifest",
- "label": obj.label,
- "metadata": [{
- "label": "Author",
- "value": obj.author
- },
- {
- "label": "Publisher",
- "value": obj.publisher
- },
- {
- "label": "Place of Publication",
- "value": obj.published_city
- },
- {
- "label": "Publication Date",
- "value": obj.published_date
- },
- {
- "label": "Notes",
- "value": obj.metadata
- },
- {
- "label": "Record Created",
- "value": obj.created_at
- },
- {
- "label": "Edition Type",
- "value": "Readux IIIF Exported Edition"
- },
- {
- "label": "About Readux",
- "value": "https://readux.ecdsdev.org/about/"
- },
- {
- "label": "Annotators",
- "value": self.annotators
- },
- {
- "label": "Export Date",
- "value": self.exportdate
- }],
- "description": obj.summary,
- "related": [obj.get_volume_url()],
- "within": within,
- "thumbnail": {
- "@id": thumbnail + "/full/600,/0/default.jpg",
- "service": {
- "@context": "http://iiif.io/api/image/2/context.json",
- "@id": thumbnail,
- "profile": "http://iiif.io/api/image/2/level1.json"
- }
- },
- "attribution": obj.attribution,
- "logo": obj.thumbnail_logo,
- "license": obj.license,
- "viewingDirection": obj.viewingDirection,
- "viewingHint": "paged",
- "sequences": [
- {
- "@id": "%s/sequence/normal" % (obj.baseurl),
- "@type": "sc:Sequence",
- "label": "Current Page Order",
- "startCanvas": obj.start_canvas,
- "canvases": json.loads(serialize('canvas', obj.canvas_set.all(), is_list=True))
- }
- ]
- }
- return data
-
- def handle_field(self, obj, field):
- super().handle_field(obj, field)
+ if self.version == 'v2' or self.version is None:
+ within = []
+ for col in obj.collections.all():
+ within.append(col.get_absolute_url())
+ try:
+ thumbnail = '{h}/{p}'.format(
+ h=obj.canvas_set.all().first().IIIF_IMAGE_SERVER_BASE,
+ p=obj.canvas_set.all().get(is_starting_page=1).pid
+ )
+ except Canvas.MultipleObjectsReturned:
+ thumbnail = '{h}/{p}'.format(
+ h=obj.canvas_set.all().first().IIIF_IMAGE_SERVER_BASE,
+ p=obj.canvas_set.all().first().pid
+ )
+ data = {
+ "@context": "http://iiif.io/api/presentation/2/context.json",
+ "@id": "%s/manifest" % (obj.baseurl),
+ "@type": "sc:Manifest",
+ "label": obj.label,
+ "metadata": [
+ {
+ "label": "Author",
+ "value": obj.author
+ },
+ {
+ "label": "Publisher",
+ "value": obj.publisher
+ },
+ {
+ "label": "Place of Publication",
+ "value": obj.published_city
+ },
+ {
+ "label": "Publication Date",
+ "value": obj.published_date
+ },
+ {
+ "label": "Notes",
+ "value": obj.metadata
+ },
+ {
+ "label": "Record Created",
+ "value": obj.created_at
+ },
+ {
+ "label": "Edition Type",
+ "value": "Readux IIIF Exported Edition"
+ },
+ {
+ "label": "About Readux",
+ "value": "https://readux.ecdsdev.org/about/"
+ },
+ {
+ "label": "Annotators",
+ "value": self.annotators
+ },
+ {
+ "label": "Export Date",
+ "value": self.exportdate
+ }
+ ],
+ "description": obj.summary,
+ "related": [obj.get_volume_url()],
+ "within": within,
+ "thumbnail": {
+ "@id": thumbnail + "/full/600,/0/default.jpg",
+ "service": {
+ "@context": "http://iiif.io/api/image/2/context.json",
+ "@id": thumbnail,
+ "profile": "http://iiif.io/api/image/2/level1.json"
+ }
+ },
+ "attribution": obj.attribution,
+ "logo": obj.thumbnail_logo,
+ "license": obj.license,
+ "viewingDirection": obj.viewingDirection,
+ "viewingHint": "paged",
+ "sequences": [
+ {
+ "@id": "%s/sequence/normal" % (obj.baseurl),
+ "@type": "sc:Sequence",
+ "label": "Current Page Order",
+ "startCanvas": obj.start_canvas,
+ "canvases": json.loads(
+ serialize(
+ 'canvas',
+ obj.canvas_set.all(),
+ is_list=True
+ )
+ )
+ }
+ ]
+ }
+ return data
+ return None
class Deserializer:
+ """Deserialize IIIF Annotation List
+
+ :raises SerializerDoesNotExist: Not yet implemented.
+ """
def __init__(self, *args, **kwargs):
raise SerializerDoesNotExist("manifest is a serialization-only serializer")
diff --git a/apps/iiif/serializers/tests.py b/apps/iiif/serializers/tests.py
index f4ee4c21..a4ee722b 100644
--- a/apps/iiif/serializers/tests.py
+++ b/apps/iiif/serializers/tests.py
@@ -1,12 +1,38 @@
+"""Test Module for IIIF Serializers"""
from django.test import TestCase
from django.core.serializers import serialize, deserialize, SerializerDoesNotExist
+from apps.iiif.canvases.models import Canvas
class SerializerTests(TestCase):
- serializers = ['annotation_list', 'annotation', 'canvas', 'collection_manifest', 'kollection', 'manifest', 'user_annotation_list']
+ serializers = [
+ 'annotation_list', 'annotation', 'canvas',
+ 'collection_manifest', 'kollection',
+ 'manifest', 'user_annotation_list'
+ ]
+
+ fixtures = [
+ 'users.json',
+ 'kollections.json',
+ 'manifests.json',
+ 'canvases.json',
+ 'annotations.json'
+ ]
+
def test_deserialization(self):
+ """Deserialization should raise for serialization only error."""
for serializer in self.serializers:
try:
deserialize(serializer, {})
except SerializerDoesNotExist as error:
assert str(error) == "'{s} is a serialization-only serializer'".format(s=serializer)
+
+ def test_empty_object(self):
+ """If specified version is not implemented, serializer returns an empty dict."""
+ for serializer in self.serializers:
+ obj = serialize(
+ serializer,
+ Canvas.objects.all(),
+ version='Some Random Version'
+ )
+ assert 'null' in obj
diff --git a/apps/iiif/serializers/user_annotation_list.py b/apps/iiif/serializers/user_annotation_list.py
index a78084e7..6073726b 100644
--- a/apps/iiif/serializers/user_annotation_list.py
+++ b/apps/iiif/serializers/user_annotation_list.py
@@ -1,51 +1,44 @@
+# pylint: disable = attribute-defined-outside-init, too-few-public-methods
+"""Module for serializing IIIF User Annotation Lists"""
+import json
from django.core.serializers.base import SerializerDoesNotExist
-from django.core.serializers.json import Serializer as JSONSerializer
-from django.db.models import Q
-import config.settings.local as settings
from django.core.serializers import serialize
-import json
-from apps.users.models import User
+from apps.iiif.serializers.annotation_list import Serializer as IIIFAnnotationListSerializer
+import config.settings.local as settings
-class Serializer(JSONSerializer):
+class Serializer(IIIFAnnotationListSerializer):
"""
IIIF V2 Annotation List https://iiif.io/api/presentation/2.1/#annotation-list
"""
- def _init_options(self):
- super()._init_options()
- self.version = self.json_kwargs.pop('version', 'v2')
- self.is_list = self.json_kwargs.pop('is_list', False)
- self.owners = self.json_kwargs.pop('owners', 0)
-
- def start_serialization(self):
- self._init_options()
- if (self.is_list):
- self.stream.write('[')
- else:
- self.stream.write('')
-
- def end_serialization(self):
- if (self.is_list):
- self.stream.write(']')
- else:
- self.stream.write('')
-
- def start_object(self, obj):
- super().start_object(obj)
def get_dump_object(self, obj):
if ((self.version == 'v2') or (self.version is None)):
data = {
"@context": "http://iiif.io/api/presentation/2/context.json",
- "@id": "%s/annotations/%s/%s/list/%s" % (settings.HOSTNAME, self.owners[0].username, obj.manifest.pid, obj.pid),
+ "@id": '{h}/annotations/{u}/{m}/list/{c}'.format(
+ h=settings.HOSTNAME,
+ u=self.owners[0].username,
+ m=obj.manifest.pid,
+ c=obj.pid
+ ),
"@type": "sc:AnnotationList",
- "resources": json.loads(serialize('annotation', obj.userannotation_set.filter(owner__in=[self.owners[0].id]), is_list=True))
+ "resources": json.loads(
+ serialize(
+ 'annotation',
+ obj.userannotation_set.filter(
+ owner__in=[self.owners[0].id]
+ ),
+ is_list=True
+ )
+ )
}
return data
-
- def handle_field(self, obj, field):
- super().handle_field(obj, field)
-
+ return None
class Deserializer:
+ """Deserialize IIIF Annotation List
+
+ :raises SerializerDoesNotExist: Not yet implemented.
+ """
def __init__(self, *args, **kwargs):
- raise SerializerDoesNotExist("user_annotation_list is a serialization-only serializer")
\ No newline at end of file
+ raise SerializerDoesNotExist("user_annotation_list is a serialization-only serializer")
diff --git a/apps/readux/admin.py b/apps/readux/admin.py
index da8dd16c..3e39df0d 100644
--- a/apps/readux/admin.py
+++ b/apps/readux/admin.py
@@ -1,14 +1,13 @@
+"""Django Admin module for Readux."""
from django.contrib import admin
-
from import_export import resources, fields
from import_export.admin import ImportExportModelAdmin
-from import_export.widgets import ForeignKeyWidget, ManyToManyWidget, JSONWidget
+from import_export.widgets import ForeignKeyWidget, JSONWidget
from apps.readux.models import UserAnnotation
-from apps.iiif.annotations.models import Annotation
from apps.iiif.canvases.models import Canvas
-import json
class UserAnnotationResource(resources.ModelResource):
+ """Django Admin Model Resource for UserAnnotation:"""
canvas_link = fields.Field(
column_name='canvas',
attribute='canvas',
@@ -19,13 +18,17 @@ class UserAnnotationResource(resources.ModelResource):
widget=JSONWidget)
class Meta: # pylint: disable=too-few-public-methods, missing-class-docstring
model = UserAnnotation
- fields = ('id', 'x','y','w','h','order','content','resource_type','motivation','format','canvas_link', 'language', 'oa_annotation')
+ fields = (
+ 'id', 'x', 'y', 'w', 'h', 'order', 'content',
+ 'resource_type', 'motivation', 'format',
+ 'canvas_link', 'language', 'oa_annotation'
+ )
class UserAnnotationAdmin(ImportExportModelAdmin, admin.ModelAdmin):
+ """Django Admin configuration for UserAnnotation"""
resource_class = UserAnnotationResource
- pass
list_display = ('id', 'canvas', 'order', 'content', 'x', 'y', 'w', 'h')
- search_fields = ('content','oa_annotation')
-
-admin.site.register(UserAnnotation, UserAnnotationAdmin)
\ No newline at end of file
+ search_fields = ('content', 'oa_annotation')
+
+admin.site.register(UserAnnotation, UserAnnotationAdmin)
diff --git a/apps/readux/annotations.py b/apps/readux/annotations.py
index 5085e18c..f357826e 100644
--- a/apps/readux/annotations.py
+++ b/apps/readux/annotations.py
@@ -1,23 +1,22 @@
+"""Django Views for USER Annotations."""
+import json
+import uuid
from django.core.exceptions import ObjectDoesNotExist
from django.core.serializers import serialize
from django.http import JsonResponse
from django.views import View
from django.views.generic import ListView
from django.contrib.auth import get_user_model
-from .models import UserAnnotation
from apps.iiif.canvases.models import Canvas
from apps.iiif.canvases.models import Manifest
-import json
-import uuid
+from .models import UserAnnotation
-User = get_user_model()
+USER = get_user_model()
class Annotations(ListView):
"""
Display a list of UserAnnotations for a specific user.
- Returns
- -------
- json
+ :rtype: json
"""
def get_queryset(self):
return Canvas.objects.filter(pid=self.kwargs['canvas'])
@@ -25,7 +24,7 @@ def get_queryset(self):
def get(self, request, *args, **kwargs):
username = kwargs['username']
try:
- owner = User.objects.get(username=username)
+ owner = USER.objects.get(username=username)
if self.request.user == owner:
return JsonResponse(
json.loads(
@@ -37,26 +36,30 @@ def get(self, request, *args, **kwargs):
),
safe=False
)
- return JsonResponse(status=401, data={"Permission to see annotations not allowed for logged in user.": username})
+ return JsonResponse(
+ status=401,
+ data={"Permission to see annotations not allowed for logged in user.": username}
+ )
except ObjectDoesNotExist:
- # attempt to get annotations for non-existent user
- return JsonResponse(status=404, data={"User not found.": username})
- # return JsonResponse(status=200, data={})
-
+ return JsonResponse(status=404, data={"USER not found.": username})
class AnnotationCrud(View):
-
+ """Endpoint for User Annotation CRUD."""
def dispatch(self, request, *args, **kwargs):
# Don't do anything if no user is authenticated.
if hasattr(request, 'user') is False or request.user.is_authenticated is False:
return self.__unauthorized()
-
+
# Get the payload from the request body.
self.payload = json.loads(self.request.body.decode('utf-8'))
- return super(AnnotationCrud, self).dispatch(request, *args, **kwargs)
+ return super(AnnotationCrud, self).dispatch(request, *args, **kwargs)
def get_queryset(self):
+ """Fetch requested :class:`apps.readux.models.UserAnnotation`
+
+ :rtype: :class:`django.db.models.QuerySet`
+ """
try:
return UserAnnotation.objects.get(
pk=self.payload['id']
@@ -65,6 +68,11 @@ def get_queryset(self):
return None
def post(self, request):
+ """HTTP POST endpoint for creating annotations.
+
+ :return: Newly created annotation as IIIF Annotation.
+ :rtype: json
+ """
oa_annotation = json.loads(self.payload['oa_annotation'])
annotation = UserAnnotation()
annotation.oa_annotation = oa_annotation
@@ -84,6 +92,11 @@ def post(self, request):
)
def put(self, request):
+ """HTTP PUT endpoint for updating annotations.
+
+ :return: Updated IIIF Annotation
+ :rtype: json
+ """
# if hasattr(request, 'user') is False or request.user.is_authenticated is False:
# return self.__unauthorized()
diff --git a/apps/readux/apps.py b/apps/readux/apps.py
index d9b2a481..2bbad366 100644
--- a/apps/readux/apps.py
+++ b/apps/readux/apps.py
@@ -1,5 +1,6 @@
+"""Django app configuration for Readux"""
from django.apps import AppConfig
-
class ReaduxConfig(AppConfig):
+ """Configuration for Readux Django app"""
name = 'apps.readux'
diff --git a/apps/readux/models.py b/apps/readux/models.py
index 5106f170..5a4d01eb 100644
--- a/apps/readux/models.py
+++ b/apps/readux/models.py
@@ -1,17 +1,20 @@
+"""Django Models for Readux"""
+import json
+import re
+from taggit.managers import TaggableManager
+from taggit.models import TaggedItemBase
from django.db import models
-from apps.iiif.annotations.models import AbstractAnnotation, Annotation
from django.db.models import signals
from django.dispatch import receiver
+from apps.iiif.annotations.models import AbstractAnnotation, Annotation
from apps.iiif.canvases.models import Canvas
-from taggit.managers import TaggableManager
-from taggit.models import TaggedItemBase
-import json
-import re
class TaggedUserAnnotations(TaggedItemBase):
+ """Model for tagging :class:`UserAnnotation`s using Django Taggit."""
content_object = models.ForeignKey('UserAnnotation', on_delete=models.CASCADE)
class UserAnnotation(AbstractAnnotation):
+ """Model for User Annotations."""
COMMENTING = 'oa:commenting'
PAINTING = 'sc:painting'
TAGGING = '%s,oa:tagging' % COMMENTING
@@ -21,8 +24,22 @@ class UserAnnotation(AbstractAnnotation):
(TAGGING, 'tagging and commenting')
)
- start_selector = models.ForeignKey(Annotation, on_delete=models.CASCADE, null=True, blank=True, related_name='start_selector', default=None)
- end_selector = models.ForeignKey(Annotation, on_delete=models.CASCADE, null=True, blank=True, related_name='end_selector', default=None)
+ start_selector = models.ForeignKey(
+ Annotation,
+ on_delete=models.CASCADE,
+ null=True,
+ blank=True,
+ related_name='start_selector',
+ default=None
+ )
+ end_selector = models.ForeignKey(
+ Annotation,
+ on_delete=models.CASCADE,
+ null=True,
+ blank=True,
+ related_name='end_selector',
+ default=None
+ )
start_offset = models.IntegerField(null=True, blank=True, default=None)
end_offset = models.IntegerField(null=True, blank=True, default=None)
tags = TaggableManager(through=TaggedUserAnnotations)
@@ -43,20 +60,10 @@ def tag_list(self):
else:
return []
- # def save(self, *args, **kwargs):
- # if self.oa_annotation is not None:
- # self.parse_mirador_annotation()
- # super().save(*args, **kwargs)
-
def parse_mirador_annotation(self):
- # TODO: Should we use multiple motivations?
- # if(type(self.oa_annotation.resource), list):
- # self.motivation = self.TAGGING
- # else:
- # self.motivation = AbstractAnnotation.COMMENTING
self.motivation = AbstractAnnotation.COMMENTING
- if (type(self.oa_annotation) == str):
+ if type(self.oa_annotation) == str:
self.oa_annotation = json.loads(self.oa_annotation)
if isinstance(self.oa_annotation['on'], list):
@@ -69,13 +76,17 @@ def parse_mirador_annotation(self):
mirador_item = anno_on['selector']['item']
- if (mirador_item['@type'] == 'oa:SvgSelector'):
+ if mirador_item['@type'] == 'oa:SvgSelector':
self.svg = mirador_item['value']
self.__set_xywh_svg_anno()
- elif (mirador_item['@type'] == 'RangeSelector'):
- self.start_selector = Annotation.objects.get(pk=mirador_item['startSelector']['value'].split("'")[1])
- self.end_selector = Annotation.objects.get(pk=mirador_item['endSelector']['value'].split("'")[1])
+ elif mirador_item['@type'] == 'RangeSelector':
+ self.start_selector = Annotation.objects.get(
+ pk=mirador_item['startSelector']['value'].split("'")[1]
+ )
+ self.end_selector = Annotation.objects.get(
+ pk=mirador_item['endSelector']['value'].split("'")[1]
+ )
self.start_offset = mirador_item['startSelector']['refinedBy']['start']
self.end_offset = mirador_item['endSelector']['refinedBy']['end']
self.__set_xywh_text_anno()
@@ -89,21 +100,30 @@ def parse_mirador_annotation(self):
# Assume all tags have been removed.
if self.tags.exists():
self.tags.clear()
- elif isinstance(self.oa_annotation['resource'], list) and len(self.oa_annotation['resource']) > 1:
+ elif (
+ isinstance(self.oa_annotation['resource'], list) and
+ len(self.oa_annotation['resource']) > 1
+ ):
# Assume tagging
self.motivation = self.TAGGING
- text = [resource for resource in self.oa_annotation['resource'] if resource['@type'] == 'dctypes:Text']
- # if len(text) > 0:
+ text = [res for res in self.oa_annotation['resource'] if res['@type'] == 'dctypes:Text']
self.content = text[0]['chars']
-
+
# 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}')
+ 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'])
return True
def __is_text_annotation(self):
+ """Check if annotation is for text.
+
+ :return: True if annotation is for text.
+ :rtype: bool
+ """
return all([
isinstance(self.end_offset, int),
isinstance(self.start_offset, int),
@@ -112,11 +132,15 @@ def __is_text_annotation(self):
])
def __is_svg_annotation(self):
+ """Check if annotation is for image region.
+
+ :return: True if annotation is for image region.
+ :rtype: bool
+ """
return self.svg is not None
+ # pylint: disable = invalid-name
def __set_xywh_text_anno(self):
- # if (self.__is_text_annotation() is None):
- # return None
start_position = self.start_selector.order
end_position = self.end_selector.order
text = Annotation.objects.filter(
@@ -128,6 +152,7 @@ def __set_xywh_text_anno(self):
self.y = max(text.values_list('y', flat=True))
self.h = max(text.values_list('h', flat=True))
self.w = text.last().x + text.last().w - self.x
+ # pylint: enable = invalid-name
def __text_anno_item(self):
return dict({
@@ -164,7 +189,7 @@ def __svg_anno_item(self):
def __set_xywh_svg_anno(self):
dimensions = None
if 'default' in self.oa_annotation['on'][0]['selector'].keys():
- dimensions = self.oa_annotation['on'][0]['selector']['default']['value'].split('=')[-1].split(',')
+ dimensions = self.oa_annotation['on'][0]['selector']['default']['value'].split('=')[-1].split(',') # pylint: disable = line-too-long
if dimensions is not None:
self.x = dimensions[0]
self.y = dimensions[1]
@@ -179,16 +204,20 @@ def parse_payload(sender, instance, **kwargs):
@receiver(signals.post_save, sender=UserAnnotation)
def set_tags(sender, instance, **kwargs):
+ """
+ Finds tags in the oa_annotation and applies them to
+ the annotation.
+ """
if instance.motivation == sender.TAGGING:
incoming_tags = []
# Get the tags from the incoming annotation.
- tags = [resource for resource in instance.oa_annotation['resource'] if resource['@type'] == 'oa:Tag']
+ tags = [res for res in instance.oa_annotation['resource'] if res['@type'] == 'oa:Tag']
for tag in tags:
# Add the tag to the annotation
instance.tags.add(tag['chars'])
# Make a list of incoming tags to compare with list of saved tags.
incoming_tags.append(tag['chars'])
-
+
# Check if any tags have been removed
if len(instance.tag_list) > 0:
for existing_tag in instance.tag_list:
diff --git a/apps/readux/tests/test_export.py b/apps/readux/tests/test_export.py
index 47626552..bfb3dd7d 100644
--- a/apps/readux/tests/test_export.py
+++ b/apps/readux/tests/test_export.py
@@ -149,8 +149,8 @@ def test_jekyll_export_exclude_download(self):
url = reverse('JekyllExport', kwargs=kwargs)
kwargs['deep_zoom'] = 'exclude'
kwargs['mode'] = 'download'
- request = self.factory.post(url, data=kwargs)
- request.user = self.user
+ 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)
@@ -186,12 +186,12 @@ def test_jekyll_export_to_github(self):
content_type="application/x-www-form-urlencoded"
)
assert response.status_code == 200
-
+
def test_use_github(self):
assert isinstance(self.jse.github, GithubApi)
assert self.jse.github_username == self.sa_acct.extra_data['login']
assert self.jse.github_token == self.sa_token.token
-
+
def test_github_auth_repo_given_name(self):
auth_repo = self.jse.github_auth_repo(repo_name=self.jse.github_repo)
assert auth_repo == "https://{t}:x-oauth-basic@github.com/{u}/{r}.git".format(t=self.sa_token.token, u=self.jse.github_username, r=self.jse.github_repo)
@@ -199,7 +199,7 @@ def test_github_auth_repo_given_name(self):
def test_github_auth_repo_given_url(self):
auth_repo = self.jse.github_auth_repo(repo_url='https://github.com/karl/{r}'.format(r=self.jse.github_repo))
assert auth_repo == "https://{t}:x-oauth-basic@github.com/karl/{r}.git".format(t=self.sa_token.token, r=self.jse.github_repo)
-
+
@httpretty.activate
def test_github_exists(self):
resp_body = '[{"name":"marx"}]'
@@ -255,7 +255,7 @@ def test_website_github_repo(self):
)
website = self.jse.website_gitrepo()
assert website == ('https://github.com/{u}/{r}'.format(u=self.jse.github_username, r=self.jse.github_repo), 'https://{u}.github.io/{r}/'.format(u=self.jse.github_username, r=self.jse.github_repo))
-
+
@httpretty.activate
def test_update_githubrepo(self):
httpretty.register_uri(
@@ -326,8 +326,11 @@ def test_github_export_update(self):
'https://{u}.github.io/{r}/'.format(u=self.jse.github_username, r=self.jse.github_repo),
'https://github.com/{u}/{r}/pull/2'.format(u=self.jse.github_username, r=self.jse.github_repo)
]
-
+
def test_download_export(self):
self.user.email = 'karl@marx.org'
download = self.jse.download_export(self.user.email, self.volume)
- assert download.endswith('.zip')
\ No newline at end of file
+ assert download.endswith('.zip')
+
+ def test_notify_message(self):
+ self.jse.notify_msg('hey')
\ No newline at end of file
diff --git a/apps/readux/tests/tests.py b/apps/readux/tests/tests.py
index ce559576..64e3a6c0 100644
--- a/apps/readux/tests/tests.py
+++ b/apps/readux/tests/tests.py
@@ -373,9 +373,9 @@ def test_delete_annotation_unauthenticated(self):
def test_user_annotations_on_canvas(self):
# fetch a manifest with no user annotations
- kwargs = { 'manifest': self.manifest.pid, 'pid': self.canvas.pid }
+ kwargs = {'manifest': self.manifest.pid, 'pid': self.canvas.pid}
url = reverse('RenderCanvasDetail', kwargs=kwargs)
- response = self.client.get(url)
+ response = self.client.get(url, data=kwargs)
serialized_canvas = json.loads(response.content.decode('UTF-8-sig'))
assert len(serialized_canvas['otherContent']) == 1
@@ -394,13 +394,13 @@ def test_user_annotations_on_canvas(self):
assert serialized_canvas['@id'] == self.canvas.identifier
assert serialized_canvas['label'] == str(self.canvas.position)
assert len(serialized_canvas['otherContent']) == 3
-
+
def test_volume_list_view_no_kwargs(self):
response = self.client.get(reverse('volumes list'))
context = response.context_data
assert context['order_url_params'] == urlencode({'sort': 'title', 'order': 'asc'})
assert context['object_list'].count() == Manifest.objects.all().count()
-
+
def test_volume_list_invalid_kwargs(self):
kwargs = {'blueberry': 'pizza', 'jay': 'awesome'}
response = self.client.get(reverse('volumes list'), data=kwargs)
diff --git a/apps/readux/urls.py b/apps/readux/urls.py
index e8e6b9c2..ce79b20c 100644
--- a/apps/readux/urls.py
+++ b/apps/readux/urls.py
@@ -1,24 +1,34 @@
-from django.conf.urls import url, include
+"""URL patterns for the Readux app"""
from django.urls import path
-from django.views.generic import RedirectView
from . import views, annotations
-# from .views import PageRedirectView
urlpatterns = [
- path('collection/', views.CollectionsList.as_view(), name='collections list' ),
- path('volume/', views.VolumesList.as_view(), name='volumes list' ),
- path('collection//', views.CollectionDetail.as_view(), name="collection" ),
- path('volume/', views.VolumeDetail.as_view(), name='volume' ),
- path('volume//page/all', views.PageDetail.as_view(), name='volumeall' ),
- # url for page altered to prevent conflict with Wagtail
- # TODO: find another way to resolve this conflict
- path('volume//page/', views.PageDetail.as_view(), name='page' ),
- path('volume//export', views.ExportOptions.as_view(), name='export' ),
- path('volume///export_download', views.ExportDownload.as_view(), name='export_download' ),
- path('volume//export_download_zip', views.ExportDownloadZip.as_view(), name='export_download_zip' ),
- path('annotations/', annotations.Annotations.as_view(), name='post_user_annotations' ),
- path('annotations///list/