Compare commits
2 Commits
c395a92b7f
...
ff77e2b301
| Author | SHA1 | Date | |
|---|---|---|---|
| ff77e2b301 | |||
| da456f7b61 |
@@ -43,16 +43,18 @@ class SubmissionStorage(
|
|||||||
return StoredSubmission(dir, names)
|
return StoredSubmission(dir, names)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Собирает zip из сохранённых файлов сдачи. */
|
/** Собирает zip из сохранённых файлов сдачи, сохраняя реальное время изменения файлов. */
|
||||||
fun zipOf(storedDir: Path): ByteArray {
|
fun zipOf(storedDir: Path): ByteArray {
|
||||||
val out = ByteArrayOutputStream()
|
val out = ByteArrayOutputStream()
|
||||||
ZipOutputStream(out).use { zos ->
|
ZipOutputStream(out).use { zos ->
|
||||||
Files.list(storedDir).forEach { file ->
|
Files.list(storedDir).use { stream ->
|
||||||
ZipEntry(file.fileName.toString()).also { entry ->
|
stream.forEach { file ->
|
||||||
entry.time = 0
|
ZipEntry(file.fileName.toString()).also { entry ->
|
||||||
zos.putNextEntry(entry)
|
entry.time = Files.getLastModifiedTime(file).toMillis()
|
||||||
Files.copy(file, zos)
|
zos.putNextEntry(entry)
|
||||||
zos.closeEntry()
|
Files.copy(file, zos)
|
||||||
|
zos.closeEntry()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import java.io.ByteArrayInputStream
|
|||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.time.Instant
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
@@ -164,14 +165,14 @@ abstract class AdminSupport(
|
|||||||
): ByteArray {
|
): ByteArray {
|
||||||
val out = ByteArrayOutputStream()
|
val out = ByteArrayOutputStream()
|
||||||
ZipOutputStream(out).use { zos ->
|
ZipOutputStream(out).use { zos ->
|
||||||
zos.putNextEntry(ZipEntry("ОПИСАНИЕ.txt").apply { time = 0 })
|
zos.putNextEntry(ZipEntry("ОПИСАНИЕ.txt"))
|
||||||
zos.write("Архив сдач группы $groupName (последние попытки)\n".toByteArray(Charsets.UTF_8))
|
zos.write("Архив сдач группы $groupName (последние попытки)\n".toByteArray(Charsets.UTF_8))
|
||||||
zos.closeEntry()
|
zos.closeEntry()
|
||||||
for (lab in labs) {
|
for (lab in labs) {
|
||||||
for (sub in api.labSubmissions(token, lab.id)) {
|
for (sub in api.labSubmissions(token, lab.id)) {
|
||||||
val student = studentById[sub.studentId] ?: continue
|
val student = studentById[sub.studentId] ?: continue
|
||||||
val bytes = api.downloadSubmissionAsAdmin(token, sub.id).bytes
|
val bytes = api.downloadSubmissionAsAdmin(token, sub.id).bytes
|
||||||
appendSubmissionArchive(zos, lab, student, bytes)
|
appendSubmissionArchive(zos, lab, student, bytes, sub.createdAt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,13 +184,21 @@ abstract class AdminSupport(
|
|||||||
lab: LabSummaryDto,
|
lab: LabSummaryDto,
|
||||||
student: StudentRowDto,
|
student: StudentRowDto,
|
||||||
submissionZip: ByteArray,
|
submissionZip: ByteArray,
|
||||||
|
submissionCreatedAt: String? = null,
|
||||||
) {
|
) {
|
||||||
|
val fallbackMillis =
|
||||||
|
submissionCreatedAt
|
||||||
|
?.let { runCatching { Instant.parse(it).toEpochMilli() }.getOrNull() }
|
||||||
|
?: System.currentTimeMillis()
|
||||||
ZipInputStream(ByteArrayInputStream(submissionZip)).use { zin ->
|
ZipInputStream(ByteArrayInputStream(submissionZip)).use { zin ->
|
||||||
var entry = zin.nextEntry
|
var entry = zin.nextEntry
|
||||||
while (entry != null) {
|
while (entry != null) {
|
||||||
if (!entry.isDirectory) {
|
if (!entry.isDirectory) {
|
||||||
val safeName = student.fullName.replace(Regex("[\\\\/:*?\"<>|]"), "_")
|
val safeName = student.fullName.replace(Regex("[\\\\/:*?\"<>|]"), "_")
|
||||||
zos.putNextEntry(ZipEntry("ЛР${lab.number}/${student.studentNo}_$safeName/${entry.name}").apply { time = 0 })
|
val entryMillis = entry.time.takeIf { it > 0 } ?: fallbackMillis
|
||||||
|
zos.putNextEntry(
|
||||||
|
ZipEntry("ЛР${lab.number}/${student.studentNo}_$safeName/${entry.name}").apply { time = entryMillis },
|
||||||
|
)
|
||||||
zin.copyTo(zos)
|
zin.copyTo(zos)
|
||||||
zos.closeEntry()
|
zos.closeEntry()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -260,8 +260,8 @@ html.modal-open body { padding-right: var(--modal-scrollbar, 0); }
|
|||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
}
|
}
|
||||||
.modal-body > #defense-actions { margin-top: auto; }
|
.defense-footer { margin-top: auto; justify-content: flex-end; align-items: center; }
|
||||||
.modal-body > #defense-actions .flex { justify-content: flex-end; }
|
.modal-body > #defense-status:empty { display: none; }
|
||||||
|
|
||||||
/* Двухуровневые формы уровней лабы */
|
/* Двухуровневые формы уровней лабы */
|
||||||
.level-row { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.5rem; }
|
.level-row { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.5rem; }
|
||||||
|
|||||||
@@ -209,16 +209,15 @@
|
|||||||
</select></div>
|
</select></div>
|
||||||
<div><label class="label" for="defense-md">Задание (markdown)</label>
|
<div><label class="label" for="defense-md">Задание (markdown)</label>
|
||||||
<textarea class="input" id="defense-md" name="assignmentMd" rows="10" required></textarea></div>
|
<textarea class="input" id="defense-md" name="assignmentMd" rows="10" required></textarea></div>
|
||||||
<button class="btn btn-primary" type="submit">Сохранить задание</button>
|
|
||||||
</form>
|
</form>
|
||||||
<div id="defense-actions" class="mt-4" hidden>
|
<p id="defense-status" class="text-sm text-muted mt-4"></p>
|
||||||
<p id="defense-status" class="text-sm text-muted mb-2"></p>
|
<div class="flex flex-wrap gap-2 defense-footer">
|
||||||
<div class="flex flex-wrap gap-2">
|
<div id="defense-actions" class="flex flex-wrap gap-2" hidden>
|
||||||
<form class="inline-form" id="defense-issue-form" method="post">
|
<form class="inline-form" id="defense-issue-form" method="post">
|
||||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||||
<input type="hidden" name="labId" id="defense-action-lab">
|
<input type="hidden" name="labId" id="defense-action-lab">
|
||||||
<input type="hidden" name="groupId" th:value="${group.id}">
|
<input type="hidden" name="groupId" th:value="${group.id}">
|
||||||
<button class="btn btn-primary" type="submit">Выдать код</button>
|
<button class="btn btn-secondary" type="submit">Выдать код</button>
|
||||||
</form>
|
</form>
|
||||||
<form class="inline-form" id="defense-reset-form" method="post"
|
<form class="inline-form" id="defense-reset-form" method="post"
|
||||||
onsubmit="return confirm('Открыть защиту заново для нового (дополнительного) задания?')">
|
onsubmit="return confirm('Открыть защиту заново для нового (дополнительного) задания?')">
|
||||||
@@ -228,6 +227,7 @@
|
|||||||
<button class="btn btn-danger" type="submit">Начать заново</button>
|
<button class="btn btn-danger" type="submit">Начать заново</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn btn-primary" type="submit" form="defense-form">Сохранить</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|||||||
Reference in New Issue
Block a user