Skip to content

Commit c3659b8

Browse files
committed
Allow configuring subtasks to always show testcases.
Also defines a new (optional) format for parameters for ScoreTypeGroup. Also allows specifying testcases by lists instead of by regex.
1 parent 9d9ef69 commit c3659b8

File tree

2 files changed

+58
-18
lines changed

2 files changed

+58
-18
lines changed

cms/grading/scoretypes/GroupThreshold.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,21 @@ class GroupThreshold(ScoreTypeGroup):
4040

4141
def get_public_outcome(self, outcome, parameter):
4242
"""See ScoreTypeGroup."""
43-
threshold = parameter[2]
43+
if isinstance(parameter, list):
44+
threshold = parameter[2]
45+
else:
46+
threshold = parameter["threshold"]
4447
if 0.0 < outcome <= threshold:
4548
return N_("Correct")
4649
else:
4750
return N_("Not correct")
4851

4952
def reduce(self, outcomes, parameter):
5053
"""See ScoreTypeGroup."""
51-
threshold = parameter[2]
54+
if isinstance(parameter, list):
55+
threshold = parameter[2]
56+
else:
57+
threshold = parameter["threshold"]
5258
if all(0 < outcome <= threshold for outcome in outcomes):
5359
return 1.0
5460
else:

cms/grading/scoretypes/abc.py

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
import logging
3434
import re
35+
from typing import TypedDict, NotRequired
3536
from abc import ABCMeta, abstractmethod
3637

3738
from cms import FEEDBACK_LEVEL_RESTRICTED
@@ -192,26 +193,38 @@ class ScoreTypeAlone(ScoreType):
192193
pass
193194

194195

196+
class ScoreTypeGroupParametersDict(TypedDict):
197+
max_score: float
198+
testcases: int | str | list[str]
199+
threshold: NotRequired[float]
200+
always_show_testcases: NotRequired[bool]
201+
202+
203+
# the format of parameters is impossible to type-hint correctly, it seems...
204+
# this hint is (mostly) correct for the methods this base class implements,
205+
# subclasses might need a longer tuple.
206+
ScoreTypeGroupParameters = tuple[float, int | str | list[str]] | ScoreTypeGroupParametersDict
207+
208+
195209
class ScoreTypeGroup(ScoreTypeAlone):
196210
"""Intermediate class to manage tasks whose testcases are
197211
subdivided in groups (or subtasks). The score type parameters must
198212
be in the form [[m, t, ...], [...], ...], where m is the maximum
199213
score for the given subtask and t is the parameter for specifying
200-
testcases.
214+
testcases, or be a list of dicts matching ScoreTypeGroupParametersDict.
201215
202216
If t is int, it is interpreted as the number of testcases
203217
comprising the subtask (that are consumed from the first to the
204218
last, sorted by num). If t is unicode, it is interpreted as the regular
205-
expression of the names of target testcases. All t must have the same type.
219+
expression of the names of target testcases. If t is a list of strings,
220+
it is interpreted as a list of testcase codenames. All t must have the
221+
same type.
206222
207223
A subclass must implement the method 'get_public_outcome' and
208224
'reduce'.
209225
210226
"""
211-
# the format of parameters is impossible to type-hint correctly, it seems...
212-
# this hint is (mostly) correct for the methods this base class implements,
213-
# subclasses might need a longer tuple.
214-
parameters: list[tuple[float, int | str]]
227+
parameters: list[ScoreTypeGroupParameters]
215228

