projects.py 7.9 KB
Newer Older
1
2
3
"""
Projects service
"""
4
import datetime
Barbora Kompišová's avatar
Barbora Kompišová committed
5
import logging
6
from datetime import timedelta
Barbora Kompišová's avatar
Barbora Kompišová committed
7
from typing import List
8

Peter Stanko's avatar
Peter Stanko committed
9
from flask_sqlalchemy import BaseQuery
Barbora Kompišová's avatar
Barbora Kompišová committed
10

11
from portal import storage
12
from portal.async_celery import tasks
13
14
15
from portal.database.models import ClientType, Course, Project, ProjectConfig, ProjectState, \
    Submission, SubmissionState, User
from portal.service import errors, filters
16
from portal.service.errors import ForbiddenError
17
18
from portal.service.general import GeneralService, get_new_name
from portal.tools import time
Barbora Kompišová's avatar
Barbora Kompišová committed
19
20
21
22

log = logging.getLogger(__name__)


23
class ProjectService(GeneralService):
24
25
26
27
    @property
    def storage(self):
        return storage

28
29
30
    @property
    def _entity_klass(self):
        return Project
Barbora Kompišová's avatar
Barbora Kompišová committed
31

32
33
34
    def _set_data(self, entity, **data):
        allowed = ['name', 'description', 'codename', 'assignment_url']
        return self.update_entity(entity, data, allowed=allowed)
Peter Stanko's avatar
Peter Stanko committed
35
36
37

    @property
    def project(self) -> Project:
38
        return self._entity
Peter Stanko's avatar
Peter Stanko committed
39
40
41
42
43
44
45
46
47

    def copy_project(self, target: Course) -> Project:
        """Copies a project to the target course

        Args:
            target(Course): Course instance

        Returns(Project): Copied project
        """
48

Peter Stanko's avatar
Peter Stanko committed
49
50
51
52
53
        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))
54
55
        log.info(f"[COPY] Project from {self.project.log_name} by"
                 f" {self.client_name}: {new_project.log_name}")
Peter Stanko's avatar
Peter Stanko committed
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
        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

79
    def create(self, course: Course, **data) -> Project:
Peter Stanko's avatar
Peter Stanko committed
80
81
82
83
84
85
86
87
88
        """Creates a new project

        Args:
            course(Course): Course instance
            data(dict): Project data

        Returns(Project): Project instance
        """
        new_project = Project(course=course)
89
90
        self._entity = new_project
        self._set_data(new_project, **data)
91
92
        log.info(f"[CREATE] Project  {new_project.log_name} "
                 f"by {self.client_name}: {new_project}")
Peter Stanko's avatar
Peter Stanko committed
93
94
95
96
97
98
99
100
101
102
103
        return new_project

    def update_project_config(self, data: dict) -> ProjectConfig:
        """Updates project config

        Args:
            data(dict): Config data

        Returns(ProjectConfig): Project config instance
        """
        self.project.set_config(**data)
104
        self.write_entity(self.project)
105
106
        log.info(f"[UPDATE] Configuration for project {self.project.log_name} "
                 f"by {self.client_name} in {data}.")
Peter Stanko's avatar
Peter Stanko committed
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
        return self.project.config

    def query_submissions(self, user: User) -> BaseQuery:
        """Queries submissions for the project and filters them by user.

        Args:
            user(User): User instance
        """
        course = self.project.course
        return Submission.query.filter(Submission.user_id == user.id) \
            .filter(Submission.project_id == self.project.id) \
            .join(Course, Course.id == self.project.course_id).filter(Course.id == course.id)

    def find_project_submissions(self, user_id: str = None) -> List[Submission]:
        """Finds all project submissions. If a user id is specified, returns only submissions created
        by that user.

        Args:
            user_id(str): User id (optional)

127
        Returns(List[Submission]): A list of all submissions in a project if no user_id is specified
Peter Stanko's avatar
Peter Stanko committed
128
129
130
131
132
        Otherwise a list of submissions created in the project by the specified user.

        """
        submissions = self.project.submissions
        if user_id:
