Mercurial > hg > orthanc-tests
view Plugins/WSI/Annotations.py @ 972:d4eacd47d487 default
merged pg-next-1099 -> default
| author | Alain Mazy <am@orthanc.team> |
|---|---|
| date | Fri, 18 Sep 2026 14:47:35 +0200 |
| parents | 7e211998c2e2 |
| children |
line wrap: on
line source
#!/usr/bin/python3 # -*- coding: utf-8 -*- # Orthanc - A Lightweight, RESTful DICOM Store # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics # Department, University Hospital of Liege, Belgium # Copyright (C) 2017-2023 Osimis S.A., Belgium # Copyright (C) 2024-2026 Orthanc Team SRL, Belgium # Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium # # This program is free software: you can redistribute it and/or # modify it under the terms of the GNU 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 # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import argparse import copy import os import pprint import sys import unittest sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'Tests')) from Toolbox import * ## ## Parse the command-line arguments ## parser = argparse.ArgumentParser(description = 'Run the integration tests for the WSI Dicomizer.') parser.add_argument('--server', default = 'localhost', help = 'Address of the Orthanc server to test') parser.add_argument('--rest', type = int, default = 8042, help = 'Port to the REST API') parser.add_argument('--username', default = 'alice', help = 'Username to the REST API') parser.add_argument('--password', default = 'orthanctest', help = 'Password to the REST API') parser.add_argument('--force', help = 'Do not warn the user', action = 'store_true') parser.add_argument('options', metavar = 'N', nargs = '*', help='Arguments to Python unittest') args = parser.parse_args() if not args.force: print(""" WARNING: This test will remove all the content of your Orthanc instance running on %s! Are you sure ["yes" to go on]?""" % args.server) if sys.stdin.readline().strip() != 'yes': print('Aborting...') exit(0) ## ## The tests ## ORTHANC = DefineOrthanc(server = args.server, username = args.username, password = args.password, restPort = args.rest) ALL_USERS = [ 'admin@uclouvain.be', 'instructor@uclouvain.be', 'learner2@uclouvain.be', 'learner@uclouvain.be', ] ALL_PROJECTS = [ '', 'hello', ] def Execute(uri, args = {}, user = 'admin@uclouvain.be'): body = { 'level' : 'Series', 'resource' : 'test' } for (key, value) in args.items(): body[key] = value return DoPost(ORTHANC, uri, body, headers = { 'Mail' : user }) CREATED_USERS = {} for user in ALL_USERS: CREATED_USERS[user] = Execute('/wsi/api/create-standard-user', { 'name' : user }) for project in ALL_PROJECTS: # Enforce the existence as an active user layer = Execute('/wsi/api/create-user-layer', { 'project' : project }, user = user) Execute('/wsi/api/delete-user-layer', { 'layer-id' : layer['id'], 'project' : project }, user = user) class Orthanc(unittest.TestCase): def setUp(self): if (sys.version_info >= (3, 0)): # Remove annoying warnings about unclosed socket in Python 3 import warnings warnings.simplefilter("ignore", ResourceWarning) DropOrthanc(ORTHANC) for user in ALL_USERS: for project in ALL_PROJECTS: layers = Execute('/wsi/api/list-user-layers', { 'project' : project }, user = user) for l in layers['user-layers']: Execute('/wsi/api/delete-user-layer', { 'layer-id' : l['id'], 'project' : project, }, user = user) for l in layers['imported-layers']: Execute('/wsi/api/remove-imported-layer', { 'layer' : l['id'], 'project' : project, }, user = user) def test_permissions(self): info = DoPostRaw(ORTHANC, '/wsi/api/workspace-info', {}) self.assertEqual(403, info[0].status) info = DoPostRaw(ORTHANC, '/wsi/api/workspace-info', {}, headers = { 'Mail' : '' }) self.assertEqual(400, info[0].status) # Bad request info = DoPostRaw(ORTHANC, '/wsi/api/workspace-info', { 'level' : 'Series', 'resource' : 'test' }, headers = { 'Mail' : '' }) self.assertEqual(403, info[0].status) # Guest users cannot access annotations info = DoPost(ORTHANC, '/wsi/api/workspace-info', { 'level' : 'Series', 'resource' : 'test' }, headers = { 'Mail' : 'admin@uclouvain.be' }) self.assertEqual(11, len(info)) self.assertEqual('', info['description']) self.assertEqual('', info['name']) self.assertEqual('', info['project']) self.assertEqual('admin@uclouvain.be', info['user']) self.assertFalse(info['is_learner']) self.assertTrue(info['is_instructor']) self.assertTrue(info['enabled']) self.assertTrue(info['persistent']) self.assertTrue(info['sharing']) self.assertTrue('learner_to_learner_sharing' in info) info = DoPost(ORTHANC, '/wsi/api/workspace-info', { 'level' : 'Series', 'resource' : 'test', 'project' : 'hello', }, headers = { 'Mail' : 'learner@uclouvain.be' }) self.assertEqual(11, len(info)) self.assertEqual('', info['description']) self.assertEqual('', info['name']) self.assertEqual('hello', info['project']) self.assertEqual('learner@uclouvain.be', info['user']) self.assertTrue(info['is_learner']) self.assertFalse(info['is_instructor']) self.assertTrue(info['enabled']) self.assertTrue(info['persistent']) self.assertTrue(info['sharing']) self.assertTrue('learner_to_learner_sharing' in info) def test_create_delete_layers(self): layers = Execute('/wsi/api/list-user-layers') self.assertEqual(2, len(layers)) self.assertTrue('imported-layers' in layers) self.assertTrue('user-layers' in layers) self.assertEqual(0, len(layers['imported-layers'])) self.assertEqual(0, len(layers['user-layers'])) a = Execute('/wsi/api/create-user-layer') self.assertEqual(6, len(a)) self.assertEqual('#e63946', a['color']) self.assertEqual('Default', a['name']) self.assertFalse(a['public']) self.assertEqual(0, len(a['shared_with'])) self.assertTrue(a['visible']) layers = Execute('/wsi/api/list-user-layers') self.assertEqual(0, len(layers['imported-layers'])) self.assertEqual(1, len(layers['user-layers'])) self.assertEqual(a['id'], layers['user-layers'][0]['id']) self.assertEqual(json.dumps(a), json.dumps(layers['user-layers'][0])) b = Execute('/wsi/api/create-user-layer') self.assertEqual(6, len(b)) self.assertEqual('#2a9d8f', b['color']) self.assertEqual('Layer 2', b['name']) self.assertFalse(b['public']) self.assertEqual(0, len(b['shared_with'])) self.assertTrue(b['visible']) c = Execute('/wsi/api/create-user-layer') self.assertEqual(6, len(c)) self.assertEqual('#e9c46a', c['color']) self.assertEqual('Layer 3', c['name']) self.assertFalse(c['public']) self.assertEqual(0, len(c['shared_with'])) self.assertTrue(c['visible']) layers = Execute('/wsi/api/list-user-layers') self.assertEqual(0, len(layers['imported-layers'])) self.assertEqual(3, len(layers['user-layers'])) self.assertEqual(json.dumps(layers), json.dumps(Execute('/wsi/api/list-user-layers', { 'project' : '' }))) Execute('/wsi/api/delete-user-layer', { 'layer-id' : a['id'] }) layers = Execute('/wsi/api/list-user-layers') self.assertEqual(0, len(layers['imported-layers'])) self.assertEqual(2, len(layers['user-layers'])) self.assertRaises(Exception, lambda: Execute('/wsi/api/delete-user-layer', { 'layer-id' : 'nope' })) Execute('/wsi/api/delete-user-layer', { 'layer-id' : c['id'] }) self.assertRaises(Exception, lambda: Execute('/wsi/api/delete-user-layer', { 'layer-id' : c['id'] })) layers = Execute('/wsi/api/list-user-layers') self.assertEqual(0, len(layers['imported-layers'])) self.assertEqual(1, len(layers['user-layers'])) self.assertEqual(b['id'], layers['user-layers'][0]['id']) self.assertEqual(json.dumps(b), json.dumps(layers['user-layers'][0])) def test_multiple_projects(self): def GetNumberOfLayers(project): return len(Execute('/wsi/api/list-user-layers', { 'project' : project }) ['user-layers']) self.assertEqual(0, GetNumberOfLayers('')) self.assertEqual(0, GetNumberOfLayers('hello')) a = Execute('/wsi/api/create-user-layer') self.assertEqual(1, GetNumberOfLayers('')) self.assertEqual(0, GetNumberOfLayers('hello')) b = Execute('/wsi/api/create-user-layer', { 'project' : 'hello' }) self.assertEqual(1, GetNumberOfLayers('')) self.assertEqual(1, GetNumberOfLayers('hello')) Execute('/wsi/api/delete-user-layer', { 'layer-id' : a['id'] }) self.assertEqual(0, GetNumberOfLayers('')) self.assertEqual(1, GetNumberOfLayers('hello')) Execute('/wsi/api/delete-user-layer', { 'layer-id' : b['id'], 'project' : 'hello' }) self.assertEqual(0, GetNumberOfLayers('')) self.assertEqual(0, GetNumberOfLayers('hello')) def test_update_layer(self): user = Execute('/wsi/api/create-standard-user', { 'name' : 'world' }) self.assertEqual(2, len(user)) self.assertEqual('world', user['name']) self.assertEqual(1, user['type']) # This corresponds to int(OrthancWSI::UserId::Type_Standard) in C++ code a = Execute('/wsi/api/create-user-layer') layers = Execute('/wsi/api/list-user-layers') self.assertEqual(json.dumps(a), json.dumps(layers['user-layers'][0])) Execute('/wsi/api/save-user-layer', { 'layer' : a }) self.assertEqual(json.dumps(a), json.dumps(layers['user-layers'][0])) # No change b = copy.copy(a) b['id'] = 'nope' self.assertRaises(Exception, lambda: Execute('/wsi/api/save-user-layer', { 'layer' : b })) self.assertEqual(json.dumps(a), json.dumps(layers['user-layers'][0])) # No change b = { 'id' : a['id'], 'color' : '#112233', 'name' : 'Hello', 'public' : True, 'shared_with' : [ user ], 'visible' : False, } Execute('/wsi/api/save-user-layer', { 'layer' : b }) layers = Execute('/wsi/api/list-user-layers') self.assertEqual(json.dumps(b, sort_keys=True), json.dumps(layers['user-layers'][0], sort_keys=True)) b['color'] = 'invalid' self.assertRaises(Exception, lambda: Execute('/wsi/api/save-user-layer', { 'layer' : b })) def test_search_active_users(self): def UnpackSetOfStandardUsers(users): s = [] for user in users: self.assertEqual(1, user['type']) # This corresponds to int(OrthancWSI::UserId::Type_Standard) in C++ code self.assertFalse(user['name'] in s) s.append(user['name']) return s v = UnpackSetOfStandardUsers(Execute('/wsi/api/search-active-users', { 'query' : '' })) self.assertEqual(3, len(v)) self.assertTrue('instructor@uclouvain.be' in v) self.assertTrue('learner@uclouvain.be' in v) self.assertTrue('learner2@uclouvain.be' in v) v = UnpackSetOfStandardUsers(Execute('/wsi/api/search-active-users', { 'query' : 'l' })) self.assertEqual(3, len(v)) self.assertTrue('instructor@uclouvain.be' in v) self.assertTrue('learner@uclouvain.be' in v) self.assertTrue('learner2@uclouvain.be' in v) v = UnpackSetOfStandardUsers(Execute('/wsi/api/search-active-users', { 'query' : 'lear' })) self.assertEqual(2, len(v)) self.assertTrue('learner@uclouvain.be' in v) self.assertTrue('learner2@uclouvain.be' in v) v = UnpackSetOfStandardUsers(Execute('/wsi/api/search-active-users', { 'query' : 'ins' })) self.assertEqual(1, len(v)) self.assertTrue('instructor@uclouvain.be' in v) v = UnpackSetOfStandardUsers(Execute('/wsi/api/search-active-users', { 'query' : '' }, user = 'instructor@uclouvain.be')) self.assertEqual(3, len(v)) self.assertTrue('admin@uclouvain.be' in v) self.assertTrue('learner@uclouvain.be' in v) self.assertTrue('learner2@uclouvain.be' in v) info = Execute('/wsi/api/workspace-info') v = UnpackSetOfStandardUsers(Execute('/wsi/api/search-active-users', { 'query' : '' }, user = 'learner@uclouvain.be')) if info['learner_to_learner_sharing']: self.assertEqual(3, len(v)) self.assertTrue('admin@uclouvain.be' in v) self.assertTrue('instructor@uclouvain.be' in v) self.assertTrue('learner2@uclouvain.be' in v) else: self.assertEqual(2, len(v)) self.assertTrue('admin@uclouvain.be' in v) self.assertTrue('instructor@uclouvain.be' in v) def test_user_features(self): Execute('/wsi/api/save-user-features', { 'features' : [ # Those are the minimal fields enforced by the plugin, the rest is managed by the JavaScript { 'type' : 'a', 'layer-id' : 'b' }, { 'type' : 'c', 'layer-id' : 'd' } ] }) a = Execute('/wsi/api/load-user-features') ['features'] self.assertEqual(2, len(a)) self.assertEqual('a', a[0]['type']) self.assertEqual('b', a[0]['layer-id']) self.assertEqual('c', a[1]['type']) self.assertEqual('d', a[1]['layer-id']) def test_list_sharing_users(self): def CheckNoSharing(viewer): s = Execute('/wsi/api/list-sharing-users', user = viewer) self.assertEqual(0, len(s)) for i in ALL_USERS: s = Execute('/wsi/api/list-shared-layers', { 'author' : CREATED_USERS[i] }, user = viewer) self.assertEqual(0, len(s)) def CheckSharingUser(expected_author, expected_layer, viewer): s = Execute('/wsi/api/list-sharing-users', user = viewer) self.assertEqual(1, len(s)) self.assertEqual(2, len(s[0])) self.assertEqual(1, s[0]['type']) self.assertEqual(expected_author, s[0]['name']) for i in ALL_USERS: import_body = { 'author' : CREATED_USERS[i], 'layer' : expected_layer, } if i == expected_author: s = Execute('/wsi/api/list-shared-layers', { 'author' : CREATED_USERS[i] }, user = viewer) self.assertEqual(1, len(s)) t = Execute('/wsi/api/import-layer', import_body, user = viewer) Execute('/wsi/api/remove-imported-layer', { 'layer' : expected_layer }, user = viewer) else: s = Execute('/wsi/api/list-shared-layers', { 'author' : CREATED_USERS[i] }, user = viewer) self.assertEqual(0, len(s)) self.assertRaises(Exception, lambda: Execute('/wsi/api/import-layer', import_body, user = viewer)) instructor = CREATED_USERS['instructor@uclouvain.be'] learner = CREATED_USERS['learner@uclouvain.be'] learner2 = CREATED_USERS['learner2@uclouvain.be'] # Test sharing from instructors a = Execute('/wsi/api/create-user-layer', user = 'admin@uclouvain.be') CheckNoSharing('admin@uclouvain.be') CheckNoSharing('instructor@uclouvain.be') CheckNoSharing('learner2@uclouvain.be') CheckNoSharing('learner@uclouvain.be') a['shared_with'] = [] a['public'] = True Execute('/wsi/api/save-user-layer', { 'layer' : a }, user = 'admin@uclouvain.be') CheckSharingUser('admin@uclouvain.be', a['id'], 'instructor@uclouvain.be') CheckSharingUser('admin@uclouvain.be', a['id'], 'learner@uclouvain.be') CheckSharingUser('admin@uclouvain.be', a['id'], 'learner2@uclouvain.be') a['shared_with'] = [] a['public'] = False Execute('/wsi/api/save-user-layer', { 'layer' : a }, user = 'admin@uclouvain.be') CheckNoSharing('instructor@uclouvain.be') CheckNoSharing('learner@uclouvain.be') CheckNoSharing('learner2@uclouvain.be') a['shared_with'] = [ learner ] a['public'] = False Execute('/wsi/api/save-user-layer', { 'layer' : a }, user = 'admin@uclouvain.be') CheckSharingUser('admin@uclouvain.be', a['id'], 'learner@uclouvain.be') CheckNoSharing('instructor@uclouvain.be') CheckNoSharing('learner2@uclouvain.be') a['shared_with'] = [ instructor ] a['public'] = False Execute('/wsi/api/save-user-layer', { 'layer' : a }, user = 'admin@uclouvain.be') CheckSharingUser('admin@uclouvain.be', a['id'], 'instructor@uclouvain.be') CheckNoSharing('learner@uclouvain.be') CheckNoSharing('learner2@uclouvain.be') Execute('/wsi/api/delete-user-layer', { 'layer-id' : a['id'] }, user = 'admin@uclouvain.be') # Test sharing from learners a = Execute('/wsi/api/create-user-layer', user = 'learner@uclouvain.be') CheckNoSharing('admin@uclouvain.be') CheckNoSharing('instructor@uclouvain.be') CheckNoSharing('learner2@uclouvain.be') CheckNoSharing('learner@uclouvain.be') a['shared_with'] = [] a['public'] = True Execute('/wsi/api/save-user-layer', { 'layer' : a }, user = 'learner@uclouvain.be') CheckSharingUser('learner@uclouvain.be', a['id'], 'instructor@uclouvain.be') CheckSharingUser('learner@uclouvain.be', a['id'], 'admin@uclouvain.be') CheckNoSharing('learner2@uclouvain.be') # For learners, "public" means "shared with any instructor" a['shared_with'] = [] a['public'] = False Execute('/wsi/api/save-user-layer', { 'layer' : a }, user = 'learner@uclouvain.be') CheckNoSharing('instructor@uclouvain.be') CheckNoSharing('admin@uclouvain.be') CheckNoSharing('learner2@uclouvain.be') a['shared_with'] = [ learner2 ] a['public'] = False Execute('/wsi/api/save-user-layer', { 'layer' : a }, user = 'learner@uclouvain.be') info = Execute('/wsi/api/workspace-info') if info['learner_to_learner_sharing']: CheckSharingUser('learner@uclouvain.be', a['id'], 'learner2@uclouvain.be') else: CheckNoSharing('learner2@uclouvain.be') CheckNoSharing('instructor@uclouvain.be') CheckNoSharing('admin@uclouvain.be') a['shared_with'] = [ instructor ] a['public'] = False Execute('/wsi/api/save-user-layer', { 'layer' : a }, user = 'learner@uclouvain.be') CheckSharingUser('learner@uclouvain.be', a['id'], 'instructor@uclouvain.be') CheckNoSharing('admin@uclouvain.be') CheckNoSharing('learner2@uclouvain.be') Execute('/wsi/api/delete-user-layer', { 'layer-id' : a['id'] }, user = 'learner@uclouvain.be') def test_import_layer(self): a = Execute('/wsi/api/create-user-layer', user = 'learner@uclouvain.be') a['public'] = True self.assertRaises(Exception, lambda: Execute('/wsi/api/save-user-layer', { 'layer' : a })) Execute('/wsi/api/save-user-layer', { 'layer' : a }, user = 'learner@uclouvain.be') b = Execute('/wsi/api/list-user-layers') self.assertEqual(0, len(b['imported-layers'])) Execute('/wsi/api/import-layer', { 'author' : CREATED_USERS['learner@uclouvain.be'], 'layer' : a['id'], }) b = Execute('/wsi/api/list-user-layers') self.assertEqual(1, len(b['imported-layers'])) imported = b['imported-layers'][0] self.assertEqual(5, len(imported)) self.assertEqual('learner@uclouvain.be', imported['author']['name']) self.assertEqual(1, imported['author']['type']) self.assertEqual('#e63946', imported['color']) self.assertEqual(a['id'], imported['id']) self.assertEqual('Default', imported['name']) self.assertTrue(imported['visible']) Execute('/wsi/api/save-imported-layer', { 'layer' : { 'author' : imported['author'], 'id' : a['id'], 'color' : '#0000ff', 'visible' : False, 'name' : 'Hello', } }) b = Execute('/wsi/api/list-user-layers') self.assertEqual(1, len(b['imported-layers'])) imported = b['imported-layers'][0] self.assertEqual(5, len(imported)) self.assertEqual('learner@uclouvain.be', imported['author']['name']) self.assertEqual(1, imported['author']['type']) self.assertEqual('#0000ff', imported['color']) self.assertEqual(a['id'], imported['id']) self.assertEqual('Hello', imported['name']) self.assertFalse(imported['visible']) Execute('/wsi/api/remove-imported-layer', { 'layer' : a['id'] }) def test_import_features(self): a = Execute('/wsi/api/create-user-layer') a['public'] = True Execute('/wsi/api/save-user-layer', { 'layer' : a }) # Make layer "a" public b = Execute('/wsi/api/create-user-layer') # Layer "b" is private Execute('/wsi/api/import-layer', { 'author' : CREATED_USERS['admin@uclouvain.be'], 'layer' : a['id'], }, user = 'learner@uclouvain.be') c = Execute('/wsi/api/load-imported-features', user = 'learner@uclouvain.be') self.assertEqual(0, len(c['features'])) Execute('/wsi/api/save-user-features', { 'features' : [ # Those are the minimal fields enforced by the plugin, the rest is managed by the JavaScript { 'type' : 'a', 'layer-id' : a['id'] }, { 'type' : 'b', 'layer-id' : b['id'] }, { 'type' : 'c', 'layer-id' : a['id'] } ] }) c = Execute('/wsi/api/load-imported-features', user = 'learner@uclouvain.be') self.assertEqual(2, len(c['features'])) self.assertEqual('a', c['features'][0]['type']) self.assertEqual(a['id'], c['features'][0]['layer-id']) self.assertEqual('c', c['features'][1]['type']) self.assertEqual(a['id'], c['features'][1]['layer-id']) Execute('/wsi/api/save-user-features', { 'features' : [ { 'type' : 'b', 'layer-id' : a['id'] }, ] }) c = Execute('/wsi/api/load-imported-features', user = 'learner@uclouvain.be') self.assertEqual(1, len(c['features'])) self.assertEqual('b', c['features'][0]['type']) self.assertEqual(a['id'], c['features'][0]['layer-id']) Execute('/wsi/api/remove-imported-layer', { 'layer' : a['id'] }, user = 'learner@uclouvain.be') c = Execute('/wsi/api/load-imported-features', user = 'learner@uclouvain.be') self.assertEqual(0, len(c['features'])) try: print('\nStarting the tests...') unittest.main(argv = [ sys.argv[0] ] + args.options) finally: print('\nDone')
