Skip to content

Commit 44f1b39

Browse files
Merge pull request #415 from OSLL/links_to_task_attempts_151
links to task attempt pages and pages themselves
2 parents a51cf6e + 63da1f1 commit 44f1b39

15 files changed

+245
-18
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
.venv
12
venv
23
.venv
34
.idea

app/api/task_attempts.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33
from bson import ObjectId
44
from flask import session, request, Blueprint
55

6-
from app.check_access import check_access
6+
from app.check_access import check_access, check_task_attempt_access
77
from app.lti_session_passback.auth_checkers import check_auth, is_admin
88
from app.mongo_odm import TaskAttemptsDBManager, TasksDBManager
99
from app.utils import check_arguments_are_convertible_to_object_id, check_argument_is_convertible_to_object_id
10+
from app.localisation import t
11+
from app.status import PassBackStatus
12+
from app.api.trainings import get_training
13+
14+
from app.mongo_models import TaskAttempts
1015

1116
api_task_attempts = Blueprint('api_task_attempts', __name__)
1217
logger = get_root_logger()
@@ -15,7 +20,7 @@
1520

1621
@check_arguments_are_convertible_to_object_id
1722
@api_task_attempts.route('/api/task-attempts/', methods=['GET'])
18-
def get_current_task_attempt() -> (dict, int):
23+
def get_current_task_attempt() -> tuple[dict, int]:
1924
"""
2025
Endpoint to get current task attempt information.
2126
@@ -56,10 +61,44 @@ def get_current_task_attempt() -> (dict, int):
5661
'attempt_count': task_db.attempt_count,
5762
}, 200
5863

64+
def get_task_attempt_information(task_attempt_db: TaskAttempts) -> dict:
65+
try:
66+
training_ids = task_attempt_db.training_scores.keys()
67+
trainings = dict(zip(training_ids, map(get_training, training_ids)))
68+
69+
return {
70+
'message': 'OK',
71+
'task_id': str(task_attempt_db.task_id),
72+
'username': str(task_attempt_db.username),
73+
'trainings': trainings,
74+
}
75+
except Exception as e:
76+
return {'message': '{}: {}'.format(e.__class__, e)}
77+
78+
79+
@check_arguments_are_convertible_to_object_id
80+
@api_task_attempts.route('/api/task-attempts/<task_attempt_id>/', methods=['GET'])
81+
def get_task_attempt(task_attempt_id) -> tuple[dict, int]:
82+
"""
83+
Endpoint to get information about a task attempt by its identifier.
84+
85+
:param task_attempt_id: Task attempt identifier
86+
:return: Dictionary with task attempt information and 'OK' message, or
87+
a dictionary with an explanation and 404 HTTP return code if a task attempt was not found, or
88+
an empty dictionary with 404 HTTP return code if access was denied.
89+
"""
90+
if not check_task_attempt_access(task_attempt_id):
91+
return {}, 404
92+
93+
task_attempt_db = TaskAttemptsDBManager().get_task_attempt(task_attempt_id)
94+
95+
if task_attempt_db is None:
96+
return {'message': 'No task attempt with task_attempt_id = {}.'.format(task_attempt_id)}, 404
97+
return get_task_attempt_information(task_attempt_db), 200
5998

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

app/api/trainings.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from app.audio import Audio
1212
from app.check_access import check_access
13-
from app.lti_session_passback.auth_checkers import is_admin, check_auth, check_admin
13+
from app.lti_session_passback.auth_checkers import is_admin, check_auth, check_admin, is_logged_in
1414
from app.mongo_models import Trainings
1515
from app.mongo_odm import TrainingsDBManager, TaskAttemptsDBManager, TasksDBManager, DBManager
1616
from app.filters import GetAllTrainingsFilterManager
@@ -439,8 +439,7 @@ def get_all_trainings() -> (dict, int):
439439

440440
print(number_page, count_items)
441441

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

446445
trainings = GetAllTrainingsFilterManager().query_with_filters(filters, number_page, count_items)

app/check_access.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from flask import session
22

3-
from app.mongo_odm import SessionsDBManager, TrainingsDBManager
3+
from app.mongo_odm import SessionsDBManager, TrainingsDBManager, TaskAttemptsDBManager
44
from app.utils import is_testing_active
55

66

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

2626
def _check_access_testing(filters: dict) -> bool:
2727
return True
28+
29+
def _check_task_attempt_access(task_attempt_id: str) -> bool:
30+
username = session.get('session_id', default=None)
31+
consumer_key = session.get('consumer_key', default=None)
32+
user_session = SessionsDBManager().get_session(username, consumer_key)
33+
34+
if not user_session:
35+
return False
36+
if user_session.is_admin:
37+
return True
38+
39+
task_attempt = TaskAttemptsDBManager().get_task_attempt(task_attempt_id)
40+
return task_attempt.username == username
41+
42+
def check_task_attempt_access(task_attempt_id: str) -> bool:
43+
if not is_testing_active():
44+
return _check_task_attempt_access(task_attempt_id)
45+
else:
46+
return _check_task_attempt_access_testing(task_attempt_id)
47+
48+
def _check_task_attempt_access_testing(task_attempt_id: str) -> bool:
49+
return True

app/feedback_evaluator.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,14 +162,14 @@ def __init__(self, weights=None):
162162

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

165-
def find_strict_speech_duration_criterion(self, criteria_keys, suffix='StrictSpeechDurationCriterion'):
165+
def rework_strict_speech_duration_criterion(self, criteria_keys, suffix='StrictSpeechDurationCriterion'):
166166
for criteria in criteria_keys:
167-
if suffix in criteria:
167+
if criteria.endswith(suffix):
168168
self.weights[criteria] = self.weights.pop('StrictSpeechDurationCriterion')
169169
return criteria
170170

171171
def evaluate_feedback(self, criteria_results):
172-
self.ssd_criterion = self.find_strict_speech_duration_criterion(criteria_results.keys())
172+
self.ssd_criterion = self.rework_strict_speech_duration_criterion(criteria_results.keys())
173173
if not criteria_results.get(self.ssd_criterion) or \
174174
criteria_results[self.ssd_criterion].result == 0:
175175
return Feedback(0)
@@ -180,11 +180,12 @@ def evaluate_feedback(self, criteria_results):
180180

181181
def get_result_as_sum_str(self, criteria_results):
182182
if not self.ssd_criterion:
183-
self.ssd_criterion = self.find_strict_speech_duration_criterion(criteria_results.keys())
183+
self.ssd_criterion = self.rework_strict_speech_duration_criterion(criteria_results.keys())
184184
if criteria_results is None or self.weights is None or \
185185
criteria_results.get(self.ssd_criterion, {}).get('result', 0) == 0 or \
186186
criteria_results.get("DEFAULT_SPEECH_PACE_CRITERION", {}).get('result', 0) == 0:
187187
return None
188+
188189
return super().get_result_as_sum_str(criteria_results)
189190

190191

app/locale/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,6 @@
105105
"Применить": "Apply",
106106
"Отмена": "Cancel",
107107
"Активные фильтры отсутствуют": "There are no active filters",
108-
"Некоторые фильтры (выделенные красным) установлены неправильным образом. Прочитайте описание фильтра, наведясь на его подчеркнутое название, и проверьте введенные данные.": "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.",
108+
"Некоторые фильтры (выделенные красным) установлены неправильным образом. Прочитайте описание фильтра, наведясь на его подчеркнутое название, и проверьте введенные данные.": "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."
109109

110110
}

app/routes/task_attempts.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from app.root_logger import get_root_logger
2+
3+
from bson import ObjectId
4+
from flask import Blueprint, render_template, jsonify, request, session
5+
from app.localisation import *
6+
7+
from app.api.task_attempts import get_task_attempt
8+
from app.check_access import check_access, check_task_attempt_access
9+
from app.lti_session_passback.auth_checkers import check_admin, check_auth
10+
from app.utils import check_arguments_are_convertible_to_object_id
11+
12+
routes_task_attempts = Blueprint('routes_task_attempts', __name__)
13+
logger = get_root_logger()
14+
15+
16+
@check_arguments_are_convertible_to_object_id
17+
@routes_task_attempts.route('/task_attempts/<task_attempt_id>/', methods=['GET'])
18+
def view_task_attempt(task_attempt_id: str):
19+
"""
20+
Route to view page with task attempt.
21+
22+
:param task_attempt_id: Task attempt identifier
23+
:return: Page or an empty dictionary with 404 HTTP code if access was denied.
24+
"""
25+
26+
if not check_task_attempt_access(task_attempt_id):
27+
return {}, 404
28+
29+
task_attempt, task_attempt_status_code = get_task_attempt(task_attempt_id)
30+
31+
print(task_attempt)
32+
33+
if task_attempt.get('message') != 'OK':
34+
return task_attempt, task_attempt_status_code
35+
36+
return render_template(
37+
'task_attempt.html',
38+
task_attempt_id=task_attempt_id,
39+
task_id=task_attempt['task_id'],
40+
username=task_attempt['username'],
41+
trainings=task_attempt['trainings'],
42+
), 200

app/routes/trainings.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from app.check_access import check_access
1313
from app.criteria_pack import CriteriaPackFactory
1414
from app.feedback_evaluator import FeedbackEvaluatorFactory
15-
from app.lti_session_passback.auth_checkers import check_admin, check_auth
15+
from app.lti_session_passback.auth_checkers import check_admin, check_auth, is_logged_in
1616
from app.mongo_odm import CriterionPackDBManager, TasksDBManager, TaskAttemptsDBManager
1717
from app.status import TrainingStatus, AudioStatus, PresentationStatus
1818
from app.utils import check_arguments_are_convertible_to_object_id
@@ -141,8 +141,7 @@ def view_all_trainings():
141141
except:
142142
pass
143143

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

148147
raw_filters = request.args.getlist('f')

app/static/css/task_attempt.css

Whitespace-only changes.

app/static/js/show_all_trainings.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ function buildCurrentTrainingRow(trainingId, trainingJson, is_Admin=false) {
2121

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

app/static/js/task_attempts.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
const tableColomnHeaders = [
2+
"№", "training_id", "start_timestamp", "training_status", "pass_back_status", "score"
3+
];
4+
5+
function get_time_string(timestampStr){
6+
const timestamp = Date.parse(timestampStr);
7+
8+
options = { weekday: 'short', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: '2-digit', second: '2-digit', timeZoneName: 'short' }
9+
if (!isNaN(timestamp)) {
10+
processing_time = new Date(timestamp);
11+
return processing_time.toLocaleString("ru-RU", options);
12+
} else {
13+
return "";
14+
}
15+
}
16+
17+
function createTableHeaderElement() {
18+
const tableHeaderElement = document.createElement("tr");
19+
20+
tableColomnHeaders.forEach(colomnHeader => {
21+
const tableColomnHeaderElement = document.createElement("th");
22+
tableColomnHeaderElement.innerHTML = colomnHeader;
23+
24+
tableHeaderElement.appendChild(tableColomnHeaderElement);
25+
});
26+
27+
return tableHeaderElement;
28+
}
29+
30+
function createTableRowElement() {
31+
const tableRowElement = document.createElement("tr");
32+
33+
const tableRow = {};
34+
35+
tableColomnHeaders.forEach(columnHeader => {
36+
const tableCellElement = document.createElement("td");
37+
38+
tableRow[columnHeader] = tableCellElement;
39+
tableRowElement.appendChild(tableCellElement);
40+
});
41+
42+
return [tableRowElement, tableRow];
43+
}
44+
45+
function TableRowFiller() {
46+
let pos = 1;
47+
48+
function fillTableRow(tableRow, training) {
49+
tableRow["№"].innerHTML = pos++;
50+
tableRow["training_id"].innerHTML = `<a href="/trainings/statistics/${training.id}/">${training.id}</a>`;
51+
tableRow["start_timestamp"].innerHTML = get_time_string(training.training_start_timestamp);
52+
tableRow["score"].innerHTML = training.score ? training.score.toFixed(2) : "none";
53+
tableRow["training_status"].innerHTML = training.training_status;
54+
tableRow["pass_back_status"].innerHTML = training.passedBackStatus || "none";
55+
}
56+
57+
return fillTableRow;
58+
}
59+
60+
function getSortedTrainingsList(trainings) {
61+
const trainingsList = [];
62+
63+
Object.keys(trainings).forEach(trainingId => {
64+
const training = trainings[trainingId];
65+
training["id"] = trainingId;
66+
67+
trainingsList.push(training);
68+
});
69+
70+
trainingsList.sort((a, b) => {
71+
return Date.parse(a) - Date.parse(b);
72+
});
73+
74+
return trainingsList;
75+
}
76+
77+
function showRelatedTrainingsTable(trainings) {
78+
const tableElement = document.getElementById("related-trainings-table");
79+
80+
tableElement.appendChild(createTableHeaderElement());
81+
82+
const trainingsList = getSortedTrainingsList(trainings);
83+
84+
const fillTableRow = TableRowFiller();
85+
86+
trainingsList.forEach(training => {
87+
const [tableRowElement, tableRow] = createTableRowElement();
88+
89+
fillTableRow(tableRow, training);
90+
91+
tableElement.appendChild(tableRowElement);
92+
});
93+
}

app/static/js/training_statistics.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ function buildCurrentAttemptStatistics() {
55
.then(response => {
66
let trainingNumber = response["training_number"];
77
let attemptCount = response["attempt_count"];
8-
if (response === {} || trainingNumber === attemptCount) {
8+
if (jQuery.isEmptyObject(response) || trainingNumber === attemptCount) {
99
return;
1010
}
1111
document.getElementById("training-number").textContent

app/templates/task_attempt.html

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{% extends 'base.html' %}
2+
3+
{% block header %}
4+
<link rel="stylesheet" href="{{ url_for('static', filename='css/task_attempt.css') }}">
5+
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
6+
<link rel="stylesheet" href="{{ url_for('static', filename='css/libraries/jquery-ui/jquery-ui.min.css') }}">
7+
8+
{% endblock %}
9+
10+
{% block content %}
11+
<div class="base-container">
12+
<h3>{{ title }}</h3>
13+
<h3>Информация по попытке с ID {{ task_attempt_id }}</h3>
14+
<h3>task_id: {{ task_id }}</h3>
15+
<h3>Имя пользователя: {{ username }}</h3>
16+
<div class="related-trainings-container">
17+
<h3>Связанные тренировки:</h3>
18+
<table id="related-trainings-table" class="table center-align-table"></table>
19+
</div>
20+
</div>
21+
<script src="{{ url_for('static', filename='js/libraries/jquery.min.js') }}"></script>
22+
<script src="{{ url_for('static', filename='js/task_attempts.js') }}"></script>
23+
<script type="text/javascript">
24+
showRelatedTrainingsTable({{ trainings|tojson|safe }});
25+
</script>
26+
{% endblock %}

app/web_speech_trainer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from app.routes.lti import routes_lti
3030
from app.routes.presentations import routes_presentations
3131
from app.routes.trainings import routes_trainings
32+
from app.routes.task_attempts import routes_task_attempts
3233
from app.routes.version import routes_version
3334
from app.status import PassBackStatus, TrainingStatus
3435
from app.training_manager import TrainingManager
@@ -52,6 +53,7 @@
5253
app.register_blueprint(routes_lti)
5354
app.register_blueprint(routes_presentations)
5455
app.register_blueprint(routes_trainings)
56+
app.register_blueprint(routes_task_attempts)
5557
app.register_blueprint(routes_version)
5658

5759
logger = get_root_logger(service_name='web')

0 commit comments

Comments
 (0)