|
48 | 48 | collections.MutableMapping = collections.abc.MutableMapping |
49 | 49 |
|
50 | 50 | import tornado.web |
| 51 | +from sqlalchemy.orm import joinedload, selectinload |
51 | 52 |
|
52 | 53 | from cms import config, TOKEN_MODE_MIXED |
53 | 54 | from cms.db import Contest, Submission, Task, UserTest |
| 55 | +from cms.grading.scoring import task_score |
54 | 56 | from cms.locale import filter_language_codes |
55 | 57 | from cms.server import FileHandlerMixin |
56 | 58 | from cms.server.contest.authentication import authenticate_request |
@@ -193,6 +195,93 @@ def get_current_user(self) -> Participation | None: |
193 | 195 | self.impersonated_by_admin = impersonated |
194 | 196 | return participation |
195 | 197 |
|
| 198 | + def _load_participation_for_scores( |
| 199 | + self, participation: Participation |
| 200 | + ) -> Participation | None: |
| 201 | + """Load participation with relationships needed for task score computation.""" |
| 202 | + return ( |
| 203 | + self.sql_session.query(Participation) |
| 204 | + .filter(Participation.id == participation.id) |
| 205 | + .options( |
| 206 | + joinedload(Participation.user), |
| 207 | + joinedload(Participation.contest) |
| 208 | + .joinedload(Contest.tasks) |
| 209 | + .joinedload(Task.active_dataset), |
| 210 | + selectinload(Participation.submissions).joinedload(Submission.token), |
| 211 | + selectinload(Participation.submissions).joinedload(Submission.results), |
| 212 | + ) |
| 213 | + .first() |
| 214 | + ) |
| 215 | + |
| 216 | + def _compute_task_scores( |
| 217 | + self, |
| 218 | + participation: Participation, |
| 219 | + *, |
| 220 | + actual_phase: int, |
| 221 | + hide_zero_max_public: bool = True, |
| 222 | + ) -> dict[int, tuple[float, float, str]]: |
| 223 | + """Compute per-task scores for UI task lists. |
| 224 | +
|
| 225 | + By default, this shows public scores. If a token has been played on a |
| 226 | + task (or we're in analysis mode), it shows the tokened/total score for |
| 227 | + that task instead. |
| 228 | + """ |
| 229 | + task_scores: dict[int, tuple[float, float, str]] = {} |
| 230 | + tokened_task_ids = { |
| 231 | + s.task_id for s in participation.submissions if s.official and s.tokened() |
| 232 | + } |
| 233 | + |
| 234 | + for task in participation.contest.tasks: |
| 235 | + score_type = task.active_dataset.score_type_object |
| 236 | + |
| 237 | + has_tokened_submission = task.id in tokened_task_ids |
| 238 | + show_tokened_total = ( |
| 239 | + score_type.max_public_score < score_type.max_score |
| 240 | + and (has_tokened_submission or actual_phase == 3) |
| 241 | + ) |
| 242 | + |
| 243 | + if show_tokened_total: |
| 244 | + if actual_phase == 3: |
| 245 | + # In analysis mode users can see full scores, so do not |
| 246 | + # restrict to tokened submissions. |
| 247 | + score_value, _ = task_score(participation, task, rounded=True) |
| 248 | + else: |
| 249 | + score_value, _ = task_score( |
| 250 | + participation, task, only_tokened=True, rounded=True |
| 251 | + ) |
| 252 | + max_score_value = round(score_type.max_score, task.score_precision) |
| 253 | + score_message = score_type.format_score( |
| 254 | + score_value, |
| 255 | + score_type.max_score, |
| 256 | + None, |
| 257 | + task.score_precision, |
| 258 | + translation=self.translation, |
| 259 | + ) |
| 260 | + else: |
| 261 | + max_public_score = round( |
| 262 | + score_type.max_public_score, task.score_precision |
| 263 | + ) |
| 264 | + |
| 265 | + # Optionally hide entries with no public score. |
| 266 | + if hide_zero_max_public and max_public_score <= 0: |
| 267 | + continue |
| 268 | + |
| 269 | + score_value, _ = task_score( |
| 270 | + participation, task, public=True, rounded=True |
| 271 | + ) |
| 272 | + max_score_value = max_public_score |
| 273 | + score_message = score_type.format_score( |
| 274 | + score_value, |
| 275 | + score_type.max_public_score, |
| 276 | + None, |
| 277 | + task.score_precision, |
| 278 | + translation=self.translation, |
| 279 | + ) |
| 280 | + |
| 281 | + task_scores[task.id] = (score_value, max_score_value, score_message) |
| 282 | + |
| 283 | + return task_scores |
| 284 | + |
196 | 285 | def render_params(self): |
197 | 286 | ret = super().render_params() |
198 | 287 |
|
@@ -230,6 +319,23 @@ def render_params(self): |
230 | 319 | # set the timezone used to format timestamps |
231 | 320 | ret["timezone"] = get_timezone(participation.user, self.contest) |
232 | 321 |
|
| 322 | + if self.contest.show_task_scores_in_sidebar and ( |
| 323 | + ret["actual_phase"] >= 0 or participation.unrestricted |
| 324 | + ): |
| 325 | + loaded_participation = self._load_participation_for_scores(participation) |
| 326 | + if loaded_participation is not None: |
| 327 | + # Keep references synchronized with the fully loaded objects. |
| 328 | + participation = loaded_participation |
| 329 | + self.contest = participation.contest |
| 330 | + ret["contest"] = self.contest |
| 331 | + ret["participation"] = participation |
| 332 | + ret["user"] = participation.user |
| 333 | + ret["sidebar_task_scores"] = self._compute_task_scores( |
| 334 | + participation, |
| 335 | + actual_phase=ret["actual_phase"], |
| 336 | + hide_zero_max_public=True, |
| 337 | + ) |
| 338 | + |
233 | 339 | # some information about token configuration |
234 | 340 | ret["tokens_contest"] = self.contest.token_mode |
235 | 341 |
|
|
0 commit comments