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

Model expanison, sample data update

parent 27904256
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
from sqlalchemy.exc import SQLAlchemyError


class PortalDbError(SQLAlchemyError):
    """Raised when a forbidden operation on portal's database is attempted.

    """
 No newline at end of file
+4 −2
Original line number Diff line number Diff line
@@ -5,6 +5,8 @@ A collection of mixins specifying common behaviour and attributes of database en
"""


# maybe use server_default, server_onupdate instead of default, onupdate; crashes tests
# (https://stackoverflow.com/questions/13370317/sqlalchemy-default-datetime)
class EntityBase(object):
    created_at = db.Column(db.DateTime, default=db.func.now())
    updated_at = db.Column(db.DateTime, default=db.func.now(), onupdate=db.func.now())
 No newline at end of file
    created_at = db.Column(db.TIMESTAMP, default=db.func.now())
    updated_at = db.Column(db.TIMESTAMP, default=db.func.now(), onupdate=db.func.now())
+193 −61
Original line number Diff line number Diff line
from portal import db
from portal.database.mixins import EntityBase
from sqlalchemy.ext.hybrid import hybrid_property
import uuid
from portal.database.exceptions import PortalDbError
from sqlalchemy import event
import enum
import datetime
# uuid as primary key source:
# https://stackoverflow.com/questions/36806403/cant-render-element-of-type-class-sqlalchemy-dialects-postgresql-base-uuid


def _repr(instance):
@@ -8,126 +14,252 @@ def _repr(instance):
    for key, value in vars(instance).items():
        if not key.startswith("_"):
            result += f"{key}={value} "
    '''
    for attr in dir(instance):
        if not callable(getattr(instance, attr)) and not (attr.startswith("_")):
            result += f"{attr}\n"
    '''
    return result


class User(db.Model, EntityBase):
    __tablename__ = 'user'
    id = db.Column(db.Integer, primary_key=True)
    id = db.Column(db.Text(length=36), default=lambda: str(uuid.uuid4()), primary_key=True)
    uco = db.Column(db.Integer, unique=True, nullable=False)
    email = db.Column(db.String(50), unique=True, nullable=False)
    xlogin = db.Column(db.String(7), unique=True, nullable=False)
    username = db.Column(db.String(15), unique=True, nullable=False)
    name = db.Column(db.String(50))
    is_admin = db.Column(db.Boolean, default=False)
    password = db.Column(db.String(50), default=None)
    passwordHash = db.Column(db.String(50), default=None)  # optional - after password change

    submissions = db.relationship("Submission", back_populates="user")
    submissions = db.relationship("Submission", back_populates="user", cascade="all, delete-orphan",
                                  passive_deletes=True)

    @hybrid_property
    @property
    def courses(self):
        return [role.course for role in self.roles]  # TODO: use db join
        result = []
        for role in self.roles:
            if role.course not in result:
                result.append(role.course)
        return result

    def __init__(self, uco, email, xlogin, is_admin=False) -> None:
    def __init__(self, uco, email, username, is_admin=False) -> None:
        self.uco = uco
        self.email = email
        self.xlogin = xlogin
        self.username = username
        self.is_admin = is_admin

    def __repr__(self):
        return _repr(self)


class Group(db.Model, EntityBase):
    __tablename__ = 'group'
    id = db.Column(db.Integer, primary_key=True)
    course_id = db.Column(db.Integer, db.ForeignKey('course.id'))
    course = db.relationship("Course", back_populates="groups")
    users = db.relationship("User", secondary="users_groups")

    def __init__(self) -> None:
        pass

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


class Course(db.Model, EntityBase):
    __tablename__ = 'course'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50))
    codename = db.Column(db.String(6))
    id = db.Column(db.Text(length=36), default=lambda: str(uuid.uuid4()), primary_key=True)
    name = db.Column(db.String(50), unique=True, nullable=False)
    codename = db.Column(db.String(15), unique=True, nullable=False)

    roles = db.relationship("Role", back_populates="course")
    groups = db.relationship("Group", back_populates="course")
    projects = db.relationship("Project", back_populates="course")
    # 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
    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", passive_deletes=True)
    projects = db.relationship("Project", back_populates="course", cascade="all, delete-orphan", passive_deletes=True)

    def __init__(self, name, codename) -> None:
        self.name = name
        self.codename = codename

    def __repr__(self):
        _repr(self)
        return _repr(self)

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


class ProjectState(enum.Enum):
    ACTIVE = 1
    INACTIVE = 2
    ARCHIVED = 3


class Project(db.Model, EntityBase):
    __tablename__ = 'project'
    id = db.Column(db.Integer, primary_key=True)
    course_id = db.Column(db.Integer, db.ForeignKey('course.id'))
    course = db.relationship("Course", back_populates="projects")
    id = db.Column(db.Text(length=36), default=lambda: str(uuid.uuid4()), primary_key=True)
    name = db.Column(db.String(50), nullable=False)
    test_files_source = db.Column(db.String(100), nullable=False)  # a git repository URL
    file_whitelist = db.Column(db.Text)
    pre_submit_script = db.Column(db.Text)
    post_submit_script = db.Column(db.Text)
    submission_parameters = db.Column(db.Text)
    submission_scheduler_config = db.Column(db.Text)

    submissions_allowed_from = db.Column(db.TIMESTAMP(timezone=True))
    submissions_allowed_to = db.Column(db.TIMESTAMP(timezone=True))
    archive_from = db.Column(db.TIMESTAMP(timezone=True))
    # TODO: validate date values: http://docs.sqlalchemy.org/en/rel_0_9/orm/mapped_attributes.html#simple-validators
    course_id = db.Column(db.Integer, db.ForeignKey('course.id'), nullable=False)
    course = db.relationship("Course", back_populates="projects", uselist=False)

    __table_args__ = (
        db.UniqueConstraint('course_id', 'name', name='course_unique_name'),
    )

    # check timestamp comparisons
    def state(self, timestamp=datetime.datetime.now()) -> ProjectState:
        if self.submissions_allowed_from < timestamp < self.submissions_allowed_to:
            return ProjectState.ACTIVE
        elif timestamp > self.archive_from:
            return ProjectState.ARCHIVED
        else:
            return ProjectState.INACTIVE

    def __init__(self, course, name, test_files_source) -> None:
        self.course = course
        self.name = name
        self.test_files_source = test_files_source

    # TODO configuration saving - kontr, submission
    def __repr__(self):
        _repr(self)
        return _repr(self)

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


class Role(db.Model, EntityBase):
    __tablename__ = 'role'
    id = db.Column(db.Integer, primary_key=True)
    id = db.Column(db.Text(length=36), default=lambda: str(uuid.uuid4()), primary_key=True)
    name = db.Column(db.String(50), nullable=False)
    description = db.Column(db.Text)
    users = db.relationship("User", secondary="users_roles")
    course_id = db.Column(db.Integer, db.ForeignKey('course.id'))
    course = db.relationship("Course", back_populates="roles")
    permissions = {}  # how to represent these
    course_id = db.Column(db.Integer, db.ForeignKey('course.id'), nullable=False)
    course = db.relationship("Course", back_populates="roles", uselist=False)
    permissions = {}  # TODO

    __table_args__ = (
        db.UniqueConstraint('course_id', 'name', name='course_unique_name'),
    )

    def __init__(self, course):
    def __init__(self, course, name):
        self.course = course
        self.name = name

    def __repr__(self):
        _repr(self)
        return _repr(self)

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


class Group(db.Model, EntityBase):
    __tablename__ = 'group'
    id = db.Column(db.Text(length=36), default=lambda: str(uuid.uuid4()), primary_key=True)
    name = db.Column(db.String(20), nullable=False)
    course_id = db.Column(db.Integer, db.ForeignKey('course.id'), nullable=False)
    course = db.relationship("Course", back_populates="groups", uselist=False)
    users = db.relationship("User", secondary="users_groups")

    __table_args__ = (
        db.UniqueConstraint('course_id', 'name', name='course_unique_name'),
    )

    def __init__(self, course, name) -> None:
        self.course = course
        self.name = name

    def __repr__(self):
        return _repr(self)

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


class SubmissionState(enum.Enum):
    CREATED = 1
    READY = 2
    IN_PROGRESS = 3
    FINISHED = 4


class Submission(db.Model, EntityBase):
    __tablename__ = 'submission'
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    user = db.relationship("User", back_populates="submissions")
    project_id = db.Column(db.Integer, db.ForeignKey('project.id'))
    id = db.Column(db.Text(length=36), default=lambda: str(uuid.uuid4()), primary_key=True)
    processing_scheduled_for = db.Column(db.TIMESTAMP(timezone=True), nullable=False)
    parameters = db.Column(db.Text, nullable=False)
    state = db.Column(db.Enum(SubmissionState, name='SubmissionState'), nullable=False)

    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    user = db.relationship("User", back_populates="submissions", uselist=False)
    project_id = db.Column(db.Integer, db.ForeignKey('project.id'), nullable=False)
    project = db.relationship("Project", uselist=False)
    review = []
    result = []
    review = db.relationship("Review", back_populates="submission", cascade="all, delete-orphan",
                             passive_deletes=True, uselist=False)

    def __init__(self):
        pass
    def __init__(self, user, project, processing_scheduled_for, parameters, review=None):
        self.user = user
        self.project = project
        self.review = review
        self.processing_scheduled_for = processing_scheduled_for
        self.parameters = parameters
        self.state = SubmissionState.CREATED

    def __repr__(self):
        _repr(self)
        return _repr(self)

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


class Review(db.Model, EntityBase):  # might not be in database, but rather in storage
class Review(db.Model, EntityBase):
    __tablename__ = 'review'
    id = db.Column(db.Integer, primary_key=True)
    id = db.Column(db.Text(length=36), default=lambda: str(uuid.uuid4()), primary_key=True)
    submission_id = db.Column(db.Integer, db.ForeignKey('submission.id'), nullable=False)
    submission = db.relationship("Submission", uselist=False)
    reviewItems = db.relationship("ReviewItem", back_populates="review",
                                  cascade="all, delete-orphan", passive_deletes=True)

    def __init__(self):
        pass
    def __init__(self, submission):
        self.submission = submission

    def __repr__(self):
        _repr(self)
        return _repr(self)

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


class ReviewItem(db.Model, EntityBase):
    __tablename__ = 'reviewItem'
    id = db.Column(db.Text(length=36), default=lambda: str(uuid.uuid4()), primary_key=True)
    review_id = db.Column(db.Integer, db.ForeignKey('review.id'), nullable=False)
    review = db.relationship("Review", uselist=False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    user = db.relationship("User", uselist=False)  # the review item's author
    content = db.Column(db.Text)
    file = db.Column(db.String(100), nullable=False)
    line = db.Column(db.Integer, nullable=False)

    def __init__(self, user, review, file, line):
        self.review = review
        self.user = user
        self.file = file
        self.line = line

    def __repr__(self):
        return _repr(self)

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


@event.listens_for(Submission.user_id, 'set', named=True)
def receive_set(**kw):
    """listen for the 'set' event"""
    submission = kw['target']
    value = kw['value']

    if submission.user_id is not None and submission.user_id != value:
        raise PortalDbError()


# Associations
# Associations - must come after affected class definitions
users_groups = db.Table("users_groups", db.Model.metadata,
                        db.Column("user_id", db.Integer, db.ForeignKey('user.id')),
                        db.Column("group_id", db.Integer, db.ForeignKey('group.id')))
@@ -142,4 +274,4 @@ projects_groups = db.Table("projects_groups", db.Model.metadata,

User.roles = db.relationship("Role", secondary="users_roles")
User.groups = db.relationship("Group", secondary="users_groups")
Project.groups = db.relationship("Group", secondary="projects_groups")  # should create a one-way relationship
Project.groups = db.relationship("Group", secondary="projects_groups")
+3 −9
Original line number Diff line number Diff line
@@ -7,15 +7,6 @@ def init_db(app):
    db.drop_all(app=app)
    db.create_all(app=app)

'''
def init_data():
    with app.app_context():
        from portal.database.models import User
        user = User(uco=445, email="foo", xlogin="xbar")
        db.session.add(user)
        db.session.commit()
        print(User.query.all()[0].__repr__())
'''

if __name__ == '__main__':
    app = create_app()
@@ -31,6 +22,9 @@ if __name__ == '__main__':
# flask-wtf for auth
# https://scotch.io/tutorials/build-a-crud-web-app-with-python-and-flask-part-one

logging: (email sending!):
http://flask.pocoo.org/docs/dev/logging/
https://docs.python.org/3/howto/logging-cookbook.html#formatting-styles

REST & js:
https://auth0.com/blog/developing-restful-apis-with-python-and-flask/
+9 −9
Original line number Diff line number Diff line
@@ -13,8 +13,8 @@ def init_data(app, db):


def create_users(db):
    db.session.add(User(uco=445, email="foo", xlogin="xfoo"))
    db.session.add(User(uco=123, email="bar", xlogin="xbar"))
    db.session.add(User(uco=445, email="foo", username="xfoo"))
    db.session.add(User(uco=123, email="bar", username="xbar"))
    db.session.commit()


@@ -25,20 +25,20 @@ def create_courses(db):


def create_roles(db):
    db.session.add(Role(course=Course.query.all()[0]))
    db.session.add(Role(course=Course.query.all()[1]))
    db.session.add(Role(name='student', course=Course.query.all()[0]))
    db.session.add(Role(name='teacher', course=Course.query.all()[1]))
    db.session.commit()


def create_projects(db):
    db.session.add(Project())
    db.session.add(Project())
    db.session.add(Project(course=Course.query.all()[0], name="p1", test_files_source="repo"))
    db.session.add(Project(course=Course.query.all()[1], name="p2", test_files_source="repo2"))
    db.session.commit()


def create_groups(db):
    db.session.add(Group())
    db.session.add(Group())
    db.session.add(Group(course=Course.query.all()[0], name="g1"))
    db.session.add(Group(course=Course.query.all()[1], name="g2"))
    db.session.commit()


@@ -47,7 +47,7 @@ def create_submissions(db):


def create_relationships(db):
    user1 = User.query.filter_by(xlogin="xfoo").first()
    user1 = User.query.filter_by(username="xfoo").first()
    user1.roles.append(Role.query.all()[0])
    user1.groups.append(Group.query.all()[0])
    db.session.commit()
Loading