Loading portal/database/mixins.py +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 Loading @@ -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) Loading Loading @@ -66,4 +111,5 @@ class NamedMixin(object): if self._codename is None: self.codename = value self._name = value # pylint: enable=too-few-public-methods portal/database/models.py +89 −111 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 """ Loading @@ -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, Loading Loading @@ -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) Loading Loading @@ -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: Loading @@ -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)) Loading @@ -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") Loading Loading @@ -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 Loading @@ -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", Loading Loading @@ -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 Loading Loading @@ -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 Loading @@ -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) Loading @@ -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'), Loading Loading @@ -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 Loading @@ -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 Loading @@ -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) Loading Loading @@ -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', Loading @@ -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 Loading Loading @@ -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 Loading @@ -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 Loading @@ -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( Loading Loading @@ -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 Loading @@ -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 Loading @@ -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) Loading Loading @@ -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) Loading @@ -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 Loading @@ -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) Loading Loading @@ -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 Loading @@ -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 Loading @@ -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) Loading Loading @@ -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 Loading portal/facade/gitlab_facade.py +3 −2 Original line number Diff line number Diff line Loading @@ -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() Loading Loading @@ -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 Loading portal/facade/users_facade.py +11 −3 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading portal/rest/schemas.py +36 −42 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
portal/database/mixins.py +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 Loading @@ -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) Loading Loading @@ -66,4 +111,5 @@ class NamedMixin(object): if self._codename is None: self.codename = value self._name = value # pylint: enable=too-few-public-methods
portal/database/models.py +89 −111 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 """ Loading @@ -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, Loading Loading @@ -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) Loading Loading @@ -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: Loading @@ -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)) Loading @@ -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") Loading Loading @@ -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 Loading @@ -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", Loading Loading @@ -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 Loading Loading @@ -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 Loading @@ -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) Loading @@ -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'), Loading Loading @@ -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 Loading @@ -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 Loading @@ -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) Loading Loading @@ -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', Loading @@ -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 Loading Loading @@ -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 Loading @@ -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 Loading @@ -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( Loading Loading @@ -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 Loading @@ -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 Loading @@ -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) Loading Loading @@ -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) Loading @@ -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 Loading @@ -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) Loading Loading @@ -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 Loading @@ -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 Loading @@ -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) Loading Loading @@ -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 Loading
portal/facade/gitlab_facade.py +3 −2 Original line number Diff line number Diff line Loading @@ -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() Loading Loading @@ -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 Loading
portal/facade/users_facade.py +11 −3 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading
portal/rest/schemas.py +36 −42 File changed.Preview size limit exceeded, changes collapsed. Show changes