Loading Pipfile +1 −0 Original line number Diff line number Diff line Loading @@ -12,6 +12,7 @@ flask-sqlalchemy = "*" pytest = "*" flask-restful = "*" marshmallow = "*" flask-jwt-extended = "*" [dev-packages] Loading portal/database/models.py +8 −1 Original line number Diff line number Diff line Loading @@ -111,6 +111,13 @@ class Project(db.Model, EntityBase): else: return ProjectState.INACTIVE def set_config(self, **kwargs): for k, w in kwargs.items(): if k in ('test_files_source', 'file_whitelist', 'pre_submit_script', 'post_submit_script', 'submission_parameters', 'submission_scheduler_config', 'submissions_allowed_from', 'submissions_allowed_to', 'archive_from'): setattr(self.config, k, w) def __init__(self, course, name, test_files_source) -> None: self.course = course self.name = name Loading Loading @@ -201,7 +208,7 @@ class Role(db.Model, EntityBase): def set_permissions(self, **kwargs): for k, w in kwargs.items(): if hasattr(self.permissions, k): if hasattr(self.permissions, k) and k not in ('id', 'role_id', 'role'): setattr(self.permissions, k, w) __table_args__ = ( Loading portal/rest/__init__.py +18 −2 Original line number Diff line number Diff line from marshmallow import Schema, fields from portal import db from portal.database.models import Course, Role, User from portal.database.models import Course, Role, User, Project from portal.tools.logging import log # Maybe: extract tuples used in 'only' to variables (-> in one place) Loading Loading @@ -31,6 +31,22 @@ def find_course(identifier): return None def find_project(course, identifier): log.debug(f"Attempting project search by id.") project = Project.query.filter_by(course=course).filter_by(id=identifier).first() if project: return project log.debug(f"Did not find project with id={identifier}.") log.debug(f"Attempting project search by name.") project = Project.query.filter_by(course=course).filter_by(name=identifier).first() if project: return project log.debug(f"Did not find project with codename={identifier}.") return None def find_role(course, identifier): log.debug(f"Attempting role search by id.") role = Role.query.filter_by(course=course).filter_by(id=identifier).first() Loading Loading @@ -164,7 +180,7 @@ class SubmissionSchema(Schema): id = fields.Str(dump_only=True) processing_scheduled_for = fields.LocalDateTime() parameters = fields.Str() state = fields.Str() # check this - enum state = fields.Str() # TODO: check this - enum note = fields.Str() user = fields.Nested('UserSchema', only=('id', 'uco', 'email', 'username')) project = fields.Nested('ProjectSchema', only=('id', 'name')) Loading portal/rest/auth/auth.py→portal/rest/auth/login.py +0 −0 File moved. portal/rest/projects/projects.py +197 −0 Original line number Diff line number Diff line from flask import Blueprint, request from flask_restful import Api, Resource, abort from marshmallow import ValidationError from flask_jwt_extended import JWTManager, jwt_required, create_access_token, get_jwt_identity from portal.rest import ProjectSchema, ProjectConfigSchema, SubmissionSchema,\ delete_entity, write_entity, find_course, find_project, find_user from portal.tools.logging import log from portal.database.models import Project, Submission from portal.service.service import can_create_submission, schedule_submission projects = Blueprint('projects', __name__, url_prefix='/courses/<string:cid>/projects') projects_api = Api(projects) project_schema = ProjectSchema() projects_schema = ProjectSchema(many=True, only=('id', 'name')) config_schema = ProjectConfigSchema() # add user information to submission schema? How? submissions_schema = SubmissionSchema(many=True, only=('id', 'processing_scheduled_for', 'parameters', 'state', 'note')) submission_schema = SubmissionSchema() class ProjectResource(Resource): def get(self, cid, pid): log.info(f"GET to {request.url}") course = find_course(cid) if not course: abort(404, message=f"Could not find project {pid}: course with identifier {cid} not found.") role = find_project(course, pid) if not role: abort(404, message=f"Could not find project {pid} in course {cid}: project not found.") return project_schema.dump(role) def delete(self, cid, pid): log.info(f"DELETE to {request.url}") course = find_course(cid) if not course: abort(404, message=f"Could not delete project {pid}: course with identifier {cid} not found.") project = find_project(course, pid) if not project: abort(404, message=f"Could not delete project {pid} in course {cid}: project not found.") delete_entity(project) return '', 204 def put(self, cid, pid): log.info(f"PUT to {request.url}") course = find_course(cid) if not course: abort(404, message=f"Could not update project {pid}: course with identifier {cid} not found.") project = find_project(course, pid) if not project: abort(404, message=f"Could not update project {pid} in course {cid}: project not found.") json_data = request.get_json() if not json_data: abort(400, message='No data provided for project update.') try: data = project_schema.load(json_data)[0] except ValidationError as err: log.info(f"Validation failed on: {err.messages}") return abort(422, errors=err.messages) # TODO: flash, process errors project.name = data['name'] write_entity(project) log.debug(f"Project update successful. Project={project}") return '', 204 class ProjectsList(Resource): def get(self, cid): log.info(f"GET to {request.url}") course = find_course(cid) if not course: abort(404, message=f"Could not list projects: course with identifier {cid} not found.") return projects_schema.dump(course.projects) def post(self, cid): log.info(f"POST to {request.url}") course = find_course(cid) if not course: abort(404, message=f"Could not create project: Course with identifier {cid} not found.") json_data = request.get_json() if not json_data: abort(400, message='No data provided for project creation.') try: data = project_schema.load(json_data)[0] except ValidationError as err: log.info(f"Validation failed on: {err.messages}") return abort(422, errors=err.messages) # TODO: flash, process errors new_project = Project(course=course, name=data['name'], test_files_source=data['test_files_source']) write_entity(new_project) log.debug(f"Created project={new_project} for course {cid}") return project_schema.dump(new_project)[0], 201 class ProjectConfigResource(Resource): def get(self, cid, pid): log.info(f"GET to {request.url}") course = find_course(cid) if not course: abort(404, message=f"Could not get project configuration: course with identifier {cid} not found.") project = find_project(course, pid) if not project: abort(404, message=f"Could not get project configuration: project {pid} not found in course {cid}") return config_schema.dump(project.config) def put(self, cid, pid): log.info(f"PUT to {request.url}") course = find_course(cid) if not course: abort(404, message=f"Could not update project configuration: course with identifier {cid} not found.") project = find_project(course, pid) if not project: abort(404, message=f"Could not update project configuration: project {pid} not found in course {cid}.") json_data = request.get_json() if not json_data: abort(400, message='No data provided for project configuration update.') try: data = config_schema.load(json_data)[0] except ValidationError as err: log.info(f"Validation failed on: {err.messages}") return abort(422, errors=err.messages) # TODO: flash, process errors project.set_config(data) # TODO: check - missing **? in role permissions too log.debug(f"Updated configuration for project {pid} in course {cid}.") return '', 204 class ProjectSubmissions(Resource): @jwt_required def get(self, cid, pid): log.info(f"GET to {request.url}") course = find_course(cid) if not course: abort(404, message=f"Could not list project submissions: course with identifier {cid} not found.") project = find_project(course, pid) if not project: abort(404, message=f"Could not list project submissions: project {pid} not found in course {cid}.") return submissions_schema.dump(project.submissions) @jwt_required def post(self, cid, pid): log.info(f"POST to {request.url}") user = find_user(get_jwt_identity()) if not user: abort(401, message="Invalid access token: user not found.") # authorization here course = find_course(cid) if not course: abort(404, message=f"Could not update project configuration: course with identifier {cid} not found.") project = find_project(course, pid) if not project: abort(404, message=f"Could not update project configuration: project {pid} not found in course {cid}.") # check if a new submission can be created by this user in this project: if not can_create_submission(user, project): # maybe use 409, 403 abort(429, message=f"New submission denied: you have an unfinished submission for this project.") json_data = request.get_json() if not json_data: abort(400, message='No data provided for project configuration update.') try: data = config_schema.load(json_data)[0] except ValidationError as err: log.info(f"Validation failed on: {err.messages}") return abort(422, errors=err.messages) # TODO: flash, process errors time = schedule_submission(project.config.submission_scheduler_config) # i feel like many things are missing here new_submission = Submission(user=user, project=project, processing_scheduled_for=time, parameters=project.config.submission_parameters, review=data['review']) write_entity(new_submission) # TODO: real submission processing: queue? return submission_schema.dump(new_submission) projects_api.add_resource(ProjectResource, '/<string:pid>') projects_api.add_resource(ProjectsList, '/') projects_api.add_resource(ProjectConfigResource, '/<string:pid>/config') projects_api.add_resource(ProjectSubmissions, '/<string:pid>/submissions') ''' ### Project test files * `[GET] /courses/{cid/projects/{id}/files` - Gets test files (feature) ### Project submissions * `[GET] /courses/{cid}/projects/{id}/submissions` - Read all submissions for project * `user=<user_selector>` - filters submissions by user * `[POST] /courses/{cid}/projects/{id}/submissions` - Create submission''' Loading
Pipfile +1 −0 Original line number Diff line number Diff line Loading @@ -12,6 +12,7 @@ flask-sqlalchemy = "*" pytest = "*" flask-restful = "*" marshmallow = "*" flask-jwt-extended = "*" [dev-packages] Loading
portal/database/models.py +8 −1 Original line number Diff line number Diff line Loading @@ -111,6 +111,13 @@ class Project(db.Model, EntityBase): else: return ProjectState.INACTIVE def set_config(self, **kwargs): for k, w in kwargs.items(): if k in ('test_files_source', 'file_whitelist', 'pre_submit_script', 'post_submit_script', 'submission_parameters', 'submission_scheduler_config', 'submissions_allowed_from', 'submissions_allowed_to', 'archive_from'): setattr(self.config, k, w) def __init__(self, course, name, test_files_source) -> None: self.course = course self.name = name Loading Loading @@ -201,7 +208,7 @@ class Role(db.Model, EntityBase): def set_permissions(self, **kwargs): for k, w in kwargs.items(): if hasattr(self.permissions, k): if hasattr(self.permissions, k) and k not in ('id', 'role_id', 'role'): setattr(self.permissions, k, w) __table_args__ = ( Loading
portal/rest/__init__.py +18 −2 Original line number Diff line number Diff line from marshmallow import Schema, fields from portal import db from portal.database.models import Course, Role, User from portal.database.models import Course, Role, User, Project from portal.tools.logging import log # Maybe: extract tuples used in 'only' to variables (-> in one place) Loading Loading @@ -31,6 +31,22 @@ def find_course(identifier): return None def find_project(course, identifier): log.debug(f"Attempting project search by id.") project = Project.query.filter_by(course=course).filter_by(id=identifier).first() if project: return project log.debug(f"Did not find project with id={identifier}.") log.debug(f"Attempting project search by name.") project = Project.query.filter_by(course=course).filter_by(name=identifier).first() if project: return project log.debug(f"Did not find project with codename={identifier}.") return None def find_role(course, identifier): log.debug(f"Attempting role search by id.") role = Role.query.filter_by(course=course).filter_by(id=identifier).first() Loading Loading @@ -164,7 +180,7 @@ class SubmissionSchema(Schema): id = fields.Str(dump_only=True) processing_scheduled_for = fields.LocalDateTime() parameters = fields.Str() state = fields.Str() # check this - enum state = fields.Str() # TODO: check this - enum note = fields.Str() user = fields.Nested('UserSchema', only=('id', 'uco', 'email', 'username')) project = fields.Nested('ProjectSchema', only=('id', 'name')) Loading
portal/rest/projects/projects.py +197 −0 Original line number Diff line number Diff line from flask import Blueprint, request from flask_restful import Api, Resource, abort from marshmallow import ValidationError from flask_jwt_extended import JWTManager, jwt_required, create_access_token, get_jwt_identity from portal.rest import ProjectSchema, ProjectConfigSchema, SubmissionSchema,\ delete_entity, write_entity, find_course, find_project, find_user from portal.tools.logging import log from portal.database.models import Project, Submission from portal.service.service import can_create_submission, schedule_submission projects = Blueprint('projects', __name__, url_prefix='/courses/<string:cid>/projects') projects_api = Api(projects) project_schema = ProjectSchema() projects_schema = ProjectSchema(many=True, only=('id', 'name')) config_schema = ProjectConfigSchema() # add user information to submission schema? How? submissions_schema = SubmissionSchema(many=True, only=('id', 'processing_scheduled_for', 'parameters', 'state', 'note')) submission_schema = SubmissionSchema() class ProjectResource(Resource): def get(self, cid, pid): log.info(f"GET to {request.url}") course = find_course(cid) if not course: abort(404, message=f"Could not find project {pid}: course with identifier {cid} not found.") role = find_project(course, pid) if not role: abort(404, message=f"Could not find project {pid} in course {cid}: project not found.") return project_schema.dump(role) def delete(self, cid, pid): log.info(f"DELETE to {request.url}") course = find_course(cid) if not course: abort(404, message=f"Could not delete project {pid}: course with identifier {cid} not found.") project = find_project(course, pid) if not project: abort(404, message=f"Could not delete project {pid} in course {cid}: project not found.") delete_entity(project) return '', 204 def put(self, cid, pid): log.info(f"PUT to {request.url}") course = find_course(cid) if not course: abort(404, message=f"Could not update project {pid}: course with identifier {cid} not found.") project = find_project(course, pid) if not project: abort(404, message=f"Could not update project {pid} in course {cid}: project not found.") json_data = request.get_json() if not json_data: abort(400, message='No data provided for project update.') try: data = project_schema.load(json_data)[0] except ValidationError as err: log.info(f"Validation failed on: {err.messages}") return abort(422, errors=err.messages) # TODO: flash, process errors project.name = data['name'] write_entity(project) log.debug(f"Project update successful. Project={project}") return '', 204 class ProjectsList(Resource): def get(self, cid): log.info(f"GET to {request.url}") course = find_course(cid) if not course: abort(404, message=f"Could not list projects: course with identifier {cid} not found.") return projects_schema.dump(course.projects) def post(self, cid): log.info(f"POST to {request.url}") course = find_course(cid) if not course: abort(404, message=f"Could not create project: Course with identifier {cid} not found.") json_data = request.get_json() if not json_data: abort(400, message='No data provided for project creation.') try: data = project_schema.load(json_data)[0] except ValidationError as err: log.info(f"Validation failed on: {err.messages}") return abort(422, errors=err.messages) # TODO: flash, process errors new_project = Project(course=course, name=data['name'], test_files_source=data['test_files_source']) write_entity(new_project) log.debug(f"Created project={new_project} for course {cid}") return project_schema.dump(new_project)[0], 201 class ProjectConfigResource(Resource): def get(self, cid, pid): log.info(f"GET to {request.url}") course = find_course(cid) if not course: abort(404, message=f"Could not get project configuration: course with identifier {cid} not found.") project = find_project(course, pid) if not project: abort(404, message=f"Could not get project configuration: project {pid} not found in course {cid}") return config_schema.dump(project.config) def put(self, cid, pid): log.info(f"PUT to {request.url}") course = find_course(cid) if not course: abort(404, message=f"Could not update project configuration: course with identifier {cid} not found.") project = find_project(course, pid) if not project: abort(404, message=f"Could not update project configuration: project {pid} not found in course {cid}.") json_data = request.get_json() if not json_data: abort(400, message='No data provided for project configuration update.') try: data = config_schema.load(json_data)[0] except ValidationError as err: log.info(f"Validation failed on: {err.messages}") return abort(422, errors=err.messages) # TODO: flash, process errors project.set_config(data) # TODO: check - missing **? in role permissions too log.debug(f"Updated configuration for project {pid} in course {cid}.") return '', 204 class ProjectSubmissions(Resource): @jwt_required def get(self, cid, pid): log.info(f"GET to {request.url}") course = find_course(cid) if not course: abort(404, message=f"Could not list project submissions: course with identifier {cid} not found.") project = find_project(course, pid) if not project: abort(404, message=f"Could not list project submissions: project {pid} not found in course {cid}.") return submissions_schema.dump(project.submissions) @jwt_required def post(self, cid, pid): log.info(f"POST to {request.url}") user = find_user(get_jwt_identity()) if not user: abort(401, message="Invalid access token: user not found.") # authorization here course = find_course(cid) if not course: abort(404, message=f"Could not update project configuration: course with identifier {cid} not found.") project = find_project(course, pid) if not project: abort(404, message=f"Could not update project configuration: project {pid} not found in course {cid}.") # check if a new submission can be created by this user in this project: if not can_create_submission(user, project): # maybe use 409, 403 abort(429, message=f"New submission denied: you have an unfinished submission for this project.") json_data = request.get_json() if not json_data: abort(400, message='No data provided for project configuration update.') try: data = config_schema.load(json_data)[0] except ValidationError as err: log.info(f"Validation failed on: {err.messages}") return abort(422, errors=err.messages) # TODO: flash, process errors time = schedule_submission(project.config.submission_scheduler_config) # i feel like many things are missing here new_submission = Submission(user=user, project=project, processing_scheduled_for=time, parameters=project.config.submission_parameters, review=data['review']) write_entity(new_submission) # TODO: real submission processing: queue? return submission_schema.dump(new_submission) projects_api.add_resource(ProjectResource, '/<string:pid>') projects_api.add_resource(ProjectsList, '/') projects_api.add_resource(ProjectConfigResource, '/<string:pid>/config') projects_api.add_resource(ProjectSubmissions, '/<string:pid>/submissions') ''' ### Project test files * `[GET] /courses/{cid/projects/{id}/files` - Gets test files (feature) ### Project submissions * `[GET] /courses/{cid}/projects/{id}/submissions` - Read all submissions for project * `user=<user_selector>` - filters submissions by user * `[POST] /courses/{cid}/projects/{id}/submissions` - Create submission'''