Добавление режима для учителя

This commit is contained in:
Anton Dzyk 2025-12-24 12:06:53 +03:00
parent 05c04c49ed
commit 23ea2ed09f
5 changed files with 522 additions and 13 deletions

287
debug/tasks_scripts/1.sh Executable file
View File

@ -0,0 +1,287 @@
#!/bin/bash
# Скрипт для создания группы, репозитория и копирования кода в GitLab
# с назначением прав пользователю из Keycloak
set -e # Прерывать выполнение при ошибках
# === КОНФИГУРАЦИЯ ===
# GitLab Private Token с правами администратора
PRIVATE_TOKEN=$1
# Пользователь из Keycloak (должен существовать в GitLab)
KEYCLOAK_USERNAME=$2
USER_ID=${KEYCLOAK_USERNAME}
GITLAB_URL="https://gitlab.oa2.ru" # URL вашего GitLab
# Данные для новой группы и репозитория
GROUP_NAME="group-${USER_ID}"
GROUP_PATH="group-${USER_ID}" # Может отличаться от имени
GROUP_DESCRIPTION="Группа для пользователя ${USER_ID}"
# Данные для нового репозитория
PROJECT_NAME="task1"
PROJECT_DESCRIPTION="Репозиторий с кодом task1"
# Внешний репозиторий для копирования
SOURCE_REPO_URL="https://git.oa2.ru/dzyk/task1" # Замените на реальный URL
# Уровень доступа для пользователя (owner = 50)
ACCESS_LEVEL=50
# Временная директория для клонирования
TEMP_DIR="/tmp/gitlab-migration-$(date +%s)"
# === ФУНКЦИИ ===
# Функция для логирования
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
# Функция для выполнения HTTP запросов к GitLab API
gitlab_api() {
local method=$1
local endpoint=$2
local data=$3
local curl_cmd="curl -s -X $method \
'$GITLAB_URL/api/v4$endpoint' \
-H 'Private-Token: $PRIVATE_TOKEN' \
-H 'Content-Type: application/json'"
if [ ! -z "$data" ]; then
curl_cmd="$curl_cmd -d '$data'"
fi
eval $curl_cmd
}
# Функция для проверки ошибок API
check_api_error() {
local response=$1
local operation=$2
if echo "$response" | jq -e '.error' > /dev/null 2>&1; then
error_msg=$(echo "$response" | jq -r '.error')
log "Ошибка при $operation: $error_msg"
return 1
fi
if echo "$response" | jq -e '.message' > /dev/null 2>&1; then
error_msg=$(echo "$response" | jq -r '.message')
if [ "$error_msg" != "null" ] && [ ! -z "$error_msg" ]; then
log "Ошибка при $operation: $error_msg"
return 1
fi
fi
return 0
}
# === ОСНОВНОЙ СКРИПТ ===
log "Начало выполнения скрипта"
# 1. Проверка зависимостей
log "Проверка зависимостей..."
for cmd in curl jq git; do
if ! command -v $cmd &> /dev/null; then
log "Ошибка: $cmd не установлен"
exit 1
fi
done
log "Все зависимости установлены"
# 2. Поиск пользователя из Keycloak
log "Поиск пользователя '$KEYCLOAK_USERNAME' в GitLab..."
user_search=$(gitlab_api "GET" "/users?username=$KEYCLOAK_USERNAME")
if ! check_api_error "$user_search" "поиске пользователя"; then
exit 1
fi
user_id=$(echo "$user_search" | jq -r '.[0].id')
user_username=$(echo "$user_search" | jq -r '.[0].username')
if [ "$user_id" = "null" ] || [ -z "$user_id" ]; then
log "Ошибка: Пользователь '$KEYCLOAK_USERNAME' не найден в GitLab"
log "Убедитесь, что пользователь вошел через Keycloak хотя бы один раз"
exit 1
fi
log "Найден пользователь: $user_username (ID: $user_id)"
# 4. Создание группы
log "Проверка существования группы '$GROUP_NAME'..."
group_check=$(gitlab_api "GET" "/groups/$GROUP_PATH")
if echo "$group_check" | jq -e '.id' > /dev/null 2>&1; then
group_id=$(echo "$group_check" | jq -r '.id')
log "Группа '$GROUP_NAME' уже существует (ID: $group_id)"
else
log "Создание группы '$GROUP_NAME'..."
group_data=$(jq -n \
--arg name "$GROUP_NAME" \
--arg path "$GROUP_PATH" \
--arg desc "$GROUP_DESCRIPTION" \
--arg visibility "private" \
'{
name: $name,
path: $path,
description: $desc,
visibility: $visibility,
lfs_enabled: true,
request_access_enabled: true
}')
create_group=$(gitlab_api "POST" "/groups" "$group_data")
if ! check_api_error "$create_group" "создании группы"; then
exit 1
fi
group_id=$(echo "$create_group" | jq -r '.id')
log "Группа создана успешно (ID: $group_id)"
fi
# 5. Создание проекта в группе
log "Проверка существования проекта '$PROJECT_NAME' в группе..."
project_check=$(gitlab_api "GET" "/projects/$GROUP_PATH%2F$PROJECT_NAME")
if echo "$project_check" | jq -e '.id' > /dev/null 2>&1; then
project_id=$(echo "$project_check" | jq -r '.id')
log "Проект '$PROJECT_NAME' уже существует (ID: $project_id)"
else
log "Создание проекта '$PROJECT_NAME' в группе..."
project_data=$(jq -n \
--arg name "$PROJECT_NAME" \
--arg path "$PROJECT_NAME" \
--arg desc "$PROJECT_DESCRIPTION" \
--arg namespace_id "$group_id" \
--arg visibility "private" \
'{
name: $name,
path: $path,
description: $desc,
namespace_id: $namespace_id,
visibility: $visibility,
initialize_with_readme: false,
lfs_enabled: true,
container_registry_enabled: true
}')
create_project=$(gitlab_api "POST" "/projects" "$project_data")
if ! check_api_error "$create_project" "создании проекта"; then
exit 1
fi
project_id=$(echo "$create_project" | jq -r '.id')
project_ssh_url=$(echo "$create_project" | jq -r '.ssh_url_to_repo')
project_http_url=$(echo "$create_project" | jq -r '.http_url_to_repo')
log "Проект создан успешно (ID: $project_id)"
log "URL проекта: $project_http_url"
fi
# 6. Назначение прав пользователю как owner группы
log "Проверка прав пользователя $user_username в группе..."
group_members=$(gitlab_api "GET" "/groups/$group_id/members")
user_in_group=$(echo "$group_members" | jq --arg user_id "$user_id" '.[] | select(.id == ($user_id|tonumber))')
if [ -z "$user_in_group" ]; then
log "Назначение прав owner пользователю $user_username в группе..."
member_data=$(jq -n \
--arg user_id "$user_id" \
--arg access_level "$ACCESS_LEVEL" \
'{
user_id: $user_id,
access_level: ($access_level|tonumber)
}')
add_member=$(gitlab_api "POST" "/groups/$group_id/members" "$member_data")
if ! check_api_error "$add_member" "добавлении пользователя в группу"; then
exit 1
fi
log "Пользователю $user_username назначены права owner в группе"
else
current_access=$(echo "$user_in_group" | jq -r '.access_level')
if [ "$current_access" != "$ACCESS_LEVEL" ]; then
log "Обновление прав пользователя $user_username с уровня $current_access на $ACCESS_LEVEL..."
member_data=$(jq -n \
--arg access_level "$ACCESS_LEVEL" \
'{
access_level: ($access_level|tonumber)
}')
update_member=$(gitlab_api "PUT" "/groups/$group_id/members/$user_id" "$member_data")
if ! check_api_error "$update_member" "обновлении прав пользователя"; then
exit 1
fi
log "Права пользователя обновлены"
else
log "Пользователь $user_username уже имеет права owner в группе"
fi
fi
# 7. Клонирование и копирование кода
log "Создание временной директории: $TEMP_DIR"
mkdir -p "$TEMP_DIR"
cd "$TEMP_DIR"
log "Клонирование исходного репозитория: $SOURCE_REPO_URL"
if ! git clone --bare "$SOURCE_REPO_URL" source-repo.git; then
log "Ошибка при клонировании исходного репозитория"
exit 1
fi
log "Клонирование целевого репозитория GitLab"
# Получаем URL репозитория с токеном для записи
repo_url_with_token=$(echo "$project_http_url" | sed "s|https://|https://gitlab-ci-token:$PRIVATE_TOKEN@|")
if ! git clone "$repo_url_with_token" target-repo; then
log "Ошибка при клонировании целевого репозитория"
exit 1
fi
cd target-repo
log "Добавление remote исходного репозитория"
git remote add source ../source-repo.git
log "Получение данных из исходного репозитория"
git fetch source
log "Объединение всех веток из исходного репозитория"
for branch in $(git branch -r | grep 'source/' | sed 's|source/||'); do
if [ "$branch" != "HEAD" ] && [ "$branch" != "->" ]; then
log " Копирование ветки: $branch"
git checkout -b "$branch" "source/$branch" 2>/dev/null || git branch "$branch" "source/$branch"
fi
done
# Возвращаемся в основную ветку (если есть)
if git show-ref --verify --quiet refs/heads/main; then
git checkout main
elif git show-ref --verify --quiet refs/heads/master; then
git checkout master
fi
log "Отправка всех веток в GitLab"
git push --all origin
git push --tags origin
log "Очистка временных файлов"
cd /
rm -rf "$TEMP_DIR"
# 8. Проверка результата
log "Проверка созданной структуры..."
final_check=$(gitlab_api "GET" "/projects/$project_id")
project_name=$(echo "$final_check" | jq -r '.name_with_namespace')
web_url=$(echo "$final_check" | jq -r '.web_url')
log "=== ОТЧЕТ ==="
log "Группа: $GROUP_NAME (ID: $group_id)"
log "Проект: $project_name (ID: $project_id)"
log "URL проекта: $web_url"
log "Пользователь с правами owner: $user_username"
log "Код скопирован из: $SOURCE_REPO_URL"
log ""
log "Скрипт успешно выполнен!"

