Не закрывайте, не обновляйте эту вкладку! Служебная вкладка!
+Не закрывайте, не обновляйте эту вкладку! Служебная вкладка для работы локального прокторинга!
${title}
+ + + `; + + const closeModal = (result) => { + overlay.remove(); + document.body.style.overflow = ''; + resolve(result); + }; + + modal.querySelector('#modal-confirm-yes').addEventListener('click', () => closeModal(true)); + modal.querySelector('#modal-confirm-no').addEventListener('click', () => closeModal(false)); + + document.body.style.overflow = 'hidden'; + overlay.appendChild(modal); + document.body.appendChild(overlay); + + }); +} + function sendModalNotifyToActiveTab(messages, title) { return new Promise((resolve, reject) => { chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { @@ -289,7 +346,7 @@ export function setReadyToUploadContainer(container, files) { const titleElement = document.createElement("div"); titleElement.id = "ready-to-upload-container-title"; - titleElement.innerHTML = `Файлов, доступных для выгрузки: ${files.length}`; + titleElement.innerHTML = `Файлов, доступных для выгрузки: ${files.length}`; container.appendChild(titleElement); diff --git a/client/scripts/index.js b/client/scripts/index.js index 157a939..83eb0db 100644 --- a/client/scripts/index.js +++ b/client/scripts/index.js @@ -24,7 +24,7 @@ const inputElements = { name: document.querySelector('#name_input'), surname: document.querySelector('#surname_input'), patronymic: document.querySelector('#patronymic_input'), - link: document.querySelector('#link_input') + title: document.querySelector('#title_input') }; const buttonElements = { @@ -74,26 +74,28 @@ const bStates = { } } +const validationStringRegExp = /^[А-ЯЁ][а-яёА-ЯЁ -]*$/; + const validationRules = { group: { regex: /^\d{4}$/, message: "Группа должна содержать ровно 4 цифры. Пример: '1234'" }, name: { - regex: /^[A-ZА-ЯЁ][a-zа-яёA-ZА-ЯЁ-]*$/, - message: "Имя должно начинаться с заглавной буквы и содержать только русские/латинские буквы и тире. Пример: 'Иван'" + regex: validationStringRegExp, + message: "Имя должно начинаться с заглавной буквы и содержать только кириллицу и тире/пробел. Пример: 'Иван'" }, surname: { - regex: /^[A-ZА-ЯЁ][a-zа-яёA-ZА-ЯЁ-]*$/, - message: "Фамилия должна начинаться с заглавной буквы и содержать только русские/латинские буквы и тире. Пример: 'Иванов'" + regex: validationStringRegExp, + message: "Фамилия должна начинаться с заглавной буквы и содержать только кириллицу и тире/пробел. Пример: 'Иванов'" }, patronymic: { - regex: /^[A-ZА-ЯЁ][a-zа-яёA-ZА-ЯЁ-]*$/, - message: "Отчество должно начинаться с заглавной буквы и содержать только русские/латинские буквы и тире. Пример: 'Иванович'" + regex: validationStringRegExp, + message: "Отчество должно начинаться с заглавной буквы и содержать только кириллицу и тире/пробел. Пример: 'Иванович'" }, - link: { + title: { regex: /.+/, - message: "Ссылка на комнату не должна быть пустой." + message: "Название работы не должно быть пустым." } }; @@ -165,7 +167,7 @@ async function saveInputValues() { surname: inputElements.surname.value, patronymic: inputElements.patronymic.value, noPatronymicChecked: noPatronymicCheckbox.checked, - link: inputElements.link.value + title: inputElements.title.value } }); logClientAction({ action: "Save input values" }); @@ -474,7 +476,7 @@ async function startRecCallback() { name: inputElements.name.value, surname: inputElements.surname.value, patronymic: noPatronymicCheckbox.checked ? "Без_отчества" : inputElements.patronymic.value.trim(), - link: inputElements.link.value + title: inputElements.title.value }; chrome.runtime.sendMessage({ @@ -604,7 +606,7 @@ async function saveFormDataFilesLocally(formData, sessionId) { async function uploadFormDataToServer(formData, sessionId) { logClientAction({ action: "Send upload request", sessionId: sessionId, messageType: "upload_video" }); - const eventSource = new EventSource(`http://127.0.0.1:5000/progress/${sessionId}`); + const eventSource = new EventSource(`${session_settings.server_url}/progress/${sessionId}`); const steps = 7; @@ -634,7 +636,7 @@ async function uploadFormDataToServer(formData, sessionId) { }; try { - const response = await fetch('http://127.0.0.1:5000/upload_video', { + const response = await fetch(`${session_settings.server_url}/upload_video`, { method: "POST", body: formData, }); @@ -699,10 +701,10 @@ async function uploadVideo(files) { await chrome.storage.local.remove("metadata"); await chrome.storage.local.set({"session_status" : "need_init"}); - inputElements.link.value = ""; - inputElements.link.classList.remove('input-valid', 'input-invalid'); + inputElements.title.value = ""; + inputElements.title.classList.remove('input-valid', 'input-invalid'); await saveInputValues(); - logClientAction("Clear link field"); + logClientAction("Clear work title field"); } }); } \ No newline at end of file diff --git a/client/scripts/media.js b/client/scripts/media.js index 8a1152f..01d426a 100644 --- a/client/scripts/media.js +++ b/client/scripts/media.js @@ -1,4 +1,4 @@ -import { showModalNotify, waitForNotificationSuppression, getBrowserFingerprint, generateObjectId, requestClearLogs, getDifferenceInTime, getFormattedDateString, getAvailableDiskSpace, saveBlobToFile } from './common.js'; +import { showModalNotify, showModalConfirm, waitForNotificationSuppression, getBrowserFingerprint, generateObjectId, requestClearLogs, getDifferenceInTime, getFormattedDateString, getAvailableDiskSpace, saveBlobToFile } from './common.js'; import { getCurrentDateString, setReadyToUploadContainer, parseDateString } from "./common.js"; import { logClientAction, flushLogs, checkAndCleanLogs, prepareLogs } from './logger.js'; @@ -21,6 +21,11 @@ var recorders = { camera: null }; +var noStreamConfirm = { + microphone: false, + camera: false +} + var readyToUploadContainer = document.querySelector('.ready-to-upload-container'); var combinedPreview = document.querySelector('.combined__preview'); @@ -69,6 +74,10 @@ class Metadata { session_client_size: undefined // MB }, recording_sessions: [], + no_stream_confirm: { + microphone: false, + camera: false + } }; } @@ -85,10 +94,14 @@ class Metadata { const screenSettings = screenVideoTrack.getSettings(); this.metadata.screen.session_client_resolution = `${screenSettings.width}×${screenSettings.height}`; - this.metadata.camera.session_client_mime = recorders.camera.mimeType; - const [cameraVideoTrack] = streams.camera.getVideoTracks(); - const cameraSettings = cameraVideoTrack.getSettings(); - this.metadata.camera.session_client_resolution = `${cameraSettings.width}×${cameraSettings.height}`; + if (recorders.camera) { + this.metadata.camera.session_client_mime = recorders.camera.mimeType; + const [cameraVideoTrack] = streams.camera.getVideoTracks(); + const cameraSettings = cameraVideoTrack.getSettings(); + this.metadata.camera.session_client_resolution = `${cameraSettings.width}×${cameraSettings.height}`; + } else + this.metadata.no_stream_confirm.microphone = noStreamConfirm.microphone; + this.metadata.no_stream_confirm.camera = noStreamConfirm.camera; } logClientAction({ action: "Set metadata record on" }); @@ -101,8 +114,10 @@ class Metadata { const screenFile = await combinedFileHandle.getFile(); this.metadata.screen.session_client_size = (screenFile.size / 1000000).toFixed(3); - const cameraFile = await cameraFileHandle.getFile(); - this.metadata.camera.session_client_size = (cameraFile.size / 1000000).toFixed(3); + if (cameraFileHandle){ + const cameraFile = await cameraFileHandle.getFile(); + this.metadata.camera.session_client_size = (cameraFile.size / 1000000).toFixed(3); + } logClientAction({ action: "Set metadata record off" }); }; @@ -130,6 +145,15 @@ const stopStreams = () => { logClientAction({ action: "Stop streams" }); }; +const stopStream = (stream) => { + const value = streams[stream]; + if (value) { + value.getTracks().forEach(track => track.stop()); + streams[stream] = null; + } + logClientAction({ action: `Stop stream ${stream}` }); +}; + async function checkOpenedPopup() { let a = await chrome.runtime.getContexts({contextTypes: ['POPUP']}); const isPopupOpen = a.length > 0; @@ -279,15 +303,23 @@ async function getMediaDevices() { if (micError.name === 'NotAllowedError') { micPermissionDenied = true; logClientAction({ action: "Microphone permission denied", error: "NotAllowedError" }); - await showModalNotify("Ошибка при доступе к микрофону: NotAllowedError", "Ошибка"); + //await showModalNotify("Ошибка при доступе к микрофону: NotAllowedError", "Ошибка"); } else { logClientAction({ action: "Microphone permission denied" }); - //alert('Ошибка при доступе к микрофону: ' + micError.message); - await showModalNotify('Ошибка при доступе к микрофону: ' + micError.message, "Ошибка"); + //await showModalNotify('Ошибка при доступе к микрофону: ' + micError.message, "Ошибка"); + } + let userAnswer = await showModalConfirm("При доступе к микрофону возникла ошибка, хотите продолжить без него?") + if (userAnswer) { + noStreamConfirm.microphone = true; + logClientAction({ action: "User've confirm work without a microphone" }) + await showModalNotify("Запись будет осуществлена без микрофона."); + stopStream('microphone'); + } + else { stopStreams(); reject(micError); - return; } + } if (!micPermissionDenied) { @@ -335,19 +367,27 @@ async function getMediaDevices() { } catch (camError) { if (camError.name === 'NotAllowedError') { logClientAction({ action: "Camera permission denied", error: "NotAllowedError" }); - await showModalNotify("Ошибка при доступе к камере: NotAllowedError", "Ошибка"); + //await showModalNotify("Ошибка при доступе к камере: NotAllowedError", "Ошибка"); camPermissionDenied = true; } else { logClientAction({ action: "Camera permission denied" }); - //alert('Ошибка при доступе к камере: ' + camError.message); - await showModalNotify('Ошибка при доступе к камере: ' + camError.message, "Ошибка"); + //await showModalNotify('Ошибка при доступе к камере: ' + camError.message, "Ошибка"); + } + + let userAnswer = await showModalConfirm("При доступе к камере возникла ошибка, хотите продолжить без неё?") + if (userAnswer) { + noStreamConfirm.camera = true; + logClientAction({ action: "User've confirm work without a camera" }) + await showModalNotify("Запись будет осуществлена без камеры."); + stopStream('camera'); + } + else { stopStreams(); reject(camError); - return; } } - if (micPermissionDenied || camPermissionDenied) { + if ((!noStreamConfirm.microphone && !noStreamConfirm.camera) && (micPermissionDenied || camPermissionDenied)) { stopStreams(); const extensionId = chrome.runtime.id; const settingsUrl = `chrome://settings/content/siteDetails?site=chrome-extension://${extensionId}`; @@ -373,26 +413,28 @@ async function getMediaDevices() { } // Обработка потери доступа - streams.camera.oninactive = async function () { - if (streamLossSource) return; - streamLossSource = 'camera'; - logClientAction('Camera stream inactive'); - - if (!recorders.combined && !recorders.camera) return; - - if (recorders.combined.state === 'inactive' && recorders.camera.state === 'inactive') { - await sendButtonsStates('needPermissions'); - await showModalNotify(["Разрешение на камеру отозвано.", - "Дайте доступ заново в расширении во всплывающем окне по кнопке Разрешения."], "Доступ к камере потерян!"); - stopStreams(); - } else { - stopDuration(startTime); - await sendButtonsStates('failedUpload'); - await showModalNotify(["Текущие записи завершатся. Чтобы продолжить запись заново, выдайте разрешения во всплывающем окне по кнопке Разрешения и начните запись."], "Доступ к камере потерян!"); - // updateInvalidStopValue(true); - stopRecord(); - } - }; + if (streams.camera) { + streams.camera.oninactive = async function () { + if (streamLossSource) return; + streamLossSource = 'camera'; + logClientAction('Camera stream inactive'); + + if (!recorders.combined && !recorders.camera) return; + + if (recorders.combined.state === 'inactive' && recorders.camera.state === 'inactive') { + await sendButtonsStates('needPermissions'); + await showModalNotify(["Разрешение на камеру отозвано.", + "Дайте доступ заново в расширении во всплывающем окне по кнопке Разрешения."], "Доступ к камере потерян!"); + stopStreams(); + } else { + stopDuration(startTime); + await sendButtonsStates('failedUpload'); + await showModalNotify(["Текущие записи завершатся. Чтобы продолжить запись заново, выдайте разрешения во всплывающем окне по кнопке Разрешения и начните запись."], "Доступ к камере потерян!"); + // updateInvalidStopValue(true); + stopRecord(); + } + }; + } streams.screen.getVideoTracks()[0].onended = async function () { chrome.runtime.sendMessage({ type: 'screenCaptureStatus', active: false }); @@ -414,29 +456,31 @@ async function getMediaDevices() { } }; - streams.microphone.getAudioTracks()[0].onended = async function () { - if (streamLossSource) return; - streamLossSource = 'microphone'; - logClientAction('Microphone stream ended'); - - if (!recorders.combined || recorders.combined.state === 'inactive') { - await sendButtonsStates('needPermissions'); - await showModalNotify(["Разрешение на микрофон отозвано.", - "Дайте доступ заново в расширении во всплывающем окне по кнопке Разрешения."], "Доступ к микрофону потерян!"); - stopStreams(); - } else { - stopDuration(startTime); - await sendButtonsStates('failedUpload'); - await showModalNotify(["Текущие записи завершатся. Чтобы продолжить запись заново, выдайте разрешения в расширении во всплывающем окне по кнопке Разрешения и начните запись."], "Доступ к микрофону потерян!"); - // updateInvalidStopValue(true); - stopRecord(); - } - }; + let combinedStreams = [streams.screen.getVideoTracks()[0]] + + if (streams.microphone) { + streams.microphone.getAudioTracks()[0].onended = async function () { + if (streamLossSource) return; + streamLossSource = 'microphone'; + logClientAction('Microphone stream ended'); + + if (!recorders.combined || recorders.combined.state === 'inactive') { + await sendButtonsStates('needPermissions'); + await showModalNotify(["Разрешение на микрофон отозвано.", + "Дайте доступ заново в расширении во всплывающем окне по кнопке Разрешения."], "Доступ к микрофону потерян!"); + stopStreams(); + } else { + stopDuration(startTime); + await sendButtonsStates('failedUpload'); + await showModalNotify(["Текущие записи завершатся. Чтобы продолжить запись заново, выдайте разрешения в расширении во всплывающем окне по кнопке Разрешения и начните запись."], "Доступ к микрофону потерян!"); + // updateInvalidStopValue(true); + stopRecord(); + } + }; + combinedStreams.push(streams.microphone.getAudioTracks()[0]) + } - streams.combined = new MediaStream([ - streams.screen.getVideoTracks()[0], - streams.microphone.getAudioTracks()[0] - ]); + streams.combined = new MediaStream(combinedStreams); combinedPreview.srcObject = streams.combined; cameraPreview.srcObject = streams.camera; @@ -451,7 +495,7 @@ async function getMediaDevices() { cameraPreview.height = 240; }; - cameraPreview.style.display = 'block'; + cameraPreview.style.display = streams.camera ? 'block' : 'none'; combinedPreview.style.display = 'block'; combinedPreview.muted = false; @@ -460,7 +504,7 @@ async function getMediaDevices() { logClientAction({ action: "Create combined recorder" }); - recorders.camera = createCameraMediaRecorder(); + recorders.camera = streams.camera ? createCameraMediaRecorder() : null; logClientAction({ action: "Create camera recorder" }); @@ -486,26 +530,27 @@ async function getMediaDevices() { } }; - recorders.camera.ondataavailable = async (event) => { - if (event.data.size > 0) { - const cameraWritableStream = await cameraFileHandle.createWritable({ keepExistingData: true }); + if (recorders.camera) + recorders.camera.ondataavailable = async (event) => { + if (event.data.size > 0) { + const cameraWritableStream = await cameraFileHandle.createWritable({ keepExistingData: true }); - const file = await cameraFileHandle.getFile(); - const size = file.size; + const file = await cameraFileHandle.getFile(); + const size = file.size; - console.log("cameraFileHandle", size); + console.log("cameraFileHandle", size); - await cameraWritableStream.write({ - type: "write", - position: size, - data: event.data - }); + await cameraWritableStream.write({ + type: "write", + position: size, + data: event.data + }); - await cameraWritableStream.close(); + await cameraWritableStream.close(); - await chrome.storage.local.set({"recording_end": (new Date()).toISOString()}); - } - }; + await chrome.storage.local.set({"recording_end": (new Date()).toISOString()}); + } + }; resolve(); } catch (error) { @@ -670,15 +715,13 @@ chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => { logClientAction({ action: "Get media devices success" }); await sendButtonsStates('readyToRecord'); await showModalNotify(["Разрешения получены. Теперь вы можете начать запись.", + "Нажмите на кнопку «Начать запись» во всплывающем окне " + + "расширения прокторинга, когда будете готовы.", + "", "Для удобства уведомление о доступе к вашему экрану можно скрыть или передвинуть. НЕЛЬЗЯ НАЖИМАТЬ НА «Закрыть доступ».", "НЕЛЬЗЯ ОБНОВЛЯТЬ, ЗАКРЫВАТЬ СЛУЖЕБНУЮ ВКЛАДКУ во время записи! НЕЛЬЗЯ ЗАКРЫВАТЬ БРАУЗЕР во время записи!", "Предпросмотр будет отключен. Его можно включить по кнопке на служебной вкладке расширения. По умолчанию звук выключен и включается в плеере.", - "Во всплывающем окне расширения прокторинга можно найти статистику начала, продолжительности записи, а также разрешений.", - "В случае потери разрешений запись будет прервана и ее необходимо будет начать заново.", - "Расширение сообщит Вам обо всем необходимом. Удачной работы!", - "", - "Нажмите на кнопку «Начать запись» во всплывающем окне " + - "расширения прокторинга, когда будете готовы.",], + ], "Готово к записи"); }) .catch(async () => { @@ -696,7 +739,7 @@ chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => { formData.append('name', message.formData.name || ''); formData.append('surname', message.formData.surname || ''); formData.append('patronymic', message.formData.patronymic || ''); - formData.append('link', message.formData.link || ''); + formData.append('title', message.formData.title || ''); function formDataToObject(formData) { const obj = {}; @@ -734,6 +777,7 @@ chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => { if (!suppress) { await showModalNotify( ["Запись экрана, микрофона и камеры началась. " + + "Во всплывающем окне разрешения доступна информация о времени начала записи и её продолжительности.", "Не отключайте разрешения этим элементам до окончания записи.", "Чтобы завершить запись, нажмите кнопку «Остановить запись» во всплывающем окне расширения прокторинга."], "Идёт запись", @@ -764,9 +808,10 @@ async function initSession(formData) { getBrowserFingerprint() try { - const response = await fetch('http://127.0.0.1:5000/start_session', { + const response = await fetch(`${session_settings.server_url}/start_session`, { method: 'POST', mode: 'cors', + credentials: 'include', body: formData }); @@ -829,6 +874,9 @@ async function stopRecord() { endTime = new Date(); + console.log(metadata) + + // ВОТ ТУТ await metadata.setMetadatasRecordOff(endTime); let combinedFileSize = 0; @@ -885,7 +933,7 @@ async function stopRecord() { if (session_settings.server_connection || session_settings.local_video_saving) { await showModalNotify( - ["Для сохранения и отправки записи необходимо нажать кнопку «Отправить» во всплывающем окне расширения прокторинга."], + ["Для скачивания записи необходимо нажать кнопку «Сохранить» во всплывающем окне расширения прокторинга."], "Отправка записи", true ); @@ -909,9 +957,15 @@ async function startRecord() { logClientAction({ action: "Fail to start recording", reason: "Insufficient disk space" }); return; } - if (!combinedPreview.srcObject || !cameraPreview.srcObject) { - console.log('Выдайте разрешения'); - logClientAction({ action: "Fail to start recording", reason: "Missing media permissions" }); + if (!combinedPreview.srcObject) { + console.log('Выдайте разрешения к экрану'); + logClientAction({ action: "Fail to start recording", reason: "Missing media permission for screen" }); + return; + } + + if (!noStreamConfirm.camera && !cameraPreview.srcObject) { + console.log('Выдайте разрешения на камеру'); + logClientAction({ action: "Fail to start recording", reason: "Missing media permission for camera" }); return; } @@ -924,18 +978,23 @@ async function startRecord() { let startRecordTime = getCurrentDateString(startTime); combinedFileName = createScreenFileName(startRecordTime); - cameraFileName = createCameraFileName(startRecordTime); - await addFileToTempList(combinedFileName); - await addFileToTempList(cameraFileName); + + cameraFileName = null; + if (cameraPreview.srcObject) { + cameraFileName = createCameraFileName(startRecordTime); + await addFileToTempList(cameraFileName); + } try { combinedFileHandle = await rootDirectory.getFileHandle(combinedFileName, {create: true}); logClientAction({ action: "Create file handle", fileType: "screen", fileName: combinedFileName }); - cameraFileHandle = await rootDirectory.getFileHandle(cameraFileName, {create: true}); - logClientAction({ action: "Create file handle", fileType: "camera", fileName: cameraFileName }); - + cameraFileHandle = null; + if (cameraPreview.srcObject) { + cameraFileHandle = await rootDirectory.getFileHandle(cameraFileName, {create: true}); + logClientAction({ action: "Create file handle", fileType: "camera", fileName: cameraFileName }); + } logClientAction('Files added to temp list'); chrome.storage.local.set({ @@ -960,14 +1019,14 @@ async function startRecord() { // startTime = new Date(); recorders.combined.start(video_settings.video_saving_interval); - recorders.camera.start(video_settings.video_saving_interval); + if (recorders.camera) + recorders.camera.start(video_settings.video_saving_interval); isRecording = true; isPreviewEnabled = false; hideMutePreviews(); updatePreviewButton(); - console.log('Запись начата'); logClientAction('recording_started'); //chrome.runtime.sendMessage({ action: "closePopup" }); diff --git a/client/settings.json b/client/settings.json index 5ad7c28..10ffaa3 100644 --- a/client/settings.json +++ b/client/settings.json @@ -20,7 +20,8 @@ "video_saving_interval": 5000 }, "session_settings" : { - "server_connection" : false, - "local_video_saving" : true + "server_connection" : true, + "local_video_saving" : true, + "server_url": "https://proctoring.moevm.pro/api/remoteSessions" } } diff --git a/client/styles/media.css b/client/styles/media.css index 9fc4ab4..f70a89e 100644 --- a/client/styles/media.css +++ b/client/styles/media.css @@ -76,6 +76,28 @@ background: #0056b3; } +#modal-confirm-yes { + margin-top: 15px; + background: #008c05; + color: #faf0ec !important; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + font-weight: 700; +} + +#modal-confirm-no { + margin-top: 15px; + background: #8c0000; + color: #faf0ec !important; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + font-weight: 700; +} + .preview-label { font-size: 18px; @@ -214,8 +236,8 @@ left: 50%; transform: translateX(-50%); margin-top: 8px; - width: 24px; - height: 24px; + width: 48px; + height: 48px; z-index: 10; pointer-events: none; } diff --git a/server/app.py b/server/app.py index 8589dcd..d84ad20 100644 --- a/server/app.py +++ b/server/app.py @@ -8,6 +8,8 @@ from bson import ObjectId from flask_cors import CORS from datetime import datetime, timezone +from logging import getLogger + app = Flask(__name__) CORS(app, resources={r"/*": {"origins": "*"}}) @@ -32,10 +34,10 @@ def start_session(): surname = data.get("surname") name = data.get("name") patronymic = data.get("patronymic") - link = data.get("link") + title = data.get("title") - if not (group and surname and name and patronymic and link): - return jsonify({"error": "Поля 'group', 'surname', 'name', 'patronymic', 'link' обязательны для заполнения"}), 400 + if not (group and surname and name and patronymic and title): + return jsonify({"error": f"Поля 'group', 'surname', 'name', 'patronymic', 'title' обязательны для заполнения. {data}"}), 400 session_start = datetime.now(timezone.utc) # Форматирование даты @@ -48,7 +50,7 @@ def start_session(): "surname": surname, "name": name, "patronymic": patronymic, - "link": link, + "title": title, "session_date_start": session_date_start, "session_time_start": session_time_start, "session_date_end": None, @@ -60,7 +62,7 @@ def start_session(): "logs_path": None } - sessions_collection.insert_one(session_data) + sessions_collection.insert_one(session_data) # TODO: add MongoModel return jsonify({"id": str(id)}), 201 except Exception as e: @@ -92,11 +94,14 @@ def convert_user_time_to_utc(user_time_str, user_time_offset): user_time_utc = user_time + user_time_offset return user_time_utc.strftime("%Y%m%dT%H%M%S") -@app.route("/upload_video", methods=["POST"]) +@app.route("/upload_video/", methods=["POST"]) def upload_video(): """Обрабатывает загрузку видео с клиента, сохраняет файл и обновляет данные сеанса.""" + getLogger().error(request.form) + getLogger().error(request.files) try: - if "screen_video" not in request.files or "camera_video" not in request.files or "id" not in request.form: + if "screen_video" not in request.files or "id" not in request.form: + getLogger('root').error(request.files) return jsonify({"error": "Отсутствует видеофайл или ID сессии"}), 400 id = request.form["id"] diff --git a/server/static/filter.js b/server/static/filter.js index ffe9909..d8b6a45 100644 --- a/server/static/filter.js +++ b/server/static/filter.js @@ -44,7 +44,7 @@ document.addEventListener("DOMContentLoaded", function () { `${fields2column}`, "status", "metadata", - "link" + "title" ]; // Создаем заголовок таблицы diff --git a/tests/functional_test.py b/tests/functional_test.py index 28cb4bd..673ce56 100644 --- a/tests/functional_test.py +++ b/tests/functional_test.py @@ -30,9 +30,15 @@ def test_name_field_validation(page: Page, extension_id: str) -> None: name_input.fill("Иван") expect(name_input).to_have_class(re.compile(r".*input-valid.*")) - name_input.fill("Albus-Severus") + name_input.fill("Альбус-Северус") + expect(name_input).to_have_class(re.compile(r".*input-valid.*")) + + name_input.fill("Альбус Северус") expect(name_input).to_have_class(re.compile(r".*input-valid.*")) + name_input.fill("Albus-Severus") + expect(name_input).to_have_class(re.compile(r".*input-invalid.*")) + name_input.fill("Дон Pedro") expect(name_input).to_have_class(re.compile(r".*input-invalid.*")) @@ -51,9 +57,15 @@ def test_surname_field_validation(page: Page, extension_id: str) -> None: surname_input.fill("Петров") expect(surname_input).to_have_class(re.compile(r".*input-valid.*")) - surname_input.fill("Smith-Johnson") + surname_input.fill("Петров-Иванов") expect(surname_input).to_have_class(re.compile(r".*input-valid.*")) + surname_input.fill("Петров Иванов") + expect(surname_input).to_have_class(re.compile(r".*input-valid.*")) + + surname_input.fill("Smith-Johnson") + expect(surname_input).to_have_class(re.compile(r".*input-invalid.*")) + surname_input.fill("Smith Johnson") expect(surname_input).to_have_class(re.compile(r".*input-invalid.*")) @@ -76,7 +88,7 @@ def test_patronymic_field_validation(page: Page, extension_id: str) -> None: expect(patronymic_input).to_have_class(re.compile(r".*input-valid.*")) patronymic_input.fill("Александровна Мария") - expect(patronymic_input).to_have_class(re.compile(r".*input-invalid.*")) + expect(patronymic_input).to_have_class(re.compile(r".*input-valid.*")) patronymic_input.fill("Иванович123") expect(patronymic_input).to_have_class(re.compile(r".*input-invalid.*")) @@ -84,14 +96,14 @@ def test_patronymic_field_validation(page: Page, extension_id: str) -> None: patronymic_input.fill("иванович") expect(patronymic_input).to_have_class(re.compile(r".*input-invalid.*")) -def test_link_field_validation(page: Page, extension_id: str) -> None: +def test_title_field_validation(page: Page, extension_id: str) -> None: page.goto(f"chrome-extension://{extension_id}/pages/index.html") - link_input = page.locator("#link_input") + title_input = page.locator("#title_input") - link_input.fill("https://se.moevm.info/doku.php/start") - expect(link_input).to_have_class(re.compile(r".*input-valid.*")) + title_input.fill("https://se.moevm.info/doku.php/start") + expect(title_input).to_have_class(re.compile(r".*input-valid.*")) def test_no_patronymic_checkbox_validation(page: Page, extension_id: str) -> None: page.goto(f"chrome-extension://{extension_id}/pages/index.html") diff --git a/tests/integration_test.py b/tests/integration_test.py index 40a0696..071e7a0 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -10,7 +10,7 @@ def check_integration(url): "surname": "Tester", "name": "Test", "patronymic": "Testerovich", - "link": "http://test.com" + "title": "http://test.com" } print(f"Создана форма {form_data} для интеграционного тестирования")