Verified Commit 1f81087f authored by Peter Stanko's avatar Peter Stanko
Browse files

CRLF -> LF; BASE_PARAMS and LIST PARAMS

parent 8af23ea7
Loading
Loading
Loading
Loading
+115 −69
Original line number Diff line number Diff line
"""
A collection of Mixins specifying common behaviour and attributes of database entities.
"""
import datetime
from typing import List

from sqlalchemy.ext.hybrid import hybrid_property

@@ -9,22 +9,67 @@ from portal import db
# maybe use server_default, server_onupdate instead of default, onupdate; crashes tests
# (https://stackoverflow.com/questions/13370317/sqlalchemy-default-datetime)
# pylint: disable=too-few-public-methods
from portal.tools.meta import bound_update_class_var
from portal.tools.naming import sanitize_code_name


class EntityBase(object):
def _repr(instance) -> str:
    """Repr helper function
    Args:
        instance: Instance of the model

    Returns(str): String representation of the model
    """
    if isinstance(instance, EntityBase):
        params = {key: getattr(instance, key) for key in instance.base_params()}
        return str(params)
    return instance.__dict__


def _str(instance) -> str:
    if hasattr(instance, 'log_name'):
        return instance.log_name
    return _repr(instance=instance)


class EntityBase:
    """Entity mixin for the models
    Attributes:

    Class attributes:
        created_at(Column): Date when the entity has been created
        updated_at(Column): Date when the entity has been updated
    """
    EXCLUDED = []
    BASE_PARAMS = ['created_at', 'updated_at']
    LISTABLE = []
    UPDATABLE = []

    @classmethod
    def base_params(cls) -> List[str]:
        return bound_update_class_var(cls, 'BASE_PARAMS')

    @classmethod
    def updatable_params(cls) -> List[str]:
        return bound_update_class_var(cls, 'UPDATABLE')

    @classmethod
    def listable_params(cls) -> List[str]:
        return bound_update_class_var(cls, 'LISTABLE')

    created_at = db.Column(db.TIMESTAMP, default=db.func.now())
    updated_at = db.Column(
        db.TIMESTAMP, default=db.func.now(), onupdate=db.func.now())
    updated_at = db.Column(db.TIMESTAMP, default=db.func.now(), onupdate=db.func.now())

    def __repr__(self):
        return _repr(self)

    def __str__(self):
        return _str(self)


class NamedMixin:
    LISTABLE = ['name', 'codename']
    UPDATABLE = [*LISTABLE, 'description']
    BASE_PARAMS = [*UPDATABLE]

class NamedMixin(object):
    _name = db.Column('name', db.String(50), nullable=False)
    _codename = db.Column('codename', db.String(30), nullable=False)
    description = db.Column(db.Text)
@@ -66,4 +111,5 @@ class NamedMixin(object):
        if self._codename is None:
            self.codename = value
        self._name = value

# pylint: enable=too-few-public-methods
+89 −111
Original line number Diff line number Diff line
@@ -7,10 +7,12 @@ import logging
import uuid
from typing import List

import sqlalchemy as sa
from flask_sqlalchemy import BaseQuery
from sqlalchemy import event
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy_continuum import make_versioned
from werkzeug.security import check_password_hash, generate_password_hash

from portal import db
@@ -19,34 +21,11 @@ from portal.database.mixins import EntityBase, NamedMixin
from portal.database.types import JSONEncodedDict, YAMLEncodedDict
from portal.tools import time
from portal.tools.time import normalize_time
from sqlalchemy_continuum import make_versioned
import sqlalchemy as sa

make_versioned(user_cls=None)
log = logging.getLogger(__name__)


