Skip to content

links to task attempt pages and pages themselves #415

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 16, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.git
venv
.venv
.idea
.ssl
__pycache__
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.venv
venv
.idea
ssl
Expand Down
47 changes: 45 additions & 2 deletions app/api/task_attempts.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
from app.lti_session_passback.auth_checkers import check_auth, is_admin
from app.mongo_odm import TaskAttemptsDBManager, TasksDBManager
from app.utils import check_arguments_are_convertible_to_object_id, check_argument_is_convertible_to_object_id
from app.localisation import t
from app.status import PassBackStatus

from app.mongo_models import TaskAttempts

api_task_attempts = Blueprint('api_task_attempts', __name__)
logger = get_root_logger()
Expand All @@ -15,7 +19,7 @@

@check_arguments_are_convertible_to_object_id
@api_task_attempts.route('/api/task-attempts/', methods=['GET'])
def get_current_task_attempt() -> (dict, int):
def get_current_task_attempt() -> tuple[dict, int]:
"""
Endpoint to get current task attempt information.

Expand Down Expand Up @@ -56,10 +60,49 @@ def get_current_task_attempt() -> (dict, int):
'attempt_count': task_db.attempt_count,
}, 200

def get_task_attempt_information(task_attempt_db: TaskAttempts) -> dict:
try:
is_passed_back = dict(task_attempt_db.is_passed_back)

for training_id, training_status in is_passed_back.items():
is_passed_back[training_id] = t(PassBackStatus.russian.get(training_status))

return {
'message': 'OK',
'task_id': str(task_attempt_db.task_id),
'username': str(task_attempt_db.username),
'training_count': task_attempt_db.training_count,
'training_scores': dict(task_attempt_db.training_scores),
'is_passed_back': is_passed_back,
'params_for_passback': dict(task_attempt_db.params_for_passback)
}
except Exception as e:
return {'message': '{}: {}'.format(e.__class__, e)}


@check_arguments_are_convertible_to_object_id
@api_task_attempts.route('/api/task-attempts/<task_attempt_id>/', methods=['GET'])
def get_task_attempt(task_attempt_id) -> tuple[dict, int]:
"""
Endpoint to get information about a task attempt by its identifier.

:param task_attempt_id: Task attempt identifier
:return: Dictionary with task attempt information and 'OK' message, or
a dictionary with an explanation and 404 HTTP return code if a task attempt was not found, or
an empty dictionary with 404 HTTP return code if access was denied.
"""
if not check_access({'_id': ObjectId(task_attempt_id)}):
return {}, 404

task_attempt_db = TaskAttemptsDBManager().get_task_attempt(task_attempt_id)

if task_attempt_db is None:
return {'message': 'No task attempt with task_attempt_id = {}.'.format(task_attempt_id)}, 404
return get_task_attempt_information(task_attempt_db), 200

@check_arguments_are_convertible_to_object_id
@api_task_attempts.route('/api/task_attempts/<task_attempt_id>/', methods=['DELETE'])
def delete_task_attempt_by_task_attempt_id(task_attempt_id: str) -> (dict, int):
def delete_task_attempt_by_task_attempt_id(task_attempt_id: str) -> tuple[dict, int]:
"""
Endpoint to delete a task attempt by its identifier.

Expand Down
9 changes: 5 additions & 4 deletions app/feedback_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,14 @@ def __init__(self, weights=None):

super().__init__(name=PredefenceEightToTenMinutesNoSlideCheckFeedbackEvaluator.CLASS_NAME, weights=weights)

def find_strict_speech_duration_criterion(self, criteria_keys, suffix='StrictSpeechDurationCriterion'):
def rework_strict_speech_duration_criterion(self, criteria_keys, suffix='StrictSpeechDurationCriterion'):
for criteria in criteria_keys:
if suffix in criteria:
if criteria.endswith(suffix):
self.weights[criteria] = self.weights.pop('StrictSpeechDurationCriterion')
return criteria

def evaluate_feedback(self, criteria_results):
self.ssd_criterion = self.find_strict_speech_duration_criterion(criteria_results.keys())
self.ssd_criterion = self.rework_strict_speech_duration_criterion(criteria_results.keys())
if not criteria_results.get(self.ssd_criterion) or \
criteria_results[self.ssd_criterion].result == 0:
return Feedback(0)
Expand All @@ -180,11 +180,12 @@ def evaluate_feedback(self, criteria_results):

