From 9880292e265212fc462154e2c96b2236e8afb69d Mon Sep 17 00:00:00 2001 From: William Moore Date: Thu, 6 Oct 2016 13:32:47 +0100 Subject: [PATCH 1/6] Moved /api/ code to new omeroweb/api/ app --- .../tools/OmeroWeb/omeroweb/api/__init__.py | 2 + .../{webgateway => api}/api_exceptions.py | 7 +- .../{webgateway => api}/api_marshal.py | 9 +- .../omeroweb/{webgateway => api}/api_query.py | 2 +- .../{webgateway => api}/decorators.py | 10 +- .../{webgateway/urls_api.py => api/urls.py} | 6 +- .../tools/OmeroWeb/omeroweb/api/views.py | 413 ++++++++++++++++++ components/tools/OmeroWeb/omeroweb/urls.py | 2 +- .../OmeroWeb/omeroweb/webclient/views.py | 3 +- .../OmeroWeb/omeroweb/webgateway/views.py | 345 +-------------- 10 files changed, 448 insertions(+), 351 deletions(-) create mode 100644 components/tools/OmeroWeb/omeroweb/api/__init__.py rename components/tools/OmeroWeb/omeroweb/{webgateway => api}/api_exceptions.py (88%) rename components/tools/OmeroWeb/omeroweb/{webgateway => api}/api_marshal.py (89%) rename components/tools/OmeroWeb/omeroweb/{webgateway => api}/api_query.py (98%) rename components/tools/OmeroWeb/omeroweb/{webgateway => api}/decorators.py (94%) rename components/tools/OmeroWeb/omeroweb/{webgateway/urls_api.py => api/urls.py} (94%) create mode 100644 components/tools/OmeroWeb/omeroweb/api/views.py diff --git a/components/tools/OmeroWeb/omeroweb/api/__init__.py b/components/tools/OmeroWeb/omeroweb/api/__init__.py new file mode 100644 index 00000000000..56c96010919 --- /dev/null +++ b/components/tools/OmeroWeb/omeroweb/api/__init__.py @@ -0,0 +1,2 @@ + +"""api module provides JSON OMERO API.""" diff --git a/components/tools/OmeroWeb/omeroweb/webgateway/api_exceptions.py b/components/tools/OmeroWeb/omeroweb/api/api_exceptions.py similarity index 88% rename from components/tools/OmeroWeb/omeroweb/webgateway/api_exceptions.py rename to components/tools/OmeroWeb/omeroweb/api/api_exceptions.py index ad24594249f..28c061bbaad 100644 --- a/components/tools/OmeroWeb/omeroweb/webgateway/api_exceptions.py +++ b/components/tools/OmeroWeb/omeroweb/api/api_exceptions.py @@ -17,15 +17,18 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +"""Exceptions used by the api/views methods.""" class BadRequestError(Exception): """ - An exception that will result in a response status of 400 - due to invalid client input + An exception that will result in a response status of 400. + + Due to invalid client input """ status = 400 def __init__(self, message, stacktrace=None): + """Override init to handle message and stacktrace.""" super(BadRequestError, self).__init__(message) self.stacktrace = stacktrace diff --git a/components/tools/OmeroWeb/omeroweb/webgateway/api_marshal.py b/components/tools/OmeroWeb/omeroweb/api/api_marshal.py similarity index 89% rename from components/tools/OmeroWeb/omeroweb/webgateway/api_marshal.py rename to components/tools/OmeroWeb/omeroweb/api/api_marshal.py index bc29769c782..bfd74c57140 100644 --- a/components/tools/OmeroWeb/omeroweb/webgateway/api_marshal.py +++ b/components/tools/OmeroWeb/omeroweb/api/api_marshal.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -''' Helper functions for views that handle object trees ''' +"""Helper functions for views that handle object trees.""" from omero_marshal import get_encoder @@ -25,7 +25,9 @@ def normalize_objects(objects): """ - Takes a list of dicts generated from omero_marshal and + Normalize the groups and owners from omero_marshal dicts. + + Take a list of dicts generated from omero_marshal and normalizes the groups and owners into separate lists. omero:details will only retain group and owner IDs. """ @@ -47,12 +49,11 @@ def normalize_objects(objects): def marshal_objects(objects, extras=None, normalize=False): """ - Marshals a list of OMERO.model objects using omero_marshal + Marshal a list of OMERO.model objects using omero_marshal. @param extras: A dict of id:dict to add extra data to each object @param normalize: If true, normalize groups & owners into separate lists """ - marshalled = [] for o in objects: encoder = get_encoder(o.__class__) diff --git a/components/tools/OmeroWeb/omeroweb/webgateway/api_query.py b/components/tools/OmeroWeb/omeroweb/api/api_query.py similarity index 98% rename from components/tools/OmeroWeb/omeroweb/webgateway/api_query.py rename to components/tools/OmeroWeb/omeroweb/api/api_query.py index 4ed4a738131..02290085436 100644 --- a/components/tools/OmeroWeb/omeroweb/webgateway/api_query.py +++ b/components/tools/OmeroWeb/omeroweb/api/api_query.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -''' Helper functions for views that handle object trees ''' +"""Helper functions for views that handle object trees.""" import omero diff --git a/components/tools/OmeroWeb/omeroweb/webgateway/decorators.py b/components/tools/OmeroWeb/omeroweb/api/decorators.py similarity index 94% rename from components/tools/OmeroWeb/omeroweb/webgateway/decorators.py rename to components/tools/OmeroWeb/omeroweb/api/decorators.py index c3e14d6fb26..33c0b267233 100644 --- a/components/tools/OmeroWeb/omeroweb/webgateway/decorators.py +++ b/components/tools/OmeroWeb/omeroweb/api/decorators.py @@ -33,15 +33,11 @@ logger = logging.getLogger(__name__) -class login_required(omeroweb.decorators.login_required): - """ - webgateway specific extension of the OMERO.web login_required() decorator. - """ +class LoginRequired(omeroweb.decorators.login_required): + """webgateway specific extension of the login_required() decorator.""" def on_not_logged_in(self, request, url, error=None): - """ - Used for json api methods - """ + """Used for json api methods.""" return JsonResponse({'message': 'Not logged in'}, status=403) diff --git a/components/tools/OmeroWeb/omeroweb/webgateway/urls_api.py b/components/tools/OmeroWeb/omeroweb/api/urls.py similarity index 94% rename from components/tools/OmeroWeb/omeroweb/webgateway/urls_api.py rename to components/tools/OmeroWeb/omeroweb/api/urls.py index 6d8c5753f1e..f3e6117a611 100644 --- a/components/tools/OmeroWeb/omeroweb/webgateway/urls_api.py +++ b/components/tools/OmeroWeb/omeroweb/api/urls.py @@ -17,11 +17,11 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -# Handles all 'api' urls without including '/webgateway/' in the url. +"""Handles all 'api' urls.""" from django.conf.urls import url, patterns -from omeroweb.webgateway import views -from omeroweb.webgateway.views import LoginView +from omeroweb.api import views +from omeroweb.api.views import LoginView from django.conf import settings import re diff --git a/components/tools/OmeroWeb/omeroweb/api/views.py b/components/tools/OmeroWeb/omeroweb/api/views.py new file mode 100644 index 00000000000..38476038982 --- /dev/null +++ b/components/tools/OmeroWeb/omeroweb/api/views.py @@ -0,0 +1,413 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (C) 2016 University of Dundee & Open Microscopy Environment. +# All rights reserved. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Views.py for the OMERO JSON api app.""" + +from django.views.generic import View +from django.middleware import csrf +from django.utils.decorators import method_decorator +from django.core.urlresolvers import reverse +from django.conf import settings +from django.http import HttpResponse, JsonResponse + +from functools import wraps +import logging +import traceback +import json + +import omero +import omero.clients +from api_query import query_projects +from omeroweb.webadmin.forms import LoginForm +from omeroweb.decorators import get_client_ip +from omeroweb.connector import Connector +from omeroweb.webadmin.webadmin_utils import upgradeCheck +from omero_marshal import get_encoder, get_decoder, OME_SCHEMA_URL +from omero import ValidationException +from omeroweb.connector import Server +from omeroweb.api.api_exceptions import BadRequestError +from omeroweb.api.decorators import LoginRequired +from omeroweb.webgateway.util import getIntOrDefault + + +logger = logging.getLogger(__name__) + +def json_response(f): + """ + Decorator for wrapping in JsonResponse and error handling. + + @param f: The function to wrap + @return: The wrapped function, which will return json + """ + @wraps(f) + def wrap(request, *args, **kwargs): + logger.debug('json_response') + try: + rv = f(request, *args, **kwargs) + if isinstance(rv, HttpResponse): + return rv + return JsonResponse(rv) + except Exception, ex: + # Default status is 500 'server error' + # But we try to handle all 'expected' errors appropriately + # TODO: handle omero.ConcurrencyException + status = 500 + trace = traceback.format_exc() + if isinstance(ex, BadRequestError): + status = 400 + trace = ex.stacktrace # Might be None + elif isinstance(ex, omero.SecurityViolation): + status = 403 + elif isinstance(ex, omero.ApiUsageException): + status = 400 + logger.debug(trace) + rsp_json = {"message": str(ex)} + if trace is not None: + rsp_json["stacktrace"] = trace + return JsonResponse(rsp_json, status=status) + wrap.func_name = f.func_name + return wrap + +def build_url(request, name, api_version, **kwargs): + """ + Helper for generating urls within /api json responses. + + By default we use request.build_absolute_uri() but this + can be configured by setting "omero.web.api.absolute_url" + to a string or empty string, used to prefix relative urls. + Extra **kwargs are passed to reverse() function. + + @param name: Name of the url + @param api_version Version string + """ + kwargs['api_version'] = api_version + url = reverse(name, kwargs=kwargs) + if settings.API_ABSOLUTE_URL is None: + return request.build_absolute_uri(url) + else: + # remove trailing slash + prefix = settings.API_ABSOLUTE_URL.rstrip('/') + return "%s%s" % (prefix, url) + + +@json_response +def api_versions(request, **kwargs): + """Base url of the webgateway json api.""" + versions = [] + for v in settings.API_VERSIONS: + versions.append({ + 'version': v, + 'base_url': build_url(request, 'api_base', v) + }) + return {'data': versions} + + +@json_response +def api_base(request, api_version=None, **kwargs): + """Base url of the webgateway json api for a specified version.""" + v = api_version + rv = {'projects_url': build_url(request, 'api_projects', v), + 'token_url': build_url(request, 'api_token', v), + 'servers_url': build_url(request, 'api_servers', v), + 'login_url': build_url(request, 'api_login', v), + 'save_url': build_url(request, 'api_save', v), + 'schema_url': OME_SCHEMA_URL} + return rv + + +@json_response +def api_token(request, api_version, **kwargs): + """Provide CSRF token for current session.""" + token = csrf.get_token(request) + return {'data': token} + + +@json_response +def api_servers(request, api_version, **kwargs): + """List the available servers to connect to.""" + servers = [] + for i, obj in enumerate(Server): + s = {'id': i + 1, + 'host': obj.host, + 'port': obj.port + } + if obj.server is not None: + s['server'] = obj.server + servers.append(s) + return {'data': servers} + + +class LoginView(View): + """Webgateway Login - Subclassed by WebclientLoginView.""" + + form_class = LoginForm + useragent = 'OMERO.webapi' + + def get(self, request, api_version=None): + """Simply return a message to say GET not supported.""" + return JsonResponse({"message": + ("POST only with username, password, " + "server and csrftoken")}, + status=405) + + def handle_logged_in(self, request, conn, connector): + """Return a response for successful login.""" + c = conn.getEventContext() + ctx = {} + for a in ['sessionId', 'sessionUuid', 'userId', 'userName', 'groupId', + 'groupName', 'isAdmin', 'eventId', 'eventType', + 'memberOfGroups', 'leaderOfGroups']: + if (hasattr(c, a)): + ctx[a] = getattr(c, a) + return JsonResponse({"success": True, "eventContext": ctx}) + + def handle_not_logged_in(self, request, error=None, form=None): + """ + Return a response for failed login. + + Reason for failure may be due to server 'error' or because + of form validation errors. + + @param request: http request + @param error: Error message + @param form: Instance of Login Form, populated with data + """ + if error is None and form is not None: + # If no error from server, maybe form wasn't valid + formErrors = [] + for field in form: + for e in field.errors: + formErrors.append("%s: %s" % (field.label, e)) + error = " ".join(formErrors) + elif error is None: + # Just in case no error or invalid form is given + error = "Login failed. Reason unknown." + return JsonResponse({"message": error}, status=403) + + def post(self, request, api_version=None): + """ + Here we handle the main login logic, creating a connection to OMERO. + + and store that on the request.session OR handling login failures + """ + error = None + form = self.form_class(request.POST.copy()) + if form.is_valid(): + username = form.cleaned_data['username'] + password = form.cleaned_data['password'] + server_id = form.cleaned_data['server'] + is_secure = form.cleaned_data['ssl'] + + connector = Connector(server_id, is_secure) + + # TODO: version check should be done on the low level, see #5983 + compatible = True + if settings.CHECK_VERSION: + compatible = connector.check_version(self.useragent) + if (server_id is not None and username is not None and + password is not None and compatible): + conn = connector.create_connection( + self.useragent, username, password, + userip=get_client_ip(request)) + if conn is not None: + request.session['connector'] = connector + # UpgradeCheck URL should be loaded from the server or + # loaded omero.web.upgrades.url allows to customize web + # only + try: + upgrades_url = settings.UPGRADES_URL + except: + upgrades_url = conn.getUpgradesUrl() + upgradeCheck(url=upgrades_url) + return self.handle_logged_in(request, conn, connector) + # Once here, we are not logged in... + # Need correct error message + if not connector.is_server_up(self.useragent): + error = ("Server is not responding," + " please contact administrator.") + elif not settings.CHECK_VERSION: + error = ("Connection not available, please check your" + " credentials and version compatibility.") + else: + if not compatible: + error = ("Client version does not match server," + " please contact administrator.") + else: + error = ("Connection not available, please check your" + " user name and password.") + return self.handle_not_logged_in(request, error, form) + + +class ProjectView(View): + """Handle access to an individual Project to GET or DELETE it.""" + + @method_decorator(LoginRequired(useragent='OMERO.webapi')) + @method_decorator(json_response) + def dispatch(self, *args, **kwargs): + """Wrap other methods to add decorators.""" + return super(ProjectView, self).dispatch(*args, **kwargs) + + def get(self, request, pid, conn=None, **kwargs): + """Simply GET a single Project and marshal it or 404 if not found.""" + project = conn.getObject("Project", pid) + if project is None: + return JsonResponse( + {'message': 'Project %s not found' % pid}, + status=404) + encoder = get_encoder(project._obj.__class__) + return encoder.encode(project._obj) + + def delete(self, request, pid, conn=None, **kwargs): + """ + Delete the Project and return marshal of deleted Project. + + Return 404 if not found. + """ + try: + project = conn.getQueryService().get('Project', long(pid)) + except ValidationException: + return JsonResponse( + {'message': 'Project %s not found' % pid}, + status=404) + encoder = get_encoder(project.__class__) + json = encoder.encode(project) + conn.deleteObject(project) + return json + + +class ProjectsView(View): + """Handles GET for /projects/ to list available Projects.""" + + @method_decorator(LoginRequired(useragent='OMERO.webapi')) + @method_decorator(json_response) + def dispatch(self, *args, **kwargs): + """Use dispatch to add decorators to class methods.""" + return super(ProjectsView, self).dispatch(*args, **kwargs) + + def get(self, request, conn=None, **kwargs): + """GET a list of Projects, filtering by various request parameters.""" + try: + page = getIntOrDefault(request, 'page', 1) + limit = getIntOrDefault(request, 'limit', settings.PAGE) + group = getIntOrDefault(request, 'group', -1) + owner = getIntOrDefault(request, 'owner', -1) + childCount = request.GET.get('childCount', False) == 'true' + normalize = request.GET.get('normalize', False) == 'true' + except ValueError as ex: + raise BadRequestError(str(ex)) + + # Get the projects + projects = query_projects(conn, + group=group, + owner=owner, + childCount=childCount, + page=page, + limit=limit, + normalize=normalize) + + return projects + + +class SaveView(View): + """ + This view provides 'Save' functionality for all types of objects. + + POST to create a new Object and PUT to replace existing one. + """ + + @method_decorator(LoginRequired(useragent='OMERO.webapi')) + @method_decorator(json_response) + def dispatch(self, *args, **kwargs): + """Apply decorators for class methods below.""" + return super(SaveView, self).dispatch(*args, **kwargs) + + def get(self, request, *args, **kwargs): + """Return a placeholder error message since GET is not supported.""" + return JsonResponse({"message": + ("POST or PUT only with object json encoded " + "in content body")}, + status=405) + + def put(self, request, conn=None, **kwargs): + """ + PUT handles saving of existing objects. + + Therefore '@id' should be set. + """ + object_json = json.loads(request.body) + if '@id' not in object_json: + raise BadRequestError( + "No '@id' attribute. Use POST to create new objects") + return self._save_object(request, conn, object_json, **kwargs) + + def post(self, request, conn=None, **kwargs): + """ + POST handles saving of NEW objects. + + Therefore '@id' should not be set. + """ + object_json = json.loads(request.body) + if '@id' in object_json: + raise BadRequestError( + "Object has '@id' attribute. Use PUT to update objects") + rsp = self._save_object(request, conn, object_json, **kwargs) + # If no error thrown, return 201 ('Created') + return JsonResponse(rsp, status=201) + + def _save_object(self, request, conn, object_json, **kwargs): + """Here we handle the saving for PUT and POST.""" + # Try to get group from request, OR from details below... + group = getIntOrDefault(request, 'group', None) + decoder = None + if '@type' not in object_json: + raise BadRequestError('Need to specify @type attribute') + objType = object_json['@type'] + decoder = get_decoder(objType) + # If we are passed incomplete object, or decoder couldn't be found... + if decoder is None: + raise BadRequestError('No decoder found for type: %s' % objType) + + # Any marshal errors most likely due to invalid input. status=400 + try: + obj = decoder.decode(object_json) + except Exception: + msg = 'Error in decode of json data by omero_marshal' + raise BadRequestError(msg, traceback.format_exc()) + + if group is None: + try: + # group might be None or unloaded + group = obj.getDetails().group.id.val + except AttributeError: + # Instead of default stack trace, give nicer message: + msg = ("Specify Group in omero:details or " + "query parameters ?group=:id") + raise BadRequestError(msg) + + # If owner was unloaded (E.g. from get() above) or if missing + # ome.model.meta.Experimenter.ldap (not supported by omero_marshal) + # then saveObject() will give ValidationException. + # Therefore we ignore any details for now: + obj.unloadDetails() + + conn.SERVICE_OPTS.setOmeroGroup(group) + obj = conn.getUpdateService().saveAndReturnObject(obj, + conn.SERVICE_OPTS) + encoder = get_encoder(obj.__class__) + return encoder.encode(obj) diff --git a/components/tools/OmeroWeb/omeroweb/urls.py b/components/tools/OmeroWeb/omeroweb/urls.py index 9e0661ef500..8e10516e918 100755 --- a/components/tools/OmeroWeb/omeroweb/urls.py +++ b/components/tools/OmeroWeb/omeroweb/urls.py @@ -100,7 +100,7 @@ def redirect_urlpatterns(): (r'^(?i)url/', include('omeroweb.webredirect.urls')), (r'^(?i)feedback/', include('omeroweb.feedback.urls')), - (r'^(?i)api/', include('omeroweb.webgateway.urls_api')), + (r'^(?i)api/', include('omeroweb.api.urls')), url(r'^index/$', 'omeroweb.webclient.views.custom_index', name="webindex_custom"), diff --git a/components/tools/OmeroWeb/omeroweb/webclient/views.py b/components/tools/OmeroWeb/omeroweb/webclient/views.py index c171ebf8383..7c3c3300af3 100755 --- a/components/tools/OmeroWeb/omeroweb/webclient/views.py +++ b/components/tools/OmeroWeb/omeroweb/webclient/views.py @@ -94,6 +94,7 @@ ScreenPlateLinkI, AnnotationAnnotationLinkI, TagAnnotationI from omero import ApiUsageException, ServerError, CmdError from omero.rtypes import rlong, rlist +from omeroweb.api.views import LoginView import tree @@ -174,7 +175,7 @@ def custom_index(request, conn=None, **kwargs): # views -class WebclientLoginView(webgateway_views.LoginView): +class WebclientLoginView(LoginView): """ Webclient Login - Customises the superclass LoginView for webclient. Also can be used by other Apps to log in to OMERO. Uses diff --git a/components/tools/OmeroWeb/omeroweb/webgateway/views.py b/components/tools/OmeroWeb/omeroweb/webgateway/views.py index ca78024f5a1..bebdf15ed76 100644 --- a/components/tools/OmeroWeb/omeroweb/webgateway/views.py +++ b/components/tools/OmeroWeb/omeroweb/webgateway/views.py @@ -23,25 +23,30 @@ from django.http import HttpResponseRedirect, HttpResponseNotAllowed, Http404 from django.template import loader as template_loader from django.views.decorators.http import require_POST +<<<<<<< 49109b0edadc90f329375f00499e4e7a2267c7fc from django.views.generic import View from django.core.urlresolvers import reverse, NoReverseMatch +======= +from django.core.urlresolvers import reverse +>>>>>>> Moved /api/ code to new omeroweb/api/ app from django.conf import settings from django.template import RequestContext as Context from django.core.servers.basehttp import FileWrapper -from django.middleware import csrf -from django.utils.decorators import method_decorator from omero.rtypes import rlong, unwrap from omero.constants.namespaces import NSBULKANNOTATIONS from omero.util.ROI_utils import pointsStringToXYlist, xyListToBbox from plategrid import PlateGrid from omero_version import build_year from marshal import imageMarshal, shapeMarshal, rgb_int2rgba +<<<<<<< 49109b0edadc90f329375f00499e4e7a2267c7fc from api_query import query_projects from omeroweb.webadmin.forms import LoginForm from omeroweb.decorators import get_client_ip from omeroweb.webadmin.webadmin_utils import upgradeCheck from omero_marshal import get_encoder, get_decoder, OME_SCHEMA_URL from django.contrib.staticfiles.templatetags.staticfiles import static +======= +>>>>>>> Moved /api/ code to new omeroweb/api/ app try: from hashlib import md5 @@ -51,9 +56,8 @@ from cStringIO import StringIO import tempfile -from omero import ApiUsageException, ValidationException +from omero import ApiUsageException from omero.util.decorators import timeit, TimeIt -from omeroweb.connector import Server from omeroweb.http import HttpJavascriptResponse, \ HttpJavascriptResponseServerError @@ -72,12 +76,17 @@ import shutil from omeroweb.decorators import login_required, ConnCleaningHttpResponse +<<<<<<< 49109b0edadc90f329375f00499e4e7a2267c7fc from omeroweb.webgateway.decorators import login_required as api_login_required, \ json_response from omeroweb.connector import Connector from omeroweb.webgateway.util import zip_archived_files, getIntOrDefault from omeroweb.webgateway.api_exceptions import BadRequestError, NotFoundError,\ CreatedObject +======= +from omeroweb.connector import Connector +from omeroweb.webgateway.util import zip_archived_files, getIntOrDefault +>>>>>>> Moved /api/ code to new omeroweb/api/ app cache = CacheBase() logger = logging.getLogger(__name__) @@ -2604,334 +2613,6 @@ def object_table_query(request, objtype, objid, conn=None, **kwargs): return tableData -def build_url(request, name, api_version, **kwargs): - """ - Helper for generating urls within /api json responses - By default we use request.build_absolute_uri() but this - can be configured by setting "omero.web.api.absolute_url" - to a string or empty string, used to prefix relative urls. - Extra **kwargs are passed to reverse() function. - - @param name: Name of the url - @param api_version Version string - """ - kwargs['api_version'] = api_version - url = reverse(name, kwargs=kwargs) - if settings.API_ABSOLUTE_URL is None: - return request.build_absolute_uri(url) - else: - # remove trailing slash - prefix = settings.API_ABSOLUTE_URL.rstrip('/') - return "%s%s" % (prefix, url) - - -@json_response() -def api_versions(request, **kwargs): - """ - Base url of the webgateway json api. - """ - versions = [] - for v in settings.API_VERSIONS: - versions.append({ - 'version': v, - 'base_url': build_url(request, 'api_base', v) - }) - return {'data': versions} - - -@json_response() -def api_base(request, api_version=None, **kwargs): - """ - Base url of the webgateway json api for a specified version. - """ - v = api_version - rv = {'projects_url': build_url(request, 'api_projects', v), - 'token_url': build_url(request, 'api_token', v), - 'servers_url': build_url(request, 'api_servers', v), - 'login_url': build_url(request, 'api_login', v), - 'save_url': build_url(request, 'api_save', v), - 'schema_url': OME_SCHEMA_URL} - return rv - - -@json_response() -def api_token(request, api_version, **kwargs): - """ - Provides CSRF token for current session - """ - token = csrf.get_token(request) - return {'data': token} - - -@json_response() -def api_servers(request, api_version, **kwargs): - """ - Lists the available servers to connect to - """ - servers = [] - for i, obj in enumerate(Server): - s = {'id': i + 1, - 'host': obj.host, - 'port': obj.port - } - if obj.server is not None: - s['server'] = obj.server - servers.append(s) - return {'data': servers} - - -class LoginView(View): - """ - Webgateway Login - Subclassed by WebclientLoginView - """ - - form_class = LoginForm - useragent = 'OMERO.webapi' - - def get(self, request, api_version=None): - """ Simply return a message to say GET not supported """ - return JsonResponse({"message": - ("POST only with username, password, " - "server and csrftoken")}, - status=405) - - def handle_logged_in(self, request, conn, connector): - """ Returns a response for successful login """ - c = conn.getEventContext() - ctx = {} - for a in ['sessionId', 'sessionUuid', 'userId', 'userName', 'groupId', - 'groupName', 'isAdmin', 'eventId', 'eventType', - 'memberOfGroups', 'leaderOfGroups']: - if (hasattr(c, a)): - ctx[a] = getattr(c, a) - return JsonResponse({"success": True, "eventContext": ctx}) - - def handle_not_logged_in(self, request, error=None, form=None): - """ - Returns a response for failed login. - Reason for failure may be due to server 'error' or because - of form validation errors. - - @param request: http request - @param error: Error message - @param form: Instance of Login Form, populated with data - """ - if error is None and form is not None: - # If no error from server, maybe form wasn't valid - formErrors = [] - for field in form: - for e in field.errors: - formErrors.append("%s: %s" % (field.label, e)) - error = " ".join(formErrors) - elif error is None: - # Just in case no error or invalid form is given - error = "Login failed. Reason unknown." - return JsonResponse({"message": error}, status=403) - - def post(self, request, api_version=None): - """ - Here we handle the main login logic, creating a connection to OMERO - and storing that on the request.session OR handling login failures - """ - error = None - form = self.form_class(request.POST.copy()) - if form.is_valid(): - username = form.cleaned_data['username'] - password = form.cleaned_data['password'] - server_id = form.cleaned_data['server'] - is_secure = form.cleaned_data['ssl'] - - connector = Connector(server_id, is_secure) - - # TODO: version check should be done on the low level, see #5983 - compatible = True - if settings.CHECK_VERSION: - compatible = connector.check_version(self.useragent) - if (server_id is not None and username is not None and - password is not None and compatible): - conn = connector.create_connection( - self.useragent, username, password, - userip=get_client_ip(request)) - if conn is not None: - request.session['connector'] = connector - # UpgradeCheck URL should be loaded from the server or - # loaded omero.web.upgrades.url allows to customize web - # only - try: - upgrades_url = settings.UPGRADES_URL - except: - upgrades_url = conn.getUpgradesUrl() - upgradeCheck(url=upgrades_url) - return self.handle_logged_in(request, conn, connector) - # Once here, we are not logged in... - # Need correct error message - if not connector.is_server_up(self.useragent): - error = ("Server is not responding," - " please contact administrator.") - elif not settings.CHECK_VERSION: - error = ("Connection not available, please check your" - " credentials and version compatibility.") - else: - if not compatible: - error = ("Client version does not match server," - " please contact administrator.") - else: - error = ("Connection not available, please check your" - " user name and password.") - return self.handle_not_logged_in(request, error, form) - - -class ProjectView(View): - """ - Handles access to an individual Project to GET or DELETE it - """ - - @method_decorator(api_login_required(useragent='OMERO.webapi')) - @method_decorator(json_response()) - def dispatch(self, *args, **kwargs): - return super(ProjectView, self).dispatch(*args, **kwargs) - - def get(self, request, pid, conn=None, **kwargs): - """ Simply GET a single Project and marshal it or 404 if not found """ - project = conn.getObject("Project", pid) - if project is None: - raise NotFoundError('Project %s not found' % pid) - encoder = get_encoder(project._obj.__class__) - return encoder.encode(project._obj) - - def delete(self, request, pid, conn=None, **kwargs): - """ - Deletes the Project and returns marshal of deleted Project or - returns 404 if not found - """ - try: - project = conn.getQueryService().get('Project', long(pid)) - except ValidationException: - raise NotFoundError('Project %s not found' % pid) - encoder = get_encoder(project.__class__) - json = encoder.encode(project) - conn.deleteObject(project) - return json - - -class ProjectsView(View): - """ - Handles GET for /projects/ to list available Projects - """ - - @method_decorator(api_login_required(useragent='OMERO.webapi')) - @method_decorator(json_response()) - def dispatch(self, *args, **kwargs): - """ Use dispatch to add decorators to class methods """ - return super(ProjectsView, self).dispatch(*args, **kwargs) - - def get(self, request, conn=None, **kwargs): - """ - GET a list of Projects, filtering by various request parameters - """ - try: - page = getIntOrDefault(request, 'page', 1) - limit = getIntOrDefault(request, 'limit', settings.PAGE) - group = getIntOrDefault(request, 'group', -1) - owner = getIntOrDefault(request, 'owner', -1) - childCount = request.GET.get('childCount', False) == 'true' - normalize = request.GET.get('normalize', False) == 'true' - except ValueError as ex: - raise BadRequestError(str(ex)) - - # Get the projects - projects = query_projects(conn, - group=group, - owner=owner, - childCount=childCount, - page=page, - limit=limit, - normalize=normalize) - - return projects - - -class SaveView(View): - """ - This view provides 'Save' functionality for all types of objects - POST to create a new Object and PUT to replace existing one. - """ - - @method_decorator(api_login_required(useragent='OMERO.webapi')) - @method_decorator(json_response()) - def dispatch(self, *args, **kwargs): - """ Apply decorators for class methods below """ - return super(SaveView, self).dispatch(*args, **kwargs) - - def put(self, request, conn=None, **kwargs): - """ - PUT handles saving of existing objects. - Therefore '@id' should be set. - """ - object_json = json.loads(request.body) - if '@id' not in object_json: - raise BadRequestError( - "No '@id' attribute. Use POST to create new objects") - return self._save_object(request, conn, object_json, **kwargs) - - def post(self, request, conn=None, **kwargs): - """ - POST handles saving of NEW objects. - Therefore '@id' should not be set. - """ - object_json = json.loads(request.body) - if '@id' in object_json: - raise BadRequestError( - "Object has '@id' attribute. Use PUT to update objects") - rsp = self._save_object(request, conn, object_json, **kwargs) - # will return 201 ('Created') - raise CreatedObject(rsp) - - def _save_object(self, request, conn, object_json, **kwargs): - """ - Here we handle the saving for PUT and POST - """ - # Try to get group from request, OR from details below... - group = getIntOrDefault(request, 'group', None) - decoder = None - if '@type' not in object_json: - raise BadRequestError('Need to specify @type attribute') - objType = object_json['@type'] - decoder = get_decoder(objType) - # If we are passed incomplete object, or decoder couldn't be found... - if decoder is None: - raise BadRequestError('No decoder found for type: %s' % objType) - - # Any marshal errors most likely due to invalid input. status=400 - try: - obj = decoder.decode(object_json) - except Exception: - msg = 'Error in decode of json data by omero_marshal' - raise BadRequestError(msg, traceback.format_exc()) - - if group is None: - try: - # group might be None or unloaded - group = obj.getDetails().group.id.val - except AttributeError: - # Instead of default stack trace, give nicer message: - msg = ("Specify Group in omero:details or " - "query parameters ?group=:id") - raise BadRequestError(msg) - - # If owner was unloaded (E.g. from get() above) or if missing - # ome.model.meta.Experimenter.ldap (not supported by omero_marshal) - # then saveObject() will give ValidationException. - # Therefore we ignore any details for now: - obj.unloadDetails() - - conn.SERVICE_OPTS.setOmeroGroup(group) - obj = conn.getUpdateService().saveAndReturnObject(obj, - conn.SERVICE_OPTS) - encoder = get_encoder(obj.__class__) - return encoder.encode(obj) - - @login_required() @jsonp def get_image_rdefs_json(request, img_id=None, conn=None, **kwargs): From 18c8544dbdb02ad32ce69d45d9a395ff9913c7ff Mon Sep 17 00:00:00 2001 From: William Moore Date: Thu, 3 Nov 2016 09:59:27 +0000 Subject: [PATCH 2/6] pep8 and other follow-ups from rebase --- .../tools/OmeroWeb/omeroweb/api/decorators.py | 14 ++-- .../tools/OmeroWeb/omeroweb/api/views.py | 81 ++++--------------- .../OmeroWeb/omeroweb/webgateway/views.py | 22 ----- 3 files changed, 22 insertions(+), 95 deletions(-) diff --git a/components/tools/OmeroWeb/omeroweb/api/decorators.py b/components/tools/OmeroWeb/omeroweb/api/decorators.py index 33c0b267233..b5ab3fc0a66 100644 --- a/components/tools/OmeroWeb/omeroweb/api/decorators.py +++ b/components/tools/OmeroWeb/omeroweb/api/decorators.py @@ -42,16 +42,17 @@ def on_not_logged_in(self, request, url, error=None): status=403) -class json_response(object): +class JsonResponseHandler(object): """ Class-based decorator for wrapping Django views methods. + Returns JsonResponse based on dict returned by views methods. Also handles exceptions from views methods, returning JsonResponse with appropriate status values. """ def __init__(self): - """Initialises the decorator.""" + """Initialise the decorator.""" pass def handle_success(self, rv): @@ -70,7 +71,6 @@ def handle_error(self, ex, trace): By default, we format exception or message and return this as a JsonResponse with an appropriate status code. """ - # Default status is 500 'server error' # But we try to handle all 'expected' errors appropriately # TODO: handle omero.ConcurrencyException @@ -95,9 +95,9 @@ def handle_error(self, ex, trace): rsp_json = ex.response return JsonResponse(rsp_json, status=status) - def __call__(ctx, f): + def __call__(self, f): """ - Returns the decorator. + Return the decorator. The decorator calls the wrapped function and handles success or exception, returning a @@ -107,8 +107,8 @@ def wrapped(request, *args, **kwargs): logger.debug('json_response') try: rv = f(request, *args, **kwargs) - return ctx.handle_success(rv) + return self.handle_success(rv) except Exception, ex: trace = traceback.format_exc() - return ctx.handle_error(ex, trace) + return self.handle_error(ex, trace) return update_wrapper(wrapped, f) diff --git a/components/tools/OmeroWeb/omeroweb/api/views.py b/components/tools/OmeroWeb/omeroweb/api/views.py index 38476038982..85e49405dff 100644 --- a/components/tools/OmeroWeb/omeroweb/api/views.py +++ b/components/tools/OmeroWeb/omeroweb/api/views.py @@ -24,14 +24,11 @@ from django.utils.decorators import method_decorator from django.core.urlresolvers import reverse from django.conf import settings -from django.http import HttpResponse, JsonResponse +from django.http import JsonResponse -from functools import wraps -import logging import traceback import json -import omero import omero.clients from api_query import query_projects from omeroweb.webadmin.forms import LoginForm @@ -41,49 +38,12 @@ from omero_marshal import get_encoder, get_decoder, OME_SCHEMA_URL from omero import ValidationException from omeroweb.connector import Server -from omeroweb.api.api_exceptions import BadRequestError -from omeroweb.api.decorators import LoginRequired +from omeroweb.api.api_exceptions import BadRequestError, NotFoundError, \ + CreatedObject +from omeroweb.api.decorators import LoginRequired, JsonResponseHandler from omeroweb.webgateway.util import getIntOrDefault -logger = logging.getLogger(__name__) - -def json_response(f): - """ - Decorator for wrapping in JsonResponse and error handling. - - @param f: The function to wrap - @return: The wrapped function, which will return json - """ - @wraps(f) - def wrap(request, *args, **kwargs): - logger.debug('json_response') - try: - rv = f(request, *args, **kwargs) - if isinstance(rv, HttpResponse): - return rv - return JsonResponse(rv) - except Exception, ex: - # Default status is 500 'server error' - # But we try to handle all 'expected' errors appropriately - # TODO: handle omero.ConcurrencyException - status = 500 - trace = traceback.format_exc() - if isinstance(ex, BadRequestError): - status = 400 - trace = ex.stacktrace # Might be None - elif isinstance(ex, omero.SecurityViolation): - status = 403 - elif isinstance(ex, omero.ApiUsageException): - status = 400 - logger.debug(trace) - rsp_json = {"message": str(ex)} - if trace is not None: - rsp_json["stacktrace"] = trace - return JsonResponse(rsp_json, status=status) - wrap.func_name = f.func_name - return wrap - def build_url(request, name, api_version, **kwargs): """ Helper for generating urls within /api json responses. @@ -106,7 +66,7 @@ def build_url(request, name, api_version, **kwargs): return "%s%s" % (prefix, url) -@json_response +@JsonResponseHandler() def api_versions(request, **kwargs): """Base url of the webgateway json api.""" versions = [] @@ -118,7 +78,7 @@ def api_versions(request, **kwargs): return {'data': versions} -@json_response +@JsonResponseHandler() def api_base(request, api_version=None, **kwargs): """Base url of the webgateway json api for a specified version.""" v = api_version @@ -131,14 +91,14 @@ def api_base(request, api_version=None, **kwargs): return rv -@json_response +@JsonResponseHandler() def api_token(request, api_version, **kwargs): """Provide CSRF token for current session.""" token = csrf.get_token(request) return {'data': token} -@json_response +@JsonResponseHandler() def api_servers(request, api_version, **kwargs): """List the available servers to connect to.""" servers = [] @@ -258,7 +218,7 @@ class ProjectView(View): """Handle access to an individual Project to GET or DELETE it.""" @method_decorator(LoginRequired(useragent='OMERO.webapi')) - @method_decorator(json_response) + @method_decorator(JsonResponseHandler()) def dispatch(self, *args, **kwargs): """Wrap other methods to add decorators.""" return super(ProjectView, self).dispatch(*args, **kwargs) @@ -267,9 +227,7 @@ def get(self, request, pid, conn=None, **kwargs): """Simply GET a single Project and marshal it or 404 if not found.""" project = conn.getObject("Project", pid) if project is None: - return JsonResponse( - {'message': 'Project %s not found' % pid}, - status=404) + raise NotFoundError('Project %s not found' % pid) encoder = get_encoder(project._obj.__class__) return encoder.encode(project._obj) @@ -282,9 +240,7 @@ def delete(self, request, pid, conn=None, **kwargs): try: project = conn.getQueryService().get('Project', long(pid)) except ValidationException: - return JsonResponse( - {'message': 'Project %s not found' % pid}, - status=404) + raise NotFoundError('Project %s not found' % pid) encoder = get_encoder(project.__class__) json = encoder.encode(project) conn.deleteObject(project) @@ -295,7 +251,7 @@ class ProjectsView(View): """Handles GET for /projects/ to list available Projects.""" @method_decorator(LoginRequired(useragent='OMERO.webapi')) - @method_decorator(json_response) + @method_decorator(JsonResponseHandler()) def dispatch(self, *args, **kwargs): """Use dispatch to add decorators to class methods.""" return super(ProjectsView, self).dispatch(*args, **kwargs) @@ -332,18 +288,11 @@ class SaveView(View): """ @method_decorator(LoginRequired(useragent='OMERO.webapi')) - @method_decorator(json_response) + @method_decorator(JsonResponseHandler()) def dispatch(self, *args, **kwargs): """Apply decorators for class methods below.""" return super(SaveView, self).dispatch(*args, **kwargs) - def get(self, request, *args, **kwargs): - """Return a placeholder error message since GET is not supported.""" - return JsonResponse({"message": - ("POST or PUT only with object json encoded " - "in content body")}, - status=405) - def put(self, request, conn=None, **kwargs): """ PUT handles saving of existing objects. @@ -367,8 +316,8 @@ def post(self, request, conn=None, **kwargs): raise BadRequestError( "Object has '@id' attribute. Use PUT to update objects") rsp = self._save_object(request, conn, object_json, **kwargs) - # If no error thrown, return 201 ('Created') - return JsonResponse(rsp, status=201) + # will return 201 ('Created') + raise CreatedObject(rsp) def _save_object(self, request, conn, object_json, **kwargs): """Here we handle the saving for PUT and POST.""" diff --git a/components/tools/OmeroWeb/omeroweb/webgateway/views.py b/components/tools/OmeroWeb/omeroweb/webgateway/views.py index bebdf15ed76..c076fe440eb 100644 --- a/components/tools/OmeroWeb/omeroweb/webgateway/views.py +++ b/components/tools/OmeroWeb/omeroweb/webgateway/views.py @@ -23,12 +23,7 @@ from django.http import HttpResponseRedirect, HttpResponseNotAllowed, Http404 from django.template import loader as template_loader from django.views.decorators.http import require_POST -<<<<<<< 49109b0edadc90f329375f00499e4e7a2267c7fc -from django.views.generic import View from django.core.urlresolvers import reverse, NoReverseMatch -======= -from django.core.urlresolvers import reverse ->>>>>>> Moved /api/ code to new omeroweb/api/ app from django.conf import settings from django.template import RequestContext as Context from django.core.servers.basehttp import FileWrapper @@ -38,15 +33,7 @@ from plategrid import PlateGrid from omero_version import build_year from marshal import imageMarshal, shapeMarshal, rgb_int2rgba -<<<<<<< 49109b0edadc90f329375f00499e4e7a2267c7fc -from api_query import query_projects -from omeroweb.webadmin.forms import LoginForm -from omeroweb.decorators import get_client_ip -from omeroweb.webadmin.webadmin_utils import upgradeCheck -from omero_marshal import get_encoder, get_decoder, OME_SCHEMA_URL from django.contrib.staticfiles.templatetags.staticfiles import static -======= ->>>>>>> Moved /api/ code to new omeroweb/api/ app try: from hashlib import md5 @@ -76,17 +63,8 @@ import shutil from omeroweb.decorators import login_required, ConnCleaningHttpResponse -<<<<<<< 49109b0edadc90f329375f00499e4e7a2267c7fc -from omeroweb.webgateway.decorators import login_required as api_login_required, \ - json_response from omeroweb.connector import Connector from omeroweb.webgateway.util import zip_archived_files, getIntOrDefault -from omeroweb.webgateway.api_exceptions import BadRequestError, NotFoundError,\ - CreatedObject -======= -from omeroweb.connector import Connector -from omeroweb.webgateway.util import zip_archived_files, getIntOrDefault ->>>>>>> Moved /api/ code to new omeroweb/api/ app cache = CacheBase() logger = logging.getLogger(__name__) From d4bca4721c0b3b90019f3a59323f0b939a1338bc Mon Sep 17 00:00:00 2001 From: William Moore Date: Thu, 3 Nov 2016 10:25:59 +0000 Subject: [PATCH 3/6] flake8 and pep8 fixes --- .../OmeroWeb/omeroweb/api/api_exceptions.py | 20 ++++++++++++++----- .../tools/OmeroWeb/omeroweb/api/views.py | 1 - 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/components/tools/OmeroWeb/omeroweb/api/api_exceptions.py b/components/tools/OmeroWeb/omeroweb/api/api_exceptions.py index 28c061bbaad..bfcfda7380c 100644 --- a/components/tools/OmeroWeb/omeroweb/api/api_exceptions.py +++ b/components/tools/OmeroWeb/omeroweb/api/api_exceptions.py @@ -19,39 +19,49 @@ """Exceptions used by the api/views methods.""" + class BadRequestError(Exception): """ An exception that will result in a response status of 400. Due to invalid client input """ + status = 400 def __init__(self, message, stacktrace=None): - """Override init to handle message and stacktrace.""" + """Override init to handle message and stacktrace.""" super(BadRequestError, self).__init__(message) self.stacktrace = stacktrace class NotFoundError(Exception): """ - An exception that will result in a response status of 404 - due to objects not being found + An exception that will result in a response status of 404. + + Raised due to objects not being found. """ + status = 404 def __init__(self, message, stacktrace=None): + """Override init to handle message and stacktrace.""" super(NotFoundError, self).__init__(message) self.stacktrace = stacktrace class CreatedObject(Exception): """ - An exception that is thrown when new object created - that returns a status of 201 + An exception that is thrown when new object created. + + This is not really an error but indicates to the handler + that a JsonResponse with status 201 should be returned. + The dict content is passed in as 'response'. """ + status = 201 def __init__(self, response): + """Override init to include response dict.""" super(CreatedObject, self).__init__(response) self.response = response diff --git a/components/tools/OmeroWeb/omeroweb/api/views.py b/components/tools/OmeroWeb/omeroweb/api/views.py index 85e49405dff..6bf68586637 100644 --- a/components/tools/OmeroWeb/omeroweb/api/views.py +++ b/components/tools/OmeroWeb/omeroweb/api/views.py @@ -29,7 +29,6 @@ import traceback import json -import omero.clients from api_query import query_projects from omeroweb.webadmin.forms import LoginForm from omeroweb.decorators import get_client_ip From 2e6863d7a2faf7c4ef1eb714d1e669e245854a8a Mon Sep 17 00:00:00 2001 From: William Moore Date: Tue, 8 Nov 2016 10:48:47 +0000 Subject: [PATCH 4/6] Add header to api/__init__.py --- .../tools/OmeroWeb/omeroweb/api/__init__.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/components/tools/OmeroWeb/omeroweb/api/__init__.py b/components/tools/OmeroWeb/omeroweb/api/__init__.py index 56c96010919..e8c86051f20 100644 --- a/components/tools/OmeroWeb/omeroweb/api/__init__.py +++ b/components/tools/OmeroWeb/omeroweb/api/__init__.py @@ -1,2 +1,20 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (C) 2016 University of Dundee & Open Microscopy Environment. +# All rights reserved. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . """api module provides JSON OMERO API.""" From a9aa0d71d392224dcbefd3bc97424bc6e7426660 Mon Sep 17 00:00:00 2001 From: William Moore Date: Tue, 8 Nov 2016 10:58:43 +0000 Subject: [PATCH 5/6] Move LoginView back to webgateway/views.py --- .../tools/OmeroWeb/omeroweb/api/urls.py | 2 +- .../tools/OmeroWeb/omeroweb/api/views.py | 106 ------------------ .../OmeroWeb/omeroweb/webclient/views.py | 2 +- .../OmeroWeb/omeroweb/webgateway/views.py | 105 +++++++++++++++++ 4 files changed, 107 insertions(+), 108 deletions(-) diff --git a/components/tools/OmeroWeb/omeroweb/api/urls.py b/components/tools/OmeroWeb/omeroweb/api/urls.py index f3e6117a611..86b3c634078 100644 --- a/components/tools/OmeroWeb/omeroweb/api/urls.py +++ b/components/tools/OmeroWeb/omeroweb/api/urls.py @@ -21,7 +21,7 @@ from django.conf.urls import url, patterns from omeroweb.api import views -from omeroweb.api.views import LoginView +from omeroweb.webgateway.views import LoginView from django.conf import settings import re diff --git a/components/tools/OmeroWeb/omeroweb/api/views.py b/components/tools/OmeroWeb/omeroweb/api/views.py index 6bf68586637..eac2b5c5ccd 100644 --- a/components/tools/OmeroWeb/omeroweb/api/views.py +++ b/components/tools/OmeroWeb/omeroweb/api/views.py @@ -24,16 +24,11 @@ from django.utils.decorators import method_decorator from django.core.urlresolvers import reverse from django.conf import settings -from django.http import JsonResponse import traceback import json from api_query import query_projects -from omeroweb.webadmin.forms import LoginForm -from omeroweb.decorators import get_client_ip -from omeroweb.connector import Connector -from omeroweb.webadmin.webadmin_utils import upgradeCheck from omero_marshal import get_encoder, get_decoder, OME_SCHEMA_URL from omero import ValidationException from omeroweb.connector import Server @@ -112,107 +107,6 @@ def api_servers(request, api_version, **kwargs): return {'data': servers} -class LoginView(View): - """Webgateway Login - Subclassed by WebclientLoginView.""" - - form_class = LoginForm - useragent = 'OMERO.webapi' - - def get(self, request, api_version=None): - """Simply return a message to say GET not supported.""" - return JsonResponse({"message": - ("POST only with username, password, " - "server and csrftoken")}, - status=405) - - def handle_logged_in(self, request, conn, connector): - """Return a response for successful login.""" - c = conn.getEventContext() - ctx = {} - for a in ['sessionId', 'sessionUuid', 'userId', 'userName', 'groupId', - 'groupName', 'isAdmin', 'eventId', 'eventType', - 'memberOfGroups', 'leaderOfGroups']: - if (hasattr(c, a)): - ctx[a] = getattr(c, a) - return JsonResponse({"success": True, "eventContext": ctx}) - - def handle_not_logged_in(self, request, error=None, form=None): - """ - Return a response for failed login. - - Reason for failure may be due to server 'error' or because - of form validation errors. - - @param request: http request - @param error: Error message - @param form: Instance of Login Form, populated with data - """ - if error is None and form is not None: - # If no error from server, maybe form wasn't valid - formErrors = [] - for field in form: - for e in field.errors: - formErrors.append("%s: %s" % (field.label, e)) - error = " ".join(formErrors) - elif error is None: - # Just in case no error or invalid form is given - error = "Login failed. Reason unknown." - return JsonResponse({"message": error}, status=403) - - def post(self, request, api_version=None): - """ - Here we handle the main login logic, creating a connection to OMERO. - - and store that on the request.session OR handling login failures - """ - error = None - form = self.form_class(request.POST.copy()) - if form.is_valid(): - username = form.cleaned_data['username'] - password = form.cleaned_data['password'] - server_id = form.cleaned_data['server'] - is_secure = form.cleaned_data['ssl'] - - connector = Connector(server_id, is_secure) - - # TODO: version check should be done on the low level, see #5983 - compatible = True - if settings.CHECK_VERSION: - compatible = connector.check_version(self.useragent) - if (server_id is not None and username is not None and - password is not None and compatible): - conn = connector.create_connection( - self.useragent, username, password, - userip=get_client_ip(request)) - if conn is not None: - request.session['connector'] = connector - # UpgradeCheck URL should be loaded from the server or - # loaded omero.web.upgrades.url allows to customize web - # only - try: - upgrades_url = settings.UPGRADES_URL - except: - upgrades_url = conn.getUpgradesUrl() - upgradeCheck(url=upgrades_url) - return self.handle_logged_in(request, conn, connector) - # Once here, we are not logged in... - # Need correct error message - if not connector.is_server_up(self.useragent): - error = ("Server is not responding," - " please contact administrator.") - elif not settings.CHECK_VERSION: - error = ("Connection not available, please check your" - " credentials and version compatibility.") - else: - if not compatible: - error = ("Client version does not match server," - " please contact administrator.") - else: - error = ("Connection not available, please check your" - " user name and password.") - return self.handle_not_logged_in(request, error, form) - - class ProjectView(View): """Handle access to an individual Project to GET or DELETE it.""" diff --git a/components/tools/OmeroWeb/omeroweb/webclient/views.py b/components/tools/OmeroWeb/omeroweb/webclient/views.py index 7c3c3300af3..59481fd9d76 100755 --- a/components/tools/OmeroWeb/omeroweb/webclient/views.py +++ b/components/tools/OmeroWeb/omeroweb/webclient/views.py @@ -94,7 +94,7 @@ ScreenPlateLinkI, AnnotationAnnotationLinkI, TagAnnotationI from omero import ApiUsageException, ServerError, CmdError from omero.rtypes import rlong, rlist -from omeroweb.api.views import LoginView +from omeroweb.webgateway.views import LoginView import tree diff --git a/components/tools/OmeroWeb/omeroweb/webgateway/views.py b/components/tools/OmeroWeb/omeroweb/webgateway/views.py index c076fe440eb..1a885e624c2 100644 --- a/components/tools/OmeroWeb/omeroweb/webgateway/views.py +++ b/components/tools/OmeroWeb/omeroweb/webgateway/views.py @@ -34,6 +34,10 @@ from omero_version import build_year from marshal import imageMarshal, shapeMarshal, rgb_int2rgba from django.contrib.staticfiles.templatetags.staticfiles import static +from django.views.generic import View +from omeroweb.webadmin.forms import LoginForm +from omeroweb.decorators import get_client_ip +from omeroweb.webadmin.webadmin_utils import upgradeCheck try: from hashlib import md5 @@ -2591,6 +2595,107 @@ def object_table_query(request, objtype, objid, conn=None, **kwargs): return tableData +class LoginView(View): + """Webgateway Login - Subclassed by WebclientLoginView.""" + + form_class = LoginForm + useragent = 'OMERO.webapi' + + def get(self, request, api_version=None): + """Simply return a message to say GET not supported.""" + return JsonResponse({"message": + ("POST only with username, password, " + "server and csrftoken")}, + status=405) + + def handle_logged_in(self, request, conn, connector): + """Return a response for successful login.""" + c = conn.getEventContext() + ctx = {} + for a in ['sessionId', 'sessionUuid', 'userId', 'userName', 'groupId', + 'groupName', 'isAdmin', 'eventId', 'eventType', + 'memberOfGroups', 'leaderOfGroups']: + if (hasattr(c, a)): + ctx[a] = getattr(c, a) + return JsonResponse({"success": True, "eventContext": ctx}) + + def handle_not_logged_in(self, request, error=None, form=None): + """ + Return a response for failed login. + + Reason for failure may be due to server 'error' or because + of form validation errors. + + @param request: http request + @param error: Error message + @param form: Instance of Login Form, populated with data + """ + if error is None and form is not None: + # If no error from server, maybe form wasn't valid + formErrors = [] + for field in form: + for e in field.errors: + formErrors.append("%s: %s" % (field.label, e)) + error = " ".join(formErrors) + elif error is None: + # Just in case no error or invalid form is given + error = "Login failed. Reason unknown." + return JsonResponse({"message": error}, status=403) + + def post(self, request, api_version=None): + """ + Here we handle the main login logic, creating a connection to OMERO. + + and store that on the request.session OR handling login failures + """ + error = None + form = self.form_class(request.POST.copy()) + if form.is_valid(): + username = form.cleaned_data['username'] + password = form.cleaned_data['password'] + server_id = form.cleaned_data['server'] + is_secure = form.cleaned_data['ssl'] + + connector = Connector(server_id, is_secure) + + # TODO: version check should be done on the low level, see #5983 + compatible = True + if settings.CHECK_VERSION: + compatible = connector.check_version(self.useragent) + if (server_id is not None and username is not None and + password is not None and compatible): + conn = connector.create_connection( + self.useragent, username, password, + userip=get_client_ip(request)) + if conn is not None: + request.session['connector'] = connector + # UpgradeCheck URL should be loaded from the server or + # loaded omero.web.upgrades.url allows to customize web + # only + try: + upgrades_url = settings.UPGRADES_URL + except: + upgrades_url = conn.getUpgradesUrl() + upgradeCheck(url=upgrades_url) + return self.handle_logged_in(request, conn, connector) + # Once here, we are not logged in... + # Need correct error message + if not connector.is_server_up(self.useragent): + error = ("Server is not responding," + " please contact administrator.") + elif not settings.CHECK_VERSION: + error = ("Connection not available, please check your" + " credentials and version compatibility.") + else: + if not compatible: + error = ("Client version does not match server," + " please contact administrator.") + else: + error = ("Connection not available, please check your" + " user name and password.") + return self.handle_not_logged_in(request, error, form) + + @login_required() @jsonp def get_image_rdefs_json(request, img_id=None, conn=None, **kwargs): From 0eb6967f38e0d2978d4330af0a8e805bff91ace4 Mon Sep 17 00:00:00 2001 From: William Moore Date: Tue, 8 Nov 2016 11:01:12 +0000 Subject: [PATCH 6/6] Rename decorators e.g. LoginRequired to login_required --- .../tools/OmeroWeb/omeroweb/api/decorators.py | 4 ++-- .../tools/OmeroWeb/omeroweb/api/views.py | 22 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/components/tools/OmeroWeb/omeroweb/api/decorators.py b/components/tools/OmeroWeb/omeroweb/api/decorators.py index b5ab3fc0a66..59718240112 100644 --- a/components/tools/OmeroWeb/omeroweb/api/decorators.py +++ b/components/tools/OmeroWeb/omeroweb/api/decorators.py @@ -33,7 +33,7 @@ logger = logging.getLogger(__name__) -class LoginRequired(omeroweb.decorators.login_required): +class login_required(omeroweb.decorators.login_required): """webgateway specific extension of the login_required() decorator.""" def on_not_logged_in(self, request, url, error=None): @@ -42,7 +42,7 @@ def on_not_logged_in(self, request, url, error=None): status=403) -class JsonResponseHandler(object): +class json_response(object): """ Class-based decorator for wrapping Django views methods. diff --git a/components/tools/OmeroWeb/omeroweb/api/views.py b/components/tools/OmeroWeb/omeroweb/api/views.py index eac2b5c5ccd..14a3c63fef1 100644 --- a/components/tools/OmeroWeb/omeroweb/api/views.py +++ b/components/tools/OmeroWeb/omeroweb/api/views.py @@ -34,7 +34,7 @@ from omeroweb.connector import Server from omeroweb.api.api_exceptions import BadRequestError, NotFoundError, \ CreatedObject -from omeroweb.api.decorators import LoginRequired, JsonResponseHandler +from omeroweb.api.decorators import login_required, json_response from omeroweb.webgateway.util import getIntOrDefault @@ -60,7 +60,7 @@ def build_url(request, name, api_version, **kwargs): return "%s%s" % (prefix, url) -@JsonResponseHandler() +@json_response() def api_versions(request, **kwargs): """Base url of the webgateway json api.""" versions = [] @@ -72,7 +72,7 @@ def api_versions(request, **kwargs): return {'data': versions} -@JsonResponseHandler() +@json_response() def api_base(request, api_version=None, **kwargs): """Base url of the webgateway json api for a specified version.""" v = api_version @@ -85,14 +85,14 @@ def api_base(request, api_version=None, **kwargs): return rv -@JsonResponseHandler() +@json_response() def api_token(request, api_version, **kwargs): """Provide CSRF token for current session.""" token = csrf.get_token(request) return {'data': token} -@JsonResponseHandler() +@json_response() def api_servers(request, api_version, **kwargs): """List the available servers to connect to.""" servers = [] @@ -110,8 +110,8 @@ def api_servers(request, api_version, **kwargs): class ProjectView(View): """Handle access to an individual Project to GET or DELETE it.""" - @method_decorator(LoginRequired(useragent='OMERO.webapi')) - @method_decorator(JsonResponseHandler()) + @method_decorator(login_required(useragent='OMERO.webapi')) + @method_decorator(json_response()) def dispatch(self, *args, **kwargs): """Wrap other methods to add decorators.""" return super(ProjectView, self).dispatch(*args, **kwargs) @@ -143,8 +143,8 @@ def delete(self, request, pid, conn=None, **kwargs): class ProjectsView(View): """Handles GET for /projects/ to list available Projects.""" - @method_decorator(LoginRequired(useragent='OMERO.webapi')) - @method_decorator(JsonResponseHandler()) + @method_decorator(login_required(useragent='OMERO.webapi')) + @method_decorator(json_response()) def dispatch(self, *args, **kwargs): """Use dispatch to add decorators to class methods.""" return super(ProjectsView, self).dispatch(*args, **kwargs) @@ -180,8 +180,8 @@ class SaveView(View): POST to create a new Object and PUT to replace existing one. """ - @method_decorator(LoginRequired(useragent='OMERO.webapi')) - @method_decorator(JsonResponseHandler()) + @method_decorator(login_required(useragent='OMERO.webapi')) + @method_decorator(json_response()) def dispatch(self, *args, **kwargs): """Apply decorators for class methods below.""" return super(SaveView, self).dispatch(*args, **kwargs)