def _repr(instance) -> str:
    """Repr helper function
    Args:
        instance: Instance of the model

    Returns(str): String representation of the model
    """
    no_include = {'password_hash', 'review', 'course', 'user', 'project', 'group', 'role',
                  'review_items', 'client', 'EXCLUDED', 'query', 'groups', 'roles', 'secrets',
                  'courses', 'projects'}
    if 'EXCLUDED' in vars(instance.__class__):
        no_include.update(instance.__class__.EXCLUDED)
    result = f"{instance.__class__.__name__}: "
    for key in dir(instance):
        if not key.startswith("_") and key not in no_include:
            value = getattr(instance, key)
            if not callable(value):
                result += f"{key}={value} "
    return result


class ClientType(enum.Enum):
    """All known client types
    """
@@ -54,12 +33,13 @@ class ClientType(enum.Enum):
    WORKER = 'worker'


class Client(db.Model):
class Client(db.Model, EntityBase):
    """Client entity model
    """
    __tablename__ = 'client'
    id = db.Column(db.String(length=36), default=lambda: str(
        uuid.uuid4()), primary_key=True)
    LISTABLE = ['id', 'type', 'codename']
    UPDATABLE = ['codename']
    id = db.Column(db.String(length=36), default=lambda: str(uuid.uuid4()), primary_key=True)
    type = db.Column(db.Enum(ClientType, name='ClientType'), nullable=False)
    codename = db.Column(db.String(30), unique=True, nullable=False)
    secrets = db.relationship('Secret', back_populates='client', uselist=True,
@@ -141,9 +121,10 @@ class Client(db.Model):

class Secret(db.Model, EntityBase):
    __tablename__ = 'secret'
    EXCLUDED = ['value', 'client']
    id = db.Column(db.String(length=36), default=lambda: str(
        uuid.uuid4()), primary_key=True)
    UPDATABLE = ['name', 'expires_at']
    BASE_PARAMS = ['id', *UPDATABLE]

    id = db.Column(db.String(length=36), default=lambda: str(uuid.uuid4()), primary_key=True)
    name = db.Column(db.String(40), nullable=False)
    value = db.Column(db.String(120))
    expires_at = db.Column(db.TIMESTAMP, nullable=True)
@@ -176,7 +157,7 @@ class Secret(db.Model, EntityBase):
        return time.normalize_time(self.expires_at) < time.current_time()


class User(EntityBase, Client):
class User(Client):
    """User entity model

    Attributes:
@@ -192,10 +173,11 @@ class User(EntityBase, Client):
        review_items: Collection of review_items created by user
        groups: Collection of groups the user belongs to
    """
    EXCLUDED = ['password_hash', 'submissions', 'review_items']
    UPDATABLE = ['name', 'email', 'uco', 'gitlab_username', 'managed']
    BASE_PARAMS = ['username', 'codename', 'is_admin', *UPDATABLE, 'managed', 'id']
    __tablename__ = 'user'
    id = db.Column(db.String(length=36), db.ForeignKey('client.id'), default=lambda: str(
        uuid.uuid4()), primary_key=True)
    id = db.Column(db.String(length=36), db.ForeignKey('client.id'),
                   default=lambda: str(uuid.uuid4()), primary_key=True)
    uco = db.Column(db.Integer)
    email = db.Column(db.String(50), unique=True, nullable=False)
    name = db.Column(db.String(50))
@@ -203,6 +185,8 @@ class User(EntityBase, Client):
    # optional - after password change
    password_hash = db.Column(db.String(120), default=None)

    gitlab_username = db.Column(db.String(50), nullable=True)
    managed = db.Column(db.Boolean, default=False)
    submissions = db.relationship("Submission", back_populates="user", cascade="all, delete-orphan",
                                  passive_deletes=True)
    review_items = db.relationship("ReviewItem", back_populates="user")
@@ -317,9 +301,6 @@ class User(EntityBase, Client):
        self.is_admin = is_admin
        super().__init__(ClientType.USER, **kwargs)

    def __repr__(self):
        return _repr(self)

    def __eq__(self, other):
        return self.id == other.id

@@ -334,15 +315,28 @@ def _get_class_based_on_client_type(client_type):
    return klass


class CourseState(enum.Enum):
    """All the states in which the project can be
    """
    ACTIVE = 1
    INACTIVE = 2
    ARCHIVED = 3


class Course(db.Model, EntityBase, NamedMixin):
    """Course model
    """
    EXCLUDED = ['notes_access_token', 'roles', 'groups', 'projects']
    UPDATABLE = ['faculty_id']
    BASE_PARAMS = ['id', *UPDATABLE, 'namespace']
    LISTABLE = [*BASE_PARAMS]
    __tablename__ = 'course'
    id = db.Column(db.String(length=36), default=lambda: str(
        uuid.uuid4()), primary_key=True)
    id = db.Column(db.String(length=36), default=lambda: str(uuid.uuid4()), primary_key=True)
    notes_access_token = db.Column(db.String(256))
    faculty_id = db.Column(db.Integer)

    state = db.Column(db.Enum(CourseState, name='CourseState'), nullable=False,
                      default=CourseState.ACTIVE)

    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",
@@ -381,9 +375,6 @@ class Course(db.Model, EntityBase, NamedMixin):
        self.notes_access_token = notes_access_token
        self.faculty_id = faculty_id

    def __repr__(self):
        return _repr(self)

    def __eq__(self, other):
        return self.id == other.id

@@ -434,7 +425,7 @@ class ProjectState(enum.Enum):

class Project(db.Model, EntityBase, NamedMixin):
    """Project model
    Attributes:
    Class Attributes:
        id: UUID of the project
        name: Name of the project
        description: Description of the project
@@ -443,10 +434,12 @@ class Project(db.Model, EntityBase, NamedMixin):
        course: course associated with the project
        submissions: list of submissions associated with the project
    """
    EXCLUDED = ['course', 'submissions', 'config']
    UPDATABLE = ['assignment_url', 'submit_instructions', 'submit_configurable']
    BASE_PARAMS = ['id', *UPDATABLE, 'namespace']
    LISTABLE = ['id', 'assignment_url', 'namespace', 'submit_configurable']

    __tablename__ = 'project'
    id = db.Column(db.String(length=36), default=lambda: str(
        uuid.uuid4()), primary_key=True)
    id = db.Column(db.String(length=36), default=lambda: str(uuid.uuid4()), primary_key=True)
    assignment_url = db.Column(db.Text)
    submit_instructions = db.Column(db.Text, nullable=True)
    submit_configurable = db.Column(db.Boolean, default=True)
@@ -454,13 +447,11 @@ class Project(db.Model, EntityBase, NamedMixin):
    config = db.relationship("ProjectConfig", back_populates="project", uselist=False,
                             cascade="all, delete-orphan", passive_deletes=True)

    course_id = db.Column(db.String(36), db.ForeignKey(
        'course.id', ondelete='cascade'), nullable=False)
    course = db.relationship(
        "Course", back_populates="projects", uselist=False)
    course_id = db.Column(db.String(36), db.ForeignKey('course.id', ondelete='cascade'),
                          nullable=False)
    course = db.relationship("Course", back_populates="projects", uselist=False)
    submissions = db.relationship("Submission", back_populates="project",
                                  cascade="all, delete-orphan",
                                  passive_deletes=True)
                                  cascade="all, delete-orphan", passive_deletes=True)

    __table_args__ = (
        db.UniqueConstraint('course_id', 'codename', name='p_course_unique_name'),
@@ -543,9 +534,6 @@ class Project(db.Model, EntityBase, NamedMixin):
        self.submit_configurable = submit_configurable
        self.config = ProjectConfig(project=self)

    def __repr__(self):
        return _repr(self)

    def __eq__(self, other):
        return self.id == other.id

@@ -555,7 +543,7 @@ class Project(db.Model, EntityBase, NamedMixin):

class ProjectConfig(db.Model, EntityBase):
    """Project's configuration
    Attributes:
    Class Attributes:
        id: UUID of the project's configuration
        project_id: Associated project's id
        project: Associated project
@@ -567,7 +555,12 @@ class ProjectConfig(db.Model, EntityBase):
        submission_parameters: Post submit script used in the submissions processing component
        submission_scheduler_config: Post submit script used in the submissions scheduler
    """
    EXCLUDED = ['project']
    UPDATABLE = ['submissions_cancellation_period', 'test_files_commit_hash',
                 'test_files_source', 'test_files_subdir', 'file_whitelist',
                 'pre_submit_script', 'post_submit_script', 'submission_scheduler_config',
                 'submissions_allowed_from', 'submissions_allowed_to', 'archive_from']
    BASE_PARAMS = ['id', *UPDATABLE]
    LISTABLE = ['id', 'assignment_url', 'namespace']
    __tablename__ = 'projectConfig'
    id = db.Column(db.String(length=36), default=lambda: str(
        uuid.uuid4()), primary_key=True)
@@ -680,31 +673,19 @@ class ProjectConfig(db.Model, EntityBase):
        self.project = project
        self.test_files_source = test_files_source

    def __repr__(self):
        return _repr(self)

    def __eq__(self, other):
        return self.id == other.id


class Role(db.Model, EntityBase, NamedMixin):
    """Role model
    Attributes:
    Class Attributes:
        id: UUID of the role
        name: Name of the role
        description: Description of the role
        clients: Clients associated with the role
        course: Course associated with the role
    """
    EXCLUDED = ['users', 'course']
    __tablename__ = 'role'
    id = db.Column(db.String(length=36), default=lambda: str(
        uuid.uuid4()), primary_key=True)
    clients = db.relationship("Client", secondary="clients_roles")
    course_id = db.Column(db.String(36), db.ForeignKey(
        'course.id', ondelete='cascade'), nullable=False)
    course = db.relationship("Course", back_populates="roles", uselist=False)

    PERMISSIONS = ('create_submissions', 'create_submissions_other',
                   'view_course_limited', 'view_course_full', 'update_course',
                   'handle_notes_access_token', 'write_roles', 'write_groups',
@@ -713,6 +694,17 @@ class Role(db.Model, EntityBase, NamedMixin):
                   'read_submissions_own', 'read_reviews_all', 'read_reviews_groups',
                   'read_reviews_own', 'write_reviews_all', 'write_reviews_group',
                   'write_reviews_own')
    BASE_PARAMS = [*PERMISSIONS, 'namespace', 'id']
    UPDATABLE = PERMISSIONS
    LISTABLE = ['id', 'namespace']

    __tablename__ = 'role'
    id = db.Column(db.String(length=36), default=lambda: str(
        uuid.uuid4()), primary_key=True)
    clients = db.relationship("Client", secondary="clients_roles")
    course_id = db.Column(db.String(36),
                          db.ForeignKey('course.id', ondelete='cascade'), nullable=False)
    course = db.relationship("Course", back_populates="roles", uselist=False)

    # all default to False, explicit setting via role.set_permissions is
    # required
@@ -775,9 +767,6 @@ class Role(db.Model, EntityBase, NamedMixin):
        self.name = name
        self.codename = codename

    def __repr__(self):
        return _repr(self)

    def __eq__(self, other):
        return self.id == other.id

@@ -787,7 +776,7 @@ class Role(db.Model, EntityBase, NamedMixin):

class Group(db.Model, EntityBase, NamedMixin):
    """Group model
    Attributes:
    Class Attributes:
        id: UUID of the group
        name: Name of the Group
        course: Course associated with the Group
@@ -795,7 +784,8 @@ class Group(db.Model, EntityBase, NamedMixin):
        projects: Collection of projects that are allowed for the Group
    """
    __tablename__ = 'group'
    EXCLUDED = ['course', 'users', 'projects']
    LISTABLE = ['id', 'namespace']
    BASE_PARAMS = LISTABLE
    id = db.Column(db.String(length=36), default=lambda: str(
        uuid.uuid4()), primary_key=True)
    course_id = db.Column(db.String(36), db.ForeignKey(
@@ -830,9 +820,6 @@ class Group(db.Model, EntityBase, NamedMixin):
        self.name = name
        self.codename = codename

    def __repr__(self):
        return _repr(self)

    def __eq__(self, other):
        return self.id == other.id

@@ -859,7 +846,7 @@ class SubmissionState(enum.Enum):

class Submission(db.Model, EntityBase):
    """Submission model
    Attributes:
    Class Attributes:
        id: UUID of the submission
        scheduled_for: Date and time when the submission should be executed
        parameters: Parameters of the submission
@@ -871,7 +858,8 @@ class Submission(db.Model, EntityBase):
        project: Associated project for the submission
        review: Review associated with the submission
    """
    EXCLUDED = ['user', 'project', 'review']
    LISTABLE = ['id', 'state', 'points', 'result', 'scheduled_for', 'namespace']
    BASE_PARAMS = ['parameters', 'source_hash', *LISTABLE]
    __tablename__ = 'submission'
    id = db.Column(db.String(length=36), default=lambda: str(
        uuid.uuid4()), primary_key=True)
@@ -975,28 +963,25 @@ class Submission(db.Model, EntityBase):
        self.state = SubmissionState.CREATED
        self.note = {}

    def __repr__(self):
        return _repr(self)

    def __eq__(self, other):
        return self.id == other.id


class Review(db.Model, EntityBase):
    """Review model
    Attributes:
    Class Attributes:
        id: UUID of the review
        submission: Associated submission for the review
        review_items: Review items (Comments) associated to review and submission
    """
    EXCLUDED = ['user', 'review_items', 'submission']
    BASE_PARAMS = ['id']
    LISTABLE = ['id']
    __tablename__ = 'review'
    id = db.Column(db.String(length=36), default=lambda: str(
        uuid.uuid4()), primary_key=True)
    submission_id = db.Column(db.String(36), db.ForeignKey(
        'submission.id', ondelete='cascade'), nullable=False)
    submission = db.relationship(
        "Submission", uselist=False, back_populates="review")
    submission_id = db.Column(db.String(36), db.ForeignKey('submission.id', ondelete='cascade'),
                              nullable=False)
    submission = db.relationship("Submission", uselist=False, back_populates="review")
    review_items = db.relationship("ReviewItem", back_populates="review",
                                   cascade="all, delete-orphan", passive_deletes=True)

@@ -1007,16 +992,13 @@ class Review(db.Model, EntityBase):
        """
        self.submission = submission

    def __repr__(self):
        return _repr(self)

    def __eq__(self, other):
        return self.id == other.id


class ReviewItem(db.Model, EntityBase):
    """Review item model
    Attributes:
    Class Attributes:
        id: UUID of the submission
        review: Associated review
        user: Author of the review
@@ -1027,16 +1009,15 @@ class ReviewItem(db.Model, EntityBase):
    """
    __versioned__ = {}
    __tablename__ = 'reviewItem'
    EXCLUDED = ['user', 'review']
    id = db.Column(db.String(length=36), default=lambda: str(
        uuid.uuid4()), primary_key=True)
    review_id = db.Column(db.String(36), db.ForeignKey(
        'review.id', ondelete='cascade'), nullable=False)
    review = db.relationship("Review", uselist=False,
                             back_populates="review_items")
    UPDATABLE = ['content', 'file', 'line_start', 'line_end', 'line']
    BASE_PARAMS = ['id', *UPDATABLE]
    LISTABLE = [*BASE_PARAMS]
    id = db.Column(db.String(length=36), default=lambda: str(uuid.uuid4()), primary_key=True)
    review_id = db.Column(db.String(36), db.ForeignKey('review.id', ondelete='cascade'),
                          nullable=False)
    review = db.relationship("Review", uselist=False, back_populates="review_items")
    user_id = db.Column(db.String(36), db.ForeignKey('user.id', ondelete='cascade'))
    user = db.relationship("User", uselist=False,
                           back_populates="review_items")  # the review item's author
    user = db.relationship("User", uselist=False, back_populates="review_items")
    content = db.Column(db.Text)
    file = db.Column(db.String(256), nullable=True)
    line_start = db.Column(db.Integer, nullable=True)
@@ -1072,9 +1053,6 @@ class ReviewItem(db.Model, EntityBase):
        """
        return self.line_start

    def __repr__(self):
        return _repr(self)

    def __eq__(self, other):
        return self.id == other.id

@@ -1087,9 +1065,9 @@ class WorkerState(enum.Enum):
    STOPPED = 'stopped'


class Worker(EntityBase, Client):
class Worker(Client):
    """Worker model:
    Attributes:
    Class Attributes:
        id: UUID of the submission
        name: Name of the component
        client: the client part of the worker, attaching secrets and roles
@@ -1097,6 +1075,9 @@ class Worker(EntityBase, Client):
        tags: a string containing tags associated with the worker, denoting its features
        portal_secret: a token used for authenticating portal against the worker
    """
    LISTABLE = ['url', 'id', 'state']
    UPDATABLE = ['url', 'tags', 'portal_secret', 'state']
    BASE_PARAMS = ['id', *UPDATABLE]
    __tablename__ = 'worker'
    id = db.Column(db.String(length=36), db.ForeignKey('client.id'), default=lambda: str(
        uuid.uuid4()), primary_key=True)
@@ -1139,9 +1120,6 @@ class Worker(EntityBase, Client):
    def is_initialized(self):
        return self.url is not None and self.portal_secret is not None

    def __repr__(self):
        return _repr(self)

    def __eq__(self, other):
        return self.id == other.id

+3 −2
Original line number Diff line number Diff line
@@ -33,7 +33,6 @@ class GitlabFacade(GeneralFacade):
    def _get_credential(self, name, prefix='gitlab'):
        return self._get_cookie(name, prefix=prefix) or self._get_header(name, prefix=prefix)


    def check_gl_enabled(self):
        if not self.is_enabled():
            raise errors.GitlabIsNotEnabledError()
@@ -93,9 +92,11 @@ class GitlabFacade(GeneralFacade):
        new_user = self.services.users.create(
            uco=None,
            username=user_info.username,
            gitlab_username=user_info.username,
            name=user_info.name,
            email=user_info.email,
            is_admin=False
            is_admin=False,
            managed=True,
        )
        log.info(f"[GITLAB] Created user after GL login: {new_user}")
        return new_user
+11 −3
Original line number Diff line number Diff line
@@ -29,11 +29,19 @@ class UsersFacade(GeneralCRUDFacade):
        """
        params = {**data}
        log.info(f"[UPDATE] User {user.log_name} by {self.client_name}: {data}")
        if not self.client.is_admin:
            params.pop('is_admin', None)
        params = self._remove_params_to_update(params, user)
        updated = self._service(user).update(**params)
        return updated

    def _remove_params_to_update(self, params, user):
        if self.client.is_admin:
            return params
        params.pop('is_admin', None)
        params.pop('managed', None)
        if user.managed and user.is_self(self.client.id):
            params.pop('gitlab_username', None)
        return params

    def update_password(self, user: User, **data):
        """Updates user's password

@@ -51,7 +59,7 @@ class UsersFacade(GeneralCRUDFacade):
            self._require_params(data, 'old_password')
            self._service.check_old_password()
        result = self._service(user).update_password(data)
        self.services.emails.notify_user(user, 'user_passwd_update',
        self.services.emails.notify_user(user, 'user_password_update',
                                         context=dict(username=user.username))
        return result

+36 −42

File changed.

Preview size limit exceeded, changes collapsed.

Loading