def get_result_as_sum_str(self, criteria_results):
if not self.ssd_criterion:
self.ssd_criterion = self.find_strict_speech_duration_criterion(criteria_results.keys())
self.ssd_criterion = self.rework_strict_speech_duration_criterion(criteria_results.keys())
if criteria_results is None or self.weights is None or \
criteria_results.get(self.ssd_criterion, {}).get('result', 0) == 0 or \
criteria_results.get("DEFAULT_SPEECH_PACE_CRITERION", {}).get('result', 0) == 0:
return None

return super().get_result_as_sum_str(criteria_results)


Expand Down
2 changes: 1 addition & 1 deletion app/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,6 @@
"Применить": "Apply",
"Отмена": "Cancel",
"Активные фильтры отсутствуют": "There are no active filters",
"Некоторые фильтры (выделенные красным) установлены неправильным образом. Прочитайте описание фильтра, наведясь на его подчеркнутое название, и проверьте введенные данные.": "Some filters (highlighted in red) are set incorrectly. Read the description of the filter by hovering over its underlined name and check the entered data.",
"Некоторые фильтры (выделенные красным) установлены неправильным образом. Прочитайте описание фильтра, наведясь на его подчеркнутое название, и проверьте введенные данные.": "Some filters (highlighted in red) are set incorrectly. Read the description of the filter by hovering over its underlined name and check the entered data."

}
42 changes: 42 additions & 0 deletions app/routes/task_attempts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from app.root_logger import get_root_logger

from bson import ObjectId
from flask import Blueprint, render_template, jsonify, request, session
from app.localisation import *

from app.api.task_attempts import get_task_attempt
from app.check_access import check_access
from app.lti_session_passback.auth_checkers import check_admin, check_auth
from app.utils import check_arguments_are_convertible_to_object_id

routes_task_attempts = Blueprint('routes_task_attempts', __name__)
logger = get_root_logger()

@check_arguments_are_convertible_to_object_id
@routes_task_attempts.route('/task_attempts/<task_attempt_id>/', methods=['GET'])
def view_task_attempt(task_attempt_id: str):
"""
Route to view page with task attempt.

:param task_attempt_id: Task attempt identifier
:return: Page or an empty dictionary with 404 HTTP code if access was denied.
"""

if not check_access({'_id': ObjectId(task_attempt_id)}):
return {}, 404

# Нужна ли проверка авторизации?

task_attempt, task_attempt_status_code = get_task_attempt(task_attempt_id)

if task_attempt.get('message') != 'OK':
return task_attempt, task_attempt_status_code

