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/', annotations.Annotations.as_view(), name='user_annotations' ), - path('annotations-crud/', annotations.AnnotationCrud.as_view(), name='crud_user_annotation' ), - path('search/', views.VolumeSearch.as_view(), name='search'), - path('_anno_count//', views.AnnotationsCount.as_view(), name='_anno_count') + 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/', + annotations.Annotations.as_view(), + name='user_annotations' + ), + path('annotations-crud/', annotations.AnnotationCrud.as_view(), name='crud_user_annotation'), + path('search/', views.VolumeSearch.as_view(), name='search'), + path('_anno_count//', views.AnnotationsCount.as_view(), name='_anno_count') ] diff --git a/apps/readux/views.py b/apps/readux/views.py index 99b576ff..ed9062e0 100644 --- a/apps/readux/views.py +++ b/apps/readux/views.py @@ -1,44 +1,42 @@ -import config.settings.local as settings +"""Django Views for the Readux app""" +from os import path from urllib.parse import urlencode from django.http import HttpResponse -from django.shortcuts import render from django.views.generic import ListView -from django.views.generic.base import TemplateView -from django.views.generic.base import View -from ..iiif.kollections.models import Collection -from apps.iiif.canvases.models import Canvas -from ..iiif.manifests.models import Manifest -from ..iiif.annotations.models import Annotation -from ..iiif.manifests.forms import JekyllExportForm -from ..iiif.manifests.export import JekyllSiteExport -from apps.readux.models import UserAnnotation -from apps.cms.models import Page, CollectionsPage -from django.views.generic.base import RedirectView +from django.views.generic.base import TemplateView, View from django.views.generic.edit import FormMixin from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank from django.contrib.sitemaps import Sitemap -from django.db.models import Max +from django.db.models import Max, Q, Count from django.urls import reverse from django.utils.datastructures import MultiValueDictKeyError -from django.db.models import Q, Count -from os import path -from wagtail.core import urls as wagtail_urls +import config.settings.local as settings +from .models import UserAnnotation +from ..cms.models import Page, CollectionsPage +from ..iiif.kollections.models import Collection +from ..iiif.canvases.models import Canvas +from ..iiif.manifests.models import Manifest +from ..iiif.annotations.models import Annotation +from ..iiif.manifests.forms import JekyllExportForm +from ..iiif.manifests.export import JekyllSiteExport SORT_OPTIONS = ['title', 'author', 'date published', 'date added'] ORDER_OPTIONS = ['asc', 'desc'] class CollectionsList(ListView): + """Django List View for :class:`apps.iiif.kollections.models.Collection`s""" template_name = "collections.html" context_object_name = 'collections' queryset = Collection.objects.all() class VolumesList(ListView): + """Django List View for :class:`apps.iiif.manifests.models.Manifest`s""" template_name = "volumes.html" SORT_OPTIONS = ['title', 'author', 'date published', 'date added'] ORDER_OPTIONS = ['asc', 'desc'] context_object_name = 'volumes' - + def get_queryset(self): return Manifest.objects.all() @@ -49,36 +47,32 @@ def get_context_data(self, **kwargs): q = self.get_queryset() - # SORT_OPTIONS = ['title', 'author', 'date published', 'date added'] - # ORDER_OPTIONS = ['asc', 'desc'] if sort not in SORT_OPTIONS: sort = 'title' if order not in ORDER_OPTIONS: order = 'asc' if sort == 'title': - if(order == 'asc'): + if order == 'asc': q = q.order_by('label') - elif(order == 'desc'): - q = q.order_by('-label') + elif order == 'desc': + q = q.order_by('-label') elif sort == 'author': - if(order == 'asc'): + if order == 'asc': q = q.order_by('author') - elif(order == 'desc'): - q = q.order_by('-author') + elif order == 'desc': + q = q.order_by('-author') elif sort == 'date published': - if(order == 'asc'): + if order == 'asc': q = q.order_by('published_date') - elif(order == 'desc'): - q = q.order_by('-published_date') + elif order == 'desc': + q = q.order_by('-published_date') elif sort == 'date added': - if(order == 'asc'): + if order == 'asc': q = q.order_by('created_at') - elif(order == 'desc'): - q = q.order_by('-created_at') + elif order == 'desc': + q = q.order_by('-created_at') - # sort_url_params = self.request.GET.copy() - # order_url_params = self.request.GET.copy() sort_url_params = {'sort': sort, 'order': order} order_url_params = {'sort': sort, 'order': order} if 'sort' in sort_url_params: @@ -94,6 +88,7 @@ def get_context_data(self, **kwargs): return context class CollectionDetail(TemplateView): + """Django Template View for a :class:`apps.iiif.kollections.models.Collection`""" template_name = "collection.html" SORT_OPTIONS = ['title', 'author', 'date published', 'date added'] ORDER_OPTIONS = ['asc', 'desc'] @@ -111,25 +106,25 @@ def get_context_data(self, **kwargs): order = 'asc' if sort == 'title': - if(order == 'asc'): + if order == 'asc': q = q.order_by('label') - elif(order == 'desc'): - q = q.order_by('-label') + elif order == 'desc': + q = q.order_by('-label') elif sort == 'author': - if(order == 'asc'): + if order == 'asc': q = q.order_by('author') - elif(order == 'desc'): - q = q.order_by('-author') + elif order == 'desc': + q = q.order_by('-author') elif sort == 'date published': - if(order == 'asc'): + if order == 'asc': q = q.order_by('published_date') - elif(order == 'desc'): - q = q.order_by('-published_date') + elif order == 'desc': + q = q.order_by('-published_date') elif sort == 'date added': - if(order == 'asc'): + if order == 'asc': q = q.order_by('created_at') - elif(order == 'desc'): - q = q.order_by('-created_at') + elif order == 'desc': + q = q.order_by('-created_at') sort_url_params = self.request.GET.copy() order_url_params = self.request.GET.copy() @@ -144,7 +139,11 @@ def get_context_data(self, **kwargs): annocount_list = [] canvaslist = [] for volume in q: - user_annotation_count = UserAnnotation.objects.filter(owner_id=self.request.user.id).filter(canvas__manifest__id=volume.id).count() + user_annotation_count = UserAnnotation.objects.filter( + owner_id=self.request.user.id + ).filter( + canvas__manifest__id=volume.id + ).count() annocount_list.append({volume.pid: user_annotation_count}) context['user_annotation_count'] = annocount_list canvasquery = Canvas.objects.filter(is_starting_page=1).filter(manifest__id=volume.id) @@ -162,6 +161,7 @@ def get_context_data(self, **kwargs): return context class VolumeDetail(TemplateView): + """Django Template View for :class:`apps.iiif.manifest.models.Manifest`""" template_name = "volume.html" def get_context_data(self, **kwargs): @@ -170,6 +170,7 @@ def get_context_data(self, **kwargs): return context class AnnotationsCount(TemplateView): + """Django Template View for :class:`apps.readux.models.UserAnnotation`""" template_name = "count.html" def get_context_data(self, **kwargs): @@ -178,14 +179,23 @@ def get_context_data(self, **kwargs): context['page'] = canvas manifest = Manifest.objects.filter(pid=kwargs['volume']).first() context['volume'] = manifest - context['user_annotation_page_count'] = UserAnnotation.objects.filter(owner_id=self.request.user.id).filter(canvas__id=canvas.id).count() - context['user_annotation_count'] = UserAnnotation.objects.filter(owner_id=self.request.user.id).filter(canvas__manifest__id=manifest.id).count() + context['user_annotation_page_count'] = UserAnnotation.objects.filter( + owner_id=self.request.user.id + ).filter( + canvas__id=canvas.id + ).count() + context['user_annotation_count'] = UserAnnotation.objects.filter( + owner_id=self.request.user.id + ).filter( + canvas__manifest__id=manifest.id + ).count() return context -# This replaces plainto_tsquery with to_tsquery so that operators ( | for or and :* for end of word) can be used. +# This replaces plain to_tsquery with to_tsquery so that operators ( | for or and :* for end of word) can be used. # If we upgrade to Django 2.2 from 2.1 we can add the operator search_type="raw" to the standard SearchQuery, and it should do the same thing. # TODO: This does not seem to be called anywhere. Is it actually needed? class MySearchQuery(SearchQuery): + """View for Search Query""" def as_sql(self, compiler, connection): params = [self.value] if self.config: @@ -197,8 +207,9 @@ def as_sql(self, compiler, connection): if self.invert: template = '!!({})'.format(template) return template, params - + class PageDetail(TemplateView): + """Django Template View for :class:`apps.iiif.canvases.models.Canvas`""" template_name = "page.html" def get_context_data(self, **kwargs): @@ -211,78 +222,94 @@ def get_context_data(self, **kwargs): manifest = Manifest.objects.filter(pid=kwargs['volume']).first() context['volume'] = manifest context['collectionlink'] = Page.objects.type(CollectionsPage).first() - context['user_annotation_page_count'] = UserAnnotation.objects.filter(owner_id=self.request.user.id).filter(canvas__id=canvas.id).count() - context['user_annotation_count'] = UserAnnotation.objects.filter(owner_id=self.request.user.id).filter(canvas__manifest__id=manifest.id).count() + context['user_annotation_page_count'] = UserAnnotation.objects.filter( + owner_id=self.request.user.id + ).filter( + canvas__id=canvas.id + ).count() + context['user_annotation_count'] = UserAnnotation.objects.filter( + owner_id=self.request.user.id + ).filter( + canvas__manifest__id=manifest.id + ).count() context['mirador_url'] = settings.MIRADOR_URL qs = Annotation.objects.all() qs2 = UserAnnotation.objects.all() try: - search_string = self.request.GET['q'] - search_type = self.request.GET['type'] - search_strings = self.request.GET['q'].split() - if search_strings: - if search_type == 'partial': - qq = Q() - query = SearchQuery('') - for search_string in search_strings: - query = query | SearchQuery(search_string) - qq |= Q(content__icontains=search_string) - vector = SearchVector('content') - qs = qs.filter(qq).filter(canvas__manifest__label=manifest.label) -# qs = qs.annotate(search=vector).filter(search=query).filter(canvas__manifest__label=manifest.label) -# qs = qs.annotate(rank=SearchRank(vector, query)).order_by('-rank') - qs = qs.values('canvas__position', 'canvas__manifest__label', 'canvas__pid').annotate(Count('canvas__position')).order_by('canvas__position') - qs1 = qs.exclude(resource_type='dctypes:Text').distinct() - qs2 = qs2.annotate(search=vector).filter(search=query).filter(canvas__manifest__label=manifest.label) - qs2 = qs2.annotate(rank=SearchRank(vector, query)).order_by('-rank') - qs2 = qs2.filter(owner_id=self.request.user.id).distinct() - elif search_type == 'exact': - qq = Q() - query = SearchQuery('') - for search_string in search_strings: - query = query | SearchQuery(search_string) - qq |= Q(content__contains=search_string) - vector = SearchVector('content') -# qs = qs.filter(qq).filter(canvas__manifest__label=manifest.label) - qs = qs.annotate(search=vector).filter(search=query).filter(canvas__manifest__label=manifest.label) -# qs = qs.annotate(rank=SearchRank(vector, query)).order_by('-rank') - qs = qs.values('canvas__position', 'canvas__manifest__label', 'canvas__pid').annotate(Count('canvas__position')).order_by('canvas__position') - qs1 = qs.exclude(resource_type='dctypes:Text').distinct() - qs2 = qs2.annotate(search=vector).filter(search=query).filter(canvas__manifest__label=manifest.label) - qs2 = qs2.annotate(rank=SearchRank(vector, query)).order_by('-rank') - qs2 = qs2.filter(owner_id=self.request.user.id).distinct() - else: - qs1 = '' - qs2 = '' - context['qs1'] = qs1 - context['qs2'] = qs2 + search_string = self.request.GET['q'] + search_type = self.request.GET['type'] + search_strings = self.request.GET['q'].split() + if search_strings: + if search_type == 'partial': + qq = Q() + query = SearchQuery('') + for search_string in search_strings: + query = query | SearchQuery(search_string) + qq |= Q(content__icontains=search_string) + vector = SearchVector('content') + qs = qs.filter(qq).filter(canvas__manifest__label=manifest.label) + qs = qs.values( + 'canvas__position', + 'canvas__manifest__label', + 'canvas__pid' + ).annotate( + Count( + 'canvas__position') + ).order_by('canvas__position') + qs1 = qs.exclude(resource_type='dctypes:Text').distinct() + qs2 = qs2.annotate( + search=vector + ).filter( + search=query + ).filter( + canvas__manifest__label=manifest.label + ) + qs2 = qs2.annotate(rank=SearchRank(vector, query)).order_by('-rank') + qs2 = qs2.filter(owner_id=self.request.user.id).distinct() + elif search_type == 'exact': + qq = Q() + query = SearchQuery('') + for search_string in search_strings: + query = query | SearchQuery(search_string) + qq |= Q(content__contains=search_string) + vector = SearchVector('content') + qs = qs.annotate( + search=vector + ).filter( + search=query + ).filter( + canvas__manifest__label=manifest.label + ) + qs = qs.values( + 'canvas__position', + 'canvas__manifest__label', + 'canvas__pid' + ).annotate( + Count('canvas__position') + ).order_by('canvas__position') + qs1 = qs.exclude(resource_type='dctypes:Text').distinct() + qs2 = qs2.annotate( + search=vector + ).filter( + search=query + ).filter( + canvas__manifest__label=manifest.label + ) + qs2 = qs2.annotate(rank=SearchRank(vector, query)).order_by('-rank') + qs2 = qs2.filter(owner_id=self.request.user.id).distinct() + else: + qs1 = '' + qs2 = '' + context['qs1'] = qs1 + context['qs2'] = qs2 except MultiValueDictKeyError: - q = '' - - return context + q = '' -# class PageRedirect(TemplateView): -# template_name = "page.html" -# -# def get_context_data(self, **kwargs): -# context = super().get_context_data(**kwargs) -# context['page'] = Canvas.objects.first() -# manifest = Manifest.objects.filter(pid=kwargs['volume']).first() -# context['volume'] = manifest -# context['user_annotation_count'] = Annotation.objects.filter(owner_id=self.request.user.id).filter(canvas__manifest__id=manifest.id).count() -# return context -# class PageRedirectView(RedirectView): -# -# permanent = False -# query_string = True -# pattern_name = 'page-redirect' -# -# def get_redirect_url(self, *args, **kwargs): -# page = Manifest.canvas_set.first() -# return super().get_redirect_url(*args, **kwargs) + return context class ExportOptions(TemplateView, FormMixin): + """Django Template View for Export""" template_name = "export.html" form_class = JekyllExportForm @@ -302,6 +329,7 @@ def get_context_data(self, **kwargs): return context class ExportDownload(TemplateView): + """Django Template View for downloading an export.""" template_name = "export_download.html" def get_context_data(self, **kwargs): @@ -318,13 +346,23 @@ def get_context_data(self, **kwargs): return context class ExportDownloadZip(View): + """Django View for downloading the zipped up export.""" def get(self, request, *args, **kwargs): + """[summary] + + :param View: [description] + :type View: [type] + :param request: [description] + :type request: [type] + :return: [description] + :rtype: [type] + """ jekyll_export = JekyllSiteExport(None, "v2", github_repo=None, deep_zoom=False, owners=[self.request.user.id], user=self.request.user); zip = jekyll_export.get_zip_file(kwargs['filename']) resp = HttpResponse(zip, content_type = "application/x-zip-compressed") resp['Content-Disposition'] = 'attachment; filename=jekyll_site_export.zip' return resp - + class VolumeSearch(ListView): '''Search across all volumes.''' template_name = 'search_results.html' @@ -335,86 +373,134 @@ def get_queryset(self): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) collection = self.request.GET.get('collection', None) + # pylint: disable = invalid-name COLSET = Collection.objects.values_list('pid', flat=True) COL_OPTIONS = list(COLSET) COL_LIST = Collection.objects.values('pid', 'label').order_by('label').distinct('label') + # pylint: enable = invalid-name collection_url_params = self.request.GET.copy() - + qs = self.get_queryset() try: - search_string = self.request.GET['q'] - search_type = self.request.GET['type'] - search_strings = self.request.GET['q'].split() - if search_strings: - if search_type == 'partial': - qq = Q() - qqq = Q() - query = SearchQuery('') - for search_string in search_strings: - query = query | SearchQuery(search_string) - qq |= Q(canvas__annotation__content__icontains=search_string) - qqq |= Q(label__icontains=search_string) | Q(author__icontains=search_string) | Q(summary__icontains=search_string) - print(query) -# vector = SearchVector('canvas__annotation__content') -# qs1 = qs.annotate(search=vector).filter(search=query) -# qs1 = qs1.annotate(rank=SearchRank(vector, query)).values('pid', 'label', 'author', 'published_date', 'created_at').annotate(pidcount = Count('pid')).order_by('-pidcount') - qs1 = qs.filter(qq) - qs1 = qs1.values('pid', 'label', 'author', 'published_date', 'created_at').annotate(pidcount = Count('pid')).order_by('-pidcount') - vector2 = SearchVector('label', weight='A') + SearchVector('author', weight='B') + SearchVector('summary', weight='C') -# qs3 = qs.annotate(search=vector2).filter(search=query) - qs3 = qs.filter(qqq) -# qs3 = qs3.annotate(rank=SearchRank(vector2, query)).values('pid', 'label', 'author', 'published_date', 'created_at').order_by('-rank') - qs2 = qs.values('label', 'author', 'published_date', 'created_at', 'canvas__pid', 'pid', 'canvas__IIIF_IMAGE_SERVER_BASE__IIIF_IMAGE_SERVER_BASE').order_by('pid').distinct('pid') - if collection not in COL_OPTIONS: - collection = None - - if collection is not None: - qs1 = qs1.filter(collections__pid = collection) - qs3 = qs3.filter(collections__pid = collection) - - if 'collection' in collection_url_params: - del collection_url_params['collection'] - elif search_type == 'exact': - qq = Q() - query = SearchQuery('') - for search_string in search_strings: - query = query | SearchQuery(search_string) - qq |= Q(canvas__annotation__content__exact=search_string) - vector = SearchVector('canvas__annotation__content') - qs1 = qs.annotate(search=vector).filter(search=query) - qs1 = qs1.annotate(rank=SearchRank(vector, query)).values('pid', 'label', 'author', 'published_date', 'created_at').annotate(pidcount = Count('pid')).order_by('-pidcount') - vector2 = SearchVector('label', weight='A') + SearchVector('author', weight='B') + SearchVector('summary', weight='C') - qs3 = qs.annotate(search=vector2).filter(search=query) - qs3 = qs3.annotate(rank=SearchRank(vector2, query)).values('pid', 'label', 'author', 'published_date', 'created_at').order_by('-rank') - qs2 = qs.values('canvas__pid', 'pid', 'canvas__IIIF_IMAGE_SERVER_BASE__IIIF_IMAGE_SERVER_BASE').order_by('pid').distinct('pid') - if collection not in COL_OPTIONS: - collection = None - - if collection is not None: - qs1 = qs1.filter(collections__pid = collection) - qs3 = qs3.filter(collections__pid = collection) - - if 'collection' in collection_url_params: - del collection_url_params['collection'] - else: - search_string = '' - search_strings = '' - qs1 = '' - qs2 = '' - qs3 = '' - context['qs1'] = qs1 - context['qs2'] = qs2 - context['qs3'] = qs3 + search_string = self.request.GET['q'] + search_type = self.request.GET['type'] + search_strings = self.request.GET['q'].split() + if search_strings: + if search_type == 'partial': + qq = Q() + qqq = Q() + query = SearchQuery('') + for search_string in search_strings: + query = query | SearchQuery(search_string) + qq |= Q(canvas__annotation__content__icontains=search_string) + qqq |= Q(label__icontains=search_string) |Q(author__icontains=search_string) | Q(summary__icontains=search_string) # pylint: disable = line-too-long + qs1 = qs.filter(qq) + qs1 = qs1.values( + 'pid', 'label', 'author', + 'published_date', 'created_at' + ).annotate( + pidcount=Count('pid') + ).order_by('-pidcount') + + vector2 = SearchVector( + 'label', weight='A' + ) + SearchVector( + 'author', weight='B' + ) + SearchVector( + 'summary', weight='C' + ) + + qs3 = qs.filter(qqq) + qs2 = qs.values( + 'label', 'author', 'published_date', 'created_at', 'canvas__pid', 'pid', + 'canvas__IIIF_IMAGE_SERVER_BASE__IIIF_IMAGE_SERVER_BASE' + ).order_by( + 'pid' + ).distinct( + 'pid' + ) + + if collection not in COL_OPTIONS: + collection = None + + if collection is not None: + qs1 = qs1.filter(collections__pid = collection) + qs3 = qs3.filter(collections__pid = collection) + + if 'collection' in collection_url_params: + del collection_url_params['collection'] + elif search_type == 'exact': + qq = Q() + query = SearchQuery('') + for search_string in search_strings: + query = query | SearchQuery(search_string) + qq |= Q(canvas__annotation__content__exact=search_string) + vector = SearchVector('canvas__annotation__content') + qs1 = qs.annotate(search=vector).filter(search=query) + qs1 = qs1.annotate( + rank=SearchRank(vector, query) + ).values( + 'pid', 'label', 'author', + 'published_date', 'created_at' + ).annotate( + pidcount = Count('pid') + ).order_by('-pidcount') + + vector2 = SearchVector( + 'label', weight='A' + ) + SearchVector( + 'author', weight='B' + ) + SearchVector( + 'summary', weight='C' + ) + + qs3 = qs.annotate(search=vector2).filter(search=query) + qs3 = qs3.annotate( + rank=SearchRank(vector2, query) + ).values( + 'pid', 'label', 'author', + 'published_date', 'created_at' + ).order_by('-rank') + + qs2 = qs.values( + 'canvas__pid', 'pid', + 'canvas__IIIF_IMAGE_SERVER_BASE__IIIF_IMAGE_SERVER_BASE' + ).order_by( + 'pid' + ).distinct('pid') + + if collection not in COL_OPTIONS: + collection = None + + if collection is not None: + qs1 = qs1.filter(collections__pid = collection) + qs3 = qs3.filter(collections__pid = collection) + + if 'collection' in collection_url_params: + del collection_url_params['collection'] + else: + search_string = '' + search_strings = '' + qs1 = '' + qs2 = '' + qs3 = '' + context['qs1'] = qs1 + context['qs2'] = qs2 + context['qs3'] = qs3 except MultiValueDictKeyError: - q = '' - search_string = '' - search_strings = '' + q = '' + search_string = '' + search_strings = '' context['volumes'] = qs.all annocount_list = [] canvaslist = [] for volume in qs: - user_annotation_count = UserAnnotation.objects.filter(owner_id=self.request.user.id).filter(canvas__manifest__id=volume.id).count() + user_annotation_count = UserAnnotation.objects.filter( + owner_id=self.request.user.id + ).filter( + canvas__manifest__id=volume.id + ).count() annocount_list.append({volume.pid: user_annotation_count}) context['user_annotation_count'] = annocount_list canvasquery = Canvas.objects.filter(is_starting_page=1).filter(manifest__id=volume.id) @@ -429,6 +515,7 @@ def get_context_data(self, **kwargs): return context class ManifestsSitemap(Sitemap): + """Django Sitemap for Manafests""" limit = 5 # priority unknown def items(self): @@ -441,6 +528,7 @@ def lastmod(self, item): return item.updated_at class CollectionsSitemap(Sitemap): + """Django Sitemap for Collections""" # priority unknown def items(self): return Collection.objects.all().annotate(modified_at=Max('manifests__updated_at')) @@ -449,6 +537,4 @@ def location(self, item): return reverse('collection', kwargs={'collection': item.pid}) def lastmod(self, item): - return item.updated_at - - + return item.updated_at