View File

@ -10,6 +10,7 @@ import ru.oa2.lti.repository.entities.LMSContent;
import ru.oa2.lti.repository.entities.Task; import ru.oa2.lti.repository.entities.Task;
import ru.oa2.lti.service.jwt.JwtService; import ru.oa2.lti.service.jwt.JwtService;
import ru.oa2.lti.service.jwt.Payload; import ru.oa2.lti.service.jwt.Payload;
import ru.oa2.lti.service.results.dto.ResultResponse;
import java.util.Collection; import java.util.Collection;
import java.util.UUID; import java.util.UUID;
@ -55,18 +56,13 @@ public class ApplicationService {
} }
} }
public String saveResult(String body) { public ResultResponse updateTask(String body) {
return new ResultResponse("success");
}
return "{\n" + public ResultResponse saveResult(String body) {
" \"success\": true,\n" +
" \"message\": \"Результат успешно получен и обработан\",\n" + //TODO заполнять из результатов проверок check скрипта
" \"data\": {\n" + return new ResultResponse("success");
" \"contextId\": \"ctx-12345\",\n" +
" \"participantId\": \"usr-67890\",\n" +
" \"submittedText\": \"console.log('Hello');\",\n" +
" \"timestamp\": \"2025-12-14T17:55:30Z\",\n" +
" \"status\": \"processed\"\n" +
" }\n" +
"}";
} }
} }

