Unverified Commit 302dfef3 authored by Peter Stanko's avatar Peter Stanko
Browse files

Submissions list implementation

parent f0df11cc
......@@ -898,6 +898,10 @@ class Submission(db.Model, EntityBase):
def course(self):
return self.project.course
@course.expression
def course(cls):
return Project.course
def change_state(self, new_state):
# open to extension (state transition validation, ...)
self.state = new_state
......
"""
Helpers to work and process the rest requests and responses
"""
from typing import Optional
from flask import request
import flask
from werkzeug.datastructures import ImmutableMultiDict
import portal.service.general
from portal.database import Course, Project, User
from portal.rest.schemas import SCHEMAS
from portal.service.errors import DataMissingError
......@@ -37,7 +41,55 @@ def require_data(action: str, resource: str) -> dict:
Returns(dict): Parsed json data
"""
json_data = request.get_json()
json_data = flask.request.get_json()
if not json_data:
raise DataMissingError(action=action, resource=resource)
return json_data
class FlaskRequestArgsHelper:
def __init__(self, parser: 'FlaskRequestHelper'):
self._parser = parser
@property
def _args(self):
return self._parser.request_args
@property
def _general(self) -> portal.service.general:
return portal.service.general
def course(self) -> Optional[Course]:
course = self._args.get('course')
if course is None:
return None
return self._general.find_course(course)
def user(self) -> Optional[User]:
user = self._args.get('user')
if user is None:
return None
return self._general.find_user(user)
def project(self) -> Optional[Project]:
project = self._args.get('project')
if project is None:
return None
course = self.course()
if course is None:
return None
return self._general.find_project(course, project)
class FlaskRequestHelper:
@property
def args(self) -> FlaskRequestArgsHelper:
return FlaskRequestArgsHelper(parser=self)
@property
def request(self) -> flask.Request:
return flask.request
@property
def request_args(self) -> ImmutableMultiDict:
return self.request.args
import time
import flask
from flask_jwt_extended import jwt_required
from flask_restplus import Namespace, Resource
from portal import logger, storage
from portal.database import Project
from portal.rest import rest_helpers
from portal.rest.schemas import SCHEMAS
from portal.service import auth, general, permissions
from portal.service.projects import ProjectService
from portal.service.reviews import ReviewService
from portal.service.submissions import SubmissionsService
......@@ -18,9 +14,18 @@ submissions_namespace = Namespace('submissions')
log = logger.get_logger(__name__)
@submissions_namespace.route('')
class SubmissionsResource(Resource):
@jwt_required
def get(self):
service = SubmissionsService()
submissions = service.find_all_submissions()
return SCHEMAS.dump('submissions', submissions)
@submissions_namespace.route('/<string:sid>')
@submissions_namespace.param('sid', 'Submission id')
@submissions_namespace.response(404, 'Submissions not found')
@submissions_namespace.response(404, 'Submission not found')
class SubmissionResource(Resource):
@jwt_required
# @submissions_namespace.response(200, 'Submission found', model=submission_schema)
......@@ -247,4 +252,3 @@ class SubmissionReview(Resource):
review_service.create_review_items(items=data['review_items'], author=client)
return SCHEMAS.dump('review', submission.review), 201
......@@ -105,7 +105,7 @@ def find_resource(identifier: str, resource: str, query, throws: bool = True):
return instance
def find_group(course: Course, identifier: str, throws=True) -> Group:
def find_group(course: Union[Course, str], identifier: str, throws=True) -> Group:
"""Gets a Group instance
Args:
......@@ -115,7 +115,8 @@ def find_group(course: Course, identifier: str, throws=True) -> Group:
Returns(Group): Group entity instance
"""
query = Group.query.filter_by(course=course).filter(
query = _get_course(Group.query, course=course)
query = query.filter(
(Group.id == identifier) | (Group.codename == sanitize_code_name(identifier))
)
......@@ -168,6 +169,14 @@ def find_course(identifier: str, throws=True) -> Course:
)
def _get_course(query, course: Union[Course, str]):
if isinstance(course, str):
query = query.filter((Course.id == course) | (Course.codename == course))
else:
query = query.filter_by(course=course)
return query
def find_project(course, identifier: str, throws=True) -> Project:
"""Gets a Project instance
......@@ -178,7 +187,8 @@ def find_project(course, identifier: str, throws=True) -> Project:
Returns(Project): Project entity instance
"""
query = Project.query.filter_by(course=course).filter(
query = _get_course(Project.query, course=course)
query = query.filter(
(Project.id == identifier) | (Project.codename == sanitize_code_name(identifier))
)
......@@ -203,7 +213,7 @@ def find_secret(client, identifier, throws=True) -> Secret:
)
def find_role(course: Course, identifier: str, throws=True) -> Role:
def find_role(course: Union[Course, str], identifier: str, throws=True) -> Role:
"""Gets a Role instance
Args:
......@@ -213,7 +223,8 @@ def find_role(course: Course, identifier: str, throws=True) -> Role:
Returns(Role): Role entity instance
"""
query = Role.query.filter_by(course=course).filter(
query = _get_course(Role.query, course=course)
query = query.filter(
(Role.id == identifier) | (Role.codename == sanitize_code_name(identifier))
)
......@@ -252,8 +263,7 @@ def find_user(identifier: str, throws=True) -> User:
Returns(User): User entity instance
"""
query = User.query.filter(
((User.id == identifier) | (User.username ==
identifier) | (User.email == identifier))
((User.id == identifier) | (User.username ==identifier) | (User.email == identifier))
)
return find_resource(
......
......@@ -66,15 +66,25 @@ class PermissionServiceCheck:
def read_submissions_all(self) -> bool:
return self.permissions(['read_submissions_all'])
def read_submissions_group(self, submission: Submission) -> bool:
def read_submissions_group(self, submission: Submission = None) -> bool:
submission = submission or self.service.submission
return self.read_submissions_all() or \
self.service.submission_access_group(submission, ['read_submissions_groups'])
def read_submissions_own(self, submission: Submission) -> bool:
def read_submissions_own(self, submission: Submission = None) -> bool:
submission = submission or self.service.submission
return self.read_submissions_all() or \
self.permissions(['read_submissions_own']) and \
submission.user == self.service.client_owner
def read_submission(self, submission=None):
submission = submission or self.service.submission
checks = [
self.read_submissions_group(submission),
self.read_submissions_own(submission=submission)
]
return self.any_check(*checks)
class PermissionServiceRequire:
def __init__(self, service: 'PermissionsService'):
......@@ -128,14 +138,16 @@ class PermissionServiceRequire:
]
self.any_check(*checks)
def read_submission(self, submission):
def read_submission(self, submission=None):
submission = submission or self.service.submission
checks = [
self._check.read_submissions_group(submission),
self._check.read_submissions_own(submission=submission)
]
self.any_check(*checks)
def read_submission_group(self, submission):
def read_submission_group(self, submission=None):
submission = submission or self.service.submission
checks = [
self._check.read_submissions_group(submission=submission)
]
......@@ -162,9 +174,12 @@ class PermissionServiceRequire:
class PermissionsService:
def __init__(self, client=None, course=None):
def __init__(self, client=None, course=None, submission=None):
self._client = client or auth.find_client()
self._course = course
if course is None and submission is not None:
self._course = submission.course
self._submission = submission
self._requires = PermissionServiceRequire(self)
self._checks = PermissionServiceCheck(self)
......@@ -188,6 +203,10 @@ class PermissionsService:
def course(self) -> Course:
return self._course
@property
def submission(self) -> Submission:
return self._submission
def get_effective_permissions(self, course_id: str = None) -> dict:
"""Gets effective permissions for a course in course.
If no course is specified, returns the client's
......
......@@ -15,8 +15,10 @@ from werkzeug.utils import secure_filename
from portal import storage
from portal.async_celery import submission_processor, tasks
from portal.database.models import Project, Submission, SubmissionState, User, Worker
from portal.rest.rest_helpers import FlaskRequestHelper
from portal.service import errors, general
from portal.service.general import delete_entity, write_entity
from portal.service.permissions import PermissionsService
from portal.service.projects import ProjectService
log = logging.getLogger(__name__)
......@@ -71,6 +73,15 @@ class SubmissionsService(object):
def __init__(self, submission: Submission = None):
self._submission = submission
self._perm_service = PermissionsService()
@property
def perm_service(self):
return self._perm_service
@property
def client(self):
return self.perm_service.client
@property
def submission(self) -> Submission:
......@@ -218,3 +229,27 @@ class SubmissionsService(object):
if project.config.test_files_commit_hash is not None:
break
time.sleep(1)
def filter_user_avail_submissions(self, query):
submissions = query.all()
return [submission for submission in submissions
if PermissionsService(submission=submission).check.read_submission()]
def find_all_submissions(self):
request_helper = FlaskRequestHelper()
query = Submission.query
user = request_helper.args.user()
project = request_helper.args.project()
course = request_helper.args.course()
if user:
query = query.filter(Submission.user == user)
if course:
query = query.filter(Submission.course == course)
if project:
query = query.filter(Submission.project == project)
return self.filter_user_avail_submissions(query=query)
import json
from portal.database.models import Course, Review, ReviewItem, Submission, SubmissionState
from portal.database.models import Course, Review, ReviewItem, Submission, SubmissionState, Project
from portal.service import general
from . import utils
......@@ -29,6 +29,48 @@ def create_submission(client) -> Submission:
return general.find_submission(new_submission['id'])
def test_list_all_avail(client):
db_subm = Submission.query.all()
response = utils.make_request(client, f'/submissions', method='get')
assert response.status_code == 200
assert response.mimetype == 'application/json'
resp_submissions = utils.extract_data(response)
assert len(resp_submissions) == len(db_subm)
def test_list_all_avail_for_user(client):
student = general.find_user('student1')
db_subm = Submission.query.filter(Submission.user == student).all()
response = utils.make_request(client, f'/submissions?user=student1', method='get')
assert response.status_code == 200
assert response.mimetype == 'application/json'
resp_submissions = utils.extract_data(response)
assert len(resp_submissions) == len(db_subm)
def test_list_all_avail_for_course(client):
course = general.find_course('testcourse1')
db_subm = Submission.query.filter(Project.course == course).all()
response = utils.make_request(client, f'/submissions?course=testcourse1', method='get')
assert response.status_code == 200
assert response.mimetype == 'application/json'
resp_submissions = utils.extract_data(response)
assert len(resp_submissions) == len(db_subm)
def test_list_all_avail_for_project(client):
project = general.find_project('testcourse1', 'hw01')
db_subm = Submission.query.filter(Submission.project == project).all()
response = utils.make_request(client, f'/submissions?course=testcourse1&project=hw01')
assert response.status_code == 200
assert response.mimetype == 'application/json'
resp_submissions = utils.extract_data(response)
assert len(resp_submissions) == len(db_subm)
# missing tests for working with zip
def test_read(client):
submissions = Submission.query.all()
......
......@@ -19,13 +19,12 @@ API_PREFIX = "/api/v1.0"
def extract_data(response: Response) -> dict:
data = response.get_data(as_text=True)
data = json.loads(data)
data = response.get_json()
log.debug(f"[EXTRACT] Data: {data}")
return data
def make_request(client: FlaskClient, url: str, method: str,
def make_request(client: FlaskClient, url: str, method: str= 'get',
credentials: str = None, headers: dict = None, data: str = None) -> Response:
""" Creates an authenticated request to an endpoint.
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment