Skip to content

Commit

Permalink
Merge pull request #218 from introlab/dev
Browse files Browse the repository at this point in the history
Main merge for 1.2.3 release
  • Loading branch information
SBriere authored May 31, 2023
2 parents 19582d7 + c9bf259 commit 07b2f54
Show file tree
Hide file tree
Showing 24 changed files with 1,045 additions and 67 deletions.
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
project = 'OpenTera'
copyright = '2023, Simon Brière, Dominic Létourneau'
author = 'Simon Brière, Dominic Létourneau'
release = '1.2.2'
release = '1.2.3'
version = release

html_logo = 'images/LogoOpenTera200px.png'
Expand Down
2 changes: 1 addition & 1 deletion teraserver/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ endif(NOT CMAKE_BUILD_TYPE)
# Software version
SET(OPENTERA_VERSION_MAJOR "1")
SET(OPENTERA_VERSION_MINOR "2")
SET(OPENTERA_VERSION_PATCH "2")
SET(OPENTERA_VERSION_PATCH "3")

SET(OPENTERA_SERVER_VERSION OpenTera_v${OPENTERA_VERSION_MAJOR}.${OPENTERA_VERSION_MINOR}.${OPENTERA_VERSION_PATCH})

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
from flask_restx import Resource
from flask_babel import gettext
from modules.LoginModule.LoginModule import LoginModule, current_service
from modules.FlaskModule.FlaskModule import service_api_ns as api
from modules.DatabaseModule.DBManager import DBManager
from opentera.db.models.TeraSessionTypeProject import TeraSessionTypeProject
from opentera.db.models.TeraSessionTypeSite import TeraSessionTypeSite
from opentera.db.models.TeraSessionType import TeraSessionType
from opentera.db.models.TeraParticipant import TeraParticipant

# Parser definition(s)
get_parser = api.parser()
get_parser.add_argument('id_site', type=int, help='ID of the site to query session types for')
get_parser.add_argument('id_project', type=int, help='ID of the project to query session types for')
get_parser.add_argument('id_participant', type=int, help='ID of the participant to query types for')
get_parser.add_argument('id_session_type', type=int, help='ID of the session type to query')


class ServiceQuerySessionTypes(Resource):
Expand Down Expand Up @@ -47,6 +48,9 @@ def get(self):
part_info = TeraParticipant.get_participant_by_id(args['id_participant'])
session_types = [st.session_type_project_session_type for st in
TeraSessionTypeProject.get_sessions_types_for_project(part_info.id_project)]
elif args['id_session_type']:
if args['id_session_type'] in service_access.get_accessible_sessions_types_ids():
session_types = [TeraSessionType.get_session_type_by_id(args['id_session_type'])]
else:
session_types = service_access.get_accessible_sessions_types()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
from flask import request
from flask_restx import Resource, reqparse, inputs
from modules.LoginModule.LoginModule import current_service, LoginModule
from modules.FlaskModule.FlaskModule import user_api_ns as api
from opentera.db.models.TeraTestTypeProject import TeraTestTypeProject
from opentera.db.models.TeraTestTypeSite import TeraTestTypeSite
from opentera.db.models.TeraTestType import TeraTestType
from opentera.db.models.TeraProject import TeraProject
from modules.DatabaseModule.DBManager import DBManager
from sqlalchemy.exc import InvalidRequestError
from sqlalchemy import exc, inspect
from flask_babel import gettext

# Parser definition(s)
get_parser = api.parser()
get_parser.add_argument('id_project', type=int, help='Project ID to query associated test types from')
get_parser.add_argument('id_test_type', type=int, help='Test type ID to query associated projects from')

get_parser.add_argument('with_sites', type=inputs.boolean, help='Used with id_test_type. Also return site '
'information of the returned projects.')

post_parser = api.parser()
post_schema = api.schema_model('service_test_type_project', {'properties': TeraTestTypeProject.get_json_schema(),
'type': 'object',
'location': 'json'})

delete_parser = reqparse.RequestParser()
delete_parser.add_argument('id', type=int, help='Specific test type - project association ID to delete. '
'Be careful: this is not the test-type or project ID, but the ID'
' of the association itself!', required=True)


class ServiceQueryTestTypeProjects(Resource):

def __init__(self, _api, *args, **kwargs):
Resource.__init__(self, _api, *args, **kwargs)
self.module = kwargs.get('flaskModule', None)
self.test = kwargs.get('test', False)

@api.doc(description='Get test types that are associated with a project. Only one "ID" parameter required and '
'supported at once.',
responses={200: 'Success - returns list of test-types - projects association',
400: 'Required parameter is missing (must have at least one id)',
500: 'Error when getting association'},
params={'token': 'Secret token'})
@api.expect(get_parser)
@LoginModule.service_token_or_certificate_required
def get(self):
service_access = DBManager.serviceAccess(current_service)
args = get_parser.parse_args()

