Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
22 changes: 21 additions & 1 deletion app/mongo_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,24 @@ class Logs(MongoModel):
lineno = fields.IntegerField()

class StorageMeta(MongoModel):
used_size = fields.IntegerField()
used_size = fields.IntegerField()


class Questions(MongoModel):
session_id = fields.CharField()
text = fields.CharField()
order = fields.IntegerField(blank=True, default=0)
created_at = fields.DateTimeField(default=datetime.now(timezone.utc))
attributes = fields.DictField(blank=True, default={})
generation_metadata = fields.DictField(blank=True, default={})

class InterviewAvatars(MongoModel):
session_id = fields.CharField()
file_id = fields.ObjectIdField()

created_at = fields.DateTimeField(default=datetime.now(timezone.utc))
last_update = fields.DateTimeField(default=datetime.now(timezone.utc))

def save(self):
self.last_update = datetime.now(timezone.utc)
return super().save()
98 changes: 97 additions & 1 deletion app/mongo_odm.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
RecognizedAudioToProcess,
RecognizedPresentationsToProcess, Sessions,
TaskAttempts, TaskAttemptsToPassBack, Tasks,
Trainings, TrainingsToProcess, StorageMeta)
Trainings, TrainingsToProcess, StorageMeta, InterviewAvatars, Questions)
from app.status import (AudioStatus, PassBackStatus, PresentationStatus,
TrainingStatus)
from app.utils import remove_blank_and_none
Expand Down Expand Up @@ -119,6 +119,28 @@ def recalculate_used_storage_data(self):
total_size += file_doc['length']
self.set_used_storage_size(total_size)
logger.info(f"Storage size recalculated: {total_size/BYTES_PER_MB:.2f} MB")

def delete_file(self, file_id):
try:
oid = ObjectId(file_id)
except InvalidId as e:
logger.warning('Invalid file_id = {}: {}.'.format(file_id, e))
return

db = _get_db()
file_doc = db.fs.files.find_one({'_id': oid})
if not file_doc:
logger.warning('No file doc for file_id = {}.'.format(file_id))
return

length = file_doc.get('length', 0)
try:
self.storage.delete(oid)
except (NoFile, ValidationError) as e:
logger.warning('Error deleting file_id = {}: {}.'.format(file_id, e))
return

self.update_storage_size(-length)


class TrainingsDBManager:
Expand Down Expand Up @@ -929,3 +951,77 @@ def get_criterion_pack_by_name(self, name):

def get_all_criterion_packs(self):
return CriterionPack.objects.all().order_by([("name", pymongo.ASCENDING)])

class QuestionsDBManager:

def add_question(self, session_id: str, text: str):
question = Questions(session_id=session_id, text=text)
return question.save()

def get_questions_by_session(self, session_id: str):
return Questions.objects.raw({"session_id": session_id})

def remove_question(self, question_id):
return Questions.objects.get({"_id": question_id}).delete()

def get_all(self):
return Questions.objects.all()

class InterviewAvatarsDBManager:
def __new__(cls):
if not hasattr(cls, 'init_done'):
cls.instance = super(InterviewAvatarsDBManager, cls).__new__(cls)
connect(Config.c.mongodb.url + Config.c.mongodb.database_name)
cls.init_done = True
return cls.instance

def get_by_session_id(self, session_id: str):
try:
return InterviewAvatars.objects.get({'session_id': session_id})
except InterviewAvatars.DoesNotExist:
return None

def add_or_update_avatar(self, session_id: str, file_obj, filename: str | None = None):
storage = DBManager()
if filename is None:
filename = str(uuid.uuid4())
avatar_file_id = storage.add_file(file_obj, filename)
avatar = self.get_by_session_id(session_id)
if avatar is None:
avatar = InterviewAvatars(session_id=session_id, file_id=avatar_file_id)
else:
try:
storage.delete_file(avatar.file_id)
except Exception:
logger.warning('Failed to delete old avatar file for session_id = {}.'.format(session_id))
avatar.file_id = avatar_file_id

saved = avatar.save()
logger.info('Avatar saved for session_id = {}, file_id = {}.'.format(session_id, avatar_file_id))
return saved

def get_avatar_record(self, session_id: str) -> Union[InterviewAvatars, None]:
return self.get_by_session_id(session_id)

def get_avatar_file(self, session_id: str):
avatar = self.get_by_session_id(session_id)
if avatar is None:
logger.info('No avatar for session_id = {}.'.format(session_id))
return None

storage = DBManager()
return storage.get_file(avatar.file_id)

def delete_avatar(self, session_id: str):
avatar = self.get_by_session_id(session_id)
if avatar is None:
return

storage = DBManager()
try:
storage.delete_file(avatar.file_id)
except Exception as e:
logger.warning('Error deleting avatar file for session_id = {}: {}.'.format(session_id, e))

avatar.delete()
logger.info('Avatar deleted for session_id = {}.'.format(session_id))
117 changes: 117 additions & 0 deletions app/routes/interview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
from flask import Blueprint, render_template, session, request, Response

from app.root_logger import get_root_logger
from app.lti_session_passback.auth_checkers import check_auth
from app.mongo_odm import InterviewAvatarsDBManager

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


@routes_interview.route('/interview/', methods=['GET'])
def interview_page():
# user_session = check_auth()
# if not user_session:
# return "User session not found", 404
#
# session_id = session.get('session_id')
# if not session_id:
# return "Session id not found", 404
session_id = "hello, bro"

