Loading Pipfile +2 −0 Original line number Original line Diff line number Diff line Loading @@ -13,6 +13,8 @@ pytest = "*" flask-restful = "*" flask-restful = "*" marshmallow = "*" marshmallow = "*" flask-jwt-extended = "*" flask-jwt-extended = "*" marshmallow-enum = "*" storage = { git="git@gitlab.fi.muni.cz:grp-kontr2/kontr-storage-module.git", editable='true' } [dev-packages] [dev-packages] Loading portal/__init__.py +16 −1 Original line number Original line Diff line number Diff line Loading @@ -2,14 +2,25 @@ from flask import Flask from portal.config import CONFIGURATIONS from portal.config import CONFIGURATIONS from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy import os import os from storage.core.storage import Storage from portal.rest.courses.courses import courses from portal.rest.users.users import users from portal.rest.roles.roles import roles from portal.rest.groups.groups import groups db = SQLAlchemy() db = SQLAlchemy() app = Flask('portal') app = Flask('portal') # TODO - urgent storage = Storage({ "submissions_dir": "todo", "test_files_dir": "todo", "workspace_dir": "todo" }) def config_app(flask_app): def config_app(flask_app): """Configures the Flask app based on production stage.""" """Configures the Flask app based on production stage.""" config_type = os.environ.get('PORTAL_CONFIG_TYPE', 'devel') # TODO: meh name config_type = os.environ.get('PORTAL_CONFIG_TYPE', 'devel') config_object = CONFIGURATIONS[config_type] config_object = CONFIGURATIONS[config_type] if config_object is None: if config_object is None: raise EnvironmentError() # TODO some custom ex raise EnvironmentError() # TODO some custom ex Loading @@ -20,6 +31,10 @@ def config_app(flask_app): def create_app(): def create_app(): # app configuration # app configuration config_app(app) config_app(app) app.register_blueprint(courses) app.register_blueprint(users) app.register_blueprint(roles) app.register_blueprint(groups) # database bind to app # database bind to app db.init_app(app) db.init_app(app) Loading portal/database/models.py +4 −0 Original line number Original line Diff line number Diff line Loading @@ -315,6 +315,10 @@ class Submission(db.Model, EntityBase): review = db.relationship("Review", back_populates="submission", cascade="all, delete-orphan", review = db.relationship("Review", back_populates="submission", cascade="all, delete-orphan", passive_deletes=True, uselist=False) passive_deletes=True, uselist=False) def change_state(self, new_state): # open to extension (state transition validation, ...) self.state = new_state def __init__(self, user, project, parameters, review=None): def __init__(self, user, project, parameters, review=None): self.user = user self.user = user self.project = project self.project = project Loading portal/rest/__init__.py +41 −111 Original line number Original line Diff line number Diff line from marshmallow import Schema, fields from marshmallow import Schema, fields, validates_schema, ValidationError from portal import db from marshmallow_enum import EnumField from portal.database.models import Course, Role, User, Project, Group from portal.tools.logging import log # Maybe: extract tuples used in 'only' to variables (-> in one place) from portal.database.models import SubmissionState def delete_entity(entity): db.session.remove(entity) db.session.commit() def write_entity(entity): db.session.add(entity) db.session.commit() def find_course(identifier): log.debug(f"Attempting course search by id.") course = Course.query.filter_by(id=identifier).first() if course: return course log.debug(f"Did not find course with id={identifier}.") log.debug(f"Attempting course search by codename.") course = Course.query.filter_by(codename=identifier).first() if course: return course log.debug(f"Did not find course with codename={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_group(course, identifier): log.debug(f"Attempting group search by id.") group = Group.query.filter_by(course=course).filter_by(id=identifier).first() if group: return group log.debug(f"Did not find group with id={identifier}.") log.debug(f"Attempting group search by name.") group = Group.query.filter_by(course=course).filter_by(name=identifier).first() if group: return group log.debug(f"Did not find group with name={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() if role: return role log.debug(f"Did not find role with id={identifier} in course {course.codename}.") log.debug(f"Attempting role search by name.") role = Role.query.filter_by(course=course).filter_by(name=identifier).first() if role: return role log.debug(f"Did not find role with name={identifier} in course {course.codename}.") return None def find_user(identifier): log.debug(f"Attempting user search by id.") user = User.query.filter_by(id=identifier).first() if user: return user log.debug(f"Did not find user with id={identifier}.") log.debug(f"Attempting user search by email.") user = User.query.filter_by(email=identifier).first() if user: return user log.debug(f"Did not find user with email={identifier}.") log.debug(f"Attempting user search by username.") user = User.query.filter_by(username=identifier).first() if user: return user log.debug(f"Did not find user with username={identifier}.") # not searching by uco return None class UserSchema(Schema): class UserSchema(Schema): id = fields.Str(dump_only=True) # TODO: check if i need the ids here at all id = fields.Str(dump_only=True) uco = fields.Int() uco = fields.Int() email = fields.Email() # TODO: check how this validates email = fields.Email() username = fields.Str() username = fields.Str() name = fields.Str() name = fields.Str() is_admin = fields.Bool() is_admin = fields.Bool() submissions = fields.Nested('SubmissionSchema', only=('id', 'note', 'state'), many=True) submissions = fields.Nested('SubmissionSchema', only=('id', 'note', 'state'), many=True) review_items = fields.Nested('ReviewItemSchema', only=('id',), many=True) # TODO: update 'only' review_items = fields.Nested('ReviewItemSchema', only=('id',), many=True) # TODO: what to show class CourseSchema(Schema): class CourseSchema(Schema): Loading @@ -123,7 +24,37 @@ class CourseSchema(Schema): projects = fields.Nested('ProjectSchema', only=('id', 'name'), many=True) projects = fields.Nested('ProjectSchema', only=('id', 'name'), many=True) class SubmissionFileParamsSchema(Schema): source = fields.Nested('SubmissionFileSourceSchema') class SubmissionFileSourceSchema(Schema): type = fields.Str() url = fields.Str() # git@gitlab.fi.muni.cz:foo/project.git branch = fields.Str() checkout = fields.Str() @validates_schema def validate_present_fields(self, data): if data.get('type') is None: raise ValidationError(f"Submission source type is required. Supported values: 'git', 'zip'.") if data['type'] not in ('git', 'zip'): raise ValidationError(f"Submission source unsupported: {data['type']}. Supported sources: git, zip. ") if data['type'] == 'git': if any(('url', 'branch', 'checkout')) not in data.keys: raise ValidationError(f"File source 'git' requires precisely 'url', 'branch' and 'checkout' attributes.") if data['type'] == 'zip': if any(('url', 'branch', 'checkout')) in data.keys: raise ValidationError(f"File source 'zip' does not require any other attributes.") class SubmissionCreateSchema(Schema): file_params = fields.Nested('SubmissionFileParamsSchema') project_params = fields.Dict() class ProjectSchema(Schema): class ProjectSchema(Schema): # TODO: add state as enum id = fields.Str(dump_only=True) id = fields.Str(dump_only=True) name = fields.Str() name = fields.Str() config = fields.Nested('ProjectConfigSchema', exclude=('id', '_submissions_allowed_from', '_submissions_allowed_to', config = fields.Nested('ProjectConfigSchema', exclude=('id', '_submissions_allowed_from', '_submissions_allowed_to', Loading Loading @@ -194,13 +125,12 @@ class GroupSchema(Schema): class SubmissionSchema(Schema): class SubmissionSchema(Schema): id = fields.Str(dump_only=True) id = fields.Str(dump_only=True) processing_scheduled_for = fields.LocalDateTime() parameters = fields.Str() state = fields.Str() # TODO: check this - enum note = fields.Str() note = fields.Str() user = fields.Nested('UserSchema', only=('id', 'uco', 'email', 'username')) state = EnumField(SubmissionState) project = fields.Nested('ProjectSchema', only=('id', 'name')) scheduled_for = fields.LocalDateTime() review = fields.Nested('ReviewSchema', only=('id',)) # maybe flatten the review here? parameters = fields.Dict() project = fields.Nested('ProjectSchema', only=('id', 'name', 'course.id')) # check dot notation user = fields.Str() # id class ReviewSchema(Schema): class ReviewSchema(Schema): Loading portal/rest/components/__init__.py 0 → 100644 +0 −0 Empty file added. Loading
Pipfile +2 −0 Original line number Original line Diff line number Diff line Loading @@ -13,6 +13,8 @@ pytest = "*" flask-restful = "*" flask-restful = "*" marshmallow = "*" marshmallow = "*" flask-jwt-extended = "*" flask-jwt-extended = "*" marshmallow-enum = "*" storage = { git="git@gitlab.fi.muni.cz:grp-kontr2/kontr-storage-module.git", editable='true' } [dev-packages] [dev-packages] Loading
portal/__init__.py +16 −1 Original line number Original line Diff line number Diff line Loading @@ -2,14 +2,25 @@ from flask import Flask from portal.config import CONFIGURATIONS from portal.config import CONFIGURATIONS from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy import os import os from storage.core.storage import Storage from portal.rest.courses.courses import courses from portal.rest.users.users import users from portal.rest.roles.roles import roles from portal.rest.groups.groups import groups db = SQLAlchemy() db = SQLAlchemy() app = Flask('portal') app = Flask('portal') # TODO - urgent storage = Storage({ "submissions_dir": "todo", "test_files_dir": "todo", "workspace_dir": "todo" }) def config_app(flask_app): def config_app(flask_app): """Configures the Flask app based on production stage.""" """Configures the Flask app based on production stage.""" config_type = os.environ.get('PORTAL_CONFIG_TYPE', 'devel') # TODO: meh name config_type = os.environ.get('PORTAL_CONFIG_TYPE', 'devel') config_object = CONFIGURATIONS[config_type] config_object = CONFIGURATIONS[config_type] if config_object is None: if config_object is None: raise EnvironmentError() # TODO some custom ex raise EnvironmentError() # TODO some custom ex Loading @@ -20,6 +31,10 @@ def config_app(flask_app): def create_app(): def create_app(): # app configuration # app configuration config_app(app) config_app(app) app.register_blueprint(courses) app.register_blueprint(users) app.register_blueprint(roles) app.register_blueprint(groups) # database bind to app # database bind to app db.init_app(app) db.init_app(app) Loading
portal/database/models.py +4 −0 Original line number Original line Diff line number Diff line Loading @@ -315,6 +315,10 @@ class Submission(db.Model, EntityBase): review = db.relationship("Review", back_populates="submission", cascade="all, delete-orphan", review = db.relationship("Review", back_populates="submission", cascade="all, delete-orphan", passive_deletes=True, uselist=False) passive_deletes=True, uselist=False) def change_state(self, new_state): # open to extension (state transition validation, ...) self.state = new_state def __init__(self, user, project, parameters, review=None): def __init__(self, user, project, parameters, review=None): self.user = user self.user = user self.project = project self.project = project Loading
portal/rest/__init__.py +41 −111 Original line number Original line Diff line number Diff line from marshmallow import Schema, fields from marshmallow import Schema, fields, validates_schema, ValidationError from portal import db from marshmallow_enum import EnumField from portal.database.models import Course, Role, User, Project, Group from portal.tools.logging import log # Maybe: extract tuples used in 'only' to variables (-> in one place) from portal.database.models import SubmissionState def delete_entity(entity): db.session.remove(entity) db.session.commit() def write_entity(entity): db.session.add(entity) db.session.commit() def find_course(identifier): log.debug(f"Attempting course search by id.") course = Course.query.filter_by(id=identifier).first() if course: return course log.debug(f"Did not find course with id={identifier}.") log.debug(f"Attempting course search by codename.") course = Course.query.filter_by(codename=identifier).first() if course: return course log.debug(f"Did not find course with codename={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_group(course, identifier): log.debug(f"Attempting group search by id.") group = Group.query.filter_by(course=course).filter_by(id=identifier).first() if group: return group log.debug(f"Did not find group with id={identifier}.") log.debug(f"Attempting group search by name.") group = Group.query.filter_by(course=course).filter_by(name=identifier).first() if group: return group log.debug(f"Did not find group with name={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() if role: return role log.debug(f"Did not find role with id={identifier} in course {course.codename}.") log.debug(f"Attempting role search by name.") role = Role.query.filter_by(course=course).filter_by(name=identifier).first() if role: return role log.debug(f"Did not find role with name={identifier} in course {course.codename}.") return None def find_user(identifier): log.debug(f"Attempting user search by id.") user = User.query.filter_by(id=identifier).first() if user: return user log.debug(f"Did not find user with id={identifier}.") log.debug(f"Attempting user search by email.") user = User.query.filter_by(email=identifier).first() if user: return user log.debug(f"Did not find user with email={identifier}.") log.debug(f"Attempting user search by username.") user = User.query.filter_by(username=identifier).first() if user: return user log.debug(f"Did not find user with username={identifier}.") # not searching by uco return None class UserSchema(Schema): class UserSchema(Schema): id = fields.Str(dump_only=True) # TODO: check if i need the ids here at all id = fields.Str(dump_only=True) uco = fields.Int() uco = fields.Int() email = fields.Email() # TODO: check how this validates email = fields.Email() username = fields.Str() username = fields.Str() name = fields.Str() name = fields.Str() is_admin = fields.Bool() is_admin = fields.Bool() submissions = fields.Nested('SubmissionSchema', only=('id', 'note', 'state'), many=True) submissions = fields.Nested('SubmissionSchema', only=('id', 'note', 'state'), many=True) review_items = fields.Nested('ReviewItemSchema', only=('id',), many=True) # TODO: update 'only' review_items = fields.Nested('ReviewItemSchema', only=('id',), many=True) # TODO: what to show class CourseSchema(Schema): class CourseSchema(Schema): Loading @@ -123,7 +24,37 @@ class CourseSchema(Schema): projects = fields.Nested('ProjectSchema', only=('id', 'name'), many=True) projects = fields.Nested('ProjectSchema', only=('id', 'name'), many=True) class SubmissionFileParamsSchema(Schema): source = fields.Nested('SubmissionFileSourceSchema') class SubmissionFileSourceSchema(Schema): type = fields.Str() url = fields.Str() # git@gitlab.fi.muni.cz:foo/project.git branch = fields.Str() checkout = fields.Str() @validates_schema def validate_present_fields(self, data): if data.get('type') is None: raise ValidationError(f"Submission source type is required. Supported values: 'git', 'zip'.") if data['type'] not in ('git', 'zip'): raise ValidationError(f"Submission source unsupported: {data['type']}. Supported sources: git, zip. ") if data['type'] == 'git': if any(('url', 'branch', 'checkout')) not in data.keys: raise ValidationError(f"File source 'git' requires precisely 'url', 'branch' and 'checkout' attributes.") if data['type'] == 'zip': if any(('url', 'branch', 'checkout')) in data.keys: raise ValidationError(f"File source 'zip' does not require any other attributes.") class SubmissionCreateSchema(Schema): file_params = fields.Nested('SubmissionFileParamsSchema') project_params = fields.Dict() class ProjectSchema(Schema): class ProjectSchema(Schema): # TODO: add state as enum id = fields.Str(dump_only=True) id = fields.Str(dump_only=True) name = fields.Str() name = fields.Str() config = fields.Nested('ProjectConfigSchema', exclude=('id', '_submissions_allowed_from', '_submissions_allowed_to', config = fields.Nested('ProjectConfigSchema', exclude=('id', '_submissions_allowed_from', '_submissions_allowed_to', Loading Loading @@ -194,13 +125,12 @@ class GroupSchema(Schema): class SubmissionSchema(Schema): class SubmissionSchema(Schema): id = fields.Str(dump_only=True) id = fields.Str(dump_only=True) processing_scheduled_for = fields.LocalDateTime() parameters = fields.Str() state = fields.Str() # TODO: check this - enum note = fields.Str() note = fields.Str() user = fields.Nested('UserSchema', only=('id', 'uco', 'email', 'username')) state = EnumField(SubmissionState) project = fields.Nested('ProjectSchema', only=('id', 'name')) scheduled_for = fields.LocalDateTime() review = fields.Nested('ReviewSchema', only=('id',)) # maybe flatten the review here? parameters = fields.Dict() project = fields.Nested('ProjectSchema', only=('id', 'name', 'course.id')) # check dot notation user = fields.Str() # id class ReviewSchema(Schema): class ReviewSchema(Schema): Loading