View File

@ -3,6 +3,7 @@ package ru.oa2.lti.controller;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -11,6 +12,8 @@ import ru.oa2.lti.infrastructure.LMSService;
import ru.oa2.lti.model.LtiLogin; import ru.oa2.lti.model.LtiLogin;
import ru.oa2.lti.service.jwt.Payload; import ru.oa2.lti.service.jwt.Payload;
import java.util.Objects;
@Slf4j @Slf4j
@Controller @Controller
@RequestMapping("/tool/lti") @RequestMapping("/tool/lti")
@ -63,6 +66,11 @@ public class LaunchController {
return "redirect:/tool/lti/select"; return "redirect:/tool/lti/select";
} }
@PostMapping("/task")
public ResponseEntity updateTask() {
return service.updateTask("TODO");
}
@GetMapping("/task") @GetMapping("/task")
public String showDockerTas(Model model, public String showDockerTas(Model model,
@ -81,6 +89,9 @@ public class LaunchController {
model.addAttribute("description", data); model.addAttribute("description", data);
} }
if (Objects.requireNonNull(payload).getCoach()) {
return "task-editor";
}
return "task"; return "task";
} }

View File

@ -55,7 +55,6 @@ public class ResultController {
jwtService.getTokenPayload(idToken) jwtService.getTokenPayload(idToken)
); );
//TODO возвращать json
return ResponseEntity return ResponseEntity
.accepted() .accepted()
.body(service.saveResult(body)); .body(service.saveResult(body));