return render_template(
'task_attempt.html',
task_attempt_id=task_attempt_id,
task_id=task_attempt['task_id'],
username=task_attempt['username'],
training_scores=task_attempt['training_scores'],
is_passed_back=task_attempt['is_passed_back'],
), 200
Empty file added app/static/css/task_attempt.css
Empty file.
5 changes: 4 additions & 1 deletion app/static/js/show_all_trainings.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ function buildCurrentTrainingRow(trainingId, trainingJson, is_Admin=false) {

const trainingAttemptIdElement = document.createElement("td");
if(trainingJson["task_attempt_id"] !== "undefined" && trainingJson["message"] === "OK"){
trainingAttemptIdElement.textContent = "..." + String(trainingJson["task_attempt_id"]).slice(-5);
const trainingAttemptIdLink = document.createElement("a");
trainingAttemptIdLink.href=`/task_attempts/${trainingJson["task_attempt_id"]}`;
trainingAttemptIdLink.textContent = `...${(trainingJson["task_attempt_id"]).slice(-5)}`;
trainingAttemptIdElement.appendChild(trainingAttemptIdLink);
}
currentTrainingRowElement.appendChild(trainingAttemptIdElement);

Expand Down
65 changes: 65 additions & 0 deletions app/static/js/task_attempts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
function getTrainingsTable(trainingScores, isPassedBack) {
const trainingsTable = {};

Object.keys(trainingScores).forEach(trainingId => {
if (trainingsTable[trainingId] === undefined) {
trainingsTable[trainingId] = {};
}
trainingsTable[trainingId].score = trainingScores[trainingId];
});

Object.keys(isPassedBack).forEach(trainingId => {
if (trainingsTable[trainingId] === undefined) {
trainingsTable[trainingId] = {};
}
trainingsTable[trainingId].passedBackStatus = isPassedBack[trainingId];
});

return trainingsTable;
}

function createTableHeaderElement() {
const trainingIdHeaderElement = document.createElement("th");
trainingIdHeaderElement.innerHTML = "training_id";

const trainingScoreHeaderElement = document.createElement("th");
trainingScoreHeaderElement.innerHTML = "score";

const trainingStatusHeaderElement = document.createElement("th");
trainingStatusHeaderElement.innerHTML = "pass_back_status";

const tableHeaderElement = document.createElement("tr");

tableHeaderElement.append(trainingIdHeaderElement, trainingScoreHeaderElement, trainingStatusHeaderElement);

return tableHeaderElement;
}

function createTableRowElement(trainingInfo, trainingId) {
const tableRowElement = document.createElement("tr");

const tableRowIdElement = document.createElement("td");
tableRowIdElement.innerHTML = `<a href="/trainings/statistics/${trainingId}/">${trainingId}</a>`;

const tableRowScoreElement = document.createElement("td");
tableRowScoreElement.innerHTML = trainingInfo.score.toFixed(2) || "none";

const tableRowStatusElement = document.createElement("td");
tableRowStatusElement.innerHTML = trainingInfo.passedBackStatus || "none";

tableRowElement.append(tableRowIdElement, tableRowScoreElement, tableRowStatusElement);

return tableRowElement;
}

function showRelatedTrainingsTable(trainingScores, isPassedBack) {
const trainingsTable = getTrainingsTable(trainingScores, isPassedBack);

const tableElement = document.getElementById("related-trainings-table");

tableElement.appendChild(createTableHeaderElement());

Object.keys(trainingsTable).forEach(trainingId => {
tableElement.appendChild(createTableRowElement(trainingsTable[trainingId], trainingId));
});
}
2 changes: 1 addition & 1 deletion app/static/js/training_statistics.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ function buildCurrentAttemptStatistics() {
.then(response => {
let trainingNumber = response["training_number"];
let attemptCount = response["attempt_count"];
if (response === {} || trainingNumber === attemptCount) {
if (jQuery.isEmptyObject(response) || trainingNumber === attemptCount) {
return;
}
document.getElementById("training-number").textContent
Expand Down
26 changes: 26 additions & 0 deletions app/templates/task_attempt.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{% extends 'base.html' %}

{% block header %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/task_attempt.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/libraries/jquery-ui/jquery-ui.min.css') }}">

{% endblock %}

{% block content %}
<div class="base-container">
<h3>{{ title }}</h3>
<h3>Информация по попытке с ID {{ task_attempt_id }}</h3>
<h3>task_id: {{ task_id }}</h3>
<h3>Имя пользователя: {{ username }}</h3>
<div class="related-trainings-container">
<h3>Связанные тренировки:</h3>
<table id="related-trainings-table" class="table center-align-table"></table>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Добавьте в таблицу столбцы с

  • порядковым номером тренировки (для наглядности)
  • временем тренировки (== "Дата создания тренировки" со страницы тренировки)
  • статусом тренировки (== "id тренировки: ... Статус: Обработана." со страницы тренировки)

</div>
</div>
<script src="{{ url_for('static', filename='js/libraries/jquery.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/task_attempts.js') }}"></script>
<script type="text/javascript">
showRelatedTrainingsTable({{ training_scores|tojson|safe }}, {{ is_passed_back|tojson|safe }});
</script>
{% endblock %}
2 changes: 2 additions & 0 deletions app/web_speech_trainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from app.routes.lti import routes_lti
from app.routes.presentations import routes_presentations
from app.routes.trainings import routes_trainings
from app.routes.task_attempts import routes_task_attempts
from app.routes.version import routes_version
from app.status import PassBackStatus, TrainingStatus
from app.training_manager import TrainingManager
Expand All @@ -52,6 +53,7 @@
app.register_blueprint(routes_lti)
app.register_blueprint(routes_presentations)
app.register_blueprint(routes_trainings)
app.register_blueprint(routes_task_attempts)
app.register_blueprint(routes_version)

logger = get_root_logger(service_name='web')
Expand Down
32 changes: 32 additions & 0 deletions app_conf/no_docker.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[constants]
presentation_file_max_size_in_megabytes=16
app_secret_key=secret_key_placeholder
lti_consumer_key=secretconsumerkey
lti_consumer_secret=supersecretconsumersecret
version_file=VERSION.json
backup_path=../dump/database-dump/

[mongodb]
url=mongodb://localhost:27017/
database_name=database

[vosk]
url=ws://localhost:2700

[whisper]
url=http://whisper:9000/asr

[user_agent_platform]
windows=True
linux=True

[user_agent_browser]
chrome=89
firefox=87

[locale]
language=ru

[bugreport]
form_link=https://docs.google.com/forms/d/e/1FAIpQLScUudcDPUwtTvmN_sbeljicHYhubK7pPQIM1o8Wh54HstT2BQ/viewform?usp=sf_link
[email protected]
2 changes: 1 addition & 1 deletion app_conf/testing.ini
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,4 @@ language=ru

[bugreport]
form_link=https://docs.google.com
[email protected]
[email protected]