diff --git a/portal/database/models.py b/portal/database/models.py index 3ec2a79e8d4e67f17b264f2d6a71a27b4a51b24b..9aefd32ea8264454eb929cdbc6ab88daa14d77c6 100644 --- a/portal/database/models.py +++ b/portal/database/models.py @@ -2,8 +2,10 @@ import uuid import enum import datetime +from flask_sqlalchemy import BaseQuery from sqlalchemy import event from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm import aliased from werkzeug.security import generate_password_hash, check_password_hash from database import SubmissionState @@ -20,7 +22,8 @@ from portal.tools import time def _repr(instance): result = f"{instance.__class__.__name__}: " for key, value in vars(instance).items(): - if not key.startswith("_") and key not in ('password_hash', 'review', 'course', 'user', 'project', 'group', 'role', 'review_items'): + if not key.startswith("_") and key not in ( + 'password_hash', 'review', 'course', 'user', 'project', 'group', 'role', 'review_items'): result += f"{key}={value} " return result @@ -58,16 +61,96 @@ class User(db.Model, EntityBase): return check_password_hash(self.password_hash, password) @property - def courses(self): - result = [] - for role in self.roles: - if role.course not in result: - result.append(role.course) - return result - - def get_roles_in_course(self, course): + def courses(self) -> list: + """Gets courses the users is signed in + + Returns(list): List of courses + """ + return self.query_courses().all() + + def query_courses(self) -> BaseQuery: + """Queries courses the users is signed in + + Returns(BaseQuery): BaseQuery that can be executed + """ + return Course.query.join(Course.roles).join(Role.users).filter(User.id == self.id) + + def get_roles_in_course(self, course: 'Course') -> list: + """Gets user's roles for the course + + Args: + course(Course): Course instance + + Returns(list): List of roles + """ + return self.query_roles_in_course(course=course).all() + + def query_roles_in_course(self, course: 'Course') -> BaseQuery: + """Queries user's roles for the course + + Args: + course(Course): Course instance + + Returns(BaseQuery): BaseQuery that can be executed + """ return Role.query.filter_by(course=course) \ - .join(Role.users).filter(User.id == self.id).all() + .join(Role.users).filter(User.id == self.id) + + def query_groups_in_course(self, course: 'Course', project: 'Project' = None) -> BaseQuery: + """Queries user's groups for course and project(optional) + + Args: + course(Course): Course instance + project(Project): Project Instance + + Returns(BaseQuery): BaseQuery that can be executed + """ + base_query = Group.query.filter_by(course=course) + if project: + base_query = base_query.join(Group.projects) \ + .filter(Project.id == project.id) + return base_query.join(Group.users).filter(User.id == self.id) + + def get_groups_in_course(self, course: 'Course', project: 'Project') -> list: + """Gers user's groups for course and project(optional) + + Args: + course(Course): Course instance + project(Project): Project Instance + + Returns(list): List of groups + """ + return self.query_groups_in_course(course=course, project=project).all() + + def query_users_in_group_based_on_role(self, course: 'Course', project: 'Project', + role: 'Role') -> BaseQuery: + """Queries users for user's group's based on role + + Args: + course(Course): Course instance + project(Project): Project Instance + role(Role): Role instance + + Returns(BaseQuery): BaseQuery that can be executed + """ + groups = self.query_groups_in_course(course=course, project=project).subquery() + grp_alias = aliased(Group) + return User.query.join(grp_alias, User.groups) \ + .join(groups).join(User.roles).filter(Role == role) + + def get_users_in_group_based_on_role(self, course: 'Course', project: 'Project', + role: 'Role') -> list: + """Gets users for user's group's based on role + + Args: + course(Course): Course instance + project(Project): Project Instance + role(Role): Role instance + + Returns(BaseQuery): BaseQuery that can be executed + """ + return self.query_users_in_group_based_on_role(course=course, project=project, + role=role).all() def __init__(self, uco, email, username, is_admin=False) -> None: self.uco = uco @@ -111,6 +194,14 @@ class Course(db.Model, EntityBase): def __eq__(self, other): return self.id == other.id + def get_users_by_role(self, role: 'Role'): + return User.query.join(User.roles) \ + .filter((Role.id == role.id) & (Role.course == self)) + + def get_users_by_group(self, group: 'Group'): + return User.query.join(User.groups) \ + .filter((Group.id == group.id) & (Role.course == self)) + class ProjectState(enum.Enum): ACTIVE = 1 @@ -149,9 +240,9 @@ class Project(db.Model, EntityBase): def set_config(self, **kwargs): for k, w in kwargs.items(): if k in ( - 'test_files_source', 'file_whitelist', 'pre_submit_script', 'post_submit_script', - 'submission_parameters', 'submission_scheduler_config', 'submissions_allowed_from', - 'submissions_allowed_to', 'archive_from'): + 'test_files_source', 'file_whitelist', 'pre_submit_script', 'post_submit_script', + 'submission_parameters', 'submission_scheduler_config', 'submissions_allowed_from', + 'submissions_allowed_to', 'archive_from'): setattr(self.config, k, w) def __init__(self, course, name, test_files_source) -> None: @@ -321,6 +412,7 @@ class Group(db.Model, EntityBase): 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") + projects = db.relationship("Project", secondary="projects_groups", back_populates="groups") __table_args__ = ( db.UniqueConstraint('course_id', 'name', name='course_unique_name'), @@ -447,4 +539,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") +Project.groups = db.relationship("Group", secondary="projects_groups", back_populates='projects') diff --git a/tests/database/test_db.py b/tests/database/test_db.py index dbc61b009cfd411f1492c7d098e6625fcb886674..5d61baaf9468051ea86aa94d2751275787ce675e 100644 --- a/tests/database/test_db.py +++ b/tests/database/test_db.py @@ -7,20 +7,6 @@ from portal.database.models import User, Course, Role, Group, Project, Submissio from portal.tools import time from portal.tools.logging import log -''' -# will be removed later, when javascript functionality is added -def test_empty(app): - with app.test_client() as client: - resp = client.get('/') - assert resp.status_code == 200 - assert ('JavaScript' in resp.data.decode()) -''' -''' -def test_logging_levels(): - log.debug("debug level - should be displayed") - log.info("info level - should be displayed") -''' - # Entity constraints & creation def test_user_create_valid(session): @@ -880,3 +866,100 @@ def test_user_roles_for_course(session): assert len(java_roles) == 1 assert student_java in java_roles + +def test_user_groups_for_course(session): + user = User(uco=123, email='foo', username='xfoo') + course = Course(name="C++", codename="PB161") + c_java = Course(name="Java", codename="PB162") + + g1_cpp = Group(course=course, name='group1') + g2_cpp = Group(course=course, name='group2') + g1_java = Group(course=c_java, name='g1') + + user.groups.append(g1_cpp) + user.groups.append(g2_cpp) + user.groups.append(g1_java) + + session.add(user) + session.flush() + + assert len(user.groups) == 3 + + cpp_groups = user.query_groups_in_course(course=course).all() + assert len(cpp_groups) == 2 + assert g1_cpp in cpp_groups + assert g2_cpp in cpp_groups + + java_groups = user.query_groups_in_course(course=c_java).all() + assert len(java_groups) == 1 + assert g1_java in java_groups + + +def test_user_groups_for_course_and_project(session): + user = User(uco=123, email='foo', username='xfoo') + course = Course(name="C++", codename="PB161") + c_java = Course(name="Java", codename="PB162") + project = Project(name="hw01", course=course, test_files_source="None") + + g1_cpp = Group(course=course, name='group1') + g2_cpp = Group(course=course, name='group2') + g1_java = Group(course=c_java, name='group1') + project.groups.append(g1_cpp) + + user.groups.append(g1_cpp) + user.groups.append(g2_cpp) + user.groups.append(g1_java) + + session.add(user) + session.flush() + + assert len(user.groups) == 3 + + cpp_groups = user.query_groups_in_course(course=course, project=project) + assert cpp_groups.count() == 1 + assert g1_cpp in cpp_groups + assert g2_cpp not in cpp_groups + + java_groups = user.query_groups_in_course(course=c_java) + assert java_groups.count() == 1 + assert g1_java in java_groups + + +def test_users_in_group_based_on_role(session): + user = User(uco=123, email='foo@bar.com', username='xfoo') + teacher = User(uco=124, email='bar@bar.com', username='xbar') + teacher2 = User(uco=125, email='bar2@bar.com', username='xbar2') + course = Course(name="C++", codename="PB161") + project = Project(name="hw01", course=course, test_files_source="None") + + g1 = Group(course=course, name='group1') + g2 = Group(course=course, name='group2') + students = Role(course=course, name="students") + teachers = Role(course=course, name="teachers") + students.users.append(user) + teachers.users.append(teacher) + teachers.users.append(teacher2) + project.groups.append(g1) + + user.groups.append(g1) + user.groups.append(g2) + teacher.groups.append(g1) + teacher2.groups.append(g2) + + session.add(user) + session.add(teacher) + session.add(teacher2) + session.flush() + + res_teach = user.get_users_in_group_based_on_role( + course=course, + project=project, + role=teachers + ) + + assert len(res_teach) == 1 + + assert teacher in res_teach + assert teacher2 not in res_teach + assert user not in res_teach + diff --git a/tests/rest/conftest.py b/tests/rest/conftest.py index 8013cf267423980e9b0e66873913bc277d9eee4d..328f24d9f97246e812fdcdd8c19e5fb2ae6434cb 100644 --- a/tests/rest/conftest.py +++ b/tests/rest/conftest.py @@ -6,13 +6,6 @@ from run import register_blueprints from sample_data.data_init import init_data from portal import db -from portal.rest.courses.courses import courses -from portal.rest.users.users import users -from portal.rest.roles.roles import roles -from portal.rest.groups.groups import groups -from rest.projects.projects import projects -from rest.submissions.submissions import submissions - @pytest.fixture(scope='function') def app():