Loading portal/database/models.py +158 −33 Original line number Diff line number Diff line import uuid import enum import datetime import pytz from flask_sqlalchemy import BaseQuery from sqlalchemy import event from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import aliased from typing import List from werkzeug.security import generate_password_hash, check_password_hash from portal.database import SubmissionState Loading @@ -15,17 +14,17 @@ from portal.database.mixins import EntityBase from portal.database.exceptions import PortalDbError from portal.tools import time # uuid as primary key source: # https://stackoverflow.com/questions/36806403/cant-render-element-of-type-class-sqlalchemy-dialects-postgresql-base-uuid from portal.tools.time import normalize_time def _repr(instance): no_include = {'password_hash', 'review', 'course', 'user', 'project', 'group', 'role', 'review_items'} result = f"{instance.__class__.__name__}: " for key, value in vars(instance).items(): if not key.startswith("_") and key not in ( 'password_hash', 'review', 'course', 'user', 'project', 'group', 'role', 'review_items'): if not key.startswith("_") and key not in no_include: result += f"{key}={value} " return result Loading @@ -46,14 +45,13 @@ class User(db.Model, EntityBase): def set_password(self, password: str): """Sets password for the user Args: password(str): Unhashed password """ self.password_hash = generate_password_hash(password) def verify_password(self, password: str) -> bool: """ """Verifies user's password Args: password(str): Unhashed password Loading @@ -64,7 +62,7 @@ class User(db.Model, EntityBase): @property def courses(self) -> list: """Gets courses the users is signed in """Gets courses the users is a part of Returns(list): List of courses """ Loading Loading @@ -99,11 +97,11 @@ class User(db.Model, EntityBase): .join(Role.users).filter(User.id == self.id) def query_groups_in_course(self, course: 'Course', project: 'Project' = None) -> BaseQuery: """Queries user's groups for course and project(optional) """Queries user's groups for course and project Args: course(Course): Course instance project(Project): Project Instance project(Project): Project Instance (optional) Returns(BaseQuery): BaseQuery that can be executed """ Loading @@ -114,11 +112,11 @@ class User(db.Model, EntityBase): return base_query.join(Group.users).filter(User.id == self.id) def get_groups_in_course(self, course: 'Course', project: 'Project' = None) -> list: """Gers user's groups for course and project(optional) """Gets user's groups for course and project (optional) Args: course(Course): Course instance project(Project): Project Instance project(Project): Project Instance (optional) Returns(list): List of groups """ Loading Loading @@ -155,15 +153,33 @@ class User(db.Model, EntityBase): role=role).all() def query_permissions_for_course(self, course: 'Course'): """Returns the query for users permissions in the course Args: course(Course): Course instance Returns: Query for the permissions """ permissions = RolePermissions.query.join(RolePermissions.role) \ .filter_by(course=course) \ .join(Role.users).filter(User.id == self.id) return permissions def get_permissions_for_course(self, course: 'Course'): def get_permissions_for_course(self, course: 'Course') -> List['RolePermissions']: """Gets list of permissions for the course Args: course(Course): Course instance Returns(list[Permissions]): List of the Permissions """ return self.query_permissions_for_course(course=course).all() def __init__(self, uco, email, username, is_admin=False) -> None: def __init__(self, uco: int = None, email: str = None, username: str = None, is_admin: bool = False) -> None: """Creates user instance Args: uco(int): User's UCO (school identifier) email(str): User's Email username(str): Username is_admin(bool): Whether user is admin """ self.uco = uco self.email = email self.username = username Loading @@ -187,7 +203,8 @@ class Course(db.Model, EntityBase): notes_access_token = db.Column(db.String(100)) # TODO: check length # Info on cascades: http://docs.sqlalchemy.org/en/latest/orm/cascades.html # Cascades here simulate a composition - roles, groups and projects exist only when associated to a course # Cascades here simulate a composition - roles, groups and projects # exist only when associated with a course roles = db.relationship("Role", back_populates="course", cascade="all, delete-orphan", passive_deletes=True) groups = db.relationship("Group", back_populates="course", cascade="all, delete-orphan", Loading @@ -195,7 +212,12 @@ class Course(db.Model, EntityBase): projects = db.relationship("Project", back_populates="course", cascade="all, delete-orphan", passive_deletes=True) def __init__(self, name, codename) -> None: def __init__(self, name: str = None, codename: str = None) -> None: """Creates course instance Args: name(str): Course name codename(str): Course codename """ self.name = name self.codename = codename Loading @@ -205,13 +227,23 @@ class Course(db.Model, EntityBase): def __eq__(self, other): return self.id == other.id def get_users_by_role(self, role: 'Role'): def get_users_by_role(self, role: 'Role') -> List['User']: """Gets all users in the course based on their role Args: role(Role): The role to filter by Returns(list[User]): List of users that have the role """ return User.query.join(User.roles) \ .filter((Role.id == role.id) & (Role.course == self)) .filter((Role.id == role.id) & (Role.course == self)).all() def get_users_by_group(self, group: 'Group'): def get_users_by_group(self, group: 'Group') -> List['User']: """Gets all users in the group Args: group(Group): The group to find users in Returns(list[User]): List of users that are in the group """ return User.query.join(User.groups) \ .filter((Group.id == group.id) & (Role.course == self)) .filter((Group.id == group.id) & (Role.course == self)).all() class ProjectState(enum.Enum): Loading Loading @@ -239,7 +271,14 @@ class Project(db.Model, EntityBase): ) def state(self, timestamp=time.NOW()) -> ProjectState: if not (self.config.submissions_allowed_from or self.config.submissions_allowed_to or self.config.archive_from): """Gets project state based on the timestamp Args: timestamp: Time for which the state should be calculated Returns: """ if not (self.config.submissions_allowed_from or self.config.submissions_allowed_to or self.config.archive_from): return ProjectState.INACTIVE if self.config.submissions_allowed_from <= timestamp < self.config.submissions_allowed_to: return ProjectState.ACTIVE Loading @@ -249,14 +288,39 @@ class Project(db.Model, EntityBase): return ProjectState.INACTIVE def set_config(self, **kwargs): """Sets project configuration Args: test_files_source(str): Repository URL file_whitelist(str): Filter string for whitelist pre_submit_script(str): Pre submit script used in the submissions processing post_submit_script(str): Post submit script used in the submissions processing submission_parameters(str): Schema that defines all the parameters that should be passed to submission submission_scheduler_config(str): Configuration for the submissions scheduler submissions_allowed_from(time): Time from all the submissions are allowed from submissions_allowed_to(time): Time to which all submissions are allowed to archive_from(time): Archive all submissions from """ 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', '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: def __init__(self, course: Course, name: str = None, test_files_source: str = None): """Creates instance of the project Args: course(Course): Course instance name(str): Project name test_files_source(str): Url to test files """ self.course = course self.name = name self.config = ProjectConfig(project=self, test_files_source=test_files_source) Loading Loading @@ -333,7 +397,12 @@ class ProjectConfig(db.Model, EntityBase): raise PortalDbError() self._archive_from = time.strip_seconds(archive_from) def __init__(self, project, test_files_source) -> None: def __init__(self, project: Project, test_files_source: str = None): """Creates instance of the project's config Args: project(Project): Project instance test_files_source: Source files """ self.project = project self.test_files_source = test_files_source Loading @@ -358,6 +427,10 @@ class Role(db.Model, EntityBase): passive_deletes=True, uselist=False) def set_permissions(self, **kwargs): """Sets permissions for the role Args: **kwargs(dict): Permissions """ for k, w in kwargs.items(): if hasattr(self.permissions, k) and k not in ('id', 'role_id', 'role'): setattr(self.permissions, k, w) Loading @@ -366,7 +439,12 @@ class Role(db.Model, EntityBase): db.UniqueConstraint('course_id', 'name', name='course_unique_name'), ) def __init__(self, course, name): def __init__(self, course: Course, name: str = None): """Creates instance of the role in a course Args: course(Course): Course instance name(str): Name of the role """ self.course = course self.name = name self.permissions = RolePermissions(self) Loading Loading @@ -418,7 +496,11 @@ class RolePermissions(db.Model, EntityBase): write_reviews_group = db.Column(db.Boolean, default=False, nullable=False) write_reviews_own = db.Column(db.Boolean, default=False, nullable=False) def __init__(self, role): def __init__(self, role: Role): """Creates instance of the permissions for the role Args: role: """ self.role = role def __repr__(self): Loading @@ -441,7 +523,12 @@ class Group(db.Model, EntityBase): db.UniqueConstraint('course_id', 'name', name='course_unique_name'), ) def __init__(self, course, name) -> None: def __init__(self, course: Course, name: str = None): """Creates instance of the group Args: course(Course): Course instance name(str): Name of the role """ self.course = course self.name = name Loading Loading @@ -471,7 +558,15 @@ class Submission(db.Model, EntityBase): # open to extension (state transition validation, ...) self.state = new_state def __init__(self, user, project, parameters, review=None): def __init__(self, user: User, project: Project, parameters: str = None, review: 'Review' = None): """Creates a new submission instance Args: user(User): User instance project(Project): Project instance parameters(str): Parameters for the submission review(Review): Review instance """ self.user = user self.project = project self.review = review Loading @@ -493,7 +588,11 @@ class Review(db.Model, EntityBase): review_items = db.relationship("ReviewItem", back_populates="review", cascade="all, delete-orphan", passive_deletes=True) def __init__(self, submission): def __init__(self, submission: Submission): """Creates a review for the submission Args: submission(Submission): """ self.submission = submission def __repr__(self): Loading @@ -512,10 +611,18 @@ class ReviewItem(db.Model, EntityBase): user = db.relationship("User", uselist=False, back_populates="review_items") # the review item's author content = db.Column(db.Text) file = db.Column(db.String(100), nullable=False) file = db.Column(db.String(256), nullable=False) line = db.Column(db.Integer, nullable=False) def __init__(self, user, review, file, line, content): def __init__(self, user: User, review: Review, file: str, line: int, content: str): """Creates a review item in the review Args: user(User): The author of the review item review(Review): Review the item belongs to file(str): File the review item binds to line(int): Line of the file content: Review item content (text) """ self.review = review self.user = user self.file = file Loading @@ -535,6 +642,24 @@ class Component(db.Model, EntityBase): name = db.Column(db.String(30), nullable=False, unique=True) secret = db.Column(db.String(250)) ip_address = db.Column(db.String(50)) type = db.Column(db.String(25)) notes = db.Column(db.Text) def __init__(self, name: str = None, secret: str = None, ip_address: str = None, type: str = None, notes: str = None): """Creates component Args: name(str): Name of the component secret(str): Component's secret key ip_address(str): Component's IP address type(str): Type of the component notes(str): Additional notes for the component """ self.name = name self.secret = secret self.ip_address = ip_address self.notes = notes self.type = type # source: http://docs.sqlalchemy.org/en/latest/orm/events.html Loading portal/rest/__init__.py +2 −0 Original line number Diff line number Diff line Loading @@ -173,6 +173,8 @@ class ComponentSchema(Schema): name = fields.Str() access_token = fields.Str() ip_address = fields.Str() type = fields.Str() notes = fields.Str() class CourseImportConfigSchema(Schema): Loading portal/rest/components/components.py +88 −0 Original line number Diff line number Diff line import logging from flask import Blueprint from flask_jwt_extended import jwt_required from flask_restful import Api, Resource from portal.rest import component_schema, rest_helpers from portal.service import permissions, auth from portal.service.auth import find_client from portal.service.components import create_component, delete_component, update_component, \ find_all_components from portal.service.errors import ForbiddenError from portal.service.general import find_component from portal.tools.decorators import error_handler components = Blueprint('components', __name__) components_api = Api(components) log = logging.getLogger(__name__) class ComponentList(Resource): @error_handler @jwt_required def get(self): client = auth.find_client() # authorization if not permissions.check_sysadmin(client): raise ForbiddenError(uid=client.id) components_list = find_all_components() return component_schema.dump(components_list), 200 @error_handler @jwt_required def post(self): client = auth.find_client() # authorization if not permissions.check_sysadmin(client): raise ForbiddenError(uid=client.id) data = rest_helpers.parse_request_data( component_schema, action='create', resource='component' ) new_component = create_component(data) return component_schema.dump(new_component)[0], 201 class ComponentResource(Resource): @error_handler @jwt_required def get(self, cid): client = find_client() if not permissions.check_sysadmin(client): raise ForbiddenError(uid=client.id) component = find_component(cid) return component_schema.dump(component)[0], 200 @error_handler @jwt_required def delete(self, cid): client = find_client() # authorization if not permissions.check_sysadmin(client): raise ForbiddenError(uid=client.id) component = find_component(cid) delete_component(component) return '', 204 @error_handler @jwt_required def put(self, cid): client = find_client() # authorization if not permissions.check_sysadmin(client): raise ForbiddenError(uid=client.id) data = rest_helpers.parse_request_data( component_schema, action='update', resource='component' ) component = find_component(cid) update_component(component, data) return '', 204 components_api.add_resource(ComponentList, '') components_api.add_resource(ComponentResource, '/<string:cid>') portal/rest/courses/courses.py +14 −24 Original line number Diff line number Diff line import logging from flask import Blueprint, request from flask import Blueprint from flask_jwt_extended import jwt_required from flask_restful import Api, Resource from portal.database.models import Course from portal.rest import course_schema, courses_schema, course_import_schema from portal.rest import course_schema, courses_schema, course_import_schema, rest_helpers from portal.service.courses import delete_course, update_course, create_course, \ update_notes_token, copy_course, filter_course_dump update_notes_token, copy_course, filter_course_dump, find_all_courses from portal.service.general import find_course from portal.service.errors import PortalAPIError, ForbiddenError from portal.service.errors import ForbiddenError from portal.service.permissions import check_client from portal.service.auth import find_client from portal.tools.decorators import error_handler Loading Loading @@ -46,11 +45,11 @@ class CourseResource(Resource): @jwt_required def delete(self, cid): client = find_client() course = find_course(cid) # authorization if not permissions.check_sysadmin(client): raise ForbiddenError(uid=client.id) course = find_course(cid) delete_course(course) return '', 204 Loading @@ -63,11 +62,9 @@ class CourseResource(Resource): if not (check_client(client=client, course=course, permissions=['update_course'])): raise ForbiddenError(uid=client.id) json_data = request.get_json() if not json_data: raise PortalAPIError(400, message='No data provided for course update.') data = course_schema.load(json_data)[0] data = rest_helpers.parse_request_data( schema=course_schema, action='update', resource='course' ) update_course(course, data) return '', 204 Loading @@ -81,7 +78,7 @@ class CourseList(Resource): if not permissions.check_sysadmin(client): raise ForbiddenError(uid=client.id) courses_list = Course.query.all() courses_list = find_all_courses() return courses_schema.dump(courses_list) @error_handler Loading @@ -92,11 +89,7 @@ class CourseList(Resource): if not permissions.check_sysadmin(client): raise ForbiddenError(uid=client.id) json_data = request.get_json() if not json_data: raise PortalAPIError(400, message='No data provided for course creation.') data = course_schema.load(json_data)[0] data = rest_helpers.parse_request_data(course_schema, resource='course', action='create') new_course = create_course(data) return course_schema.dump(new_course)[0], 201 Loading Loading @@ -124,9 +117,7 @@ class CourseNotesToken(Resource): permissions=['handle_notes_access_token'])): raise ForbiddenError(uid=client.id) json_data = request.get_json() if not json_data: raise PortalAPIError(400, message='No data provided for notes token update.') json_data = rest_helpers.require_data(action='update_notes_token', resource='course') update_notes_token(course, json_data) return '', 204 Loading @@ -142,10 +133,9 @@ class CourseImport(Resource): if not check_client(client=client, course=course, permissions=['update_course']): raise ForbiddenError(uid=client.id) json_data = request.get_json() if not json_data: raise PortalAPIError(400, message='No data provided for notes token update.') data = course_import_schema.load(json_data)[0] data = rest_helpers.parse_request_data( course_import_schema, action='import', resource='course' ) source_course = find_course(data['source_course']) config = data['config'] Loading portal/rest/groups/groups.py +28 −33 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
portal/database/models.py +158 −33 Original line number Diff line number Diff line import uuid import enum import datetime import pytz from flask_sqlalchemy import BaseQuery from sqlalchemy import event from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import aliased from typing import List from werkzeug.security import generate_password_hash, check_password_hash from portal.database import SubmissionState Loading @@ -15,17 +14,17 @@ from portal.database.mixins import EntityBase from portal.database.exceptions import PortalDbError from portal.tools import time # uuid as primary key source: # https://stackoverflow.com/questions/36806403/cant-render-element-of-type-class-sqlalchemy-dialects-postgresql-base-uuid from portal.tools.time import normalize_time def _repr(instance): no_include = {'password_hash', 'review', 'course', 'user', 'project', 'group', 'role', 'review_items'} result = f"{instance.__class__.__name__}: " for key, value in vars(instance).items(): if not key.startswith("_") and key not in ( 'password_hash', 'review', 'course', 'user', 'project', 'group', 'role', 'review_items'): if not key.startswith("_") and key not in no_include: result += f"{key}={value} " return result Loading @@ -46,14 +45,13 @@ class User(db.Model, EntityBase): def set_password(self, password: str): """Sets password for the user Args: password(str): Unhashed password """ self.password_hash = generate_password_hash(password) def verify_password(self, password: str) -> bool: """ """Verifies user's password Args: password(str): Unhashed password Loading @@ -64,7 +62,7 @@ class User(db.Model, EntityBase): @property def courses(self) -> list: """Gets courses the users is signed in """Gets courses the users is a part of Returns(list): List of courses """ Loading Loading @@ -99,11 +97,11 @@ class User(db.Model, EntityBase): .join(Role.users).filter(User.id == self.id) def query_groups_in_course(self, course: 'Course', project: 'Project' = None) -> BaseQuery: """Queries user's groups for course and project(optional) """Queries user's groups for course and project Args: course(Course): Course instance project(Project): Project Instance project(Project): Project Instance (optional) Returns(BaseQuery): BaseQuery that can be executed """ Loading @@ -114,11 +112,11 @@ class User(db.Model, EntityBase): return base_query.join(Group.users).filter(User.id == self.id) def get_groups_in_course(self, course: 'Course', project: 'Project' = None) -> list: """Gers user's groups for course and project(optional) """Gets user's groups for course and project (optional) Args: course(Course): Course instance project(Project): Project Instance project(Project): Project Instance (optional) Returns(list): List of groups """ Loading Loading @@ -155,15 +153,33 @@ class User(db.Model, EntityBase): role=role).all() def query_permissions_for_course(self, course: 'Course'): """Returns the query for users permissions in the course Args: course(Course): Course instance Returns: Query for the permissions """ permissions = RolePermissions.query.join(RolePermissions.role) \ .filter_by(course=course) \ .join(Role.users).filter(User.id == self.id) return permissions def get_permissions_for_course(self, course: 'Course'): def get_permissions_for_course(self, course: 'Course') -> List['RolePermissions']: """Gets list of permissions for the course Args: course(Course): Course instance Returns(list[Permissions]): List of the Permissions """ return self.query_permissions_for_course(course=course).all() def __init__(self, uco, email, username, is_admin=False) -> None: def __init__(self, uco: int = None, email: str = None, username: str = None, is_admin: bool = False) -> None: """Creates user instance Args: uco(int): User's UCO (school identifier) email(str): User's Email username(str): Username is_admin(bool): Whether user is admin """ self.uco = uco self.email = email self.username = username Loading @@ -187,7 +203,8 @@ class Course(db.Model, EntityBase): notes_access_token = db.Column(db.String(100)) # TODO: check length # Info on cascades: http://docs.sqlalchemy.org/en/latest/orm/cascades.html # Cascades here simulate a composition - roles, groups and projects exist only when associated to a course # Cascades here simulate a composition - roles, groups and projects # exist only when associated with a course roles = db.relationship("Role", back_populates="course", cascade="all, delete-orphan", passive_deletes=True) groups = db.relationship("Group", back_populates="course", cascade="all, delete-orphan", Loading @@ -195,7 +212,12 @@ class Course(db.Model, EntityBase): projects = db.relationship("Project", back_populates="course", cascade="all, delete-orphan", passive_deletes=True) def __init__(self, name, codename) -> None: def __init__(self, name: str = None, codename: str = None) -> None: """Creates course instance Args: name(str): Course name codename(str): Course codename """ self.name = name self.codename = codename Loading @@ -205,13 +227,23 @@ class Course(db.Model, EntityBase): def __eq__(self, other): return self.id == other.id def get_users_by_role(self, role: 'Role'): def get_users_by_role(self, role: 'Role') -> List['User']: """Gets all users in the course based on their role Args: role(Role): The role to filter by Returns(list[User]): List of users that have the role """ return User.query.join(User.roles) \ .filter((Role.id == role.id) & (Role.course == self)) .filter((Role.id == role.id) & (Role.course == self)).all() def get_users_by_group(self, group: 'Group'): def get_users_by_group(self, group: 'Group') -> List['User']: """Gets all users in the group Args: group(Group): The group to find users in Returns(list[User]): List of users that are in the group """ return User.query.join(User.groups) \ .filter((Group.id == group.id) & (Role.course == self)) .filter((Group.id == group.id) & (Role.course == self)).all() class ProjectState(enum.Enum): Loading Loading @@ -239,7 +271,14 @@ class Project(db.Model, EntityBase): ) def state(self, timestamp=time.NOW()) -> ProjectState: if not (self.config.submissions_allowed_from or self.config.submissions_allowed_to or self.config.archive_from): """Gets project state based on the timestamp Args: timestamp: Time for which the state should be calculated Returns: """ if not (self.config.submissions_allowed_from or self.config.submissions_allowed_to or self.config.archive_from): return ProjectState.INACTIVE if self.config.submissions_allowed_from <= timestamp < self.config.submissions_allowed_to: return ProjectState.ACTIVE Loading @@ -249,14 +288,39 @@ class Project(db.Model, EntityBase): return ProjectState.INACTIVE def set_config(self, **kwargs): """Sets project configuration Args: test_files_source(str): Repository URL file_whitelist(str): Filter string for whitelist pre_submit_script(str): Pre submit script used in the submissions processing post_submit_script(str): Post submit script used in the submissions processing submission_parameters(str): Schema that defines all the parameters that should be passed to submission submission_scheduler_config(str): Configuration for the submissions scheduler submissions_allowed_from(time): Time from all the submissions are allowed from submissions_allowed_to(time): Time to which all submissions are allowed to archive_from(time): Archive all submissions from """ 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', '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: def __init__(self, course: Course, name: str = None, test_files_source: str = None): """Creates instance of the project Args: course(Course): Course instance name(str): Project name test_files_source(str): Url to test files """ self.course = course self.name = name self.config = ProjectConfig(project=self, test_files_source=test_files_source) Loading Loading @@ -333,7 +397,12 @@ class ProjectConfig(db.Model, EntityBase): raise PortalDbError() self._archive_from = time.strip_seconds(archive_from) def __init__(self, project, test_files_source) -> None: def __init__(self, project: Project, test_files_source: str = None): """Creates instance of the project's config Args: project(Project): Project instance test_files_source: Source files """ self.project = project self.test_files_source = test_files_source Loading @@ -358,6 +427,10 @@ class Role(db.Model, EntityBase): passive_deletes=True, uselist=False) def set_permissions(self, **kwargs): """Sets permissions for the role Args: **kwargs(dict): Permissions """ for k, w in kwargs.items(): if hasattr(self.permissions, k) and k not in ('id', 'role_id', 'role'): setattr(self.permissions, k, w) Loading @@ -366,7 +439,12 @@ class Role(db.Model, EntityBase): db.UniqueConstraint('course_id', 'name', name='course_unique_name'), ) def __init__(self, course, name): def __init__(self, course: Course, name: str = None): """Creates instance of the role in a course Args: course(Course): Course instance name(str): Name of the role """ self.course = course self.name = name self.permissions = RolePermissions(self) Loading Loading @@ -418,7 +496,11 @@ class RolePermissions(db.Model, EntityBase): write_reviews_group = db.Column(db.Boolean, default=False, nullable=False) write_reviews_own = db.Column(db.Boolean, default=False, nullable=False) def __init__(self, role): def __init__(self, role: Role): """Creates instance of the permissions for the role Args: role: """ self.role = role def __repr__(self): Loading @@ -441,7 +523,12 @@ class Group(db.Model, EntityBase): db.UniqueConstraint('course_id', 'name', name='course_unique_name'), ) def __init__(self, course, name) -> None: def __init__(self, course: Course, name: str = None): """Creates instance of the group Args: course(Course): Course instance name(str): Name of the role """ self.course = course self.name = name Loading Loading @@ -471,7 +558,15 @@ class Submission(db.Model, EntityBase): # open to extension (state transition validation, ...) self.state = new_state def __init__(self, user, project, parameters, review=None): def __init__(self, user: User, project: Project, parameters: str = None, review: 'Review' = None): """Creates a new submission instance Args: user(User): User instance project(Project): Project instance parameters(str): Parameters for the submission review(Review): Review instance """ self.user = user self.project = project self.review = review Loading @@ -493,7 +588,11 @@ class Review(db.Model, EntityBase): review_items = db.relationship("ReviewItem", back_populates="review", cascade="all, delete-orphan", passive_deletes=True) def __init__(self, submission): def __init__(self, submission: Submission): """Creates a review for the submission Args: submission(Submission): """ self.submission = submission def __repr__(self): Loading @@ -512,10 +611,18 @@ class ReviewItem(db.Model, EntityBase): user = db.relationship("User", uselist=False, back_populates="review_items") # the review item's author content = db.Column(db.Text) file = db.Column(db.String(100), nullable=False) file = db.Column(db.String(256), nullable=False) line = db.Column(db.Integer, nullable=False) def __init__(self, user, review, file, line, content): def __init__(self, user: User, review: Review, file: str, line: int, content: str): """Creates a review item in the review Args: user(User): The author of the review item review(Review): Review the item belongs to file(str): File the review item binds to line(int): Line of the file content: Review item content (text) """ self.review = review self.user = user self.file = file Loading @@ -535,6 +642,24 @@ class Component(db.Model, EntityBase): name = db.Column(db.String(30), nullable=False, unique=True) secret = db.Column(db.String(250)) ip_address = db.Column(db.String(50)) type = db.Column(db.String(25)) notes = db.Column(db.Text) def __init__(self, name: str = None, secret: str = None, ip_address: str = None, type: str = None, notes: str = None): """Creates component Args: name(str): Name of the component secret(str): Component's secret key ip_address(str): Component's IP address type(str): Type of the component notes(str): Additional notes for the component """ self.name = name self.secret = secret self.ip_address = ip_address self.notes = notes self.type = type # source: http://docs.sqlalchemy.org/en/latest/orm/events.html Loading
portal/rest/__init__.py +2 −0 Original line number Diff line number Diff line Loading @@ -173,6 +173,8 @@ class ComponentSchema(Schema): name = fields.Str() access_token = fields.Str() ip_address = fields.Str() type = fields.Str() notes = fields.Str() class CourseImportConfigSchema(Schema): Loading
portal/rest/components/components.py +88 −0 Original line number Diff line number Diff line import logging from flask import Blueprint from flask_jwt_extended import jwt_required from flask_restful import Api, Resource from portal.rest import component_schema, rest_helpers from portal.service import permissions, auth from portal.service.auth import find_client from portal.service.components import create_component, delete_component, update_component, \ find_all_components from portal.service.errors import ForbiddenError from portal.service.general import find_component from portal.tools.decorators import error_handler components = Blueprint('components', __name__) components_api = Api(components) log = logging.getLogger(__name__) class ComponentList(Resource): @error_handler @jwt_required def get(self): client = auth.find_client() # authorization if not permissions.check_sysadmin(client): raise ForbiddenError(uid=client.id) components_list = find_all_components() return component_schema.dump(components_list), 200 @error_handler @jwt_required def post(self): client = auth.find_client() # authorization if not permissions.check_sysadmin(client): raise ForbiddenError(uid=client.id) data = rest_helpers.parse_request_data( component_schema, action='create', resource='component' ) new_component = create_component(data) return component_schema.dump(new_component)[0], 201 class ComponentResource(Resource): @error_handler @jwt_required def get(self, cid): client = find_client() if not permissions.check_sysadmin(client): raise ForbiddenError(uid=client.id) component = find_component(cid) return component_schema.dump(component)[0], 200 @error_handler @jwt_required def delete(self, cid): client = find_client() # authorization if not permissions.check_sysadmin(client): raise ForbiddenError(uid=client.id) component = find_component(cid) delete_component(component) return '', 204 @error_handler @jwt_required def put(self, cid): client = find_client() # authorization if not permissions.check_sysadmin(client): raise ForbiddenError(uid=client.id) data = rest_helpers.parse_request_data( component_schema, action='update', resource='component' ) component = find_component(cid) update_component(component, data) return '', 204 components_api.add_resource(ComponentList, '') components_api.add_resource(ComponentResource, '/<string:cid>')
portal/rest/courses/courses.py +14 −24 Original line number Diff line number Diff line import logging from flask import Blueprint, request from flask import Blueprint from flask_jwt_extended import jwt_required from flask_restful import Api, Resource from portal.database.models import Course from portal.rest import course_schema, courses_schema, course_import_schema from portal.rest import course_schema, courses_schema, course_import_schema, rest_helpers from portal.service.courses import delete_course, update_course, create_course, \ update_notes_token, copy_course, filter_course_dump update_notes_token, copy_course, filter_course_dump, find_all_courses from portal.service.general import find_course from portal.service.errors import PortalAPIError, ForbiddenError from portal.service.errors import ForbiddenError from portal.service.permissions import check_client from portal.service.auth import find_client from portal.tools.decorators import error_handler Loading Loading @@ -46,11 +45,11 @@ class CourseResource(Resource): @jwt_required def delete(self, cid): client = find_client() course = find_course(cid) # authorization if not permissions.check_sysadmin(client): raise ForbiddenError(uid=client.id) course = find_course(cid) delete_course(course) return '', 204 Loading @@ -63,11 +62,9 @@ class CourseResource(Resource): if not (check_client(client=client, course=course, permissions=['update_course'])): raise ForbiddenError(uid=client.id) json_data = request.get_json() if not json_data: raise PortalAPIError(400, message='No data provided for course update.') data = course_schema.load(json_data)[0] data = rest_helpers.parse_request_data( schema=course_schema, action='update', resource='course' ) update_course(course, data) return '', 204 Loading @@ -81,7 +78,7 @@ class CourseList(Resource): if not permissions.check_sysadmin(client): raise ForbiddenError(uid=client.id) courses_list = Course.query.all() courses_list = find_all_courses() return courses_schema.dump(courses_list) @error_handler Loading @@ -92,11 +89,7 @@ class CourseList(Resource): if not permissions.check_sysadmin(client): raise ForbiddenError(uid=client.id) json_data = request.get_json() if not json_data: raise PortalAPIError(400, message='No data provided for course creation.') data = course_schema.load(json_data)[0] data = rest_helpers.parse_request_data(course_schema, resource='course', action='create') new_course = create_course(data) return course_schema.dump(new_course)[0], 201 Loading Loading @@ -124,9 +117,7 @@ class CourseNotesToken(Resource): permissions=['handle_notes_access_token'])): raise ForbiddenError(uid=client.id) json_data = request.get_json() if not json_data: raise PortalAPIError(400, message='No data provided for notes token update.') json_data = rest_helpers.require_data(action='update_notes_token', resource='course') update_notes_token(course, json_data) return '', 204 Loading @@ -142,10 +133,9 @@ class CourseImport(Resource): if not check_client(client=client, course=course, permissions=['update_course']): raise ForbiddenError(uid=client.id) json_data = request.get_json() if not json_data: raise PortalAPIError(400, message='No data provided for notes token update.') data = course_import_schema.load(json_data)[0] data = rest_helpers.parse_request_data( course_import_schema, action='import', resource='course' ) source_course = find_course(data['source_course']) config = data['config'] Loading
portal/rest/groups/groups.py +28 −33 File changed.Preview size limit exceeded, changes collapsed. Show changes