diff --git a/migrations/versions/3401e67c571b_.py b/migrations/versions/3401e67c571b_.py deleted file mode 100644 index f4da2ef44199de432f5b960fb19ef535b5b67dd4..0000000000000000000000000000000000000000 --- a/migrations/versions/3401e67c571b_.py +++ /dev/null @@ -1,28 +0,0 @@ -"""empty message - -Revision ID: 3401e67c571b -Revises: c21ce0e65d64 -Create Date: 2018-09-11 09:45:04.606844 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '3401e67c571b' -down_revision = 'c21ce0e65d64' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('projectConfig', sa.Column('test_files_subdir', sa.String(length=50), nullable=True)) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('projectConfig', 'test_files_subdir') - # ### end Alembic commands ### diff --git a/migrations/versions/8c3c07862a02_.py b/migrations/versions/8c3c07862a02_.py deleted file mode 100644 index 9a53c6cd9cd6f917e310c299abf727f7630ca20b..0000000000000000000000000000000000000000 --- a/migrations/versions/8c3c07862a02_.py +++ /dev/null @@ -1,32 +0,0 @@ -"""empty message - -Revision ID: 8c3c07862a02 -Revises: 0ad81fc4487e -Create Date: 2018-09-04 14:24:06.598289 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '8c3c07862a02' -down_revision = '0ad81fc4487e' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('submission', sa.Column('async_task_id', sa.String(length=36), nullable=True)) - op.drop_column('submission', 'process_task_id') - op.drop_column('submission', 'storage_task_id') - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('submission', sa.Column('storage_task_id', sa.VARCHAR(length=36), autoincrement=False, nullable=True)) - op.add_column('submission', sa.Column('process_task_id', sa.VARCHAR(length=36), autoincrement=False, nullable=True)) - op.drop_column('submission', 'async_task_id') - # ### end Alembic commands ### diff --git a/migrations/versions/0ad81fc4487e_.py b/migrations/versions/bd2121c14eaa_.py similarity index 86% rename from migrations/versions/0ad81fc4487e_.py rename to migrations/versions/bd2121c14eaa_.py index bec02e1491bfa498a03b424cd30b051533ddb2b3..34404f8a4cc0ba3b3d1f164e9174230b1bd749c8 100644 --- a/migrations/versions/0ad81fc4487e_.py +++ b/migrations/versions/bd2121c14eaa_.py @@ -1,16 +1,17 @@ """empty message -Revision ID: 0ad81fc4487e +Revision ID: bd2121c14eaa Revises: -Create Date: 2018-09-02 17:09:55.862167 +Create Date: 2018-09-16 16:10:06.408159 """ from alembic import op import sqlalchemy as sa +import portal.database.types # revision identifiers, used by Alembic. -revision = '0ad81fc4487e' +revision = 'bd2121c14eaa' down_revision = None branch_labels = None depends_on = None @@ -21,7 +22,9 @@ def upgrade(): op.create_table('client', sa.Column('id', sa.String(length=36), nullable=False), sa.Column('type', sa.Enum('USER', 'WORKER', name='ClientType'), nullable=False), - sa.PrimaryKeyConstraint('id') + sa.Column('codename', sa.String(length=30), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('codename') ) op.create_table('course', sa.Column('created_at', sa.TIMESTAMP(), nullable=True), @@ -87,27 +90,23 @@ def upgrade(): sa.Column('id', sa.String(length=36), nullable=False), sa.Column('uco', sa.Integer(), nullable=True), sa.Column('email', sa.String(length=50), nullable=False), - sa.Column('username', sa.String(length=30), nullable=False), sa.Column('name', sa.String(length=50), nullable=True), sa.Column('is_admin', sa.Boolean(), nullable=True), sa.Column('password_hash', sa.String(length=120), nullable=True), sa.ForeignKeyConstraint(['id'], ['client.id'], ), sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('email'), - sa.UniqueConstraint('username') + sa.UniqueConstraint('email') ) op.create_table('worker', sa.Column('created_at', sa.TIMESTAMP(), nullable=True), sa.Column('updated_at', sa.TIMESTAMP(), nullable=True), sa.Column('id', sa.String(length=36), nullable=False), - sa.Column('name', sa.String(length=30), nullable=False), sa.Column('url', sa.String(length=250), nullable=False), sa.Column('tags', sa.Text(), nullable=True), sa.Column('portal_secret', sa.String(length=64), nullable=True), sa.Column('state', sa.Enum('CREATED', 'READY', 'STOPPED', name='WorkerState'), nullable=False), sa.ForeignKeyConstraint(['id'], ['client.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('name') + sa.PrimaryKeyConstraint('id') ) op.create_table('clients_roles', sa.Column('client_id', sa.String(length=36), nullable=True), @@ -122,6 +121,7 @@ def upgrade(): sa.Column('project_id', sa.String(length=36), nullable=False), sa.Column('submissions_cancellation_period', sa.Integer(), nullable=True), sa.Column('test_files_source', sa.String(length=600), nullable=True), + sa.Column('test_files_subdir', sa.String(length=50), nullable=True), sa.Column('test_files_commit_hash', sa.String(length=64), nullable=True), sa.Column('file_whitelist', sa.Text(), nullable=True), sa.Column('pre_submit_script', sa.Text(), nullable=True), @@ -158,6 +158,7 @@ def upgrade(): sa.Column('read_projects', sa.Boolean(), nullable=False), sa.Column('archive_projects', sa.Boolean(), nullable=False), sa.Column('create_submissions', sa.Boolean(), nullable=False), + sa.Column('create_submissions_other', sa.Boolean(), nullable=False), sa.Column('resubmit_submissions', sa.Boolean(), nullable=False), sa.Column('evaluate_submissions', sa.Boolean(), nullable=False), sa.Column('read_submissions_all', sa.Boolean(), nullable=False), @@ -174,22 +175,21 @@ def upgrade(): sa.PrimaryKeyConstraint('id') ) op.create_table('submission', - sa.Column('created_at', sa.TIMESTAMP(), nullable=True), - sa.Column('updated_at', sa.TIMESTAMP(), nullable=True), - sa.Column('id', sa.String(length=36), nullable=False), - sa.Column('scheduled_for', sa.TIMESTAMP(timezone=True), nullable=True), - sa.Column('parameters', sa.Text(), nullable=False), - sa.Column('state', sa.Enum('CREATED', 'READY', 'QUEUED', 'IN_PROGRESS', 'FINISHED', 'CANCELLED', 'ABORTED', 'ARCHIVED', name='SubmissionState'), nullable=False), - sa.Column('note', sa.Text(), nullable=True), - sa.Column('source_hash', sa.String(length=64), nullable=True), - sa.Column('user_id', sa.String(length=36), nullable=False), - sa.Column('project_id', sa.String(length=36), nullable=False), - sa.Column('storage_task_id', sa.String(length=36), nullable=True), - sa.Column('process_task_id', sa.String(length=36), nullable=True), - sa.ForeignKeyConstraint(['project_id'], ['project.id'], ondelete='cascade'), - sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='cascade'), - sa.PrimaryKeyConstraint('id') - ) + sa.Column('created_at', sa.TIMESTAMP(), nullable=True), + sa.Column('updated_at', sa.TIMESTAMP(), nullable=True), + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('scheduled_for', sa.TIMESTAMP(timezone=True), nullable=True), + sa.Column('parameters', portal.database.types.JSONEncodedDict(), nullable=True), + sa.Column('state', sa.Enum('CREATED', 'READY', 'QUEUED', 'IN_PROGRESS', 'FINISHED', 'CANCELLED', 'ABORTED', 'ARCHIVED', name='SubmissionState'), nullable=False), + sa.Column('note', portal.database.types.JSONEncodedDict(), nullable=True), + sa.Column('source_hash', sa.String(length=64), nullable=True), + sa.Column('user_id', sa.String(length=36), nullable=False), + sa.Column('project_id', sa.String(length=36), nullable=False), + sa.Column('async_task_id', sa.String(length=36), nullable=True), + sa.ForeignKeyConstraint(['project_id'], ['project.id'], ondelete='cascade'), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='cascade'), + sa.PrimaryKeyConstraint('id') + ) op.create_table('users_groups', sa.Column('user_id', sa.String(length=36), nullable=True), sa.Column('group_id', sa.String(length=36), nullable=True), diff --git a/migrations/versions/c21ce0e65d64_.py b/migrations/versions/c21ce0e65d64_.py deleted file mode 100644 index 14c3ccc2987d1f733d271589bdb1decf5a68a5f0..0000000000000000000000000000000000000000 --- a/migrations/versions/c21ce0e65d64_.py +++ /dev/null @@ -1,32 +0,0 @@ -"""empty message - -Revision ID: c21ce0e65d64 -Revises: 8c3c07862a02 -Create Date: 2018-09-10 20:45:32.588254 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'c21ce0e65d64' -down_revision = '8c3c07862a02' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('submission', 'parameters', - existing_type=sa.TEXT(), - nullable=True) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('submission', 'parameters', - existing_type=sa.TEXT(), - nullable=False) - # ### end Alembic commands ### diff --git a/portal/database/models.py b/portal/database/models.py index 6d5e1e5436601947a5ec4d2d05cd20553d66e876..988dbcf44809a1451ad69f15d7075747edba10f7 100644 --- a/portal/database/models.py +++ b/portal/database/models.py @@ -3,42 +3,25 @@ Models module where all of the models are specified """ import enum -import json import logging import uuid from typing import List -import sqlalchemy from flask_sqlalchemy import BaseQuery -from sqlalchemy import TypeDecorator, event +from sqlalchemy import event from sqlalchemy.ext.hybrid import hybrid_property from werkzeug.security import check_password_hash, generate_password_hash from portal import db from portal.database.exceptions import PortalDbError from portal.database.mixins import EntityBase, NamedMixin +from portal.database.types import JSONEncodedDict from portal.tools import time from portal.tools.time import normalize_time log = logging.getLogger(__name__) -class JSONEncodedDict(TypeDecorator): - "Represents an immutable structure as a json-encoded string." - - impl = sqlalchemy.Text - - def process_bind_param(self, value, dialect): - if value is not None: - value = json.dumps(value) - return value - - def process_result_value(self, value, dialect): - if value is not None: - value = json.loads(value) - return value - - def _repr(instance) -> str: """Repr helper function Args: diff --git a/portal/database/types.py b/portal/database/types.py new file mode 100644 index 0000000000000000000000000000000000000000..f7e532bea644f82f90b68b32233e7e1211546552 --- /dev/null +++ b/portal/database/types.py @@ -0,0 +1,20 @@ +import json + +import sqlalchemy +from sqlalchemy import TypeDecorator + + +class JSONEncodedDict(TypeDecorator): + "Represents an immutable structure as a json-encoded string." + + impl = sqlalchemy.Text + + def process_bind_param(self, value, dialect): + if value is not None: + value = json.dumps(value) + return value + + def process_result_value(self, value, dialect): + if value is not None: + value = json.loads(value) + return value \ No newline at end of file diff --git a/portal/rest/schemas.py b/portal/rest/schemas.py index c8935a22386a1e8f958202aa01e8f1216c16afc2..227320a9fcfe9f6774a9ab7d465cc38113ed9598 100644 --- a/portal/rest/schemas.py +++ b/portal/rest/schemas.py @@ -302,7 +302,7 @@ class SecretSchema(BaseSchema, Schema): """ name = fields.Str() expires_at = fields.LocalDateTime(dump_only=True) - client = NESTED('permissions') + client = NESTED('client') class CourseImportConfigSchema(Schema): @@ -346,15 +346,6 @@ class ProjectImportSchema(Schema): source_project = fields.Str() -class NotificationSendToSchema(Schema): - """Notification Send To Schema - """ - role = fields.Str() - users = fields.List(fields.Str()) - group = fields.Str() - course = fields.Str() - - class SubmissionResultTemplateSchema(Schema): """Submission Result Template Schema """ diff --git a/portal/service/auth.py b/portal/service/auth.py index 194b18ef75a0d4348ffe59d55f15a22c55f0e591..6884ac91557a37477a88ca27953bfb124b7c1613 100644 --- a/portal/service/auth.py +++ b/portal/service/auth.py @@ -68,7 +68,7 @@ def login_secret(identifier: str, secret: str) -> Client: def validate_gitlab_token(token: str, username: str, throws: bool = True): - """Validates gitlab access token using the gitlab permissions + """Validates gitlab access token using the gitlab client Args: token(str): Gitlab access token username(str): Username diff --git a/portal/service/errors.py b/portal/service/errors.py index 55ee1d1afef8514d0619b058531c6741bee73118..f531d0cdc0a3d3e9724550eb22b480eaf41f1291 100644 --- a/portal/service/errors.py +++ b/portal/service/errors.py @@ -68,7 +68,7 @@ class DataMissingError(PortalAPIError): resource(str): Name of the resource """ message = dict(action=action, resource=resource, - message='Data is missing!') + message='Data is missing') super(DataMissingError, self).__init__(code=400, message=message) @@ -96,9 +96,7 @@ class ResourceNotFoundError(PortalAPIError): class UnauthorizedError(PortalAPIError): def __init__(self, note=None): - message = dict( - message=f"You are not authorized.", - ) + message = dict(message=f"You are not authorized.",) if note: message['note'] = note @@ -108,7 +106,7 @@ class UnauthorizedError(PortalAPIError): class ForbiddenError(PortalAPIError): # could use a resource identification (like 404) def __init__(self, client=None, note=None): - user_message = f"Forbidden for {client.type}: {client.id} ({client.codename})!" if client else \ + user_message = f"Forbidden for {client.type}: {client.id} ({client.codename})" if client else \ 'Forbidden action.' message = dict(uid=client.id, message=user_message) if note: diff --git a/portal/service/groups.py b/portal/service/groups.py index fef9811bb90082647121107f3c4d43d04864eba2..cd572ab0224c369efbe34b3c8f98e5b4a4aafd57 100644 --- a/portal/service/groups.py +++ b/portal/service/groups.py @@ -182,7 +182,6 @@ class GroupService: """List all groups Args: course(Course): Course instance - permissions: Client instance Returns(list): List of all groups diff --git a/portal/service/permissions.py b/portal/service/permissions.py index e1274b8589539a1e89d735b4bfcc6524053f5626..d7c7c44057a15ec153abc9f7ea06d091eefd60c7 100644 --- a/portal/service/permissions.py +++ b/portal/service/permissions.py @@ -189,14 +189,14 @@ class PermissionsService: return self._course def get_effective_permissions(self, course_id: str = None) -> dict: - """Gets effective permissions for a permissions in course. - If no course is specified, returns the permissions's + """Gets effective permissions for a course in course. + If no course is specified, returns the client's effective permissions in all courses he is a part of. Args: course_id(str): Course ID - Returns(dict): Effective permissions for the permissions. Keys are course IDs, values + Returns(dict): Effective permissions for the course. Keys are course IDs, values dictionaries of permissions with their values. """ course = general.find_course(course_id, throws=False) @@ -204,7 +204,7 @@ class PermissionsService: return self.build_effective_permissions(*courses) def build_effective_permissions(self, *courses) -> dict: - """Builds effective permissions in a list of courses for the permissions + """Builds effective permissions in a list of courses for the client Args: *courses: List of courses @@ -217,7 +217,7 @@ class PermissionsService: return result def effective_permissions_for_course(self, course: Course = None) -> dict: - """Extracts effective permissions for a permissions in one course + """Extracts effective permissions for a client in one course Returns(dict): Effective permissions dictionary """ diff --git a/portal/service/projects.py b/portal/service/projects.py index 3ed8acf37c74e0bc024404dbce9ef607825489f3..a90d036a5ed9423b52c6fbb4af32d57b6c4de1a7 100644 --- a/portal/service/projects.py +++ b/portal/service/projects.py @@ -145,10 +145,7 @@ class ProjectService: """List of all projects Args: course(Course): Course instance - permissions: Client instance - Returns(list): list of projects - """ perm_service = permissions.PermissionsService(course=course) if perm_service.check.permissions(['view_course_full']): diff --git a/portal/service/roles.py b/portal/service/roles.py index 0c114588e44e963e49127809de4bbdfddd1a18d9..af41a6439b33e966de050091a75a32ec57c5bc3c 100644 --- a/portal/service/roles.py +++ b/portal/service/roles.py @@ -101,14 +101,14 @@ class RoleService: """Finds all clients based on their ids Args: - ids(List[str]): List of permissions ids + ids(List[str]): List of clients ids Returns(List[Client]): List of clients """ return [find_client(i) for i in ids] def update_clients_membership(self, data: dict) -> Role: - """Updates permissions membership in the role + """Updates client membership in the role Args: data(dict): Data provided to update role membership @@ -155,7 +155,6 @@ class RoleService: def remove_client(self, client: Client) -> Role: """Removes single client from the role Args: - role(Role): Role instance client(Client): Client instance Returns(Role): Updated role """ @@ -175,8 +174,6 @@ class RoleService: """List of all roles Args: course(Course): Course instance - permissions(Client): Client instance - Returns(list): List of roles """ diff --git a/portal/service/users.py b/portal/service/users.py index c463374575e64016f062fb7eadbe2657a7e91432..e135a20f6530c06f4354702ed5c14febc96f4b8b 100644 --- a/portal/service/users.py +++ b/portal/service/users.py @@ -110,7 +110,7 @@ class UserService: return self.user def update_password(self, data: dict): - log.info(f"[UPDATE] User password: {self.user.id} by {self.permissions.id}") + log.info(f"[UPDATE] User password: {self.user.id} by {self.client.id}") self.__require_param(data, 'new_password') if self.client == self.user and not self.user.is_admin: self.__check_old_password(data) @@ -172,7 +172,6 @@ class UserService: """Find all groups for the user, optionally filtered by course. Args: - user(User): User instance course_id(str): Course id (optional) Returns(List[Group]): List of all groups @@ -185,7 +184,6 @@ class UserService: """Get all roles for the user, optionally filtered by course Args: - user(User): User instance course_id(str): Course id (optional) Returns(List[Role]): @@ -198,9 +196,6 @@ class UserService: def find_reviews(self) -> List[Review]: """Gets reviews the user has contributed to. - Args: - user(User): User instance - Returns(List[Review]): """ reviews = [] @@ -214,7 +209,6 @@ class UserService: """Get all submissions of a user, optionally filtered by course and project. Args: - user(User): course_id(str): Course id (optional) project_ids(List[str]): List of project ids (optional) diff --git a/portal/tools/gitlab_client.py b/portal/tools/gitlab_client.py index f6e67de7a1dc5d7eb8b9f3450f88777b9c8351d2..8ef0588de469a387a806dd92132fa767f2d3ff89 100644 --- a/portal/tools/gitlab_client.py +++ b/portal/tools/gitlab_client.py @@ -1,5 +1,5 @@ """ -Gitlab permissions module +Gitlab client module """ import gitlab @@ -7,7 +7,7 @@ from flask import Flask class GitlabFactory(object): - """Gitlab permissions wrapper for flask + """Gitlab client wrapper for flask DOC: http://python-gitlab.readthedocs.io/en/stable/api-usage.html """ @@ -19,19 +19,19 @@ class GitlabFactory(object): self.app: Flask = app def init_app(self, app: Flask): - """Initializes the permissions with a flask application + """Initializes the client with a flask application Args: app(Flask): Flask application """ self.app = app def instance(self, *args, **kwargs) -> gitlab.Gitlab: - """Creates instance of the Gitlab permissions + """Creates instance of the Gitlab client Args: *args: **kwargs: - Returns(gitlab.Gitlab): Gitlab permissions instance + Returns(gitlab.Gitlab): Gitlab client instance """ gitlab_url = self.app.config.get('GITLAB_URL') return gitlab.Gitlab(gitlab_url, *args, **kwargs) diff --git a/tests/database/test_db.py b/tests/database/test_db.py index a482c17275b75f2b3145ed5f65a06da1047d3ab3..fe05df38db9c99db01d3beb628d7af56d7f674b1 100644 --- a/tests/database/test_db.py +++ b/tests/database/test_db.py @@ -228,7 +228,7 @@ def test_submission_create_valid(session): user = User(uco=123, email='foo', username='xfoo') course = Course(name="C++", codename="testcourse1") project = Project(course=course, name="p1") - submission = Submission(user=user, project=project, parameters="") + submission = Submission(user=user, project=project, parameters={}) session.add(submission) session.flush() @@ -243,7 +243,7 @@ def test_submission_update_user(session): user2 = User(uco=456, email='bar', username='xbar') course = Course(name="C++", codename="testcourse1") project = Project(course=course, codename="p1") - submission = Submission(user=user1, project=project, parameters="") + submission = Submission(user=user1, project=project, parameters={}) session.add_all([submission, user2]) session.flush() @@ -581,7 +581,7 @@ def test_relation_course_project_delete_project_entity(session): assert len(Project.query.all()) == 2 -# permissions-role-course +# client-role-course def test_relation_client_role(session): user1 = User(uco=123, email="foo", username="xfoo") user2 = User(uco=456, email="bar", username="xbar") @@ -833,7 +833,7 @@ def test_user_submission_review(session): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1") - submission = Submission(user=user, project=project, parameters="") + submission = Submission(user=user, project=project, parameters={}) session.add(submission) session.flush() assert submission in user.submissions diff --git a/tests/rest/test_project.py b/tests/rest/test_project.py index 4b9e27b36952128b8d78dc470dc6850a2d4aa859..5ccf8bb2cdaf1187f1e0adb1c3eff3bcb4d48a56 100644 --- a/tests/rest/test_project.py +++ b/tests/rest/test_project.py @@ -177,7 +177,7 @@ def test_create_submission(client): p_submissions = len(p.submissions) request_dict = { - "project_params": "data for Kontr", + "project_params": {}, "file_params": { "source": { "type": "git", @@ -187,7 +187,7 @@ def test_create_submission(client): } } } - request_json = json.dumps(request_dict, cls=utils.DateTimeEncoder) + request_json = json.dumps(request_dict) path = f"/courses/{cpp.codename}/projects/{p.name}/submissions" response = utils.make_request( @@ -212,7 +212,7 @@ def test_create_submission_as_different_user(client): p_submissions = len(p.submissions) request_dict = { - "project_params": "data for Kontr", + "project_params": {}, "file_params": { "source": { "type": "git", @@ -222,7 +222,7 @@ def test_create_submission_as_different_user(client): } } } - request_json = json.dumps(request_dict, cls=utils.DateTimeEncoder) + request_json = json.dumps(request_dict) path = f"/courses/{cpp.codename}/projects/{p.name}/submissions?user=student1" response = utils.make_request(client, path, data=request_json, method='post') diff --git a/tests/rest/utils.py b/tests/rest/utils.py index deb5e9e20e14490c4469e81fa509a46cf6e5c104..215004933cd8f492fbac9887e4223cb228289df5 100644 --- a/tests/rest/utils.py +++ b/tests/rest/utils.py @@ -31,7 +31,7 @@ def make_request(client: FlaskClient, url: str, method: str, """ Creates an authenticated request to an endpoint. Args: - client: Flask's test permissions + client: Flask's test client url: request url method: get, post, put credentials: json data for login