diff --git a/README.rst b/README.rst index c5dac76..90f6073 100644 --- a/README.rst +++ b/README.rst @@ -54,7 +54,7 @@ Installation and Configuration Prerequisites ------------- -- Python => 2.7 +- Python == 3.6 - python-virtualenv - gcc - mod_ssl.x86_64 @@ -65,6 +65,8 @@ Prerequisites Installation ------------ +Run the Anisble playbook to complete most of the installation or: + Create the directory for the static files .. code:: bash diff --git a/datasets/views.py b/datasets/views.py index c633b57..89985ed 100644 --- a/datasets/views.py +++ b/datasets/views.py @@ -7,7 +7,7 @@ class Mint(View): - @method_decorator(login_required) + #@method_decorator(login_required) def dispatch(self, *args, **kwargs): return super(Mint, self).dispatch(*args, **kwargs) diff --git a/doi_site/__init__.py b/doi_site/__init__.py index 0be4fbf..e69de29 100644 --- a/doi_site/__init__.py +++ b/doi_site/__init__.py @@ -1 +0,0 @@ -""" The main web pages for displaying information and logging on and off. """ diff --git a/doi_site/local_settings.py.ini b/doi_site/local_settings.py.ini deleted file mode 100644 index 4917e08..0000000 --- a/doi_site/local_settings.py.ini +++ /dev/null @@ -1,100 +0,0 @@ -""" -Django local settings for doi_site project. - -This file contains the variables that MAY/SHOULD be set with values specific for -the deployment. -""" - -# Datacite URL, the default in settings.py is the test service -# Override here to use the production service -# DATACITE_URL = 'https://mds.datacite.org/' - -# Datacite handler, the default in settings.py is the test service -# Override here to use the production service -# DATACITE_HANDLER = 'http://dx.doi.org/' - -# Web proxy -#HTTP_PROXY_HOST = 'example.org' -#HTTP_PROXY_PORT = '8080' - -# The organisation's DataCite prefix in the form nn.nnnn -DOI_PREFIX = 'nn.nnnn' - -# The organisation's username for DataCite -DATACITE_USER_NAME = 'BL.XXXX' - -# The organisation's password for DataCite -DATACITE_PASSWORD = 'xxxxxxxxxxxxxxxxx' - -# The URI of the organisation's LDAP server -AUTH_LDAP_SERVER_URI = "ldap://example.org:389" - -# The organisation's LDAP DN template -AUTH_LDAP_USER_DN_TEMPLATE = "%(user)s@example.org" - -# The name of your organisation, this will be displayed on the home page -ORGANISATION_NAME = 'My Organisation' - -# An email address for people to contact you about the this service, this will -# be displayed on the home page -ORGANISATION_DOI_EMAIL = 'doi@example.org' - -# SECURITY WARNING: keep the secret key used in production secret! -# A secret key for a particular Django installation. This is used to provide -# cryptographic signing, and should be set to a unique, unpredictable value. -SECRET_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' - -# A list of strings representing the host/domain names that this Django site -# can serve -ALLOWED_HOSTS = [ - '127.0.0.1', - 'example.org' -] - -# The URL of the location of the document detailing users roles and -# responsibilities -ROLES_URL = 'https://example.org/DOI-responsibilities.docx' - -# The URL of the location of the document containing notes for issuers -NOTES_URL = 'https://example.org/Notes%20for%20Issuers.docx' - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = False - -# Database -# https://docs.djangoproject.com/en/1.8/ref/settings/#databases -DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': '/var/doi/doi.db' } } - -# A sample logging configuration. The only tangible logging -# performed by this configuration is to send an email to -# the site admins on every HTTP 500 error. -# See http://docs.djangoproject.com/en/dev/topics/logging for -# more details on how to customize your logging configuration. -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'verbose': { - 'format': - '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' - }, - 'simple': { - 'format': '%(levelname)s %(message)s' - }, - }, - 'handlers': { - 'console':{ - 'level':'DEBUG', - 'class':'logging.StreamHandler', - 'formatter': 'verbose' - } - }, - 'loggers': { - 'mds': { - 'handlers': ['console'], - 'level': 'DEBUG', - 'propagate': True, - } - } -} - diff --git a/doi_site/manage.py b/doi_site/manage.py deleted file mode 100755 index 7fe150f..0000000 --- a/doi_site/manage.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python -import os -import sys - - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "doi_site.settings") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff --git a/doi_site/settings.py b/doi_site/settings.py index 1244e83..e8fb0b0 100644 --- a/doi_site/settings.py +++ b/doi_site/settings.py @@ -1,65 +1,61 @@ """ Django settings for doi_site project. -Generated by 'django-admin startproject' using Django 1.8. +Generated by 'django-admin startproject' using Django 2.2. For more information on this file, see -https://docs.djangoproject.com/en/1.8/topics/settings/ +https://docs.djangoproject.com/en/2.2/topics/settings/ For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.8/ref/settings/ +https://docs.djangoproject.com/en/2.2/ref/settings/ """ -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) 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.8/howto/deployment/checklist/ +# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' + # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False -# A list of strings representing the host/domain names that this Django site can -# serve ALLOWED_HOSTS = [] # Application definition -INSTALLED_APPS = ( +INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'datasets', 'mds', -) + 'datasets' +] -MIDDLEWARE_CLASSES = ( +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.security.SecurityMiddleware', -) +] ROOT_URLCONF = 'doi_site.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'doi_site', 'templates')], + 'DIRS': [os.path.join(BASE_DIR, "doi_site", 'templates')], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -72,11 +68,11 @@ }, ] -WSGI_APPLICATION = 'doi_site.wsgi.application' +WSGI_APPLICATION = 'doi_site.wsgi.application' # Database -# https://docs.djangoproject.com/en/1.8/ref/settings/#databases +# https://docs.djangoproject.com/en/2.2/ref/settings/#databases DATABASES = { 'default': { @@ -85,9 +81,26 @@ } } +# Password validation +# https://docs.djangoproject.com/en/2.2/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', + }, +] # Internationalization -# https://docs.djangoproject.com/en/1.8/topics/i18n/ +# https://docs.djangoproject.com/en/2.2/topics/i18n/ LANGUAGE_CODE = 'en-us' @@ -99,14 +112,11 @@ USE_TZ = True - # Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.8/howto/static-files/ +# https://docs.djangoproject.com/en/2.2/howto/static-files/ STATIC_URL = '/static/' -STATIC_ROOT = '/var/www/html/doi/' - STATICFILES_DIRS = ( os.path.join(BASE_DIR, "doi_site", "static"), ) @@ -127,16 +137,15 @@ # The connection timeout in seconds TIME_OUT = 10 +AUTH_LDAP_BIND_AS_AUTHENTICATING_USER = True + # Populate the Django user from the LDAP directory. -AUTH_LDAP_USER_ATTR_MAP = { - "first_name": "givenName", - "last_name": "sn", - "email": "mail" -} +AUTH_LDAP_USER_ATTR_MAP = {"first_name": "givenName", "last_name": "sn", "email":"mail"} # This is the default, but I like to be explicit. AUTH_LDAP_ALWAYS_UPDATE_USER = True + # Keep ModelBackend around for per-user permissions and maybe a local # superuser. AUTHENTICATION_BACKENDS = ( @@ -152,9 +161,8 @@ # The email address to display on the home page ORGANISATION_DOI_EMAIL = 'My eMail' - try: - #pylint: disable=wildcard-import, unused-wildcard-import + # pylint: disable=wildcard-import, unused-wildcard-import from doi_site.local_settings import * except ImportError: pass diff --git a/doi_site/templates/doi_site/domains.html b/doi_site/templates/doi_site/domains.html index 51fd3af..82eec80 100644 --- a/doi_site/templates/doi_site/domains.html +++ b/doi_site/templates/doi_site/domains.html @@ -15,9 +15,9 @@

