доработка редактора кода

This commit is contained in:
Anton Dzyk 2026-01-07 16:05:00 +03:00
parent 6f42fe0ee6
commit 6e7bf47814
9 changed files with 259 additions and 87 deletions

View File

@ -2,16 +2,21 @@ package ru.oa2.lti.application.controller;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import ru.oa2.lti.domain.model.auth.Payload;
import ru.oa2.lti.application.service.task.TaskService;
import ru.oa2.lti.domain.model.auth.LtiLogin;
import ru.oa2.lti.domain.model.ResultRequest;
import ru.oa2.lti.domain.model.auth.LtiLogin;
import ru.oa2.lti.domain.model.auth.Payload;
import ru.oa2.lti.domain.model.results.ResultResponse;
import ru.oa2.lti.domain.model.task.RequestUpdateTask;
import ru.oa2.lti.domain.model.task.TaskData;
import java.util.Objects;
@ -47,21 +52,36 @@ public class TaskController {
return "redirect:/error";
}
model.addAttribute("description", data);
}
model.addAttribute("description", data.getDescription());
if (Objects.requireNonNull(payload).getCoach()) {
model.addAttribute("initScript", data.getInitScript());
model.addAttribute("checkScript", data.getVerificationScript());
return "task-editor";
}
}
return "task";
}
@PostMapping
public ResponseEntity updateTask() {
public ResponseEntity updateTask(@RequestBody TaskData data,
HttpServletRequest request) {
//TODO exception
taskService.saveTask("TODO", "TODO", "TODO");
return ResponseEntity.accepted().build();
var session = request.getSession();
var payload = (Payload) session.getAttribute("payload");
if (payload != null && payload.getCoach()) {
return ResponseEntity.accepted().body(
taskService.saveTask(RequestUpdateTask.builder()
.contextId(payload.getContextId())
.data(data)
.build())
);
}
return ResponseEntity.status(HttpStatusCode.valueOf(403))
.body(new ResultResponse("forbidden"));
}
@PostMapping("/submit")

View File

@ -2,12 +2,14 @@ package ru.oa2.lti.application.infrastructure.repository.entities;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
import ru.oa2.lti.domain.model.task.TaskData;
import ru.oa2.lti.domain.model.task.TaskType;
@Getter
@Setter
@Entity
@Table(name = "task")
public class Task {

View File

@ -2,6 +2,8 @@ package ru.oa2.lti.application.service.task;
import ru.oa2.lti.domain.model.ResultRequest;
import ru.oa2.lti.domain.model.results.ResultResponse;
import ru.oa2.lti.domain.model.task.RequestUpdateTask;
import ru.oa2.lti.domain.model.task.TaskData;
import java.util.UUID;
@ -10,7 +12,7 @@ public interface TaskService {
/*
Получение задачи (контекста лабораторной работы)
*/
String getTask(UUID contextId);
TaskData getTask(UUID contextId);
/*
Запуск проверки через скрипт
@ -20,5 +22,5 @@ public interface TaskService {
/*
Сохранение/обновление task-а
*/
void saveTask(String initScript, String checkScript, String description);
ResultResponse saveTask(RequestUpdateTask requestDataTask);
}

View File

@ -1,20 +1,25 @@
package ru.oa2.lti.application.service.task;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import ru.oa2.lti.application.infrastructure.lms.LMSService;
import ru.oa2.lti.application.infrastructure.runner.Runner;
import ru.oa2.lti.application.service.results.ResultService;
import ru.oa2.lti.domain.model.ResultRequest;
import ru.oa2.lti.domain.model.task.TaskData;
import ru.oa2.lti.application.infrastructure.repository.LMSContentRepository;
import ru.oa2.lti.application.infrastructure.repository.entities.LMSContent;
import ru.oa2.lti.application.infrastructure.repository.entities.Task;
import ru.oa2.lti.application.infrastructure.runner.Runner;
import ru.oa2.lti.application.service.results.ResultService;
import ru.oa2.lti.domain.model.ResultRequest;
import ru.oa2.lti.domain.model.results.ResultResponse;
import ru.oa2.lti.domain.model.task.RequestUpdateTask;
import ru.oa2.lti.domain.model.task.TaskData;
import java.util.Collection;
import java.util.UUID;
@Slf4j
@Service
@Transactional(rollbackOn = Throwable.class)
public class TaskServiceImpl implements TaskService {
private final LMSContentRepository lmsContentRepository;
@ -36,7 +41,7 @@ public class TaskServiceImpl implements TaskService {
}
@Override
public String getTask(UUID contextId) {
public TaskData getTask(UUID contextId) {
var content = lmsContentRepository.getLMSContentByContentId(contextId);
if(content.isPresent()) {
@ -48,12 +53,12 @@ public class TaskServiceImpl implements TaskService {
//TODO string -> exception?
if (!runner.run(contextId, data.getInitScript())) {
return "Init script FAILED";
return new TaskData("Init script FAILED", "", "");
}
return data.getDescription();
return data;
} else {
return "Not Page";
return new TaskData("Not Page", "", "");
}
}
@ -70,7 +75,21 @@ public class TaskServiceImpl implements TaskService {
}
@Override
public void saveTask(String initScript, String checkScript, String description) {
//TODO
public ResultResponse saveTask(RequestUpdateTask requestUpdateTask) {
log.info("save");
var result = lmsContentRepository.getLMSContentByContentId(requestUpdateTask.getContextId());
//TODO доработать версионирование task-ов
if (result.isPresent()) {
var content = result.get();
var tasks = content.getTasks();
var currentTask = tasks.stream().findFirst();
currentTask.ifPresent(task -> task.setData(requestUpdateTask.getData()));
lmsContentRepository.save(content);
}
//TODO обработка exception - failed / success
return new ResultResponse("success");
}
}

View File

@ -0,0 +1,13 @@
package ru.oa2.lti.domain.model.task;
import lombok.Builder;
import lombok.Data;
import java.util.UUID;
@Data
@Builder
public class RequestUpdateTask {
UUID contextId;
TaskData data;
}

View File

@ -1,10 +1,14 @@
package ru.oa2.lti.domain.model.task;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TaskData {
String initScript;
String description;
String initScript;
String verificationScript;
}

View File

@ -0,0 +1,50 @@
document.getElementById('saveBtn').addEventListener('click', async () => {
const description = document.getElementById('description').value;
const initScript = document.getElementById('initScript').value;
const verificationScript = document.getElementById('checkScript').value;
const payload = {
description,
initScript,
verificationScript
};
try {
const response = await fetch('/tool/lti/task', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const result = await response.json();
const statusContainer = document.getElementById('statusContainer');
const statusIcon = document.getElementById('icon');
const statusLabel = document.getElementById('statusLabel');
if (response.ok) {
statusIcon.className = 'fa-solid fa-check-circle';
statusIcon.classList.add('status-success');
statusLabel.textContent = 'Задача успешно сохранена';
statusContainer.style.display = 'flex';
statusContainer.classList.remove('status-failed');
statusContainer.classList.add('status-success');
} else {
throw new Error(result.message || 'Ошибка сохранения');
}
} catch (error) {
const statusContainer = document.getElementById('statusContainer');
const statusIcon = document.getElementById('icon');
const statusLabel = document.getElementById('statusLabel');
statusIcon.className = 'fa-solid fa-times-circle';
statusIcon.classList.add('status-failed');
statusLabel.textContent = 'Ошибка: ' + error.message;
statusContainer.style.display = 'flex';
statusContainer.classList.remove('status-success');
statusContainer.classList.add('status-failed');
}
});

View File

@ -0,0 +1,105 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Доступ запрещён — 403</title>
<!-- Bootstrap 5 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
color: #495057;
}
.forbidden-card {
background: white;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
padding: 40px;
max-width: 500px;
text-align: center;
border: 1px solid #e0e7ff;
}
.forbidden-icon {
font-size: 70px;
color: #dc3545;
margin-bottom: 20px;
animation: pulse 2s infinite;
}
.error-code {
font-size: 100px;
font-weight: 700;
color: #6c757d;
margin: 10px 0;
}
.error-message {
font-size: 20px;
font-weight: 600;
color: #212529;
margin-bottom: 15px;
}
.error-description {
font-size: 16px;
color: #6c757d;
margin-bottom: 30px;
line-height: 1.6;
}
.back-btn {
background-color: #0d6efd;
color: white;
padding: 10px 24px;
border-radius: 8px;
text-decoration: none;
font-weight: 500;
transition: background-color 0.3s;
}
.back-btn:hover {
background-color: #0b5ed7;
color: white;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
</style>
</head>
<body>
<div class="forbidden-card">
<!-- Иконка "запрещено" -->
<div class="forbidden-icon">
<i class="fas fa-ban"></i>
</div>
<!-- Код ошибки -->
<div class="error-code">403</div>
<!-- Основное сообщение -->
<div class="error-message">Доступ запрещён</div>
<!-- Описание -->
<p class="error-description" th:text="${message}">
У вас нет прав для просмотра этой страницы.
Пожалуйста, обратитесь к администратору, если считаете, что это ошибка.
</p>
<!-- Кнопка "Назад" -->
<a th:href="@{${redirectUrl} ?: '/'}" class="back-btn">
<i class="fas fa-arrow-left"></i> Вернуться назад
</a>
</div>
<!-- Bootstrap JS (опционально) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Редактор задачи LTI</title>
<title>Редактор задачи LTI #TODO подтягивать name?</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
@ -115,19 +115,28 @@
<!-- Поле: Описание задачи (редактируемое) -->
<div class="form-group">
<label class="form-label">Описание задачи (HTML)</label>
<textarea id="description" class="code-area" placeholder="Введите HTML-описание задачи..."><th:block th:utext="${description ?: ''}"/></textarea>
<textarea
id="description"
class="code-area"
placeholder="Введите HTML-описание задачи..."><th:block th:utext="${description ?: ''}"/></textarea>
</div>
<!-- Поле: initScript -->
<div class="form-group">
<label class="form-label">initScript (JavaScript)</label>
<textarea id="initScript" class="code-area" placeholder="JavaScript-код для инициализации среды..."><th:block th:text="${initScript ?: ''}"/></textarea>
<label class="form-label">initScript</label>
<textarea
id="initScript"
class="code-area"
placeholder="Скрипт инициализации задания"><th:block th:text="${initScript ?: ''}"/></textarea>
</div>
<!-- Поле: checkScript -->
<div class="form-group">
<label class="form-label">checkScript (JavaScript)</label>
<textarea id="checkScript" class="code-area" placeholder="JavaScript-код для проверки результата..."><th:block th:text="${checkScript ?: ''}"/></textarea>
<label class="form-label">checkScript</label>
<textarea
id="checkScript"
class="code-area"
placeholder="Скрипт проверки задания"><th:block th:text="${checkScript ?: ''}"/></textarea>
</div>
</div>
@ -148,69 +157,17 @@
<i class="fas fa-save"></i> Сохранить задачу
</button>
</div>
<div>
<button id="submitBtn" class="btn btn-primary submit-btn">
<i class="fas fa-paper-plane"></i> Отправить на проверку
</button>
</div>
</div>
</div>
<!-- Скрытые поля -->
<input type="hidden" id="contextId" th:value="${contextId}" />
<input type="hidden" id="participantId" th:value="${participantId}" />
<!-- Скрипт отправки -->
<script th:src="@{/tool/lti/js/main.js}"></script>
<script>
document.getElementById('saveBtn').addEventListener('click', async () => {
const contextId = document.getElementById('contextId').value;
const description = document.getElementById('description').value;
const initScript = document.getElementById('initScript').value;
const checkScript = document.getElementById('checkScript').value;
const payload = {
contextId,
description,
initScript,
checkScript
};
try {
const response = await fetch('/tool/lti/task', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const result = await response.json();
const statusContainer = document.getElementById('statusContainer');
const statusIcon = document.getElementById('icon');
const statusLabel = document.getElementById('statusLabel');
if (response.ok) {
statusIcon.className = 'fa-solid fa-check-circle';
statusIcon.classList.add('status-success');
statusLabel.textContent = 'Задача успешно сохранена';
statusContainer.style.display = 'flex';
statusContainer.classList.remove('status-failed');
statusContainer.classList.add('status-success');
} else {
throw new Error(result.message || 'Ошибка сохранения');
}
} catch (error) {
const statusContainer = document.getElementById('statusContainer');
const statusIcon = document.getElementById('icon');
const statusLabel = document.getElementById('statusLabel');
statusIcon.className = 'fa-solid fa-times-circle';
statusIcon.classList.add('status-failed');
statusLabel.textContent = 'Ошибка: ' + error.message;
statusContainer.style.display = 'flex';
statusContainer.classList.remove('status-success');
statusContainer.classList.add('status-failed');
}
});
</script>
<script th:src="@{/tool/lti/js/edit.js}"></script>
</body>
</html>