View File

@ -0,0 +1,216 @@
<!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>Редактор задачи LTI</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">
<style>
body, html {
height: 100%;
margin: 0;
font-family: Arial, sans-serif;
background-color: #f5f7fa;
}
.container-fluid {
height: 100vh;
display: flex;
flex-direction: column;
padding: 0;
}
.header-section {
padding: 20px;
background-color: #ffffff;
border-bottom: 1px solid #e0e4e8;
}
.content-section {
flex: 1;
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-label {
font-weight: 500;
color: #495057;
margin-bottom: 8px;
display: block;
}
textarea {
width: 100%;
min-height: 120px;
padding: 12px;
border: 1px solid #ced4da;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 14px;
resize: vertical;
background-color: #fdfefe;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
}
textarea:focus {
outline: none;
border-color: #80bdff;
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
}
.btn {
font-weight: 500;
border-radius: 6px;
}
.footer {
padding: 15px 20px;
background-color: #f1f3f5;
border-top: 1px solid #dee2e6;
display: flex;
justify-content: space-between;
align-items: center;
}
.status-container {
display: flex;
align-items: center;
gap: 10px;
margin-left: 15px;
}
.status-icon {
font-size: 18px;
}
.status-success {
color: #28a745;
}
.status-failed {
color: #dc3545;
}
.status-label {
font-size: 14px;
font-weight: 500;
}
.submit-btn {
padding: 10px 24px;
font-size: 15px;
}
.code-area {
font-family: 'Courier New', monospace;
background-color: #f0f4f8;
border: 1px solid #d0d7de;
}
</style>
</head>
<body>
<div class="container-fluid">
<!-- Заголовок -->
<div class="header-section">
<h4>Редактор задачи</h4>
</div>
<!-- Основное содержимое -->
<div class="content-section">
<!-- Поле: Описание задачи (редактируемое) -->
<div class="form-group">
<label class="form-label">Описание задачи (HTML)</label>
<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>
</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>
</div>
</div>
<!-- Футер: статус + кнопки -->
<div class="footer">
<!-- Статус -->
<div id="statusContainer" class="status-container" style="display: none;">
<div id="statusIcon" class="status-icon">
<i id="icon" class="fa-solid"></i>
</div>
<span id="statusLabel" class="status-label"></span>
</div>
<!-- Кнопки -->
<div>
<button id="saveBtn" class="btn btn-success submit-btn me-2">
<i class="fas fa-save"></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>
</body>
</html>