From 96c4cf97c5e5da5a46734d805ff780ef0cd62eb0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 Oct 2021 10:24:20 +0000 Subject: [PATCH 01/43] build(deps): Bump faker from 9.5.1 to 9.5.2 Bumps [faker](https://github.com/joke2k/faker) from 9.5.1 to 9.5.2. - [Release notes](https://github.com/joke2k/faker/releases) - [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) - [Commits](https://github.com/joke2k/faker/compare/v9.5.1...v9.5.2) --- updated-dependencies: - dependency-name: faker dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements/local.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/local.txt b/requirements/local.txt index 99a771133..2c3469723 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -29,7 +29,7 @@ coveralls # Django # ------------------------------------------------------------------------------ factory-boy==3.2.0 # https://github.com/FactoryBoy/factory_boy -faker==9.5.1 +faker==9.5.2 django-debug-toolbar==3.2.2 # https://github.com/jazzband/django-debug-toolbar django-extensions==3.1.3 # https://github.com/django-extensions/django-extensions From a1337002f5908f8f2e2dd14c7aa7c25c9f618f88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Oct 2021 10:22:49 +0000 Subject: [PATCH 02/43] build(deps): Bump boto3 from 1.19.0 to 1.19.2 Bumps [boto3](https://github.com/boto/boto3) from 1.19.0 to 1.19.2. - [Release notes](https://github.com/boto/boto3/releases) - [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst) - [Commits](https://github.com/boto/boto3/compare/1.19.0...1.19.2) --- updated-dependencies: - dependency-name: boto3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index 9ed5e507c..e62695ae7 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -55,7 +55,7 @@ gitpython==3.1.24 django-background-tasks==1.2.5 # S3 Uploads -boto3==1.19.0 +boto3==1.19.2 django-storages==1.12.2 # https://github.com/jschneier/django-storages # Python function to stream unzip all the files in a ZIP archive, without loading the entire ZIP file into memory or any of its uncompressed files. From 3cbfaf32b155c69773e72b731653a2bb8de70c8e Mon Sep 17 00:00:00 2001 From: Ben Silverman Date: Mon, 25 Oct 2021 11:28:35 -0400 Subject: [PATCH 03/43] Fix incorrect absolute URL in success email --- apps/ingest/mail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ingest/mail.py b/apps/ingest/mail.py index 60351458c..0ad5fd8d2 100644 --- a/apps/ingest/mail.py +++ b/apps/ingest/mail.py @@ -50,7 +50,7 @@ def send_email_on_success(task_watcher=None): 'admin:manifests_manifest_change', args=(task_watcher.associated_manifest.id,) ) context['manifest_pid'] = task_watcher.associated_manifest.pid - context['volume_url'] = task_watcher.associated_manifest.get_absolute_url() + context['volume_url'] = task_watcher.associated_manifest.get_volume_url() else: context['manifests_list_url'] = settings.HOSTNAME + reverse( 'admin:manifests_manifest_changelist' From ee4cbd98aed667f8d563046ae17c3b6b865e49e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Oct 2021 10:22:04 +0000 Subject: [PATCH 04/43] build(deps): Bump moto from 2.2.10 to 2.2.11 Bumps [moto](https://github.com/spulec/moto) from 2.2.10 to 2.2.11. - [Release notes](https://github.com/spulec/moto/releases) - [Changelog](https://github.com/spulec/moto/blob/master/CHANGELOG.md) - [Commits](https://github.com/spulec/moto/compare/2.2.10...2.2.11) --- updated-dependencies: - dependency-name: moto dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements/local.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/local.txt b/requirements/local.txt index 2c3469723..143947b04 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -18,7 +18,7 @@ httpretty==1.1.4 # https://pypi.org/project/httpretty/ #mock==4.0.2 cssutils==2.3.0 # https://pypi.org/project/cssutils/ pytest-django==4.4.0 # https://github.com/pytest-dev/pytest-django -moto==2.2.10 # https://github.com/spulec/moto +moto==2.2.11 # https://github.com/spulec/moto # Code quality # ------------------------------------------------------------------------------ From cb0cfbdbce0f45d83d42d57256e679558ada6b40 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Oct 2021 10:22:23 +0000 Subject: [PATCH 05/43] build(deps): Bump faker from 9.5.2 to 9.6.0 Bumps [faker](https://github.com/joke2k/faker) from 9.5.2 to 9.6.0. - [Release notes](https://github.com/joke2k/faker/releases) - [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) - [Commits](https://github.com/joke2k/faker/compare/v9.5.2...v9.6.0) --- updated-dependencies: - dependency-name: faker dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements/local.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/local.txt b/requirements/local.txt index 2c3469723..b31a45165 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -29,7 +29,7 @@ coveralls # Django # ------------------------------------------------------------------------------ factory-boy==3.2.0 # https://github.com/FactoryBoy/factory_boy -faker==9.5.2 +faker==9.6.0 django-debug-toolbar==3.2.2 # https://github.com/jazzband/django-debug-toolbar django-extensions==3.1.3 # https://github.com/django-extensions/django-extensions From 65cee68b92c5a5bcdfcdce3d6997b992e131b047 Mon Sep 17 00:00:00 2001 From: Ben Silverman Date: Tue, 26 Oct 2021 14:08:27 -0400 Subject: [PATCH 06/43] PID automigrations --- .../migrations/0014_auto_20211026_1736.py | 18 ++++++++++++++++++ .../migrations/0010_auto_20211026_1736.py | 18 ++++++++++++++++++ .../migrations/0031_auto_20211026_1736.py | 18 ++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 apps/iiif/canvases/migrations/0014_auto_20211026_1736.py create mode 100644 apps/iiif/kollections/migrations/0010_auto_20211026_1736.py create mode 100644 apps/iiif/manifests/migrations/0031_auto_20211026_1736.py diff --git a/apps/iiif/canvases/migrations/0014_auto_20211026_1736.py b/apps/iiif/canvases/migrations/0014_auto_20211026_1736.py new file mode 100644 index 000000000..72f5f3a99 --- /dev/null +++ b/apps/iiif/canvases/migrations/0014_auto_20211026_1736.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.24 on 2021-10-26 17:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('canvases', '0013_auto_20211018_1913'), + ] + + operations = [ + migrations.AlterField( + model_name='canvas', + name='pid', + field=models.CharField(default='2qkw6szd', help_text="Unique ID. Do not use _'s or spaces in the pid.", max_length=255), + ), + ] diff --git a/apps/iiif/kollections/migrations/0010_auto_20211026_1736.py b/apps/iiif/kollections/migrations/0010_auto_20211026_1736.py new file mode 100644 index 000000000..36a440d89 --- /dev/null +++ b/apps/iiif/kollections/migrations/0010_auto_20211026_1736.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.24 on 2021-10-26 17:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('kollections', '0009_auto_20211018_1913'), + ] + + operations = [ + migrations.AlterField( + model_name='collection', + name='pid', + field=models.CharField(default='2qkw6szd', help_text="Unique ID. Do not use _'s or spaces in the pid.", max_length=255), + ), + ] diff --git a/apps/iiif/manifests/migrations/0031_auto_20211026_1736.py b/apps/iiif/manifests/migrations/0031_auto_20211026_1736.py new file mode 100644 index 000000000..b34039be8 --- /dev/null +++ b/apps/iiif/manifests/migrations/0031_auto_20211026_1736.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.24 on 2021-10-26 17:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('manifests', '0030_auto_20211018_1913'), + ] + + operations = [ + migrations.AlterField( + model_name='manifest', + name='pid', + field=models.CharField(default='2qkw6szd', help_text="Unique ID. Do not use _'s or spaces in the pid.", max_length=255), + ), + ] From cf65a8f0422698efd136eaa58f08582bf6555763 Mon Sep 17 00:00:00 2001 From: Ben Silverman Date: Tue, 26 Oct 2021 17:23:10 -0400 Subject: [PATCH 07/43] fix: offload batch upload to s3 to celery task --- apps/ingest/admin.py | 45 +++++++++-------- .../migrations/0032_auto_20211026_1736.py | 25 ++++++++++ apps/ingest/models.py | 3 +- apps/ingest/tasks.py | 50 ++++++++++++++++++- .../admin/ingest/bulk/change_form.html | 29 ++++------- apps/ingest/tests/test_admin.py | 3 +- 6 files changed, 112 insertions(+), 43 deletions(-) create mode 100644 apps/ingest/migrations/0032_auto_20211026_1736.py diff --git a/apps/ingest/admin.py b/apps/ingest/admin.py index 4b29bb983..fae157567 100644 --- a/apps/ingest/admin.py +++ b/apps/ingest/admin.py @@ -2,8 +2,8 @@ import logging from mimetypes import guess_type from os import environ, path, remove, listdir, rmdir +from django.core.files.base import ContentFile from django.contrib import admin -from django.core.files.storage import FileSystemStorage from django.shortcuts import redirect from django.urls import reverse from django.utils.html import format_html @@ -83,7 +83,6 @@ class BulkAdmin(admin.ModelAdmin): form = BulkVolumeUploadForm def save_model(self, request, obj, form, change): - form.storage = IngestStorage() obj.save() # Get files from multi upload form files = request.FILES.getlist("volume_files") @@ -100,35 +99,36 @@ def save_model(self, request, obj, form, change): else: file_meta = {} - # Save in storage - bundle_path = form.storage.save( - path.join("bulk", str(obj.id), file.name), file - ) - - # Create local + # Create a Local object new_local = Local.objects.create( bulk=obj, - bundle=bundle_path, image_server=obj.image_server, - metadata=file_meta, creator=request.user ) - new_local.save() - new_local.manifest = create_manifest(new_local) + if file_meta: + new_local.metadata=file_meta + + # Save tempfile in bundle_from_bulk + with ContentFile(file.read()) as file_content: + new_local.bundle_from_bulk.save(file.name, file_content) new_local.save() new_local.refresh_from_db() + + # Queue task to upload to S3 if environ["DJANGO_ENV"] != 'test': - local_task_id = tasks.create_canvas_form_local_task.delay(new_local.id) - local_task_result = TaskResult(task_id=local_task_id) - local_task_result.save() + upload_task = tasks.upload_to_s3_task.delay( + local_id=new_local.id, + file_path=path.join("bulk", str(obj.id), file.name), + user_id=request.user.id, + ) + upload_task_result = TaskResult(task_id=upload_task.id) + upload_task_result.save() IngestTaskWatcher.manager.create_watcher( - task_id=local_task_id, - task_result=local_task_result, + task_id=upload_task.id, + task_result=upload_task_result, task_creator=request.user, - associated_manifest=new_local.manifest, filename=file.name ) - obj.refresh_from_db() super().save_model(request, obj, form, change) @@ -137,11 +137,14 @@ def response_add(self, request, obj, post_url_continue=None): file_path = obj.volume_files.path if path.isfile(file_path): remove(file_path) + else: + LOGGER.error(f"Could not cleanup {file_path}") dir_path = file_path[0:file_path.rindex('/')] - if len(listdir(dir_path)) == 0: + if not path.isfile(file_path) and len(listdir(dir_path)) == 0: rmdir(dir_path) obj.delete() - return redirect("/admin/manifests/manifest/?o=-4") + url_to = reverse('admin:ingest_ingesttaskwatcher_changelist') + return redirect(url_to) class Meta: # pylint: disable=too-few-public-methods, missing-class-docstring model = Bulk diff --git a/apps/ingest/migrations/0032_auto_20211026_1736.py b/apps/ingest/migrations/0032_auto_20211026_1736.py new file mode 100644 index 000000000..02a285ddc --- /dev/null +++ b/apps/ingest/migrations/0032_auto_20211026_1736.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2.24 on 2021-10-26 17:36 + +import apps.ingest.models +import apps.ingest.storages +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ingest', '0031_ingesttaskwatcher_associated_manifest'), + ] + + operations = [ + migrations.AddField( + model_name='local', + name='bundle_from_bulk', + field=models.FileField(null=True, upload_to=apps.ingest.models.bulk_path), + ), + migrations.AlterField( + model_name='local', + name='bundle', + field=models.FileField(null=True, storage=apps.ingest.storages.IngestStorage(), upload_to=''), + ), + ] diff --git a/apps/ingest/models.py b/apps/ingest/models.py index 6c10017db..d32dde1c5 100644 --- a/apps/ingest/models.py +++ b/apps/ingest/models.py @@ -77,7 +77,8 @@ class Meta: class Local(IngestAbstractModel): """ Model class for ingesting a volume from local files. """ bulk = models.ForeignKey(Bulk, related_name='local_uploads', on_delete=models.SET_NULL, null=True) - bundle = models.FileField(blank=False, storage=IngestStorage()) + bundle_from_bulk = models.FileField(null=True, blank=True, upload_to=bulk_path) + bundle = models.FileField(null=True, blank=True, storage=IngestStorage()) image_server = models.ForeignKey(ImageServer, on_delete=models.DO_NOTHING, null=True) creator = models.ForeignKey( settings.AUTH_USER_MODEL, diff --git a/apps/ingest/tasks.py b/apps/ingest/tasks.py index c046f57ae..18aa67376 100644 --- a/apps/ingest/tasks.py +++ b/apps/ingest/tasks.py @@ -2,10 +2,14 @@ """ Common tasks for ingest. """ import logging +from os import path, remove, listdir, rmdir from celery import Celery from celery.signals import task_success, task_failure from django.apps import apps from django.conf import settings +from django.contrib.auth import get_user_model +from django.core.files.base import ContentFile +from django_celery_results.models import TaskResult from apps.ingest.models import IngestTaskWatcher from .services import create_manifest from .mail import send_email_on_failure, send_email_on_success @@ -64,6 +68,50 @@ def create_remote_canvases(ingest_id, *args, **kwargs): remote_ingest.create_canvases() +@app.task(name='uploading_to_s3', autoretry_for=(Exception,), retry_backoff=True, max_retries=20) +def upload_to_s3_task(local_id, file_path, user_id): + """Task to create Canavs objects from remote IIIF manifest + + :param local_id: Primary key for .models.Local object + :type local_id: UUID + :param file_path: File path for uploaded file + :param user_id: Primary key for User object + :type local_id: UUID + """ + local = Local.objects.get(pk=local_id) + # Upload tempfile to S3 + with ContentFile(local.bundle_from_bulk.read()) as file_content: + local.bundle.save(file_path, file_content) + local.save() + + # Create manifest now that we have a file + local.manifest = create_manifest(local) + local.save() + local.refresh_from_db() + + # Delete tempfile + if local.bundle is not None and local.bundle_from_bulk is not None: + old_path = local.bundle_from_bulk.path + if path.isfile(old_path): + remove(old_path) + else: + LOGGER.error(f"Could not find for cleanup: {old_path}") + dir_path = old_path[0:old_path.rindex('/')] + if not path.isfile(old_path) and len(listdir(dir_path)) == 0: + rmdir(dir_path) + + # Queue task to create canvases etc. + local_task_id = create_canvas_form_local_task.delay(local_id) + local_task_result = TaskResult(task_id=local_task_id) + local_task_result.save() + file_name = local.bundle.name + IngestTaskWatcher.manager.create_watcher( + task_id=local_task_id, + task_result=local_task_result, + task_creator=get_user_model().objects.get(pk=user_id), + associated_manifest=local.manifest, + filename=file_name[file_name.rindex('/')+1:] + ) @task_failure.connect def send_email_on_failure_task(sender=None, exception=None, task_id=None, traceback=None, *args, **kwargs): @@ -88,4 +136,4 @@ def send_email_on_success_task(sender=None, **kwargs): task_id = sender.request.id task_watcher = IngestTaskWatcher.manager.get(task_id=task_id) if task_watcher is not None: - send_email_on_success(task_watcher) + send_email_on_success(task_watcher) diff --git a/apps/ingest/templates/admin/ingest/bulk/change_form.html b/apps/ingest/templates/admin/ingest/bulk/change_form.html index 6ef03a4cb..3d779de04 100644 --- a/apps/ingest/templates/admin/ingest/bulk/change_form.html +++ b/apps/ingest/templates/admin/ingest/bulk/change_form.html @@ -61,7 +61,6 @@ const percentage = document.getElementById('overlay-percentage'); const progressBar = document.getElementById('overlay-progress'); const buttonUsed = django.jQuery('#bulk_form').data("callerid"); - let errored = false; // Set up POST req const form = e.currentTarget; @@ -76,36 +75,28 @@ label.textContent = 'Uploading...'; }; - xhr.upload.onprogress = function (e) { + xhr.upload.onprogress = (e) => { const percent = e.lengthComputable ? (e.loaded / e.total) * 100 : 0; progressBar.value = percent.toFixed(2); progressBar.setAttribute('aria-valuenow', percent.toFixed(2)); percentage.textContent = percent.toFixed(2) + '%'; }; - function handleError (e) { - errored = true; - label.textContent = e.type.toString(); + xhr.upload.onloadend = (e) => { + label.textContent = 'Upload complete, please wait...'; } - xhr.upload.addEventListener('abort', handleError); - xhr.upload.addEventListener('error', handleError); - xhr.addEventListener('abort', handleError); - xhr.addEventListener('error', handleError); - - xhr.upload.onloadend = (e) => { - // Redirect to manifests page - if (!errored) { + xhr.onreadystatechange = (e) => { + if (xhr.readyState == XMLHttpRequest.DONE) { if (buttonUsed === 'start_upload') { - location.href = '/admin/manifests/manifest/?o=-4'; + location.href = '/admin/ingest/ingesttaskwatcher/'; } else { location.reload(); } } - }; - + } // Send POST req - xhr.open('POST', url); + xhr.open('POST', url, true); xhr.send(new FormData(form)); }) @@ -123,8 +114,8 @@ You must leave this window open during upload.

- Once upload completes, you will be sent to the list of manifests. - You may navigate away while the rest of the ingest completes; you + Once upload completes, you will be sent to a new page. + You may then navigate away while the rest of the ingest completes; you will receive an email to notify you when the ingest has completed.

diff --git a/apps/ingest/tests/test_admin.py b/apps/ingest/tests/test_admin.py index 5c7a16d63..615e1faae 100644 --- a/apps/ingest/tests/test_admin.py +++ b/apps/ingest/tests/test_admin.py @@ -6,6 +6,7 @@ from django.http import HttpResponseRedirect from django.test import TestCase from django.test.client import RequestFactory +from django.urls.base import reverse from django_celery_results.models import TaskResult from moto import mock_s3 from apps.ingest.forms import BulkVolumeUploadForm @@ -177,7 +178,7 @@ def test_bulk_admin_response_add(self): with self.assertRaises(Bulk.DoesNotExist): bulk.refresh_from_db() assert isinstance(response, HttpResponseRedirect) - assert response.url == '/admin/manifests/manifest/?o=-4' + assert response.url == reverse('admin:ingest_ingesttaskwatcher_changelist') def test_bulk_admin_with_external_metadata(self): """It should add the metadata to the matching Local object""" From f6f9f867a677e566eb74eff9a1b05886690667f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Oct 2021 10:26:11 +0000 Subject: [PATCH 08/43] build(deps): Bump boto3 from 1.19.2 to 1.19.4 Bumps [boto3](https://github.com/boto/boto3) from 1.19.2 to 1.19.4. - [Release notes](https://github.com/boto/boto3/releases) - [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst) - [Commits](https://github.com/boto/boto3/compare/1.19.2...1.19.4) --- updated-dependencies: - dependency-name: boto3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index e62695ae7..be0a2af88 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -55,7 +55,7 @@ gitpython==3.1.24 django-background-tasks==1.2.5 # S3 Uploads -boto3==1.19.2 +boto3==1.19.4 django-storages==1.12.2 # https://github.com/jschneier/django-storages # Python function to stream unzip all the files in a ZIP archive, without loading the entire ZIP file into memory or any of its uncompressed files. From 62ba27d74428488e57264c0d3601f5387207cc67 Mon Sep 17 00:00:00 2001 From: Ben Silverman Date: Wed, 27 Oct 2021 09:13:03 -0400 Subject: [PATCH 09/43] fix: Error handling in upload overlay --- .../templates/admin/ingest/bulk/change_form.html | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/ingest/templates/admin/ingest/bulk/change_form.html b/apps/ingest/templates/admin/ingest/bulk/change_form.html index 3d779de04..051b8962d 100644 --- a/apps/ingest/templates/admin/ingest/bulk/change_form.html +++ b/apps/ingest/templates/admin/ingest/bulk/change_form.html @@ -86,6 +86,16 @@ label.textContent = 'Upload complete, please wait...'; } + function handleError (e) { + errored = true; + label.textContent = 'Error: Please see dev tools console for details'; + } + + xhr.upload.addEventListener('abort', handleError); + xhr.upload.addEventListener('error', handleError); + xhr.addEventListener('abort', handleError); + xhr.addEventListener('error', handleError); + xhr.onreadystatechange = (e) => { if (xhr.readyState == XMLHttpRequest.DONE) { if (buttonUsed === 'start_upload') { From 7b2ff0525c67ffa4e95699e65dae6a1c71220307 Mon Sep 17 00:00:00 2001 From: Ben Silverman Date: Wed, 27 Oct 2021 14:52:06 -0400 Subject: [PATCH 10/43] fix: Move bundle_to_s3 func to Local model, +tests --- apps/ingest/admin.py | 3 -- apps/ingest/models.py | 23 ++++++++++++ apps/ingest/tasks.py | 34 +++++------------- apps/ingest/tests/test_local.py | 64 ++++++++++++++++++++++++++------- 4 files changed, 84 insertions(+), 40 deletions(-) diff --git a/apps/ingest/admin.py b/apps/ingest/admin.py index fae157567..4d6819f77 100644 --- a/apps/ingest/admin.py +++ b/apps/ingest/admin.py @@ -9,7 +9,6 @@ from django.utils.html import format_html from django_celery_results.models import TaskResult from apps.ingest import tasks -from apps.ingest.storages import IngestStorage from .models import Bulk, IngestTaskWatcher, Local, Remote from .services import clean_metadata, create_manifest, get_associated_meta, get_metadata_from from .forms import BulkVolumeUploadForm @@ -118,8 +117,6 @@ def save_model(self, request, obj, form, change): if environ["DJANGO_ENV"] != 'test': upload_task = tasks.upload_to_s3_task.delay( local_id=new_local.id, - file_path=path.join("bulk", str(obj.id), file.name), - user_id=request.user.id, ) upload_task_result = TaskResult(task_id=upload_task.id) upload_task_result.save() diff --git a/apps/ingest/models.py b/apps/ingest/models.py index d32dde1c5..6dc7ea19e 100644 --- a/apps/ingest/models.py +++ b/apps/ingest/models.py @@ -4,6 +4,7 @@ import logging from io import BytesIO from mimetypes import guess_type +from django.core.files.base import ContentFile import httpretty from stream_unzip import stream_unzip, TruncatedDataError from boto3 import client @@ -153,6 +154,28 @@ def volume_to_s3(self): f'{self.manifest.pid}/_*ocr*_/{file_name}' ) + def bundle_to_s3(self): + """Uploads the zipfile stored in bundle_from_bulk to S3 + :param file_path: File path for uploaded file + :type file_path: str + """ + if bool(self.bundle_from_bulk): + # Save to bundle + if not bool(self.bundle): + if not os.path.isfile(self.bundle_from_bulk.path): + raise Exception(f"Could not find file: {self.bundle_from_bulk.path}") + bulk_name = self.bundle_from_bulk.name + with ContentFile(self.bundle_from_bulk.read()) as file_content: + self.bundle.save(bulk_name, file_content) + # Delete tempfile + if bool(self.bundle): + old_path = self.bundle_from_bulk.path + os.remove(old_path) + dir_path = old_path[0:old_path.rindex('/')] + if not os.path.isfile(old_path) and len(os.listdir(dir_path)) == 0: + os.rmdir(dir_path) + self.bundle_from_bulk.delete() + @property def file_list(self): """Returns a list of files in the zip. Used for testing. diff --git a/apps/ingest/tasks.py b/apps/ingest/tasks.py index 18aa67376..bcc07a8cb 100644 --- a/apps/ingest/tasks.py +++ b/apps/ingest/tasks.py @@ -2,7 +2,6 @@ """ Common tasks for ingest. """ import logging -from os import path, remove, listdir, rmdir from celery import Celery from celery.signals import task_success, task_failure from django.apps import apps @@ -69,48 +68,33 @@ def create_remote_canvases(ingest_id, *args, **kwargs): remote_ingest.create_canvases() @app.task(name='uploading_to_s3', autoretry_for=(Exception,), retry_backoff=True, max_retries=20) -def upload_to_s3_task(local_id, file_path, user_id): +def upload_to_s3_task(local_id): """Task to create Canavs objects from remote IIIF manifest :param local_id: Primary key for .models.Local object :type local_id: UUID - :param file_path: File path for uploaded file - :param user_id: Primary key for User object - :type local_id: UUID """ local = Local.objects.get(pk=local_id) + # Upload tempfile to S3 - with ContentFile(local.bundle_from_bulk.read()) as file_content: - local.bundle.save(file_path, file_content) - local.save() + local.bundle_to_s3() # Create manifest now that we have a file - local.manifest = create_manifest(local) - local.save() - local.refresh_from_db() - - # Delete tempfile - if local.bundle is not None and local.bundle_from_bulk is not None: - old_path = local.bundle_from_bulk.path - if path.isfile(old_path): - remove(old_path) - else: - LOGGER.error(f"Could not find for cleanup: {old_path}") - dir_path = old_path[0:old_path.rindex('/')] - if not path.isfile(old_path) and len(listdir(dir_path)) == 0: - rmdir(dir_path) + if local.manifest is None: + local.manifest = create_manifest(local) # Queue task to create canvases etc. + local.save() + local.refresh_from_db() local_task_id = create_canvas_form_local_task.delay(local_id) local_task_result = TaskResult(task_id=local_task_id) local_task_result.save() - file_name = local.bundle.name IngestTaskWatcher.manager.create_watcher( task_id=local_task_id, task_result=local_task_result, - task_creator=get_user_model().objects.get(pk=user_id), + task_creator=get_user_model().objects.get(pk=local.creator.id), associated_manifest=local.manifest, - filename=file_name[file_name.rindex('/')+1:] + filename=local.bundle.name ) @task_failure.connect diff --git a/apps/ingest/tests/test_local.py b/apps/ingest/tests/test_local.py index 16dc318f1..3ed3855f3 100644 --- a/apps/ingest/tests/test_local.py +++ b/apps/ingest/tests/test_local.py @@ -1,17 +1,14 @@ """ Tests for local ingest """ +from os import path, remove +from tempfile import gettempdir import pytest import boto3 from moto import mock_s3 -from shutil import copy -from os.path import exists, join -from tempfile import gettempdir -from uuid import UUID -from unittest import mock +from os.path import join from django.test import TestCase -from django.core.files.uploadedfile import SimpleUploadedFile +from django.core.files.uploadedfile import SimpleUploadedFile, TemporaryUploadedFile from django.conf import settings from apps.iiif.canvases.models import Canvas -from apps.iiif.manifests.models import Manifest from apps.iiif.manifests.tests.factories import ManifestFactory, ImageServerFactory from ..models import Local from ..services import create_manifest @@ -35,16 +32,21 @@ def setUp(self): conn.create_bucket(Bucket=self.image_server.storage_path) conn.create_bucket(Bucket='readux-ingest') - def mock_local(self, bundle, with_manifest=False, metadata={}): + def mock_local(self, bundle, with_manifest=False, metadata={}, from_bulk=False): # Note, I tried to use the factory here, but could not get it to override the file for bundle. local = Local( image_server = self.image_server, metadata = metadata ) - local.bundle = SimpleUploadedFile( - name=bundle, - content=open(join(self.fixture_path, bundle), 'rb').read() - ) + local.save() + file = SimpleUploadedFile( + name=bundle, + content=open(join(self.fixture_path, bundle), 'rb').read() + ) + if from_bulk: + local.bundle_from_bulk.save(bundle, file) + else: + local.bundle = file local.save() @@ -252,3 +254,41 @@ def test_it_creates_mainfest_with_metadata_property(self): local.manifest = create_manifest(local) assert local.manifest.pid == '808' assert local.manifest.title == 'Goodie Mob' + + def test_moving_bulk_bundle_to_s3(self): + """ + It should upload Local.bundle_from_bulk to mock S3 by saving it to + Local.bundle, then it should clean up tempfiles + """ + # Make sure local with from_bulk is mocked correctly + local = self.mock_local('bundle.zip', from_bulk=True) + assert bool(local.bundle_from_bulk) is True + bulk_name = local.bundle_from_bulk.name + bulk_path = local.bundle_from_bulk.path + dir_path = bulk_path[0:bulk_path.rindex('/')] + assert bulk_name[bulk_name.rindex('/')+1:] == 'bundle.zip' + assert path.isfile(bulk_path) is True + assert path.isdir(dir_path) is True + + # Call bundle_to_s3() and test that it uploaded the file + local.bundle_to_s3() + assert bool(local.bundle) is True + assert local.bundle.name[local.bundle.name.rindex('/')+1:] == 'bundle.zip' + assert local.bundle.storage.exists(f'bulk/{local.id}/bundle.zip') # pylint: disable=no-member + + # Test tempfile cleanup + assert bool(local.bundle_from_bulk) is False + assert path.isfile(bulk_path) is False + assert path.isdir(dir_path) is False + + def test_bundle_to_s3_fails_for_deleted_tempfile(self): + """ + It should raise an exception because we deleted the tempfile before + running bundle_to_s3 + """ + local = self.mock_local('bundle.zip', from_bulk=True) + bulk_path = local.bundle_from_bulk.path + assert path.isfile(bulk_path) is True + remove(bulk_path) + assert path.isfile(bulk_path) is False + self.assertRaises(Exception, local.bundle_to_s3) From 9ef7a418604974ed9de6e311a5f0ea78e4afe5d2 Mon Sep 17 00:00:00 2001 From: Ben Silverman Date: Wed, 27 Oct 2021 14:53:33 -0400 Subject: [PATCH 11/43] fix(test): Cleanup unused imports --- apps/ingest/tests/test_local.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/ingest/tests/test_local.py b/apps/ingest/tests/test_local.py index 3ed3855f3..0ac55ba79 100644 --- a/apps/ingest/tests/test_local.py +++ b/apps/ingest/tests/test_local.py @@ -1,12 +1,11 @@ """ Tests for local ingest """ from os import path, remove -from tempfile import gettempdir import pytest import boto3 from moto import mock_s3 from os.path import join from django.test import TestCase -from django.core.files.uploadedfile import SimpleUploadedFile, TemporaryUploadedFile +from django.core.files.uploadedfile import SimpleUploadedFile from django.conf import settings from apps.iiif.canvases.models import Canvas from apps.iiif.manifests.tests.factories import ManifestFactory, ImageServerFactory From 2625df8c5da1a9465430bef2ec6b28b7ffc7a3f7 Mon Sep 17 00:00:00 2001 From: Ben Silverman Date: Wed, 27 Oct 2021 14:54:20 -0400 Subject: [PATCH 12/43] refactor(test): Consolidate imports --- apps/ingest/tests/test_local.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/ingest/tests/test_local.py b/apps/ingest/tests/test_local.py index 0ac55ba79..733f026a8 100644 --- a/apps/ingest/tests/test_local.py +++ b/apps/ingest/tests/test_local.py @@ -3,7 +3,6 @@ import pytest import boto3 from moto import mock_s3 -from os.path import join from django.test import TestCase from django.core.files.uploadedfile import SimpleUploadedFile from django.conf import settings @@ -20,7 +19,7 @@ class LocalTest(TestCase): """ Tests for ingest.models.Local """ def setUp(self): """ Set instance variables. """ - self.fixture_path = join(settings.APPS_DIR, 'ingest/fixtures/') + self.fixture_path = path.join(settings.APPS_DIR, 'ingest/fixtures/') self.image_server = ImageServerFactory( server_base='http://readux.s3.amazonaws.com', storage_service='s3', @@ -40,7 +39,7 @@ def mock_local(self, bundle, with_manifest=False, metadata={}, from_bulk=False): local.save() file = SimpleUploadedFile( name=bundle, - content=open(join(self.fixture_path, bundle), 'rb').read() + content=open(path.join(self.fixture_path, bundle), 'rb').read() ) if from_bulk: local.bundle_from_bulk.save(bundle, file) From bfd7037b19e6542b440208f88b230c68171b5551 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Oct 2021 19:42:50 +0000 Subject: [PATCH 13/43] build(deps): Bump factory-boy from 3.2.0 to 3.2.1 Bumps [factory-boy](https://github.com/FactoryBoy/factory_boy) from 3.2.0 to 3.2.1. - [Release notes](https://github.com/FactoryBoy/factory_boy/releases) - [Changelog](https://github.com/FactoryBoy/factory_boy/blob/3.2.1/docs/changelog.rst) - [Commits](https://github.com/FactoryBoy/factory_boy/compare/3.2.0...3.2.1) --- updated-dependencies: - dependency-name: factory-boy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements/local.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/local.txt b/requirements/local.txt index 2150a8d63..d483969ed 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -28,7 +28,7 @@ coveralls # Django # ------------------------------------------------------------------------------ -factory-boy==3.2.0 # https://github.com/FactoryBoy/factory_boy +factory-boy==3.2.1 # https://github.com/FactoryBoy/factory_boy faker==9.6.0 django-debug-toolbar==3.2.2 # https://github.com/jazzband/django-debug-toolbar From 607faf6f0b85f354b0125f146c9b4a3dcbc2dfa9 Mon Sep 17 00:00:00 2001 From: Ben Silverman Date: Thu, 28 Oct 2021 14:38:24 -0400 Subject: [PATCH 14/43] feat: Allow attaching collection(s) to bulk ingest --- .../migrations/0015_auto_20211028_1527.py | 18 ++++++ .../migrations/0011_auto_20211028_1527.py | 18 ++++++ .../migrations/0032_auto_20211028_1527.py | 18 ++++++ apps/ingest/admin.py | 7 ++- apps/ingest/forms.py | 2 +- .../migrations/0033_auto_20211028_1527.py | 36 +++++++++++ apps/ingest/models.py | 13 +++- apps/ingest/services.py | 12 +++- apps/ingest/tests/test_admin.py | 61 +++++++++++++++++++ apps/ingest/tests/test_local.py | 4 +- 10 files changed, 181 insertions(+), 8 deletions(-) create mode 100644 apps/iiif/canvases/migrations/0015_auto_20211028_1527.py create mode 100644 apps/iiif/kollections/migrations/0011_auto_20211028_1527.py create mode 100644 apps/iiif/manifests/migrations/0032_auto_20211028_1527.py create mode 100644 apps/ingest/migrations/0033_auto_20211028_1527.py diff --git a/apps/iiif/canvases/migrations/0015_auto_20211028_1527.py b/apps/iiif/canvases/migrations/0015_auto_20211028_1527.py new file mode 100644 index 000000000..9b6e7cf4a --- /dev/null +++ b/apps/iiif/canvases/migrations/0015_auto_20211028_1527.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.24 on 2021-10-28 15:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('canvases', '0014_auto_20211026_1736'), + ] + + operations = [ + migrations.AlterField( + model_name='canvas', + name='pid', + field=models.CharField(default='2qpxp706', help_text="Unique ID. Do not use _'s or spaces in the pid.", max_length=255), + ), + ] diff --git a/apps/iiif/kollections/migrations/0011_auto_20211028_1527.py b/apps/iiif/kollections/migrations/0011_auto_20211028_1527.py new file mode 100644 index 000000000..cb26d18d1 --- /dev/null +++ b/apps/iiif/kollections/migrations/0011_auto_20211028_1527.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.24 on 2021-10-28 15:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('kollections', '0010_auto_20211026_1736'), + ] + + operations = [ + migrations.AlterField( + model_name='collection', + name='pid', + field=models.CharField(default='2qpxp706', help_text="Unique ID. Do not use _'s or spaces in the pid.", max_length=255), + ), + ] diff --git a/apps/iiif/manifests/migrations/0032_auto_20211028_1527.py b/apps/iiif/manifests/migrations/0032_auto_20211028_1527.py new file mode 100644 index 000000000..2e6831216 --- /dev/null +++ b/apps/iiif/manifests/migrations/0032_auto_20211028_1527.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.24 on 2021-10-28 15:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('manifests', '0031_auto_20211026_1736'), + ] + + operations = [ + migrations.AlterField( + model_name='manifest', + name='pid', + field=models.CharField(default='2qpxp706', help_text="Unique ID. Do not use _'s or spaces in the pid.", max_length=255), + ), + ] diff --git a/apps/ingest/admin.py b/apps/ingest/admin.py index 4d6819f77..b99b34954 100644 --- a/apps/ingest/admin.py +++ b/apps/ingest/admin.py @@ -16,7 +16,7 @@ LOGGER = logging.getLogger(__name__) class LocalAdmin(admin.ModelAdmin): """Django admin ingest.models.local resource.""" - fields = ('bundle', 'image_server') + fields = ('bundle', 'image_server', 'collections') show_save_and_add_another = False def save_model(self, request, obj, form, change): @@ -82,6 +82,10 @@ class BulkAdmin(admin.ModelAdmin): form = BulkVolumeUploadForm def save_model(self, request, obj, form, change): + # Save M2M relationships with collections so we can access them later + if form.is_valid(): + form.save(commit=False) + form.save_m2m() obj.save() # Get files from multi upload form files = request.FILES.getlist("volume_files") @@ -104,6 +108,7 @@ def save_model(self, request, obj, form, change): image_server=obj.image_server, creator=request.user ) + new_local.collections.set(obj.collections.all()) if file_meta: new_local.metadata=file_meta diff --git a/apps/ingest/forms.py b/apps/ingest/forms.py index 564e402ca..eb7414535 100644 --- a/apps/ingest/forms.py +++ b/apps/ingest/forms.py @@ -5,7 +5,7 @@ class BulkVolumeUploadForm(forms.ModelForm): class Meta: model = Bulk - fields = ['image_server', 'volume_files'] + fields = ['image_server', 'volume_files', 'collections'] widgets = { 'volume_files': ClearableFileInput(attrs={'multiple': True}), } diff --git a/apps/ingest/migrations/0033_auto_20211028_1527.py b/apps/ingest/migrations/0033_auto_20211028_1527.py new file mode 100644 index 000000000..a10e2faec --- /dev/null +++ b/apps/ingest/migrations/0033_auto_20211028_1527.py @@ -0,0 +1,36 @@ +# Generated by Django 2.2.24 on 2021-10-28 15:27 + +import apps.ingest.models +import apps.ingest.storages +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('kollections', '0011_auto_20211028_1527'), + ('ingest', '0032_auto_20211026_1736'), + ] + + operations = [ + migrations.AddField( + model_name='bulk', + name='collections', + field=models.ManyToManyField(blank=True, help_text='Optional: Collections to attach to ALL volumes ingested in this form.', null=True, to='kollections.Collection'), + ), + migrations.AddField( + model_name='local', + name='collections', + field=models.ManyToManyField(blank=True, help_text='Optional: Collections to attach to the volume ingested in this form.', null=True, to='kollections.Collection'), + ), + migrations.AlterField( + model_name='local', + name='bundle', + field=models.FileField(blank=True, null=True, storage=apps.ingest.storages.IngestStorage(), upload_to=''), + ), + migrations.AlterField( + model_name='local', + name='bundle_from_bulk', + field=models.FileField(blank=True, null=True, upload_to=apps.ingest.models.bulk_path), + ), + ] diff --git a/apps/ingest/models.py b/apps/ingest/models.py index 6dc7ea19e..6af1c2a40 100644 --- a/apps/ingest/models.py +++ b/apps/ingest/models.py @@ -4,7 +4,6 @@ import logging from io import BytesIO from mimetypes import guess_type -from django.core.files.base import ContentFile import httpretty from stream_unzip import stream_unzip, TruncatedDataError from boto3 import client @@ -12,9 +11,11 @@ from django.db import models from django.conf import settings from django.contrib.postgres.fields import JSONField +from django.core.files.base import ContentFile from django_celery_results.models import TaskResult from apps.iiif.canvases.models import Canvas from apps.iiif.canvases.tasks import add_ocr_task +from apps.iiif.kollections.models import Collection from apps.iiif.manifests.models import Manifest, ImageServer from apps.ingest import services from apps.utils.fetch import fetch_url @@ -71,6 +72,11 @@ class Bulk(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) image_server = models.ForeignKey(ImageServer, on_delete=models.DO_NOTHING, null=True) volume_files = models.FileField(blank=False, upload_to=bulk_path) + collections = models.ManyToManyField( + Collection, + blank=True, + help_text="Optional: Collections to attach to ALL volumes ingested in this form." + ) class Meta: verbose_name_plural = 'Bulk' @@ -87,6 +93,11 @@ class Local(IngestAbstractModel): null=True, related_name='created_locals' ) + collections = models.ManyToManyField( + Collection, + blank=True, + help_text="Optional: Collections to attach to the volume ingested in this form." + ) class Meta: verbose_name_plural = 'Local' diff --git a/apps/ingest/services.py b/apps/ingest/services.py index cb7052aa8..18857a6b9 100644 --- a/apps/ingest/services.py +++ b/apps/ingest/services.py @@ -1,11 +1,9 @@ """ Module of service classes and methods for ingest. """ from mimetypes import guess_type -from time import time from urllib.parse import unquote, urlparse from django.apps import apps from tablib.core import Dataset from apps.iiif.manifests.models import Manifest, RelatedLink -from apps.utils.noid import encode_noid def clean_metadata(metadata): """Remove keys that do not aligin with Manifest fields. @@ -54,10 +52,18 @@ def create_manifest(ingest): manifest = Manifest() manifest.image_server = ingest.image_server - manifest.save() # This was giving me a 'django.core.exceptions.AppRegistryNotReady: Models aren't loaded yet' error. Remote = apps.get_model('ingest.remote') + + # Ensure that manifest has an ID before updating the M2M relationship + manifest.save() + if not isinstance(ingest, Remote): + manifest.refresh_from_db() + manifest.collections.set(ingest.collections.all()) + # Save again once relationship is set + manifest.save() + # if type(ingest, .models.Remote): if isinstance(ingest, Remote): RelatedLink( diff --git a/apps/ingest/tests/test_admin.py b/apps/ingest/tests/test_admin.py index 615e1faae..7b20eab8e 100644 --- a/apps/ingest/tests/test_admin.py +++ b/apps/ingest/tests/test_admin.py @@ -11,6 +11,8 @@ from moto import mock_s3 from apps.ingest.forms import BulkVolumeUploadForm from apps.iiif.canvases.models import Canvas +from apps.iiif.kollections.models import Collection +from apps.iiif.kollections.tests.factories import CollectionFactory from apps.iiif.manifests.models import Manifest from apps.iiif.manifests.tests.factories import ManifestFactory, ImageServerFactory from apps.ingest.models import Bulk, Local, Remote, IngestTaskWatcher @@ -80,6 +82,42 @@ def test_local_admin_save(self): assert Manifest.objects.count() == original_manifest_count + 1 assert Canvas.objects.count() == original_canvas_count + 10 + def test_local_ingest_with_collections(self): + """It should add chosen collections to the Local's manifests""" + local = LocalFactory.build( + image_server=self.image_server + ) + + # Force evaluation to get the true current list of manifests + manifests_before = list(Manifest.objects.all()) + + # Assign collections to Local + for _ in range(3): + CollectionFactory.create() + collections = Collection.objects.all() + local.save() + local.collections.set(collections) + assert len(local.collections.all()) == 3 + + # Make a local ingest + request_factory = RequestFactory() + with open(join(self.fixture_path, 'no_meta_file.zip'), 'rb') as f: + content = files.base.ContentFile(f.read()) + local.bundle = files.File(content.file, 'no_meta_file.zip') + req = request_factory.post('/admin/ingest/local/add/', data={}) + req.user = self.user + local_model_admin = LocalAdmin(model=Local, admin_site=AdminSite()) + local_model_admin.save_model(obj=local, request=req, form=None, change=None) + + # Get the newly created manifest by comparing current list to the list before + manifests_after = list(Manifest.objects.all()) + new_manifests = [x for x in manifests_after if x not in manifests_before] + assert len(new_manifests) == 1 + assert isinstance(new_manifests[0], Manifest) + + # The new manifest shouhld get the Local's collections + assert new_manifests[0].collections.count() == 3 + def test_local_admin_response_add(self): """It should redirect to new manifest""" @@ -230,6 +268,29 @@ def test_task_watcher_admin_functions(self): assert watcher_admin.task_name(watcher) == 'fake_task' assert 'PENDING' in watcher_admin.task_status(watcher) + def test_bulk_ingest_with_collections(self): + """It should add the collections to the matching Local object""" + bulk = BulkFactory.create() + for _ in range(3): + CollectionFactory.create() + collections = Collection.objects.all() + bulk.save() + bulk.collections.set(collections) + data = {} + data['volume_files'] = [bulk.volume_files] + + request_factory = RequestFactory() + req = request_factory.post('/admin/ingest/bulk/add/', data=data) + req.user = self.user + + bulk_model_admin = BulkAdmin(model=Bulk, admin_site=AdminSite()) + mock_form = BulkVolumeUploadForm() + bulk_model_admin.save_model(obj=bulk, request=req, form=mock_form, change=None) + bulk.refresh_from_db() + created_local = bulk.local_uploads.first() + + assert created_local.collections.count() == 3 + # def test_local_admin_save_update_manifest(self): # """It should add a manifest to the Local object""" diff --git a/apps/ingest/tests/test_local.py b/apps/ingest/tests/test_local.py index 733f026a8..d676f588e 100644 --- a/apps/ingest/tests/test_local.py +++ b/apps/ingest/tests/test_local.py @@ -81,13 +81,13 @@ def test_ocr_upload_to_s3(self): assert f'{local.manifest.pid}/_*ocr*_/00000008.tsv' in ocr_files def test_metadata_from_excel(self): - """ It should create a manifest with metadat supplied in an Excel file. """ + """ It should create a manifest with metadata supplied in an Excel file. """ local = self.mock_local('bundle.zip', with_manifest=True) assert 'pid' in local.metadata.keys() for key in local.metadata.keys(): - assert local.metadata[key] == getattr(local.manifest, key) + assert str(local.metadata[key]) == str(getattr(local.manifest, key)) def test_metadata_from_csv(self): """ It should create a manifest with metadata supplied in a CSV file. """ From 670c7b65800cda4c6dfab23079467ad0dcd7f6d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 10:20:04 +0000 Subject: [PATCH 15/43] build(deps): Bump django-storages from 1.12.2 to 1.12.3 Bumps [django-storages](https://github.com/jschneier/django-storages) from 1.12.2 to 1.12.3. - [Release notes](https://github.com/jschneier/django-storages/releases) - [Changelog](https://github.com/jschneier/django-storages/blob/master/CHANGELOG.rst) - [Commits](https://github.com/jschneier/django-storages/compare/1.12.2...1.12.3) --- updated-dependencies: - dependency-name: django-storages dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index be0a2af88..af15a7395 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -56,7 +56,7 @@ django-background-tasks==1.2.5 # S3 Uploads boto3==1.19.4 -django-storages==1.12.2 # https://github.com/jschneier/django-storages +django-storages==1.12.3 # https://github.com/jschneier/django-storages # Python function to stream unzip all the files in a ZIP archive, without loading the entire ZIP file into memory or any of its uncompressed files. stream-unzip>=0.0.58 From 31063d558d770851ce5a1e019435f5b624f4309f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 10:20:15 +0000 Subject: [PATCH 16/43] build(deps): Bump moto from 2.2.11 to 2.2.12 Bumps [moto](https://github.com/spulec/moto) from 2.2.11 to 2.2.12. - [Release notes](https://github.com/spulec/moto/releases) - [Changelog](https://github.com/spulec/moto/blob/master/CHANGELOG.md) - [Commits](https://github.com/spulec/moto/compare/2.2.11...2.2.12) --- updated-dependencies: - dependency-name: moto dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements/local.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/local.txt b/requirements/local.txt index d483969ed..737c366d0 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -18,7 +18,7 @@ httpretty==1.1.4 # https://pypi.org/project/httpretty/ #mock==4.0.2 cssutils==2.3.0 # https://pypi.org/project/cssutils/ pytest-django==4.4.0 # https://github.com/pytest-dev/pytest-django -moto==2.2.11 # https://github.com/spulec/moto +moto==2.2.12 # https://github.com/spulec/moto # Code quality # ------------------------------------------------------------------------------ From 3ad49a8d81762ba3e6bfda429c37b3aee9682511 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Nov 2021 10:49:21 +0000 Subject: [PATCH 17/43] build(deps): Bump faker from 9.6.0 to 9.8.0 Bumps [faker](https://github.com/joke2k/faker) from 9.6.0 to 9.8.0. - [Release notes](https://github.com/joke2k/faker/releases) - [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) - [Commits](https://github.com/joke2k/faker/compare/v9.6.0...v9.8.0) --- updated-dependencies: - dependency-name: faker dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements/local.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/local.txt b/requirements/local.txt index d483969ed..e28c0d147 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -29,7 +29,7 @@ coveralls # Django # ------------------------------------------------------------------------------ factory-boy==3.2.1 # https://github.com/FactoryBoy/factory_boy -faker==9.6.0 +faker==9.8.0 django-debug-toolbar==3.2.2 # https://github.com/jazzband/django-debug-toolbar django-extensions==3.1.3 # https://github.com/django-extensions/django-extensions From 870b90598dee651658242bb3180c4467a4c60db4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Nov 2021 16:06:00 +0000 Subject: [PATCH 18/43] build(deps): Bump boto3 from 1.19.4 to 1.19.9 Bumps [boto3](https://github.com/boto/boto3) from 1.19.4 to 1.19.9. - [Release notes](https://github.com/boto/boto3/releases) - [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst) - [Commits](https://github.com/boto/boto3/compare/1.19.4...1.19.9) --- updated-dependencies: - dependency-name: boto3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index af15a7395..dfe077c63 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -55,7 +55,7 @@ gitpython==3.1.24 django-background-tasks==1.2.5 # S3 Uploads -boto3==1.19.4 +boto3==1.19.9 django-storages==1.12.3 # https://github.com/jschneier/django-storages # Python function to stream unzip all the files in a ZIP archive, without loading the entire ZIP file into memory or any of its uncompressed files. From 6cf730e2619d19c33ebd2b76955f318eb1fbe304 Mon Sep 17 00:00:00 2001 From: Ben Silverman Date: Wed, 3 Nov 2021 17:04:28 -0400 Subject: [PATCH 19/43] feat: Action/form to add manifests to collections --- apps/iiif/manifests/admin.py | 24 ++++++++++++ apps/iiif/manifests/forms.py | 17 ++++++++ .../add_manifests_to_collections.html | 38 ++++++++++++++++++ apps/iiif/manifests/views.py | 39 ++++++++++++++++++- 4 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 apps/iiif/manifests/templates/add_manifests_to_collections.html diff --git a/apps/iiif/manifests/admin.py b/apps/iiif/manifests/admin.py index 459537181..3b4072836 100644 --- a/apps/iiif/manifests/admin.py +++ b/apps/iiif/manifests/admin.py @@ -1,11 +1,15 @@ """Django admin module for maninfests""" from django.contrib import admin +from django.http import HttpResponseRedirect +from django.urls.conf import path from import_export import resources, fields from import_export.admin import ImportExportModelAdmin from import_export.widgets import ManyToManyWidget, ForeignKeyWidget from django_summernote.admin import SummernoteModelAdmin + from .models import Manifest, Note, ImageServer from .forms import ManifestAdminForm +from .views import AddToCollectionsView from ..kollections.models import Collection class ManifestResource(resources.ModelResource): @@ -38,6 +42,26 @@ class ManifestAdmin(ImportExportModelAdmin, SummernoteModelAdmin, admin.ModelAdm search_fields = ('id', 'label', 'author', 'published_date') summernote_fields = ('summary',) form = ManifestAdminForm + actions = ['add_to_collections_action'] + + def add_to_collections_action(self, request, queryset): + """Action choose manifests to add to collections""" + selected = queryset.values_list('pk', flat=True) + selected_ids = ','.join(str(pk) for pk in selected) + return HttpResponseRedirect(f'add_to_collections/?ids={selected_ids}') + add_to_collections_action.short_description = 'Add selected manifests to collection(s)' + + def get_urls(self): + urls = super().get_urls() + my_urls = [ + path( + 'add_to_collections/', + self.admin_site.admin_view(AddToCollectionsView.as_view()), + {'model_admin': self, }, + name="AddManifestsToCollections", + ) + ] + return my_urls + urls class NoteAdmin(admin.ModelAdmin): """Django admin configuration for a note.""" diff --git a/apps/iiif/manifests/forms.py b/apps/iiif/manifests/forms.py index c03257548..f8ef13bc6 100644 --- a/apps/iiif/manifests/forms.py +++ b/apps/iiif/manifests/forms.py @@ -1,6 +1,9 @@ """Django Forms for export.""" import logging from django import forms +from django.contrib.admin import site as admin_site, widgets + +from apps.iiif.kollections.models import Collection from .models import Manifest from ..canvases.models import Canvas @@ -84,3 +87,17 @@ def __init__(self, *args, **kwargs): self.fields['start_canvas'].queryset = kwargs['instance'].canvas_set.all() else: self.fields['start_canvas'].queryset = Canvas.objects.none() + +class ManifestsCollectionsForm(forms.ModelForm): + """Form to add manifests to collections.""" + class Meta: + model = Manifest + fields=('collections',) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['collections'].widget = widgets.RelatedFieldWidgetWrapper( + self.fields['collections'].widget, + self.instance._meta.get_field('collections').remote_field, + admin_site, + ) diff --git a/apps/iiif/manifests/templates/add_manifests_to_collections.html b/apps/iiif/manifests/templates/add_manifests_to_collections.html new file mode 100644 index 000000000..3aefa8236 --- /dev/null +++ b/apps/iiif/manifests/templates/add_manifests_to_collections.html @@ -0,0 +1,38 @@ +{% extends "admin/base_site.html" %} +{% load i18n admin_urls %} +{% load static %} + +{% block extrastyle %} + +{% endblock %} + +{% block content %} +
{% csrf_token %} +
+
+
+ +
+
    + {% for manifest in manifests %} +
  • + {% trans 'unlabeled' as unlabeled %} + {{ manifest.label|default:unlabeled }} + ({{ manifest.pk }}) +
  • + {% endfor %} +
+
+
+
+
+ {{ form }} +
+
+ {% block submit_buttons_bottom %} +
+ +
+ {% endblock %} +
+{% endblock %} \ No newline at end of file diff --git a/apps/iiif/manifests/views.py b/apps/iiif/manifests/views.py index 32e0da38c..70e1d325f 100644 --- a/apps/iiif/manifests/views.py +++ b/apps/iiif/manifests/views.py @@ -2,18 +2,21 @@ import json import logging from datetime import datetime +from django.contrib import messages from django.http import JsonResponse from django.http import HttpResponse from django.shortcuts import render +from django.template.response import TemplateResponse from django.views import View from django.views.generic.base import TemplateView +from django.views.generic.edit import FormView from django.core.serializers import serialize from django.contrib.sitemaps import Sitemap from django.urls import reverse from ..canvases.models import Canvas from .models import Manifest from .export import IiifManifestExport -from .forms import JekyllExportForm +from .forms import JekyllExportForm, ManifestsCollectionsForm from .tasks import github_export_task from .tasks import download_export_task @@ -198,3 +201,37 @@ def get(self, request, *args, **kwargs): for canvas in self.get_queryset() : annotations.append(canvas.result) return HttpResponse(' '.join(annotations)) + +class AddToCollectionsView(FormView): + """Intermediate page to choose collections to which you are adding manifests""" + + template_name = 'add_manifests_to_collections.html' + form_class = ManifestsCollectionsForm + + def get_context_data(self, **kwargs): + ids = self.request.GET.get('ids', '').split(',') + manifests = Manifest.objects.filter(pk__in=ids) + model_admin = self.kwargs['model_admin'] + context = super().get_context_data(**kwargs) + context['model_admin'] = model_admin.admin_site.each_context(self.request) + context['manifests'] = manifests + context['title'] = 'Add selected manifests to collection(s)' + return context + + def form_valid(self, form): + self.add_manifests_to_collections(form) + return super().form_valid(form) + + def add_manifests_to_collections(self, form): + context = self.get_context_data() + manifests = context['manifests'] + collections = form.cleaned_data['collections'] + for manifest in manifests: + manifest.collections.add(*collections) + manifest.save() + + def get_success_url(self): + messages.add_message( + self.request, messages.SUCCESS, 'Successfully added manifests to collections' + ) + return reverse('admin:manifests_manifest_changelist') From 76b3af61674e8a7017d87a2f3852b9edfa6442c8 Mon Sep 17 00:00:00 2001 From: Ben Silverman Date: Thu, 4 Nov 2021 10:31:12 -0400 Subject: [PATCH 20/43] feat(test): Basic tests for adding to collections --- apps/iiif/manifests/admin.py | 1 - apps/iiif/manifests/tests/test_views.py | 53 +++++++++++++++++++++++-- apps/iiif/manifests/views.py | 4 +- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/apps/iiif/manifests/admin.py b/apps/iiif/manifests/admin.py index 3b4072836..64d496621 100644 --- a/apps/iiif/manifests/admin.py +++ b/apps/iiif/manifests/admin.py @@ -6,7 +6,6 @@ from import_export.admin import ImportExportModelAdmin from import_export.widgets import ManyToManyWidget, ForeignKeyWidget from django_summernote.admin import SummernoteModelAdmin - from .models import Manifest, Note, ImageServer from .forms import ManifestAdminForm from .views import AddToCollectionsView diff --git a/apps/iiif/manifests/tests/test_views.py b/apps/iiif/manifests/tests/test_views.py index 673fc2120..1a62c2250 100644 --- a/apps/iiif/manifests/tests/test_views.py +++ b/apps/iiif/manifests/tests/test_views.py @@ -2,21 +2,24 @@ ''' import json from datetime import datetime -from time import sleep from django.test import TestCase, Client from django.test import RequestFactory from django.conf import settings +from django.contrib.admin.sites import AdminSite from django.contrib.auth import get_user_model from django.urls import reverse from django.core.serializers import serialize from allauth.socialaccount.models import SocialAccount -from iiif_prezi.loader import ManifestReader -from ..views import ManifestSitemap, ManifestRis + +from ..admin import ManifestAdmin +from ..views import AddToCollectionsView, ManifestSitemap, ManifestRis from ..models import Manifest -from ..forms import JekyllExportForm +from ..forms import JekyllExportForm, ManifestsCollectionsForm from .factories import ManifestFactory, EmptyManifestFactory from ...canvases.models import Canvas from ...canvases.tests.factories import CanvasFactory +from ...kollections.models import Collection +from ...kollections.tests.factories import CollectionFactory USER = get_user_model() @@ -137,3 +140,45 @@ def test_no_starting_canvases(self): ) ) assert manifest.canvas_set.all().first().pid in serialized_manifest['thumbnail']['@id'] + + # TODO: Test with 0 manifests, 1 manifests, non-manifest obj + def test_add_to_collections_view_context(self): + """it should add the passed manifests to view context""" + manifest1 = ManifestFactory.create() + manifest2 = ManifestFactory.create() + ids = ','.join([str(manifest1.pk), str(manifest2.pk)]) + request = self.factory.get( + reverse('admin:manifests_manifest_changelist') + 'add_to_collections/?ids=' + ids + ) + request.user = self.user + view = AddToCollectionsView() + model_admin = ManifestAdmin(model=Manifest, admin_site=AdminSite()) + view.setup(request, model_admin=model_admin) + + context = view.get_context_data() + self.assertIn('manifests', context) + self.assertIn(manifest1, context['manifests']) + self.assertIn(manifest2, context['manifests']) + + def test_add_to_collections_view_function(self): + """it should add the passed manifests to the selected collections""" + manifest1 = ManifestFactory.create() + manifest2 = ManifestFactory.create() + collection1 = CollectionFactory.create() + collection2 = CollectionFactory.create() + ids = ','.join([str(manifest1.pk), str(manifest2.pk)]) + request = self.factory.get( + reverse('admin:manifests_manifest_changelist') + 'add_to_collections/?ids=' + ids + ) + request.user = self.user + view = AddToCollectionsView() + model_admin = ManifestAdmin(model=Manifest, admin_site=AdminSite()) + view.setup(request, model_admin=model_admin) + qs = Collection.objects.filter(pk__in=[collection1.pk, collection2.pk]) + form = ManifestsCollectionsForm(data={"collections": qs}) + + assert len(manifest1.collections.all()) == 0 + assert len(manifest2.collections.all()) == 0 + view.add_manifests_to_collections(form) + assert len(manifest1.collections.all()) == 2 + assert len(manifest2.collections.all()) == 2 diff --git a/apps/iiif/manifests/views.py b/apps/iiif/manifests/views.py index 70e1d325f..c03a4a4af 100644 --- a/apps/iiif/manifests/views.py +++ b/apps/iiif/manifests/views.py @@ -223,9 +223,11 @@ def form_valid(self, form): return super().form_valid(form) def add_manifests_to_collections(self, form): + """Adds selected manifests to selected collections from form""" context = self.get_context_data() manifests = context['manifests'] - collections = form.cleaned_data['collections'] + if form.is_valid(): + collections = form.cleaned_data['collections'] for manifest in manifests: manifest.collections.add(*collections) manifest.save() From 882787b0ff67bde91acd187a75f82a51e5fbd866 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Nov 2021 10:30:36 +0000 Subject: [PATCH 21/43] build(deps): Bump boto3 from 1.19.9 to 1.19.11 Bumps [boto3](https://github.com/boto/boto3) from 1.19.9 to 1.19.11. - [Release notes](https://github.com/boto/boto3/releases) - [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst) - [Commits](https://github.com/boto/boto3/compare/1.19.9...1.19.11) --- updated-dependencies: - dependency-name: boto3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index dfe077c63..7b09dcee3 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -55,7 +55,7 @@ gitpython==3.1.24 django-background-tasks==1.2.5 # S3 Uploads -boto3==1.19.9 +boto3==1.19.11 django-storages==1.12.3 # https://github.com/jschneier/django-storages # Python function to stream unzip all the files in a ZIP archive, without loading the entire ZIP file into memory or any of its uncompressed files. From dd8ad25cf7426fd00e7c66b2497e53fab67645ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Nov 2021 10:21:46 +0000 Subject: [PATCH 22/43] build(deps): Bump django-extensions from 3.1.3 to 3.1.5 Bumps [django-extensions](https://github.com/django-extensions/django-extensions) from 3.1.3 to 3.1.5. - [Release notes](https://github.com/django-extensions/django-extensions/releases) - [Changelog](https://github.com/django-extensions/django-extensions/blob/main/CHANGELOG.md) - [Commits](https://github.com/django-extensions/django-extensions/compare/3.1.3...3.1.5) --- updated-dependencies: - dependency-name: django-extensions dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements/local.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/local.txt b/requirements/local.txt index 9a0249c22..77d2af942 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -32,7 +32,7 @@ factory-boy==3.2.1 # https://github.com/FactoryBoy/factory_boy faker==9.8.0 django-debug-toolbar==3.2.2 # https://github.com/jazzband/django-debug-toolbar -django-extensions==3.1.3 # https://github.com/django-extensions/django-extensions +django-extensions==3.1.5 # https://github.com/django-extensions/django-extensions django-coverage-plugin==2.0.1 # https://github.com/nedbat/django_coverage_plugin pyopenssl # for running dev server under https From 562ee607d8aeb91db11cd3c039593ad495500395 Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Wed, 10 Nov 2021 10:27:42 -0500 Subject: [PATCH 23/43] Add test for Jekyll export error --- .coveragerc | 2 +- .gitignore | 3 +-- apps/iiif/manifests/export.py | 2 +- apps/readux/tests/test_export.py | 11 ++++++++++- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.coveragerc b/.coveragerc index 4ae91d22f..48f76908b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -11,7 +11,7 @@ omit = exclude_lines = # Ignore imports from - import + import\s logger LOGGER pragma: no cover \ No newline at end of file diff --git a/.gitignore b/.gitignore index 295e976e4..cc2843b83 100644 --- a/.gitignore +++ b/.gitignore @@ -67,11 +67,10 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ -.coverage .cache nosetests.xml coverage.xml -.coveragerc +.coverage* # Packages *.egg *.egg-info diff --git a/apps/iiif/manifests/export.py b/apps/iiif/manifests/export.py index 03f61a5cc..4145f017d 100644 --- a/apps/iiif/manifests/export.py +++ b/apps/iiif/manifests/export.py @@ -673,7 +673,7 @@ def update_gitrepo(self): if os.environ['DJANGO_ENV'] != 'test': # run the script to import IIIF as jekyll site content - self.import_iiif_jekyll(self.manifest, self.jekyll_site_dir) + self.import_iiif_jekyll(self.manifest, self.jekyll_site_dir) # pragma: no cover # add any files that could be updated to the git index repo.index.add([ # pragma: no cover diff --git a/apps/readux/tests/test_export.py b/apps/readux/tests/test_export.py index bd9942ef0..46e98a49c 100644 --- a/apps/readux/tests/test_export.py +++ b/apps/readux/tests/test_export.py @@ -14,7 +14,7 @@ from apps.iiif.manifests.models import Manifest from apps.iiif.manifests.views import ManifestExport, JekyllExport from apps.iiif.canvases.models import Canvas -from apps.iiif.manifests.export import IiifManifestExport, JekyllSiteExport, GithubExportException +from apps.iiif.manifests.export import IiifManifestExport, JekyllSiteExport, GithubExportException, ExportException from apps.iiif.manifests.github import GithubApi from apps.users.tests.factories import UserFactory, SocialAccountFactory, SocialAppFactory, SocialTokenFactory from iiif_prezi.loader import ManifestReader @@ -121,6 +121,15 @@ def test_jekyll_site_export(self): # verify user annotation count is correct assert len(os.listdir(os.path.join(jekyll_path, '_annotations'))) == 1 + def test_jekyll_export_error(self): + export = JekyllSiteExport(self.volume, 'v2', owners=[self.user.id], deep_zoom='exclude') + export.jekyll_site_dir = 'nope' + try: + export.import_iiif_jekyll(self.volume, 'non_existing_directory') + assert False + except ExportException: + assert True + def test_get_zip_file(self): # Make an empty file dummy_file = os.path.join(tempfile.tempdir, 'file.txt') From df70aa051b995e48be682bc03abf35046e042b31 Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Wed, 10 Nov 2021 12:59:00 -0500 Subject: [PATCH 24/43] Move export tasks to new app and convert to celery --- apps/export/__init__.py | 0 apps/export/apps.py | 6 ++++ apps/export/celery.py | 16 ++++++++++ apps/{iiif/manifests => export}/export.py | 0 apps/{iiif/manifests => export}/github.py | 0 apps/{iiif/manifests => export}/tasks.py | 18 ++++++----- apps/{readux => export}/tests/test_export.py | 4 +-- apps/{readux => export}/tests/test_github.py | 4 +-- apps/iiif/canvases/tasks.py | 1 - apps/iiif/manifests/views.py | 10 +++--- .../commands/restart_background_tasks.py | 31 ------------------- apps/readux/views.py | 2 +- config/settings/base.py | 3 +- requirements/base.txt | 2 -- 14 files changed, 45 insertions(+), 52 deletions(-) create mode 100644 apps/export/__init__.py create mode 100644 apps/export/apps.py create mode 100644 apps/export/celery.py rename apps/{iiif/manifests => export}/export.py (100%) rename apps/{iiif/manifests => export}/github.py (100%) rename apps/{iiif/manifests => export}/tasks.py (82%) rename apps/{readux => export}/tests/test_export.py (98%) rename apps/{readux => export}/tests/test_github.py (97%) delete mode 100644 apps/readux/management/commands/restart_background_tasks.py diff --git a/apps/export/__init__.py b/apps/export/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/export/apps.py b/apps/export/apps.py new file mode 100644 index 000000000..e990d32c8 --- /dev/null +++ b/apps/export/apps.py @@ -0,0 +1,6 @@ +"""Django app configuration for Export""" +from django.apps import AppConfig + +class ExportConfig(AppConfig): + """Configuration for Export Django app""" + name = 'apps.export' diff --git a/apps/export/celery.py b/apps/export/celery.py new file mode 100644 index 000000000..7dfbe4c55 --- /dev/null +++ b/apps/export/celery.py @@ -0,0 +1,16 @@ +""" +Celery config for ingest tasks. +""" +import os +from celery import Celery +from django.conf import settings +# import config.settings.local as settings + +# set the default Django settings module for the 'celery' program. +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.local') +app = Celery('apps.export', result_extended=True) + +# Using a string here means the worker will not have to +# pickle the object when using Windows. +app.config_from_object('django.conf:settings') +app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) diff --git a/apps/iiif/manifests/export.py b/apps/export/export.py similarity index 100% rename from apps/iiif/manifests/export.py rename to apps/export/export.py diff --git a/apps/iiif/manifests/github.py b/apps/export/github.py similarity index 100% rename from apps/iiif/manifests/github.py rename to apps/export/github.py diff --git a/apps/iiif/manifests/tasks.py b/apps/export/tasks.py similarity index 82% rename from apps/iiif/manifests/tasks.py rename to apps/export/tasks.py index d1946443e..6966dbc0c 100644 --- a/apps/iiif/manifests/tasks.py +++ b/apps/export/tasks.py @@ -1,15 +1,19 @@ """Module to manage background export task.""" import logging import os -from background_task import background +from celery import Celery +from django.conf import settings from apps.users.models import User +from apps.iiif.manifests.models import Manifest from .export import JekyllSiteExport -from .models import Manifest - LOGGER = logging.getLogger(__name__) -@background(schedule=1) +app = Celery('apps.ingest', result_extended=True) +app.config_from_object('django.conf:settings') +app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) + +@app.task(name='github_export', autoretry_for=(Exception,), retry_backoff=True, max_retries=20) def github_export_task( manifest_pid, version, github_repo=None, user_id=None, owner_ids=None, deep_zoom=False): @@ -45,7 +49,7 @@ def github_export_task( jekyll_exporter.github_export(user.email) LOGGER.info('Background github export finished.') -@background(schedule=1) +@app.task(name='download_export', autoretry_for=(Exception,), retry_backoff=True, max_retries=20) def download_export_task( manifest_pid, version, github_repo=None, user_id=None, owner_ids=None, deep_zoom=False): @@ -79,10 +83,10 @@ def download_export_task( ) zipfile_name = jekyll_exporter.download_export(user.email, manifest) - delete_download_task(zipfile_name) + delete_download_task.apply_async((zipfile_name), countdown=86400) LOGGER.info('Background download export finished.') -@background(schedule=86400) +@app.task(name='delete_download', autoretry_for=(Exception,), retry_backoff=True, max_retries=20) def delete_download_task(download_path): """Background delete download task. diff --git a/apps/readux/tests/test_export.py b/apps/export/tests/test_export.py similarity index 98% rename from apps/readux/tests/test_export.py rename to apps/export/tests/test_export.py index 46e98a49c..d59aa58bb 100644 --- a/apps/readux/tests/test_export.py +++ b/apps/export/tests/test_export.py @@ -14,8 +14,8 @@ from apps.iiif.manifests.models import Manifest from apps.iiif.manifests.views import ManifestExport, JekyllExport from apps.iiif.canvases.models import Canvas -from apps.iiif.manifests.export import IiifManifestExport, JekyllSiteExport, GithubExportException, ExportException -from apps.iiif.manifests.github import GithubApi +from apps.export.export import IiifManifestExport, JekyllSiteExport, GithubExportException, ExportException +from apps.export.github import GithubApi from apps.users.tests.factories import UserFactory, SocialAccountFactory, SocialAppFactory, SocialTokenFactory from iiif_prezi.loader import ManifestReader diff --git a/apps/readux/tests/test_github.py b/apps/export/tests/test_github.py similarity index 97% rename from apps/readux/tests/test_github.py rename to apps/export/tests/test_github.py index ae5b544c2..162ab2633 100644 --- a/apps/readux/tests/test_github.py +++ b/apps/export/tests/test_github.py @@ -1,7 +1,7 @@ from django.test import TestCase -from apps.iiif.manifests.github import GithubApi, GithubApiException, GithubAccountNotFound -from apps.users.tests.factories import UserFactory, SocialAccountFactory, SocialAppFactory, SocialTokenFactory import httpretty +from apps.users.tests.factories import UserFactory, SocialAccountFactory, SocialAppFactory, SocialTokenFactory +from apps.export.github import GithubApi, GithubApiException, GithubAccountNotFound class TestGithubApi(TestCase): def setUp(self): diff --git a/apps/iiif/canvases/tasks.py b/apps/iiif/canvases/tasks.py index e447d9314..778806d5b 100644 --- a/apps/iiif/canvases/tasks.py +++ b/apps/iiif/canvases/tasks.py @@ -1,6 +1,5 @@ """ Common tasks for canvases. """ from celery import Celery -from background_task import background from django.apps import apps from ..annotations.models import Annotation from .models import Canvas diff --git a/apps/iiif/manifests/views.py b/apps/iiif/manifests/views.py index c03a4a4af..b6a37b7b3 100644 --- a/apps/iiif/manifests/views.py +++ b/apps/iiif/manifests/views.py @@ -15,10 +15,10 @@ from django.urls import reverse from ..canvases.models import Canvas from .models import Manifest -from .export import IiifManifestExport +from apps.export.export import IiifManifestExport from .forms import JekyllExportForm, ManifestsCollectionsForm -from .tasks import github_export_task -from .tasks import download_export_task +from apps.export.tasks import github_export_task +from apps.export.tasks import download_export_task LOGGER = logging.getLogger(__name__) @@ -149,7 +149,7 @@ def post(self, request, *args, **kwargs): # pylint: disable = unused-argument if export_mode == 'download': context = self.get_context_data() manifest_pid = manifest.pid - download_export_task( + download_export_task.delay( manifest_pid, kwargs['version'], github_repo=github_repo, @@ -164,7 +164,7 @@ def post(self, request, *args, **kwargs): # pylint: disable = unused-argument #github exports context = self.get_context_data() manifest_pid = manifest.pid - github_export_task( + github_export_task.delay( manifest_pid, kwargs['version'], github_repo=github_repo, diff --git a/apps/readux/management/commands/restart_background_tasks.py b/apps/readux/management/commands/restart_background_tasks.py deleted file mode 100644 index 8dd8dd3e1..000000000 --- a/apps/readux/management/commands/restart_background_tasks.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Manage command to restart background tasks. -""" -from os import kill -from signal import SIGKILL -from subprocess import check_output, Popen -from django.core.management.base import BaseCommand -from background_task.models import CompletedTask - -class Command(BaseCommand): - """ - Manage command that looks up system pids for the background tasks and kills them. - - Finally, it starts three new processes for the background tasks. - - TODO: Instead of assuming all background processes have been locked by a completed task, - we might should look to some other way to track the processes. The danger is that an - outdated process will pickup a job and it will be hard to figure out why something - bad happened. - """ - def handle(self, *args, **options): - pids = [task.locked_by for task in CompletedTask.objects.all()] - for pid in pids: - try: - kill(pid, SIGKILL) - except ProcessLookupError: - pass - - python_path = check_output(['which', 'python']).decode('utf-8').rstrip() - for _ in range(3): - Popen(['nohup', python_path, 'manage.py', 'process_tasks']) diff --git a/apps/readux/views.py b/apps/readux/views.py index 118af9f4b..e624b9ce0 100644 --- a/apps/readux/views.py +++ b/apps/readux/views.py @@ -11,13 +11,13 @@ from django.urls import reverse from django.utils.datastructures import MultiValueDictKeyError import config.settings.local as settings +from apps.export.export import JekyllSiteExport from .models import UserAnnotation from ..cms.models import Page, CollectionsPage, VolumesPage from ..iiif.kollections.models import Collection from ..iiif.canvases.models import Canvas from ..iiif.manifests.models import Manifest from ..iiif.manifests.forms import JekyllExportForm -from ..iiif.manifests.export import JekyllSiteExport SORT_OPTIONS = ['title', 'author', 'date published', 'date added'] ORDER_OPTIONS = ['asc', 'desc'] diff --git a/config/settings/base.py b/config/settings/base.py index ce493a5a5..bc75125e0 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -114,7 +114,8 @@ 'apps.cms.apps.CmsConfig', 'apps.templates', 'apps.custom_styles.apps.CustomStylesConfig', - 'apps.ingest.apps.IngestConfig' + 'apps.ingest.apps.IngestConfig', + 'apps.export.apps.ExportConfig' ] # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS diff --git a/requirements/base.txt b/requirements/base.txt index 7b09dcee3..a1f94c126 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -52,8 +52,6 @@ wagtail-cache==1.0.2 django-import-export==2.6.1 gitpython==3.1.24 -django-background-tasks==1.2.5 - # S3 Uploads boto3==1.19.11 django-storages==1.12.3 # https://github.com/jschneier/django-storages From fed291cb0dd97ea855e006488636a18caa8bce95 Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Wed, 10 Nov 2021 13:10:04 -0500 Subject: [PATCH 25/43] Update deploy to mount extra volume and remove restart background task --- fabfile.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/fabfile.py b/fabfile.py index fcdf11d7d..427ba0422 100644 --- a/fabfile.py +++ b/fabfile.py @@ -8,7 +8,7 @@ env.user = 'deploy' -def deploy(branch='release', path='/readux.io/readux'): +def deploy(branch='release', path='/readux.io/readux', volume=None): """Execute group of tasks for deployment. :param branch: Git branch to clone, defaults to 'master' @@ -30,7 +30,9 @@ def deploy(branch='release', path='/readux.io/readux'): _get_latest_source(branch, options) _update_virtualenv(options) _link_settings(options) - _create_static_media_symlinks(options) + _create_staticfiles_symlink(options) + if volume is not None: + _mount_media(volume) _update_static_files(options) _update_database(options) _update_symlink(options) @@ -63,7 +65,11 @@ def _link_settings(options): with cd('config/settings'): run('ln -s {rp}/local.py local.py'.format(rp=options['ROOT_PATH'])) -def _create_static_media_symlinks(options): +def _mount_media(volume): + with cd('apps'): + run(f'mkdir media && sudo mount /dev/{volume} media') + +def _create_staticfiles_symlink(options): run('ln -s {rp}/staticfiles staticfiles'.format(rp=options['ROOT_PATH'])) with cd('apps'): run('ln -s {rp}/media media'.format(rp=options['ROOT_PATH'])) @@ -84,8 +90,6 @@ def _restart_webserver(): run('sudo /bin/systemctl reload apache2') def _restart_background_tasks(options): - with cd(options['ROOT_PATH']): - run('nohup ./restart_export_tasks.sh &') run('sudo /bin/systemctl restart celeryd') def _clean_old_builds(options): From 61cf673f0f9e737c2cc36b8b3414cc38fa55eb22 Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Wed, 10 Nov 2021 13:16:58 -0500 Subject: [PATCH 26/43] Remove references to background_tasks --- apps/ingest/tasks.py | 5 ----- config/settings/base.py | 1 - fabfile.py | 4 ++-- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/ingest/tasks.py b/apps/ingest/tasks.py index bcc07a8cb..adfb66b88 100644 --- a/apps/ingest/tasks.py +++ b/apps/ingest/tasks.py @@ -19,11 +19,6 @@ Remote = apps.get_model('ingest.remote') LOGGER = logging.getLogger(__name__) -logging.getLogger("background_task").setLevel(logging.ERROR) -logging.getLogger('boto3').setLevel(logging.ERROR) -logging.getLogger('botocore').setLevel(logging.ERROR) -logging.getLogger('s3transfer').setLevel(logging.ERROR) -logging.getLogger('factory').setLevel(logging.ERROR) app = Celery('apps.ingest', result_extended=True) app.config_from_object('django.conf:settings') diff --git a/config/settings/base.py b/config/settings/base.py index bc75125e0..9e934e58c 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -81,7 +81,6 @@ 'allauth.socialaccount.providers.github', 'allauth.socialaccount.providers.google', 'allauth.socialaccount.providers.twitter', - 'background_task', 'corsheaders', 'crispy_forms', 'django_celery_results', diff --git a/fabfile.py b/fabfile.py index 427ba0422..64481a21e 100644 --- a/fabfile.py +++ b/fabfile.py @@ -37,7 +37,7 @@ def deploy(branch='release', path='/readux.io/readux', volume=None): _update_database(options) _update_symlink(options) _restart_webserver() - _restart_background_tasks(options) + _restart_celery(options) _clean_old_builds(options) def _get_latest_source(branch, options): @@ -89,7 +89,7 @@ def _update_symlink(options): def _restart_webserver(): run('sudo /bin/systemctl reload apache2') -def _restart_background_tasks(options): +def _restart_celery(options): run('sudo /bin/systemctl restart celeryd') def _clean_old_builds(options): From d09b389ac64237e22d577e272310f4ee65902dc9 Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Wed, 10 Nov 2021 13:48:22 -0500 Subject: [PATCH 27/43] Remove media symlink from deploy --- fabfile.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/fabfile.py b/fabfile.py index 64481a21e..e3f4a18b1 100644 --- a/fabfile.py +++ b/fabfile.py @@ -71,8 +71,6 @@ def _mount_media(volume): def _create_staticfiles_symlink(options): run('ln -s {rp}/staticfiles staticfiles'.format(rp=options['ROOT_PATH'])) - with cd('apps'): - run('ln -s {rp}/media media'.format(rp=options['ROOT_PATH'])) def _update_static_files(options): run('{v}/bin/python manage.py collectstatic --noinput'.format(v=options['VENV_PATH'])) From 4a3ce1f8d9faa2fba885bc0531c80dd03b20d8e0 Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Thu, 11 Nov 2021 08:48:58 -0500 Subject: [PATCH 28/43] Move celery module --- apps/export/tasks.py | 4 +--- apps/ingest/celery.py | 16 ---------------- apps/ingest/tasks.py | 4 +--- apps/{export => readux}/celery.py | 2 +- config/settings/__init__.py | 2 +- 5 files changed, 4 insertions(+), 24 deletions(-) delete mode 100644 apps/ingest/celery.py rename apps/{export => readux}/celery.py (90%) diff --git a/apps/export/tasks.py b/apps/export/tasks.py index 6966dbc0c..77df29244 100644 --- a/apps/export/tasks.py +++ b/apps/export/tasks.py @@ -9,9 +9,7 @@ LOGGER = logging.getLogger(__name__) -app = Celery('apps.ingest', result_extended=True) -app.config_from_object('django.conf:settings') -app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) +app = Celery('apps.readux', result_extended=True) @app.task(name='github_export', autoretry_for=(Exception,), retry_backoff=True, max_retries=20) def github_export_task( diff --git a/apps/ingest/celery.py b/apps/ingest/celery.py deleted file mode 100644 index 0b506b526..000000000 --- a/apps/ingest/celery.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Celery config for ingest tasks. -""" -import os -from celery import Celery -from django.conf import settings -# import config.settings.local as settings - -# set the default Django settings module for the 'celery' program. -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.local') -app = Celery('apps.ingest', result_extended=True) - -# Using a string here means the worker will not have to -# pickle the object when using Windows. -app.config_from_object('django.conf:settings') -app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) diff --git a/apps/ingest/tasks.py b/apps/ingest/tasks.py index adfb66b88..7bf3e6cc6 100644 --- a/apps/ingest/tasks.py +++ b/apps/ingest/tasks.py @@ -20,9 +20,7 @@ LOGGER = logging.getLogger(__name__) -app = Celery('apps.ingest', result_extended=True) -app.config_from_object('django.conf:settings') -app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) +app = Celery('apps.readux', result_extended=True) @app.task(name='creating_canvases_from_local', autoretry_for=(Exception,), retry_backoff=True, max_retries=20) def create_canvas_form_local_task(ingest_id): diff --git a/apps/export/celery.py b/apps/readux/celery.py similarity index 90% rename from apps/export/celery.py rename to apps/readux/celery.py index 7dfbe4c55..2ddf125b8 100644 --- a/apps/export/celery.py +++ b/apps/readux/celery.py @@ -8,7 +8,7 @@ # set the default Django settings module for the 'celery' program. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.local') -app = Celery('apps.export', result_extended=True) +app = Celery('apps.readux', result_extended=True) # Using a string here means the worker will not have to # pickle the object when using Windows. diff --git a/config/settings/__init__.py b/config/settings/__init__.py index a417f4cc9..e6bd9d2d3 100644 --- a/config/settings/__init__.py +++ b/config/settings/__init__.py @@ -1 +1 @@ -from apps.ingest import celery \ No newline at end of file +from apps.readux import celery \ No newline at end of file From 29f6da0c8cb7577f7919d8074b4e801a3d6d8611 Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Thu, 11 Nov 2021 08:49:26 -0500 Subject: [PATCH 29/43] Fix failing export test --- apps/export/tests/test_export.py | 10 ++++++++++ apps/iiif/canvases/tasks.py | 4 +--- apps/iiif/manifests/views.py | 28 ++++++++++++++++++++-------- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/apps/export/tests/test_export.py b/apps/export/tests/test_export.py index d59aa58bb..f02e5374f 100644 --- a/apps/export/tests/test_export.py +++ b/apps/export/tests/test_export.py @@ -183,10 +183,20 @@ def test_jekyll_export_include_download(self): ) assert isinstance(response.getvalue(), bytes) + @httpretty.httprettified(allow_net_connect=False) def test_jekyll_export_to_github(self): ''' Docstring ''' + httpretty.register_uri( + httpretty.GET, + 'https://api.github.com/users/{u}/repos?per_page=3'.format(u=self.jse.github_username), + body='[{"name":"marx"}]', + content_type="text/json" + ) + httpretty.register_uri( + httpretty.POST, 'https://api.github.com/user/repos' + ) kwargs = {'pid': self.volume.pid, 'version': 'v2'} url = reverse('JekyllExport', kwargs=kwargs) kwargs['deep_zoom'] = 'exclude' diff --git a/apps/iiif/canvases/tasks.py b/apps/iiif/canvases/tasks.py index 778806d5b..dcc8e1fe6 100644 --- a/apps/iiif/canvases/tasks.py +++ b/apps/iiif/canvases/tasks.py @@ -10,9 +10,7 @@ # create a background task have to be serializable, we can't just pass in the model object. # Canvas = apps.get_model('canvases.canvas') -app = Celery('apps.ingest') -app.config_from_object('django.conf:settings') -app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) +app = Celery('apps.readux') @app.task(name='adding_ocr_to_canvas', autoretry_for=(Canvas.DoesNotExist,), retry_backoff=5) def add_ocr_task(canvas_id, *args, **kwargs): diff --git a/apps/iiif/manifests/views.py b/apps/iiif/manifests/views.py index b6a37b7b3..ae6adfada 100644 --- a/apps/iiif/manifests/views.py +++ b/apps/iiif/manifests/views.py @@ -1,6 +1,7 @@ """Django views for manifests""" import json import logging +from os import environ from datetime import datetime from django.contrib import messages from django.http import JsonResponse @@ -164,14 +165,25 @@ def post(self, request, *args, **kwargs): # pylint: disable = unused-argument #github exports context = self.get_context_data() manifest_pid = manifest.pid - github_export_task.delay( - manifest_pid, - kwargs['version'], - github_repo=github_repo, - deep_zoom=False, - owner_ids=owners, - user_id=self.request.user.id - ) + + if environ['DJANGO_ENV'] != 'test': # pragma: no cover + github_export_task.delay( + manifest_pid, + kwargs['version'], + github_repo=github_repo, + deep_zoom=False, + owner_ids=owners, + user_id=self.request.user.id + ) + else: + github_export_task( + manifest_pid, + kwargs['version'], + github_repo=github_repo, + deep_zoom=False, + owner_ids=owners, + user_id=self.request.user.id + ) context['email'] = request.user.email context['mode'] = "github" From 61cf6b39106e9036948d92eb5d10720319ab0c3a Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Thu, 11 Nov 2021 09:22:55 -0500 Subject: [PATCH 30/43] Fix hanging test trying to start celery task --- apps/export/tests/test_export.py | 7 ++----- apps/iiif/manifests/views.py | 17 +++++++++-------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/apps/export/tests/test_export.py b/apps/export/tests/test_export.py index f02e5374f..1dbaf6064 100644 --- a/apps/export/tests/test_export.py +++ b/apps/export/tests/test_export.py @@ -146,8 +146,8 @@ def test_manifest_export(self): response = self.manifest_export_view(request, pid=self.volume.pid, version='v2') assert isinstance(response.getvalue(), bytes) - def test_setting_jekyll_site_dir(self): - self.jse + # def test_setting_jekyll_site_dir(self): + # self.jse # Things I want to test: # * Unzip the IIIF zip file @@ -185,9 +185,6 @@ def test_jekyll_export_include_download(self): @httpretty.httprettified(allow_net_connect=False) def test_jekyll_export_to_github(self): - ''' - Docstring - ''' httpretty.register_uri( httpretty.GET, 'https://api.github.com/users/{u}/repos?per_page=3'.format(u=self.jse.github_username), diff --git a/apps/iiif/manifests/views.py b/apps/iiif/manifests/views.py index ae6adfada..6cb45d94a 100644 --- a/apps/iiif/manifests/views.py +++ b/apps/iiif/manifests/views.py @@ -150,14 +150,15 @@ def post(self, request, *args, **kwargs): # pylint: disable = unused-argument if export_mode == 'download': context = self.get_context_data() manifest_pid = manifest.pid - download_export_task.delay( - manifest_pid, - kwargs['version'], - github_repo=github_repo, - deep_zoom=False, - owner_ids=owners, - user_id=self.request.user.id - ) + if environ['DJANGO_ENV'] != 'test': # pragma: no cover + download_export_task.delay( + manifest_pid, + kwargs['version'], + github_repo=github_repo, + deep_zoom=False, + owner_ids=owners, + user_id=self.request.user.id + ) context['email'] = request.user.email context['mode'] = "download" return render(request, self.template_name, context) From 261a7ac005bb4a1352dcf86a513cfdaddcd24f92 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Nov 2021 14:40:44 +0000 Subject: [PATCH 31/43] build(deps): Bump coverage from 5.2.1 to 6.1.2 Bumps [coverage](https://github.com/nedbat/coveragepy) from 5.2.1 to 6.1.2. - [Release notes](https://github.com/nedbat/coveragepy/releases) - [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst) - [Commits](https://github.com/nedbat/coveragepy/compare/coverage-5.2.1...6.1.2) --- updated-dependencies: - dependency-name: coverage dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements/local.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/local.txt b/requirements/local.txt index 77d2af942..3f26a851d 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -23,7 +23,7 @@ moto==2.2.12 # https://github.com/spulec/moto # Code quality # ------------------------------------------------------------------------------ flake8==4.0.1 # https://github.com/PyCQA/flake8 -coverage==5.2.1 # https://github.com/nedbat/coveragepy +coverage==6.1.2 # https://github.com/nedbat/coveragepy coveralls # Django From 7b349c6273dd8ccbc436abad2631da0e72140917 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Nov 2021 14:40:49 +0000 Subject: [PATCH 32/43] build(deps): Bump celery from 5.1.2 to 5.2.0 Bumps [celery](https://github.com/celery/celery) from 5.1.2 to 5.2.0. - [Release notes](https://github.com/celery/celery/releases) - [Changelog](https://github.com/celery/celery/blob/master/Changelog.rst) - [Commits](https://github.com/celery/celery/compare/v5.1.2...v5.2.0) --- updated-dependencies: - dependency-name: celery dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements/local.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/local.txt b/requirements/local.txt index 77d2af942..2d35229b9 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -41,5 +41,5 @@ pyopenssl # for running dev server under https # TODO: Why are we using this fork? Fabric3==1.14.post1 # https://github.com/mathiasertl/fabric -celery==5.1.2 +celery==5.2.0 django-celery-results==2.2.0 From d535fe85c1b5f5978baf15facff113f7f5e1a892 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Nov 2021 14:49:15 +0000 Subject: [PATCH 33/43] build(deps): Bump moto from 2.2.12 to 2.2.13 Bumps [moto](https://github.com/spulec/moto) from 2.2.12 to 2.2.13. - [Release notes](https://github.com/spulec/moto/releases) - [Changelog](https://github.com/spulec/moto/blob/master/CHANGELOG.md) - [Commits](https://github.com/spulec/moto/compare/2.2.12...2.2.13) --- updated-dependencies: - dependency-name: moto dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements/local.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/local.txt b/requirements/local.txt index 2d35229b9..0425f6778 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -18,7 +18,7 @@ httpretty==1.1.4 # https://pypi.org/project/httpretty/ #mock==4.0.2 cssutils==2.3.0 # https://pypi.org/project/cssutils/ pytest-django==4.4.0 # https://github.com/pytest-dev/pytest-django -moto==2.2.12 # https://github.com/spulec/moto +moto==2.2.13 # https://github.com/spulec/moto # Code quality # ------------------------------------------------------------------------------ From 3005d3a631fb8754eb156a73450b3ad6dbfce611 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Nov 2021 15:00:50 +0000 Subject: [PATCH 34/43] build(deps): Bump boto3 from 1.19.11 to 1.20.3 Bumps [boto3](https://github.com/boto/boto3) from 1.19.11 to 1.20.3. - [Release notes](https://github.com/boto/boto3/releases) - [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst) - [Commits](https://github.com/boto/boto3/compare/1.19.11...1.20.3) --- updated-dependencies: - dependency-name: boto3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index a1f94c126..67a81052b 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -53,7 +53,7 @@ django-import-export==2.6.1 gitpython==3.1.24 # S3 Uploads -boto3==1.19.11 +boto3==1.20.3 django-storages==1.12.3 # https://github.com/jschneier/django-storages # Python function to stream unzip all the files in a ZIP archive, without loading the entire ZIP file into memory or any of its uncompressed files. From d8253ae14b085ffa1e1dad7b8db583ea058d03d2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Nov 2021 15:08:25 +0000 Subject: [PATCH 35/43] build(deps): Bump sphinx from 4.2.0 to 4.3.0 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 4.2.0 to 4.3.0. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/4.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v4.2.0...v4.3.0) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements/local.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/local.txt b/requirements/local.txt index e0f4302bb..fb5cfe7ed 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -2,7 +2,7 @@ Werkzeug==2.0.2 # https://github.com/pallets/werkzeug ipdb==0.13.9 # https://github.com/gotcha/ipdb -Sphinx==4.2.0 # https://github.com/sphinx-doc/sphinx +Sphinx==4.3.0 # https://github.com/sphinx-doc/sphinx # TODO: Upgrade after moving to Django 3 psycopg2-binary==2.8.4 # https://github.com/psycopg/psycopg2 From 7675a35d6e8aae837bb94d6f8a7c45f2b23bed2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Nov 2021 10:21:40 +0000 Subject: [PATCH 36/43] build(deps): Bump django-coverage-plugin from 2.0.1 to 2.0.2 Bumps [django-coverage-plugin](https://github.com/nedbat/django_coverage_plugin) from 2.0.1 to 2.0.2. - [Release notes](https://github.com/nedbat/django_coverage_plugin/releases) - [Commits](https://github.com/nedbat/django_coverage_plugin/compare/v2.0.1...v2.0.2) --- updated-dependencies: - dependency-name: django-coverage-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements/local.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/local.txt b/requirements/local.txt index fb5cfe7ed..9a0efc471 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -33,7 +33,7 @@ faker==9.8.0 django-debug-toolbar==3.2.2 # https://github.com/jazzband/django-debug-toolbar django-extensions==3.1.5 # https://github.com/django-extensions/django-extensions -django-coverage-plugin==2.0.1 # https://github.com/nedbat/django_coverage_plugin +django-coverage-plugin==2.0.2 # https://github.com/nedbat/django_coverage_plugin pyopenssl # for running dev server under https # Deployment From cb1daa0966059de0e02543738d120afd97f0bb67 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Nov 2021 10:21:54 +0000 Subject: [PATCH 37/43] build(deps): Bump moto from 2.2.13 to 2.2.15 Bumps [moto](https://github.com/spulec/moto) from 2.2.13 to 2.2.15. - [Release notes](https://github.com/spulec/moto/releases) - [Changelog](https://github.com/spulec/moto/blob/master/CHANGELOG.md) - [Commits](https://github.com/spulec/moto/compare/2.2.13...2.2.15) --- updated-dependencies: - dependency-name: moto dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements/local.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/local.txt b/requirements/local.txt index fb5cfe7ed..c627b7155 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -18,7 +18,7 @@ httpretty==1.1.4 # https://pypi.org/project/httpretty/ #mock==4.0.2 cssutils==2.3.0 # https://pypi.org/project/cssutils/ pytest-django==4.4.0 # https://github.com/pytest-dev/pytest-django -moto==2.2.13 # https://github.com/spulec/moto +moto==2.2.15 # https://github.com/spulec/moto # Code quality # ------------------------------------------------------------------------------ From f38c70ab9401c236b87cc9181471e5686ff8d29f Mon Sep 17 00:00:00 2001 From: J Date: Mon, 15 Nov 2021 13:54:07 -0500 Subject: [PATCH 38/43] flagging color for collection banner text text color is getting overwritten by a general 'p' tag. --- apps/static/css/project.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/static/css/project.scss b/apps/static/css/project.scss index 074e8becf..15812e468 100644 --- a/apps/static/css/project.scss +++ b/apps/static/css/project.scss @@ -332,7 +332,7 @@ header#page-bg .collection-image-info h3 { } header#page-bg .collection-image-info p { width: 100%; - color: #dedede; + color: #dedede !important; text-align: left; margin-bottom: 0px; margin-top: 0px; From f69d6f52d35a6f76615cfeb7ef59042a86ba4a82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Nov 2021 10:24:59 +0000 Subject: [PATCH 39/43] build(deps): Bump redis from 3.5.3 to 4.0.0 Bumps [redis](https://github.com/redis/redis-py) from 3.5.3 to 4.0.0. - [Release notes](https://github.com/redis/redis-py/releases) - [Changelog](https://github.com/redis/redis-py/blob/master/CHANGES) - [Commits](https://github.com/redis/redis-py/compare/3.5.3...v4.0.0) --- updated-dependencies: - dependency-name: redis dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index 67a81052b..ea05b9c46 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -2,7 +2,7 @@ pytz==2021.3 # https://github.com/stub42/pytz python-slugify==5.0.2 # https://github.com/un33k/python-slugify Pillow argon2-cffi==21.1.0 # https://github.com/hynek/argon2_cffi -redis==3.5.3 # https://github.com/antirez/redis +redis==4.0.0 # https://github.com/antirez/redis bs4 pyyaml progress From 5b22a3f7c4e6d7983d7c206c0b15efcb537bd5f9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Nov 2021 10:25:15 +0000 Subject: [PATCH 40/43] build(deps): Bump faker from 9.8.0 to 9.8.2 Bumps [faker](https://github.com/joke2k/faker) from 9.8.0 to 9.8.2. - [Release notes](https://github.com/joke2k/faker/releases) - [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) - [Commits](https://github.com/joke2k/faker/compare/v9.8.0...v9.8.2) --- updated-dependencies: - dependency-name: faker dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements/local.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/local.txt b/requirements/local.txt index fb5cfe7ed..0f1dfdc5e 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -29,7 +29,7 @@ coveralls # Django # ------------------------------------------------------------------------------ factory-boy==3.2.1 # https://github.com/FactoryBoy/factory_boy -faker==9.8.0 +faker==9.8.2 django-debug-toolbar==3.2.2 # https://github.com/jazzband/django-debug-toolbar django-extensions==3.1.5 # https://github.com/django-extensions/django-extensions From 367e0477c4b0995bbad3e336f93378325596e5c0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Nov 2021 10:22:41 +0000 Subject: [PATCH 41/43] build(deps): Bump celery from 5.2.0 to 5.2.1 Bumps [celery](https://github.com/celery/celery) from 5.2.0 to 5.2.1. - [Release notes](https://github.com/celery/celery/releases) - [Changelog](https://github.com/celery/celery/blob/master/Changelog.rst) - [Commits](https://github.com/celery/celery/compare/v5.2.0...v5.2.1) --- updated-dependencies: - dependency-name: celery dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements/local.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/local.txt b/requirements/local.txt index fb5cfe7ed..15e19fc6c 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -41,5 +41,5 @@ pyopenssl # for running dev server under https # TODO: Why are we using this fork? Fabric3==1.14.post1 # https://github.com/mathiasertl/fabric -celery==5.2.0 +celery==5.2.1 django-celery-results==2.2.0 From 99570745a060c9e74e349f45ec46fb40a8bba24b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Nov 2021 10:22:58 +0000 Subject: [PATCH 42/43] build(deps): Bump boto3 from 1.20.3 to 1.20.7 Bumps [boto3](https://github.com/boto/boto3) from 1.20.3 to 1.20.7. - [Release notes](https://github.com/boto/boto3/releases) - [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst) - [Commits](https://github.com/boto/boto3/compare/1.20.3...1.20.7) --- updated-dependencies: - dependency-name: boto3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index 67a81052b..96576a11f 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -53,7 +53,7 @@ django-import-export==2.6.1 gitpython==3.1.24 # S3 Uploads -boto3==1.20.3 +boto3==1.20.7 django-storages==1.12.3 # https://github.com/jschneier/django-storages # Python function to stream unzip all the files in a ZIP archive, without loading the entire ZIP file into memory or any of its uncompressed files. From 063819fd5797f190a9440252e332a93ad417ab4d Mon Sep 17 00:00:00 2001 From: Jay Varner Date: Wed, 17 Nov 2021 14:23:08 -0500 Subject: [PATCH 43/43] Bump version number and update changelog --- CHANGELOG.rst | 4 ++++ apps/__init__.py | 2 +- apps/readux/__init__.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1a6cef5ae..f4d606484 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,10 @@ CHANGELOG ========= +Release 2.2.1 +--------------------- +* Improvements to bulk ingest +* Export tasks converted to Celery. Release 2.2.0 --------------------- diff --git a/apps/__init__.py b/apps/__init__.py index c88b4e185..b73cbe3cc 100644 --- a/apps/__init__.py +++ b/apps/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2.2.0" +__version__ = "2.2.1" __version_info__ = tuple( [ int(num) if num.isdigit() else num diff --git a/apps/readux/__init__.py b/apps/readux/__init__.py index 04188a16d..36a511eca 100644 --- a/apps/readux/__init__.py +++ b/apps/readux/__init__.py @@ -1 +1 @@ -__version__ = '2.2.0' +__version__ = '2.2.1'