133
            user = self.find.user(user_id)
Peter Stanko's avatar
Peter Stanko committed
134
135
136
            submissions = self.query_submissions(user)
        return submissions

137
138
    def check_submission_create(self, client=None):
        client = client or self.client
139
140
141
142
143
144
        if client.type != ClientType.USER:
            raise errors.SubmissionRefusedError(f"Worker cannot create submission.")
        user = self.find.user(client.id)
        # TODO: Might be good to add check whether it is in DEBUG Mode
        if user.is_admin:
            return True
145
        if self.project.state(timestamp=time.current_time()) != ProjectState.ACTIVE:
146
            raise errors.SubmissionRefusedError(f"Project {self.project.log_name} not active.")
147
148
        if not ProjectService(self.project).can_create_submission(client):
            raise errors.SubmissionRefusedError(
149
150
151
152
153
                f"Submission in project {self.project.log_name} already active.")
        self._check_latest_submission(user)
        return True

    def _check_latest_submission(self, user: User):
Peter Stanko's avatar
Peter Stanko committed
154
        latest_submission = self.get_latest_submission(user, ignore_cancel=True)
155
156
157
158
159
        if not latest_submission:
            return True
        latest_time = latest_submission.created_at
        now_time = datetime.datetime.utcnow()
        diff_time = now_time - latest_time
Peter Stanko's avatar
Peter Stanko committed
160
        delta = timedelta(minutes=30)  # TODO this should be configurable @mdujava
161
        log.info(f"Submission for project {self.project.log_name} by user {user.log_name},"
162
163
                 f" time delta: {diff_time} < {delta} = {diff_time < delta} created by"
                 f" {self.client_name}")
164
165
        if diff_time < delta:
            log.debug(f"Submission for {self.project.log_name} by {user.log_name} "
166
                      f"created by {self.client_name} "
167
168
169
170
                      f"rejected based on the time delta: {diff_time}")
            raise errors.SubmissionDiffTimeError(self.project, diff_time, user=user)
        return True

171
    def get_latest_submission(self, user, ignore_cancel: bool = False) -> Submission:
Peter Stanko's avatar
Peter Stanko committed
172
173
174
175
176
177
        query = Submission.query.filter(
            (Submission.user == user) & (Submission.project == self.project))
        if ignore_cancel:
            query = query.filter((Submission.state != SubmissionState.CANCELLED) &
                                 (SubmissionState != SubmissionState.ABORTED))
        return query.order_by(Submission.created_at.desc()).first()
178
179

    def find_all(self, course: Course) -> list:
Peter Stanko's avatar
Peter Stanko committed
180
181
182
183
184
        """List of all projects
        Args:
            course(Course): Course instance
        Returns(list): list of projects
        """
185
        perm_service = self._rest_service.permissions(course=course)
186
        if perm_service.check.permissions(['view_course_full']):
Peter Stanko's avatar
Peter Stanko committed
187
            return course.projects
188
        elif perm_service.check.permissions(['view_course_limited']):
Peter Stanko's avatar
Peter Stanko committed
189
            return filters.filter_projects_from_course(course=course, user=perm_service.client)
190
        raise ForbiddenError(perm_service.client)
Peter Stanko's avatar
Peter Stanko committed
191

Peter Stanko's avatar
Peter Stanko committed
192
193
194
    def update_project_test_files(self):
        """ Sends a request to Storage to update the project's test_files to the newest version.
        """
195
        log.info(f"[SERVICE] Updating project files for {self.project.log_name}")
Peter Stanko's avatar
Peter Stanko committed
196
        tasks.update_project_test_files.delay(self.project.course.id, self.project.id)
197
198
199
200
201
202
203
204

    def calculate_wait_time(self, user: User) -> datetime.datetime:
        latest_submission = self.get_latest_submission(user, ignore_cancel=True)
        if not latest_submission:
            return time.current_time()
        latest_time = latest_submission.created_at
        delta = datetime.timedelta(minutes=30)
        return latest_time + delta