diff --git a/components/tools/OmeroWeb/omeroweb/api/__init__.py b/components/tools/OmeroWeb/omeroweb/api/__init__.py new file mode 100644 index 00000000000..e8c86051f20 --- /dev/null +++ b/components/tools/OmeroWeb/omeroweb/api/__init__.py @@ -0,0 +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.""" diff --git a/components/tools/OmeroWeb/omeroweb/webgateway/api_exceptions.py b/components/tools/OmeroWeb/omeroweb/api/api_exceptions.py similarity index 68% rename from components/tools/OmeroWeb/omeroweb/webgateway/api_exceptions.py rename to components/tools/OmeroWeb/omeroweb/api/api_exceptions.py index ad24594249f..bfcfda7380c 100644 --- a/components/tools/OmeroWeb/omeroweb/webgateway/api_exceptions.py +++ b/components/tools/OmeroWeb/omeroweb/api/api_exceptions.py @@ -17,38 +17,51 @@ # 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 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/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 91% rename from components/tools/OmeroWeb/omeroweb/webgateway/decorators.py rename to components/tools/OmeroWeb/omeroweb/api/decorators.py index c3e14d6fb26..59718240112 100644 --- a/components/tools/OmeroWeb/omeroweb/webgateway/decorators.py +++ b/components/tools/OmeroWeb/omeroweb/api/decorators.py @@ -34,14 +34,10 @@ class login_required(omeroweb.decorators.login_required): - """ - webgateway specific extension of the OMERO.web login_required() decorator. - """ + """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) @@ -49,13 +45,14 @@ def on_not_logged_in(self, request, url, error=None): class json_response(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): @@ -74,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 @@ -99,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 @@ -111,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/webgateway/urls_api.py b/components/tools/OmeroWeb/omeroweb/api/urls.py similarity index 96% rename from components/tools/OmeroWeb/omeroweb/webgateway/urls_api.py rename to components/tools/OmeroWeb/omeroweb/api/urls.py index 6d8c5753f1e..86b3c634078 100644 --- a/components/tools/OmeroWeb/omeroweb/webgateway/urls_api.py +++ b/components/tools/OmeroWeb/omeroweb/api/urls.py @@ -17,10 +17,10 @@ # 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.api import views 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 new file mode 100644 index 00000000000..14a3c63fef1 --- /dev/null +++ b/components/tools/OmeroWeb/omeroweb/api/views.py @@ -0,0 +1,255 @@ +#!/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 + +import traceback +import json + +from api_query import query_projects +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, NotFoundError, \ + CreatedObject +from omeroweb.api.decorators import login_required, json_response +from omeroweb.webgateway.util import getIntOrDefault + + +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 ProjectView(View): + """Handle access to an individual Project to GET or DELETE it.""" + + @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) + + 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): + """ + Delete the Project and return marshal of deleted Project. + + Return 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(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(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) 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..59481fd9d76 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.webgateway.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..1a885e624c2 100644 --- a/components/tools/OmeroWeb/omeroweb/webgateway/views.py +++ b/components/tools/OmeroWeb/omeroweb/webgateway/views.py @@ -23,25 +23,21 @@ from django.http import HttpResponseRedirect, HttpResponseNotAllowed, Http404 from django.template import loader as template_loader from django.views.decorators.http import require_POST -from django.views.generic import View from django.core.urlresolvers import reverse, NoReverseMatch 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 -from api_query import query_projects +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 -from omero_marshal import get_encoder, get_decoder, OME_SCHEMA_URL -from django.contrib.staticfiles.templatetags.staticfiles import static try: from hashlib import md5 @@ -51,9 +47,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 +67,8 @@ import shutil from omeroweb.decorators import login_required, ConnCleaningHttpResponse -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 cache = CacheBase() logger = logging.getLogger(__name__) @@ -2604,99 +2595,21 @@ 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 - """ + """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 """ + """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 """ + """Return a response for successful login.""" c = conn.getEventContext() ctx = {} for a in ['sessionId', 'sessionUuid', 'userId', 'userName', 'groupId', @@ -2708,7 +2621,8 @@ def handle_logged_in(self, request, conn, connector): def handle_not_logged_in(self, request, error=None, form=None): """ - Returns a response for failed login. + Return a response for failed login. + Reason for failure may be due to server 'error' or because of form validation errors. @@ -2730,8 +2644,9 @@ def handle_not_logged_in(self, request, error=None, form=None): 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 + 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()) @@ -2781,157 +2696,6 @@ def post(self, request, api_version=None): 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):