diff --git a/components/tools/OmeroWeb/omeroweb/decorators.py b/components/tools/OmeroWeb/omeroweb/decorators.py
index 3e4b1a5675a..7c7d202fc75 100644
--- a/components/tools/OmeroWeb/omeroweb/decorators.py
+++ b/components/tools/OmeroWeb/omeroweb/decorators.py
@@ -25,7 +25,8 @@
import logging
import traceback
-from django.http import Http404, HttpResponse, HttpResponseRedirect
+from django.http import Http404, HttpResponse, HttpResponseRedirect, \
+ JsonResponse
from django.http import HttpResponseForbidden, StreamingHttpResponse
from django.conf import settings
@@ -36,7 +37,6 @@
from django.template import RequestContext
from django.core.cache import cache
-from omeroweb.http import HttpJsonResponse
from omeroweb.utils import reverse_with_params
from omeroweb.connector import Connector
from omero.gateway.utils import propertiesToDict
@@ -558,7 +558,9 @@ def wrapper(request, *args, **kwargs):
# allows us to return the dict as json (NB: BlitzGateway objects
# don't serialize)
if template is None or template == 'json':
- return HttpJsonResponse(context)
+ # We still need to support non-dict data:
+ safe = type(context) is dict
+ return JsonResponse(context, safe=safe)
else:
# allow additional processing of context dict
ctx.prepare_context(request, context, *args, **kwargs)
diff --git a/components/tools/OmeroWeb/omeroweb/feedback/templates/403_csrf.html b/components/tools/OmeroWeb/omeroweb/feedback/templates/403_csrf.html
deleted file mode 100644
index f44556a2304..00000000000
--- a/components/tools/OmeroWeb/omeroweb/feedback/templates/403_csrf.html
+++ /dev/null
@@ -1,8 +0,0 @@
-{% extends "base_error.html" %}
-{% load i18n %}
-
-{% block content %}
-
{% trans "Forbidden" %} (403)
-
{% trans "CSRF Error. You don't have permission to access this page on this server." %}
-
-{% endblock %}
\ No newline at end of file
diff --git a/components/tools/OmeroWeb/omeroweb/feedback/views.py b/components/tools/OmeroWeb/omeroweb/feedback/views.py
index b8728444480..795fbd2f597 100644
--- a/components/tools/OmeroWeb/omeroweb/feedback/views.py
+++ b/components/tools/OmeroWeb/omeroweb/feedback/views.py
@@ -35,9 +35,8 @@
from django.conf import settings
from django.template import loader as template_loader
-from django.http import HttpResponse, HttpResponseRedirect
+from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.http import HttpResponseServerError, HttpResponseNotFound
-from django.http import HttpResponseForbidden
from django.template import RequestContext
from django.views.defaults import page_not_found
from django.core.urlresolvers import reverse
@@ -131,12 +130,16 @@ def send_comment(request):
##############################################################################
# handlers
-
def csrf_failure(request, reason=""):
- logger.warn('csrf_failure: Forbidden')
- t = template_loader.get_template("403_csrf.html")
- c = RequestContext(request, {})
- return HttpResponseForbidden(t.render(c))
+ """
+ Always return Json response
+ since this is accepted by browser and API users
+ """
+ error = ("CSRF Error. You need to include valid CSRF tokens for any"
+ " POST, PUT, PATCH or DELETE operations."
+ " You have to include CSRF token in the POST data or"
+ " add the token to the HTTP header.")
+ return JsonResponse({"message": error}, status=403)
def handler500(request):
diff --git a/components/tools/OmeroWeb/omeroweb/http.py b/components/tools/OmeroWeb/omeroweb/http.py
index 92d386fa377..30b28fc0a95 100644
--- a/components/tools/OmeroWeb/omeroweb/http.py
+++ b/components/tools/OmeroWeb/omeroweb/http.py
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
#
#
-# Copyright (C) 2011-2014 University of Dundee & Open Microscopy Environment.
+# Copyright (C) 2011-2016 University of Dundee & Open Microscopy Environment.
# All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
@@ -19,14 +19,13 @@
# along with this program. If not, see .
#
-import json
-
from django.http import HttpResponse, HttpResponseServerError
class HttpJavascriptResponse(HttpResponse):
def __init__(self, content):
- HttpResponse.__init__(self, content, content_type="text/javascript")
+ HttpResponse.__init__(self, content,
+ content_type="application/javascript")
class HttpJavascriptResponseServerError(HttpResponseServerError):
@@ -35,12 +34,6 @@ def __init__(self, content):
self, content, content_type="text/javascript")
-class HttpJsonResponse(HttpResponse):
- def __init__(self, content):
- HttpResponse.__init__(
- self, json.dumps(content), content_type="application/json")
-
-
class HttpJPEGResponse(HttpResponse):
def __init__(self, content):
HttpResponse.__init__(self, content, content_type="image/jpeg")
diff --git a/components/tools/OmeroWeb/omeroweb/settings.py b/components/tools/OmeroWeb/omeroweb/settings.py
index 2fd550da489..017da8a9f4b 100644
--- a/components/tools/OmeroWeb/omeroweb/settings.py
+++ b/components/tools/OmeroWeb/omeroweb/settings.py
@@ -472,7 +472,15 @@ def leave_none_unset_int(s):
("Workers silent for more than this many seconds are killed "
"and restarted. Check Gunicorn Documentation "
"http://docs.gunicorn.org/en/stable/settings.html#timeout")],
-
+ "omero.web.api.absolute_url":
+ ["API_ABSOLUTE_URL",
+ None,
+ str_slash,
+ ("URL to use for generating urls within API json responses. "
+ "By default this is None, and we use Django's "
+ "request.build_absolute_uri() to generate absolute urls "
+ "based on each request. If set to a string or empty string, "
+ "this will be used as prefix to relative urls.")],
# Public user
"omero.web.public.enabled":
@@ -1155,6 +1163,12 @@ def report_settings(module):
# FEEDBACK_APP: 6 = OMERO.web
FEEDBACK_APP = 6
+# For any given release of api, we may support
+# one or more versions of the api.
+# E.g. /api/v1.0/
+# TODO - need to decide how this is configured, strategy for extending etc.
+API_VERSIONS = ('0.1',)
+
# IGNORABLE_404_STARTS:
# Default: ('/cgi-bin/', '/_vti_bin', '/_vti_inf')
# IGNORABLE_404_ENDS:
diff --git a/components/tools/OmeroWeb/omeroweb/urls.py b/components/tools/OmeroWeb/omeroweb/urls.py
index ddc44edafb2..9e0661ef500 100755
--- a/components/tools/OmeroWeb/omeroweb/urls.py
+++ b/components/tools/OmeroWeb/omeroweb/urls.py
@@ -100,6 +100,8 @@ 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')),
+
url(r'^index/$', 'omeroweb.webclient.views.custom_index',
name="webindex_custom"),
)
diff --git a/components/tools/OmeroWeb/omeroweb/webadmin/urls.py b/components/tools/OmeroWeb/omeroweb/webadmin/urls.py
index f3f936b702c..c3d282ab60d 100644
--- a/components/tools/OmeroWeb/omeroweb/webadmin/urls.py
+++ b/components/tools/OmeroWeb/omeroweb/webadmin/urls.py
@@ -26,13 +26,14 @@
from django.conf.urls import url, patterns
from omeroweb.webadmin import views
-from omeroweb.webclient import views as webclient_views
+from omeroweb.webclient.views import WebclientLoginView
# url patterns
urlpatterns = patterns(
'',
url(r'^$', views.index, name="waindex"),
- url(r'^login/$', webclient_views.login, name="walogin"),
+ url(r'^login/$', WebclientLoginView.as_view(),
+ name="walogin"),
url(r'^logout/$', views.logout, name="walogout"),
url(r'^forgottenpassword/$', views.forgotten_password,
name="waforgottenpassword"),
diff --git a/components/tools/OmeroWeb/omeroweb/webclient/urls.py b/components/tools/OmeroWeb/omeroweb/webclient/urls.py
index d1160cc94a2..aba95377408 100644
--- a/components/tools/OmeroWeb/omeroweb/webclient/urls.py
+++ b/components/tools/OmeroWeb/omeroweb/webclient/urls.py
@@ -47,7 +47,7 @@
views.load_template, {'menu': 'history'},
name="history"),
- url(r'^login/$', views.login, name="weblogin"),
+ url(r'^login/$', views.WebclientLoginView.as_view(), name="weblogin"),
url(r'^logout/$', views.logout, name="weblogout"),
url(r'^active_group/$',
views.change_active_group,
diff --git a/components/tools/OmeroWeb/omeroweb/webclient/views.py b/components/tools/OmeroWeb/omeroweb/webclient/views.py
index 712a3ecf4cb..c171ebf8383 100755
--- a/components/tools/OmeroWeb/omeroweb/webclient/views.py
+++ b/components/tools/OmeroWeb/omeroweb/webclient/views.py
@@ -46,7 +46,8 @@
from django.conf import settings
from django.template import loader as template_loader
-from django.http import Http404, HttpResponse, HttpResponseRedirect
+from django.http import Http404, HttpResponse, HttpResponseRedirect, \
+ JsonResponse
from django.http import HttpResponseServerError, HttpResponseBadRequest
from django.template import RequestContext as Context
from django.utils.http import urlencode
@@ -74,21 +75,17 @@
from controller.share import BaseShare
from omeroweb.webadmin.forms import LoginForm
-from omeroweb.webadmin.webadmin_utils import upgradeCheck
from omeroweb.webgateway import views as webgateway_views
from omeroweb.webgateway.marshal import chgrpMarshal
from omeroweb.feedback.views import handlerInternalError
-from omeroweb.http import HttpJsonResponse
from omeroweb.webclient.decorators import login_required
from omeroweb.webclient.decorators import render_response
from omeroweb.webclient.show import Show, IncorrectMenuError, \
paths_to_object, paths_to_tag
-from omeroweb.connector import Connector
from omeroweb.decorators import ConnCleaningHttpResponse, parse_url
-from omeroweb.decorators import get_client_ip
from omeroweb.webgateway.util import getIntOrDefault
from omero.model import ProjectI, DatasetI, ImageI, \
@@ -177,9 +174,10 @@ def custom_index(request, conn=None, **kwargs):
# views
-def login(request):
+class WebclientLoginView(webgateway_views.LoginView):
"""
- Webclient Login - Also can be used by other Apps to log in to OMERO. Uses
+ Webclient Login - Customises the superclass LoginView
+ for webclient. Also can be used by other Apps to log in to OMERO. Uses
the 'server' id from request to lookup the server-id (index), host and
port from settings. E.g. "localhost", 4064. Stores these details, along
with username, password etc in the request.session. Resets other data
@@ -189,108 +187,80 @@ def login(request):
with appropriate error messages.
"""
- request.session.modified = True
-
- conn = None
- error = None
-
- form = LoginForm(data=request.POST.copy())
+ template = "webclient/login.html"
useragent = 'OMERO.web'
- 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(useragent)
- if (server_id is not None and username is not None and
- password is not None and compatible):
- conn = connector.create_connection(
- useragent, username, password, userip=get_client_ip(request))
- if conn is not None:
- # Check if user is in "user" group
- roles = conn.getAdminService().getSecurityRoles()
- userGroupId = roles.userGroupId
- if userGroupId in conn.getEventContext().memberOfGroups:
- 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)
- # if 'active_group' remains in session from previous
- # login, check it's valid for this user
- if request.session.get('active_group'):
- if (request.session.get('active_group') not in
- conn.getEventContext().memberOfGroups):
- del request.session['active_group']
- if request.session.get('user_id'):
- # always want to revert to logged-in user
- del request.session['user_id']
- if request.session.get('server_settings'):
- # always clean when logging in
- del request.session['server_settings']
- # do we ned to display server version ?
- # server_version = conn.getServerVersion()
- if request.POST.get('noredirect'):
- return HttpResponse('OK')
- url = request.GET.get("url")
- if url is None or len(url) == 0:
- try:
- url = parse_url(settings.LOGIN_REDIRECT)
- except:
- url = reverse("webindex")
- return HttpResponseRedirect(url)
- else:
- error = "This user is not active."
- if not connector.is_server_up(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.")
+ def get(self, request):
+ """
+ GET simply returns the login page
+ """
+ return self.handle_not_logged_in(request)
+
+ def handle_logged_in(self, request, conn, connector):
+ """
+ We override this to provide webclient-specific functionality
+ such as cleaning up any previous sessions (if user didn't logout)
+ and redirect to specified url or webclient index page.
+ """
+
+ # webclient has various state that needs cleaning up...
+ # if 'active_group' remains in session from previous
+ # login, check it's valid for this user
+ if request.session.get('active_group'):
+ if (request.session.get('active_group') not in
+ conn.getEventContext().memberOfGroups):
+ del request.session['active_group']
+ if request.session.get('user_id'):
+ # always want to revert to logged-in user
+ del request.session['user_id']
+ if request.session.get('server_settings'):
+ # always clean when logging in
+ del request.session['server_settings']
+ # do we ned to display server version ?
+ # server_version = conn.getServerVersion()
+ if request.POST.get('noredirect'):
+ return HttpResponse('OK')
+ url = request.GET.get("url")
+ if url is None or len(url) == 0:
+ try:
+ url = parse_url(settings.LOGIN_REDIRECT)
+ except:
+ url = reverse("webindex")
+ return HttpResponseRedirect(url)
+
+ 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 form is None:
+ server_id = request.GET.get('server', request.POST.get('server'))
+ if server_id is not None:
+ initial = {'server': unicode(server_id)}
+ form = LoginForm(initial=initial)
else:
- error = ("Connection not available, please check your"
- " user name and password.")
-
- url = request.GET.get("url")
-
- template = "webclient/login.html"
- if request.method != 'POST':
- server_id = request.GET.get('server', request.POST.get('server'))
- if server_id is not None:
- initial = {'server': unicode(server_id)}
- form = LoginForm(initial=initial)
- else:
- form = LoginForm()
-
- context = {
- 'version': omero_version,
- 'build_year': build_year,
- 'error': error,
- 'form': form}
- if url is not None and len(url) != 0:
- context['url'] = urlencode({'url': url})
-
- if hasattr(settings, 'LOGIN_LOGO'):
- context['LOGIN_LOGO'] = settings.LOGIN_LOGO
-
- t = template_loader.get_template(template)
- c = Context(request, context)
- rsp = t.render(c)
- return HttpResponse(rsp)
+ form = LoginForm()
+ context = {
+ 'version': omero_version,
+ 'build_year': build_year,
+ 'error': error,
+ 'form': form}
+ url = request.GET.get("url")
+ if url is not None and len(url) != 0:
+ context['url'] = urlencode({'url': url})
+
+ if hasattr(settings, 'LOGIN_LOGO'):
+ context['LOGIN_LOGO'] = settings.LOGIN_LOGO
+
+ t = template_loader.get_template(self.template)
+ c = Context(request, context)
+ rsp = t.render(c)
+ return HttpResponse(rsp)
@login_required(ignore_login_fail=True)
@@ -554,7 +524,7 @@ def api_group_list(request, conn=None, **kwargs):
except IceException as e:
return HttpResponseServerError(e.message)
- return HttpJsonResponse({'groups': groups})
+ return JsonResponse({'groups': groups})
@login_required()
@@ -573,7 +543,7 @@ def api_experimenter_list(request, conn=None, **kwargs):
group_id=group_id,
page=page,
limit=limit)
- return HttpJsonResponse({'experimenters': experimenters})
+ return JsonResponse({'experimenters': experimenters})
except ApiUsageException as e:
return HttpResponseBadRequest(e.serverStackTrace)
except ServerError as e:
@@ -598,7 +568,7 @@ def api_experimenter_detail(request, experimenter_id, conn=None, **kwargs):
# Get the experimenter
experimenter = tree.marshal_experimenter(
conn=conn, experimenter_id=experimenter_id)
- return HttpJsonResponse({'experimenter': experimenter})
+ return JsonResponse({'experimenter': experimenter})
except ApiUsageException as e:
return HttpResponseBadRequest(e.serverStackTrace)
@@ -686,7 +656,7 @@ def api_container_list(request, conn=None, **kwargs):
except IceException as e:
return HttpResponseServerError(e.message)
- return HttpJsonResponse(r)
+ return JsonResponse(r)
@login_required()
@@ -714,7 +684,7 @@ def api_dataset_list(request, conn=None, **kwargs):
except IceException as e:
return HttpResponseServerError(e.message)
- return HttpJsonResponse({'datasets': datasets})
+ return JsonResponse({'datasets': datasets})
@login_required()
@@ -769,7 +739,7 @@ def api_image_list(request, conn=None, **kwargs):
except IceException as e:
return HttpResponseServerError(e.message)
- return HttpJsonResponse({'images': images})
+ return JsonResponse({'images': images})
@login_required()
@@ -797,7 +767,7 @@ def api_plate_list(request, conn=None, **kwargs):
except IceException as e:
return HttpResponseServerError(e.message)
- return HttpJsonResponse({'plates': plates})
+ return JsonResponse({'plates': plates})
@login_required()
@@ -826,7 +796,7 @@ def api_plate_acquisition_list(request, conn=None, **kwargs):
except IceException as e:
return HttpResponseServerError(e.message)
- return HttpJsonResponse({'acquisitions': plate_acquisitions})
+ return JsonResponse({'acquisitions': plate_acquisitions})
def get_object_links(conn, parent_type, parent_id, child_type, child_ids):
@@ -985,7 +955,7 @@ def _api_links_POST(conn, json_data, **kwargs):
pass
response['success'] = True
- return HttpJsonResponse(response)
+ return JsonResponse(response)
def _api_links_DELETE(conn, json_data):
@@ -1036,7 +1006,7 @@ def _api_links_DELETE(conn, json_data):
# If we got here, DELETE was OK
response['success'] = True
- return HttpJsonResponse(response)
+ return JsonResponse(response)
@login_required()
@@ -1079,7 +1049,7 @@ def api_paths_to_object(request, conn=None, **kwargs):
paths = paths_to_object(conn, experimenter_id, project_id,
dataset_id, image_id, screen_id, plate_id,
acquisition_id, well_id, group_id)
- return HttpJsonResponse({'paths': paths})
+ return JsonResponse({'paths': paths})
@login_required()
@@ -1138,7 +1108,7 @@ def api_tags_and_tagged_list_GET(request, conn=None, **kwargs):
except IceException as e:
return HttpResponseServerError(e.message)
- return HttpJsonResponse(tagged)
+ return JsonResponse(tagged)
def api_tags_and_tagged_list_DELETE(request, conn=None, **kwargs):
@@ -1173,7 +1143,7 @@ def api_tags_and_tagged_list_DELETE(request, conn=None, **kwargs):
except IceException as e:
return HttpResponseServerError(e.message)
- return HttpJsonResponse('')
+ return JsonResponse('')
@login_required()
@@ -1202,7 +1172,7 @@ def api_annotations(request, conn=None, **kwargs):
page=page,
limit=limit)
- return HttpJsonResponse({'annotations': anns, 'experimenters': exps})
+ return JsonResponse({'annotations': anns, 'experimenters': exps})
@login_required()
@@ -1240,7 +1210,7 @@ def api_share_list(request, conn=None, **kwargs):
except IceException as e:
return HttpResponseServerError(e.message)
- return HttpJsonResponse({'shares': shares, 'discussions': discussions})
+ return JsonResponse({'shares': shares, 'discussions': discussions})
@login_required()
@@ -2168,7 +2138,7 @@ def annotate_file(request, conn=None, **kwargs):
newFileId = manager.createFileAnnotations(
fileupload, oids, well_index=index)
added_files.append(newFileId)
- return HttpJsonResponse({'fileIds': added_files})
+ return JsonResponse({'fileIds': added_files})
else:
return HttpResponse(form_file.errors)
@@ -2198,7 +2168,7 @@ def annotate_rating(request, conn=None, **kwargs):
o.setRating(rating)
# return a summary of ratings
- return HttpJsonResponse({'success': True})
+ return JsonResponse({'success': True})
@login_required()
@@ -2499,9 +2469,9 @@ def annotate_tags(request, conn=None, **kwargs):
"%s-%s" % (dtype, obj.id)
for dtype, objs in oids.items()
for obj in objs], index, tag_owner_id=self_id)
- return HttpJsonResponse({'added': tags,
- 'removed': removed,
- 'new': new_tags})
+ return JsonResponse({'added': tags,
+ 'removed': removed,
+ 'new': new_tags})
else:
# TODO: handle invalid form error
return HttpResponse(str(form_tags.errors))
@@ -2611,13 +2581,13 @@ def manage_action_containers(request, action, o_type=None, o_id=None,
description = form.cleaned_data['description']
oid = manager.createDataset(name, description)
rdict = {'bad': 'false', 'id': oid}
- return HttpJsonResponse(rdict)
+ return JsonResponse(rdict)
else:
d = dict()
for e in form.errors.iteritems():
d.update({e[0]: unicode(e[1])})
rdict = {'bad': 'true', 'errs': d}
- return HttpJsonResponse(rdict)
+ return JsonResponse(rdict)
elif o_type == "tagset" and o_id > 0:
form = ContainerForm(data=request.POST.copy())
if form.is_valid():
@@ -2625,13 +2595,13 @@ def manage_action_containers(request, action, o_type=None, o_id=None,
description = form.cleaned_data['description']
oid = manager.createTag(name, description)
rdict = {'bad': 'false', 'id': oid}
- return HttpJsonResponse(rdict)
+ return JsonResponse(rdict)
else:
d = dict()
for e in form.errors.iteritems():
d.update({e[0]: unicode(e[1])})
rdict = {'bad': 'true', 'errs': d}
- return HttpJsonResponse(rdict)
+ return JsonResponse(rdict)
elif request.POST.get('folder_type') in ("project", "screen",
"dataset", "tag", "tagset"):
# No parent specified. We can create orphaned 'project', 'dataset'
@@ -2651,13 +2621,13 @@ def manage_action_containers(request, action, o_type=None, o_id=None,
oid = getattr(manager, "create" +
folder_type.capitalize())(name, description)
rdict = {'bad': 'false', 'id': oid}
- return HttpJsonResponse(rdict)
+ return JsonResponse(rdict)
else:
d = dict()
for e in form.errors.iteritems():
d.update({e[0]: unicode(e[1])})
rdict = {'bad': 'true', 'errs': d}
- return HttpJsonResponse(rdict)
+ return JsonResponse(rdict)
else:
return HttpResponseServerError("Object does not exist")
elif action == 'add':
@@ -2781,13 +2751,13 @@ def manage_action_containers(request, action, o_type=None, o_id=None,
manager.image = manager.well.getWellSample(index).image()
o_type = "image"
manager.updateName(o_type, name)
- return HttpJsonResponse(rdict)
+ return JsonResponse(rdict)
else:
d = dict()
for e in form.errors.iteritems():
d.update({e[0]: unicode(e[1])})
rdict = {'bad': 'true', 'errs': d}
- return HttpJsonResponse(rdict)
+ return JsonResponse(rdict)
else:
return HttpResponseServerError("Object does not exist")
elif action == 'editdescription':
@@ -2818,13 +2788,13 @@ def manage_action_containers(request, action, o_type=None, o_id=None,
o_type = "image"
manager.updateDescription(o_type, description)
rdict = {'bad': 'false'}
- return HttpJsonResponse(rdict)
+ return JsonResponse(rdict)
else:
d = dict()
for e in form.errors.iteritems():
d.update({e[0]: unicode(e[1])})
rdict = {'bad': 'true', 'errs': d}
- return HttpJsonResponse(rdict)
+ return JsonResponse(rdict)
else:
return HttpResponseServerError("Object does not exist")
elif action == 'remove':
@@ -2837,10 +2807,10 @@ def manage_action_containers(request, action, o_type=None, o_id=None,
except Exception, x:
logger.error(traceback.format_exc())
rdict = {'bad': 'true', 'errs': str(x)}
- return HttpJsonResponse(rdict)
+ return JsonResponse(rdict)
rdict = {'bad': 'false'}
- return HttpJsonResponse(rdict)
+ return JsonResponse(rdict)
elif action == 'removefromshare':
image_id = request.POST.get('source')
try:
@@ -2848,9 +2818,9 @@ def manage_action_containers(request, action, o_type=None, o_id=None,
except Exception, x:
logger.error(traceback.format_exc())
rdict = {'bad': 'true', 'errs': str(x)}
- return HttpJsonResponse(rdict)
+ return JsonResponse(rdict)
rdict = {'bad': 'false'}
- return HttpJsonResponse(rdict)
+ return JsonResponse(rdict)
elif action == 'delete':
# Handles delete of a file attached to object.
child = toBoolean(request.POST.get('child'))
@@ -2874,7 +2844,7 @@ def manage_action_containers(request, action, o_type=None, o_id=None,
rdict = {'bad': 'true', 'errs': str(x)}
else:
rdict = {'bad': 'false'}
- return HttpJsonResponse(rdict)
+ return JsonResponse(rdict)
elif action == 'deletemany':
# Handles multi-delete from jsTree.
object_ids = {
@@ -2920,7 +2890,7 @@ def manage_action_containers(request, action, o_type=None, o_id=None,
raise
else:
rdict = {'bad': 'false'}
- return HttpJsonResponse(rdict)
+ return JsonResponse(rdict)
context['template'] = template
return context
@@ -3541,7 +3511,7 @@ def activities(request, conn=None, **kwargs):
rv['inprogress'] = in_progress
rv['failure'] = failure
rv['jobs'] = len(request.session['callback'])
- return HttpJsonResponse(rv) # json
+ return JsonResponse(rv) # json
jobs = []
new_errors = False
@@ -3594,7 +3564,7 @@ def activities_update(request, action, **kwargs):
rv['removed'] = True
else:
rv['removed'] = False
- return HttpJsonResponse(rv)
+ return JsonResponse(rv)
else:
for key, data in request.session['callback'].items():
if data['status'] != "in progress":
@@ -4300,7 +4270,7 @@ def getObjectOwnerId(r):
request.session.get('user_id'))
# return HttpResponse("OK")
- return HttpJsonResponse({'update': update})
+ return JsonResponse({'update': update})
@login_required(setGroupContext=True)
@@ -4321,7 +4291,7 @@ def script_run(request, scriptId, conn=None, **kwargs):
# Delegate to run_script() for handling 'No processor available'
rsp = run_script(
request, conn, sId, inputMap, scriptName='Script')
- return HttpJsonResponse(rsp)
+ return JsonResponse(rsp)
else:
raise
params = scriptService.getParams(sId)
@@ -4429,7 +4399,7 @@ def script_run(request, scriptId, conn=None, **kwargs):
except:
pass
rsp = run_script(request, conn, sId, inputMap, scriptName)
- return HttpJsonResponse(rsp)
+ return JsonResponse(rsp)
@require_POST
@@ -4455,7 +4425,7 @@ def ome_tiff_script(request, imageId, conn=None, **kwargs):
inputMap['Format'] = wrap('OME-TIFF')
rsp = run_script(
request, conn, sId, inputMap, scriptName='Create OME-TIFF')
- return HttpJsonResponse(rsp)
+ return JsonResponse(rsp)
def run_script(request, conn, sId, inputMap, scriptName='Script'):
diff --git a/components/tools/OmeroWeb/omeroweb/webgateway/api_exceptions.py b/components/tools/OmeroWeb/omeroweb/webgateway/api_exceptions.py
new file mode 100644
index 00000000000..ad24594249f
--- /dev/null
+++ b/components/tools/OmeroWeb/omeroweb/webgateway/api_exceptions.py
@@ -0,0 +1,54 @@
+#!/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 .
+
+
+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):
+ 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
+ """
+ status = 404
+
+ def __init__(self, message, stacktrace=None):
+ 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
+ """
+ status = 201
+
+ def __init__(self, response):
+ super(CreatedObject, self).__init__(response)
+ self.response = response
diff --git a/components/tools/OmeroWeb/omeroweb/webgateway/api_marshal.py b/components/tools/OmeroWeb/omeroweb/webgateway/api_marshal.py
new file mode 100644
index 00000000000..bc29769c782
--- /dev/null
+++ b/components/tools/OmeroWeb/omeroweb/webgateway/api_marshal.py
@@ -0,0 +1,68 @@
+#!/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 .
+
+''' Helper functions for views that handle object trees '''
+
+
+from omero_marshal import get_encoder
+
+
+def normalize_objects(objects):
+ """
+ Takes 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.
+ """
+ experimenters = {}
+ groups = {}
+ objs = []
+ for o in objects:
+ exp = o['omero:details']['owner']
+ experimenters[exp['@id']] = exp
+ o['omero:details']['owner'] = {'@id': exp['@id']}
+ grp = o['omero:details']['group']
+ groups[grp['@id']] = grp
+ o['omero:details']['group'] = {'@id': grp['@id']}
+ objs.append(o)
+ experimenters = experimenters.values()
+ groups = groups.values()
+ return objs, {'experimenters': experimenters, 'experimenterGroups': groups}
+
+
+def marshal_objects(objects, extras=None, normalize=False):
+ """
+ Marshals 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__)
+ m = encoder.encode(o)
+ if extras is not None and o.id.val in extras:
+ m.update(extras[o.id.val])
+ marshalled.append(m)
+
+ if not normalize:
+ return {'data': marshalled}
+ data, extra_objs = normalize_objects(marshalled)
+ extra_objs['data'] = data
+ return extra_objs
diff --git a/components/tools/OmeroWeb/omeroweb/webgateway/api_query.py b/components/tools/OmeroWeb/omeroweb/webgateway/api_query.py
new file mode 100644
index 00000000000..4ed4a738131
--- /dev/null
+++ b/components/tools/OmeroWeb/omeroweb/webgateway/api_query.py
@@ -0,0 +1,92 @@
+#!/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 .
+
+''' Helper functions for views that handle object trees '''
+
+import omero
+
+from omero.rtypes import unwrap, rlong
+from django.conf import settings
+
+from api_marshal import marshal_objects
+from copy import deepcopy
+
+
+def query_projects(conn, childCount=False,
+ group=None, owner=None,
+ page=1, limit=settings.PAGE,
+ normalize=False):
+ """
+ Query OMERO and marshal omero.model.Projects.
+
+ Build a query based on a number of parameters,
+ queries OMERO with the query service and
+ marshals Projects with omero_marshal.
+
+ @param conn: BlitzGateway
+ @param childCount: If true, also load Dataset counts as omero:childCount
+ @param group: Filter by group Id
+ @param owner: Filter by owner Id
+ @param page: Pagination page. Default is 1
+ @param limit: Page size
+ @param normalize: If true, marshal groups and experimenters separately
+ """
+ qs = conn.getQueryService()
+ params = omero.sys.ParametersI()
+ if page:
+ params.page((page-1) * limit, limit)
+ ctx = deepcopy(conn.SERVICE_OPTS)
+
+ # Set the desired group context and owner
+ if group is None:
+ group = -1
+ ctx.setOmeroGroup(group)
+ where_clause = ''
+ if owner is not None and owner != -1:
+ params.add('owner', rlong(owner))
+ where_clause = 'where project.details.owner.id = :owner'
+
+ withChildCount = ""
+ if childCount:
+ withChildCount = """, (select count(id) from ProjectDatasetLink pdl
+ where pdl.parent=project.id)"""
+
+ # Need to load owners specifically, else can be unloaded if group != -1
+ query = """
+ select project %s from Project project
+ join fetch project.details.owner
+ %s
+ order by lower(project.name), project.id
+ """ % (withChildCount, where_clause)
+
+ projects = []
+ extras = {}
+ if childCount:
+ result = qs.projection(query, params, ctx)
+ for p in result:
+ project = unwrap(p[0])
+ projects.append(project)
+ extras[project.id.val] = {'omero:childCount': unwrap(p[1])}
+ else:
+ extras = None
+ result = qs.findAllByQuery(query, params, ctx)
+ for p in result:
+ projects.append(p)
+
+ return marshal_objects(projects, extras=extras, normalize=normalize)
diff --git a/components/tools/OmeroWeb/omeroweb/webgateway/decorators.py b/components/tools/OmeroWeb/omeroweb/webgateway/decorators.py
new file mode 100644
index 00000000000..c3e14d6fb26
--- /dev/null
+++ b/components/tools/OmeroWeb/omeroweb/webgateway/decorators.py
@@ -0,0 +1,118 @@
+#!/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 .
+#
+
+"""Decorators for use with the webgateway application."""
+
+import omero
+import omeroweb.decorators
+import logging
+import traceback
+from django.http import JsonResponse
+from functools import update_wrapper
+from api_exceptions import NotFoundError, BadRequestError, CreatedObject
+
+
+logger = logging.getLogger(__name__)
+
+
+class login_required(omeroweb.decorators.login_required):
+ """
+ webgateway specific extension of the OMERO.web login_required() decorator.
+ """
+
+ def on_not_logged_in(self, request, url, error=None):
+ """
+ Used for json api methods
+ """
+ return JsonResponse({'message': 'Not logged in'},
+ status=403)
+
+
+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."""
+ pass
+
+ def handle_success(self, rv):
+ """
+ Handle successful response from wrapped function.
+
+ By default, we simply return a JsonResponse() but this can be
+ overwritten by subclasses if needed.
+ """
+ return JsonResponse(rv)
+
+ def handle_error(self, ex, trace):
+ """
+ Handle errors from wrapped function.
+
+ 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
+ status = 500
+ if isinstance(ex, NotFoundError):
+ status = ex.status
+ if isinstance(ex, BadRequestError):
+ status = ex.status
+ 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
+ # In this case, there's no Error and the response
+ # is valid (status code is 201)
+ if isinstance(ex, CreatedObject):
+ status = ex.status
+ rsp_json = ex.response
+ return JsonResponse(rsp_json, status=status)
+
+ def __call__(ctx, f):
+ """
+ Returns the decorator.
+
+ The decorator calls the wrapped function and
+ handles success or exception, returning a
+ JsonResponse
+ """
+ def wrapped(request, *args, **kwargs):
+ logger.debug('json_response')
+ try:
+ rv = f(request, *args, **kwargs)
+ return ctx.handle_success(rv)
+ except Exception, ex:
+ trace = traceback.format_exc()
+ return ctx.handle_error(ex, trace)
+ return update_wrapper(wrapped, f)
diff --git a/components/tools/OmeroWeb/omeroweb/webgateway/urls.py b/components/tools/OmeroWeb/omeroweb/webgateway/urls.py
index 15f539d73b7..19716501b7c 100644
--- a/components/tools/OmeroWeb/omeroweb/webgateway/urls.py
+++ b/components/tools/OmeroWeb/omeroweb/webgateway/urls.py
@@ -427,6 +427,7 @@
This url will retrieve all rendering definitions for a given image (id)
"""
+
urlpatterns = patterns(
'',
webgateway,
@@ -478,6 +479,4 @@
table_query,
object_table_query,
open_with_options,
- # Debug stuff
-
)
diff --git a/components/tools/OmeroWeb/omeroweb/webgateway/urls_api.py b/components/tools/OmeroWeb/omeroweb/webgateway/urls_api.py
new file mode 100644
index 00000000000..6d8c5753f1e
--- /dev/null
+++ b/components/tools/OmeroWeb/omeroweb/webgateway/urls_api.py
@@ -0,0 +1,95 @@
+#!/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 .
+
+# Handles all 'api' urls without including '/webgateway/' in the url.
+
+from django.conf.urls import url, patterns
+from omeroweb.webgateway import views
+from omeroweb.webgateway.views import LoginView
+from django.conf import settings
+import re
+
+versions = '|'.join([re.escape(v)
+ for v in settings.API_VERSIONS])
+
+api_versions = url(r'^$', views.api_versions, name='api_versions')
+
+api_base = url(r'^v(?P%s)/$' % versions,
+ views.api_base,
+ name='api_base')
+"""
+GET various urls listed below
+"""
+
+api_token = url(r'^v(?P%s)/token/$' % versions,
+ views.api_token,
+ name='api_token')
+"""
+GET the CSRF token for this session. Needs to be included
+in header with all POST, PUT & DELETE requests
+"""
+
+api_servers = url(r'^v(?P%s)/servers/$' % versions,
+ views.api_servers,
+ name='api_servers')
+"""
+GET list of available OMERO servers to login to.
+"""
+
+api_login = url(r'^v(?P%s)/login/$' % versions,
+ LoginView.as_view(),
+ name='api_login')
+"""
+Login to OMERO. POST with 'username', 'password' and 'server' index
+"""
+
+api_save = url(r'^v(?P%s)/m/save/$' % versions,
+ views.SaveView.as_view(),
+ name='api_save')
+"""
+POST to create a new object or PUT to update existing object.
+In both cases content body encodes json data.
+"""
+
+api_projects = url(r'^v(?P%s)/m/projects/$' % versions,
+ views.ProjectsView.as_view(),
+ name='api_projects')
+"""
+GET all projects, using omero-marshal to generate json
+"""
+
+api_project = url(
+ r'^v(?P%s)/m/projects/(?P[0-9]+)/$' % versions,
+ views.ProjectView.as_view(),
+ name='api_project')
+"""
+Project url to GET or DELETE a single Project
+"""
+
+urlpatterns = patterns(
+ '',
+ api_versions,
+ api_base,
+ api_token,
+ api_servers,
+ api_login,
+ api_save,
+ api_projects,
+ api_project,
+)
diff --git a/components/tools/OmeroWeb/omeroweb/webgateway/views.py b/components/tools/OmeroWeb/omeroweb/webgateway/views.py
index 81986221e45..ca78024f5a1 100644
--- a/components/tools/OmeroWeb/omeroweb/webgateway/views.py
+++ b/components/tools/OmeroWeb/omeroweb/webgateway/views.py
@@ -19,20 +19,28 @@
import omero.clients
from django.http import HttpResponse, HttpResponseBadRequest, \
- HttpResponseServerError
+ HttpResponseServerError, JsonResponse
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 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:
@@ -43,9 +51,10 @@
from cStringIO import StringIO
import tempfile
-from omero import ApiUsageException
+from omero import ApiUsageException, ValidationException
from omero.util.decorators import timeit, TimeIt
-from omeroweb.http import HttpJavascriptResponse, HttpJsonResponse, \
+from omeroweb.connector import Server
+from omeroweb.http import HttpJavascriptResponse, \
HttpJavascriptResponseServerError
import glob
@@ -55,8 +64,6 @@
from webgateway_cache import webgateway_cache, CacheBase, webgateway_tempfile
-cache = CacheBase()
-
import logging
import os
import traceback
@@ -65,9 +72,14 @@
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__)
try:
@@ -1081,8 +1093,8 @@ def render_ome_tiff(request, ctx, cid, conn=None, **kwargs):
except:
logger.debug(traceback.format_exc())
raise
- return HttpResponseRedirect(settings.STATIC_URL + 'webgateway/tfiles/'
- + rpath)
+ return HttpResponseRedirect(settings.STATIC_URL +
+ 'webgateway/tfiles/' + rpath)
@login_required()
@@ -1241,24 +1253,35 @@ def wrap(request, *args, **kwargs):
return rv
if isinstance(rv, HttpResponse):
return rv
- rv = json.dumps(rv)
c = request.GET.get('callback', None)
if c is not None and not kwargs.get('_internal', False):
+ rv = json.dumps(rv)
rv = '%s(%s)' % (c, rv)
+ # mimetype for JSONP is application/javascript
+ return HttpJavascriptResponse(rv)
if kwargs.get('_internal', False):
return rv
- return HttpJavascriptResponse(rv)
- except omero.ServerError:
+ # mimetype for JSON is application/json
+ # NB: To support old api E.g. /get_rois_json/
+ # We need to support lists
+ safe = type(rv) is dict
+ return JsonResponse(rv, safe=safe)
+ except Exception, ex:
+ # Default status is 500 'server error'
+ # But we try to handle all 'expected' errors appropriately
+ # TODO: handle omero.ConcurrencyException
+ status = 500
+ if isinstance(ex, omero.SecurityViolation):
+ status = 403
+ elif isinstance(ex, omero.ApiUsageException):
+ status = 400
+ trace = traceback.format_exc()
+ logger.debug(trace)
if kwargs.get('_raw', False) or kwargs.get('_internal', False):
raise
- return HttpJavascriptResponseServerError(
- '("error in call","%s")' % traceback.format_exc())
- except:
- logger.debug(traceback.format_exc())
- if kwargs.get('_raw', False) or kwargs.get('_internal', False):
- raise
- return HttpJavascriptResponseServerError(
- '("error in call","%s")' % traceback.format_exc())
+ return JsonResponse(
+ {"message": str(ex), "stacktrace": trace},
+ status=status)
wrap.func_name = f.func_name
return wrap
@@ -1775,8 +1798,8 @@ def compat(i):
return False
pp = i.getPrimaryPixels()
if (pp is None or
- i.getPrimaryPixels().getPixelsType().getValue() != img_ptype
- or i.getSizeC() != img_ccount):
+ i.getPrimaryPixels().getPixelsType().getValue() != img_ptype or
+ i.getSizeC() != img_ccount):
return False
ew = [x.getLabel() for x in i.getChannels()]
ew.sort()
@@ -2309,7 +2332,7 @@ def get_shape_json(request, roiId, shapeId, conn=None, **kwargs):
if shape is None:
logger.debug('No such shape: %r' % shapeId)
raise Http404
- return HttpJsonResponse(shapeMarshal(shape))
+ return JsonResponse(shapeMarshal(shape))
@login_required()
@@ -2581,6 +2604,334 @@ 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):
diff --git a/components/tools/OmeroWeb/requirements-common.txt b/components/tools/OmeroWeb/requirements-common.txt
index d939edddc0f..89392cac3cf 100644
--- a/components/tools/OmeroWeb/requirements-common.txt
+++ b/components/tools/OmeroWeb/requirements-common.txt
@@ -6,4 +6,4 @@
Django>=1.8,<1.9
django-pipeline==1.3.20
-git+git://github.com/openmicroscopy/omero-marshal.git@v0.4.0#egg=omero-marshal
+omero-marshal==0.4.1
diff --git a/components/tools/OmeroWeb/test/integration/test_api_errors.py b/components/tools/OmeroWeb/test/integration/test_api_errors.py
new file mode 100644
index 00000000000..6f4e2206031
--- /dev/null
+++ b/components/tools/OmeroWeb/test/integration/test_api_errors.py
@@ -0,0 +1,215 @@
+#!/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 .
+
+"""
+Tests querying & editing Projects with webgateway json api
+"""
+
+from weblibrary import IWebTest, _csrf_post_json, _csrf_put_json
+from django.core.urlresolvers import reverse
+from django.conf import settings
+from omero.gateway import BlitzGateway
+import pytest
+from omero.model import ProjectI
+from omero.rtypes import rstring
+from omero_marshal import get_encoder, get_decoder, OME_SCHEMA_URL
+from omero import ValidationException
+
+
+def get_connection(user, group_id=None):
+ """
+ Get a BlitzGateway connection for the given user's client
+ """
+ connection = BlitzGateway(client_obj=user[0])
+ # Refresh the session context
+ connection.getEventContext()
+ if group_id is not None:
+ connection.SERVICE_OPTS.setOmeroGroup(group_id)
+ return connection
+
+
+class TestErrors(IWebTest):
+ """
+ Tests the response status with various error types
+ """
+
+ # Create a read-annotate group
+ @pytest.fixture(scope='function')
+ def group_A(self):
+ """Returns a new read-only group."""
+ return self.new_group(perms='rwra--')
+
+ # Create a read-only group
+ @pytest.fixture(scope='function')
+ def group_B(self):
+ """Returns a new read-only group."""
+ return self.new_group(perms='rwr---')
+
+ # Create users in the read-only group
+ @pytest.fixture()
+ def user_A(self, group_A, group_B):
+ """Returns a new user in the group_A group and also add to group_B"""
+ user = self.new_client_and_user(group=group_A)
+ self.add_groups(user[1], [group_B])
+ return user
+
+ def test_save_post_no_id(self):
+ """ If POST to /save/ data shouldn't contain @id """
+ django_client = self.django_root_client
+ version = settings.API_VERSIONS[-1]
+ save_url = reverse('api_save', kwargs={'api_version': version})
+ payload = {'Name': 'test_save_post_no_id',
+ '@id': 1}
+ rsp = _csrf_post_json(django_client, save_url, payload,
+ status_code=400)
+ assert (rsp['message'] ==
+ "Object has '@id' attribute. Use PUT to update objects")
+
+ def test_save_put_id(self):
+ """ If PUT to /save/ to update, data must contain @id """
+ django_client = self.django_root_client
+ version = settings.API_VERSIONS[-1]
+ save_url = reverse('api_save', kwargs={'api_version': version})
+ payload = {'Name': 'test_save_put_id'}
+ rsp = _csrf_put_json(django_client, save_url, payload,
+ status_code=400)
+ assert (rsp['message'] ==
+ "No '@id' attribute. Use POST to create new objects")
+
+ def test_marshal_type(self):
+ """ If no decoder found for @type, get suitable message"""
+ django_client = self.django_root_client
+ version = settings.API_VERSIONS[-1]
+ save_url = reverse('api_save', kwargs={'api_version': version})
+ objType = 'SomeInvalid#Type'
+ payload = {'Name': 'test_marshal_type',
+ '@type': objType}
+ rsp = _csrf_post_json(django_client, save_url, payload,
+ status_code=400)
+ assert (rsp['message'] ==
+ 'No decoder found for type: %s' % objType)
+
+ def test_marshal_validation(self):
+ django_client = self.django_root_client
+ version = settings.API_VERSIONS[-1]
+ save_url = reverse('api_save', kwargs={'api_version': version})
+ payload = {'Name': 'test_marshal_validation',
+ '@type': OME_SCHEMA_URL + '#Project',
+ 'omero:details': {'@type': 'foo'}}
+ rsp = _csrf_post_json(django_client, save_url, payload,
+ status_code=400)
+ assert (rsp['message'] ==
+ "Error in decode of json data by omero_marshal")
+ assert rsp['stacktrace'].startswith(
+ 'Traceback (most recent call last):')
+
+ def test_security_violation(self, group_B, user_A):
+ conn = get_connection(user_A)
+ groupAid = conn.getEventContext().groupId
+ userName = conn.getUser().getName()
+ django_client = self.new_django_client(userName, userName)
+ version = settings.API_VERSIONS[-1]
+ groupBid = group_B.id.val
+ save_url = reverse('api_save', kwargs={'api_version': version})
+ # Create project in group_A (default group)
+ payload = {'Name': 'test_security_violation',
+ '@type': OME_SCHEMA_URL + '#Project'}
+ save_url_grpA = save_url + '?group=' + str(groupAid)
+ pr_json = _csrf_post_json(django_client, save_url_grpA, payload,
+ status_code=201)
+ projectId = pr_json['@id']
+ # Try to save again into group B
+ save_url_grpB = save_url + '?group=' + str(groupBid)
+ rsp = _csrf_put_json(django_client, save_url_grpB, pr_json,
+ status_code=403)
+ assert 'message' in rsp
+ msg = "Cannot read ome.model.containers.Project:Id_%s" % projectId
+ assert msg in rsp['message']
+ assert rsp['stacktrace'].startswith(
+ 'Traceback (most recent call last):')
+
+ def test_marshal_exception(self):
+ django_client = self.django_root_client
+ version = settings.API_VERSIONS[-1]
+ save_url = reverse('api_save', kwargs={'api_version': version})
+ payload = {'Name': 'test_type_error',
+ '@type': OME_SCHEMA_URL + '#Project',
+ 'omero:details': {'@type': 'foo'}}
+ rsp = _csrf_post_json(django_client, save_url, payload,
+ status_code=400)
+ assert (rsp['message'] ==
+ "Error in decode of json data by omero_marshal")
+ assert rsp['stacktrace'].startswith(
+ 'Traceback (most recent call last):')
+
+ def test_validation_exception(self, user_A):
+ conn = get_connection(user_A)
+ group = conn.getEventContext().groupId
+ userName = conn.getUser().getName()
+ django_client = self.new_django_client(userName, userName)
+ version = settings.API_VERSIONS[-1]
+ save_url = reverse('api_save', kwargs={'api_version': version})
+ save_url += '?group=' + str(group)
+
+ # Create Tag
+ tag = {'Value': 'test_tag',
+ '@type': OME_SCHEMA_URL + '#TagAnnotation'}
+ tag_rsp = _csrf_post_json(django_client, save_url, tag,
+ status_code=201)
+
+ # Add Tag twice to Project to get Validation Exception
+ del tag_rsp['omero:details']
+ payload = {'Name': 'test_validation',
+ '@type': OME_SCHEMA_URL + '#Project',
+ 'Annotations': [tag_rsp, tag_rsp]}
+ rsp = _csrf_post_json(django_client, save_url, payload,
+ status_code=400)
+ # NB: message contains whole stack trace
+ assert "ValidationException" in rsp['message']
+ assert rsp['stacktrace'].startswith(
+ 'Traceback (most recent call last):')
+
+ def test_project_validation(self, user_A):
+ """
+ This test illustrates the ValidationException we see when
+ Project is encoded to dict then decoded back to Project
+ and saved.
+ No exception is seen if the original Project is simply
+ saved without encode & decode OR if the details are unloaded
+ before saving
+ """
+ conn = get_connection(user_A)
+ project = ProjectI()
+ project.name = rstring('test_project_validation')
+ project = conn.getUpdateService().saveAndReturnObject(project)
+
+ # Saving original Project again is OK
+ conn.getUpdateService().saveObject(project)
+
+ # encode and decode before Save raises Validation Exception
+ project_json = get_encoder(project.__class__).encode(project)
+ decoder = get_decoder(project_json['@type'])
+ p = decoder.decode(project_json)
+ with pytest.raises(ValidationException):
+ conn.getUpdateService().saveObject(p)
+
+ p = decoder.decode(project_json)
+ # Unloading details allows Save without exception
+ p.unloadDetails()
+ conn.getUpdateService().saveObject(p)
diff --git a/components/tools/OmeroWeb/test/integration/test_api_login.py b/components/tools/OmeroWeb/test/integration/test_api_login.py
new file mode 100644
index 00000000000..532862f8500
--- /dev/null
+++ b/components/tools/OmeroWeb/test/integration/test_api_login.py
@@ -0,0 +1,182 @@
+#!/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 .
+
+"""
+Tests logging in with webgateway json api
+"""
+
+import pytest
+from weblibrary import IWebTest, _get_response_json, _post_response_json, \
+ _csrf_post_response_json
+from django.core.urlresolvers import reverse, NoReverseMatch
+from django.conf import settings
+from django.test import Client
+from omero_marshal import OME_SCHEMA_URL
+import json
+
+
+class TestLogin(IWebTest):
+ """
+ Tests login workflow: getting url, csfv tokens etc.
+ """
+
+ def test_versions(self):
+ """
+ Start at the base url, get versions
+ """
+ django_client = self.django_root_client
+ request_url = reverse('api_versions')
+ rsp = _get_response_json(django_client, request_url, {})
+ versions = rsp['data']
+ assert len(versions) == len(settings.API_VERSIONS)
+ for v in versions:
+ assert v['version'] in settings.API_VERSIONS
+
+ def test_base_url(self):
+ """
+ Tests that the base url for a given version provides other urls
+ """
+ django_client = self.django_root_client
+ # test the most recent version
+ version = settings.API_VERSIONS[-1]
+ request_url = reverse('api_base', kwargs={'api_version': version})
+ rsp = _get_response_json(django_client, request_url, {})
+ assert 'servers_url' in rsp
+ assert 'login_url' in rsp
+ assert 'projects_url' in rsp
+ assert 'save_url' in rsp
+ assert rsp['schema_url'] == OME_SCHEMA_URL
+
+ def test_base_url_versions_404(self):
+ """
+ Tests that the base url gives 404 for invalid versions
+ """
+ version = '0'
+ with pytest.raises(NoReverseMatch):
+ reverse('api_base', kwargs={'api_version': version})
+
+ def test_login_get(self):
+ """
+ Tests that we get a suitable message if we try to GET login_url
+ """
+ django_client = self.django_root_client
+ version = settings.API_VERSIONS[-1]
+ request_url = reverse('api_login', kwargs={'api_version': version})
+ rsp = _get_response_json(django_client, request_url, {},
+ status_code=405)
+ assert (rsp['message'] ==
+ "POST only with username, password, server and csrftoken")
+
+ def test_login_csrf(self):
+ """
+ Tests that we can only login with CSRF
+ """
+ django_client = self.django_root_client
+ # test the most recent version
+ version = settings.API_VERSIONS[-1]
+ request_url = reverse('api_login', kwargs={'api_version': version})
+ rsp = _post_response_json(django_client, request_url, {},
+ status_code=403)
+ assert (rsp['message'] ==
+ ("CSRF Error. You need to include valid CSRF tokens for any"
+ " POST, PUT, PATCH or DELETE operations."
+ " You have to include CSRF token in the POST data or"
+ " add the token to the HTTP header."))
+
+ @pytest.mark.parametrize("credentials", [
+ [{'username': 'guest', 'password': 'fake', 'server': 1},
+ "Username: Guest account is not supported."],
+ [{'username': 'nobody', 'password': '', 'server': 1},
+ "Password: This field is required."],
+ [{'password': 'fake'},
+ # No username OR server. Test concatenation of 2 errors
+ ("Username: This field is required. "
+ "Server: This field is required.")],
+ [{'username': 'nobody', 'password': 'fake', 'server': 1},
+ ("Connection not available, "
+ "please check your user name and password.")]
+ ])
+ def test_login_errors(self, credentials):
+ """
+ Tests that we get expected form validation errors if try to login
+ without required fields, as 'guest' or with invalid username/password.
+ """
+ django_client = self.django_root_client
+ # test the most recent version
+ version = settings.API_VERSIONS[-1]
+ request_url = reverse('api_login', kwargs={'api_version': version})
+ data = credentials[0]
+ message = credentials[1]
+ rsp = _csrf_post_response_json(django_client, request_url, data,
+ status_code=403)
+ assert rsp['message'] == message
+
+ def test_login_example(self):
+ """
+ Example of successful login as user would do for real,
+ starting at base url and getting all other urls and info from there.
+ """
+ # Create test user and get username & password for use below
+ user = self.new_user()
+ username = password = user.getOmeName().val
+
+ # Django client, not logged in yet
+ django_client = Client()
+ # Start at the /api/ url to list versions...
+ request_url = reverse('api_versions')
+ rsp = _get_response_json(django_client, request_url, {})
+ # Pick the last version
+ version = rsp['data'][-1]
+ base_url = version['base_url']
+ # Base url will give a bunch of other urls
+ base_rsp = _get_response_json(django_client, base_url, {})
+ login_url = base_rsp['login_url']
+ servers_url = base_rsp['servers_url']
+ login_url = base_rsp['login_url']
+ token_url = base_rsp['token_url']
+ # See what servers we can log in to
+ servers_rsp = _get_response_json(django_client, servers_url, {})
+ server_id = servers_rsp['data'][0]['id']
+ # Need a CSRF token
+ token_rsp = _get_response_json(django_client, token_url, {})
+ token = token_rsp['data']
+ # Can also get this from our session cookies
+ csrf_token = django_client.cookies['csrftoken'].value
+ assert token == csrf_token
+ # Now we have all info we need for login.
+ # Set the header, so we don't need to do this for every POST/PUT/DELETE
+ # OR we could add it to each POST as 'csrfmiddlewaretoken'
+ django_client = Client(HTTP_X_CSRFTOKEN=token)
+ data = {
+ 'username': username,
+ 'password': password,
+ 'server': server_id,
+ # 'csrfmiddlewaretoken': token,
+ }
+ login_rsp = django_client.post(login_url, data)
+ login_json = json.loads(login_rsp.content)
+ assert login_json['success']
+ event_context = login_json['eventContext']
+ # eventContext gives a bunch of info
+ member_of_groups = event_context['memberOfGroups']
+ current_group = event_context['groupId']
+ user_id = event_context['userId']
+ assert len(member_of_groups) == 2 # includes 'user' group
+ assert current_group in member_of_groups
+ assert user_id > 0
diff --git a/components/tools/OmeroWeb/test/integration/test_api_projects.py b/components/tools/OmeroWeb/test/integration/test_api_projects.py
new file mode 100644
index 00000000000..76c9f7d606b
--- /dev/null
+++ b/components/tools/OmeroWeb/test/integration/test_api_projects.py
@@ -0,0 +1,597 @@
+#!/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 .
+
+"""
+Tests querying & editing Projects with webgateway json api
+"""
+
+from weblibrary import IWebTest, _get_response_json, \
+ _csrf_post_json, _csrf_put_json, _csrf_delete_response_json
+from django.core.urlresolvers import reverse
+from django.conf import settings
+from django.test import Client
+import pytest
+from omero.gateway import BlitzGateway
+from omero.model import ProjectI, DatasetI
+from omero.rtypes import unwrap, rstring
+from omero_marshal import get_encoder, OME_SCHEMA_URL
+
+
+def get_update_service(user):
+ """
+ Get the update_service for the given user's client
+ """
+ return user[0].getSession().getUpdateService()
+
+
+def lower_or_none(x):
+ """ Lower the case or `None`"""
+ if x is not None:
+ return x.lower()
+ return None
+
+
+def cmp_name_insensitive(x, y):
+ """Case-insensitive name comparator."""
+ return cmp(lower_or_none(unwrap(x.name)),
+ lower_or_none(unwrap(y.name)))
+
+
+def get_connection(user, group_id=None):
+ """
+ Get a BlitzGateway connection for the given user's client
+ """
+ connection = BlitzGateway(client_obj=user[0])
+ # Refresh the session context
+ connection.getEventContext()
+ if group_id is not None:
+ connection.SERVICE_OPTS.setOmeroGroup(group_id)
+ return connection
+
+
+# Some names
+@pytest.fixture(scope='module')
+def names1(request):
+ return ('Apple', 'bat')
+
+
+@pytest.fixture(scope='module')
+def names2(request):
+ return ('Axe',)
+
+
+@pytest.fixture(scope='module')
+def names3(request):
+ return ('Bark', 'custard')
+
+
+# Projects
+@pytest.fixture(scope='function')
+def projects_user1_group1(request, names1, user1,
+ project_hierarchy_user1_group1):
+ """
+ Returns new OMERO Projects with required fields set and with names
+ that can be used to exercise sorting semantics.
+ """
+ to_save = []
+ for name in names1:
+ project = ProjectI()
+ project.name = rstring(name)
+ to_save.append(project)
+ projects = get_update_service(user1).saveAndReturnArray(to_save)
+ projects.extend(project_hierarchy_user1_group1[:2])
+ projects.sort(cmp_name_insensitive)
+ return projects
+
+
+@pytest.fixture(scope='function')
+def projects_user2_group1(request, names2, user2):
+ """
+ Returns a new OMERO Project with required fields set and with a name
+ that can be used to exercise sorting semantics.
+ """
+ to_save = []
+ for name in names2:
+ project = ProjectI()
+ project.name = rstring(name)
+ to_save.append(project)
+ projects = get_update_service(user2).saveAndReturnArray(
+ to_save)
+ projects.sort(cmp_name_insensitive)
+ return projects
+
+
+@pytest.fixture(scope='function')
+def projects_user1_group2(request, names3, user1, group2):
+ """
+ Returns new OMERO Projects with required fields set and with names
+ that can be used to exercise sorting semantics.
+ """
+ to_save = []
+ for name in names3:
+ project = ProjectI()
+ project.name = rstring(name)
+ to_save.append(project)
+ conn = get_connection(user1, group2.id.val)
+ projects = conn.getUpdateService().saveAndReturnArray(to_save,
+ conn.SERVICE_OPTS)
+ projects.sort(cmp_name_insensitive)
+ return projects
+
+
+@pytest.fixture(scope='function')
+def projects_user1(request, projects_user1_group1,
+ projects_user1_group2):
+ """
+ Returns OMERO Projects for user1 in both group1 and group2
+ """
+ projects = projects_user1_group1 + projects_user1_group2
+ projects.sort(cmp_name_insensitive)
+ return projects
+
+
+def marshal_objects(objects):
+ """ Marshal objects using omero_marshal """
+ expected = []
+ for obj in objects:
+ encoder = get_encoder(obj.__class__)
+ expected.append(encoder.encode(obj))
+ return expected
+
+
+def assert_objects(conn, json_objects, omero_ids_objects, dtype="Project",
+ group='-1'):
+ """
+ Load objects from OMERO, via conn.getObjects(), marshal with
+ omero_marshal and compare with json_objects.
+ omero_ids_objects can be IDs or list of omero.model objects.
+ """
+ pids = []
+ for p in omero_ids_objects:
+ try:
+ pids.append(long(p))
+ except TypeError:
+ pids.append(p.id.val)
+ conn.SERVICE_OPTS.setOmeroGroup(group)
+ projects = conn.getObjects(dtype, pids, respect_order=True)
+ projects = [p._obj for p in projects]
+ expected = marshal_objects(projects)
+ assert len(json_objects) == len(expected)
+ for o1, o2 in zip(json_objects, expected):
+ assert o1 == o2
+
+
+class TestProjects(IWebTest):
+ """
+ Tests querying & editing Projects
+ """
+
+ # Create a read-annotate group
+ @pytest.fixture(scope='function')
+ def group1(self):
+ """Returns a new read-only group."""
+ return self.new_group(perms='rwra--')
+
+ # Create a read-only group
+ @pytest.fixture(scope='function')
+ def group2(self):
+ """Returns a new read-only group."""
+ return self.new_group(perms='rwr---')
+
+ # Create users in the read-only group
+ @pytest.fixture()
+ def user1(self, group1, group2):
+ """Returns a new user in the group1 group and also add to group2"""
+ user = self.new_client_and_user(group=group1)
+ self.add_groups(user[1], [group2])
+ return user
+
+ @pytest.fixture()
+ def user2(self, group1):
+ """Returns another new user in the read-only group."""
+ return self.new_client_and_user(group=group1)
+
+ @pytest.fixture()
+ def project_hierarchy_user1_group1(self, user1):
+ """
+ Returns OMERO Projects with Dataset Children with Image Children
+
+ Note: This returns a list of mixed objects in a specified order
+ """
+
+ # Create and name all the objects
+ project1 = ProjectI()
+ project1.name = rstring('Project1')
+ project2 = ProjectI()
+ project2.name = rstring('Project2')
+ dataset1 = DatasetI()
+ dataset1.name = rstring('Dataset1')
+ dataset2 = DatasetI()
+ dataset2.name = rstring('Dataset2')
+ image1 = self.new_image(name='Image1')
+ image2 = self.new_image(name='Image2')
+
+ # Link them together like so:
+ # project1
+ # dataset1
+ # image1
+ # image2
+ # dataset2
+ # image2
+ # project2
+ # dataset2
+ # image2
+ project1.linkDataset(dataset1)
+ project1.linkDataset(dataset2)
+ project2.linkDataset(dataset2)
+ dataset1.linkImage(image1)
+ dataset1.linkImage(image2)
+ dataset2.linkImage(image2)
+
+ to_save = [project1, project2]
+ projects = get_update_service(user1).saveAndReturnArray(to_save)
+ projects.sort(cmp_name_insensitive)
+
+ datasets = projects[0].linkedDatasetList()
+ datasets.sort(cmp_name_insensitive)
+
+ images = datasets[0].linkedImageList()
+ images.sort(cmp_name_insensitive)
+
+ return projects + datasets + images
+
+ def test_marshal_projects_not_logged_in(self):
+ """
+ Test marshalling projects without log-in
+ """
+ django_client = Client()
+ version = settings.API_VERSIONS[-1]
+ request_url = reverse('api_projects', kwargs={'api_version': version})
+ rsp = _get_response_json(django_client, request_url, {},
+ status_code=403)
+ assert rsp['message'] == "Not logged in"
+
+ def test_marshal_projects_no_results(self, user1):
+ """
+ Test marshalling projects where there are none
+ """
+ conn = get_connection(user1)
+ user_name = conn.getUser().getName()
+ django_client = self.new_django_client(user_name, user_name)
+ version = settings.API_VERSIONS[-1]
+ request_url = reverse('api_projects', kwargs={'api_version': version})
+ rsp = _get_response_json(django_client, request_url, {})
+ assert rsp['data'] == []
+
+ def test_marshal_projects_user(self, user1, projects_user1_group1):
+ """
+ Test marshalling user's own projects in current group
+ """
+ conn = get_connection(user1)
+ user_name = conn.getUser().getName()
+ django_client = self.new_django_client(user_name, user_name)
+ version = settings.API_VERSIONS[-1]
+ request_url = reverse('api_projects', kwargs={'api_version': version})
+ rsp = _get_response_json(django_client, request_url, {})
+ # Reload projects with group '-1' to get same 'canLink' perms
+ # on owner and group permissions
+ assert_objects(conn, rsp['data'], projects_user1_group1)
+
+ def test_marshal_projects_another_user(self, user1, user2,
+ projects_user2_group1):
+ """
+ Test marshalling another user's projects in current group
+ Project is Owned by user2. We are testing user1's perms.
+ """
+ conn = get_connection(user1)
+ user_name = conn.getUser().getName()
+ django_client = self.new_django_client(user_name, user_name)
+ version = settings.API_VERSIONS[-1]
+ request_url = reverse('api_projects', kwargs={'api_version': version})
+ rsp = _get_response_json(django_client, request_url, {})
+ # user1 reloads user2's projects
+ assert_objects(conn, rsp['data'], projects_user2_group1)
+
+ def test_marshal_projects_another_group(self, user1, group2,
+ projects_user1_group2):
+ """
+ Test marshalling user's projects in another group
+ """
+ conn = get_connection(user1)
+ user_name = conn.getUser().getName()
+ django_client = self.new_django_client(user_name, user_name)
+ version = settings.API_VERSIONS[-1]
+ request_url = reverse('api_projects', kwargs={'api_version': version})
+ rsp = _get_response_json(django_client, request_url, {})
+
+ # Group 1 is rwra-- Group 2 is rwr--
+ # user1 reloads projects with group '-1' so that permissions on owner
+ # are same as owner's default group Group 1 (rwra--) instead of
+ # group that the data is in Group 2 (rwr--)
+ assert_objects(conn, rsp['data'], projects_user1_group2)
+
+ def test_marshal_projects_all_groups(self, user1, group1, group2,
+ projects_user1):
+ """
+ Test marshalling all projects for a user regardless of group and
+ filtering by group.
+ """
+ conn = get_connection(user1)
+ user_name = conn.getUser().getName()
+ django_client = self.new_django_client(user_name, user_name)
+ version = settings.API_VERSIONS[-1]
+ request_url = reverse('api_projects', kwargs={'api_version': version})
+
+ # All groups
+ rsp = _get_response_json(django_client, request_url, {})
+ assert_objects(conn, rsp['data'], projects_user1)
+ # Filter by group A...
+ gid = group1.id.val
+ rsp = _get_response_json(django_client, request_url, {'group': gid})
+ assert_objects(conn, rsp['data'], projects_user1, group=gid)
+ # ...and group B
+ gid = group2.id.val
+ rsp = _get_response_json(django_client, request_url, {'group': gid})
+ assert_objects(conn, rsp['data'], projects_user1, group=gid)
+
+ def test_marshal_projects_all_users(self, user1, user2,
+ projects_user1_group1,
+ projects_user2_group1):
+ """
+ Test marshalling all projects for a group regardless of owner
+ and filtering by owner.
+ """
+ projects = projects_user1_group1 + projects_user2_group1
+ projects.sort(cmp_name_insensitive)
+ conn = get_connection(user1)
+ user_name = conn.getUser().getName()
+ django_client = self.new_django_client(user_name, user_name)
+ version = settings.API_VERSIONS[-1]
+ request_url = reverse('api_projects', kwargs={'api_version': version})
+
+ # Both users
+ rsp = _get_response_json(django_client, request_url, {})
+ assert_objects(conn, rsp['data'], projects)
+
+ eid = user1[1].id.val
+ rsp = _get_response_json(django_client, request_url, {'owner': eid})
+ assert_objects(conn, rsp['data'], projects_user1_group1)
+
+ eid = user2[1].id.val
+ rsp = _get_response_json(django_client, request_url, {'owner': eid})
+ assert_objects(conn, rsp['data'], projects_user2_group1)
+
+ def test_marshal_projects_pagination(self, user1, user2,
+ projects_user1_group1,
+ projects_user2_group1):
+ """
+ Test pagination of projects
+ """
+ projects = projects_user1_group1 + projects_user2_group1
+ projects.sort(cmp_name_insensitive)
+ conn = get_connection(user1)
+ user_name = conn.getUser().getName()
+ django_client = self.new_django_client(user_name, user_name)
+ version = settings.API_VERSIONS[-1]
+ request_url = reverse('api_projects', kwargs={'api_version': version})
+
+ # First page, just 2 projects. Page = 1 by default
+ limit = 2
+ rsp = _get_response_json(django_client, request_url, {'limit': limit})
+ assert len(rsp['data']) == limit
+ assert_objects(conn, rsp['data'], projects[0:limit])
+
+ # Check that page 2 gives next 2 projects
+ page = 2
+ payload = {'limit': limit, 'page': page}
+ rsp = _get_response_json(django_client, request_url, payload)
+ assert_objects(conn, rsp['data'], projects[limit:limit * page])
+
+ def test_marshal_projects_params(self, user1, user2,
+ projects_user1_group1,
+ projects_user2_group1):
+ """
+ Tests normalize, childCount and callback params of projects
+ """
+ projects = projects_user1_group1 + projects_user2_group1
+ projects.sort(cmp_name_insensitive)
+ conn = get_connection(user1)
+ user_name = conn.getUser().getName()
+ django_client = self.new_django_client(user_name, user_name)
+ version = settings.API_VERSIONS[-1]
+ request_url = reverse('api_projects', kwargs={'api_version': version})
+
+ # Test 'childCount' parameter
+ payload = {'childCount': 'true'}
+ rsp = _get_response_json(django_client, request_url, payload)
+ childCounts = [p['omero:childCount'] for p in rsp['data']]
+ assert childCounts == [0, 0, 0, 2, 1]
+
+ # make dict of owners and groups to use in next test...
+ owners = {}
+ groups = {}
+ for p in rsp['data']:
+ details = p['omero:details']
+ owner = details['owner']
+ group = details['group']
+ owners[owner['@id']] = owner
+ groups[group['@id']] = group
+
+ # Test 'normalize' parameter.
+ payload = {'normalize': 'true'}
+ rsp = _get_response_json(django_client, request_url, payload)
+ for p in rsp['data']:
+ details = p['omero:details']
+ owner = details['owner']
+ group = details['group']
+ # owner and group have @id only
+ assert owner.keys() == ['@id']
+ assert group.keys() == ['@id']
+ # check normaliszed owners and groups are same as before
+ rsp_owners = {}
+ for o in rsp['experimenters']:
+ rsp_owners[o['@id']] = o
+ rsp_groups = {}
+ for g in rsp['experimenterGroups']:
+ rsp_groups[g['@id']] = g
+ assert owners == rsp_owners
+ assert groups == rsp_groups
+
+ def test_project_create_read(self):
+ """
+ Tests creation by POST to /save and reading with GET of /project/:id/
+ """
+ django_client = self.django_root_client
+ group = self.ctx.groupId
+ version = settings.API_VERSIONS[-1]
+ # Need to get the Schema url to create @type
+ base_url = reverse('api_base', kwargs={'api_version': version})
+ rsp = _get_response_json(django_client, base_url, {})
+ schema_url = rsp['schema_url']
+ # specify group via query params
+ save_url = "%s?group=%s" % (rsp['save_url'], group)
+ projects_url = rsp['projects_url']
+ project_name = 'test_api_projects'
+ payload = {'Name': project_name,
+ '@type': schema_url + '#Project'}
+ rsp = _csrf_post_json(django_client, save_url, payload,
+ status_code=201)
+ # We get the complete new Project returned
+ assert rsp['Name'] == project_name
+ project_id = rsp['@id']
+
+ # Read Project
+ project_url = "%s%s/" % (projects_url, project_id)
+ rsp = _get_response_json(django_client, project_url, {})
+ assert rsp['@id'] == project_id
+ conn = BlitzGateway(client_obj=self.root)
+ assert_objects(conn, [rsp], [project_id])
+
+ def test_project_create_other_group(self, user1, projects_user1_group2):
+ """
+ Test saving to non-default group
+ """
+ conn = get_connection(user1)
+ user_name = conn.getUser().getName()
+ django_client = self.new_django_client(user_name, user_name)
+ version = settings.API_VERSIONS[-1]
+ # We're only using projects_user1_group2 to get group2 id
+ group2_id = projects_user1_group2[0].getDetails().group.id.val
+ # This seems to be the minimum details needed to pass group ID
+ group2_details = {'group': {
+ '@id': group2_id,
+ '@type': OME_SCHEMA_URL + '#ExperimenterGroup'
+ },
+ '@type': 'TBD#Details'}
+ save_url = reverse('api_save', kwargs={'api_version': version})
+ project_name = 'test_project_create_group'
+ payload = {'Name': project_name,
+ '@type': OME_SCHEMA_URL + '#Project'}
+ # Saving fails with NO group specified
+ rsp = _csrf_post_json(django_client, save_url, payload,
+ status_code=400)
+ assert rsp['message'] == ("Specify Group in omero:details or "
+ "query parameters ?group=:id")
+ # Add group details and try again
+ payload['omero:details'] = group2_details
+ rsp = _csrf_post_json(django_client, save_url, payload,
+ status_code=201)
+ new_project_id = rsp['@id']
+ assert rsp['omero:details']['group']['@id'] == group2_id
+ # Read Project
+ project_url = reverse('api_project', kwargs={'api_version': version,
+ 'pid': new_project_id})
+ rsp = _get_response_json(django_client, project_url, {})
+ assert rsp['omero:details']['group']['@id'] == group2_id
+
+ def test_project_update(self, user1):
+ conn = get_connection(user1)
+ group = conn.getEventContext().groupId
+ user_name = conn.getUser().getName()
+ django_client = self.new_django_client(user_name, user_name)
+
+ project = ProjectI()
+ project.name = rstring('test_project_update')
+ project.description = rstring('Test update')
+ project = get_update_service(user1).saveAndReturnObject(project)
+
+ # Update Project in 2 ways...
+ version = settings.API_VERSIONS[-1]
+ project_url = reverse('api_project', kwargs={'api_version': version,
+ 'pid': project.id.val})
+ save_url = reverse('api_save', kwargs={'api_version': version})
+ # 1) Get Project, update and save back
+ project_json = _get_response_json(django_client, project_url, {})
+ assert project_json['Name'] == 'test_project_update'
+ project_json['Name'] = 'new name'
+ rsp = _csrf_put_json(django_client, save_url, project_json)
+ assert rsp['@id'] == project.id.val
+ assert rsp['Name'] == 'new name' # Name has changed
+ assert rsp['Description'] == 'Test update' # No change
+
+ # 2) Put from scratch (will delete empty fields, E.g. Description)
+ save_url += '?group=' + str(group)
+ payload = {'Name': 'updated name',
+ '@id': project.id.val}
+ # Test error message if we don't pass @type:
+ rsp = _csrf_put_json(django_client, save_url, payload, status_code=400)
+ assert rsp['message'] == 'Need to specify @type attribute'
+ # Add @type and try again
+ payload['@type'] = project_json['@type']
+ rsp = _csrf_put_json(django_client, save_url, payload)
+ assert rsp['@id'] == project.id.val
+ assert rsp['Name'] == 'updated name'
+ assert 'Description' not in rsp
+ # Get project again to check update
+ pr_json = _get_response_json(django_client, project_url, {})
+ assert pr_json['Name'] == 'updated name'
+ assert 'Description' not in pr_json
+
+ def test_project_delete(self, user1):
+ conn = get_connection(user1)
+ user_name = conn.getUser().getName()
+ django_client = self.new_django_client(user_name, user_name)
+
+ project = ProjectI()
+ project.name = rstring('test_project_delete')
+ project.description = rstring('Test update')
+ project = get_update_service(user1).saveAndReturnObject(project)
+ version = settings.API_VERSIONS[-1]
+ project_url = reverse('api_project', kwargs={'api_version': version,
+ 'pid': project.id.val})
+ # Before delete, we can read
+ pr_json = _get_response_json(django_client, project_url, {})
+ assert pr_json['Name'] == 'test_project_delete'
+ # Delete
+ _csrf_delete_response_json(django_client, project_url, {})
+ # Get should now return 404
+ rsp = _get_response_json(django_client, project_url, {},
+ status_code=404)
+ assert rsp['message'] == 'Project %s not found' % project.id.val
+ # Delete (again) should return 404
+ rsp = _csrf_delete_response_json(django_client, project_url, {},
+ status_code=404)
+ assert rsp['message'] == 'Project %s not found' % project.id.val
+ save_url = reverse('api_save', kwargs={'api_version': version})
+ # TODO: Try to save deleted object - should return ApiException
+ # see https://trello.com/c/qWNt9vLN/178-save-deleted-object
+ with pytest.raises(AssertionError):
+ rsp = _csrf_put_json(django_client, save_url, pr_json,
+ status_code=400)
+ assert rsp['message'] == 'Project %s not found' % project.id.val
diff --git a/components/tools/OmeroWeb/test/integration/test_login.py b/components/tools/OmeroWeb/test/integration/test_login.py
index 14d915bd495..9d103cdcd32 100644
--- a/components/tools/OmeroWeb/test/integration/test_login.py
+++ b/components/tools/OmeroWeb/test/integration/test_login.py
@@ -21,32 +21,67 @@
Tests webclient login
"""
-from weblibrary import IWebTest, _csrf_post_response
+from weblibrary import IWebTest, _csrf_post_response, _get_response
from django.core.urlresolvers import reverse
+from django.test import Client
from random import random
import pytest
+tag_url = reverse('load_template', kwargs={'menu': 'usertags'})
+
class TestLogin(IWebTest):
"""
Tests login
"""
- @pytest.mark.parametrize("login", [['guest', 'guest'],
- ['g', str(random())]])
- def test_guest_login_not_supported(self, login):
+ @pytest.mark.parametrize("credentials", [
+ [{'username': 'guest', 'password': 'fake', 'server': 1},
+ "Guest account is not supported."],
+ [{'username': 'nobody', 'password': '', 'server': 1},
+ "This field is required."],
+ [{'password': 'fake'},
+ "This field is required."],
+ [{'username': 'g', 'password': str(random()), 'server': 1},
+ "please check your user name and password."]
+ ])
+ def test_login_errors(self, credentials):
"""
- Test that guest login is not permitted and login as 'g' is not
- confused as 'guest':
- https://trello.com/c/U47AiD1R/682-weird-guest-login
+ Tests handling of various login errors.
+ E.g. missing fields, invalid credentials and guest login
"""
django_client = self.django_root_client
request_url = reverse('weblogin')
- data = {'username': login[0], 'password': login[1], 'server': 1}
+ data = credentials[0]
+ data['server'] = 1
+ message = credentials[1]
rsp = _csrf_post_response(django_client, request_url, data,
status_code=200)
- if login[0] == 'guest':
- assert "Guest account is not supported." in rsp.content
- else:
- assert "Guest account is not supported." not in rsp.content
- assert "please check your user name and password" in rsp.content
+ assert message in rsp.content
+
+ def test_get_login_page(self):
+ """
+ Simply test if a GET of the login url returns login page
+ """
+ django_client = Client()
+ request_url = reverse('weblogin')
+ rsp = _get_response(django_client, request_url, {}, status_code=200)
+ assert 'OMERO.web - Login' in rsp.content
+
+ @pytest.mark.parametrize("redirect", ['', tag_url])
+ def test_login_redirect(self, redirect):
+ """
+ Test that a successful login redirects to /webclient/
+ or to specified url
+ """
+ django_client = self.django_root_client
+ # redirect = reverse('load_template', kwargs={'menu': 'usertags'})
+ request_url = "%s?url=%s" % (reverse('weblogin'), redirect)
+ data = {'username': self.ctx.userName,
+ 'password': self.ctx.userName,
+ 'server': 1}
+ rsp = _csrf_post_response(django_client, request_url, data,
+ status_code=302)
+ if len(redirect) == 0:
+ redirect = reverse('webindex')
+ assert rsp['Location'].endswith(redirect)
diff --git a/components/tools/OmeroWeb/test/integration/weblibrary.py b/components/tools/OmeroWeb/test/integration/weblibrary.py
index 38be6a0b021..cf8ff6d2d0b 100644
--- a/components/tools/OmeroWeb/test/integration/weblibrary.py
+++ b/components/tools/OmeroWeb/test/integration/weblibrary.py
@@ -22,6 +22,7 @@
"""
import library as lib
+import json
from django.test import Client
from django.test.client import MULTIPART_CONTENT
@@ -108,6 +109,15 @@ def _post_response(django_client, request_url, data, status_code=403,
**extra)
+def _post_response_json(django_client, request_url, data, status_code=403,
+ content_type=MULTIPART_CONTENT, **extra):
+ rsp = _response(django_client, request_url, method='post', data=data,
+ status_code=status_code, content_type=content_type,
+ **extra)
+ assert rsp.get('Content-Type') == 'application/json'
+ return json.loads(rsp.content)
+
+
def _csrf_post_response(django_client, request_url, data, status_code=200,
content_type=MULTIPART_CONTENT):
csrf_token = django_client.cookies['csrftoken'].value
@@ -117,6 +127,43 @@ def _csrf_post_response(django_client, request_url, data, status_code=200,
**extra)
+def _csrf_post_response_json(django_client, request_url,
+ query_string, status_code=200):
+ rsp = _csrf_post_response(django_client, request_url,
+ query_string, status_code)
+ assert rsp.get('Content-Type') == 'application/json'
+ return json.loads(rsp.content)
+
+
+# POST json encoded as a string
+def _csrf_post_json(django_client, request_url, data,
+ status_code=200, content_type='application/json'):
+ csrf_token = django_client.cookies['csrftoken'].value
+ extra = {'HTTP_X_CSRFTOKEN': csrf_token}
+ rsp = django_client.post(request_url, json.dumps(data),
+ status_code=status_code,
+ content_type=content_type,
+ **extra)
+ print rsp
+ assert rsp.status_code == status_code
+ assert rsp.get('Content-Type') == 'application/json'
+ return json.loads(rsp.content)
+
+
+# PUT json encoded as a string
+def _csrf_put_json(django_client, request_url, data,
+ status_code=200, content_type='application/json'):
+ csrf_token = django_client.cookies['csrftoken'].value
+ extra = {'HTTP_X_CSRFTOKEN': csrf_token}
+ rsp = django_client.put(request_url, json.dumps(data),
+ status_code=status_code, content_type=content_type,
+ **extra)
+ print rsp
+ assert rsp.status_code == status_code
+ assert rsp.get('Content-Type') == 'application/json'
+ return json.loads(rsp.content)
+
+
# DELETE
def _delete_response(django_client, request_url, data, status_code=403,
content_type=MULTIPART_CONTENT, **extra):
@@ -134,11 +181,18 @@ def _csrf_delete_response(django_client, request_url, data, status_code=200,
**extra)
+def _csrf_delete_response_json(django_client, request_url,
+ data, status_code=200):
+ rsp = _csrf_delete_response(django_client, request_url, data, status_code)
+ assert rsp.get('Content-Type') == 'application/json'
+ return json.loads(rsp.content)
+
+
# GET
def _get_response(django_client, request_url, query_string, status_code=405):
query_string = urlencode(query_string.items())
response = django_client.get('%s?%s' % (request_url, query_string))
- assert response.status_code == status_code, response
+ assert response.status_code == status_code
return response
@@ -148,3 +202,10 @@ def _csrf_get_response(django_client, request_url, query_string,
query_string['csrfmiddlewaretoken'] = csrf_token
return _get_response(django_client, request_url, query_string,
status_code)
+
+
+def _get_response_json(django_client, request_url,
+ query_string, status_code=200):
+ rsp = _get_response(django_client, request_url, query_string, status_code)
+ assert rsp.get('Content-Type') == 'application/json'
+ return json.loads(rsp.content)
diff --git a/examples/Training/python/Json_Api/Login.py b/examples/Training/python/Json_Api/Login.py
new file mode 100644
index 00000000000..ada738c3ada
--- /dev/null
+++ b/examples/Training/python/Json_Api/Login.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+#
+# Copyright (C) 2016 University of Dundee & Open Microscopy Environment.
+# All Rights Reserved.
+# Use is subject to license terms supplied in LICENSE.txt
+#
+
+import requests
+
+from Parse_OMERO_Properties import USERNAME, PASSWORD, OMERO_WEB_HOST
+
+session = requests.Session()
+
+# Start by getting supported versions from the base url...
+r = session.get('%s/api/' % OMERO_WEB_HOST)
+# we get a list of versions
+versions = r.json()['data']
+print 'Versions', versions
+
+# use most recent version...
+version = versions[-1]
+# get the 'base' url
+base_url = version['base_url']
+r = session.get(base_url)
+# which lists a bunch of urls as starting points
+urls = r.json()
+servers_url = urls['servers_url']
+login_url = urls['login_url']
+projects_url = urls['projects_url']
+save_url = urls['save_url']
+schema_url = urls['schema_url']
+
+# To login we need to get CSRF token
+token_url = urls['token_url']
+token = session.get(token_url).json()['data']
+print 'CSRF token', token
+# We add this to our session header
+# Needed for all POST, PUT, DELETE requests
+session.headers.update({'X-CSRFToken': token,
+ 'Referer': login_url})
+
+# List the servers available to connect to
+servers = session.get(servers_url).json()['data']
+print 'Servers:'
+for s in servers:
+ print '-id:', s['id']
+ print ' name:', s['server']
+ print ' host:', s['host']
+ print ' port:', s['port']
+# find one called 'omero'
+servers = [s for s in servers if s['server'] == 'omero']
+if len(servers) < 1:
+ print "Found no server called 'omero'"
+server = servers[0]
+
+# Login with username, password and token
+payload = {'username': USERNAME,
+ 'password': PASSWORD,
+ # 'csrfmiddlewaretoken': token, # Using CSRFToken in header instead
+ 'server': server['id']}
+
+r = session.post(login_url, data=payload)
+login_rsp = r.json()
+assert r.status_code == 200
+assert login_rsp['success']
+eventContext = login_rsp['eventContext']
+print 'eventContext', eventContext
+# Can get our 'default' group
+groupId = eventContext['groupId']
+
+# With succesful login, request.session will contain
+# OMERO session details and reconnect to OMERO on
+# each subsequent call...
+
+# List projects:
+# Limit number of projects per page
+payload = {'limit': 2}
+data = session.get(projects_url, params=payload).json()
+assert len(data['data']) < 3
+print "Projects:"
+for p in data['data']:
+ print ' ', p['@id'], p['Name']
+
+# Create a project:
+projType = schema_url + '#Project'
+# Need to specify target group
+url = save_url + '?group=' + str(groupId)
+r = session.post(url, json={'Name': 'API TEST foo', '@type': projType})
+assert r.status_code == 201
+project = r.json()
+project_id = project['@id']
+print 'Created Project:', project_id, project['Name']
+
+# Get project by ID
+project_url = projects_url + str(project_id) + '/'
+r = session.get(project_url)
+project = r.json()
+print project
+
+# Update a project
+project['Name'] = 'API test updated'
+r = session.put(save_url, json=project)
+
+# Delete a project:
+r = session.delete(project_url)
diff --git a/examples/Training/python/Parse_OMERO_Properties.py b/examples/Training/python/Parse_OMERO_Properties.py
index 01b289da149..7efae285dc4 100755
--- a/examples/Training/python/Parse_OMERO_Properties.py
+++ b/examples/Training/python/Parse_OMERO_Properties.py
@@ -24,6 +24,7 @@
PORT = omeroProperties.get('omero.port', 4064)
USERNAME = omeroProperties.get('omero.user')
PASSWORD = omeroProperties.get('omero.pass')
+OMERO_WEB_HOST = "http://localhost"
projectId = omeroProperties.get('omero.projectid')
datasetId = omeroProperties.get('omero.datasetid')
imageId = omeroProperties.get('omero.imageid')