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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.venv
venv
.venv
.idea
Expand Down
45 changes: 42 additions & 3 deletions app/api/task_attempts.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
from bson import ObjectId
from flask import session, request, Blueprint

from app.check_access import check_access
from app.check_access import check_access, check_task_attempt_access
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.api.trainings import get_training

from app.mongo_models import TaskAttempts

api_task_attempts = Blueprint('api_task_attempts', __name__)
logger = get_root_logger()
Expand All @@ -15,7 +20,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 +61,44 @@ 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:
training_ids = task_attempt_db.training_scores.keys()
trainings = dict(zip(training_ids, map(get_training, training_ids)))

return {
'message': 'OK',
'task_id': str(task_attempt_db.task_id),
'username': str(task_attempt_db.username),
'trainings': trainings,
}
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_task_attempt_access(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
5 changes: 2 additions & 3 deletions app/api/trainings.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from app.audio import Audio
from app.check_access import check_access
from app.lti_session_passback.auth_checkers import is_admin, check_auth, check_admin
from app.lti_session_passback.auth_checkers import is_admin, check_auth, check_admin, is_logged_in
from app.mongo_models import Trainings
from app.mongo_odm import TrainingsDBManager, TaskAttemptsDBManager, TasksDBManager, DBManager
from app.filters import GetAllTrainingsFilterManager
Expand Down Expand Up @@ -439,8 +439,7 @@ def get_all_trainings() -> (dict, int):

print(number_page, count_items)

authorized = check_auth() is not None
if not (check_admin() or (authorized and [session.get('session_id')] == username)):
if not (check_admin() or (is_logged_in() and [session.get('session_id')] == username)):
return {}, 404

trainings = GetAllTrainingsFilterManager().query_with_filters(filters, number_page, count_items)
Expand Down
24 changes: 23 additions & 1 deletion app/check_access.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from flask import session

from app.mongo_odm import SessionsDBManager, TrainingsDBManager
from app.mongo_odm import SessionsDBManager, TrainingsDBManager, TaskAttemptsDBManager
from app.utils import is_testing_active


Expand All @@ -25,3 +25,25 @@ def _check_access(filters: dict) -> bool:

def _check_access_testing(filters: dict) -> bool:
return True

def _check_task_attempt_access(task_attempt_id: str) -> bool:
username = session.get('session_id', default=None)
consumer_key = session.get('consumer_key', default=None)
user_session = SessionsDBManager().get_session(username, consumer_key)

if not user_session:
return False
if user_session.is_admin:
return True

task_attempt = TaskAttemptsDBManager().get_task_attempt(task_attempt_id)
return task_attempt.username == username

def check_task_attempt_access(task_attempt_id: str) -> bool:
if not is_testing_active():
return _check_task_attempt_access(task_attempt_id)
else:
return _check_task_attempt_access_testing(task_attempt_id)

def _check_task_attempt_access_testing(task_attempt_id: str) -> bool:
return True
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, check_task_attempt_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_task_attempt_access(task_attempt_id):
return {}, 404

task_attempt, task_attempt_status_code = get_task_attempt(task_attempt_id)

print(task_attempt)

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'],
trainings=task_attempt['trainings'],
), 200
5 changes: 2 additions & 3 deletions app/routes/trainings.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from app.check_access import check_access
from app.criteria_pack import CriteriaPackFactory
from app.feedback_evaluator import FeedbackEvaluatorFactory
from app.lti_session_passback.auth_checkers import check_admin, check_auth
from app.lti_session_passback.auth_checkers import check_admin, check_auth, is_logged_in
from app.mongo_odm import CriterionPackDBManager, TasksDBManager, TaskAttemptsDBManager
from app.status import TrainingStatus, AudioStatus, PresentationStatus
from app.utils import check_arguments_are_convertible_to_object_id
Expand Down Expand Up @@ -141,8 +141,7 @@ def view_all_trainings():
except:
pass

authorized = check_auth() is not None
if not (check_admin() or (authorized and session.get('session_id') == username)):
if not (check_admin() or (is_logged_in() and session.get('session_id') == username)):
return {}, 404

raw_filters = request.args.getlist('f')
Expand Down
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
93 changes: 93 additions & 0 deletions app/static/js/task_attempts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
const tableColomnHeaders = [
"№", "training_id", "start_timestamp", "training_status", "pass_back_status", "score"
];

function get_time_string(timestampStr){
const timestamp = Date.parse(timestampStr);

options = { weekday: 'short', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: '2-digit', second: '2-digit', timeZoneName: 'short' }
if (!isNaN(timestamp)) {
processing_time = new Date(timestamp);
return processing_time.toLocaleString("ru-RU", options);
} else {
return "";
}
}

function createTableHeaderElement() {
const tableHeaderElement = document.createElement("tr");

tableColomnHeaders.forEach(colomnHeader => {
const tableColomnHeaderElement = document.createElement("th");
tableColomnHeaderElement.innerHTML = colomnHeader;

tableHeaderElement.appendChild(tableColomnHeaderElement);
});

return tableHeaderElement;
}

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

const tableRow = {};

tableColomnHeaders.forEach(columnHeader => {
const tableCellElement = document.createElement("td");

tableRow[columnHeader] = tableCellElement;
tableRowElement.appendChild(tableCellElement);
});

return [tableRowElement, tableRow];
}

function TableRowFiller() {
let pos = 1;

function fillTableRow(tableRow, training) {
tableRow["№"].innerHTML = pos++;
tableRow["training_id"].innerHTML = `<a href="/trainings/statistics/${training.id}/">${training.id}</a>`;
tableRow["start_timestamp"].innerHTML = get_time_string(training.training_start_timestamp);
tableRow["score"].innerHTML = training.score ? training.score.toFixed(2) : "none";
tableRow["training_status"].innerHTML = training.training_status;
tableRow["pass_back_status"].innerHTML = training.passedBackStatus || "none";
}

return fillTableRow;
}

function getSortedTrainingsList(trainings) {
const trainingsList = [];

Object.keys(trainings).forEach(trainingId => {
const training = trainings[trainingId];
training["id"] = trainingId;

trainingsList.push(training);
});

trainingsList.sort((a, b) => {
return Date.parse(a) - Date.parse(b);
});

return trainingsList;
}

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

tableElement.appendChild(createTableHeaderElement());

const trainingsList = getSortedTrainingsList(trainings);

const fillTableRow = TableRowFiller();

trainingsList.forEach(training => {
const [tableRowElement, tableRow] = createTableRowElement();

fillTableRow(tableRow, training);

tableElement.appendChild(tableRowElement);
});
}
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({{ trainings|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
Loading