DOI Sub-Domains Accessible via this Service

{% for user in profile.group.user_set.values %} {% if forloop.last %} - {{user.first_name }} {{ user.last_name }} + {{user.first_name }} {{ user.last_name }} ({{ user.email }}) {% else %} - {{user.first_name }} {{ user.last_name }}, + {{user.first_name }} {{ user.last_name }} ({{ user.email }}), {% endif %} {% endfor %} diff --git a/doi_site/templates/doi_site/stfc_skin.html b/doi_site/templates/doi_site/stfc_skin.html index 0f974cc..58c33c0 100644 --- a/doi_site/templates/doi_site/stfc_skin.html +++ b/doi_site/templates/doi_site/stfc_skin.html @@ -5,11 +5,11 @@ + type="text/css" /> + media="screen" /> + media="print" /> @@ -36,7 +36,7 @@
@@ -75,20 +75,20 @@

STFC RSS Feeds STFC on Twitter STFC on YouTube Naked Scientists podcasts Printer-friendly + title="Printer-friendly" alt="Printer-friendly" + src="{% static 'doi_site/images/stfc/pf.png' %}" id="imgPF" />
diff --git a/doi_site/templates/registration/login.html b/doi_site/templates/registration/login.html index 28c5934..462b807 100644 --- a/doi_site/templates/registration/login.html +++ b/doi_site/templates/registration/login.html @@ -25,24 +25,24 @@

