Unverified Commit 5e847e32 authored by Peter Stanko's avatar Peter Stanko
Browse files

Project service

parent 56d4e794
......@@ -7,8 +7,7 @@ from portal.rest import rest_helpers
from portal.rest.schemas import course_import_schema, course_schema, courses_schema, users_schema
from portal.service import permissions
from portal.service.auth import find_client
from portal.service.courses import copy_course, create_course, delete_course, find_all_courses, \
get_users_filtered, update_course, update_notes_token
from portal.service.courses import CourseService
from portal.service.errors import ForbiddenError, PortalAPIError
from portal.service.filters import filter_course_dump
from portal.service.general import find_course
......@@ -26,7 +25,7 @@ class CourseList(Resource):
# authorization
permissions.PermissionsService().require.sysadmin()
courses_list = find_all_courses()
courses_list = CourseService().find_all_courses()
return courses_schema.dump(courses_list)
@jwt_required
......@@ -37,7 +36,7 @@ class CourseList(Resource):
data = rest_helpers.parse_request_data(
course_schema, resource='course', action='create')
new_course = create_course(**data)
new_course = CourseService().create_course(**data)
return course_schema.dump(new_course)[0], 201
......@@ -70,7 +69,7 @@ class CourseResource(Resource):
def delete(self, cid: str):
permissions.PermissionsService().require.sysadmin()
course = find_course(cid)
delete_course(course)
CourseService(course=course).delete_course()
return '', 204
@jwt_required
......@@ -84,7 +83,7 @@ class CourseResource(Resource):
data = rest_helpers.parse_request_data(
schema=course_schema, action='update', resource='course', partial=True
)
update_course(course, data)
CourseService(course=course).update_course(data)
return '', 204
......@@ -114,7 +113,7 @@ class CourseNotesToken(Resource):
json_data = rest_helpers.require_data(
action='update_notes_token', resource='course')
update_notes_token(course, json_data['token'])
CourseService(course=course).update_notes_token(json_data['token'])
return '', 204
......@@ -141,7 +140,7 @@ class CourseImport(Resource):
config = data['config']
copied_course = copy_course(source_course, course, config)
copied_course = CourseService(course=course).copy_course(course, config)
return course_schema.dump(copied_course)[0]
......@@ -158,5 +157,5 @@ class CourseUsers(Resource):
permissions.PermissionsService(course=course).require.client(['view_course_full'])
group_ids = request.args.getlist('group')
role_ids = request.args.getlist('role')
users = get_users_filtered(course, group_ids, role_ids)
users = CourseService(course=course).get_users_filtered(group_ids, role_ids)
return users_schema.dump(users)[0]
......@@ -11,9 +11,7 @@ from portal.rest.schemas import config_schema, config_schema_reduced, project_sc
projects_schema, submission_create_schema, submission_schema, submissions_schema
from portal.service import auth, general, permissions
from portal.service.errors import ForbiddenError, SubmissionRefusedError
from portal.service.projects import can_create_submission, create_project, delete_project, \
find_project_submissions, list_projects, update_project, update_project_config, \
update_project_test_files_hash
from portal.service.projects import ProjectService
from portal.service.submissions import SubmissionsService
from portal.tools import time
......@@ -31,9 +29,9 @@ class ProjectsList(Resource):
@projects_namespace.response(404, 'Course not found')
# @projects_namespace.response(200, 'Projects list', model=projects_schema)
def get(self, cid: str):
client = auth.find_client()
course = general.find_course(cid)
return projects_schema.dump(list_projects(course, client))
projects = ProjectService().list_projects(course)
return projects_schema.dump(projects)
@jwt_required
@projects_namespace.response(404, 'Course not found')
......@@ -47,7 +45,7 @@ class ProjectsList(Resource):
project_schema, action='create', resource='project'
)
new_project = create_project(course, data)
new_project = ProjectService().create_project(course, data)
return project_schema.dump(new_project)[0], 201
......@@ -77,7 +75,7 @@ class ProjectResource(Resource):
permissions.PermissionsService(course=course).require.update_course()
project = general.find_project(course, pid)
delete_project(project)
ProjectService(project).delete_project()
return '', 204
@jwt_required
......@@ -93,7 +91,7 @@ class ProjectResource(Resource):
)
project = general.find_project(course, pid)
update_project(project, data)
ProjectService(project).update_project(project)
return '', 204
......@@ -125,7 +123,7 @@ class ProjectConfigResource(Resource):
)
project = general.find_project(course, pid)
update_project_config(project, data)
ProjectService(project).update_project_config(data)
return '', 204
......@@ -142,7 +140,7 @@ class ProjectTestFilesRefresh(Resource):
project = general.find_project(course, pid)
# authorization
permissions.PermissionsService(course=course).require.write_projects()
update_project_test_files_hash(project)
ProjectService(project).update_project_test_files_hash()
return '', 204
......@@ -163,7 +161,7 @@ class ProjectSubmissions(Resource):
user_id = request.args.get('user')
project = general.find_project(course, pid)
submissions = find_project_submissions(project, user_id)
submissions = ProjectService(project).find_project_submissions(user_id)
return submissions_schema.dump(submissions)
@jwt_required
......@@ -210,7 +208,7 @@ class ProjectTestFiles(Resource):
def _check_submission_create(client, project):
if project.state(timestamp=time.current_time()) != ProjectState.ACTIVE:
raise SubmissionRefusedError(f"Project {project.name} not active.")
if not can_create_submission(client, project):
if not ProjectService(project).can_create_submission(client):
raise SubmissionRefusedError(
f"Submission in project {project.name} already active.")
......
......@@ -289,5 +289,5 @@ def get_submissions_based_on_permissions_for_course(client, course_id, project_i
]
perm_service.require.any_check(client, *checks)
return users.find_submissions_filtered(
user, course_id=course_id, project_ids=project_ids)
return UserService(user=user).find_submissions_filtered(
course_id=course_id, project_ids=project_ids)
......@@ -2,7 +2,6 @@
Courses service
"""
import logging
from typing import List
from portal import logger
......@@ -15,109 +14,104 @@ from portal.service.roles import copy_role
log = logger.get_logger(__name__)
def copy_course(source: Course, target: Course, config: dict) -> Course:
"""Copies course and it's other resources (roles, groups, projects)
The other resources that should be copied are specified in the config
Args:
source(Course): From which course the resources will be copied
target(Course): To which course the resources will be copied
config(dict): Configuration to specify which resource should be copied
Returns(Course): Copied course instance (target)
"""
if config.get('roles'):
for role in source.roles:
copy_role(role, target, with_clients=config['roles'])
if config.get('groups'):
for group in source.groups:
copy_group(group, target, with_users=config['groups'])
if config.get('projects'):
for project in source.projects:
copy_project(project, target)
general.write_entity(target)
log.info(f"[IMPORT] From {source.id} to: {target.id}.")
return target
def delete_course(course: Course):
"""Deletes course
Args:
course(Course): Course instance
"""
log.info(f"[DELETE] Course: {course.id} ({course.codename})")
general.delete_entity(course)
def __set_course_props(course: Course, data: dict):
def _set_course_props(course: Course, data: dict):
return general.update_entity(course, data, allowed=['name', 'codename', 'description'])
def update_course(course: Course, data: dict) -> Course:
"""Updates course
Args:
course(Course): Course instance
data(dict): Dictionary containing data which should be changed
Returns(Course): Updated course instance
"""
__set_course_props(course=course, data=data)
log.info(f"[UPDATE] Course {course.id} ({course.codename}): {course}.")
return course
def create_course(**data) -> Course:
"""Creates new course
Keyword Args:
name(str): name of the course
codename(str): codename of the course
Returns(Course): Course instance
"""
course = Course()
__set_course_props(course=course, data=data)
log.debug(f"[CREATE] Course {course.id}: {course}")
return course
def update_notes_token(course: Course, token: str) -> Course:
"""Updates notes access token of a course.
Args:
course(Course): Course instance for which the token is set
token(str): The new token
Returns(Course): Course instance
"""
course.notes_access_token = token
general.write_entity(course)
log.info(
f"[UPDATE] Notes access token ({course.codename}) [{course.id}]: {token}")
return course
def find_all_courses() -> List[Course]:
"""Find all courses
Returns(list): List of courses
"""
return Course.query.all()
def get_users_filtered(
course: Course, groups: List[str], roles: List[str]) -> List[User]:
"""Get all users for course filtered
Args:
course(Course): Course instance
groups(list): Group names list
roles(list): Role names list
Returns(List[User]):
"""
groups_entities = Group.query.filter(Group.id.in_(groups)).all()
roles_entities = Role.query.filter(Role.id.in_(roles)).all()
return course.get_users_filtered(groups_entities, roles_entities)
class CourseService:
def __init__(self, course: Course = None):
self._course = course
@property
def course(self):
return self._course
def create_course(self, **data) -> Course:
"""Creates new course
Keyword Args:
name(str): name of the course
codename(str): codename of the course
Returns(Course): Course instance
"""
course = Course()
self._course = course
_set_course_props(course=course, data=data)
log.debug(f"[CREATE] Course {course.id}: {course}")
return course
def copy_course(self, target: Course, config: dict) -> Course:
"""Copies course and it's other resources (roles, groups, projects)
The other resources that should be copied are specified in the config
Args:
target(Course): To which course the resources will be copied
config(dict): Configuration to specify which resource should be copied
Returns(Course): Copied course instance (target)
"""
if config.get('roles'):
for role in self.course.roles:
copy_role(role, target, with_clients=config['roles'])
if config.get('groups'):
for group in self.course.groups:
copy_group(group, target, with_users=config['groups'])
if config.get('projects'):
for project in self.course.projects:
copy_project(project, target)
general.write_entity(target)
log.info(f"[IMPORT] From {self.course.id} to: {target.id}.")
return target
def delete_course(self):
"""Deletes course
"""
log.info(f"[DELETE] Course: {self.course.id} ({self.course.codename})")
general.delete_entity(self.course)
def update_course(self, data: dict) -> Course:
"""Updates course
Args:
data(dict): Dictionary containing data which should be changed
Returns(Course): Updated course instance
"""
_set_course_props(course=self.course, data=data)
log.info(f"[UPDATE] Course {self.course.id} ({self.course.codename}): {self.course}.")
return self.course
def update_notes_token(self, token: str) -> Course:
"""Updates notes access token of a course.
Args:
token(str): The new token
Returns(Course): Course instance
"""
self.course.notes_access_token = token
general.write_entity(self.course)
log.info(
f"[UPDATE] Notes access token ({self.course.codename}) [{self.course.id}]: {token}")
return self.course
def find_all_courses(self) -> List[Course]:
"""Find all courses
Returns(list): List of courses
"""
return Course.query.all()
def get_users_filtered(self, groups: List[str], roles: List[str]) -> List[User]:
"""Get all users for course filtered
Args:
groups(list): Group names list
roles(list): Role names list
Returns(List[User]):
"""
groups_entities = Group.query.filter(Group.id.in_(groups)).all()
roles_entities = Role.query.filter(Role.id.in_(roles)).all()
return self.course.get_users_filtered(groups_entities, roles_entities)
......@@ -16,161 +16,150 @@ from portal.service.general import get_new_name
log = logging.getLogger(__name__)
def copy_project(source: Project, target: Course) -> Project:
"""Copies a project to the target course
Args:
source(Project): Project instance
target(Course): Course instance
Returns(Project): Copied project
"""
new_name = get_new_name(source, target)
new_project = Project(target, codename=new_name)
new_project.description = source.description
new_project.name = source.name
new_project.set_config(**vars(source.config))
return new_project
def can_create_submission(user: User, project: Project) -> bool:
"""Checks if a user may create a new submission.
A user is permitted to create a new submission if there are no submissions
created or queued for him in the given project.
Args:
user(User): the user who wants to crete a new submission
project(Project): the project in which the user wants to create a submission
Returns: whether the user can create a new submission in the project
"""
conflicting_states = [SubmissionState.CREATED,
SubmissionState.READY, SubmissionState.QUEUED]
conflict = Submission.query.filter(Submission.user == user) \
.filter(Submission.project == project) \
.filter(Submission.state.in_(conflicting_states)) \
.first()
return conflict is None
def delete_project(project: Project):
"""Deletes a project
Args:
project(Project): project instance
"""
log.info(f"[DELETE] Project {project.id} ({project.codename}).")
general.delete_entity(project)
def update_project(project: Project, data: dict) -> Project:
"""Updates a project
Args:
project(Project): Project instance
data(dict): Data dictionary
Returns(Project): Updated project
"""
log.info(f"[UPDATE] Project {project.id} ({project.codename}): {data}.")
return __set_project_data(project, data)
def __set_project_data(project: Project, data: dict) -> Project:
def _set_project_data(project: Project, data: dict) -> Project:
allowed = ['name', 'description', 'codename', 'assignment_url']
return general.update_entity(project, data, allowed=allowed)
def create_project(course: Course, data: dict) -> Project:
"""Creates a new project
Args:
course(Course): Course instance
data(dict): Project data
Returns(Project): Project instance
"""
new_project = Project(course=course)
__set_project_data(project=new_project, data=data)
log.info(f"[CREATE] Project for course {course.id} ({course.codename}): "
f"{new_project}")
return new_project
def update_project_config(project: Project, data: dict) -> ProjectConfig:
"""Updates project config
Args:
project(Project): Project instance
data(dict): Config data
Returns(ProjectConfig): Project config instance
"""
course = project.course
project.set_config(**data)
general.write_entity(project)
log.info(f"[UPDATE] Configuration for project {project.id} in '"
f"course {course.id} to {data}.")
return project.config
def query_submissions(project: Project, user: User) -> BaseQuery:
"""Queries submissions for the project and filters them by user.
Args:
project(Project): Project instance
user(User): User instance
"""
course = project.course
return Submission.query.filter(Submission.user_id == user.id) \
.filter(Submission.project_id == project.id) \
.join(Course, Course.id == project.course_id).filter(Course.id == course.id)
def find_project_submissions(
project: Project, user_id: str = None) -> List[Submission]:
"""Finds all project submissions. If a user id is specified, returns only submissions created
by that user.
Args:
project(Project): Project instance
user_id(str): User id (optional)
Returns(List[Submission]): A list of all submissions in a project if no user_id is specified.
Otherwise a list of submissions created in the project by the specified user.
"""
submissions = project.submissions
if user_id:
user = general.find_user(user_id)
submissions = query_submissions(project, user)
return submissions
def list_projects(course: Course, client) -> list:
"""List of all projects
Args:
course(Course): Course instance
client: Client instance
Returns(list): list of projects
"""
perm_service = permissions.PermissionsService(course=course)
if perm_service.check.client(['view_course_full']):
return course.projects
elif perm_service.check.client(['view_course_limited']):
return filters.filter_projects_from_course(course=course, user=client)
raise ForbiddenError(uid=client.id)
def update_project_test_files_hash(project: Project):
""" Sends a request to Storage to update the project's test_files to the newest version.
:param project: the project to update
:return: nothing
"""
tasks.update_project_test_files.delay(project.id)
class ProjectService:
def __init__(self, project: Project = None):
self._project = project
@property
def project(self) -> Project:
return self._project
def copy_project(self, target: Course) -> Project:
"""Copies a project to the target course
Args:
target(Course): Course instance
Returns(Project): Copied project
"""
new_name = get_new_name(self.project, target)
new_project = Project(target, codename=new_name)
new_project.description = self.project.description
new_project.name = self.project.name
new_project.set_config(**vars(self.project.config))
return new_project
def can_create_submission(self, user: User) -> bool:
"""Checks if a user may create a new submission.
A user is permitted to create a new submission if there are no submissions
created or queued for him in the given project.
Args:
user(User): the user who wants to crete a new submission
Returns: whether the user can create a new submission in the project
"""
conflicting_states = [SubmissionState.CREATED,
SubmissionState.READY, SubmissionState.QUEUED]
conflict = Submission.query.filter(Submission.user == user) \
.filter(Submission.project == self.project) \
.filter(Submission.state.in_(conflicting_states)) \
.first()
return conflict is None
def delete_project(self):
"""Deletes a project
"""
log.info(f"[DELETE] Project {self.project.id} ({self.project.codename}).")
general.delete_entity(self.project)
def update_project(self, data: dict) -> Project:
"""Updates a project
Args:
data(dict): Data dictionary
Returns(Project): Updated project
"""
log.info(f"[UPDATE] Project {self.project.id} ({self.project.codename}): {data}.")
return _set_project_data(self.project, data)
def create_project(self, course: Course, data: dict) -> Project:
"""Creates a new project
Args:
course(Course): Course instance
data(dict): Project data
Returns(Project): Project instance
"""
new_project = Project(course=course)
self._project = new_project
_set_project_data(project=new_project, data=data)
log.info(f"[CREATE] Project for course {course.id} ({course.codename}): "