Commit 4fa5dbad authored by Barbora Kompišová's avatar Barbora Kompišová
Browse files

project first draft; jwt introduced

parent 9dade85d
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -12,6 +12,7 @@ flask-sqlalchemy = "*"
pytest = "*"
flask-restful = "*"
marshmallow = "*"
flask-jwt-extended = "*"


[dev-packages]
+8 −1
Original line number Diff line number Diff line
@@ -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
@@ -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__ = (
+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)

@@ -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()
@@ -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'))
+0 −0

File moved.

+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