test_type_projects = []
# If we have no arguments, return error
if not any(args.values()):
return gettext('Missing arguments'), 400

if args['id_project']:
if args['id_project'] in service_access.get_accessible_projects_ids():
test_type_projects = TeraTestTypeProject.get_tests_types_for_project(args['id_project'])
elif args['id_test_type']:
if args['id_test_type'] in service_access.get_accessible_tests_types_ids():
test_type_projects = TeraTestTypeProject.get_projects_for_test_type(args['id_test_type'])
try:
ttp_list = []
for ttp in test_type_projects:
json_ttp = ttp.to_json()
obj_type = inspect(ttp)
if not obj_type.transient:
json_ttp['test_type_name'] = ttp.test_type_project_test_type.test_type_name
json_ttp['project_name'] = ttp.test_type_project_project.project_name
if args['with_sites']:
json_ttp['id_site'] = ttp.test_type_project_project.id_site
json_ttp['site_name'] = ttp.test_type_project_project.project_site.site_name
else:
# Temporary object, a not-committed object, result of listing projects not associated to a
# test type.
if ttp.id_test_type:
tt: TeraTestType = TeraTestType.get_test_type_by_id(ttp.id_test_type)
json_ttp['test_type_name'] = tt.test_type_name
else:
json_ttp['test_type_name'] = None
if ttp.id_project:
proj = TeraProject.get_project_by_id(ttp.id_project)
json_ttp['project_name'] = proj.project_name
if args['with_sites']:
json_ttp['id_site'] = proj.id_site
json_ttp['site_name'] = proj.project_site.site_name
else:
json_ttp['project_name'] = None
ttp_list.append(json_ttp)

return ttp_list

except InvalidRequestError as e:
self.module.logger.log_error(self.module.module_name,
ServiceQueryTestTypeProjects.__name__,
'get', 500, 'InvalidRequestError', e)
return '', 500

@api.doc(description='Create/update test-type - project association.',
responses={200: 'Success',
403: 'Logged service can\'t modify association (not associated to project or test type)',
400: 'Badly formed JSON or missing fields in the JSON body',
500: 'Internal error occurred when saving association'},
params={'token': 'Secret token'})
@api.expect(post_schema)
@LoginModule.service_token_or_certificate_required
def post(self):
service_access = DBManager.serviceAccess(current_service)

accessible_projects_ids = service_access.get_accessible_projects_ids(admin_only=True)
if 'test_type' in request.json:
# We have a test_type. Get list of items
if 'id_test_type' not in request.json['test_type']:
return gettext('Missing id_test_type'), 400
if 'projects' not in request.json['test_type']:
return gettext('Missing projects'), 400
id_test_type = request.json['test_type']['id_test_type']

if id_test_type not in service_access.get_accessible_tests_types_ids():
return gettext("Forbidden"), 403

# Get all current association for test type
current_projects = TeraTestTypeProject.get_projects_for_test_type(test_type_id=id_test_type)
current_projects_ids = [proj.id_project for proj in current_projects]

for proj_id in current_projects_ids:
if proj_id not in accessible_projects_ids:
return gettext('Access denied to at least one project'), 403

received_proj_ids = [proj['id_project'] for proj in request.json['test_type']['projects']]
# Difference - we must delete sites not anymore in the list
todel_ids = set(current_projects_ids).difference(received_proj_ids)
# Also filter projects already there
received_proj_ids = set(received_proj_ids).difference(current_projects_ids)
try:
for proj_id in todel_ids:
if proj_id in accessible_projects_ids: # Don't remove from the list if not admin for that project!
TeraTestTypeProject.delete_with_ids(test_type_id=id_test_type, project_id=proj_id,
autocommit=False)
TeraTestTypeProject.commit()
except exc.IntegrityError as e:
self.module.logger.log_warning(self.module.module_name, ServiceQueryTestTypeProjects.__name__, 'delete',
500, 'Integrity error', str(e))
return gettext('Can\'t delete test type from project: please delete all tests of that type in the '
'project before deleting.'), 500
# Build projects association to add
json_ttp = [{'id_test_type': id_test_type, 'id_project': proj_id} for proj_id in received_proj_ids]
elif 'project' in request.json:
# We have a project. Get list of items
if 'id_project' not in request.json['project']:
return gettext('Missing project ID'), 400
if 'testtypes' not in request.json['project']:
return gettext('Missing test types'), 400
id_project = request.json['project']['id_project']

# Check if accessibles
if id_project not in accessible_projects_ids:
return gettext('Forbidden'), 403