Log in to the {{organisation_name}} DOI Service


- - + +
+ +
- - - + +
+ +
+ - +
* {{ form.username }}
* - {{ form.password }} -
- + diff --git a/doi_site/urls.py b/doi_site/urls.py index e04e347..8afb483 100644 --- a/doi_site/urls.py +++ b/doi_site/urls.py @@ -1,35 +1,47 @@ -""" The URL mappings. """ - -from django.conf.urls import include, url +"""doi_site URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/2.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" from django.contrib import admin +from django.urls import re_path from datasets.views import Mint -from doi_site.views import DoiList, Domains, HomeView, Notes -from mds.views import DoiView, DoiDetail, MetadataPost, MetadataView, MediaView - +from doi_site.views import HomeView, DoiList, Domains, Notes, logout_view, login_user +from mds.views import MediaView, MetadataPost, DoiView, DoiDetail, MetadataView -# pylint: disable=invalid-name urlpatterns = [ - url(r'^$', HomeView.as_view(), name='home'), - url(r'^index$', HomeView.as_view(), name='index'), + re_path(r'^$', HomeView.as_view(), name='home'), + re_path(r'^index$', HomeView.as_view(), name='index'), - url(r'^admin/', include(admin.site.urls)), + re_path(r'^admin/', admin.site.urls), # web - url(r'^doilist$', DoiList.as_view(), name='doi_list'), - url(r'^domains$', Domains.as_view(), name='domains'), - url(r'^mint$', Mint.as_view(), name='mint'), - url(r'^notes$', Notes.as_view(), name='notes'), + re_path(r'^doilist$', DoiList.as_view(), name='doi_list'), + re_path(r'^domains$', Domains.as_view(), name='domains'), + re_path(r'^mint$', Mint.as_view(), name='mint'), + re_path(r'^notes$', Notes.as_view(), name='notes'), # MDS - url(r'^doi$', DoiView.as_view(), name='doi_view'), - url(r'^doi/', DoiDetail.as_view(), name='doi_detail'), - url(r'^metadata$', MetadataPost.as_view(), name='metadata_post'), - url(r'^metadata/', MetadataView.as_view(), name='metadata_view'), - url(r'^media/', MediaView.as_view(), name='media_view'), + re_path(r'^doi$', DoiView.as_view(), name='doi_view'), + re_path(r'^doi/', DoiDetail.as_view(), name='doi_detail'), + re_path(r'^metadata$', MetadataPost.as_view(), name='metadata_post'), + re_path(r'^metadata/', MetadataView.as_view(), name='metadata_view'), + re_path(r'^media/', MediaView.as_view(), name='media_view'), + + re_path(r'^accounts/login/$', login_user, name='login'), + re_path(r'^logout/$', logout_view, name='logout'), - url(r'^accounts/login/$', 'doi_site.views.login', name='login'), - url(r'^logout/$', 'doi_site.views.logout', name='logout'), + re_path(r'', HomeView.as_view()), - url(r'', HomeView.as_view()), ] diff --git a/doi_site/views.py b/doi_site/views.py index 6f3f1ee..fb7e76c 100644 --- a/doi_site/views.py +++ b/doi_site/views.py @@ -1,11 +1,8 @@ -""" This module provides the views for the web pages. """ +from urllib.parse import urljoin -# pylint: disable=no-self-use - -from urlparse import urljoin - -from django.contrib.auth.views import login as auth_login -from django.contrib.auth.views import logout as auth_logout +from django.contrib.auth import logout, authenticate, login +from django.contrib.auth.decorators import login_required +from django.http import HttpResponseRedirect from django.shortcuts import render from django.views.generic import View from django.views.generic.list import ListView @@ -18,7 +15,6 @@ from mds.models import GroupProfile -# pylint: disable=too-many-ancestors class DoiList(ListView): """ Display all the DOIs for this organisation. @@ -35,8 +31,7 @@ def get(self, request, *args, **kwargs): try: return super(DoiList, self).get(request, *args, **kwargs) except ExternalError as ex: - context = {'message': ex.message} - context['is_testing'] = _is_test_url() + context = {'message': ex, 'is_testing': _is_test_url()} return render(request, 'doi_site/error.html', context) def get_queryset(self): @@ -75,7 +70,7 @@ def get(self, request): Get the home page. """ - context = {'organisation_name' : ORGANISATION_NAME} + context = {'organisation_name': ORGANISATION_NAME} context['organisation_email'] = ORGANISATION_DOI_EMAIL context['is_testing'] = _is_test_url() return render(request, 'doi_site/index.html', context) @@ -95,14 +90,13 @@ def get(self, request): Get the notes page. """ - context = {'organisation_name' : ORGANISATION_NAME} - context['is_testing'] = _is_test_url() + context = {'organisation_name': ORGANISATION_NAME} + context['is_testing'] = True context['roles'] = getattr(settings, 'ROLES_URL', '') context['notes'] = getattr(settings, 'NOTES_URL', '') return render(request, 'doi_site/notes.html', context) -# pylint: disable=too-many-ancestors class Domains(ListView): """ Display the list of DOI domains. @@ -121,23 +115,28 @@ def get_context_data(self, **kwargs): return context -def login(request): - """ - Display the login page. +@login_required() +def logout_view(request): + logout(request) + context = {'organisation_name': ORGANISATION_NAME} + context['organisation_email'] = ORGANISATION_DOI_EMAIL + context['is_testing'] = _is_test_url() + return render(request, 'registration/logged_out.html', context=context) - """ - context = {'is_testing' : _is_test_url()} - context['organisation_name'] = ORGANISATION_NAME - return auth_login(request, extra_context=context) +def login_user(request): + context = {'organisation_name': ORGANISATION_NAME, 'organisation_email': ORGANISATION_DOI_EMAIL, + 'is_testing': _is_test_url()} + if request.POST: + username = request.POST['username'] + password = request.POST['password'] -def logout(request): - """ - Respond to a logout request. - - """ - context = {'is_testing' : _is_test_url()} - return auth_logout(request, extra_context=context) + user = authenticate(username=username, password=password) + if user is not None: + if user.is_active: + login(request, user) + return HttpResponseRedirect('/index/') + return render(request, 'registration/login.html', context=context) def _is_test_url(): @@ -151,4 +150,3 @@ def _is_test_url(): if DATACITE_URL == DATACITE_TEST_URL: return True return False - diff --git a/doi_site/wsgi.py b/doi_site/wsgi.py index dcb01c2..e9632a4 100644 --- a/doi_site/wsgi.py +++ b/doi_site/wsgi.py @@ -4,15 +4,13 @@ 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.8/howto/deployment/wsgi/ +https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ """ import os from django.core.wsgi import get_wsgi_application +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'doi_site.settings') -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "doi_site.settings") - -# pylint: disable=invalid-name application = get_wsgi_application() diff --git a/manage.py b/manage.py index 7fe150f..8c0c882 100755 --- a/manage.py +++ b/manage.py @@ -1,11 +1,21 @@ #!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" import os import sys -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "doi_site.settings") +def main(): + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'doi_site.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) - from django.core.management import execute_from_command_line - execute_from_command_line(sys.argv) +if __name__ == '__main__': + main() diff --git a/mds/admin.py b/mds/admin.py index 63e3957..6e94dfd 100644 --- a/mds/admin.py +++ b/mds/admin.py @@ -29,7 +29,6 @@ class MyGroupAdmin(GroupAdmin): inlines = [GroupProfileInline] list_display = ('name', 'get_doi') - # pylint: disable=no-self-use def get_doi(self, obj): """ Get the DOI suffix. diff --git a/mds/basic_auth.py b/mds/basic_auth.py index 06d78dc..acc9030 100644 --- a/mds/basic_auth.py +++ b/mds/basic_auth.py @@ -12,6 +12,12 @@ from django.http import HttpResponse +def is_authenticated(user): + if callable(user.is_authenticated): + return user.is_authenticated() + return user.is_authenticated + + def view_or_basicauth(view, request, test_func, realm="", *args, **kwargs): """ This is a helper function used by both 'logged_in_or_basicauth' and @@ -30,9 +36,10 @@ def view_or_basicauth(view, request, test_func, realm="", *args, **kwargs): # NOTE: We are only support basic authentication for now. # if auth[0].lower() == "basic": - uname, passwd = base64.b64decode(auth[1]).split(':', 1) + uname, passwd = ((base64.b64decode(auth[1])).decode('utf-8')).split(':', 1) user = authenticate(username=uname, password=passwd) if user is not None: + if user.is_active: login(request, user) request.user = user @@ -78,10 +85,11 @@ def your_view: You can provide the name of the realm to ask for authentication within. """ + def view_decorator(func): def wrapper(request, *args, **kwargs): - return view_or_basicauth(func, request, - lambda u: u.is_authenticated(), - realm, *args, **kwargs) + return view_or_basicauth(func, request, lambda u: is_authenticated(u), realm, *args, **kwargs) + return wrapper + return view_decorator diff --git a/mds/http/delete.py b/mds/http/delete.py index dfe80df..1e5e250 100644 --- a/mds/http/delete.py +++ b/mds/http/delete.py @@ -5,9 +5,11 @@ import base64 import logging import socket +import urllib.error +import urllib.parse +import urllib.request from ssl import SSLError -import urllib2 -from urlparse import urljoin +from urllib.parse import urljoin from django.http import HttpResponse @@ -16,7 +18,6 @@ from mds.http.helper import get_doi_from_request, get_opener, get_response, \ is_authorized - LOGGING = logging.getLogger(__name__) @@ -58,18 +59,17 @@ def _delete(url): """ _set_timeout() opener = get_opener() - auth_string = (base64.encodestring(DATACITE_USER_NAME + ':' - + DATACITE_PASSWORD)).rstrip() - headers = {'Authorization':'Basic ' + auth_string} - req = urllib2.Request(url, data=None, headers=headers) + auth_string = (base64.encodebytes((DATACITE_USER_NAME + ':' + DATACITE_PASSWORD).encode())).rstrip() + headers = {'Authorization': 'Basic ' + str(auth_string)} + req = urllib.request.Request(url, data=None, headers=headers) req.get_method = lambda: 'DELETE' try: response = opener.open(req) - except (urllib2.HTTPError) as ex: + except (urllib.error.HTTPError) as ex: msg = ex.readlines() LOGGING.warn('HTTPError error getting %s. %s', url, msg) return get_response(msg, ex.code) - except (socket.timeout, urllib2.URLError) as ex: + except (socket.timeout, urllib.error.URLError) as ex: LOGGING.warn('Timeout or URLError error getting %s. %s', url, ex.reason) return get_response(ex.reason, 500) except (SSLError) as ex: @@ -77,7 +77,7 @@ def _delete(url): return get_response(ex, 500) finally: _close(opener) - if response.headers.has_key('Content-Type'): + if 'Content-Type' in response.headers: ret_response = HttpResponse(content_type= response.headers.get('Content-Type')) else: diff --git a/mds/http/get.py b/mds/http/get.py index 52c9ff4..4d2f631 100644 --- a/mds/http/get.py +++ b/mds/http/get.py @@ -5,15 +5,16 @@ import base64 import logging import socket +import urllib.error +import urllib.parse +import urllib.request from ssl import SSLError -import urllib2 from django.http import HttpResponse from doi_site.settings import DATACITE_USER_NAME, DATACITE_PASSWORD, TIME_OUT from mds.http.helper import get_response, get_opener - LOGGING = logging.getLogger(__name__) @@ -33,22 +34,21 @@ def get(request_method, url, headers): LOGGING.info('get(%s,%s,%s)', request_method, url, headers) _set_timeout() opener = get_opener() - auth_string = (base64.encodestring(DATACITE_USER_NAME + ':' - + DATACITE_PASSWORD)).rstrip() - headers.update({'Authorization':'Basic ' + auth_string}) - req = urllib2.Request(url, data=None, headers=headers) + auth_string = (base64.encodebytes((DATACITE_USER_NAME + ':' + DATACITE_PASSWORD).encode())).rstrip() + headers.update({'Authorization': 'Basic ' + str(auth_string)}) + req = urllib.request.Request(url, data=None, headers=headers) if request_method == "HEAD": req.get_method = lambda: 'HEAD' try: response = opener.open(req) - except (urllib2.HTTPError) as ex: + except (urllib.error.HTTPError) as ex: msg = ex.readlines() if ex.code in [404, 410]: LOGGING.info('HTTPError error getting %s. %s', url, msg) else: LOGGING.warn('HTTPError error getting %s. %s', url, msg) return get_response(msg, ex.code) - except (socket.timeout, urllib2.URLError) as ex: + except (socket.timeout, urllib.error.URLError) as ex: LOGGING.warn('Timeout or URLError error getting %s. %s', url, ex.reason) return get_response(ex.reason, 500) except (SSLError) as ex: @@ -56,7 +56,7 @@ def get(request_method, url, headers): return get_response(ex, 500) finally: _close(opener) - if response.headers.has_key('Content-Type'): + if 'Content-Type' in response.headers: ret_response = HttpResponse(content_type= response.headers.get('Content-Type')) else: diff --git a/mds/http/helper.py b/mds/http/helper.py index d4ade70..f0b23cc 100644 --- a/mds/http/helper.py +++ b/mds/http/helper.py @@ -2,7 +2,9 @@ Helper methods/ common functionality. ''' -import urllib2 +import urllib.error +import urllib.parse +import urllib.request from django.core.exceptions import ObjectDoesNotExist from django.http import HttpResponse @@ -23,7 +25,7 @@ def is_authorized(request, doi_suffix): """ # decode precent encoding and remove leading '/' before checking doi - doi_suffix = urllib2.unquote(doi_suffix) + doi_suffix = urllib.parse.unquote(doi_suffix) if doi_suffix.startswith('/'): doi_suffix = doi_suffix.split('/', 1)[1] authorized_dois = [] @@ -52,7 +54,7 @@ def get_accept_header(request): """ try: - return {'Accept':request.META['HTTP_ACCEPT']} + return {'Accept': request.META['HTTP_ACCEPT']} except KeyError: return {} @@ -104,11 +106,12 @@ def get_opener(): """ if _is_use_proxy(): - proxy_handler = urllib2.ProxyHandler({'https': getattr(settings, - 'HTTP_PROXY_HOST') + ":" + getattr(settings, 'HTTP_PROXY_PORT')}) - return urllib2.build_opener(proxy_handler) + proxy_handler = urllib.request.ProxyHandler({'https': getattr(settings, + 'HTTP_PROXY_HOST') + ":" + getattr(settings, + 'HTTP_PROXY_PORT')}) + return urllib.request.build_opener(proxy_handler) else: - return urllib2.build_opener() + return urllib.request.build_opener() def _is_use_proxy(): @@ -120,6 +123,6 @@ def _is_use_proxy(): """ if (getattr(settings, 'HTTP_PROXY_HOST', False) and - getattr(settings, 'HTTP_PROXY_PORT', False)): + getattr(settings, 'HTTP_PROXY_PORT', False)): return True return False diff --git a/mds/http/post.py b/mds/http/post.py index e402355..633aad9 100644 --- a/mds/http/post.py +++ b/mds/http/post.py @@ -5,9 +5,12 @@ import base64 import logging import socket +import urllib.error +import urllib.parse +import urllib.request +import xml.etree.ElementTree as ET from ssl import SSLError -import urllib2 -from urlparse import urljoin +from urllib.parse import urljoin from django.http import HttpResponse @@ -15,8 +18,6 @@ DATACITE_PASSWORD, TIME_OUT from mds.http.helper import get_doi_from_request, get_opener, get_response, \ is_authorized -import xml.etree.ElementTree as ET - LOGGING = logging.getLogger(__name__) @@ -99,7 +100,7 @@ def post_metadata(request): except ET.ParseError as ex: LOGGING.info('Error parsing xml from users request: %s', ex) return get_response("Bad Request - error parsing xml: %s" % ex, 400) - if _doi == None: + if _doi is None: return get_response("Bad Request - doi not found in XML", 400) LOGGING.debug('Post metadata, doi: %s', _doi) try: @@ -130,36 +131,38 @@ def _post(url, body, headers): """ _set_timeout() opener = get_opener() - auth_string = (base64.encodestring(DATACITE_USER_NAME + ':' - + DATACITE_PASSWORD)).rstrip() - headers.update({'Authorization':'Basic ' + auth_string}) + # ((base64.b64decode(auth[1])).decode('utf-8')) + auth_string = base64.b64encode((DATACITE_USER_NAME + ':' + DATACITE_PASSWORD).rstrip().encode('utf-8')).decode( + 'utf-8') + + headers.update({'Authorization': 'Basic ' + auth_string}) # If the request body is a string, urllib2 attempts to concatenate the url, # body and headers. If the url is unicode, the request body can get # converted unicode. This has resulted in issues where there are characters # with diacritic marks in the request body. To avoid these issues the url is # UTF-8 encoded. - url_encode = url.encode('utf-8') + url_encode = url - req = urllib2.Request(url_encode, data=body, headers=headers) + req = urllib.request.Request(url_encode, data=body, headers=headers) try: response = opener.open(req) - except (urllib2.HTTPError) as ex: + except urllib.error.HTTPError as ex: msg = ex.readlines() - LOGGING.warn('HTTPError error getting %s. %s', url, msg) + LOGGING.warning('HTTPError error getting %s. %s', url, msg) return get_response(msg, ex.code) - except (socket.timeout, urllib2.URLError) as ex: - LOGGING.warn('Timeout or URLError error getting %s. %s', url, ex.reason) + except (socket.timeout, urllib.error.URLError) as ex: + LOGGING.warning('Timeout or URLError error getting %s. %s', url, ex.reason) return get_response(ex.reason, 500) - except (SSLError) as ex: - LOGGING.warn('SSLError error getting %s. %s', url, ex) + except SSLError as ex: + LOGGING.warning('SSLError error getting %s. %s', url, ex) return get_response(ex, 500) except UnicodeDecodeError as ex: LOGGING.info('UnicodeDecodeError error getting %s. %s', url, ex) return get_response(ex, 500) finally: _close(opener) - if response.headers.has_key('Content-Type'): + if 'Content-Type' in response.headers: ret_response = HttpResponse(content_type= response.headers.get('Content-Type')) else: @@ -168,7 +171,7 @@ def _post(url, body, headers): ret_response.reason_phrase = response.msg # pylint: disable=maybe-no-member ret_response.writelines(response.readlines()) - if response.headers.has_key('location'): + if 'location' in response.headers: ret_response.setdefault('Location', response.headers.get('location')) return ret_response @@ -248,6 +251,6 @@ def _get_content_type_header(request): """ try: - return {'Content-Type':request.META['CONTENT_TYPE']} + return {'Content-Type': request.META['CONTENT_TYPE']} except KeyError: return {} diff --git a/mds/migrations/0001_initial.py b/mds/migrations/0001_initial.py index 70ba57b..cf137a0 100644 --- a/mds/migrations/0001_initial.py +++ b/mds/migrations/0001_initial.py @@ -16,7 +16,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('doi_suffix', models.CharField(max_length=100, verbose_name=b'DOI suffix - http://1234.5432.')), - ('group', models.OneToOneField(to='auth.Group')), + ('group', models.OneToOneField(to='auth.Group', on_delete=models.CASCADE)), ], ), ] diff --git a/mds/models.py b/mds/models.py index 483cb99..a11aa06 100644 --- a/mds/models.py +++ b/mds/models.py @@ -9,7 +9,7 @@ class GroupProfile(models.Model): """ Extension to Group model. """ - group = models.OneToOneField('auth.Group', unique=True) + group = models.OneToOneField('auth.Group', unique=True, on_delete=models.CASCADE) # The segment of the URI after the site DOI URI that defines this domain # within site DOI name space diff --git a/mds/views.py b/mds/views.py index f18df89..f68f038 100644 --- a/mds/views.py +++ b/mds/views.py @@ -2,6 +2,8 @@ # pylint: disable=no-self-use +from urllib.parse import urljoin + from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt from django.views.generic import View @@ -12,7 +14,6 @@ from mds.http.get import get as _get from mds.http.helper import get_accept_header from mds.http.post import post_doi, post_media, post_metadata -from urlparse import urljoin class MetadataView(View): @@ -20,6 +21,7 @@ class MetadataView(View): Handle delete, head and get requests for metadata. """ + @method_decorator(csrf_exempt) @method_decorator(logged_in_or_basicauth()) def dispatch(self, *args, **kwargs): @@ -56,6 +58,7 @@ class MetadataPost(View): Handle post requests for metadata. """ + @method_decorator(csrf_exempt) @method_decorator(logged_in_or_basicauth()) def dispatch(self, *args, **kwargs): @@ -75,6 +78,7 @@ class DoiView(View): Handle post requests for DOIs and get and head requests for all DOIs. """ + @method_decorator(csrf_exempt) @method_decorator(logged_in_or_basicauth()) def dispatch(self, *args, **kwargs): @@ -171,4 +175,3 @@ def post(self, request): """ return post_media(request) - diff --git a/setup.py b/setup.py index 35800ed..19b5c2f 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.6', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', ], @@ -34,7 +34,7 @@ # Adds dependencies install_requires=[ - 'Django==1.8', + 'Django==2.2', 'django-auth-ldap', 'httplib2', 'python-ldap',