diff --git a/components/tools/OmeroWeb/omeroweb/api/urls.py b/components/tools/OmeroWeb/omeroweb/api/urls.py index eb623dbbaef..f2a77063fd3 100644 --- a/components/tools/OmeroWeb/omeroweb/api/urls.py +++ b/components/tools/OmeroWeb/omeroweb/api/urls.py @@ -75,7 +75,7 @@ """ api_project = url( - r'^v(?P%s)/m/projects/(?P[0-9]+)/$' % versions, + r'^v(?P%s)/m/projects/(?P[0-9]+)/$' % versions, views.ProjectView.as_view(), name='api_project') """ @@ -99,7 +99,7 @@ """ api_dataset = url( - r'^v(?P%s)/m/datasets/(?P[0-9]+)/$' % versions, + r'^v(?P%s)/m/datasets/(?P[0-9]+)/$' % versions, views.DatasetView.as_view(), name='api_dataset') """ @@ -123,7 +123,7 @@ """ api_image = url( - r'^v(?P%s)/m/images/(?P[0-9]+)/$' % versions, + r'^v(?P%s)/m/images/(?P[0-9]+)/$' % versions, views.ImageView.as_view(), name='api_image') """ @@ -131,7 +131,7 @@ """ api_screen = url( - r'^v(?P%s)/m/screens/(?P[0-9]+)/$' % versions, + r'^v(?P%s)/m/screens/(?P[0-9]+)/$' % versions, views.ScreenView.as_view(), name='api_screen') """ @@ -162,7 +162,7 @@ """ api_plate = url( - r'^v(?P%s)/m/plates/(?P[0-9]+)/$' % versions, + r'^v(?P%s)/m/plates/(?P[0-9]+)/$' % versions, views.PlateView.as_view(), name='api_plate') """ diff --git a/components/tools/OmeroWeb/omeroweb/api/views.py b/components/tools/OmeroWeb/omeroweb/api/views.py index 8bd7835bd18..b1b8bcae317 100644 --- a/components/tools/OmeroWeb/omeroweb/api/views.py +++ b/components/tools/OmeroWeb/omeroweb/api/views.py @@ -67,7 +67,7 @@ def api_versions(request, **kwargs): for v in settings.API_VERSIONS: versions.append({ 'version': v, - 'base_url': build_url(request, 'api_base', v) + 'url:base': build_url(request, 'api_base', v) }) return {'data': versions} @@ -76,16 +76,16 @@ def api_versions(request, **kwargs): 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), - 'datasets_url': build_url(request, 'api_datasets', v), - 'images_url': build_url(request, 'api_images', v), - 'screens_url': build_url(request, 'api_screens', v), - 'plates_url': build_url(request, 'api_plates', 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} + rv = {'url:projects': build_url(request, 'api_projects', v), + 'url:datasets': build_url(request, 'api_datasets', v), + 'url:images': build_url(request, 'api_images', v), + 'url:screens': build_url(request, 'api_screens', v), + 'url:plates': build_url(request, 'api_plates', v), + 'url:token': build_url(request, 'api_token', v), + 'url:servers': build_url(request, 'api_servers', v), + 'url:login': build_url(request, 'api_login', v), + 'url:save': build_url(request, 'api_save', v), + 'url:schema': OME_SCHEMA_URL} return rv @@ -111,39 +111,71 @@ def api_servers(request, api_version, **kwargs): return {'data': servers} -class ObjectView(View): - """Handle access to an individual Object to GET or DELETE it.""" +class ApiView(View): + """Base class extended by ObjectView and ObjectsView.""" + + # urls extended by subclasses to add urls to marshalled objects + urls = {} @method_decorator(login_required(useragent='OMERO.webapi')) @method_decorator(json_response()) def dispatch(self, *args, **kwargs): """Wrap other methods to add decorators.""" - return super(ObjectView, self).dispatch(*args, **kwargs) + return super(ApiView, self).dispatch(*args, **kwargs) + + def add_data(self, marshalled, request, **kwargs): + """ + Post-process marshalled object to add any extra data. + + Used to add urls to marshalled json. + Subclasses can configure self.urls to specify urls to add. + See ProjectsView urls as example + """ + object_id = marshalled['@id'] + version = kwargs['api_version'] + for key, args in self.urls.items(): + name = args['name'] + kwargs = args['kwargs'].copy() + # If kwargs has 'OBJECT_ID' placeholder, we replace with id + for k, v in kwargs.items(): + if v == 'OBJECT_ID': + kwargs[k] = object_id + url = build_url(request, name, version, **kwargs) + marshalled[key] = url + return marshalled + + +class ObjectView(ApiView): + """Handle access to an individual Object to GET or DELETE it.""" def get_opts(self, request): """Return a dict for use in conn.getObjects() based on request.""" return {} - def get(self, request, pid, conn=None, **kwargs): + def get(self, request, object_id, conn=None, **kwargs): """Simply GET a single Object and marshal it or 404 if not found.""" opts = self.get_opts(request) - obj = conn.getObject(self.OMERO_TYPE, pid, opts=opts) + obj = conn.getObject(self.OMERO_TYPE, object_id, opts=opts) if obj is None: - raise NotFoundError('%s %s not found' % (self.OMERO_TYPE, pid)) + raise NotFoundError('%s %s not found' % (self.OMERO_TYPE, + object_id)) encoder = get_encoder(obj._obj.__class__) - return encoder.encode(obj._obj) + marshalled = encoder.encode(obj._obj) + self.add_data(marshalled, request, **kwargs) + return marshalled - def delete(self, request, pid, conn=None, **kwargs): + def delete(self, request, object_id, conn=None, **kwargs): """ Delete the Object and return marshal of deleted Object. Return 404 if not found. """ try: - obj = conn.getQueryService().get(self.OMERO_TYPE, long(pid), + obj = conn.getQueryService().get(self.OMERO_TYPE, long(object_id), conn.SERVICE_OPTS) except ValidationException: - raise NotFoundError('%s %s not found' % (self.OMERO_TYPE, pid)) + raise NotFoundError('%s %s not found' % (self.OMERO_TYPE, + object_id)) encoder = get_encoder(obj.__class__) json = encoder.encode(obj) conn.deleteObject(obj) @@ -155,12 +187,24 @@ class ProjectView(ObjectView): OMERO_TYPE = 'Project' + # Urls to add to marshalled object. See ProjectsView for more details + urls = { + 'url:datasets': {'name': 'api_project_datasets', + 'kwargs': {'project_id': 'OBJECT_ID'}}, + } + class DatasetView(ObjectView): """Handle access to an individual Dataset to GET or DELETE it.""" OMERO_TYPE = 'Dataset' + # Urls to add to marshalled object. See ProjectsView for more details + urls = { + 'url:images': {'name': 'api_dataset_images', + 'kwargs': {'dataset_id': 'OBJECT_ID'}}, + } + class ImageView(ObjectView): """Handle access to an individual Image to GET or DELETE it.""" @@ -181,6 +225,12 @@ class ScreenView(ObjectView): OMERO_TYPE = 'Screen' + # Urls to add to marshalled object. See ProjectsView for more details + urls = { + 'url:plates': {'name': 'api_screen_plates', + 'kwargs': {'screen_id': 'OBJECT_ID'}}, + } + class PlateView(ObjectView): """Handle access to an individual Plate to GET or DELETE it.""" @@ -188,15 +238,9 @@ class PlateView(ObjectView): OMERO_TYPE = 'Plate' -class ObjectsView(View): +class ObjectsView(ApiView): """Base class for listing objects.""" - @method_decorator(login_required(useragent='OMERO.webapi')) - @method_decorator(json_response()) - def dispatch(self, *args, **kwargs): - """Use dispatch to add decorators to class methods.""" - return super(ObjectsView, self).dispatch(*args, **kwargs) - def get_opts(self, request, **kwargs): """Return an options dict based on request parameters.""" try: @@ -224,7 +268,11 @@ def get(self, request, conn=None, **kwargs): group = getIntOrDefault(request, 'group', -1) normalize = request.GET.get('normalize', False) == 'true' # Get the data - return query_objects(conn, self.OMERO_TYPE, group, opts, normalize) + marshalled = query_objects(conn, self.OMERO_TYPE, group, + opts, normalize) + for m in marshalled['data']: + self.add_data(m, request, **kwargs) + return marshalled class ProjectsView(ObjectsView): @@ -232,6 +280,17 @@ class ProjectsView(ObjectsView): OMERO_TYPE = 'Project' + # To add a url to marshalled object add to this dict + # 'name' is url name, kwargs are passed to reverse() + # If any kwargs values are 'OBJECT_ID' then this placeholder will be + # filled with the actual project_id + urls = { + 'url:datasets': {'name': 'api_project_datasets', + 'kwargs': {'project_id': 'OBJECT_ID'}}, + 'url:project': {'name': 'api_project', + 'kwargs': {'object_id': 'OBJECT_ID'}} + } + class DatasetsView(ObjectsView): """Handles GET for /datasets/ to list available Datasets.""" @@ -251,12 +310,28 @@ def get_opts(self, request, **kwargs): opts['project'] = project return opts + # Urls to add to marshalled object. See ProjectsView for more details + urls = { + 'url:images': {'name': 'api_dataset_images', + 'kwargs': {'dataset_id': 'OBJECT_ID'}}, + 'url:dataset': {'name': 'api_dataset', + 'kwargs': {'object_id': 'OBJECT_ID'}}, + } + class ScreensView(ObjectsView): """Handles GET for /screens/ to list available Screens.""" OMERO_TYPE = 'Screen' + # Urls to add to marshalled object. See ProjectsView for more details + urls = { + 'url:plates': {'name': 'api_screen_plates', + 'kwargs': {'screen_id': 'OBJECT_ID'}}, + 'url:screen': {'name': 'api_screen', + 'kwargs': {'object_id': 'OBJECT_ID'}} + } + class PlatesView(ObjectsView): """Handles GET for /plates/ to list available Plates.""" @@ -276,12 +351,24 @@ def get_opts(self, request, **kwargs): opts['screen'] = screen return opts + # Urls to add to marshalled object. See ProjectsView for more details + urls = { + 'url:plate': {'name': 'api_plate', + 'kwargs': {'object_id': 'OBJECT_ID'}} + } + class ImagesView(ObjectsView): """Handles GET for /images/ to list available Images.""" OMERO_TYPE = 'Image' + # Urls to add to marshalled object. See ProjectsView for more details + urls = { + 'url:image': {'name': 'api_image', + 'kwargs': {'object_id': 'OBJECT_ID'}}, + } + def get_opts(self, request, **kwargs): """Add filtering by 'dataset' and other params to the opts dict.""" opts = super(ImagesView, self).get_opts(request, **kwargs) diff --git a/components/tools/OmeroWeb/test/integration/test_api_containers.py b/components/tools/OmeroWeb/test/integration/test_api_containers.py index 584ee08a359..eb4a19d73a9 100644 --- a/components/tools/OmeroWeb/test/integration/test_api_containers.py +++ b/components/tools/OmeroWeb/test/integration/test_api_containers.py @@ -50,6 +50,16 @@ def cmp_name_insensitive(x, y): return cmp(unwrap(x.name).lower(), unwrap(y.name).lower()) +def build_url(client, url_name, url_kwargs): + """Build an absolute url using client response url.""" + response = client.request() + # http://testserver/webclient/ + webclient_url = response.url + url = reverse(url_name, kwargs=url_kwargs) + url = webclient_url.replace('/webclient/', url) + return url + + def marshal_objects(objects): """Marshal objects using omero_marshal.""" expected = [] @@ -60,7 +70,7 @@ def marshal_objects(objects): def assert_objects(conn, json_objects, omero_ids_objects, dtype="Project", - group='-1', extra=None): + group='-1', extra=None, opts=None): """ Load objects from OMERO, via conn.getObjects(). @@ -77,13 +87,17 @@ def assert_objects(conn, json_objects, omero_ids_objects, dtype="Project", except TypeError: pids.append(p.id.val) conn.SERVICE_OPTS.setOmeroGroup(group) - objs = conn.getObjects(dtype, pids, respect_order=True) + objs = conn.getObjects(dtype, pids, respect_order=True, opts=opts) objs = [p._obj for p in objs] expected = marshal_objects(objs) assert len(json_objects) == len(expected) for i, o1, o2 in zip(range(len(expected)), json_objects, expected): if extra is not None and i < len(extra): o2.update(extra[i]) + # remove any urls from json, if not in both objects + for key in o1.keys(): + if key.startswith('url:') and key not in o2: + del(o1[key]) assert o1 == o2 @@ -109,8 +123,10 @@ def project_datasets(self, user1): dataset1 = DatasetI() dataset1.name = rstring('Dataset%s' % d) for i in range(d): - image = ImageI() - image.name = rstring('Image%s' % i) + image = self.create_test_image(size_x=5, size_y=5, + session=user1[0].getSession(), + name="Image%s" % i) + image = ImageI(image.id.val, False) dataset1.linkImage(image) project.linkDataset(dataset1) @@ -171,9 +187,9 @@ def test_container_crud(self, dtype): # 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'] + schema_url = rsp['url:schema'] # specify group via query params - save_url = "%s?group=%s" % (rsp['save_url'], group) + save_url = "%s?group=%s" % (rsp['url:save'], group) project_name = 'test_container_create_read' payload = {'Name': project_name, '@type': '%s#%s' % (schema_url, dtype)} @@ -262,10 +278,141 @@ def test_screens(self, user1, user_screens): """Test listing of Screens.""" conn = get_connection(user1) user_name = conn.getUser().getName() - django_client = self.new_django_client(user_name, user_name) + client = self.new_django_client(user_name, user_name) version = settings.API_VERSIONS[-1] request_url = reverse('api_screens', kwargs={'api_version': version}) # List ALL Screens - rsp = _get_response_json(django_client, request_url, {}) - assert_objects(conn, rsp['data'], user_screens, dtype="Screen") + rsp = _get_response_json(client, request_url, {}) + extra = [] + for screen in user_screens: + s_url = build_url(client, 'api_screen', + {'api_version': version, + 'object_id': screen.id.val}) + p_url = build_url(client, 'api_screen_plates', + {'api_version': version, + 'screen_id': screen.id.val}) + extra.append({ + 'url:screen': s_url, + 'url:plates': p_url + }) + assert_objects(conn, rsp['data'], user_screens, + dtype="Screen", extra=extra) + + def test_spw_urls(self, user1, screen_plates): + """Test browsing via urls in json /api/->SPW.""" + conn = get_connection(user1) + user_name = conn.getUser().getName() + client = self.new_django_client(user_name, user_name) + version = settings.API_VERSIONS[-1] + base_url = reverse('api_base', kwargs={'api_version': version}) + base_rsp = _get_response_json(client, base_url, {}) + + # List screens + screen, plate = screen_plates + screens_url = base_rsp['url:screens'] + rsp = _get_response_json(client, screens_url, {}) + screens_json = rsp['data'] + extra = [{ + 'url:screen': build_url(client, 'api_screen', + {'api_version': version, + 'object_id': screen.id.val}), + 'url:plates': build_url(client, 'api_screen_plates', + {'api_version': version, + 'screen_id': screen.id.val}) + }] + assert_objects(conn, screens_json, [screen], dtype='Screen', + extra=extra) + # View single screen + rsp = _get_response_json(client, screens_json[0]['url:screen'], {}) + assert_objects(conn, [rsp], [screen], dtype='Screen', + extra=[{'url:plates': extra[0]['url:plates']}]) + + # List plates + plates_url = screens_json[0]['url:plates'] + plates = screen.linkedPlateList() + plates.sort(cmp_name_insensitive) + rsp = _get_response_json(client, plates_url, {}) + plates_json = rsp['data'] + extra = [] + for p in plates: + extra.append({ + 'url:plate': build_url(client, 'api_plate', + {'api_version': version, + 'object_id': p.id.val}), + }) + assert_objects(conn, plates_json, plates, dtype='Plate', extra=extra) + # View single plate + rsp = _get_response_json(client, plates_json[0]['url:plate'], {}) + assert_objects(conn, [rsp], plates[0:1], dtype='Plate') + + def test_pdi_urls(self, user1, project_datasets): + """Test browsing via urls in json /api/->PDI.""" + conn = get_connection(user1) + user_name = conn.getUser().getName() + client = self.new_django_client(user_name, user_name) + version = settings.API_VERSIONS[-1] + base_url = reverse('api_base', kwargs={'api_version': version}) + base_rsp = _get_response_json(client, base_url, {}) + + # List projects + project, dataset = project_datasets + projects_url = base_rsp['url:projects'] + rsp = _get_response_json(client, projects_url, {}) + projects_json = rsp['data'] + extra = [{ + 'url:project': build_url(client, 'api_project', + {'api_version': version, + 'object_id': project.id.val}), + 'url:datasets': build_url(client, 'api_project_datasets', + {'api_version': version, + 'project_id': project.id.val}) + }] + assert_objects(conn, projects_json, [project], extra=extra) + # View single Project + rsp = _get_response_json(client, projects_json[0]['url:project'], {}) + assert_objects(conn, [rsp], [project], + extra=[{'url:datasets': extra[0]['url:datasets']}]) + + # List datasets + datasets_url = projects_json[0]['url:datasets'] + datasets = project.linkedDatasetList() + datasets.sort(cmp_name_insensitive) + rsp = _get_response_json(client, datasets_url, {}) + datasets_json = rsp['data'] + extra = [] + for d in datasets: + extra.append({ + 'url:dataset': build_url(client, 'api_dataset', + {'api_version': version, + 'object_id': d.id.val}), + 'url:images': build_url(client, 'api_dataset_images', + {'api_version': version, + 'dataset_id': d.id.val}) + }) + assert_objects(conn, datasets_json, datasets, + dtype='Dataset', extra=extra) + # View single Dataset + rsp = _get_response_json(client, datasets_json[0]['url:dataset'], {}) + assert_objects(conn, [rsp], datasets[0:1], dtype='Dataset', + extra=[{'url:images': extra[0]['url:images']}]) + + # List images (from last Dataset) + images_url = datasets_json[-1]['url:images'] + images = datasets[-1].linkedImageList() + images.sort(cmp_name_insensitive) + rsp = _get_response_json(client, images_url, {}) + images_json = rsp['data'] + extra = [] + for i in images: + extra.append({ + 'url:image': build_url(client, 'api_image', + {'api_version': version, + 'object_id': i.id.val}), + }) + assert_objects(conn, images_json, images, + dtype='Image', extra=extra, opts={'load_pixels': True}) + # View single Image + rsp = _get_response_json(client, images_json[0]['url:image'], {}) + assert_objects(conn, [rsp], images[0:1], dtype='Image', + opts={'load_channels': True}) diff --git a/components/tools/OmeroWeb/test/integration/test_api_images.py b/components/tools/OmeroWeb/test/integration/test_api_images.py index 1c66186c99c..e0fadf2a9d6 100644 --- a/components/tools/OmeroWeb/test/integration/test_api_images.py +++ b/components/tools/OmeroWeb/test/integration/test_api_images.py @@ -92,6 +92,10 @@ def assert_objects(conn, json_objects, omero_ids_objects, dtype="Project", # dumping to json and loading (same as test data) means that # unicode has been handled in same way, e.g. Pixel size symbols. o2 = json.loads(json.dumps(o2)) + # remove any urls from json (tested elsewhere) + for key in o1.keys(): + if key.startswith('url:'): + del(o1[key]) assert o1 == o2 diff --git a/components/tools/OmeroWeb/test/integration/test_api_login.py b/components/tools/OmeroWeb/test/integration/test_api_login.py index b40e258b029..3250b09fc42 100644 --- a/components/tools/OmeroWeb/test/integration/test_api_login.py +++ b/components/tools/OmeroWeb/test/integration/test_api_login.py @@ -22,8 +22,8 @@ """ import pytest -from omeroweb.testlib import IWebTest, _get_response_json, _post_response_json, \ - _csrf_post_response_json +from omeroweb.testlib 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 @@ -57,15 +57,15 @@ def test_base_url(self): 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 'datasets_url' in rsp - assert 'images_url' in rsp - assert 'screens_url' in rsp - assert 'plates_url' in rsp - assert 'save_url' in rsp - assert rsp['schema_url'] == OME_SCHEMA_URL + assert 'url:servers' in rsp + assert 'url:login' in rsp + assert 'url:projects' in rsp + assert 'url:datasets' in rsp + assert 'url:images' in rsp + assert 'url:screens' in rsp + assert 'url:plates' in rsp + assert 'url:save' in rsp + assert rsp['url:schema'] == OME_SCHEMA_URL def test_base_url_versions_404(self): """ @@ -147,13 +147,13 @@ def test_login_example(self): rsp = _get_response_json(django_client, request_url, {}) # Pick the last version version = rsp['data'][-1] - base_url = version['base_url'] + base_url = version['url:base'] # 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'] + login_url = base_rsp['url:login'] + servers_url = base_rsp['url:servers'] + login_url = base_rsp['url:login'] + token_url = base_rsp['url:token'] # See what servers we can log in to servers_rsp = _get_response_json(django_client, servers_url, {}) server_id = servers_rsp['data'][0]['id'] diff --git a/components/tools/OmeroWeb/test/integration/test_api_projects.py b/components/tools/OmeroWeb/test/integration/test_api_projects.py index e44f8ee3499..83a867dfc51 100644 --- a/components/tools/OmeroWeb/test/integration/test_api_projects.py +++ b/components/tools/OmeroWeb/test/integration/test_api_projects.py @@ -174,6 +174,10 @@ def assert_objects(conn, json_objects, omero_ids_objects, dtype="Project", expected = marshal_objects(projects) assert len(json_objects) == len(expected) for o1, o2 in zip(json_objects, expected): + # remove any urls from json + for key in o1.keys(): + if key.startswith('url:'): + del(o1[key]) assert o1 == o2 @@ -463,10 +467,10 @@ def test_project_create_read(self): # 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'] + schema_url = rsp['url:schema'] # specify group via query params - save_url = "%s?group=%s" % (rsp['save_url'], group) - projects_url = rsp['projects_url'] + save_url = "%s?group=%s" % (rsp['url:save'], group) + projects_url = rsp['url:projects'] project_name = 'test_api_projects' payload = {'Name': project_name, '@type': schema_url + '#Project'} @@ -515,8 +519,9 @@ def test_project_create_other_group(self, user1, projects_user1_group2): 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}) + project_url = reverse('api_project', + kwargs={'api_version': version, + 'object_id': new_project_id}) rsp = _get_response_json(django_client, project_url, {}) assert rsp['omero:details']['group']['@id'] == group2_id @@ -533,8 +538,9 @@ def test_project_update(self, user1): # Update Project in 2 ways... version = settings.API_VERSIONS[-1] - project_url = reverse('api_project', kwargs={'api_version': version, - 'pid': project.id.val}) + project_url = reverse('api_project', + kwargs={'api_version': version, + 'object_id': 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, {}) @@ -573,8 +579,9 @@ def test_project_delete(self, user1): 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}) + project_url = reverse('api_project', + kwargs={'api_version': version, + 'object_id': project.id.val}) # Before delete, we can read pr_json = _get_response_json(django_client, project_url, {}) assert pr_json['Name'] == 'test_project_delete'