From a43bc0c60878fe90da75f9eb35bbd268a5c73006 Mon Sep 17 00:00:00 2001 From: Oskar Persson Date: Wed, 1 Feb 2017 15:02:02 +0100 Subject: [PATCH] Add files accidentally deleted by merge --- ESSArch_TP/config/__init__.py | 29 + ESSArch_TP/config/settings.py | 234 ++++ ESSArch_TP/config/urls.py | 113 ++ ESSArch_TP/config/wsgi.py | 40 + ESSArch_TP/configuration/__init__.py | 24 + ESSArch_TP/install/__init__.py | 24 + .../install/install_default_config_etp.py | 132 ++ ESSArch_TP/ip/__init__.py | 24 + ESSArch_TP/ip/views.py | 1063 +++++++++++++++++ ESSArch_TP/profiles/__init__.py | 24 + ESSArch_TP/profiles/views.py | 323 +++++ 11 files changed, 2030 insertions(+) create mode 100644 ESSArch_TP/config/__init__.py create mode 100644 ESSArch_TP/config/settings.py create mode 100644 ESSArch_TP/config/urls.py create mode 100644 ESSArch_TP/config/wsgi.py create mode 100644 ESSArch_TP/configuration/__init__.py create mode 100644 ESSArch_TP/install/__init__.py create mode 100644 ESSArch_TP/install/install_default_config_etp.py create mode 100644 ESSArch_TP/ip/__init__.py create mode 100644 ESSArch_TP/ip/views.py create mode 100644 ESSArch_TP/profiles/__init__.py create mode 100644 ESSArch_TP/profiles/views.py diff --git a/ESSArch_TP/config/__init__.py b/ESSArch_TP/config/__init__.py new file mode 100644 index 00000000..a25606eb --- /dev/null +++ b/ESSArch_TP/config/__init__.py @@ -0,0 +1,29 @@ +""" + ESSArch is an open source archiving and digital preservation system + + ESSArch Tools for Producer (ETP) + Copyright (C) 2005-2017 ES Solutions AB + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + Contact information: + Web - http://www.essolutions.se + Email - essarch@essolutions.se +""" + +from __future__ import absolute_import + +# This will make sure the app is always imported when +# Django starts so that shared_task will use this app. +from .celery import app as celery_app # noqa diff --git a/ESSArch_TP/config/settings.py b/ESSArch_TP/config/settings.py new file mode 100644 index 00000000..4ba21bb2 --- /dev/null +++ b/ESSArch_TP/config/settings.py @@ -0,0 +1,234 @@ +""" + ESSArch is an open source archiving and digital preservation system + + ESSArch Tools for Producer (ETP) + Copyright (C) 2005-2017 ES Solutions AB + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + Contact information: + Web - http://www.essolutions.se + Email - essarch@essolutions.se +""" + +""" +Django settings for ETP project. + +Generated by 'django-admin startproject' using Django 1.9.7. + +For more information on this file, see +https://docs.djangoproject.com/en/1.9/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.9/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'ze7xnd#&9_m)05j&j8wpu!=dp+jlj3olk&@k7amq9-s2x+b=$%' +SESSION_COOKIE_NAME = 'etp' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + +REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'preingest.pagination.LinkHeaderPagination', + 'PAGE_SIZE': 10 +} + + +# Application definition + +INSTALLED_APPS = [ + 'allauth', + 'allauth.account', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.admin', + 'django.contrib.sites', + 'rest_auth', + 'rest_auth.registration', + 'rest_framework', + 'rest_framework.authtoken', + 'corsheaders', + 'frontend', + 'ESSArch_Core.config', + 'ESSArch_Core.configuration', + 'ESSArch_Core.ip', + 'ESSArch_Core.profiles', + 'ESSArch_Core.essxml.Generator', + 'ESSArch_Core.essxml.ProfileMaker', + 'ESSArch_Core.WorkflowEngine', +] + +SITE_ID = 1 + +MIDDLEWARE_CLASSES = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +CORS_ORIGIN_ALLOW_ALL = True +ROOT_URLCONF = 'config.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'templates')], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'config.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.9/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. + #'STORAGE_ENGINE': 'MyISAM', # STORAGE_ENGINE for MySQL database tables, 'MyISAM' or 'INNODB' + 'NAME': 'etp', # Or path to database file if using sqlite3. + 'USER': 'arkiv', # Not used with sqlite3. + 'PASSWORD': 'password', # Not used with sqlite3. + 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '', # Set to empty string for default. Not used with sqlite3. + # This options for storage_engine have to be set for "south migrate" to work. + 'OPTIONS': { + #"init_command": "SET storage_engine=MyISAM", # MySQL (<= 5.5.2) + "init_command": "SET default_storage_engine=MyISAM", # MySQL (>= 5.5.3) + } + } +} + +# Password validation +# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +# Django Rest Auth serializers +# http://django-rest-auth.readthedocs.io/en/latest/configuration.html + +REST_AUTH_SERIALIZERS = { + 'USER_DETAILS_SERIALIZER': 'preingest.serializers.UserSerializer' +} + + +# File elements in different metadata standards + +FILE_ELEMENTS = { + "file": { + "path": "FLocat@href", + "pathprefix": "file:///", + "checksum": "@CHECKSUM", + "checksumtype": "@CHECKSUMTYPE", + }, + "mdRef": { + "path": "@href", + "pathprefix": "file:///", + "checksum": "@CHECKSUM", + "checksumtype": "@CHECKSUMTYPE", + }, + "object": { + "path": "storage/contentLocation/contentLocationValue", + "pathprefix": "file:///", + "checksum": "objectCharacteristics/fixity/messageDigest", + "checksumtype": "objectCharacteristics/fixity/messageDigestAlgorithm", + "format": "objectCharacteristics/format/formatDesignation/formatName", + }, +} + +# Internationalization +# https://docs.djangoproject.com/en/1.9/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'Europe/Stockholm' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.9/howto/static-files/ + +STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'static_root') +STATICFILES_DIRS = ( + os.path.join(BASE_DIR, 'static'), +) + +# Add etp vhost to rabbitmq: +# rabbitmqctl add_user guest guest +# rabbitmqctl add_vhost etp +# rabbitmqctl set_permissions -p etp guest ".*" ".*" ".*" + +# Celery settings +BROKER_URL = 'amqp://guest:guest@localhost:5672/etp' +CELERY_IMPORTS = ("preingest.tasks", "ESSArch_Core.WorkflowEngine.tests.tasks") +CELERY_RESULT_BACKEND = 'amqp://' + +# Rest auth settings +OLD_PASSWORD_FIELD_ENABLED = True + +try: + from local_etp_settings import * +except ImportError, exp: + pass diff --git a/ESSArch_TP/config/urls.py b/ESSArch_TP/config/urls.py new file mode 100644 index 00000000..1d87277c --- /dev/null +++ b/ESSArch_TP/config/urls.py @@ -0,0 +1,113 @@ +""" + ESSArch is an open source archiving and digital preservation system + + ESSArch Tools for Producer (ETP) + Copyright (C) 2005-2017 ES Solutions AB + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + Contact information: + Web - http://www.essolutions.se + Email - essarch@essolutions.se +""" + +"""etp URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.9/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf.urls import include, url +from django.contrib import admin +from django.contrib.auth import views as auth_views + +from rest_framework import routers + +from ESSArch_Core.configuration.views import ( + AgentViewSet, + EventTypeViewSet, + ParameterViewSet, + PathViewSet, + SysInfoView, +) + +from ip.views import ( + ArchivalInstitutionViewSet, + ArchivistOrganizationViewSet, + ArchivalTypeViewSet, + ArchivalLocationViewSet, + EventIPViewSet, + InformationPackageViewSet, +) + +from preingest.views import ( + GroupViewSet, + PermissionViewSet, + ProcessStepViewSet, + ProcessTaskViewSet, + UserViewSet, +) + +from profiles.views import ( + ProfileViewSet, + ProfileSAViewSet, + ProfileIPViewSet, + SubmissionAgreementViewSet, +) + +admin.site.site_header = 'ESSArch Tools Producer Administration' +admin.site.site_title = 'ESSArch Tools Producer Administration' + +router = routers.DefaultRouter() +router.register(r'users', UserViewSet) +router.register(r'groups', GroupViewSet) +router.register(r'permissions', PermissionViewSet) +router.register(r'archival-institutions', ArchivalInstitutionViewSet) +router.register(r'archivist-organizations', ArchivistOrganizationViewSet) +router.register(r'archival-types', ArchivalTypeViewSet) +router.register(r'archival-locations', ArchivalLocationViewSet) +router.register(r'information-packages', InformationPackageViewSet) +router.register(r'steps', ProcessStepViewSet) +router.register(r'tasks', ProcessTaskViewSet) +router.register(r'events', EventIPViewSet) +router.register(r'event-types', EventTypeViewSet) +router.register(r'submission-agreements', SubmissionAgreementViewSet) +router.register(r'profiles', ProfileViewSet) +router.register(r'profile-sa', ProfileSAViewSet) +router.register(r'profile-ip', ProfileIPViewSet) +router.register(r'agents', AgentViewSet) +router.register(r'parameters', ParameterViewSet) +router.register(r'paths', PathViewSet) + +urlpatterns = [ + url(r'^', include('frontend.urls'), name='home'), + url(r'^admin/', admin.site.urls), + url(r'^api/sysinfo/', SysInfoView.as_view()), + url(r'^api/', include(router.urls)), + url(r'^accounts/changepassword', auth_views.password_change, {'post_change_redirect': '/'} ), + url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), + url(r'^template/', include('ESSArch_Core.essxml.ProfileMaker.urls')), + url(r'^accounts/login/$', auth_views.login), + url(r'^rest-auth/', include('rest_auth.urls')), + url(r'^rest-auth/registration/', include('rest_auth.registration.urls')), +] diff --git a/ESSArch_TP/config/wsgi.py b/ESSArch_TP/config/wsgi.py new file mode 100644 index 00000000..77940447 --- /dev/null +++ b/ESSArch_TP/config/wsgi.py @@ -0,0 +1,40 @@ +""" + ESSArch is an open source archiving and digital preservation system + + ESSArch Tools for Producer (ETP) + Copyright (C) 2005-2017 ES Solutions AB + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + Contact information: + Web - http://www.essolutions.se + Email - essarch@essolutions.se +""" + +""" +WSGI config for etp project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + +application = get_wsgi_application() diff --git a/ESSArch_TP/configuration/__init__.py b/ESSArch_TP/configuration/__init__.py new file mode 100644 index 00000000..dcf2731c --- /dev/null +++ b/ESSArch_TP/configuration/__init__.py @@ -0,0 +1,24 @@ +""" + ESSArch is an open source archiving and digital preservation system + + ESSArch Tools for Producer (ETP) + Copyright (C) 2005-2017 ES Solutions AB + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + Contact information: + Web - http://www.essolutions.se + Email - essarch@essolutions.se +""" + diff --git a/ESSArch_TP/install/__init__.py b/ESSArch_TP/install/__init__.py new file mode 100644 index 00000000..dcf2731c --- /dev/null +++ b/ESSArch_TP/install/__init__.py @@ -0,0 +1,24 @@ +""" + ESSArch is an open source archiving and digital preservation system + + ESSArch Tools for Producer (ETP) + Copyright (C) 2005-2017 ES Solutions AB + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + Contact information: + Web - http://www.essolutions.se + Email - essarch@essolutions.se +""" + diff --git a/ESSArch_TP/install/install_default_config_etp.py b/ESSArch_TP/install/install_default_config_etp.py new file mode 100644 index 00000000..f138f41e --- /dev/null +++ b/ESSArch_TP/install/install_default_config_etp.py @@ -0,0 +1,132 @@ +# -*- coding: UTF-8 -*- + +""" + ESSArch is an open source archiving and digital preservation system + + ESSArch Tools for Producer (ETP) + Copyright (C) 2005-2017 ES Solutions AB + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + Contact information: + Web - http://www.essolutions.se + Email - essarch@essolutions.se +""" + +import django +django.setup() + +from django.contrib.auth.models import User, Group, Permission +from ESSArch_Core.configuration.models import EventType, Path + + +def installDefaultConfiguration(): + print "Installing users, groups and permissions..." + installDefaultUsers() + print "\nInstalling paths..." + installDefaultPaths() + print "\nInstalling event types..." + installDefaultEventTypes() + + return 0 + + +def installDefaultUsers(): + user_user, _ = User.objects.get_or_create( + username='user', email='usr1@essolutions.se' + ) + user_user.set_password('user') + user_user.save() + + user_admin, _ = User.objects.get_or_create( + username='admin', email='admin@essolutions.se', + is_staff=True + ) + user_admin.set_password('admin') + user_admin.save() + + user_sysadmin, _ = User.objects.get_or_create( + username='sysadmin', email='sysadmin@essolutions.se', + is_staff=True, is_superuser=True + ) + user_sysadmin.set_password('sysadmin') + user_sysadmin.save() + + group_user, _ = Group.objects.get_or_create(name='user') + group_admin, _ = Group.objects.get_or_create(name='admin') + group_sysadmin, _ = Group.objects.get_or_create(name='sysadmin') + + can_add_ip_event = Permission.objects.get(codename='add_eventip') + can_change_ip_event = Permission.objects.get(codename='change_eventip') + can_delete_ip_event = Permission.objects.get(codename='delete_eventip') + + group_user.permissions.add(can_add_ip_event, can_change_ip_event, can_delete_ip_event) + + group_user.user_set.add(user_user) + group_admin.user_set.add(user_admin) + group_sysadmin.user_set.add(user_sysadmin) + + return 0 + + +def installDefaultPaths(): + dct = { + 'path_mimetypes_definitionfile': '/ESSArch/config/mime.types', + 'path_definitions': '/ESSArch/etp/env', + 'path_preingest_prepare': '/ESSArch/data/etp/prepare', + 'path_preingest_reception': '/ESSArch/data/etp/reception', + 'path_ingest_reception': '/ESSArch/data/eta/reception/eft', + } + + for key in dct: + print '-> %s: %s' % (key, dct[key]) + Path.objects.get_or_create(entity=key, value=dct[key]) + + return 0 + + +def installDefaultEventTypes(): + dct = { + 'Other': '10000', + 'Prepare IP': '10100', + 'Create IP root directory': '10110', + 'Create physical model': '10115', + 'Upload file': '10120', + 'Create SIP': '10200', + 'Calculate checksum ': '10210', + 'Identify format': '10220', + 'Generate XML files': '10230', + 'Append events': '10240', + 'Copy schemas': '10250', + 'Validate file format': '10260', + 'Validate XML file': '10261', + 'Validate logical representation against physical representation': '10262', + 'Validate checksum': '10263', + 'Create TAR': '10270', + 'Create ZIP': '10271', + 'Delete files': '10275', + 'Update IP status': '10280', + 'Update IP path': '10285', + 'Submit SIP': '10300', + } + + for key in dct: + print '-> %s: %s' % (key, dct[key]) + EventType.objects.get_or_create(eventType=dct[key], eventDetail=key) + + return 0 + + +if __name__ == '__main__': + installDefaultConfiguration() diff --git a/ESSArch_TP/ip/__init__.py b/ESSArch_TP/ip/__init__.py new file mode 100644 index 00000000..dcf2731c --- /dev/null +++ b/ESSArch_TP/ip/__init__.py @@ -0,0 +1,24 @@ +""" + ESSArch is an open source archiving and digital preservation system + + ESSArch Tools for Producer (ETP) + Copyright (C) 2005-2017 ES Solutions AB + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + Contact information: + Web - http://www.essolutions.se + Email - essarch@essolutions.se +""" + diff --git a/ESSArch_TP/ip/views.py b/ESSArch_TP/ip/views.py new file mode 100644 index 00000000..21cbf14c --- /dev/null +++ b/ESSArch_TP/ip/views.py @@ -0,0 +1,1063 @@ +""" + ESSArch is an open source archiving and digital preservation system + + ESSArch Tools for Producer (ETP) + Copyright (C) 2005-2017 ES Solutions AB + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + Contact information: + Web - http://www.essolutions.se + Email - essarch@essolutions.se +""" + +from _version import get_versions + +from collections import OrderedDict +from operator import itemgetter + +import errno +import glob +import os +import shutil + +from django.db.models import Prefetch +from django_filters.rest_framework import DjangoFilterBackend + +from django.http import HttpResponse +from rest_framework import filters +from rest_framework.decorators import detail_route +from rest_framework.response import Response + +from ESSArch_Core.configuration.models import ( + EventType, + Path, +) + +from ESSArch_Core.essxml.Generator.xmlGenerator import ( + find_destination +) + +from ESSArch_Core.ip.models import ( + ArchivalInstitution, + ArchivistOrganization, + ArchivalType, + ArchivalLocation, + InformationPackage, + EventIP +) + +from ESSArch_Core.ip.permissions import ( + CanChangeSA, + CanCreateSIP, + CanDeleteIP, + CanSetUploaded, + CanSubmitSIP, + CanUnlockProfile, +) + +from ESSArch_Core.profiles.models import ( + Profile, + ProfileIP, +) + +from ESSArch_Core.util import ( + create_event, + creation_date, + get_event_spec, + get_files_and_dirs, + mkdir_p, + timestamp_to_datetime, +) + +from ESSArch_Core.WorkflowEngine.models import ( + ProcessStep, ProcessTask, +) + +from ip.filters import ( + ArchivalInstitutionFilter, + ArchivistOrganizationFilter, + ArchivalTypeFilter, + ArchivalLocationFilter, + InformationPackageFilter, +) + +from ip.serializers import ( + ArchivalInstitutionSerializer, + ArchivistOrganizationSerializer, + ArchivalTypeSerializer, + ArchivalLocationSerializer, + InformationPackageSerializer, + InformationPackageDetailSerializer, + EventIPSerializer, +) + +from preingest.serializers import ( + ProcessStepSerializer, +) + +from ip.steps import ( + prepare_ip, +) + +from rest_framework import viewsets + + +class ArchivalInstitutionViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows archival institutions to be viewed or edited. + """ + queryset = ArchivalInstitution.objects.all() + serializer_class = ArchivalInstitutionSerializer + + filter_backends = (DjangoFilterBackend,) + filter_class = ArchivalInstitutionFilter + + +class ArchivistOrganizationViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows archivist organizations to be viewed or edited. + """ + queryset = ArchivistOrganization.objects.all() + serializer_class = ArchivistOrganizationSerializer + + filter_backends = (DjangoFilterBackend,) + filter_class = ArchivistOrganizationFilter + + +class ArchivalTypeViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows archival types to be viewed or edited. + """ + queryset = ArchivalType.objects.all() + serializer_class = ArchivalTypeSerializer + + filter_backends = (DjangoFilterBackend,) + filter_class = ArchivalTypeFilter + + +class ArchivalLocationViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows archival locations to be viewed or edited. + """ + queryset = ArchivalLocation.objects.all() + serializer_class = ArchivalLocationSerializer + + filter_backends = (DjangoFilterBackend,) + filter_class = ArchivalLocationFilter + + +class InformationPackageViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows information packages to be viewed or edited. + """ + queryset = InformationPackage.objects.all().prefetch_related( + Prefetch('profileip_set', to_attr='profiles'), 'profiles__profile', + 'ArchivalInstitution', 'ArchivistOrganization', 'ArchivalType', 'ArchivalLocation', + 'Responsible__user_permissions', 'Responsible__groups__permissions', 'steps', + ).select_related('SubmissionAgreement') + serializer_class = InformationPackageSerializer + filter_backends = ( + filters.OrderingFilter, DjangoFilterBackend, filters.SearchFilter, + ) + ordering_fields = ( + 'Label', 'Responsible', 'CreateDate', 'State', 'eventDateTime', + 'eventType', 'eventOutcomeDetailNote', 'eventOutcome', + 'linkingAgentIdentifierValue', 'id' + ) + search_fields = ('Label', 'Responsible', 'State', 'SubmissionAgreement__sa_name') + filter_class = InformationPackageFilter + + def get_serializer_class(self): + if self.action == 'list': + return InformationPackageSerializer + + return InformationPackageDetailSerializer + + def get_permissions(self): + if self.action == 'partial_update': + if self.request.data.get('SubmissionAgreement'): + self.permission_classes = [CanChangeSA] + if self.action == 'destroy': + self.permission_classes = [CanDeleteIP] + + return super(InformationPackageViewSet, self).get_permissions() + + def get_queryset(self): + queryset = self.queryset + + other = self.request.query_params.get('other') + + if other is not None: + queryset = queryset.filter( + ArchivalInstitution=None, + ArchivistOrganization=None, + ArchivalType=None, + ArchivalLocation=None + ) + + return queryset + + def create(self, request): + """ + Prepares a new information package (IP) using the following tasks: + + 1. Creates a new IP in the database. + + 2. Creates a directory in the prepare directory with the name set to + the id of the new IP. + + 3. Creates an event in the database connected to the IP and with the + detail "Prepare IP". + + Args: + + Returns: + None + """ + + self.check_permissions(request) + + label = request.data.get('label', None) + responsible = self.request.user + + prepare_ip(label, responsible).run() + return Response({"status": "Prepared IP"}) + + def destroy(self, request, pk=None): + ip = self.get_object() + self.check_object_permissions(request, ip) + path = ip.ObjectPath + + try: + shutil.rmtree(path) + except OSError as e: + if e.errno == errno.ENOTDIR: + no_ext = os.path.splitext(path)[0] + + for fl in glob.glob(no_ext + "*"): + try: + os.remove(fl) + except: + raise + + try: + shutil.rmtree(ip.ObjectPath) + except: + pass + + try: + os.remove(ip.ObjectPath + ".tar") + except: + pass + + try: + os.remove(ip.ObjectPath + ".zip") + except: + pass + + return super(InformationPackageViewSet, self).destroy(request, pk=pk) + + @detail_route() + def events(self, request, pk=None): + ip = self.get_object() + events = filters.OrderingFilter().filter_queryset(request, ip.events.all(), self) + page = self.paginate_queryset(events) + if page is not None: + serializers = EventIPSerializer(page, many=True, context={'request': request}) + return self.get_paginated_response(serializers.data) + serializers = EventIPSerializer(events, many=True, context={'request': request}) + return Response(serializers.data) + + @detail_route() + def steps(self, request, pk=None): + ip = self.get_object() + steps = ip.steps.all() + serializer = ProcessStepSerializer( + data=steps, many=True, context={'request': request} + ) + serializer.is_valid() + return Response(serializer.data) + + @detail_route() + def files(self, request, pk=None): + ip = self.get_object() + entries = [] + path = os.path.join(ip.ObjectPath, request.query_params.get('path', '')) + + for entry in get_files_and_dirs(path): + entry_type = "dir" if entry.is_dir() else "file" + entries.append( + { + "name": os.path.basename(entry.path), + "type": entry_type + } + ) + + sorted_entries = sorted(entries, key=itemgetter('name')) + return Response(sorted_entries) + + @detail_route(methods=['post'], url_path='create', permission_classes=[CanCreateSIP]) + def create_ip(self, request, pk=None): + """ + Creates the specified information package + + Args: + pk: The primary key (id) of the information package to create + + Returns: + None + """ + + ip = self.get_object() + sa = ip.SubmissionAgreement + agent = request.user + + if ip.State != "Uploaded": + raise ValueError( + "The IP (%s) is in the state '%s' but should be 'Uploaded'" % (pk, ip.State) + ) + + validators = request.data.get('validators', {}) + + validate_xml_file = validators.get('validate_xml_file', False) + validate_file_format = validators.get('validate_file_format', False) + validate_integrity = validators.get('validate_integrity', False) + validate_logical_physical_representation = validators.get('validate_logical_physical_representation', False) + + container_format = ip.get_container_format() + + main_step = ProcessStep.objects.create( + name="Create SIP", + ) + + t0 = ProcessTask.objects.create( + name="preingest.tasks.UpdateIPStatus", + params={ + "ip": ip, + "status": "Creating", + }, + processstep_pos=0, + log=EventIP, + information_package=ip, + responsible=self.request.user, + ) + start_create_sip_step = ProcessStep.objects.create( + name="Update IP Status", + parent_step_pos=0 + ) + + start_create_sip_step.tasks.add(t0) + + event_type = EventType.objects.get(eventType=10200) + + create_event(event_type, 0, "Created SIP", get_versions()['version'], agent, ip=ip) + + prepare_path = Path.objects.get( + entity="path_preingest_prepare" + ).value + + reception_path = Path.objects.get( + entity="path_preingest_reception" + ).value + + ip_prepare_path = os.path.join(prepare_path, str(ip.pk)) + ip_reception_path = os.path.join(reception_path, str(ip.pk)) + events_path = os.path.join(ip_prepare_path, "ipevents.xml") + + structure = ip.get_profile('sip').structure + + info = ip.get_profile('sip').fill_specification_data(sa, ip) + + # ensure premis is created before mets + filesToCreate = OrderedDict() + + if ip.profile_locked('preservation_metadata'): + premis_profile = ip.get_profile('preservation_metadata') + premis_dir, premis_name = find_destination("preservation_description_file", structure) + premis_path = os.path.join(ip.ObjectPath, premis_dir, premis_name) + filesToCreate[premis_path] = premis_profile.specification + + mets_dir, mets_name = find_destination("mets_file", structure) + mets_path = os.path.join(ip.ObjectPath, mets_dir, mets_name) + filesToCreate[mets_path] = ip.get_profile('sip').specification + + generate_xml_step = ProcessStep.objects.create( + name="Generate XML", + parent_step_pos=1 + ) + + for fname, template in filesToCreate.iteritems(): + dirname = os.path.dirname(fname) + t = ProcessTask.objects.create( + name="ESSArch_Core.tasks.DownloadSchemas", + params={ + "template": template, + "dirname": dirname, + "structure": structure, + "root": ip.ObjectPath, + }, + processstep_pos=1, + log=EventIP, + information_package=ip, + responsible=self.request.user, + ) + + generate_xml_step.tasks.add(t) + + t = ProcessTask.objects.create( + name="preingest.tasks.GenerateXML", + params={ + "info": info, + "filesToCreate": filesToCreate, + "folderToParse": ip_prepare_path, + "algorithm": ip.get_checksum_algorithm(), + }, + processstep_pos=3, + log=EventIP, + information_package=ip, + responsible=self.request.user, + ) + + generate_xml_step.tasks.add(t) + + if any(validators.itervalues()): + validate_step = ProcessStep.objects.create( + name="Validation", parent_step=main_step, + parent_step_pos=2, + ) + + if validate_xml_file: + validate_step.tasks.add( + ProcessTask.objects.create( + name="preingest.tasks.ValidateXMLFile", + params={ + "xml_filename": mets_path, + }, + processstep_pos=1, + log=EventIP, + information_package=ip, + responsible=self.request.user, + ) + ) + + if ip.profile_locked("preservation_metadata"): + validate_step.tasks.add( + ProcessTask.objects.create( + name="preingest.tasks.ValidateXMLFile", + params={ + "xml_filename": premis_path, + }, + processstep_pos=2, + log=EventIP, + information_package=ip, + responsible=self.request.user, + ) + ) + + if validate_logical_physical_representation: + validate_step.tasks.add( + ProcessTask.objects.create( + name="preingest.tasks.ValidateLogicalPhysicalRepresentation", + params={ + "dirname": ip.ObjectPath, + "xmlfile": mets_path, + }, + processstep_pos=3, + log=EventIP, + information_package=ip, + responsible=self.request.user, + ) + ) + + validate_step.tasks.add( + ProcessTask.objects.create( + name="preingest.tasks.ValidateFiles", + params={ + "ip": ip, + "xmlfile": mets_path, + "validate_fileformat": validate_file_format, + "validate_integrity": validate_integrity, + }, + processstep_pos=4, + log=EventIP, + information_package=ip, + responsible=self.request.user, + ) + ) + + validate_step.save() + + info = { + "_OBJID": str(ip.pk), + "_OBJLABEL": ip.Label + } + + filesToCreate = OrderedDict() + filesToCreate[events_path] = get_event_spec() + + create_sip_step = ProcessStep.objects.create( + name="Create SIP", + parent_step_pos=3 + ) + + for fname, template in filesToCreate.iteritems(): + dirname = os.path.dirname(fname) + create_sip_step.tasks.add(ProcessTask.objects.create( + name="ESSArch_Core.tasks.DownloadSchemas", + params={ + "template": template, + "dirname": dirname, + "structure": structure, + "root": ip.ObjectPath, + }, + processstep_pos=-1, + log=EventIP, + information_package=ip, + responsible=self.request.user, + )) + + create_sip_step.tasks.add(ProcessTask.objects.create( + name="preingest.tasks.GenerateXML", + params={ + "info": info, + "filesToCreate": filesToCreate, + "algorithm": ip.get_checksum_algorithm(), + }, + processstep_pos=0, + log=EventIP, + information_package=ip, + responsible=self.request.user, + )) + + create_sip_step.tasks.add(ProcessTask.objects.create( + name="preingest.tasks.AppendEvents", + params={ + "filename": events_path, + }, + processstep_pos=1, + log=EventIP, + information_package=ip, + responsible=self.request.user, + )) + + spec = { + "-name": "object", + "-namespace": "premis", + "-children": [ + { + "-name": "objectIdentifier", + "-namespace": "premis", + "-children": [ + { + "-name": "objectIdentifierType", + "-namespace": "premis", + "#content": [{"var": "FIDType"}], + "-children": [] + }, + { + "-name": "objectIdentifierValue", + "-namespace": "premis", + "#content": [{"var": "FID"}], + "-children": [] + } + ] + }, + { + "-name": "objectCharacteristics", + "-namespace": "premis", + "-children": [ + { + "-name": "format", + "-namespace": "premis", + "-children": [ + { + "-name": "formatDesignation", + "-namespace": "premis", + "-children": [ + { + "-name": "formatName", + "-namespace": "premis", + "#content": [{"var": "FFormatName"}], + "-children": [] + } + ] + } + ] + } + ] + }, + { + "-name": "storage", + "-namespace": "premis", + "-children": [ + { + "-name": "contentLocation", + "-namespace": "premis", + "-children": [ + { + "-name": "contentLocationType", + "-namespace": "premis", + "#content": [{"var": "FLocationType"}], + "-children": [] + }, + { + "-name": "contentLocationValue", + "-namespace": "premis", + "#content": [{"text": "file:///%s.%s" % (ip.pk, container_format.lower())}], + "-children": [] + } + ] + } + ] + } + ], + "-attr": [ + { + "-name": "type", + '-namespace': 'xsi', + "-req": "1", + "#content": [{"text": "premis:file"}] + } + ], + } + + info = { + 'FIDType': "UUID", + 'FID': ip.ObjectIdentifierValue, + 'FFormatName': container_format.upper(), + 'FLocationType': 'URI', + 'FName': ip.ObjectPath, + } + + create_sip_step.tasks.add(ProcessTask.objects.create( + name="ESSArch_Core.tasks.InsertXML", + params={ + "filename": events_path, + "elementToAppendTo": "premis", + "spec": spec, + "info": info, + "index": 0 + }, + processstep_pos=2, + information_package=ip, + responsible=self.request.user, + )) + + if validate_xml_file: + create_sip_step.tasks.add( + ProcessTask.objects.create( + name="preingest.tasks.ValidateXMLFile", + params={ + "xml_filename": events_path, + }, + processstep_pos=3, + log=EventIP, + information_package=ip, + responsible=self.request.user, + ) + ) + + if container_format.lower() == 'zip': + zipname = os.path.join(ip_reception_path) + '.zip' + container_task = ProcessTask.objects.create( + name="preingest.tasks.CreateZIP", + params={ + "dirname": ip_prepare_path, + "zipname": zipname, + }, + processstep_pos=4, + log=EventIP, + information_package=ip, + responsible=self.request.user, + ) + + else: + tarname = os.path.join(ip_reception_path) + '.tar' + container_task = ProcessTask.objects.create( + name="preingest.tasks.CreateTAR", + params={ + "dirname": ip_prepare_path, + "tarname": tarname, + }, + processstep_pos=4, + log=EventIP, + information_package=ip, + responsible=self.request.user, + ) + + create_sip_step.tasks.add(container_task) + + create_sip_step.tasks.add( + ProcessTask.objects.create( + name="preingest.tasks.DeleteFiles", + params={ + "path": ip.ObjectPath + }, + processstep_pos=45, + log=EventIP, + information_package=ip, + responsible=self.request.user, + ) + ) + + create_sip_step.tasks.add( + ProcessTask.objects.create( + name="preingest.tasks.UpdateIPPath", + params={ + "ip": ip, + }, + result_params={ + "path": container_task.pk + }, + processstep_pos=50, + log=EventIP, + information_package=ip, + responsible=self.request.user, + ) + ) + + create_sip_step.tasks.add( + ProcessTask.objects.create( + name="preingest.tasks.UpdateIPStatus", + params={ + "ip": ip, + "status": "Created", + }, + processstep_pos=60, + log=EventIP, + information_package=ip, + responsible=self.request.user, + ) + ) + + create_sip_step.save() + + main_step.child_steps.add( + start_create_sip_step, generate_xml_step, create_sip_step + ) + main_step.information_package = ip + main_step.save() + main_step.run() + + return Response({'status': 'creating ip'}) + + @detail_route(methods=['post'], url_path='submit', permission_classes=[CanSubmitSIP]) + def submit(self, request, pk=None): + """ + Submits the specified information package + + Args: + pk: The primary key (id) of the information package to submit + + Returns: + None + """ + + ip = self.get_object() + + if ip.State != "Created": + raise ValueError( + "The IP (%s) is in the state '%s' but should be 'Created'" % (pk, ip.State) + ) + + validators = request.data.get('validators', {}) + + validate_xml_file = validators.get('validate_xml_file', False) + validate_file_format = validators.get('validate_file_format', False) + validate_integrity = validators.get('validate_integrity', False) + validate_logical_physical_representation = validators.get('validate_logical_physical_representation', False) + + step = ProcessStep.objects.create( + name="Submit SIP", + information_package=ip + ) + + step.tasks.add(ProcessTask.objects.create( + name="preingest.tasks.UpdateIPStatus", + params={ + "ip": ip, + "status": "Submitting", + }, + processstep_pos=0, + log=EventIP, + information_package=ip, + responsible=self.request.user, + )) + + reception = Path.objects.get(entity="path_preingest_reception").value + + sd_profile = ip.get_profile('submit_description') + + container_format = ip.get_container_format() + container_file = os.path.join(reception, str(ip.pk) + ".%s" % container_format.lower()) + + sa = ip.SubmissionAgreement + + info = sd_profile.fill_specification_data(sa, ip) + info["_IP_CREATEDATE"] = timestamp_to_datetime(creation_date(container_file)).isoformat() + + infoxml = os.path.join(reception, str(ip.pk) + ".xml") + + filesToCreate = { + infoxml: sd_profile.specification + } + + step.tasks.add(ProcessTask.objects.create( + name="preingest.tasks.GenerateXML", + params={ + "info": info, + "filesToCreate": filesToCreate, + "folderToParse": container_file, + "algorithm": ip.get_checksum_algorithm(), + }, + processstep_pos=10, + log=EventIP, + information_package=ip, + responsible=self.request.user, + )) + + if validate_xml_file: + step.tasks.add( + ProcessTask.objects.create( + name="preingest.tasks.ValidateXMLFile", + params={ + "xml_filename": infoxml + }, + processstep_pos=14, + log=EventIP, + information_package=ip, + responsible=self.request.user, + ) + ) + + if validate_file_format or validate_integrity: + step.tasks.add( + ProcessTask.objects.create( + name="preingest.tasks.ValidateFiles", + params={ + "ip": ip, + "rootdir": reception, + "xmlfile": infoxml, + "validate_fileformat": validate_file_format, + "validate_integrity": validate_integrity, + }, + processstep_pos=15, + log=EventIP, + information_package=ip, + responsible=self.request.user, + ) + ) + + if validate_logical_physical_representation: + step.tasks.add( + ProcessTask.objects.create( + name="preingest.tasks.ValidateLogicalPhysicalRepresentation", + params={ + "files": [os.path.basename(ip.ObjectPath)], + "xmlfile": infoxml, + }, + processstep_pos=16, + log=EventIP, + information_package=ip, + responsible=self.request.user, + ) + ) + + step.tasks.add(ProcessTask.objects.create( + name="preingest.tasks.SubmitSIP", + params={ + "ip": ip + }, + processstep_pos=20, + log=EventIP, + information_package=ip, + responsible=self.request.user, + )) + + if ip.get_email_recipient(): + recipients = [ip.get_email_recipient()] + subject = request.data.get('subject') + body = request.data.get('body') + + attachments = [ip.ObjectPath] + + step.tasks.add(ProcessTask.objects.create( + name="ESSArch_Core.tasks.SendEmail", + params={ + 'sender': self.request.user.email, + 'recipients': recipients, + 'subject': subject, + 'body': body, + 'attachments': attachments + }, + processstep_pos=25, + information_package=ip, + responsible=self.request.user + )) + + step.tasks.add(ProcessTask.objects.create( + name="preingest.tasks.UpdateIPStatus", + params={ + "ip": ip, + "status": "Submitted" + }, + processstep_pos=30, + log=EventIP, + information_package=ip, + responsible=self.request.user, + )) + + step.save() + step.run() + + return Response({'status': 'submitting ip'}) + + @detail_route(methods=['put'], url_path='check-profile') + def check_profile(self, request, pk=None): + ip = self.get_object() + ptype = request.data.get("type") + + try: + pip = ProfileIP.objects.get(ip=ip, profile__profile_type=ptype) + + if not pip.LockedBy: + pip.included = request.data.get('checked', not pip.included) + pip.save() + except ProfileIP.DoesNotExist: + print "pip does not exist" + pass + + return Response() + + @detail_route(methods=['put'], url_path='change-profile') + def change_profile(self, request, pk=None): + ip = self.get_object() + new_profile = Profile.objects.get(pk=request.data["new_profile"]) + + ip.change_profile(new_profile) + + return Response({ + 'status': 'updating IP (%s) with new profile (%s)' % ( + ip.pk, new_profile + ) + }) + + @detail_route(methods=['post'], url_path='unlock-profile', permission_classes=[CanUnlockProfile]) + def unlock_profile(self, request, pk=None): + ip = self.get_object() + ptype = request.data.get("type") + + if ptype: + ip.unlock_profile(ptype) + return Response({ + 'status': 'unlocking profile with type "%s" in IP "%s"' % ( + ptype, ip.pk + ) + }) + + return Response() + + @detail_route(methods=['get', 'post'], url_path='upload') + def upload(self, request, pk=None): + ip = self.get_object() + ip.State = "Uploading" + ip.save() + dst, _ = find_destination('content', ip.get_profile('sip').structure) + + if dst is None: + dst = '' + + if request.method == 'GET': + path = os.path.join(dst, request.GET.get('flowRelativePath', '')) + chunk_nr = request.GET.get('flowChunkNumber') + chunk_path = "%s_%s" % (path, chunk_nr) + + if os.path.exists(os.path.join(ip.ObjectPath, chunk_path)): + return HttpResponse(status=200) + return HttpResponse(status=204) + + if request.method == 'POST': + path = os.path.join(dst, request.data.get('flowRelativePath', '')) + chunk_nr = request.data.get('flowChunkNumber') + chunk_path = "%s_%s" % (path, chunk_nr) + chunk_path = os.path.join(ip.ObjectPath, chunk_path) + + chunk = request.FILES['file'] + + if not os.path.exists(os.path.dirname(chunk_path)): + mkdir_p(os.path.dirname(chunk_path)) + + with open(chunk_path, 'wb+') as dst: + for c in chunk.chunks(): + dst.write(c) + + if chunk_nr == request.data.get('flowTotalChunks'): + path = os.path.join(ip.ObjectPath, path) + + with open(path, 'wb') as f: + for chunk_file in glob.glob('%s_*' % path): + f.write(open(chunk_file).read()) + os.remove(chunk_file) + + event_type = EventType.objects.get(eventType=10120) + agent = request.user + create_event( + event_type, 0, "Uploaded %s" % path, + get_versions()['version'], agent, ip=ip + ) + + return Response("Uploaded files") + + @detail_route(methods=['post'], url_path='set-uploaded', permission_classes=[CanSetUploaded]) + def set_uploaded(self, request, pk=None): + ip = self.get_object() + ip.State = "Uploaded" + ip.save() + return Response() + + +class EventIPViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows events to be viewed or edited. + """ + queryset = EventIP.objects.all() + serializer_class = EventIPSerializer + filter_backends = ( + filters.OrderingFilter, DjangoFilterBackend, + ) + ordering_fields = ( + 'id', 'eventType', 'eventOutcomeDetailNote', 'eventOutcome', + 'linkingAgentIdentifierValue', 'eventDateTime', + ) + + def create(self, request): + """ + """ + + outcomeDetailNote = request.data.get('eventOutcomeDetailNote', None) + outcome = request.data.get('eventOutcome', 0) + type_id = request.data.get('eventType', None) + ip_id = request.data.get('information_package', None) + + eventType = EventType.objects.get(pk=type_id) + ip = InformationPackage.objects.get(pk=ip_id) + agent = request.user + + EventIP.objects.create( + eventOutcome=outcome, eventOutcomeDetailNote=outcomeDetailNote, + eventType=eventType, linkingObjectIdentifierValue=ip, + linkingAgentIdentifierValue=agent + ) + return Response({"status": "Created event"}) diff --git a/ESSArch_TP/profiles/__init__.py b/ESSArch_TP/profiles/__init__.py new file mode 100644 index 00000000..dcf2731c --- /dev/null +++ b/ESSArch_TP/profiles/__init__.py @@ -0,0 +1,24 @@ +""" + ESSArch is an open source archiving and digital preservation system + + ESSArch Tools for Producer (ETP) + Copyright (C) 2005-2017 ES Solutions AB + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + Contact information: + Web - http://www.essolutions.se + Email - essarch@essolutions.se +""" + diff --git a/ESSArch_TP/profiles/views.py b/ESSArch_TP/profiles/views.py new file mode 100644 index 00000000..1a2e30e4 --- /dev/null +++ b/ESSArch_TP/profiles/views.py @@ -0,0 +1,323 @@ +""" + ESSArch is an open source archiving and digital preservation system + + ESSArch Tools for Producer (ETP) + Copyright (C) 2005-2017 ES Solutions AB + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + Contact information: + Web - http://www.essolutions.se + Email - essarch@essolutions.se +""" + +import os + +from django.core.exceptions import ValidationError +from django.db import IntegrityError +from django.db.models import Prefetch + +from rest_framework import status +from rest_framework.decorators import detail_route +from rest_framework.response import Response + +from ESSArch_Core.configuration.models import ( + Path, +) + +from ESSArch_Core.ip.models import ( + ArchivalInstitution, + ArchivistOrganization, + ArchivalLocation, + ArchivalType, + EventIP, + InformationPackage, +) + +from ESSArch_Core.ip.permissions import ( + CanLockSA, +) + +from ESSArch_Core.WorkflowEngine.models import ( + ProcessStep, + ProcessTask, +) + +from ESSArch_Core.profiles.serializers import ( + ProfileSerializer, + ProfileSASerializer, + ProfileIPSerializer, + SubmissionAgreementSerializer +) + +from ESSArch_Core.profiles.models import ( + SubmissionAgreement, + Profile, + ProfileSA, + ProfileIP, +) + +from rest_framework import viewsets + + +class SubmissionAgreementViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows submission agreements to be viewed or edited. + """ + queryset = SubmissionAgreement.objects.all().prefetch_related( + Prefetch('profilesa_set', to_attr='profiles') + ) + serializer_class = SubmissionAgreementSerializer + + @detail_route(methods=['post'], url_path='include-type') + def include_type(self, request, pk=None): + sa = SubmissionAgreement.objects.get(pk=pk) + ptype = request.data["type"] + + setattr(sa, "include_profile_%s" % ptype, True) + sa.save() + + return Response({ + 'status': 'Including profile type %s in SA %s' % (ptype, sa) + }) + + @detail_route(methods=['post'], url_path='exclude-type') + def exclude_type(self, request, pk=None): + sa = SubmissionAgreement.objects.get(pk=pk) + ptype = request.data["type"] + + setattr(sa, "include_profile_%s" % ptype, False) + sa.save() + + return Response({ + 'status': 'Excluding profile type %s in SA %s' % (ptype, sa) + }) + + @detail_route(methods=["post"]) + def lock(self, request, pk=None): + sa = self.get_object() + ip_id = request.data.get("ip") + + try: + ip = InformationPackage.objects.get( + pk=ip_id + ) + except InformationPackage.DoesNotExist: + return Response( + {'status': 'Information Package with id %s does not exist' % ip_id}, + status=status.HTTP_404_NOT_FOUND + ) + + permission = CanLockSA() + if not permission.has_object_permission(request, self, ip): + self.permission_denied( + request, message=getattr(permission, 'message', None) + ) + + if ip.SubmissionAgreement == sa: + ip.SubmissionAgreementLocked = True + ip.save() + + return Response({'status': 'locking submission_agreement'}) + elif ip.SubmissionAgreement is None: + return Response( + {'status': 'No SA connected to IP'}, + status=status.HTTP_400_BAD_REQUEST + ) + else: + return Response( + {'status': 'This SA is not connected to the selected IP'}, + status=status.HTTP_400_BAD_REQUEST + ) + + return Response({'status': 'Not allowed to lock SA'}, status=status.HTTP_400_BAD_REQUEST) + + +class ProfileSAViewSet(viewsets.ModelViewSet): + queryset = ProfileSA.objects.all() + serializer_class = ProfileSASerializer + + +class ProfileIPViewSet(viewsets.ModelViewSet): + queryset = ProfileIP.objects.all() + serializer_class = ProfileIPSerializer + + +class ProfileViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows profiles to be viewed or edited. + """ + queryset = Profile.objects.all() + serializer_class = ProfileSerializer + + def get_queryset(self): + queryset = Profile.objects.all() + profile_type = self.request.query_params.get('type', None) + + if profile_type is not None: + queryset = queryset.filter(profile_type=profile_type) + + return queryset + + @detail_route(methods=['post']) + def save(self, request, pk=None): + profile = Profile.objects.get(pk=pk) + new_data = request.data.get("specification_data", {}) + new_structure = request.data.get("structure", {}) + + changed_data = (profile.specification_data.keys().sort() == new_data.keys().sort() and + profile.specification_data != new_data) + + changed_structure = profile.structure != new_structure + + if (changed_data or changed_structure): + new_profile = profile.copy_and_switch( + ip=InformationPackage.objects.get( + pk=request.data["information_package"] + ), + specification_data=new_data, + new_name=request.data["new_name"], + structure=new_structure, + ) + serializer = ProfileSerializer( + new_profile, context={'request': request} + ) + return Response(serializer.data) + + return Response({'status': 'no changes, not saving'}, status=status.HTTP_400_BAD_REQUEST) + + @detail_route(methods=["post"]) + def lock(self, request, pk=None): + profile = self.get_object() + + try: + profile.clean() + except ValidationError as e: + return Response({'status': repr(e)}, status=status.HTTP_400_BAD_REQUEST) + + ip_id = request.data.get( + "information_package", {} + ) + + try: + ip = InformationPackage.objects.get( + pk=ip_id + ) + except InformationPackage.DoesNotExist: + return Response( + {'status': 'Information Package with id %s does not exist' % ip_id}, + status=status.HTTP_404_NOT_FOUND + ) + + if not (ip.SubmissionAgreement and ip.SubmissionAgreementLocked): + return Response( + {'status': 'IP needs a locked SA before locking profile'}, + status=status.HTTP_400_BAD_REQUEST + ) + + profile_ip, _ = ProfileIP.objects.get_or_create(profile=profile, ip=ip) + profile_ip.lock(request.user) + + if profile.profile_type == "sip": + root = os.path.join( + Path.objects.get( + entity="path_preingest_prepare" + ).value, + str(ip.pk) + ) + + step = ProcessStep.objects.create( + name="Create Physical Model", + information_package=ip + ) + task = ProcessTask.objects.create( + name="preingest.tasks.CreatePhysicalModel", + params={ + "structure": profile.structure, + "root": root + }, + log=EventIP, + information_package=ip, + responsible=self.request.user, + ) + + step.tasks = [task] + step.save() + step.run_eagerly() + + if profile.profile_type == "transfer_project": + archival_institution = profile.specification_data.get("archival_institution") + archivist_organization = profile.specification_data.get("archivist_organization") + archival_type = profile.specification_data.get("archival_type") + archival_location = profile.specification_data.get("archival_location") + + if archival_institution: + try: + (arch, _) = ArchivalInstitution.objects.get_or_create( + name=archival_institution + ) + except IntegrityError: + arch = ArchivalInstitution.objects.get( + name=archival_institution + ) + ip.ArchivalInstitution = arch + + if archivist_organization: + try: + (arch, _) = ArchivistOrganization.objects.get_or_create( + name=archivist_organization + ) + except IntegrityError: + arch = ArchivistOrganization.objects.get( + name=archivist_organization + ) + ip.ArchivistOrganization = arch + + if archival_type: + try: + (arch, _) = ArchivalType.objects.get_or_create( + name=archival_type + ) + except IntegrityError: + arch = ArchivalType.objects.get( + name=archival_type + ) + ip.ArchivalType = arch + + if archival_location: + try: + (arch, _) = ArchivalLocation.objects.get_or_create( + name=archival_location + ) + except IntegrityError: + arch = ArchivalLocation.objects.get( + name=archival_location + ) + ip.ArchivalLocation = arch + + ip.save() + + non_locked_sa_profiles = ProfileSA.objects.filter( + submission_agreement=ip.SubmissionAgreement, + ).exclude( + profile__profile_type__in=ProfileIP.objects.filter( + ip=ip, LockedBy__isnull=False + ).values('profile__profile_type') + ).exists() + + if not non_locked_sa_profiles: + ip.State = "Prepared" + ip.save(update_fields=['State']) + + return Response({'status': 'locking profile'})