Unverified Commit b3dac4d5 authored by Peter Stanko's avatar Peter Stanko
Browse files

Refactor Schemas

parent 370b1540
......@@ -3,7 +3,7 @@ from flask_restplus import Namespace, Resource
from portal import logger
from portal.database.models import ClientType
from portal.rest.schemas import user_schema, worker_schema
from portal.rest.schemas import SCHEMAS
from portal.service import permissions
client_namespace = Namespace('client')
......@@ -18,5 +18,5 @@ class ClientController(Resource):
def get(self):
perm_service = permissions.PermissionsService()
client = perm_service.client_owner
schema = user_schema if client.type == ClientType.USER else worker_schema
schema = SCHEMAS.user() if client.type == ClientType.USER else SCHEMAS.worker()
return schema.dump(client)[0], 200
......@@ -4,7 +4,7 @@ from flask_restplus import Namespace, Resource
from portal import logger
from portal.rest import rest_helpers
from portal.rest.schemas import course_import_schema, course_schema, courses_schema, users_schema
from portal.rest.schemas import SCHEMAS
from portal.service import permissions
from portal.service.auth import find_client
from portal.service.courses import CourseService
......@@ -26,7 +26,7 @@ class CourseList(Resource):
permissions.PermissionsService().require.sysadmin()
courses_list = CourseService().find_all_courses()
return courses_schema.dump(courses_list)
return SCHEMAS.dump('courses', courses_list)
@jwt_required
# @courses_namespace.response(200, 'Created course', model=course_schema)
......@@ -34,10 +34,9 @@ class CourseList(Resource):
def post(self):
permissions.PermissionsService().require.sysadmin()
data = rest_helpers.parse_request_data(
course_schema, resource='course', action='create')
data = rest_helpers.parse_request_data(resource='course', action='create')
new_course = CourseService().create_course(**data)
return course_schema.dump(new_course)[0], 201
return SCHEMAS.dump('course', new_course), 201
@courses_namespace.route('/<string:cid>')
......@@ -54,11 +53,12 @@ class CourseResource(Resource):
# authorization
perm_service = permissions.PermissionsService(course=course)
if perm_service.check.permissions(['view_course_full']):
return course_schema.dump(course)
return SCHEMAS.dump('course', course)
elif perm_service.check.permissions(['view_course_limited']):
dump = course_schema.dump(course)
filtered_course = filter_course_dump(course, dump.data, client)
# TODO: Refactor
dump = SCHEMAS.dump('course', course)
filtered_course = filter_course_dump(course, dump, client)
return filtered_course
raise ForbiddenError(client=client)
......@@ -80,9 +80,7 @@ class CourseResource(Resource):
# authorization
permissions.PermissionsService(course=course).require.update_course()
data = rest_helpers.parse_request_data(
schema=course_schema, action='update', resource='course', partial=True
)
data = rest_helpers.parse_request_data(action='update', resource='course', partial=True)
CourseService(course=course).update_course(data)
return '', 204
......@@ -110,8 +108,7 @@ class CourseNotesToken(Resource):
# authorization
permissions.PermissionsService(course=course).require.course_access_token()
json_data = rest_helpers.require_data(
action='update_notes_token', resource='course')
json_data = rest_helpers.require_data(action='update_notes_token', resource='course')
CourseService(course=course).update_notes_token(json_data['token'])
return '', 204
......@@ -131,7 +128,7 @@ class CourseImport(Resource):
permissions.PermissionsService(course=course).require.update_course()
data = rest_helpers.parse_request_data(
course_import_schema, action='import', resource='course'
schema=SCHEMAS.course_import, action='import', resource='course'
)
source_course = find_course(data['source_course'])
if source_course == course:
......@@ -141,14 +138,13 @@ class CourseImport(Resource):
config = data['config']
copied_course = CourseService(course=course).copy_course(course, config)
return course_schema.dump(copied_course)[0]
return SCHEMAS.dump('course', copied_course)
@courses_namespace.route('/<string:cid>/users')
@courses_namespace.param('cid', 'Course id')
@courses_namespace.response(404, 'Course not found')
class CourseUsers(Resource):
@jwt_required
# @courses_namespace.response(200, 'Users in the course', model=users_schema)
@courses_namespace.response(403, 'Not allowed to see users in the course')
......@@ -158,4 +154,4 @@ class CourseUsers(Resource):
group_ids = request.args.getlist('group')
role_ids = request.args.getlist('role')
users = CourseService(course=course).get_users_filtered(group_ids, role_ids)
return users_schema.dump(users)[0]
return SCHEMAS.dump('users', users)
......@@ -4,8 +4,7 @@ from flask_restplus import Namespace, Resource
from portal import logger
from portal.rest import rest_helpers
from portal.rest.schemas import client_list_update_schema, group_import_schema, group_schema, \
groups_schema, projects_schema, users_schema
from portal.rest.schemas import SCHEMAS
from portal.service import general, permissions
from portal.service.groups import GroupService
......@@ -23,7 +22,7 @@ class GroupsList(Resource):
def get(self, cid: str):
course = general.find_course(cid)
groups = GroupService().list_groups(course)
return groups_schema.dump(groups)[0]
return SCHEMAS.dump('groups', groups)
@jwt_required
# @groups_namespace.response(201, 'Group created', model=group_schema)
......@@ -33,10 +32,10 @@ class GroupsList(Resource):
# authorization
permissions.PermissionsService(course=course).require.update_course()
data = rest_helpers.parse_request_data(group_schema, action='create', resource='group')
data = rest_helpers.parse_request_data(action='create', resource='group')
new_group = GroupService().create_group(course, **data)
return group_schema.dump(new_group)[0], 201
return SCHEMAS.dump('group', new_group), 201
@groups_namespace.route('/courses/<string:cid>/groups/<string:gid>')
......@@ -54,7 +53,7 @@ class GroupResource(Resource):
permissions.PermissionsService(course=course).require.view_course()
group = general.find_group(course, gid)
return group_schema.dump(group)
return SCHEMAS.dump('group', group)
@jwt_required
@groups_namespace.response(204, 'Group deleted')
......@@ -76,10 +75,7 @@ class GroupResource(Resource):
# authorization
permissions.PermissionsService(course=course).require.write_groups()
data = rest_helpers.parse_request_data(
group_schema, action='update', resource='group', partial=True
)
data = rest_helpers.parse_request_data(action='update', resource='group', partial=True)
group = general.find_group(course, gid)
GroupService(group).update_group(data)
return '', 204
......@@ -101,7 +97,7 @@ class GroupUsersList(Resource):
permissions.PermissionsService(course=course).require.belongs_to_group(group)
users = GroupService(group).find_users_by_role(course, role_id)
return users_schema.dump(users)
return SCHEMAS.dump('users', users)
@jwt_required
@groups_namespace.response(204, 'User\'s list updated')
......@@ -112,7 +108,7 @@ class GroupUsersList(Resource):
permissions.PermissionsService(course=course).require.write_groups()
data = rest_helpers.parse_request_data(
client_list_update_schema, action='update', resource='group_membership'
schema=SCHEMAS.client_list_update, action='update', resource='group_membership'
)
group = general.find_group(course, gid)
......@@ -173,7 +169,7 @@ class GroupProjectsList(Resource):
permissions.PermissionsService(course=course).require.belongs_to_group(group)
projects = GroupService(group).find_projects()
return projects_schema.dump(projects)
return SCHEMAS.dump('projects', projects)
@groups_namespace.route(
......@@ -228,7 +224,7 @@ class GroupImport(Resource):
# authorization
permissions.PermissionsService(course=target_course).require.write_groups()
data = rest_helpers.parse_request_data(
group_import_schema, action='import', resource='group'
schema=SCHEMAS.group_import, action='import', resource='group'
)
new_group = GroupService().import_group(data, target_course)
return group_schema.dump(new_group), 201
return SCHEMAS.dump('group', new_group), 201
......@@ -5,8 +5,7 @@ from flask_restplus import Namespace, Resource
from portal import logger
from portal.database.models import ClientType, ProjectState
from portal.rest import rest_helpers
from portal.rest.schemas import config_schema, config_schema_reduced, project_schema, \
projects_schema, submission_create_schema, submission_schema, submissions_schema
from portal.rest.schemas import SCHEMAS
from portal.service import auth, general, permissions
from portal.service.errors import ForbiddenError, SubmissionRefusedError
from portal.service.projects import ProjectService
......@@ -29,7 +28,7 @@ class ProjectsList(Resource):
def get(self, cid: str):
course = general.find_course(cid)
projects = ProjectService().list_projects(course)
return projects_schema.dump(projects)
return SCHEMAS.dump('projects', projects)
@jwt_required
@projects_namespace.response(404, 'Course not found')
......@@ -39,12 +38,10 @@ class ProjectsList(Resource):
# authorization
permissions.PermissionsService(course=course).require.update_course()
data = rest_helpers.parse_request_data(
project_schema, action='create', resource='project'
)
data = rest_helpers.parse_request_data(action='create', resource='project')
new_project = ProjectService().create_project(course, data)
return project_schema.dump(new_project)[0], 201
return SCHEMAS.dump('project', new_project), 201
@projects_namespace.route('/courses/<string:cid>/projects/<string:pid>')
......@@ -62,7 +59,7 @@ class ProjectResource(Resource):
permissions.PermissionsService(course=course).require.view_course()
project = general.find_project(course, pid)
return project_schema.dump(project)
return SCHEMAS.dump('project', project)
@jwt_required
@projects_namespace.response(204, 'Project deleted')
......@@ -84,9 +81,7 @@ class ProjectResource(Resource):
# authorization
permissions.PermissionsService(course=course).require.write_projects()
data = rest_helpers.parse_request_data(
project_schema, action='update', resource='project', partial=True
)
data = rest_helpers.parse_request_data(action='update', resource='project', partial=True)
project = general.find_project(course, pid)
ProjectService(project).update_project(data)
......@@ -116,10 +111,7 @@ class ProjectConfigResource(Resource):
# authorization
permissions.PermissionsService(course=course).require.write_projects()
data = rest_helpers.parse_request_data(
config_schema, action='update', resource='project_config'
)
data = rest_helpers.parse_request_data(action='update', resource='config')
project = general.find_project(course, pid)
ProjectService(project).update_project_config(data)
return '', 204
......@@ -160,7 +152,7 @@ class ProjectSubmissions(Resource):
user_id = request.args.get('user')
project = general.find_project(course, pid)
submissions = ProjectService(project).find_project_submissions(user_id)
return submissions_schema.dump(submissions)
return SCHEMAS.dump('submissions', submissions)
@jwt_required
# @projects_namespace.response(201, 'Project submission create', model=submission_schema)
......@@ -177,7 +169,7 @@ class ProjectSubmissions(Resource):
_check_submission_create(user, project)
data = rest_helpers.parse_request_data(
submission_create_schema, action='create', resource='submission'
schema=SCHEMAS.submission_create, action='create', resource='submission'
)
log.debug(f"[REST] Create submission: {data}")
......@@ -188,7 +180,7 @@ class ProjectSubmissions(Resource):
project=project,
submission_params=data
)
return submission_schema.dump(new_submission)[0], 201
return SCHEMAS.dump('submission', new_submission), 201
@projects_namespace.route('/courses/<string:cid>/projects/<string:pid>/files')
......@@ -218,8 +210,8 @@ def _check_submission_create(client, project):
def get_config_schema_based_on_permissions(course):
perm_service = permissions.PermissionsService(course=course)
if perm_service.check.permissions(['view_course_full']):
return config_schema
return SCHEMAS.config()
elif perm_service.check.permissions(['view_course_limited']):
return config_schema_reduced
return SCHEMAS.config_reduced()
else:
raise ForbiddenError(perm_service.client)
......@@ -4,10 +4,12 @@ Helpers to work and process the rest requests and responses
from flask import request
from portal.rest.schemas import SCHEMAS
from portal.service.errors import DataMissingError
def parse_request_data(schema, action: str, resource: str, partial=False) -> dict:
def parse_request_data(schema=None, action: str = None, resource: str = None,
partial=False) -> dict:
"""Parses the request data using schema
Args:
schema(Schema): Resource schema
......@@ -18,6 +20,10 @@ def parse_request_data(schema, action: str, resource: str, partial=False) -> dic
Returns(dict): Parsed dictionary
"""
if schema is None:
schema = getattr(SCHEMAS, resource)
if callable(schema):
schema = schema()
json_data = require_data(action, resource=resource)
return schema.load(json_data, partial=partial)[0]
......
......@@ -5,8 +5,7 @@ from flask_restplus import Namespace, Resource
from portal import logger
from portal.database.models import ClientType
from portal.rest import rest_helpers
from portal.rest.schemas import client_list_update_schema, permissions_schema, role_schema, \
roles_schema, users_schema
from portal.rest.schemas import SCHEMAS
from portal.service import general, permissions
from portal.service.roles import RoleService
......@@ -23,7 +22,7 @@ class RoleList(Resource):
def get(self, cid):
course = general.find_course(cid)
roles = RoleService().list_roles(course)
return roles_schema.dump(roles)[0]
return SCHEMAS.dump('roles', roles)
@jwt_required
# @roles_namespace.response(201, 'Role created', model=role_schema)
......@@ -32,11 +31,9 @@ class RoleList(Resource):
# authorization
permissions.PermissionsService(course=course).require.update_course()
data = rest_helpers.parse_request_data(
role_schema, action='create', resource='role'
)
data = rest_helpers.parse_request_data(action='create', resource='role')
new_role = RoleService().create_role(course, **data)
return role_schema.dump(new_role)[0], 201
return SCHEMAS.dump('role', new_role), 201
@roles_namespace.route('/courses/<string:cid>/roles/<string:rid>')
......@@ -53,7 +50,7 @@ class RoleResource(Resource):
permissions.PermissionsService(course=course).require.view_course()
role = general.find_role(course, rid)
return role_schema.dump(role)
return SCHEMAS.dump('role', role)
@jwt_required
@roles_namespace.response(204, 'Deleted role')
......@@ -73,9 +70,7 @@ class RoleResource(Resource):
# authorization
permissions.PermissionsService(course=course).require.write_roles()
data = rest_helpers.parse_request_data(
role_schema, action='update', resource='role', partial=True
)
data = rest_helpers.parse_request_data(action='update', resource='role', partial=True)
role = general.find_role(course, rid)
RoleService(role).update_role(data)
......@@ -88,7 +83,6 @@ class RoleResource(Resource):
@roles_namespace.response(404, 'Course not found')
@roles_namespace.response(404, 'Role not found')
class RolePermissions(Resource):
@jwt_required
# @roles_namespace.response(200, 'Get permissions for role', model=permissions_schema)
def get(self, cid: str, rid: str):
......@@ -98,7 +92,7 @@ class RolePermissions(Resource):
# authorization
permissions.PermissionsService(course=course).require.belongs_to_role(role)
return permissions_schema.dump(role.permissions)
return SCHEMAS.dump('permissions', role.permissions)
@jwt_required
# @roles_namespace.response(204, 'Update permissions for course', model=permissions_schema)
......@@ -107,13 +101,11 @@ class RolePermissions(Resource):
# authorization
permissions.PermissionsService(course=course).require.write_roles()
data = rest_helpers.parse_request_data(
permissions_schema, action='update', resource='role_permissions'
)
data = rest_helpers.parse_request_data(action='update', resource='permissions')
role = general.find_role(course, rid)
RoleService(role).update_permissions(data)
return permissions_schema.dump(role.permissions), 204
return SCHEMAS.dump('permissions', role.permissions), 200
@roles_namespace.route('/courses/<string:cid>/roles/<string:rid>/clients')
......@@ -132,8 +124,8 @@ class RoleUsersList(Resource):
permissions.PermissionsService(course=course).require.belongs_to_role(role)
return users_schema.dump(
[client for client in role.clients if client.type == ClientType[type.upper()]])
users = [client for client in role.clients if client.type == ClientType[type.upper()]]
return SCHEMAS.dump('users', users)
@jwt_required
@roles_namespace.response(204, 'Updates users membership in role')
......@@ -143,7 +135,7 @@ class RoleUsersList(Resource):
permissions.PermissionsService(course=course).require.write_roles()
data = rest_helpers.parse_request_data(
client_list_update_schema, action='update', resource='role_membership'
schema=SCHEMAS.client_list_update, action='update', resource='role_membership'
)
# everything from users_add is added, THEN everything from users_remove
......
"""
Schemas used to serialize/deserialize and validate of the models in the DB
"""
import functools
from marshmallow import Schema, ValidationError, fields, validates_schema
from marshmallow_enum import EnumField
from portal.database.models import ProjectState, SubmissionState, WorkerState
from portal.database.models import ClientType, ProjectState, SubmissionState, WorkerState
class NestedCollection:
ENTITIES = {
'Role': ('id', 'codename', 'course.id'),
'Group': ('id', 'codename', 'course.id'),
'Project': ('id', 'codename', 'course.id'),
'Course': ('id', 'codename'),
'User': ('id', 'username', 'uco', 'codename'),
'Worker': ('id', 'codename', 'name'),
'Client': ('id', 'type', 'name', 'codename'),
'Component': ('id', 'name', 'type'),
'Submission': ('id', 'state', 'user.id', 'user.username',
'project.id', 'project.codename',
'project.course.id', 'project.course.codename',
'created_at', 'updated_at'),
'ReviewItem': ('id', 'review.id', 'line', 'content', 'file', 'user.id'),
'Secret': ('id', 'name', 'expires_at')
}
def __init__(self, mod_name):
self.mod_name = mod_name
self.collection = {}
......@@ -38,6 +57,7 @@ class NestedCollection:
def _set_nested_and_list(self, schema_name, params):
self._set_nested(schema_name=schema_name, params=params)
# List version
self._set_nested(schema_name=schema_name, params=params, many=True)
def __build_nested(self):
......@@ -46,22 +66,7 @@ class NestedCollection:
@property
def entities(self):
return {
'Role': ('id', 'codename', 'course.id'),
'Group': ('id', 'codename', 'course.id'),
'Project': ('id', 'codename', 'course.id'),
'Course': ('id', 'codename'),
'User': ('id', 'username', 'uco', 'codename'),
'Worker': ('id', 'codename', 'name'),
'Client': ('id', 'type', 'name', 'codename'),
'Component': ('id', 'name', 'type'),
'Submission': ('id', 'state', 'user.id', 'user.username',
'project.id', 'project.codename',
'project.course.id', 'project.course.codename',
'created_at', 'updated_at'),
'ReviewItem': ('id', 'review.id', 'line', 'content', 'file', 'user.id'),
'Secret': ('id', 'name', 'expires_at')
}
return self.__class__.ENTITIES
def get(self, name):
return self.collection[name]
......@@ -158,7 +163,7 @@ class SubmissionCreateSchema(Schema):
"""Submission Create Schema
"""
file_params = NESTED('SubmissionFileParams', required=True)
project_params = fields.Dict()
project_params = fields.Dict(required=False, allow_none=True)
class ProjectSchema(BaseSchema, NamedSchema, Schema):
......@@ -202,7 +207,7 @@ class RoleSchema(BaseSchema, NamedSchema, Schema):
class ClientSchema(BaseSchema, Schema):
type = fields.Str()
type = EnumField(ClientType, by_value=True)
class RolePermissionsSchema(BaseSchema, Schema):
......@@ -330,6 +335,17 @@ class GroupImportSchema(Schema):
with_users = fields.Str()
class RoleImportSchema(Schema):
source_course = fields.Str()
source_role = fields.Str()
with_users = fields.Str()
class ProjectImportSchema(Schema):
source_course = fields.Str()
source_project = fields.Str()
class NotificationSendToSchema(Schema):
"""Notification Send To Schema
"""
......@@ -358,42 +374,177 @@ class SubmissionResultSchema(Schema):
# pylint: enable=too-few-public-methods
ALWAYS_ALLOWED = ['updated_at', 'created_at', 'id']
CODENAME_W_DESC = [*ALWAYS_ALLOWED, 'name', 'codename', 'description']
user_schema = UserSchema(strict=True)
user_schema_reduced = UserSchema(strict=True,
only=(*ALWAYS_ALLOWED, 'username', 'uco', 'email', 'name'))
users_schema = UserSchema(many=True, only=(
*ALWAYS_ALLOWED, 'username', 'uco', 'email'))
password_change_schema = PasswordChangeSchema()
submission_schema = SubmissionSchema()
submissions_schema = SubmissionSchema(many=True,
only=(*ALWAYS_ALLOWED, 'state', 'project', 'scheduled_for',
'user')
)
submission_create_schema = SubmissionCreateSchema()
submission_state_schema = SubmissionSchema(only=('state',))
reviews_schema = ReviewSchema(many=True)
review_schema = ReviewSchema()
role_schema = RoleSchema(strict=True)
roles_schema = RoleSchema(many=True, only=(*CODENAME_W_DESC, 'course'))
permissions_schema = RolePermissionsSchema()
course_schema = CourseSchema(strict=True)
course_schema_reduced = CourseSchema(strict=True, only=(*CODENAME_W_DESC,))
courses_schema = CourseSchema(many=True, only=(*CODENAME_W_DESC,))
course_import_schema = CourseImportSchema(strict=True)
group_schema = GroupSchema(strict=True)
groups_schema = GroupSchema(many=True, only=(*CODENAME_W_DESC, 'course'))
project_schema = ProjectSchema(strict=True)
projects_schema = ProjectSchema(many=True, only=(*CODENAME_W_DESC, 'course'))
config_schema = ProjectConfigSchema()
config_schema_reduced = ProjectConfigSchema(only=('id', 'project', 'submissions_allowed_from',
'submissions_allowed_to', 'file_whitelist'))
client_list_update_schema = ClientListUpdateSchema(strict=True)
group_import_schema = GroupImportSchema(strict=True)
worker_schema = WorkerSchema(strict=True)
workers_schema = WorkerSchema(strict=True, many=True)
submission_result_schema = SubmissionResultSchema(strict=True)
secrets_schema = SecretSchema(strict=True, many=True, only=(*ALWAYS_ALLOWED, 'name', 'expires_at'))
secret_schema = SecretSchema(strict=True, only=(*ALWAYS_ALLOWED, 'name', 'expires_at'))
def fn_name(func):
@functools.wraps(func)
def __wrap(*args, **kwargs):
return func(*args, select_params=func.__name__, **kwargs)
return __wrap
class Schemas:
ALWAYS_ALLOWED = ['updated_at', 'created_at', 'id']
CODENAME_W_DESC = [*ALWAYS_ALLOWED, 'name', 'codename', 'description']
ENT_W_COURSE = (*CODENAME_W_DESC, 'course')
PARAMS = {
'user_reduced': (*ALWAYS_ALLOWED, 'username', 'uco', 'email', 'name'),
'users': (*ALWAYS_ALLOWED, 'username', 'uco', 'email'),
'submissions': (*ALWAYS_ALLOWED, 'state', 'project', 'scheduled_for', 'user'),
'submission_state': (*ALWAYS_ALLOWED, 'state'),
'roles': ENT_W_COURSE,
'groups': ENT_W_COURSE,
'projects': ENT_W_COURSE,
'course_reduced': (*CODENAME_W_DESC,),
'courses': (*CODENAME_W_DESC,),
'config_reduced': (*ALWAYS_ALLOWED, 'project', 'submissions_allowed_from',
'submissions_allowed_to', 'file_whitelist'),
'secrets': (*ALWAYS_ALLOWED, 'name', 'expires_at'),
'secret': (*ALWAYS_ALLOWED, 'name', 'expires_at')
}
def __get_schema(self, schema_klass, select_params=None, only=None, strict=True, **kwargs):
if only is None:
only = self._select_params(select_params)
params = {'strict': strict, **kwargs}
if only is not None: