Skip to content

Commit d5c57bd

Browse files
authored
feat: change deadlines config (#95)
* feat: move schedule in deadlines field * test: add deadlines tests * fix: export only started public * test: remove test where group name = task name * style: fix style
1 parent 76bec3b commit d5c57bd

File tree

6 files changed

+596
-167
lines changed

6 files changed

+596
-167
lines changed

checker/configs/manytask.py

+121-63
Original file line numberDiff line numberDiff line change
@@ -29,44 +29,19 @@ class ManytaskUiConfig(CustomBaseModel):
2929
task_url_template: str # $GROUP_NAME $TASK_NAME vars are available
3030
links: Optional[dict[str, str]] = None # pedantic 3.9 require Optional, not | None
3131

32-
33-
class ManytaskDeadlinesType(Enum):
34-
HARD = "hard"
35-
INTERPOLATE = "interpolate"
36-
37-
38-
class ManytaskDeadlinesConfig(CustomBaseModel):
39-
timezone: str
40-
41-
# Note: use Optional/Union[...] instead of ... | None as pydantic does not support | in older python versions
42-
deadlines: ManytaskDeadlinesType = ManytaskDeadlinesType.HARD
43-
max_submissions: Optional[int] = None
44-
submission_penalty: float = 0
45-
46-
task_url: Optional[AnyUrl] = None # $GROUP_NAME $TASK_NAME vars are available
47-
48-
@field_validator("task_url")
32+
@field_validator("task_url_template")
4933
@classmethod
50-
def check_task_url(cls, data: AnyUrl | None) -> AnyUrl | None:
51-
if data is not None and data.scheme not in ("http", "https"):
52-
raise ValueError("task_url should be http or https")
34+
def check_task_url_template(cls, data: str | None) -> str | None:
35+
if data is not None and (not data.startswith("http://") and not data.startswith("https://")):
36+
raise ValueError("task_url_template should be http or https")
37+
# if data is not None and "$GROUP_NAME" not in data and "$TASK_NAME" not in data:
38+
# raise ValueError("task_url should contain at least one of $GROUP_NAME and $TASK_NAME vars")
5339
return data
5440

55-
@field_validator("max_submissions")
56-
@classmethod
57-
def check_max_submissions(cls, data: int | None) -> int | None:
58-
if data is not None and data <= 0:
59-
raise ValueError("max_submissions should be positive")
60-
return data
6141

62-
@field_validator("timezone")
63-
@classmethod
64-
def check_valid_timezone(cls, timezone: str) -> str:
65-
try:
66-
ZoneInfo(timezone)
67-
except ZoneInfoNotFoundError as e:
68-
raise ValueError(str(e))
69-
return timezone
42+
class ManytaskDeadlinesType(Enum):
43+
HARD = "hard"
44+
INTERPOLATE = "interpolate"
7045

7146

7247
class ManytaskTaskConfig(CustomBaseModel):
@@ -102,6 +77,11 @@ class ManytaskGroupConfig(CustomBaseModel):
10277
def name(self) -> str:
10378
return self.group
10479

80+
def replace_timezone(self, timezone: ZoneInfo) -> None:
81+
self.start = self.start.replace(tzinfo=timezone)
82+
self.end = self.end.replace(tzinfo=timezone) if isinstance(self.end, datetime) else self.end
83+
self.steps = {k: v.replace(tzinfo=timezone) for k, v in self.steps.items() if isinstance(v, datetime)}
84+
10585
@model_validator(mode="after")
10686
def check_dates(self) -> "ManytaskGroupConfig":
10787
# check end
@@ -135,36 +115,106 @@ def check_dates(self) -> "ManytaskGroupConfig":
135115
return self
136116

137117

138-
class ManytaskConfig(CustomBaseModel, YamlLoaderMixin["ManytaskConfig"]):
139-
"""Manytask configuration."""
118+
class ManytaskDeadlinesConfig(CustomBaseModel):
119+
timezone: str
140120

141-
version: int # if config exists, version is always present
121+
# Note: use Optional/Union[...] instead of ... | None as pydantic does not support | in older python versions
122+
deadlines: ManytaskDeadlinesType = ManytaskDeadlinesType.HARD
123+
max_submissions: Optional[int] = None
124+
submission_penalty: float = 0
142125

143-
settings: ManytaskSettingsConfig
144-
ui: ManytaskUiConfig
145-
deadlines: ManytaskDeadlinesConfig
146-
schedule: list[ManytaskGroupConfig]
126+
schedule: list[ManytaskGroupConfig] # list of groups with tasks
127+
128+
def get_now_with_timezone(self) -> datetime:
129+
return datetime.now(tz=ZoneInfo(self.timezone))
130+
131+
@field_validator("max_submissions")
132+
@classmethod
133+
def check_max_submissions(cls, data: int | None) -> int | None:
134+
if data is not None and data <= 0:
135+
raise ValueError("max_submissions should be positive")
136+
return data
137+
138+
@field_validator("submission_penalty")
139+
@classmethod
140+
def check_submission_penalty(cls, data: float) -> float:
141+
if data < 0:
142+
raise ValueError("submission_penalty should be non-negative")
143+
return data
144+
145+
@field_validator("timezone")
146+
@classmethod
147+
def check_valid_timezone(cls, timezone: str) -> str:
148+
try:
149+
ZoneInfo(timezone)
150+
except ZoneInfoNotFoundError as e:
151+
raise ValueError(str(e))
152+
return timezone
153+
154+
@field_validator("schedule")
155+
@classmethod
156+
def check_group_task_names_unique(cls, data: list[ManytaskGroupConfig]) -> list[ManytaskGroupConfig]:
157+
group_names = [group.name for group in data]
158+
tasks_names = [task.name for group in data for task in group.tasks]
159+
160+
# group names unique
161+
group_names_duplicates = [name for name in group_names if group_names.count(name) > 1]
162+
if group_names_duplicates:
163+
raise ValueError(f"Group names should be unique, duplicates: {group_names_duplicates}")
164+
165+
# task names unique
166+
tasks_names_duplicates = [name for name in tasks_names if tasks_names.count(name) > 1]
167+
if tasks_names_duplicates:
168+
raise ValueError(f"Task names should be unique, duplicates: {tasks_names_duplicates}")
169+
170+
# # group names and task names not intersect (except single task in a group with the same name)
171+
# no_single_task_groups = [group for group in data if not (len(group.tasks) == 1
172+
# and group.name == group.tasks[0].name)]
173+
174+
return data
175+
176+
@model_validator(mode="after")
177+
def set_timezone(self) -> "ManytaskDeadlinesConfig":
178+
timezone = ZoneInfo(self.timezone)
179+
for group in self.schedule:
180+
group.replace_timezone(timezone)
181+
return self
147182

148183
def get_groups(
149184
self,
150185
enabled: bool | None = None,
186+
started: bool | None = None,
187+
*,
188+
now: datetime | None = None,
151189
) -> list[ManytaskGroupConfig]:
190+
if now is None:
191+
now = self.get_now_with_timezone()
192+
152193
groups = [group for group in self.schedule]
153194

154195
if enabled is not None:
155196
groups = [group for group in groups if group.enabled == enabled]
156197

157-
# TODO: check time
198+
if started is not None:
199+
if started:
200+
groups = [group for group in groups if group.start <= now]
201+
else:
202+
groups = [group for group in groups if group.start > now]
158203

159204
return groups
160205

161206
def get_tasks(
162207
self,
163208
enabled: bool | None = None,
209+
started: bool | None = None,
210+
*,
211+
now: datetime | None = None,
164212
) -> list[ManytaskTaskConfig]:
165213
# TODO: refactor
214+
if now is None:
215+
now = self.get_now_with_timezone()
166216

167-
groups = self.get_groups()
217+
groups = self.get_groups(started=started, now=now)
168218

169219
if enabled is True:
170220
groups = [group for group in groups if group.enabled]
@@ -185,31 +235,39 @@ def get_tasks(
185235
if extra_task not in tasks:
186236
tasks.append(extra_task)
187237

188-
# TODO: check time
189-
190238
return tasks
191239

240+
241+
class ManytaskConfig(CustomBaseModel, YamlLoaderMixin["ManytaskConfig"]):
242+
"""Manytask configuration."""
243+
244+
version: int # if config exists, version is always present
245+
246+
settings: ManytaskSettingsConfig
247+
ui: ManytaskUiConfig
248+
deadlines: ManytaskDeadlinesConfig
249+
250+
def get_groups(
251+
self,
252+
enabled: bool | None = None,
253+
started: bool | None = None,
254+
*,
255+
now: datetime | None = None,
256+
) -> list[ManytaskGroupConfig]:
257+
return self.deadlines.get_groups(enabled=enabled, started=started, now=now)
258+
259+
def get_tasks(
260+
self,
261+
enabled: bool | None = None,
262+
started: bool | None = None,
263+
*,
264+
now: datetime | None = None,
265+
) -> list[ManytaskTaskConfig]:
266+
return self.deadlines.get_tasks(enabled=enabled, started=started, now=now)
267+
192268
@field_validator("version")
193269
@classmethod
194270
def check_version(cls, data: int) -> int:
195271
if data != 1:
196272
raise ValueError(f"Only version 1 is supported for {cls.__name__}")
197273
return data
198-
199-
@field_validator("schedule")
200-
@classmethod
201-
def check_group_names_unique(cls, data: list[ManytaskGroupConfig]) -> list[ManytaskGroupConfig]:
202-
groups = [group.name for group in data]
203-
duplicates = [name for name in groups if groups.count(name) > 1]
204-
if duplicates:
205-
raise ValueError(f"Group names should be unique, duplicates: {duplicates}")
206-
return data
207-
208-
@field_validator("schedule")
209-
@classmethod
210-
def check_task_names_unique(cls, data: list[ManytaskGroupConfig]) -> list[ManytaskGroupConfig]:
211-
tasks_names = [task.name for group in data for task in group.tasks]
212-
duplicates = [name for name in tasks_names if tasks_names.count(name) > 1]
213-
if duplicates:
214-
raise ValueError(f"Task names should be unique, duplicates: {duplicates}")
215-
return data

checker/course.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import warnings
44
from collections.abc import Generator
55
from dataclasses import dataclass
6+
from datetime import datetime
67
from pathlib import Path
78
from typing import Any
89

@@ -66,8 +67,11 @@ def validate(self) -> None:
6667
def get_groups(
6768
self,
6869
enabled: bool | None = None,
70+
started: bool | None = None,
71+
*,
72+
now: datetime | None = None,
6973
) -> list[FileSystemGroup]:
70-
search_deadlines_groups = self.manytask_config.get_groups(enabled=enabled)
74+
search_deadlines_groups = self.manytask_config.get_groups(enabled=enabled, started=started, now=now)
7175

7276
return [
7377
self.potential_groups[deadline_group.name]
@@ -78,8 +82,11 @@ def get_groups(
7882
def get_tasks(
7983
self,
8084
enabled: bool | None = None,
85+
started: bool | None = None,
86+
*,
87+
now: datetime | None = None,
8188
) -> list[FileSystemTask]:
82-
search_deadlines_tasks = self.manytask_config.get_tasks(enabled=enabled)
89+
search_deadlines_tasks = self.manytask_config.get_tasks(enabled=enabled, started=started, now=now)
8390

8491
return [
8592
self.potential_tasks[deadline_task.name]

checker/exporter.py

+8
Original file line numberDiff line numberDiff line change
@@ -214,10 +214,18 @@ def export_public(
214214
str(Path(group.relative_path).relative_to(self.reference_root))
215215
for group in self.course.get_groups(enabled=False)
216216
],
217+
*[
218+
str(Path(group.relative_path).relative_to(self.reference_root))
219+
for group in self.course.get_groups(started=False)
220+
],
217221
*[
218222
str(Path(task.relative_path).relative_to(self.reference_root))
219223
for task in self.course.get_tasks(enabled=False)
220224
],
225+
*[
226+
str(Path(task.relative_path).relative_to(self.reference_root))
227+
for task in self.course.get_tasks(started=False)
228+
],
221229
]
222230

223231
print(f"Copy from {self.reference_root} to {target}")

0 commit comments

Comments
 (0)