avatar_record = InterviewAvatarsDBManager().get_avatar_record(session_id)
has_avatar = avatar_record is not None

return render_template(
'interview.html',
has_avatar=has_avatar,
session_id=session_id
), 200


def _partial_response_file(grid_out):
file_size = getattr(grid_out, 'length', None)
if file_size is None:
grid_out.seek(0, 2) # SEEK_END
file_size = grid_out.tell()
grid_out.seek(0)

content_type = getattr(grid_out, 'content_type', None) or 'video/mp4'

range_header = request.headers.get('Range', None)
if not range_header:
def full_stream():
chunk_size = 8192
grid_out.seek(0)
while True:
chunk = grid_out.read(chunk_size)
if not chunk:
break
yield chunk

headers = {
'Content-Length': str(file_size),
'Accept-Ranges': 'bytes',
'Content-Type': content_type,
}
return Response(full_stream(), status=200, headers=headers)

try:
byte_range = range_header.strip().split('=')[1]
start_str, end_str = byte_range.split('-')
except Exception:
return Response(status=416)

try:
start = int(start_str) if start_str else 0
except ValueError:
start = 0

try:
end = int(end_str) if end_str else file_size - 1
except ValueError:
end = file_size - 1

if end >= file_size:
end = file_size - 1
if start > end:
return Response(status=416)

length = end - start + 1

def stream_range():
chunk_size = 8192
grid_out.seek(start)
remaining = length
while remaining > 0:
size = chunk_size if remaining >= chunk_size else remaining
data = grid_out.read(size)
if not data:
break
remaining -= len(data)
yield data

headers = {
'Content-Range': f'bytes {start}-{end}/{file_size}',
'Accept-Ranges': 'bytes',
'Content-Length': str(length),
'Content-Type': content_type,
}
return Response(stream_range(), status=206, headers=headers)


@routes_interview.route('/avatar_video')
def avatar_video():
user_session = check_auth()
if not user_session:
return '', 404

session_id = session.get('session_id')
if not session_id:
return '', 404

grid_out = InterviewAvatarsDBManager().get_avatar_file(session_id)
if grid_out is None:
return '', 404

return _partial_response_file(grid_out)
66 changes: 53 additions & 13 deletions app/routes/lti.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,7 @@
logger = get_root_logger()



@routes_lti.route('/lti', methods=['POST'])
def lti():
"""
Route that is an entry point for LTI.

:return: Redirects to training_greeting page, or
an empty dictionary with 404 HTTP return code if access was denied.
"""
def _handle_lti_common():
params = request.form

consumer_key = params.get('oauth_consumer_key', '')
Expand All @@ -30,25 +22,29 @@ def lti():
headers=dict(request.headers),
data=params,
url=request.url,
secret=consumer_secret
secret=consumer_secret,
)
if not check_request(request_info):
return {}, 404
return None

full_name = utils.get_person_name(params)
username = utils.get_username(params)
custom_params = utils.get_custom_params(params)
task_id = custom_params.get('task_id', '')
task_description = custom_params.get('task_description', '')
attempt_count = int(custom_params.get('attempt_count', 1))
required_points = float(custom_params.get('required_points', 0))
criteria_pack_id = CriteriaPackFactory().get_criteria_pack(custom_params.get('criteria_pack_id', '')).name
criteria_pack_id = CriteriaPackFactory().get_criteria_pack(
custom_params.get('criteria_pack_id', '')
).name
presentation_id = custom_params.get('presentation_id')
feedback_evaluator_id = int(custom_params.get('feedback_evaluator_id', 1))
role = utils.get_role(params)
params_for_passback = utils.extract_passback_params(params)
pres_formats = list(set(custom_params.get('formats', '').split(',')) & ALLOWED_EXTENSIONS) or [DEFAULT_EXTENSION]

SessionsDBManager().add_session(username, consumer_key, task_id, params_for_passback, role, pres_formats)

session['session_id'] = username
session['task_id'] = task_id
session['consumer_key'] = consumer_key
Expand All @@ -62,6 +58,50 @@ def lti():
if not PresentationFilesDBManager().get_presentation_file(presentation_id):
presentation_id = None

TasksDBManager().add_task_if_absent(task_id, task_description, attempt_count, required_points, criteria_pack_id, presentation_id)
TasksDBManager().add_task_if_absent(
task_id,
task_description,
attempt_count,
required_points,
criteria_pack_id,
presentation_id,
)

return {
"username": username,
"task_id": task_id,
"full_name": full_name,
"criteria_pack_id": criteria_pack_id,
"feedback_evaluator_id": feedback_evaluator_id,
"presentation_id": presentation_id,
"attempt_count": attempt_count,
"required_points": required_points,
}


@routes_lti.route('/lti', methods=['POST'])
def lti():
"""
Route that is an entry point for LTI (тренировки).

:return: Redirects to training_greeting page, or
an empty dictionary with 404 HTTP return code if access was denied.
"""
ctx = _handle_lti_common()
if ctx is None:
return {}, 404

return redirect(url_for('routes_trainings.view_training_greeting'))


@routes_lti.route('/lti_interview', methods=['POST'])
def lti_interview():
"""
LTI entry point для интервью.
"""
ctx = _handle_lti_common()
if ctx is None:
return {}, 404

username = ctx["username"]
return redirect(url_for('routes_interview.interview_page'), )
Loading