From 9fc08e54f290548f6061551c0a9d7cecc379aa95 Mon Sep 17 00:00:00 2001 From: Roman Shishkin Date: Wed, 19 Mar 2025 00:04:03 +0300 Subject: [PATCH 1/3] feat: Added ability to create open-dated contests --- .../developer/DeveloperContestController.kt | 12 ++++-- .../testsys/webclient/entity/impl/Contest.kt | 6 +++ .../view/impl/ContestCreationView.kt | 10 +++-- .../webclient/view/impl/ContestView.kt | 11 +++-- src/main/resources/static/js/scripts.js | 19 +++++++++ .../templates/developer/contest.html | 39 +++++++++++------ .../templates/developer/contests.html | 42 +++++++++++++------ 7 files changed, 104 insertions(+), 35 deletions(-) diff --git a/src/main/kotlin/trik/testsys/webclient/controller/impl/user/developer/DeveloperContestController.kt b/src/main/kotlin/trik/testsys/webclient/controller/impl/user/developer/DeveloperContestController.kt index 9efe24d8..cfc6dc8b 100644 --- a/src/main/kotlin/trik/testsys/webclient/controller/impl/user/developer/DeveloperContestController.kt +++ b/src/main/kotlin/trik/testsys/webclient/controller/impl/user/developer/DeveloperContestController.kt @@ -49,7 +49,7 @@ class DeveloperContestController( ): String { val webUser = loginData.validate(redirectAttributes) ?: return "redirect:$LOGIN_PATH" - if (!contestView.duration.isLocalTimeFormatted()) { + if (contestView.isOpenEnded == false && !contestView.duration.isLocalTimeFormatted()) { redirectAttributes.addPopupMessage("Время на выполнение Тура должно быть в формате HH:mm.") return "redirect:$CONTESTS_PATH" } @@ -63,7 +63,9 @@ class DeveloperContestController( return "redirect:$CONTESTS_PATH" } - if (contest.startDate.isEqual(contest.endDate) || contest.duration == LocalTime.of(0, 0)) { + if (contestView.isOpenEnded == false && + (contest.startDate.isEqual(contest.endDate) || contest.duration == LocalTime.of(0, 0)) + ) { redirectAttributes.addPopupMessage("Время на выполнение Тура должно быть положительным.") return "redirect:$CONTESTS_PATH" } @@ -132,7 +134,7 @@ class DeveloperContestController( } - if (!contestView.duration.isLocalTimeFormatted()) { + if (contestView.isOpenEnded == false && !contestView.duration.isLocalTimeFormatted()) { redirectAttributes.addPopupMessage("Время на выполнение Тура должно быть в формате HH:mm.") return "redirect:$CONTEST_PATH/$contestId" } @@ -146,7 +148,9 @@ class DeveloperContestController( return "redirect:$CONTEST_PATH/$contestId" } - if (contest.startDate.isEqual(contest.endDate) || contest.duration == LocalTime.of(0, 0)) { + if (contestView.isOpenEnded == false && + (contest.startDate.isEqual(contest.endDate) || contest.duration == LocalTime.of(0, 0)) + ) { redirectAttributes.addPopupMessage("Время на выполнение Тура должно быть положительным.") return "redirect:$CONTEST_PATH/$contestId" } diff --git a/src/main/kotlin/trik/testsys/webclient/entity/impl/Contest.kt b/src/main/kotlin/trik/testsys/webclient/entity/impl/Contest.kt index a0e0efed..110429b3 100644 --- a/src/main/kotlin/trik/testsys/webclient/entity/impl/Contest.kt +++ b/src/main/kotlin/trik/testsys/webclient/entity/impl/Contest.kt @@ -93,6 +93,12 @@ class Contest( @ManyToMany(mappedBy = "contests") val tasks: MutableSet = mutableSetOf() + /** + * @author Roman Shishkin + * @since %CURRENT_VERSION% + */ + var isOpenEnded: Boolean = false + enum class Visibility(override val dbkey: String) : Enum { PUBLIC("PLC"), diff --git a/src/main/kotlin/trik/testsys/webclient/view/impl/ContestCreationView.kt b/src/main/kotlin/trik/testsys/webclient/view/impl/ContestCreationView.kt index a2b6fdae..3b8cd2b0 100644 --- a/src/main/kotlin/trik/testsys/webclient/view/impl/ContestCreationView.kt +++ b/src/main/kotlin/trik/testsys/webclient/view/impl/ContestCreationView.kt @@ -6,6 +6,7 @@ import trik.testsys.webclient.entity.user.impl.Developer import trik.testsys.webclient.util.convertToLocalTime import trik.testsys.webclient.util.fromTimeZone import java.time.LocalDateTime +import java.time.LocalTime import java.time.format.DateTimeFormatter /** @@ -20,17 +21,19 @@ data class ContestCreationView( val startDate: LocalDateTime, @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm") val endDate: LocalDateTime, - val duration: String + val duration: String, + val isOpenEnded: Boolean?, ) { fun toEntity(developer: Developer, timeZoneId: String?) = Contest( name, startDate.fromTimeZone(timeZoneId), endDate.fromTimeZone(timeZoneId), - duration.convertToLocalTime() + if (isOpenEnded == true) LocalTime.MIN else duration.convertToLocalTime() ).also { it.additionalInfo = additionalInfo it.note = note it.developer = developer + it.isOpenEnded = isOpenEnded ?: false } val formattedStartDate: String @@ -43,7 +46,8 @@ data class ContestCreationView( fun empty() = ContestCreationView( "", "", "", - LocalDateTime.now(), LocalDateTime.now(), "01:00" + LocalDateTime.now(), LocalDateTime.now(), "01:00", + false ) } } \ No newline at end of file diff --git a/src/main/kotlin/trik/testsys/webclient/view/impl/ContestView.kt b/src/main/kotlin/trik/testsys/webclient/view/impl/ContestView.kt index 5e441167..5e3bd9d6 100644 --- a/src/main/kotlin/trik/testsys/webclient/view/impl/ContestView.kt +++ b/src/main/kotlin/trik/testsys/webclient/view/impl/ContestView.kt @@ -8,6 +8,7 @@ import trik.testsys.webclient.util.format import trik.testsys.webclient.util.fromTimeZone import trik.testsys.webclient.view.NotedEntityView import java.time.LocalDateTime +import java.time.LocalTime /** * @author Roman Shishkin @@ -24,16 +25,19 @@ data class ContestView( val startDate: LocalDateTime, @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm") val endDate: LocalDateTime, - val duration: String + val duration: String, + var isOpenEnded: Boolean?, ) : NotedEntityView { override fun toEntity(timeZoneId: String?) = Contest( - name, startDate.fromTimeZone(timeZoneId), endDate.fromTimeZone(timeZoneId), duration.convertToLocalTime() + name, startDate.fromTimeZone(timeZoneId), endDate.fromTimeZone(timeZoneId), + if (isOpenEnded == true) LocalTime.MIN else duration.convertToLocalTime() ).also { it.id = id it.additionalInfo = additionalInfo it.note = note it.visibility = visibility + it.isOpenEnded = isOpenEnded ?: false } val formattedStartDate: String @@ -56,7 +60,8 @@ data class ContestView( visibility = this.visibility, startDate = this.startDate.atTimeZone(timeZone), endDate = this.endDate.atTimeZone(timeZone), - duration = this.duration.toString() + duration = this.duration.toString(), + isOpenEnded = this.isOpenEnded, ) } } \ No newline at end of file diff --git a/src/main/resources/static/js/scripts.js b/src/main/resources/static/js/scripts.js index 23c43943..c4fd320d 100644 --- a/src/main/resources/static/js/scripts.js +++ b/src/main/resources/static/js/scripts.js @@ -18,4 +18,23 @@ function copy(id) { textArea.select(); document.execCommand("Copy"); textArea.remove(); +} + +function toggleVisibility(dependencyId, labelId, fieldId) { + let dependency = document.getElementById(dependencyId); + let label = document.getElementById(labelId); + let field = document.getElementById(fieldId); + + + if (dependency.checked) { + label.setAttribute("hidden", ""); // Скрываем поле + field.setAttribute("hidden", ""); // Скрываем поле + + field.removeAttribute("required"); // Убираем обязательность + } else { + label.removeAttribute("hidden"); // Показываем поле + field.removeAttribute("hidden"); // Показываем поле + + field.setAttribute("required", ""); // Делаем обязательным + } } \ No newline at end of file diff --git a/src/main/resources/templates/developer/contest.html b/src/main/resources/templates/developer/contest.html index a1fb1969..1ea990ba 100644 --- a/src/main/resources/templates/developer/contest.html +++ b/src/main/resources/templates/developer/contest.html @@ -71,31 +71,46 @@

Информация о Туре

size='50', isReadonly='true', isRequired='true', isAccessToken='false')"> -
-
+
+
-
+
-
-
+
+
-
+
-
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
-
-
+
+
-
+
-
-
+
+
-
- +
-
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
Информация о Туре
diff --git a/src/main/resources/templates/developer/contests.html b/src/main/resources/templates/developer/contests.html index 3d71ec16..591b5d8d 100644 --- a/src/main/resources/templates/developer/contests.html +++ b/src/main/resources/templates/developer/contests.html @@ -83,6 +83,7 @@

Создание Тура

From 95ebcaaa7b00361138bdda9df253032c744c95a0 Mon Sep 17 00:00:00 2001 From: Roman Shishkin Date: Fri, 21 Mar 2025 00:49:02 +0300 Subject: [PATCH 3/3] feat: Updated computing remaining time for contests --- .../trik/testsys/webclient/entity/impl/Contest.kt | 2 +- .../testsys/webclient/entity/user/impl/Student.kt | 2 ++ .../testsys/webclient/view/impl/ContestView.kt | 3 +-- .../webclient/view/impl/StudentContestView.kt | 2 +- src/main/resources/templates/admin/group.html | 4 ++-- src/main/resources/templates/student/contest.html | 15 ++++++++++++--- .../resources/templates/student/contests.html | 4 ++-- 7 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/trik/testsys/webclient/entity/impl/Contest.kt b/src/main/kotlin/trik/testsys/webclient/entity/impl/Contest.kt index 110429b3..7b34fe80 100644 --- a/src/main/kotlin/trik/testsys/webclient/entity/impl/Contest.kt +++ b/src/main/kotlin/trik/testsys/webclient/entity/impl/Contest.kt @@ -78,7 +78,7 @@ class Contest( val now = LocalDateTime.now(DEFAULT_ZONE_ID) val lastSeconds = endDate.toEpochSecond() - now.toEpochSecond() + 1 - return min(duration.toSecondOfDay().toLong(), lastSeconds) + return if (isOpenEnded) lastSeconds else min(duration.toSecondOfDay().toLong(), lastSeconds) } @ElementCollection diff --git a/src/main/kotlin/trik/testsys/webclient/entity/user/impl/Student.kt b/src/main/kotlin/trik/testsys/webclient/entity/user/impl/Student.kt index e62e54c2..a3f0bdbf 100644 --- a/src/main/kotlin/trik/testsys/webclient/entity/user/impl/Student.kt +++ b/src/main/kotlin/trik/testsys/webclient/entity/user/impl/Student.kt @@ -52,6 +52,8 @@ class Student( val nowInSeconds = now.toEpochSecond() val contestLastSeconds = contest.lastSeconds + if (contest.isOpenEnded && contestLastSeconds > 0) return LocalTime.MAX + val startTime = startTimesByContestId[contest.id!!] ?: return LocalTime.ofSecondOfDay(contestLastSeconds) val startTimeInSeconds = startTime.toEpochSecond() diff --git a/src/main/kotlin/trik/testsys/webclient/view/impl/ContestView.kt b/src/main/kotlin/trik/testsys/webclient/view/impl/ContestView.kt index 5e3bd9d6..04ed2dfe 100644 --- a/src/main/kotlin/trik/testsys/webclient/view/impl/ContestView.kt +++ b/src/main/kotlin/trik/testsys/webclient/view/impl/ContestView.kt @@ -46,8 +46,7 @@ data class ContestView( val formattedEndDate: String get() = endDate.format() - val formattedDuration: String - get() = duration.format() + val formattedDuration = if (isOpenEnded == true) "Неограниченно" else duration companion object { diff --git a/src/main/kotlin/trik/testsys/webclient/view/impl/StudentContestView.kt b/src/main/kotlin/trik/testsys/webclient/view/impl/StudentContestView.kt index 94f142ee..28de5aa3 100644 --- a/src/main/kotlin/trik/testsys/webclient/view/impl/StudentContestView.kt +++ b/src/main/kotlin/trik/testsys/webclient/view/impl/StudentContestView.kt @@ -50,7 +50,7 @@ data class StudentContestView( additionalInfo = this.additionalInfo, creationDate = this.creationDate?.atTimeZone(timeZone), name = this.name, - lastTime = lastTime.format(DateTimeFormatter.ofPattern("HH:mm:ss")), + lastTime = if (isOpenEnded) "Неограниченно" else lastTime.format(DateTimeFormatter.ofPattern("HH:mm:ss")), startDate = this.startDate.atTimeZone(timeZone), endDate = this.endDate.atTimeZone(timeZone), isGoingOn = this.isGoingOn(), diff --git a/src/main/resources/templates/admin/group.html b/src/main/resources/templates/admin/group.html index 244b11c4..0fa5d7bb 100644 --- a/src/main/resources/templates/admin/group.html +++ b/src/main/resources/templates/admin/group.html @@ -109,7 +109,7 @@

Список прикрепленных Туров

Дата и время окончания - + Время на выполнение @@ -159,7 +159,7 @@

Список доступных к прикреплению Туров

Дата и время окончания - + Время на выполнение diff --git a/src/main/resources/templates/student/contest.html b/src/main/resources/templates/student/contest.html index d7cd1d76..5ef948d4 100644 --- a/src/main/resources/templates/student/contest.html +++ b/src/main/resources/templates/student/contest.html @@ -47,11 +47,20 @@

Информация о Туре

size='50', isReadonly='true', isRequired='true', isAccessToken='false')">
+
+
+ +
+
+ +
+
Дата и время окончания - + Время на выполнение @@ -101,7 +101,7 @@

Список оконченных Туров

Дата и время окончания - + Время на выполнение