From 9059aeb2d6800d92713ac1e1decfbe079780c211 Mon Sep 17 00:00:00 2001
From: Loris Sauter
Date: Tue, 16 Jul 2024 14:48:34 +0200
Subject: [PATCH] First draft #467 and tested in synchronous setting
---
.../types/evaluation/ApiEvaluationState.kt | 4 +-
.../template/tasks/options/ApiScoreOption.kt | 3 +-
.../kotlin/dev/dres/mgmt/TemplateManager.kt | 12 +++--
.../run/InteractiveAsynchronousRunManager.kt | 25 +++++-----
.../dev/dres/run/InteractiveRunManager.kt | 6 +--
.../run/InteractiveSynchronousRunManager.kt | 18 ++++---
.../run/updatables/EndOnSubmitUpdatable.kt | 22 +++++----
.../updatables/ProlongOnSubmitUpdatable.kt | 8 +++-
doc/oas-client.json | 18 +++----
doc/oas.json | 18 +++----
.../advanced-builder-dialog.component.ts | 4 +-
...mpetition-builder-task-dialog.component.ts | 10 ++--
.../competition-builder.component.ts | 4 +-
.../competition-builder.module.ts | 3 +-
.../task-controls.component.html | 2 +-
.../create-task-type-dialog.component.html} | 5 +-
.../create-task-type-dialog.component.scss} | 0
.../create-task-type-dialog.component.ts} | 48 ++++---------------
...scription-external-form-field.component.ts | 6 +--
...ion-external-image-form-field.component.ts | 4 +-
.../query-description-form-field.component.ts | 6 +--
...ription-media-item-form-field.component.ts | 6 +--
.../task-template-editor.component.html | 3 +-
.../task-template-editor.component.ts | 10 ++--
.../task-types-list.component.ts | 6 +--
.../template-builder-components.module.ts | 8 +++-
.../template-builder}/require-match.ts | 0
.../task-template-form.builder.ts} | 19 ++++----
.../src/app/viewer/task-viewer.component.ts | 23 +++++----
29 files changed, 150 insertions(+), 151 deletions(-)
rename frontend/src/app/{competition/competition-builder/competition-builder-task-type-dialog/competition-builder-task-type-dialog.component.html => template/template-builder/components/create-task-type-dialog/create-task-type-dialog.component.html} (96%)
rename frontend/src/app/{competition/competition-builder/competition-builder-task-type-dialog/competition-builder-task-type-dialog.component.scss => template/template-builder/components/create-task-type-dialog/create-task-type-dialog.component.scss} (100%)
rename frontend/src/app/{competition/competition-builder/competition-builder-task-type-dialog/competition-builder-task-type-dialog.component.ts => template/template-builder/components/create-task-type-dialog/create-task-type-dialog.component.ts} (82%)
rename frontend/src/app/{competition/competition-builder/competition-builder-task-dialog => template/template-builder}/require-match.ts (100%)
rename frontend/src/app/{competition/competition-builder/competition-builder-task-dialog/competition-form.builder.ts => template/template-builder/task-template-form.builder.ts} (98%)
diff --git a/backend/src/main/kotlin/dev/dres/api/rest/types/evaluation/ApiEvaluationState.kt b/backend/src/main/kotlin/dev/dres/api/rest/types/evaluation/ApiEvaluationState.kt
index bda41113c..bb92bb023 100644
--- a/backend/src/main/kotlin/dev/dres/api/rest/types/evaluation/ApiEvaluationState.kt
+++ b/backend/src/main/kotlin/dev/dres/api/rest/types/evaluation/ApiEvaluationState.kt
@@ -18,7 +18,7 @@ data class ApiEvaluationState(
val taskId: String?,
val taskStatus: ApiTaskStatus,
val taskTemplateId: String?,
- val timeLeft: Long,
+ val timeLeft: Long?,
val timeElapsed: Long
) {
constructor(run: InteractiveRunManager, context: RunActionContext) : this(
@@ -27,7 +27,7 @@ data class ApiEvaluationState(
run.currentTask(context)?.taskId,
run.currentTask(context)?.status ?: ApiTaskStatus.NO_TASK,
run.currentTaskTemplate(context).templateId,
- run.timeLeft(context) / 1000,
+ run.timeLeft(context)?.div(1000),
run.timeElapsed(context) / 1000
)
}
diff --git a/backend/src/main/kotlin/dev/dres/api/rest/types/template/tasks/options/ApiScoreOption.kt b/backend/src/main/kotlin/dev/dres/api/rest/types/template/tasks/options/ApiScoreOption.kt
index 09403f938..66940ab86 100644
--- a/backend/src/main/kotlin/dev/dres/api/rest/types/template/tasks/options/ApiScoreOption.kt
+++ b/backend/src/main/kotlin/dev/dres/api/rest/types/template/tasks/options/ApiScoreOption.kt
@@ -10,7 +10,7 @@ import dev.dres.data.model.template.task.options.DbScoreOption
* @version 1.1.0
*/
enum class ApiScoreOption {
- KIS, AVS, LEGACY_AVS;
+ KIS, AVS, LEGACY_AVS, NOOP;
/**
* Converts this [ApiScoreOption] to a [DbScoreOption] representation. Requires an ongoing transaction.
@@ -21,5 +21,6 @@ enum class ApiScoreOption {
KIS -> DbScoreOption.KIS
AVS -> DbScoreOption.AVS
LEGACY_AVS -> DbScoreOption.LEGACY_AVS
+ NOOP -> DbScoreOption.NOOP
}
}
diff --git a/backend/src/main/kotlin/dev/dres/mgmt/TemplateManager.kt b/backend/src/main/kotlin/dev/dres/mgmt/TemplateManager.kt
index 896edcd20..5156a94d7 100644
--- a/backend/src/main/kotlin/dev/dres/mgmt/TemplateManager.kt
+++ b/backend/src/main/kotlin/dev/dres/mgmt/TemplateManager.kt
@@ -93,9 +93,15 @@ object TemplateManager {
/* Update task type information. */
val taskTypes = apiEvaluationTemplate.taskTypes.map { it.name }.toTypedArray()
- dbEvaluationTemplate.taskTypes.removeAll(
- DbTaskType.query(DbTaskType::evaluation eq dbEvaluationTemplate and not(DbTaskType::name.containsIn(*taskTypes)))
- )
+ val taskTypesToDeleteQuery = DbTaskType.query(DbTaskType::evaluation eq dbEvaluationTemplate and not(DbTaskType::name.containsIn(*taskTypes)))
+ /* Manual cleanup, as removeAll does not cleanup related children. */
+ val configuredOptionsToDelIds= taskTypesToDeleteQuery.toList().map {
+ it.configurations.toList().map{opt -> opt.entityId}
+ }.flatten().toTypedArray()
+ DbConfiguredOption.all().toList().filter{configuredOptionsToDelIds.contains(it.entityId)}.forEach{it.delete()}
+
+ dbEvaluationTemplate.taskTypes.removeAll(taskTypesToDeleteQuery)
+
for (apiTaskType in apiEvaluationTemplate.taskTypes) {
val taskType =
DbTaskType.findOrNew(DbTaskType.query((DbTaskType::name eq apiTaskType.name) and (DbTaskType::evaluation eq dbEvaluationTemplate))) {
diff --git a/backend/src/main/kotlin/dev/dres/run/InteractiveAsynchronousRunManager.kt b/backend/src/main/kotlin/dev/dres/run/InteractiveAsynchronousRunManager.kt
index a658417b9..ce3422c29 100644
--- a/backend/src/main/kotlin/dev/dres/run/InteractiveAsynchronousRunManager.kt
+++ b/backend/src/main/kotlin/dev/dres/run/InteractiveAsynchronousRunManager.kt
@@ -371,22 +371,25 @@ class InteractiveAsynchronousRunManager(
/**
* Returns the time in milliseconds that is left until the end of the currently running task for the given team.
* Only works if the [InteractiveAsynchronousRunManager] is in state [RunManagerStatus.ACTIVE].
- * If no task is running or is running perpetually, this method returns -1L.
+ * If no task is running, this method returns -1L.
+ * If the task runs perpetually, this method returns NULL.
*
* @param context The [RunActionContext] used for the invocation.
- * @return Time remaining until the task will end or -1, if no task is running.
+ * @return Time remaining until the task will end or -1, if no task is running. NULL if the task runs perpetually.
*/
- override fun timeLeft(context: RunActionContext): Long = this.stateLock.read {
-
+ override fun timeLeft(context: RunActionContext): Long? = this.stateLock.read {
val currentTaskRun = this.currentTask(context)
-
- return if (currentTaskRun?.isRunning == true && currentTaskRun.duration != null) { // TODO what is the semantic of a perpetual IA task?
- max(
- 0L,
- currentTaskRun.duration!! * 1000L - (System.currentTimeMillis() - currentTaskRun.started!!) + InteractiveRunManager.COUNTDOWN_DURATION
- )
+ return if (currentTaskRun?.duration == null) {
+ null
} else {
- -1L
+ if (currentTaskRun.isRunning) {
+ max(
+ 0L,
+ currentTaskRun.duration!! * 1000L - (System.currentTimeMillis() - currentTaskRun.started!!) + InteractiveRunManager.COUNTDOWN_DURATION
+ )
+ } else {
+ -1L
+ }
}
}
diff --git a/backend/src/main/kotlin/dev/dres/run/InteractiveRunManager.kt b/backend/src/main/kotlin/dev/dres/run/InteractiveRunManager.kt
index 44bd9ff5a..28d5872ba 100644
--- a/backend/src/main/kotlin/dev/dres/run/InteractiveRunManager.kt
+++ b/backend/src/main/kotlin/dev/dres/run/InteractiveRunManager.kt
@@ -104,9 +104,9 @@ interface InteractiveRunManager : RunManager {
* this method returns -1L.
*
* @param context The [RunActionContext] used for the invocation.
- * @return Time remaining until the task will end or -1, if no task is running.
+ * @return Time remaining until the task will end or -1, if no task is running. Null if the task runs perpetually.
*/
- fun timeLeft(context: RunActionContext): Long
+ fun timeLeft(context: RunActionContext): Long?
/**
* Returns the time in milliseconds that has elapsed since the start of the currently running task.
@@ -178,4 +178,4 @@ interface InteractiveRunManager : RunManager {
* @return true on success, false otherwise
*/
fun overrideReadyState(context: RunActionContext, viewerId: String): Boolean
-}
\ No newline at end of file
+}
diff --git a/backend/src/main/kotlin/dev/dres/run/InteractiveSynchronousRunManager.kt b/backend/src/main/kotlin/dev/dres/run/InteractiveSynchronousRunManager.kt
index 052bb5de7..87fe9eaca 100644
--- a/backend/src/main/kotlin/dev/dres/run/InteractiveSynchronousRunManager.kt
+++ b/backend/src/main/kotlin/dev/dres/run/InteractiveSynchronousRunManager.kt
@@ -116,7 +116,10 @@ class InteractiveSynchronousRunManager(
/* End ongoing tasks upon initialization (in case server crashed during task execution). */
for (task in this.evaluation.taskRuns) {
if (task.isRunning || task.status == ApiTaskStatus.RUNNING) {
- task.end()
+ /* We exclude perpetual tasks here */
+ if(task.duration != null){
+ task.end()
+ }
}
}
@@ -367,12 +370,15 @@ class InteractiveSynchronousRunManager(
/**
* Returns the time in milliseconds that is left until the end of the current [DbTask].
* Only works if the [RunManager] is in right [RunManagerStatus]. If no task is running,
- * OR a perpetual task is running, this method returns -1L.
+ * this method returns -1L. If the task is to run perpetually, NULL is returned.
*
- * @return Time remaining until the task will end or -1, if no task is running.
+ * @return Time remaining until the task will end or -1, if no task is running. NULL if the task runs perpetually.
*/
- override fun timeLeft(context: RunActionContext): Long {
- return if (this.evaluation.currentTaskRun?.status == ApiTaskStatus.RUNNING && this.evaluation.currentTaskRun?.duration != null) {
+ override fun timeLeft(context: RunActionContext): Long? {
+ return if(this.evaluation.currentTaskRun?.duration == null){
+ null
+ }else{
+ if (this.evaluation.currentTaskRun?.status == ApiTaskStatus.RUNNING) {
val currentTaskRun = this.currentTask(context)
?: throw IllegalStateException("SynchronizedRunManager is in status ${this.status} but has no active TaskRun. This is a serious error!")
max(
@@ -381,7 +387,7 @@ class InteractiveSynchronousRunManager(
)
} else {
-1L
- }
+ }}
}
/**
diff --git a/backend/src/main/kotlin/dev/dres/run/updatables/EndOnSubmitUpdatable.kt b/backend/src/main/kotlin/dev/dres/run/updatables/EndOnSubmitUpdatable.kt
index 7d0fb8864..80b418494 100644
--- a/backend/src/main/kotlin/dev/dres/run/updatables/EndOnSubmitUpdatable.kt
+++ b/backend/src/main/kotlin/dev/dres/run/updatables/EndOnSubmitUpdatable.kt
@@ -46,18 +46,20 @@ class EndOnSubmitUpdatable(private val manager: InteractiveRunManager) : Updatab
val limit = taskType.configuration["LIMIT_CORRECT_PER_TEAM"]?.toIntOrNull() ?: 1
/* Count number of correct submissions per team. */
- if (this.manager.timeLeft(context) > 0) {
- for (s in submissions) {
- for (a in s.answerSets.toList()) {
- if (a.status == DbVerdictStatus.CORRECT) {
- teams[s.team.id] = teams[s.team.id]!! + 1
+ if (currentTask.duration != null) {
+ if (this.manager.timeLeft(context)!! > 0) {
+ for (s in submissions) {
+ for (a in s.answerSets.toList()) {
+ if (a.status == DbVerdictStatus.CORRECT) {
+ teams[s.team.id] = teams[s.team.id]!! + 1
+ }
}
}
- }
- /* If all teams have reached the limit, end the task. */
- if (teams.all { it.value >= limit }) {
- this.manager.abortTask(context)
+ /* If all teams have reached the limit, end the task. */
+ if (teams.all { it.value >= limit }) {
+ this.manager.abortTask(context)
+ }
}
}
}
@@ -74,4 +76,4 @@ class EndOnSubmitUpdatable(private val manager: InteractiveRunManager) : Updatab
*/
override fun shouldBeUpdated(runStatus: RunManagerStatus, taskStatus: ApiTaskStatus?): Boolean =
(runStatus == RunManagerStatus.ACTIVE && taskStatus == ApiTaskStatus.RUNNING)
-}
\ No newline at end of file
+}
diff --git a/backend/src/main/kotlin/dev/dres/run/updatables/ProlongOnSubmitUpdatable.kt b/backend/src/main/kotlin/dev/dres/run/updatables/ProlongOnSubmitUpdatable.kt
index 8ce41edcb..59205e830 100644
--- a/backend/src/main/kotlin/dev/dres/run/updatables/ProlongOnSubmitUpdatable.kt
+++ b/backend/src/main/kotlin/dev/dres/run/updatables/ProlongOnSubmitUpdatable.kt
@@ -34,6 +34,10 @@ class ProlongOnSubmitUpdatable(private val manager: InteractiveRunManager): Upda
override fun update(runStatus: RunManagerStatus, taskStatus: ApiTaskStatus?, context: RunActionContext) {
if (runStatus == RunManagerStatus.ACTIVE && taskStatus == ApiTaskStatus.RUNNING) {
val currentTask = this.manager.currentTask(context) ?: return
+ /* This is only sensible to do, if the task has a duration */
+ if(currentTask.duration == null){
+ return
+ }
val taskType = this.manager.template.taskTypes.firstOrNull { it.name == currentTask.template.taskType }!!
val prolongOnSubmit = taskType.taskOptions.contains(ApiTaskOption.PROLONG_ON_SUBMISSION)
if (prolongOnSubmit) {
@@ -47,7 +51,7 @@ class ProlongOnSubmitUpdatable(private val manager: InteractiveRunManager): Upda
if (lastSubmission == null || (correctOnly && lastSubmission.answerSets.asSequence().all { it.status != DbVerdictStatus.CORRECT })) {
return
}
- val timeLeft = Math.floorDiv(this.manager.timeLeft(context), 1000)
+ val timeLeft = Math.floorDiv(this.manager.timeLeft(context)!!, 1000)
if (timeLeft in 0 until limit) {
this.manager.adjustDuration(context, prolongBy)
}
@@ -64,4 +68,4 @@ class ProlongOnSubmitUpdatable(private val manager: InteractiveRunManager): Upda
*/
override fun shouldBeUpdated(runStatus: RunManagerStatus, taskStatus: ApiTaskStatus?): Boolean =
(runStatus == RunManagerStatus.ACTIVE && taskStatus == ApiTaskStatus.RUNNING)
-}
\ No newline at end of file
+}
diff --git a/doc/oas-client.json b/doc/oas-client.json
index f62c56225..a3541647c 100644
--- a/doc/oas-client.json
+++ b/doc/oas-client.json
@@ -2,8 +2,8 @@
"openapi" : "3.0.3",
"info" : {
"title" : "DRES Client API",
- "version" : "2.0.4",
- "description" : "Client API for DRES (Distributed Retrieval Evaluation Server), Version 2.0.4"
+ "version" : "2.0.5-SNAPSHOT",
+ "description" : "Client API for DRES (Distributed Retrieval Evaluation Server), Version 2.0.5-SNAPSHOT"
},
"paths" : {
"/api/v2/client/evaluation/currentTask/{evaluationId}" : {
@@ -1159,7 +1159,7 @@
"format" : "int64"
}
},
- "required" : [ "name", "taskGroup", "taskType", "duration" ]
+ "required" : [ "name", "taskGroup", "taskType" ]
},
"ApiEvaluation" : {
"type" : "object",
@@ -1282,7 +1282,7 @@
"format" : "int64"
}
},
- "required" : [ "evaluationId", "evaluationStatus", "taskStatus", "timeLeft", "timeElapsed" ]
+ "required" : [ "evaluationId", "evaluationStatus", "taskStatus", "timeElapsed" ]
},
"ApiEvaluationStatus" : {
"type" : "string",
@@ -1383,7 +1383,7 @@
"format" : "int64"
}
},
- "required" : [ "id", "name", "type", "group", "duration", "taskId", "status" ]
+ "required" : [ "id", "name", "type", "group", "taskId", "status" ]
},
"ApiTaskStatus" : {
"type" : "string",
@@ -1413,7 +1413,7 @@
"format" : "int64"
}
},
- "required" : [ "templateId", "name", "taskGroup", "taskType", "duration" ]
+ "required" : [ "templateId", "name", "taskGroup", "taskType" ]
},
"ApiTeamInfo" : {
"type" : "object",
@@ -2154,7 +2154,7 @@
"type" : "string"
}
},
- "required" : [ "name", "taskGroup", "taskType", "duration", "collectionId", "targets", "hints" ]
+ "required" : [ "name", "taskGroup", "taskType", "collectionId", "targets", "hints" ]
},
"ApiTaskType" : {
"type" : "object",
@@ -2198,7 +2198,7 @@
}
}
},
- "required" : [ "name", "duration", "targetOption", "hintOptions", "submissionOptions", "taskOptions", "scoreOption", "configuration" ]
+ "required" : [ "name", "targetOption", "hintOptions", "submissionOptions", "taskOptions", "scoreOption", "configuration" ]
},
"ApiHintOption" : {
"type" : "string",
@@ -2206,7 +2206,7 @@
},
"ApiScoreOption" : {
"type" : "string",
- "enum" : [ "KIS", "AVS", "LEGACY_AVS" ]
+ "enum" : [ "KIS", "AVS", "LEGACY_AVS", "NOOP" ]
},
"ApiSubmissionOption" : {
"type" : "string",
diff --git a/doc/oas.json b/doc/oas.json
index f35b8b4aa..c089ca293 100644
--- a/doc/oas.json
+++ b/doc/oas.json
@@ -2,8 +2,8 @@
"openapi" : "3.0.3",
"info" : {
"title" : "DRES API",
- "version" : "2.0.4",
- "description" : "API for DRES (Distributed Retrieval Evaluation Server), Version 2.0.4",
+ "version" : "2.0.5-SNAPSHOT",
+ "description" : "API for DRES (Distributed Retrieval Evaluation Server), Version 2.0.5-SNAPSHOT",
"contact" : {
"name" : "The DRES Dev Team",
"url" : "https://dres.dev"
@@ -5573,7 +5573,7 @@
"format" : "int64"
}
},
- "required" : [ "name", "taskGroup", "taskType", "duration" ]
+ "required" : [ "name", "taskGroup", "taskType" ]
},
"ApiEvaluation" : {
"type" : "object",
@@ -5696,7 +5696,7 @@
"format" : "int64"
}
},
- "required" : [ "evaluationId", "evaluationStatus", "taskStatus", "timeLeft", "timeElapsed" ]
+ "required" : [ "evaluationId", "evaluationStatus", "taskStatus", "timeElapsed" ]
},
"ApiEvaluationStatus" : {
"type" : "string",
@@ -5797,7 +5797,7 @@
"format" : "int64"
}
},
- "required" : [ "id", "name", "type", "group", "duration", "taskId", "status" ]
+ "required" : [ "id", "name", "type", "group", "taskId", "status" ]
},
"ApiTaskStatus" : {
"type" : "string",
@@ -5827,7 +5827,7 @@
"format" : "int64"
}
},
- "required" : [ "templateId", "name", "taskGroup", "taskType", "duration" ]
+ "required" : [ "templateId", "name", "taskGroup", "taskType" ]
},
"ApiTeamInfo" : {
"type" : "object",
@@ -6568,7 +6568,7 @@
"type" : "string"
}
},
- "required" : [ "name", "taskGroup", "taskType", "duration", "collectionId", "targets", "hints" ]
+ "required" : [ "name", "taskGroup", "taskType", "collectionId", "targets", "hints" ]
},
"ApiTaskType" : {
"type" : "object",
@@ -6612,7 +6612,7 @@
}
}
},
- "required" : [ "name", "duration", "targetOption", "hintOptions", "submissionOptions", "taskOptions", "scoreOption", "configuration" ]
+ "required" : [ "name", "targetOption", "hintOptions", "submissionOptions", "taskOptions", "scoreOption", "configuration" ]
},
"ApiHintOption" : {
"type" : "string",
@@ -6620,7 +6620,7 @@
},
"ApiScoreOption" : {
"type" : "string",
- "enum" : [ "KIS", "AVS", "LEGACY_AVS" ]
+ "enum" : [ "KIS", "AVS", "LEGACY_AVS", "NOOP" ]
},
"ApiSubmissionOption" : {
"type" : "string",
diff --git a/frontend/src/app/competition/competition-builder/competition-builder-task-dialog/advanced-builder-dialog/advanced-builder-dialog.component.ts b/frontend/src/app/competition/competition-builder/competition-builder-task-dialog/advanced-builder-dialog/advanced-builder-dialog.component.ts
index 3beb6f4dc..130aec526 100644
--- a/frontend/src/app/competition/competition-builder/competition-builder-task-dialog/advanced-builder-dialog/advanced-builder-dialog.component.ts
+++ b/frontend/src/app/competition/competition-builder/competition-builder-task-dialog/advanced-builder-dialog/advanced-builder-dialog.component.ts
@@ -1,11 +1,11 @@
import { Component, Inject, OnInit } from '@angular/core';
-import { CompetitionFormBuilder } from '../competition-form.builder';
+import { TaskTemplateFormBuilder } from '../../../../template/template-builder/task-template-form.builder';
import { AppConfig } from '../../../../app.config';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { VideoPlayerSegmentBuilderData } from '../video-player-segment-builder/video-player-segment-builder.component';
export class AdvancedBuilderDialogData {
- builder: CompetitionFormBuilder;
+ builder: TaskTemplateFormBuilder;
}
// TODO rewrite
diff --git a/frontend/src/app/competition/competition-builder/competition-builder-task-dialog/competition-builder-task-dialog.component.ts b/frontend/src/app/competition/competition-builder/competition-builder-task-dialog/competition-builder-task-dialog.component.ts
index 77aabbc22..7770aab94 100644
--- a/frontend/src/app/competition/competition-builder/competition-builder-task-dialog/competition-builder-task-dialog.component.ts
+++ b/frontend/src/app/competition/competition-builder/competition-builder-task-dialog/competition-builder-task-dialog.component.ts
@@ -4,7 +4,7 @@ import { UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import { filter, first } from 'rxjs/operators';
import { AppConfig } from '../../../app.config';
-import { CompetitionFormBuilder } from './competition-form.builder';
+import { TaskTemplateFormBuilder } from '../../../template/template-builder/task-template-form.builder';
import { VideoPlayerSegmentBuilderData } from './video-player-segment-builder/video-player-segment-builder.component';
import {
AdvancedBuilderDialogComponent,
@@ -47,8 +47,8 @@ export class CompetitionBuilderTaskDialogComponent {
units = ['FRAME_NUMBER', 'SECONDS', 'MILLISECONDS', 'TIMECODE'];
/** Data source for list of {@link MediaCollection}. Loaded upon construction of the dialog. */
mediaCollectionSource: Observable;
- /** The {@link CompetitionFormBuilder} used by this dialogue. */
- builder: CompetitionFormBuilder;
+ /** The {@link TaskTemplateFormBuilder} used by this dialogue. */
+ builder: TaskTemplateFormBuilder;
@ViewChild('videoPlayer', { static: false }) video: ElementRef;
viewLayout = 'list';
showVideo = false;
@@ -63,7 +63,7 @@ export class CompetitionBuilderTaskDialogComponent {
public config: AppConfig,
private builderService: TemplateBuilderService // To make the compiler happy
) {
- this.builder = new CompetitionFormBuilder(this.data.taskGroup, this.data.taskType, this.collectionService, this.builderService, this.data.task);
+ this.builder = new TaskTemplateFormBuilder(this.data.taskGroup, this.data.taskType, this.collectionService, this.builderService, this.data.task);
this.form = this.builder.form;
this.mediaCollectionSource = this.collectionService.getApiV2CollectionList();
}
@@ -76,7 +76,7 @@ export class CompetitionBuilderTaskDialogComponent {
uploaded = (taskData: string) => {
const task = JSON.parse(taskData) as ApiTaskTemplate;
- this.builder = new CompetitionFormBuilder(this.data.taskGroup, this.data.taskType, this.collectionService, this.builderService, task);
+ this.builder = new TaskTemplateFormBuilder(this.data.taskGroup, this.data.taskType, this.collectionService, this.builderService, task);
this.form = this.builder.form;
console.log('Loaded task: ' + JSON.stringify(task));
};
diff --git a/frontend/src/app/competition/competition-builder/competition-builder.component.ts b/frontend/src/app/competition/competition-builder/competition-builder.component.ts
index a6a864ce4..6f3ec52de 100644
--- a/frontend/src/app/competition/competition-builder/competition-builder.component.ts
+++ b/frontend/src/app/competition/competition-builder/competition-builder.component.ts
@@ -10,7 +10,7 @@ import {
CompetitionBuilderTaskGroupDialogData,
} from './competition-builder-task-group-dialog/competition-builder-task-group.component';
import { MatTable } from '@angular/material/table';
-import { CompetitionBuilderTaskTypeDialogComponent } from './competition-builder-task-type-dialog/competition-builder-task-type-dialog.component';
+import { CreateTaskTypeDialogComponent } from '../../template/template-builder/components/create-task-type-dialog/create-task-type-dialog.component';
import {
CompetitionBuilderTaskDialogComponent,
CompetitionBuilderTaskDialogData,
@@ -257,7 +257,7 @@ export class CompetitionBuilderComponent implements OnInit, OnDestroy, Deactivat
}
public addTaskType(type?: ApiTaskType) {
- const dialogRef = this.dialog.open(CompetitionBuilderTaskTypeDialogComponent, { data: type ? type : null, width: '750px' });
+ const dialogRef = this.dialog.open(CreateTaskTypeDialogComponent, { data: type ? type : null, width: '750px' });
dialogRef
.afterClosed()
.pipe(filter((g) => g != null))
diff --git a/frontend/src/app/competition/competition-builder/competition-builder.module.ts b/frontend/src/app/competition/competition-builder/competition-builder.module.ts
index 416c6b318..4f69a8b6f 100644
--- a/frontend/src/app/competition/competition-builder/competition-builder.module.ts
+++ b/frontend/src/app/competition/competition-builder/competition-builder.module.ts
@@ -17,7 +17,7 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatSelectModule } from '@angular/material/select';
import { CompetitionBuilderTaskGroupDialogComponent } from './competition-builder-task-group-dialog/competition-builder-task-group.component';
import { MatChipsModule } from '@angular/material/chips';
-import { CompetitionBuilderTaskTypeDialogComponent } from './competition-builder-task-type-dialog/competition-builder-task-type-dialog.component';
+import { CreateTaskTypeDialogComponent } from '../../template/template-builder/components/create-task-type-dialog/create-task-type-dialog.component';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatCardModule } from '@angular/material/card';
import { VideoPlayerSegmentBuilderComponent } from './competition-builder-task-dialog/video-player-segment-builder/video-player-segment-builder.component';
@@ -66,7 +66,6 @@ import { ColorPickerModule } from 'ngx-color-picker';
CompetitionBuilderComponent,
CompetitionBuilderTaskDialogComponent,
CompetitionBuilderTaskGroupDialogComponent,
- CompetitionBuilderTaskTypeDialogComponent,
VideoPlayerSegmentBuilderComponent,
AdvancedBuilderDialogComponent,
VideoPlayerSegmentBuilderDialogComponent,
diff --git a/frontend/src/app/evaluation/task-controls/task-controls.component.html b/frontend/src/app/evaluation/task-controls/task-controls.component.html
index 1c407ddc0..e1a980499 100644
--- a/frontend/src/app/evaluation/task-controls/task-controls.component.html
+++ b/frontend/src/app/evaluation/task-controls/task-controls.component.html
@@ -42,7 +42,7 @@
*ngIf="(isAdmin | async) && (runState | async)?.taskStatus === 'RUNNING'"
class="mat-body" style="align-self: center;"
>
- Time left: {{ (runState | async)?.timeLeft | formatTime:false }}
+ Time left: {{ ((runState | async)?.timeLeft ? ((runState | async)?.timeLeft | formatTime:false) : '∞')}}
diff --git a/frontend/src/app/competition/competition-builder/competition-builder-task-type-dialog/competition-builder-task-type-dialog.component.scss b/frontend/src/app/template/template-builder/components/create-task-type-dialog/create-task-type-dialog.component.scss
similarity index 100%
rename from frontend/src/app/competition/competition-builder/competition-builder-task-type-dialog/competition-builder-task-type-dialog.component.scss
rename to frontend/src/app/template/template-builder/components/create-task-type-dialog/create-task-type-dialog.component.scss
diff --git a/frontend/src/app/competition/competition-builder/competition-builder-task-type-dialog/competition-builder-task-type-dialog.component.ts b/frontend/src/app/template/template-builder/components/create-task-type-dialog/create-task-type-dialog.component.ts
similarity index 82%
rename from frontend/src/app/competition/competition-builder/competition-builder-task-type-dialog/competition-builder-task-type-dialog.component.ts
rename to frontend/src/app/template/template-builder/components/create-task-type-dialog/create-task-type-dialog.component.ts
index 18543a9b2..ad0ebdd25 100644
--- a/frontend/src/app/competition/competition-builder/competition-builder-task-type-dialog/competition-builder-task-type-dialog.component.ts
+++ b/frontend/src/app/template/template-builder/components/create-task-type-dialog/create-task-type-dialog.component.ts
@@ -2,7 +2,7 @@ import { AfterViewInit, Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { UntypedFormArray, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms';
import { MatCheckboxChange } from '@angular/material/checkbox';
-import {ApiHintOption, ApiScoreOption, ApiSubmissionOption, ApiTargetOption, ApiTaskOption, ApiTaskType} from '../../../../../openapi';
+import {ApiHintOption, ApiScoreOption, ApiSubmissionOption, ApiTargetOption, ApiTaskOption, ApiTaskType} from '../../../../../../openapi';
/**
* Wrapper to be able to have an enum value boolean tuple
@@ -13,11 +13,11 @@ interface ActivatedType {
}
@Component({
- selector: 'app-competition-builder-task-type',
- templateUrl: './competition-builder-task-type-dialog.component.html',
- styleUrls: ['./competition-builder-task-type-dialog.component.scss'],
+ selector: 'app-create-task-type',
+ templateUrl: './create-task-type-dialog.component.html',
+ styleUrls: ['./create-task-type-dialog.component.scss'],
})
-export class CompetitionBuilderTaskTypeDialogComponent implements OnInit, AfterViewInit {
+export class CreateTaskTypeDialogComponent implements OnInit, AfterViewInit {
/** FromGroup for this dialog. */
form: UntypedFormGroup;
@@ -44,7 +44,7 @@ export class CompetitionBuilderTaskTypeDialogComponent implements OnInit, AfterV
});
constructor(
- public dialogRef: MatDialogRef,
+ public dialogRef: MatDialogRef,
@Inject(MAT_DIALOG_DATA) public data: ApiTaskType
) {
this.init();
@@ -165,38 +165,6 @@ export class CompetitionBuilderTaskTypeDialogComponent implements OnInit, AfterV
console.log(param);
parameters.push(param);
}
- /*
- --- Legacy. Keep to check validity
- if (this.data?.targetType?.parameters) {
- Object.keys(this.data?.targetType?.parameters).forEach((key) => {
- parameters.push([this.data.score.option, key, this.data.score.parameters[key]]);
- });
- }
-
- if (this.data?.score?.parameters) {
- Object.keys(this.data?.score?.parameters).forEach((key) => {
- parameters.push([this.data.score.option, key, this.data.score.parameters[key]]);
- });
- }
-
- this.data?.components?.forEach((domain) => {
- Object.keys(domain.parameters).forEach((key) => {
- parameters.push([domain.option, key, domain.parameters[key]]);
- });
- });
-
- this.data?.filter?.forEach((domain) => {
- Object.keys(domain.parameters).forEach((key) => {
- parameters.push([domain.option, key, domain.parameters[key]]);
- });
- });
-
- this.data?.options?.forEach((domain) => {
- Object.keys(domain.parameters).forEach((key) => {
- parameters.push([domain.option, key, domain.parameters[key]]);
- });
- });
- */
/* Prepare empty FormControl. */
this.form = new UntypedFormGroup({
@@ -204,7 +172,7 @@ export class CompetitionBuilderTaskTypeDialogComponent implements OnInit, AfterV
name: new UntypedFormControl(this.data?.name, [Validators.required, Validators.minLength(3)]),
/* Default Duration. Required */
- defaultTaskDuration: new UntypedFormControl(this.data?.duration, [Validators.required, Validators.min(1), Validators.max(999999)]),
+ defaultTaskDuration: new UntypedFormControl(this.data?.duration, [Validators.min(1), Validators.max(999999)]),
/* Target Type. Required */
target: new UntypedFormControl(this.data?.targetOption, [Validators.required]),
@@ -239,7 +207,7 @@ export class CompetitionBuilderTaskTypeDialogComponent implements OnInit, AfterV
private fetchFromForm(): ApiTaskType {
return {
name: this.form.get('name').value,
- duration: this.form.get('defaultTaskDuration').value,
+ duration: this.form.get('defaultTaskDuration').value ?? null,
targetOption: this.form.get('target').value as ApiTargetOption,
hintOptions: (this.form.get('components') as UntypedFormArray).controls.map((c) => {
return c.value as ApiHintOption;
diff --git a/frontend/src/app/template/template-builder/components/query-description-external-form-field/query-description-external-form-field.component.ts b/frontend/src/app/template/template-builder/components/query-description-external-form-field/query-description-external-form-field.component.ts
index fb8c91c05..8f9195ab8 100644
--- a/frontend/src/app/template/template-builder/components/query-description-external-form-field/query-description-external-form-field.component.ts
+++ b/frontend/src/app/template/template-builder/components/query-description-external-form-field/query-description-external-form-field.component.ts
@@ -1,8 +1,8 @@
import { Component, Input } from "@angular/core";
import { UntypedFormControl } from "@angular/forms";
import {
- CompetitionFormBuilder
-} from "../../../../competition/competition-builder/competition-builder-task-dialog/competition-form.builder";
+ TaskTemplateFormBuilder
+} from "../../task-template-form.builder";
import { AppConfig } from "../../../../app.config";
@Component({
@@ -14,7 +14,7 @@ export class QueryDescriptionExternalFormFieldComponent {
@Input()
pathControl: UntypedFormControl
@Input()
- formBuilder: CompetitionFormBuilder
+ formBuilder: TaskTemplateFormBuilder
@Input()
index: number
diff --git a/frontend/src/app/template/template-builder/components/query-description-external-image-form-field/query-description-external-image-form-field.component.ts b/frontend/src/app/template/template-builder/components/query-description-external-image-form-field/query-description-external-image-form-field.component.ts
index bce5d1d69..d6c249e51 100644
--- a/frontend/src/app/template/template-builder/components/query-description-external-image-form-field/query-description-external-image-form-field.component.ts
+++ b/frontend/src/app/template/template-builder/components/query-description-external-image-form-field/query-description-external-image-form-field.component.ts
@@ -3,8 +3,8 @@ import {
QueryDescriptionExternalFormFieldComponent
} from "../query-description-external-form-field/query-description-external-form-field.component";
import {
- CompetitionFormBuilder
-} from "../../../../competition/competition-builder/competition-builder-task-dialog/competition-form.builder";
+ TaskTemplateFormBuilder
+} from "../../task-template-form.builder";
import { AppConfig } from "../../../../app.config";
@Component({
diff --git a/frontend/src/app/template/template-builder/components/query-description-form-field/query-description-form-field.component.ts b/frontend/src/app/template/template-builder/components/query-description-form-field/query-description-form-field.component.ts
index dc8083cff..69811bcab 100644
--- a/frontend/src/app/template/template-builder/components/query-description-form-field/query-description-form-field.component.ts
+++ b/frontend/src/app/template/template-builder/components/query-description-form-field/query-description-form-field.component.ts
@@ -1,8 +1,8 @@
import { Component, Input } from "@angular/core";
import { AbstractControl, UntypedFormControl, UntypedFormGroup } from "@angular/forms";
import {
- CompetitionFormBuilder
-} from "../../../../competition/competition-builder/competition-builder-task-dialog/competition-form.builder";
+ TaskTemplateFormBuilder
+} from "../../task-template-form.builder";
import { ApiHintOption, ApiHintType } from "../../../../../../openapi";
@Component({
@@ -33,7 +33,7 @@ export class QueryDescriptionFormFieldComponent {
@Input()
unitControl: UntypedFormControl
@Input()
- formBuilder: CompetitionFormBuilder
+ formBuilder: TaskTemplateFormBuilder
@Input()
index: number
diff --git a/frontend/src/app/template/template-builder/components/query-description-media-item-form-field/query-description-media-item-form-field.component.ts b/frontend/src/app/template/template-builder/components/query-description-media-item-form-field/query-description-media-item-form-field.component.ts
index 877ab9e84..5b78ab1d0 100644
--- a/frontend/src/app/template/template-builder/components/query-description-media-item-form-field/query-description-media-item-form-field.component.ts
+++ b/frontend/src/app/template/template-builder/components/query-description-media-item-form-field/query-description-media-item-form-field.component.ts
@@ -1,8 +1,8 @@
import { Component, Injector, Input } from "@angular/core";
import { UntypedFormControl } from "@angular/forms";
import {
- CompetitionFormBuilder
-} from "../../../../competition/competition-builder/competition-builder-task-dialog/competition-form.builder";
+ TaskTemplateFormBuilder
+} from "../../task-template-form.builder";
import { FormatMediaItemPipe, MediaItemDisplayOptions } from "../../../../services/pipes/format-media-item.pipe";
import { ApiMediaItem } from "../../../../../../openapi";
@@ -15,7 +15,7 @@ export class QueryDescriptionMediaItemFormFieldComponent {
@Input()
itemControl: UntypedFormControl;
@Input()
- formBuilder: CompetitionFormBuilder;
+ formBuilder: TaskTemplateFormBuilder;
@Input()
index: number;
diff --git a/frontend/src/app/template/template-builder/components/task-template-editor/task-template-editor.component.html b/frontend/src/app/template/template-builder/components/task-template-editor/task-template-editor.component.html
index 63f4fd29c..eda44f571 100644
--- a/frontend/src/app/template/template-builder/components/task-template-editor/task-template-editor.component.html
+++ b/frontend/src/app/template/template-builder/components/task-template-editor/task-template-editor.component.html
@@ -60,7 +60,8 @@
Duration [s]
-
+
diff --git a/frontend/src/app/template/template-builder/components/task-template-editor/task-template-editor.component.ts b/frontend/src/app/template/template-builder/components/task-template-editor/task-template-editor.component.ts
index fc651cdb8..c0929755c 100644
--- a/frontend/src/app/template/template-builder/components/task-template-editor/task-template-editor.component.ts
+++ b/frontend/src/app/template/template-builder/components/task-template-editor/task-template-editor.component.ts
@@ -12,8 +12,8 @@ import {
} from "../../../../../../openapi";
import { UntypedFormControl, UntypedFormGroup } from "@angular/forms";
import {
- CompetitionFormBuilder
-} from "../../../../competition/competition-builder/competition-builder-task-dialog/competition-form.builder";
+ TaskTemplateFormBuilder
+} from "../../task-template-form.builder";
import {
VideoPlayerSegmentBuilderData
} from "../../../../competition/competition-builder/competition-builder-task-dialog/video-player-segment-builder/video-player-segment-builder.component";
@@ -42,7 +42,7 @@ export class TaskTemplateEditorComponent implements OnInit, OnDestroy {
mediaCollectionSource: Observable;
- formBuilder: CompetitionFormBuilder;
+ formBuilder: TaskTemplateFormBuilder;
@ViewChild('videoPlayer', {static: false}) video: ElementRef;
@@ -99,7 +99,7 @@ export class TaskTemplateEditorComponent implements OnInit, OnDestroy {
}
public init(){
- this.formBuilder = new CompetitionFormBuilder(this.taskGroup, this.taskType, this.collectionService, this.builderService, this.task);
+ this.formBuilder = new TaskTemplateFormBuilder(this.taskGroup, this.taskType, this.collectionService, this.builderService, this.task);
this.form = this.formBuilder.form;
this.form.valueChanges.subscribe(newValue => {
this.formBuilder.storeFormData();
@@ -140,7 +140,7 @@ export class TaskTemplateEditorComponent implements OnInit, OnDestroy {
uploaded = (taskData: string) => {
const task = JSON.parse(taskData) as ApiTaskTemplate;
- this.formBuilder = new CompetitionFormBuilder(this.taskGroup, this.taskType, this.collectionService, this.builderService, task);
+ this.formBuilder = new TaskTemplateFormBuilder(this.taskGroup, this.taskType, this.collectionService, this.builderService, task);
this.form = this.formBuilder.form;
};
diff --git a/frontend/src/app/template/template-builder/components/task-types-list/task-types-list.component.ts b/frontend/src/app/template/template-builder/components/task-types-list/task-types-list.component.ts
index 42c85139b..0abbf8bb3 100644
--- a/frontend/src/app/template/template-builder/components/task-types-list/task-types-list.component.ts
+++ b/frontend/src/app/template/template-builder/components/task-types-list/task-types-list.component.ts
@@ -6,8 +6,8 @@ import { ApiTaskType, TemplateService } from "../../../../../../openapi";
import { Observable } from "rxjs";
import { filter, map, shareReplay, tap } from "rxjs/operators";
import {
- CompetitionBuilderTaskTypeDialogComponent
-} from "../../../../competition/competition-builder/competition-builder-task-type-dialog/competition-builder-task-type-dialog.component";
+ CreateTaskTypeDialogComponent
+} from "../create-task-type-dialog/create-task-type-dialog.component";
import {
ActionableDynamicTable,
ActionableDynamicTableActionType,
@@ -79,7 +79,7 @@ export class TaskTypesListComponent extends AbstractTemplateBuilderComponent imp
public addTaskType(type?: ApiTaskType) {
- const dialogRef = this.dialog.open(CompetitionBuilderTaskTypeDialogComponent, { data: type ?? null, width: "750px", closeOnNavigation: false });
+ const dialogRef = this.dialog.open(CreateTaskTypeDialogComponent, { data: type ?? null, width: "750px", closeOnNavigation: false });
dialogRef.afterClosed()
.pipe(filter((t) => t != null))
.subscribe((t) => {
diff --git a/frontend/src/app/template/template-builder/components/template-builder-components.module.ts b/frontend/src/app/template/template-builder/components/template-builder-components.module.ts
index 48b141567..10d09cf80 100644
--- a/frontend/src/app/template/template-builder/components/template-builder-components.module.ts
+++ b/frontend/src/app/template/template-builder/components/template-builder-components.module.ts
@@ -53,6 +53,8 @@ import { UserListFilterPipe } from './team-builder-dialog/user-list-filter.pipe'
import { UserListInOtherFilterPipe } from './team-builder-dialog/user-list-in-other-filter.pipe';
import { BatchAddTargetDialogComponent } from './batch-add-target-dialog/batch-add-target-dialog.component';
import { MatSortModule } from "@angular/material/sort";
+import { CreateTaskTypeDialogComponent } from "./create-task-type-dialog/create-task-type-dialog.component";
+import { MatCheckbox } from "@angular/material/checkbox";
@NgModule({
@@ -76,7 +78,8 @@ import { MatSortModule } from "@angular/material/sort";
ViewersListComponent,
UserListFilterPipe,
UserListInOtherFilterPipe,
- BatchAddTargetDialogComponent
+ BatchAddTargetDialogComponent,
+ CreateTaskTypeDialogComponent,
],
imports: [
CommonModule,
@@ -102,7 +105,8 @@ import { MatSortModule } from "@angular/material/sort";
CdkDropList,
CdkDrag,
MatCardModule,
- MatSortModule
+ MatSortModule,
+ MatCheckbox
],
exports: [TemplateInformationComponent,
JudgesListComponent,
diff --git a/frontend/src/app/competition/competition-builder/competition-builder-task-dialog/require-match.ts b/frontend/src/app/template/template-builder/require-match.ts
similarity index 100%
rename from frontend/src/app/competition/competition-builder/competition-builder-task-dialog/require-match.ts
rename to frontend/src/app/template/template-builder/require-match.ts
diff --git a/frontend/src/app/competition/competition-builder/competition-builder-task-dialog/competition-form.builder.ts b/frontend/src/app/template/template-builder/task-template-form.builder.ts
similarity index 98%
rename from frontend/src/app/competition/competition-builder/competition-builder-task-dialog/competition-form.builder.ts
rename to frontend/src/app/template/template-builder/task-template-form.builder.ts
index d28ebc2b2..870e1d3e1 100644
--- a/frontend/src/app/competition/competition-builder/competition-builder-task-dialog/competition-form.builder.ts
+++ b/frontend/src/app/template/template-builder/task-template-form.builder.ts
@@ -2,7 +2,7 @@ import { AbstractControl, UntypedFormArray, UntypedFormControl, UntypedFormGroup
import { filter, first, map, switchMap } from "rxjs/operators";
import { Observable } from 'rxjs';
import { RequireMatch } from './require-match';
-import { TimeUtilities } from '../../../utilities/time.utilities';
+import { TimeUtilities } from '../../utilities/time.utilities';
import {
ApiHint,
ApiHintOption, ApiHintType,
@@ -12,22 +12,21 @@ import {
ApiTaskTemplate,
ApiTaskType, ApiTemporalPoint, ApiTemporalRange,
CollectionService
-} from '../../../../../openapi';
-import { TemplateBuilderService } from "../../../template/template-builder/template-builder.service";
+} from '../../../../openapi';
+import { TemplateBuilderService } from "./template-builder.service";
-// TODO rename to TemplateFormBuilder
-export class CompetitionFormBuilder {
+export class TaskTemplateFormBuilder {
/** The default duration of a query hint. This is currently a hard-coded constant. */
private static DEFAULT_HINT_DURATION = 30;
- /** The {@link UntypedFormGroup} held by this {@link CompetitionFormBuilder}. */
+ /** The {@link UntypedFormGroup} held by this {@link TaskTemplateFormBuilder}. */
public form: UntypedFormGroup;
/**
* Constructor for CompetitionFormBuilder.
*
- * @param taskGroup The {@link ApiTaskGroup} to create this {@link CompetitionFormBuilder} for.
- * @param taskType The {@link ApiTaskType} to create this {@link CompetitionFormBuilder} for.
+ * @param taskGroup The {@link ApiTaskGroup} to create this {@link TaskTemplateFormBuilder} for.
+ * @param taskType The {@link ApiTaskType} to create this {@link TaskTemplateFormBuilder} for.
* @param collectionService The {@link CollectionService} reference used to fetch data through the DRES API.
* @param builderService The {@link TemplateBuilderService} reference
* @param data The {@link ApiTaskTemplate} to initialize the form with.
@@ -113,7 +112,7 @@ export class CompetitionFormBuilder {
} else if (previousItem.get('end').value) {
component.get('start').setValue(previousItem.get('end').value, {emitEvent: false});
} else {
- previousItem.get('end').setValue(previousItem.get('start').value + CompetitionFormBuilder.DEFAULT_HINT_DURATION, {emitEvent: false});
+ previousItem.get('end').setValue(previousItem.get('start').value + TaskTemplateFormBuilder.DEFAULT_HINT_DURATION, {emitEvent: false});
component.get('start').setValue(previousItem.get('end').value, {emitEvent: false});
}
@@ -312,7 +311,7 @@ export class CompetitionFormBuilder {
name: new UntypedFormControl(this.data?.name, [Validators.required]),
comment: new UntypedFormControl(this.data?.comment || ''),
taskGroup: new UntypedFormControl(taskGroup.name),
- duration: new UntypedFormControl(this.durationInitValue, [Validators.required, Validators.min(1)]),
+ duration: new UntypedFormControl(this.durationInitValue, [Validators.min(1), Validators.max(9999999)]),
mediaCollection: new UntypedFormControl(this.data?.collectionId ?? this.builderService.defaultCollection, [Validators.required]),
});
this.form.addControl('target', this.formForTarget());
diff --git a/frontend/src/app/viewer/task-viewer.component.ts b/frontend/src/app/viewer/task-viewer.component.ts
index 82c4706bf..cd58b8a84 100644
--- a/frontend/src/app/viewer/task-viewer.component.ts
+++ b/frontend/src/app/viewer/task-viewer.component.ts
@@ -248,6 +248,7 @@ export class TaskViewerComponent implements AfterViewInit, OnDestroy {
map(([info, state]) => info.taskTemplates.find(t => t.templateId == state.taskTemplateId)?.name)
)
+
/* Observable for the time that is still left. */
this.timeLeft = this.state.pipe(
map((s) => s.timeLeft) /* Compensating for added countdown. */,
@@ -277,14 +278,18 @@ export class TaskViewerComponent implements AfterViewInit, OnDestroy {
*
* @param sec The number of seconds to convert.
*/
- public toFormattedTime(sec: number): string {
- const hours = Math.floor(sec / 3600);
- const minutes = Math.floor(sec / 60) % 60;
- const seconds = sec % 60;
-
- return [hours, minutes, seconds]
- .map((v) => (v < 10 ? '0' + v : v))
- .filter((v, i) => v !== '00' || i > 0)
- .join(':');
+ public toFormattedTime(sec?: number): string {
+ if (sec) {
+ const hours = Math.floor(sec / 3600);
+ const minutes = Math.floor(sec / 60) % 60;
+ const seconds = sec % 60;
+
+ return [hours, minutes, seconds]
+ .map((v) => (v < 10 ? "0" + v : v))
+ .filter((v, i) => v !== "00" || i > 0)
+ .join(":");
+ } else {
+ return "∞";
+ }
}
}