# Get all current association
current_test_types = TeraTestTypeProject.get_tests_types_for_project(project_id=id_project)
current_test_types_ids = [tt.id_test_type for tt in current_test_types]
received_tt_ids = [tt['id_test_type'] for tt in request.json['project']['testtypes']]
# Difference - we must delete types not anymore in the list
todel_ids = set(current_test_types_ids).difference(received_tt_ids)
# Also filter types already there
received_tt_ids = set(received_tt_ids).difference(current_test_types_ids)
try:
for tt_id in todel_ids:
TeraTestTypeProject.delete_with_ids(test_type_id=tt_id, project_id=id_project, autocommit=False)
TeraTestTypeProject.commit()
except exc.IntegrityError as e:
self.module.logger.log_warning(self.module.module_name, ServiceQueryTestTypeProjects.__name__, 'delete',
500, 'Integrity error', str(e))
return gettext('Can\'t delete test type from project: please delete all tests of that type in the '
'project before deleting.'), 500
# Build associations to add
json_ttp = [{'id_test_type': tt_id, 'id_project': id_project} for tt_id in received_tt_ids]
elif 'test_type_project' in request.json:
json_ttp = request.json['test_type_project']
if not isinstance(json_ttp, list):
json_ttp = [json_ttp]
else:
return gettext('Unknown format'), 400

# Validate if we have an id and access
for json_tt in json_ttp:
if 'id_test_type' not in json_tt or 'id_project' not in json_tt:
return gettext('Badly formatted request'), 400

if json_tt['id_project'] not in accessible_projects_ids:
return gettext('Forbidden'), 403

proj = TeraProject.get_project_by_id(json_tt['id_project'])
site_access = TeraTestTypeSite.get_test_type_site_for_test_type_and_site(site_id=proj.id_site,
test_type_id=
json_tt['id_test_type']
)
if not site_access:
# At service level, if we have access to the project, we automatically associate the test type to its
# site, since we know we are allowed to (prevent a call to another API)
ttts = TeraTestTypeSite()
ttts.id_test_type = json_tt['id_test_type']
ttts.id_site = proj.id_site
TeraTestTypeSite.insert(ttts)

for json_tt in json_ttp:
if 'id_test_type_project' not in json_tt:
# Check if already exists
tt = TeraTestTypeProject.\
get_test_type_project_for_test_type_project(project_id=int(json_tt['id_project']),
test_type_id=int(json_tt['id_test_type']))
if tt:
json_tt['id_test_type_project'] = tt.id_test_type_project
else:
json_tt['id_test_type_project'] = 0

# Do the update!
if int(json_tt['id_test_type_project']) == 0:
try:
new_ttp = TeraTestTypeProject()
new_ttp.from_json(json_tt)
new_ttp = TeraTestTypeProject.insert(new_ttp)
# Update ID for further use
json_tt['id_test_type_project'] = new_ttp.id_test_type_project
except exc.SQLAlchemyError as e:
import sys
print(sys.exc_info())
self.module.logger.log_error(self.module.module_name,
ServiceQueryTestTypeProjects.__name__,
'post', 500, 'Database error', str(e))
return gettext('Database error'), 500

return json_ttp

@api.doc(description='Delete a specific test-type - project association.',
responses={200: 'Success',
403: 'Logged service can\'t delete association (no access to test-type or project)',
400: 'Association not found (invalid id?)'},
params={'token': 'Secret token'})
@api.expect(delete_parser)
@LoginModule.service_token_or_certificate_required
def delete(self):
service_access = DBManager.serviceAccess(current_service)
args = delete_parser.parse_args()
id_todel = args['id']

# Check if current user can delete
ttp = TeraTestTypeProject.get_test_type_project_by_id(id_todel)
if not ttp:
return gettext('Not found'), 400

if ttp.id_project not in service_access.get_accessible_tests_types_ids() or ttp.id_test_type \
not in service_access.get_accessible_tests_types_ids():
return gettext('Forbidden'), 403

# If we are here, we are allowed to delete. Do so.
try:
TeraTestTypeProject.delete(id_todel=id_todel)
except exc.IntegrityError as e:
# Causes that could make an integrity error when deleting:
# - Associated project still have sessions with tests of that type
self.module.logger.log_warning(self.module.module_name, ServiceQueryTestTypeProjects.__name__, 'delete',
500, 'Integrity error', str(e))
return gettext('Can\'t delete test type from project: please delete all tests of that type in the project '
'before deleting.'), 500
except exc.SQLAlchemyError as e:
import sys
print(sys.exc_info())
self.module.logger.log_error(self.module.module_name,
ServiceQueryTestTypeProjects.__name__,
'delete', 500, 'Database error', e)
return gettext('Database error'), 500

return '', 200
Loading

0 comments on commit 07b2f54

Please sign in to comment.