216229
# Mark strings for localization.
217230
N_("Subtask %(index)s")
@@ -333,6 +346,24 @@ class ScoreTypeGroup(ScoreTypeAlone):
333346
</div>
334347
{% endfor %}"""
335348

349+
def get_max_score(self, group_parameter: ScoreTypeGroupParameters) -> float:
350+
if isinstance(group_parameter, tuple) or isinstance(group_parameter, list):
351+
return group_parameter[0]
352+
else:
353+
return group_parameter["max_score"]
354+
355+
def get_testcases(self, group_parameter: ScoreTypeGroupParameters) -> int | str | list[str]:
356+
if isinstance(group_parameter, tuple) or isinstance(group_parameter, list):
357+
return group_parameter[1]
358+
else:
359+
return group_parameter["testcases"]
360+
361+
def get_always_show_testcases(self, group_parameter: ScoreTypeGroupParameters) -> bool:
362+
if isinstance(group_parameter, tuple) or isinstance(group_parameter, list):
363+
return False
364+
else:
365+
return group_parameter.get("always_show_testcases", False)
366+
336367
def retrieve_target_testcases(self) -> list[list[str]]:
337368
"""Return the list of the target testcases for each subtask.
338369
@@ -345,7 +376,7 @@ def retrieve_target_testcases(self) -> list[list[str]]:
345376
346377
"""
347378

348-
t_params = [p[1] for p in self.parameters]
379+
t_params = [self.get_testcases(p) for p in self.parameters]
349380

350381
if all(isinstance(t, int) for t in t_params):
351382

@@ -379,9 +410,12 @@ def retrieve_target_testcases(self) -> list[list[str]]:
379410

380411
return targets
381412

413+
elif all(isinstance(t, list) for t in t_params) and all(all(isinstance(t, str) for t in s) for s in t_params):
414+
return t_params
415+
382416
raise ValueError(
383417
"In the score type parameters, the second value of each element "
384-
"must have the same type (int or unicode)")
418+
"must have the same type (int, unicode or list of strings)")
385419

386420
def max_scores(self):
387421
"""See ScoreType.max_score."""
@@ -393,10 +427,10 @@ def max_scores(self):
393427

394428
for st_idx, parameter in enumerate(self.parameters):
395429
target = targets[st_idx]
396-
score += parameter[0]
430+
score += self.get_max_score(parameter)
397431
if all(self.public_testcases[tc_idx] for tc_idx in target):
398-
public_score += parameter[0]
399-
headers += ["Subtask %d (%g)" % (st_idx, parameter[0])]
432+
public_score += self.get_max_score(parameter)
433+
headers += ["Subtask %d (%g)" % (st_idx, self.get_max_score(parameter))]
400434

401435
return score, public_score, headers
402436

@@ -461,10 +495,10 @@ def compute_score(self, submission_result):
461495
st_score_fraction = self.reduce(
462496
[float(evaluations[tc_idx].outcome) for tc_idx in target],
463497
parameter)
464-
st_score = st_score_fraction * parameter[0]
498+
st_score = st_score_fraction * self.get_max_score(parameter)
465499
rounded_score = round(st_score, score_precision)
466500

467-
if tc_first_lowest_idx is not None and st_score_fraction < 1.0:
501+
if tc_first_lowest_idx is not None and st_score_fraction < 1.0 and not self.get_always_show_testcases(parameter):
468502
for tc in testcases:
469503
if not self.public_testcases[tc["idx"]]:
470504
continue
@@ -482,7 +516,7 @@ def compute_score(self, submission_result):
482516
"score_fraction": st_score_fraction,
483517
# But we also want the properly rounded score for display.
484518
"score": rounded_score,
485-
"max_score": parameter[0],
519+
"max_score": self.get_max_score(parameter),
486520
"testcases": testcases})
487521
if all(self.public_testcases[tc_idx] for tc_idx in target):
488522
public_score += st_score
@@ -495,7 +529,7 @@ def compute_score(self, submission_result):
495529
return score, subtasks, public_score, public_subtasks, ranking_details
496530

497531
@abstractmethod
498-
def get_public_outcome(self, outcome: float, parameter: list) -> str:
532+
def get_public_outcome(self, outcome: float, parameter: ScoreTypeGroupParameters) -> str:
499533
"""Return a public outcome from an outcome.
500534
501535
The public outcome is shown to the user, and this method
@@ -512,7 +546,7 @@ def get_public_outcome(self, outcome: float, parameter: list) -> str:
512546
pass
513547

514548
@abstractmethod
515-
def reduce(self, outcomes: list[float], parameter: list) -> float:
549+
def reduce(self, outcomes: list[float], parameter: ScoreTypeGroupParameters) -> float:
516550
"""Return the score of a subtask given the outcomes.
517551
518552
outcomes: the outcomes of the submission in

0 commit comments

Comments
 (0)