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

documentation, refactor

parent 55464f6a
Loading
Loading
Loading
Loading
+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
@@ -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

@@ -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

@@ -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
        """
@@ -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
        """
@@ -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
        """
@@ -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
@@ -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",
@@ -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

@@ -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):
@@ -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
@@ -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)
@@ -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

@@ -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)
@@ -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)
@@ -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):
@@ -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

@@ -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
@@ -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):
@@ -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
@@ -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
+2 −0
Original line number Diff line number Diff line
@@ -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):
+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>')
+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
@@ -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

@@ -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

@@ -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
@@ -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

@@ -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
@@ -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']

+28 −33

File changed.

Preview size limit exceeded, changes collapsed.

Loading