diff --git a/.env.sample b/.env.sample
new file mode 100644
index 0000000..3083a2a
--- /dev/null
+++ b/.env.sample
@@ -0,0 +1,32 @@
+# FLASK_ENV="development"
+# FLASK_DEBUG="1"
+# LEARNERS_CONFIG="config.yml"
+# MAIL="True"
+# REMOVE_DB="True"
+# TESTING="False"
+# PROPAGATE_EXCEPTIONS="None"
+# PRESERVE_CONTEXT_ON_EXCEPTION="None"
+# SECRET_KEY="None"
+# PERMANENT_SESSION_LIFETIME="datetime.timedelta(days=31)"
+# USE_X_SENDFILE="False"
+# SERVER_NAME="None"
+# APPLICATION_ROOT="/"
+# SESSION_COOKIE_NAME="session"
+# SESSION_COOKIE_DOMAIN="None"
+# SESSION_COOKIE_PATH="None"
+# SESSION_COOKIE_HTTPONLY="True"
+# SESSION_COOKIE_SECURE="False"
+# SESSION_COOKIE_SAMESITE="None"
+# SESSION_REFRESH_EACH_REQUEST="True"
+# MAX_CONTENT_LENGTH="None"
+# SEND_FILE_MAX_AGE_DEFAULT="None"
+# TRAP_BAD_REQUEST_ERRORS="None"
+# TRAP_HTTP_EXCEPTIONS="False"
+# EXPLAIN_TEMPLATE_LOADING="False"
+# PREFERRED_URL_SCHEME="http"
+# JSON_AS_ASCII="True"
+# JSON_SORT_KEYS="True"
+# JSONIFY_PRETTYPRINT_REGULAR="False"
+# JSONIFY_MIMETYPE="application/json"
+# TEMPLATES_AUTO_RELOAD="None"
+# MAX_COOKIE_SIZE="4093"
diff --git a/.flaskenv b/.flaskenv
new file mode 100644
index 0000000..1499678
--- /dev/null
+++ b/.flaskenv
@@ -0,0 +1 @@
+FLASK_APP="backend"
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
new file mode 100644
index 0000000..8dd1e05
--- /dev/null
+++ b/.github/workflows/build.yaml
@@ -0,0 +1,84 @@
+name: build
+
+on:
+ push:
+ tags:
+ - '**'
+
+env:
+ LEARNERS_VERSION: ${{ github.ref_name }}
+
+jobs:
+ lint:
+ name: lint & check formatting
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ['3.8', '3.10']
+ steps:
+ - uses: actions/checkout@v3
+ - name: Install dependencies
+ run: |
+ sudo apt update -y
+ sudo apt install python3-venv -y
+ sudo apt purge python3-blinker -y
+ curl https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py
+ python /tmp/get-pip.py
+ python -m pip install --upgrade pip setuptools wheel
+ python -m pip install .
+ - name: Lint with flake8
+ run: |
+ flake8 backend --count --exit-zero --max-complexity=10 --max-line-length=142 --statistics
+ - name: lint with black
+ uses: rickstaa/action-black@v1
+ with:
+ black_args: 'backend --check'
+ build_wheel:
+ name: build wheel
+ runs-on: ubuntu-latest
+ needs: lint
+ steps:
+ - uses: actions/checkout@v3
+ - name: build
+ run: |
+ sudo apt update -y
+ sudo apt install python3-venv -y
+ sudo apt purge python3-blinker -y
+ curl https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py
+ python /tmp/get-pip.py
+ python -m pip install --upgrade pip setuptools wheel build
+ python -m build
+ - name: Release to GitHub
+ uses: softprops/action-gh-release@v1
+ if: startsWith(github.ref, 'refs/tags/')
+ with:
+ files: |
+ dist/*.whl
+ dist/*.tar.gz
+ # - name: Release to PyPI
+ # uses: pypa/gh-action-pypi-publish@release/v1
+ # with:
+ # password: ${{ secrets.PYPI_API_TOKEN }}
+
+ build_container:
+ name: build container
+ needs: build_wheel
+ runs-on: ubuntu-latest
+ steps:
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v2
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - name: get ghcr owner repository
+ run: |
+ echo "GHCR_OWNER=${GITHUB_REPOSITORY_OWNER,,}" >>${GITHUB_ENV}
+ - name: Build and push image
+ uses: docker/build-push-action@v3
+ with:
+ file: Containerfile
+ push: true
+ tags: |
+ ghcr.io/${{ env.GHCR_OWNER }}/learners:latest
+ ghcr.io/${{ env.GHCR_OWNER }}/learners:${{ github.ref_name }}
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
new file mode 100644
index 0000000..b640a8a
--- /dev/null
+++ b/.github/workflows/lint.yaml
@@ -0,0 +1,34 @@
+name: lint
+
+on:
+ push:
+ paths:
+ - 'learners/**'
+ branches:
+ - master
+ pull_request:
+jobs:
+ lint:
+ name: lint & check formatting
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ['3.8', '3.10']
+ steps:
+ - uses: actions/checkout@v3
+ - name: Install dependencies
+ run: |
+ sudo apt update -y
+ sudo apt install python3-venv -y
+ sudo apt purge python3-blinker -y
+ curl https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py
+ python /tmp/get-pip.py
+ python -m pip install --upgrade pip setuptools wheel
+ python -m pip install .
+ - name: Lint with flake8
+ run: |
+ flake8 backend --count --exit-zero --max-complexity=10 --max-line-length=142 --statistics
+ - name: lint with black
+ uses: rickstaa/action-black@v1
+ with:
+ black_args: 'backend --check'
diff --git a/.gitignore b/.gitignore
index 496ee2c..3f33d2a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,151 @@
-.DS_Store
\ No newline at end of file
+*.retry
+.idea/
+.envrc
+backend/data.db
+develop_config.yml
+backend/statics/gen
+backend/statics/hugo
+backend/statics/uploads/*
+backend/statics/documentation
+backend/statics/exercises
+rendered_hugo_content/
+frontend/node_modules/
+frontend/node_modules/
+frontend/public
+frontend/dist
+frontend/.DS_Store
+public
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# DOCKER
+docker/backend/__data/data.db
+docker/backend/__templates/*
+docker/backend/__webroot/*
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+.db
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+
+# Flask
+.DS_Store
+.env
+*.pyc
+*.pyo
+env/
+venv/
+.venv/
+env*
+dist/
+build/
+*.egg
+*.egg-info/
+_mailinglist
+.tox/
+.cache/
+.pytest_cache/
+.idea/
+docs/_build/
+.vscode
+
+# Coverage reports
+htmlcov/
+.coverage
+.coverage.*
+*,cover
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..6d6be1d
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,12 @@
+
+repos:
+ - repo: https://github.com/psf/black
+ rev: 22.3.0
+ hooks:
+ - id: black
+
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.1.0
+ hooks:
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
diff --git a/backend/__init__.py b/backend/__init__.py
index b3aea44..c01a1ff 100644
--- a/backend/__init__.py
+++ b/backend/__init__.py
@@ -44,6 +44,7 @@ def main():
app.register_blueprint(routes.stream_api)
app.register_blueprint(routes.questionnaires_api)
app.register_blueprint(routes.executions_api)
+ app.register_blueprint(routes.results_api)
app.register_blueprint(routes.comments_api)
app.register_blueprint(routes.cache_api)
app.register_blueprint(routes.callback_api)
diff --git a/backend/classes/SubmissionResponse.py b/backend/classes/SubmissionResponse.py
index 97b850c..ab1dcc4 100644
--- a/backend/classes/SubmissionResponse.py
+++ b/backend/classes/SubmissionResponse.py
@@ -1,5 +1,6 @@
import json
import string
+from backend.functions.helpers import extract_history
class SubmissionResponse:
@@ -7,55 +8,50 @@ def __init__(
self,
completed: bool = False,
executed: bool = False,
- msg: string = "",
+ status_msg: string = "",
response_timestamp: string = "",
- connection_failed: bool = False,
history: list = [],
partial: bool = False,
exercise_type: string = "",
filename: string = "",
uuid: string = "",
+ script_response: string = "",
):
self.completed = completed
self.executed = executed
- self.msg = msg
+ self.status_msg = status_msg
self.response_timestamp = response_timestamp
- self.connection_failed = connection_failed
self.history = history
self.partial = partial
self.exercise_type = exercise_type
self.filename = filename
self.uuid = uuid
+ self.script_response = script_response
def update(self, executions) -> dict:
if not len(executions):
return
if isinstance(executions, list):
- from backend.functions.helpers import extract_history
-
last_execution = executions[0]
self.history = extract_history(executions)
else:
last_execution = executions
- self.msg = last_execution.get("msg")
+ self.status_msg = last_execution.get("status_msg")
self.response_timestamp = last_execution.get("response_timestamp")
- self.connection_failed = last_execution.get("connection_failed")
self.partial = last_execution.get("partial")
self.completed = last_execution.get("completed")
+ self.executed = last_execution.get("executed")
self.exercise_type = last_execution.get("exercise_type")
-
- if self.connection_failed:
- self.executed = False
- self.msg = "Connection failed."
+ self.script_response = last_execution.get("script_response")
if self.response_timestamp:
if self.exercise_type == "form":
self.executed = True
else:
- # Get error msg
+ # Get error status_msg
error = json.loads(last_execution.get("response_content")).get("stderr")
self.executed = bool(not error)
- # Apply error msg to msg if none given
- self.msg = self.msg or error
+ # Apply error status_msg to status_msg if none given
+ self.status_msg = self.status_msg or error
diff --git a/backend/conf/db_models.py b/backend/conf/db_models.py
index 0f39c98..11ba541 100644
--- a/backend/conf/db_models.py
+++ b/backend/conf/db_models.py
@@ -8,7 +8,7 @@ class User(db.Model):
role = db.Column(db.String(20), unique=False, nullable=False, default="participant")
admin = db.Column(db.Integer, nullable=False, default=0)
meta = db.Column(db.String(), unique=False, nullable=True)
- executions = db.relationship("Execution", backref="user", lazy=True)
+ submission = db.relationship("Submission", backref="user", lazy=True)
usergroups = db.relationship("UsergroupAssociation", back_populates="user")
@@ -34,45 +34,6 @@ class UsergroupAssociation(db.Model):
user = db.relationship("User", back_populates="usergroups")
-class Execution(db.Model):
- id = db.Column(db.Integer, primary_key=True)
- exercise_type = db.Column(db.String(120), nullable=False)
- script = db.Column(db.String(120), nullable=True)
- execution_timestamp = db.Column(db.DateTime, nullable=False, default=func.current_timestamp())
- response_timestamp = db.Column(db.DateTime, nullable=True)
- response_content = db.Column(db.Text, nullable=True)
- form_data = db.Column(db.String(), nullable=True)
- msg = db.Column(db.String(240), nullable=True)
- execution_uuid = db.Column(db.String(120), unique=True, nullable=True)
- completed = db.Column(db.Integer, nullable=False, default=0)
- partial = db.Column(db.Integer, nullable=False, default=0)
- connection_failed = db.Column(db.Integer, nullable=False, default=0)
- user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
- exercise_id = db.Column(db.Integer, db.ForeignKey("exercise.id"), nullable=False)
-
-
-class Attachment(db.Model):
- id = db.Column(db.Integer, primary_key=True)
- filename = db.Column(db.String(120), nullable=False)
- filename_hash = db.Column(db.String(120), nullable=False)
- user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
-
-
-class Exercise(db.Model):
- id = db.Column(db.Integer, primary_key=True)
- global_exercise_id = db.Column(db.String(32), nullable=False)
- local_exercise_id = db.Column(db.Integer, nullable=False)
- exercise_type = db.Column(db.String(120), nullable=False)
- exercise_name = db.Column(db.String(120), nullable=False)
- page_title = db.Column(db.String(120), nullable=False)
- parent_page_title = db.Column(db.String(120), nullable=False)
- root_weight = db.Column(db.Integer, nullable=False)
- parent_weight = db.Column(db.Integer, nullable=False)
- child_weight = db.Column(db.Integer, nullable=False)
- order_weight = db.Column(db.Integer, nullable=False)
- executions = db.relationship("Execution", backref="exercise", lazy=True)
-
-
parent_child_relationship = db.Table(
"parent_child_relationship",
db.Column("parent_id", db.Integer, db.ForeignKey("page.id"), primary_key=True),
@@ -102,7 +63,7 @@ class Page(db.Model):
class Cache(db.Model):
user_id = db.Column(db.ForeignKey("user.id"), primary_key=True)
- global_exercise_id = db.Column(db.ForeignKey("exercise.global_exercise_id"), primary_key=True)
+ exercise_id = db.Column(db.ForeignKey("exercise.id"), primary_key=True)
form_data = db.Column(db.String(), nullable=True)
@@ -114,8 +75,7 @@ class Comment(db.Model):
class Questionnaire(db.Model):
- id = db.Column(db.Integer, primary_key=True)
- global_questionnaire_id = db.Column(db.String(32), nullable=False)
+ id = db.Column(db.String(32), primary_key=True)
page_title = db.Column(db.String(120), nullable=False)
parent_page_title = db.Column(db.String(120), nullable=False)
root_weight = db.Column(db.Integer, nullable=False)
@@ -126,14 +86,13 @@ class Questionnaire(db.Model):
class QuestionnaireQuestion(db.Model):
- global_question_id = db.Column(db.String(32), primary_key=True)
- id = db.Column(db.Integer, nullable=False)
+ id = db.Column(db.String(32), primary_key=True)
question = db.Column(db.String(), nullable=False)
answer_options = db.Column(db.String(), nullable=False)
language = db.Column(db.String(), nullable=False, primary_key=True)
multiple = db.Column(db.Integer, nullable=False, default=1)
active = db.Column(db.Integer, nullable=False, default=0)
- global_questionnaire_id = db.Column(db.String(), db.ForeignKey("questionnaire.global_questionnaire_id"), primary_key=True)
+ questionnaire_id = db.Column(db.String(), db.ForeignKey("questionnaire.id"), primary_key=True)
class QuestionnaireAnswer(db.Model):
@@ -141,7 +100,7 @@ class QuestionnaireAnswer(db.Model):
answers = db.Column(db.String(), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
timestamp = db.Column(db.DateTime, nullable=False, default=func.current_timestamp())
- global_question_id = db.Column(db.Integer, db.ForeignKey("questionnaire_question.global_question_id"), nullable=False)
+ question_id = db.Column(db.Integer, db.ForeignKey("questionnaire_question.id"), nullable=False)
class TokenBlocklist(db.Model):
@@ -150,15 +109,53 @@ class TokenBlocklist(db.Model):
created_at = db.Column(db.DateTime, nullable=False)
-class VenjixExecution(db.Model):
+class Exercise(db.Model):
+ id = db.Column(db.String(32), primary_key=True)
+ local_exercise_id = db.Column(db.Integer, nullable=False)
+ exercise_type = db.Column(db.String(120), nullable=False)
+ exercise_name = db.Column(db.String(120), nullable=False)
+ page_title = db.Column(db.String(120), nullable=False)
+ parent_page_title = db.Column(db.String(120), nullable=False)
+ root_weight = db.Column(db.Integer, nullable=False)
+ parent_weight = db.Column(db.Integer, nullable=False)
+ child_weight = db.Column(db.Integer, nullable=False)
+ order_weight = db.Column(db.Integer, nullable=False)
+ script_name = db.Column(db.String(120), nullable=True)
+ submissions = db.relationship("Submission", backref="exercise", lazy=True)
+
+
+class Attachment(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ filename = db.Column(db.String(120), nullable=False)
+ filename_hash = db.Column(db.String(120), nullable=False)
+ user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
+
+
+class Submission(db.Model):
+ # General Fields
id = db.Column(db.Integer, primary_key=True)
- script = db.Column(db.String(120), nullable=True)
+ exercise_type = db.Column(db.String(120), nullable=False)
execution_timestamp = db.Column(db.DateTime, nullable=False, default=func.current_timestamp())
- response_timestamp = db.Column(db.DateTime, nullable=True)
- response_content = db.Column(db.Text, nullable=True)
- msg = db.Column(db.String(240), nullable=True)
- execution_uuid = db.Column(db.String(120), unique=True, nullable=True)
completed = db.Column(db.Integer, nullable=False, default=0)
+ executed = db.Column(db.Integer, nullable=False, default=0)
partial = db.Column(db.Integer, nullable=False, default=0)
- connection_failed = db.Column(db.Integer, nullable=False, default=0)
+ status_msg = db.Column(db.String(240), nullable=True)
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
+ exercise_id = db.Column(db.String(32), db.ForeignKey("exercise.id"), nullable=False)
+
+ # Form Submission
+ form_data = db.Column(db.String(), nullable=True)
+
+ # Script Submission
+ execution_uuid = db.Column(db.String(120), unique=True, nullable=True)
+ response_timestamp = db.Column(db.DateTime, nullable=True)
+ response_content = db.Column(db.Text, nullable=True)
+ script_response = db.Column(db.Text, nullable=True)
+
+
+class Timetracker(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ start_time = db.Column(db.String(120), nullable=True)
+ pause_time = db.Column(db.String(120), nullable=True)
+ offset = db.Column(db.Integer, nullable=False, default=0)
+ running = db.Column(db.Integer, nullable=False, default=0)
diff --git a/backend/functions/database.py b/backend/functions/database.py
index 956912a..37e61f0 100644
--- a/backend/functions/database.py
+++ b/backend/functions/database.py
@@ -1,6 +1,6 @@
import hashlib
import json
-from datetime import datetime, timezone
+import datetime
import string
from typing import Tuple
from backend.classes.SSE import SSE_Event
@@ -10,10 +10,10 @@
from backend.conf.db_models import (
Attachment,
Cache,
- Execution,
Exercise,
Notification,
QuestionnaireAnswer,
+ Submission,
User,
Page,
Comment,
@@ -21,7 +21,7 @@
QuestionnaireQuestion,
Usergroup,
UsergroupAssociation,
- VenjixExecution,
+ Timetracker,
)
from backend.database import db
from backend.functions.helpers import convert_to_dict, extract_json_content
@@ -108,7 +108,8 @@ def db_insert_exercises(app, *args, **kwargs):
exercise_json = f"{cfg.statics.get('base_url')}/hugo/exercises.json"
exercises = extract_json_content(app, exercise_json)
for exercise in exercises:
- db_create_or_update(Exercise, ["global_exercise_id"], exercise)
+ exercise["id"] = exercise.pop("global_exercise_id")
+ db_create_or_update(Exercise, ["id"], exercise)
def db_insert_questionnaires(app, *args, **kwargs):
@@ -116,7 +117,7 @@ def db_insert_questionnaires(app, *args, **kwargs):
questionnaires = extract_json_content(app, questionnaire_json)
for questionnaire in questionnaires:
new_questionnaire = {
- "global_questionnaire_id": questionnaire["global_questionnaire_id"],
+ "id": questionnaire["global_questionnaire_id"],
"page_title": questionnaire["page_title"],
"parent_page_title": questionnaire["parent_page_title"],
"root_weight": questionnaire["root_weight"],
@@ -125,22 +126,21 @@ def db_insert_questionnaires(app, *args, **kwargs):
"order_weight": questionnaire["order_weight"],
}
- db_create_or_update(Questionnaire, ["global_questionnaire_id"], new_questionnaire)
+ db_create_or_update(Questionnaire, ["id"], new_questionnaire)
db.session.flush()
for language in questionnaire["questions"]:
for question in questionnaire["questions"][language]:
new_question = {
- "global_question_id": question["global_question_id"],
- "id": question["id"],
+ "id": question["global_question_id"],
"question": question["question"],
"answer_options": json.dumps(question["answers"]),
"language": language,
"multiple": question.get("multiple") or False,
- "global_questionnaire_id": questionnaire["global_questionnaire_id"],
+ "questionnaire_id": questionnaire["global_questionnaire_id"],
}
- db_create_or_update(QuestionnaireQuestion, ["global_question_id", "language"], new_question)
+ db_create_or_update(QuestionnaireQuestion, ["id", "language"], new_question)
def db_create_or_update(db_model, filter_keys: list = [], passed_element: dict = None, nolog: bool = False) -> bool:
@@ -177,37 +177,9 @@ def db_create_or_update(db_model, filter_keys: list = [], passed_element: dict =
return True
-def db_create_venjix_execution(execution_uuid: str, user_id: int, script_name: str) -> bool:
+def db_update_venjix_execution(updates) -> bool:
try:
- execution = VenjixExecution(
- script=script_name,
- execution_uuid=execution_uuid,
- user_id=user_id,
- )
-
- db.session.add(execution)
- db.session.commit()
- return True
-
- except Exception as e:
- logger.exception(e)
-
-
-def db_update_venjix_execution(
- execution_uuid: str,
- connection_failed: bool = False,
- response_timestamp: str = None,
- response_content: str = None,
- completed: bool = False,
- msg: str = None,
- partial: bool = False,
-) -> bool:
- try:
- execution = VenjixExecution.query.filter_by(execution_uuid=execution_uuid).first()
- for key, value in list(locals().items())[:-1]:
- if value:
- setattr(execution, key, value)
- db.session.commit()
+ db_create_or_update(Submission, ["execution_uuid"], updates, nolog=True)
return True
except Exception as e:
@@ -218,11 +190,11 @@ def db_update_venjix_execution(
def db_get_running_executions_by_name(user_id: int, script: str) -> dict:
try:
running_executions = (
- VenjixExecution.query.filter_by(user_id=user_id)
+ Submission.query.filter_by(user_id=user_id)
.filter_by(script=script)
- .filter_by(connection_failed=False)
+ .filter_by(executed=True)
.filter_by(response_timestamp=None)
- .order_by(VenjixExecution.execution_timestamp.desc())
+ .order_by(Submission.execution_timestamp.desc())
.all()
)
return convert_to_dict(running_executions)
@@ -232,42 +204,39 @@ def db_get_running_executions_by_name(user_id: int, script: str) -> dict:
return None
-def db_get_venjix_execution(execution_uuid: str) -> dict:
- try:
- execution = generic_getter(VenjixExecution, "execution_uuid", execution_uuid)
- return convert_to_dict(execution)
+def db_get_submission_by_execution_uuid(execution_uuid: str) -> dict:
+ return generic_getter(Submission, "execution_uuid", execution_uuid)
- except Exception as e:
- logger.exception(e)
- return None
+def db_get_submission_by_exercise_id(exercise_id: str) -> dict:
+ return generic_getter(Submission, "exercise_id", exercise_id, all=True)
-def db_create_execution(exercise_type: str, global_exercise_id: str, data: dict, user_id: int, execution_uuid: str) -> bool:
- form_data = json.dumps(data, indent=4, sort_keys=False)
+def db_create_submission(exercise_type: str, exercise_id: str, user_id: int, data: dict = None, execution_uuid: str = None) -> bool:
try:
- exercise_id = db_get_exercise_by_global_exercise_id(global_exercise_id).id
-
- execution = Execution(
+ submission = Submission(
exercise_type=exercise_type,
- script="script",
- form_data=form_data,
- execution_uuid=execution_uuid,
user_id=user_id,
exercise_id=exercise_id,
)
if exercise_type == "form":
- execution.completed = True
- execution.response_timestamp = datetime.now(timezone.utc)
+ form_data = json.dumps(data, indent=4, sort_keys=False)
+ submission.form_data = form_data
+ submission.completed = True
+ submission.executed = True
+
+ if exercise_type == "script":
+ submission.execution_uuid = execution_uuid
- db.session.add(execution)
+ db.session.add(submission)
db.session.commit()
- return True
+ return submission
except Exception as e:
- logger.exception(e)
- return False
+ logger.error(f"Error creating submission: {e}")
+ db.session.rollback()
+ return None
def db_create_comment(comment: str, page: str, user_id: int) -> bool:
@@ -285,18 +254,84 @@ def db_create_comment(comment: str, page: str, user_id: int) -> bool:
return False
-def db_get_current_submissions(user_id: int, global_exercise_id: string) -> Tuple[dict, dict]:
+def db_set_time(action: str, offset: int = 0) -> bool:
+ try:
+ updated_time = {
+ "id": 0,
+ }
+
+ if action == "start":
+ updated_time["start_time"] = datetime.datetime.now(datetime.timezone.utc)
+ updated_time["running"] = True
+
+ if action == "pause":
+ updated_time["pause_time"] = datetime.datetime.now(datetime.timezone.utc)
+ updated_time["running"] = False
+
+ if action == "continue":
+ current_timer = db_get_time()
+ current_start_time = current_timer.start_time or datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S.%f%z")
+
+ _current_start_time = datetime.datetime.strptime(current_start_time, "%Y-%m-%d %H:%M:%S.%f%z")
+
+ now_timestamp = datetime.datetime.now(datetime.timezone.utc)
+ pause_time = datetime.datetime.strptime(current_timer.pause_time, "%Y-%m-%d %H:%M:%S.%f%z")
+ delta = now_timestamp - pause_time
+
+ updated_time["start_time"] = _current_start_time + delta
+ updated_time["pause_time"] = None
+ updated_time["running"] = True
+
+ if action == "reset":
+ updated_time["start_time"] = None
+ updated_time["pause_time"] = None
+ updated_time["running"] = False
+ updated_time["offset"] = 0
+
+ if action == "offset":
+ updated_time["offset"] = offset
+
+ db_create_or_update(Timetracker, ["id"], updated_time)
+
+ print("db_get_time", db_get_time().__dict__)
+ return db_get_time()
+
+ except Exception as e:
+ logger.exception(e)
+ return False
+
+
+def db_get_current_submissions(user_id: int, exercise_id: string) -> Tuple[dict, dict]:
try:
- exercise = db_get_exercise_by_global_exercise_id(global_exercise_id)
+ exercise = db_get_exercise_by_id(exercise_id)
+ submissions = []
+
submissions = (
- db.session.query(Execution)
+ db.session.query(Submission)
.filter_by(user_id=user_id)
.filter_by(exercise_id=exercise.id)
- .order_by(Execution.response_timestamp.desc(), Execution.execution_timestamp.desc())
+ .order_by(Submission.execution_timestamp.desc(), Submission.response_timestamp.desc())
.all()
)
+
return submissions
- except Execution as e:
+ except Exception as e:
+ logger.exception(e)
+ return []
+
+
+def db_get_current_submissions(user_id: int, exercise_id: string) -> Tuple[dict, dict]:
+ try:
+ exercise = db_get_exercise_by_id(exercise_id)
+ submissions = (
+ db.session.query(Submission)
+ .filter_by(user_id=user_id)
+ .filter_by(exercise_id=exercise.id)
+ .order_by(Submission.execution_timestamp.desc(), Submission.response_timestamp.desc())
+ .all()
+ )
+ return submissions
+ except Exception as e:
logger.exception(e)
return None, None
@@ -305,23 +340,19 @@ def db_get_exercise_by_name(exercise_name: str) -> dict:
return generic_getter(Exercise, "exercise_name", exercise_name)
-def db_get_exercise_by_global_exercise_id(global_exercise_id: str) -> dict:
- return generic_getter(Exercise, "global_exercise_id", global_exercise_id)
-
+def db_get_questionnaire_by_id(questionnaire_id: str) -> dict:
+ return generic_getter(Questionnaire, "id", questionnaire_id)
-def db_get_questionnaire_by_global_questionnaire_id(global_questionnaire_id: str) -> dict:
- return generic_getter(Questionnaire, "global_questionnaire_id", global_questionnaire_id)
+def db_get_questionnaire_question_by_id(question_id: str) -> dict:
+ return generic_getter(QuestionnaireQuestion, "id", question_id)
-def db_get_questionnaire_question_by_global_question_id(global_question_id: str) -> dict:
- return generic_getter(QuestionnaireQuestion, "global_question_id", global_question_id)
-
-def db_get_all_questionnaires_questions(global_questionnaire_id: str) -> dict:
+def db_get_all_questionnaires_questions(questionnaire_id: str) -> dict:
try:
return (
db.session.query(QuestionnaireQuestion)
- .filter_by(global_questionnaire_id=global_questionnaire_id)
+ .filter_by(id=questionnaire_id)
.order_by(QuestionnaireQuestion.local_question_id.asc())
.all()
)
@@ -330,9 +361,9 @@ def db_get_all_questionnaires_questions(global_questionnaire_id: str) -> dict:
return None
-def db_get_questionnaire_results_by_global_question_id(global_question_id: str) -> dict:
- questionnaire_question = generic_getter(QuestionnaireQuestion, "global_question_id", global_question_id)
- questionnaire_answers = generic_getter(QuestionnaireAnswer, "global_question_id", global_question_id, all=True)
+def db_get_questionnaire_results_by_question_id(question_id: str) -> dict:
+ questionnaire_question = generic_getter(QuestionnaireQuestion, "id", question_id)
+ questionnaire_answers = generic_getter(QuestionnaireAnswer, "question_id", question_id, all=True)
labels = json.loads(questionnaire_question.answer_options)
results = [0] * len(labels)
@@ -368,6 +399,10 @@ def db_get_all_users() -> list:
return generic_getter(User, "role", "participant", all=True)
+def db_get_participants_userids() -> list:
+ return [id[0] for id in db.session.query(User).filter_by(role="participant").with_entities(User.id).all()]
+
+
def db_get_all_userids() -> list:
return [id[0] for id in db.session.query(User).with_entities(User.id).all()]
@@ -396,6 +431,10 @@ def db_get_page_by_id(page_id) -> dict:
return generic_getter(Page, "page_id", page_id)
+def db_get_time() -> dict:
+ return generic_getter(Timetracker, "id", 0)
+
+
def db_get_all_active_pages() -> dict:
active_pages = []
db_active_pages = generic_getter(Page, "hidden", False, all=True)
@@ -553,13 +592,13 @@ def db_convert_ids_to_usernames(user_ids: list = []) -> list:
return usernames
-def db_get_executions_by_user_exercise(user_id: int, exercise_id: int) -> list:
+def db_get_submissions_by_user_exercise(user_id: int, exercise_id: int) -> list:
try:
return (
- db.session.query(Execution)
+ db.session.query(Submission)
.filter_by(user_id=user_id)
.filter_by(exercise_id=exercise_id)
- .order_by(Execution.execution_timestamp.desc())
+ .order_by(Submission.execution_timestamp.desc())
.all()
)
except Exception as e:
@@ -572,9 +611,9 @@ def db_get_completed_state(user_id: int, exercise_id: int) -> dict:
return (
db.session.query(User)
.filter_by(id=user_id)
- .join(Execution)
+ .join(Submission)
.filter_by(exercise_id=exercise_id)
- .with_entities(Execution.completed)
+ .with_entities(Submission.completed)
.all()
)
except Exception as e:
@@ -603,9 +642,9 @@ def db_get_filename_from_hash(filename_hash):
return None
-def db_get_cache_by_ids(user_id: int, global_exercise_id: str) -> dict:
+def db_get_cache_by_ids(user_id: int, exercise_id: str) -> dict:
try:
- return db.session.query(Cache).filter_by(user_id=user_id).filter_by(global_exercise_id=global_exercise_id).first()
+ return db.session.query(Cache).filter_by(user_id=user_id).filter_by(exercise_id=exercise_id).first()
except Exception as e:
logger.exception(e)
return None
@@ -643,7 +682,6 @@ def db_get_grouped_questionnaires() -> list:
"answer_options": question.answer_options,
"language": question.language,
"active": question.active,
- "global_question_id": question.global_question_id,
}
)
@@ -660,16 +698,16 @@ def db_get_grouped_questionnaires() -> list:
return None
-def db_activate_questioniare_question(global_question_id) -> bool:
+def db_activate_questioniare_question(question_id) -> bool:
try:
- question = db.session.query(QuestionnaireQuestion).filter_by(global_question_id=global_question_id).first()
+ question = db.session.query(QuestionnaireQuestion).filter_by(id=question_id).first()
# Set active state
setattr(question, "active", True)
db.session.flush()
db.session.commit()
- questionnaire = db.session.query(Questionnaire).filter_by(global_questionnaire_id=question.global_questionnaire_id).first()
+ questionnaire = db.session.query(Questionnaire).filter_by(id=question.questionnaire_id).first()
question_dict = convert_to_dict(question)
# Adjust dict
@@ -684,12 +722,12 @@ def db_activate_questioniare_question(global_question_id) -> bool:
return False
-def db_create_questionnaire_answer(global_question_id: str, answers: str, user_id: int) -> bool:
+def db_create_questionnaire_answer(question_id: str, answers: str, user_id: int) -> bool:
try:
if isinstance(answers, int):
answers = [answers]
- submission = QuestionnaireAnswer(answers=json.dumps(answers), user_id=user_id, global_question_id=global_question_id)
+ submission = QuestionnaireAnswer(answers=json.dumps(answers), user_id=user_id, question_id=question_id)
db.session.add(submission)
db.session.commit()
@@ -700,8 +738,8 @@ def db_create_questionnaire_answer(global_question_id: str, answers: str, user_i
return False
-def db_get_questionnaire_question_answers_by_user(global_question_id: str, user_id: int) -> list:
- answers = generic_getter(QuestionnaireAnswer, ["global_question_id", "user_id"], [global_question_id, user_id], all=True)
+def db_get_questionnaire_question_answers_by_user(question_id: str, user_id: int) -> list:
+ answers = generic_getter(QuestionnaireAnswer, ["question_id", "user_id"], [question_id, user_id], all=True)
return answers
@@ -710,11 +748,11 @@ def db_get_completion_percentage(exercise_id):
try:
executions = (
- db.session.query(Execution)
+ db.session.query(Submission)
.filter_by(exercise_id=exercise_id)
.join(User)
.group_by(User.id)
- .with_entities(db.func.max(Execution.completed))
+ .with_entities(db.func.max(Submission.completed))
.all()
)
return len(executions) / len(users) * 100
diff --git a/backend/functions/execution.py b/backend/functions/execution.py
index 3b68911..ea89510 100644
--- a/backend/functions/execution.py
+++ b/backend/functions/execution.py
@@ -3,12 +3,19 @@
from typing import Tuple
import requests
+from backend.functions.helpers import convert_to_dict
from backend.logger import logger
from backend.conf.config import cfg
-from backend.functions.database import db_get_venjix_execution, db_update_venjix_execution
+from backend.functions.database import (
+ db_get_exercise_by_id,
+ db_get_submission_by_execution_uuid,
+ db_update_venjix_execution,
+)
-def call_venjix(username: str, script: str, execution_uuid: str) -> Tuple[bool, bool]:
+def call_venjix(exercise_id: str, username: str, callback_url: str, execution_uuid: str) -> Tuple[bool, bool]:
+ script_response = None
+ script = db_get_exercise_by_id(exercise_id).script_name
try:
response = requests.post(
# TODO: Remove verify line
@@ -19,7 +26,7 @@ def call_venjix(username: str, script: str, execution_uuid: str) -> Tuple[bool,
{
"script": script,
"user_id": username,
- "callback": f"/callback/{execution_uuid}",
+ "callback": f"{callback_url}/{execution_uuid}",
}
),
)
@@ -28,19 +35,27 @@ def call_venjix(username: str, script: str, execution_uuid: str) -> Tuple[bool,
if response.status_code != 200:
connected = False
executed = False
- msg = f"{response.status_code}: {resp['response']}"
+ status_msg = f"{response.status_code}: {resp['response']}"
else:
connected = True
executed = bool(resp["response"] == "script started")
- msg = resp.get("msg") or None
+ status_msg = resp.get("status_msg") or None
+ script_response = resp.get("script_response") or None
except Exception as connection_exception:
- logger.exception(connection_exception)
+ logger.error(connection_exception)
connected = False
executed = False
- msg = "Connection failed"
+ status_msg = "connection failed"
- db_update_venjix_execution(execution_uuid, connection_failed=(not connected), msg=msg)
+ updates = {
+ "execution_uuid": execution_uuid,
+ "executed": executed,
+ "status_msg": status_msg,
+ "script_response": script_response,
+ }
+
+ db_update_venjix_execution(updates)
return connected, executed
@@ -50,19 +65,16 @@ def wait_for_venjix_response(execution_uuid: str) -> dict:
time.sleep(0.5)
try:
- execution = db_get_venjix_execution(execution_uuid)
- if execution["response_timestamp"] or execution["connection_failed"]:
- if execution["connection_failed"]:
- execution["executed"] = False
- execution["msg"] = "Connection failed."
- else:
- # Get error msg
- error = json.loads(execution["response_content"]).get("stderr")
- execution["executed"] = bool(not error)
- # Apply error msg to msg if none given
- execution["msg"] = execution["msg"] or error
+ submission = db_get_submission_by_execution_uuid(execution_uuid)
+ submission = convert_to_dict(submission)
+ if not submission["executed"]:
+ submission["status_msg"] = (
+ submission["status_msg"] or json.loads(submission["response_content"]).get("stderr") or "connection failed"
+ )
+ return submission
- return execution
+ if submission["response_timestamp"]:
+ return submission
except Exception as e:
logger.exception(e)
diff --git a/backend/functions/helpers.py b/backend/functions/helpers.py
index b78716d..84a4274 100644
--- a/backend/functions/helpers.py
+++ b/backend/functions/helpers.py
@@ -42,7 +42,7 @@ def extract_history(executions):
"start_time": utc_to_local(execution.get("execution_timestamp"), date=True),
"response_time": utc_to_local(execution.get("response_timestamp"), date=False),
"completed": bool(execution.get("completed")),
- "msg": execution.get("msg"),
+ "status_msg": execution.get("status_msg"),
"partial": bool(execution.get("partial")),
}
for i, execution in enumerate(executions)
@@ -100,6 +100,7 @@ def sse_create_and_publish(
recipients=None,
positions=None,
question=None,
+ timer=None,
) -> bool:
# Import
from backend.classes.SSE import SSE_Event, sse
@@ -114,6 +115,11 @@ def sse_create_and_publish(
message = f"
New Comment
User: {user.name}
Page: {page}"
recipients = [admin_user.id for admin_user in db_get_admin_users()]
+ if _type == "timer":
+ event = "timer"
+ message = f"{json.dumps(timer)}"
+ recipients = [admin_user.id for admin_user in db_get_admin_users()]
+
if _type == "content":
message = """
@@ -129,8 +135,9 @@ def sse_create_and_publish(
new_event = SSE_Event(event=event, _type=_type, message=message, question=question, recipients=recipients, positions=positions)
- # Create Database entry
- db_create_notification(new_event)
+ if event != "questionnaire" and event != "timer":
+ # Create Database entry
+ db_create_notification(new_event)
# Notify Users
sse.publish(new_event)
diff --git a/backend/routes/__init__.py b/backend/routes/__init__.py
index fbaa982..ad71003 100644
--- a/backend/routes/__init__.py
+++ b/backend/routes/__init__.py
@@ -11,3 +11,4 @@
from backend.routes.setup import setup_api
from backend.routes.users import users_api
from backend.routes.pages import pages_api
+from backend.routes.results import results_api
diff --git a/backend/routes/cache.py b/backend/routes/cache.py
index d1c32e8..1b48e61 100644
--- a/backend/routes/cache.py
+++ b/backend/routes/cache.py
@@ -19,20 +19,20 @@ def putCache():
new_cache_entry = {
"user_id": current_user.id,
- "global_exercise_id": data.get("global_exercise_id"),
+ "exercise_id": data.get("global_exercise_id"),
"form_data": json.dumps(data.get("form_data")),
}
- db_create_or_update(Cache, ["user_id", "global_exercise_id"], new_cache_entry)
+ db_create_or_update(Cache, ["user_id", "exercise_id"], new_cache_entry)
return jsonify(updated=True), 200
# Post new comment
-@cache_api.route("/cache/", methods=["GET"])
+@cache_api.route("/cache/", methods=["GET"])
@jwt_required()
-def getCache(global_exercise_id):
- if cache := db_get_cache_by_ids(current_user.id, global_exercise_id):
+def getCache(exercise_id):
+ if cache := db_get_cache_by_ids(current_user.id, exercise_id):
return jsonify(form_data=cache.form_data), 200
else:
return jsonify(form_data=None), 200
diff --git a/backend/routes/callback.py b/backend/routes/callback.py
index ae5f301..e7250e6 100644
--- a/backend/routes/callback.py
+++ b/backend/routes/callback.py
@@ -1,7 +1,12 @@
from datetime import datetime, timezone
from flask import Blueprint, json, jsonify, request
-from backend.functions.database import db_update_venjix_execution
+from backend.functions.database import (
+ db_get_exercise_by_id,
+ db_get_submission_by_execution_uuid,
+ db_update_venjix_execution,
+)
+from backend.functions.helpers import sse_create_and_publish
from backend.logger import logger
callback_api = Blueprint("callback_api", __name__)
@@ -11,14 +16,23 @@
def callback(execution_uuid):
try:
resp = request.get_json()
- db_update_venjix_execution(
- execution_uuid,
- response_timestamp=datetime.now(timezone.utc),
- response_content=json.dumps(resp),
- completed=bool(resp.get("returncode") == 0),
- msg=resp.get("msg") or None,
- partial=resp.get("partial") or False,
- )
+ updates = {
+ "execution_uuid": execution_uuid,
+ "response_timestamp": datetime.now(timezone.utc),
+ "response_content": json.dumps(resp),
+ "completed": bool(resp.get("returncode") == 0),
+ "status_msg": resp.get("status_msg") or None,
+ "script_response": resp.get("script_response") or None,
+ "partial": resp.get("partial") or False,
+ }
+
+ logger.info(f"Callback for { execution_uuid } received")
+ db_update_venjix_execution(updates)
+
+ submission = db_get_submission_by_execution_uuid(execution_uuid)
+
+ sse_create_and_publish(_type="submission", user=submission.user, exercise=submission.exercise)
+
return jsonify(success=True), 200
except Exception as e:
diff --git a/backend/routes/executions.py b/backend/routes/executions.py
index e401512..740a125 100644
--- a/backend/routes/executions.py
+++ b/backend/routes/executions.py
@@ -2,24 +2,28 @@
import uuid
from backend.classes.SubmissionResponse import SubmissionResponse
-from flask import Blueprint, jsonify, request, send_from_directory
-from flask_jwt_extended import get_jwt_identity, jwt_required, current_user
+from flask import Blueprint, jsonify, request, send_from_directory, make_response
+from flask_jwt_extended import jwt_required, current_user
from backend.jwt_manager import admin_required, only_self_or_admin
from backend.logger import logger
from backend.functions.database import (
- db_create_execution,
+ db_create_submission,
db_create_file,
- db_create_venjix_execution,
db_get_current_submissions,
+ db_get_current_submissions,
+ db_get_exercise_by_id,
db_get_running_executions_by_name,
db_get_all_exercises,
db_get_all_users,
db_get_completed_state,
- db_get_executions_by_user_exercise,
- db_get_exercise_by_global_exercise_id,
+ db_get_submission_by_exercise_id,
+ db_get_submissions_by_user_exercise,
db_get_exercise_groups,
db_get_exercises_by_group,
+ db_get_submissions_by_user_exercise,
+ db_get_time,
db_get_user_by_id,
+ db_set_time,
)
from backend.functions.execution import (
call_venjix,
@@ -35,24 +39,115 @@
executions_api = Blueprint("executions_api", __name__)
-@executions_api.route("/execution/", methods=["POST"])
-@jwt_required()
-def postExecution(exercise_type):
- username = get_jwt_identity()
- execution_uuid = f"{str(username)}_{uuid.uuid4().int & (1 << 64) - 1}"
+@executions_api.route("/submissions", methods=["GET"])
+@admin_required()
+def getAllSubmissions():
+ submissions = []
+
+ for user in db_get_all_users():
+ executions = {"user_id": user.id, "username": user.name}
+ for exercise in db_get_all_exercises():
+ # completed_state = [state[0] for state in db_get_completed_state(user.id, exercise.id)]
+ _submissions = db_get_submissions_by_user_exercise(user.id, exercise.id)
+ execution_ids = [execution.get("id") for execution in convert_to_dict(_submissions)]
+ executions[exercise.id] = {
+ # "completed": int(any(completed_state)) if completed_state else -1,
+ "completed": int(_submissions[0].completed) if _submissions and _submissions[0] else 0,
+ "executed": int(_submissions[0].executed) if _submissions and _submissions[0] else 0,
+ "executions": execution_ids,
+ }
+ submissions.append(executions)
+
+ return jsonify(submissions=submissions)
- response = {"uuid": execution_uuid, "connected": False, "executed": False}
+
+@executions_api.route("/submissions/form/", methods=["POST"])
+@jwt_required()
+def postFormSubmission(exercise_id):
+ response = SubmissionResponse()
data = request.get_json()
+ if db_create_submission("form", exercise_id, current_user.id, data=data):
+ response.executed = True
+ response.completed = True
+
+ sse_create_and_publish(_type="submission", user=current_user, exercise=db_get_exercise_by_id(exercise_id))
+
+ return jsonify(response.__dict__)
+
+
+@executions_api.route("/submissions/script/", methods=["POST"])
+@jwt_required()
+def postScriptSubmission(exercise_id):
+ execution_uuid = f"{str(current_user.name)}_{uuid.uuid4().int & (1 << 64) - 1}"
+ response = SubmissionResponse(uuid=execution_uuid)
+ callback_url = f"{request.headers.get('Referer').split('/statics')[0]}/callback"
- if db_create_execution(exercise_type, data, username, execution_uuid):
- if exercise_type == "script":
- response["connected"], response["executed"] = call_venjix(username, data["script"], execution_uuid)
- if exercise_type == "form":
- response["connected"] = True
- response["executed"] = True
+ if db_create_submission("script", exercise_id, current_user.id, execution_uuid=execution_uuid):
+ response.connected, response.executed = call_venjix(exercise_id, current_user.name, callback_url, execution_uuid)
+
+ return jsonify(response.__dict__)
+
+
+@executions_api.route("/submissions/", methods=["GET"])
+@jwt_required()
+def getExerciseSubmissions(exercise_id):
+ response = None
+
+ if submissions := db_get_current_submissions(current_user.id, exercise_id):
+ response = SubmissionResponse()
+ response.update(convert_to_dict(submissions))
+ response = response.__dict__
- sse_create_and_publish(_type="submission", user=current_user, exercise=db_get_exercise_by_global_exercise_id(data.get("name")))
+ return jsonify(response)
+
+
+@executions_api.route("/submissions//", methods=["GET"])
+@only_self_or_admin()
+def getExerciseSubmissionsByUser(user_id, exercise_id):
+ exercise = db_get_exercise_by_id(exercise_id)
+ db_submissions = db_get_submissions_by_user_exercise(user_id, exercise.id)
+
+ return jsonify(
+ exercise_name=exercise.exercise_name, user_name=db_get_user_by_id(user_id).name, submissions=convert_to_dict(db_submissions)
+ )
+
+
+@executions_api.route("/executions/", methods=["GET"])
+@jwt_required()
+def getExecutions(exercise_id):
+ response = None
+
+ if executions := db_get_current_submissions(current_user.id, exercise_id):
+ response = SubmissionResponse()
+ response.update(convert_to_dict(executions))
+ response = response.__dict__
+
+ return jsonify(response)
+
+
+@executions_api.route("/executions/state/", methods=["GET"])
+@jwt_required()
+def getExecutionState(execution_uuid):
+ response = SubmissionResponse()
+ venjix_response = wait_for_venjix_response(execution_uuid)
+ response.update(venjix_response)
+ response = response.__dict__
+
+ return jsonify(response)
+
+
+@executions_api.route("/executions/active/", methods=["GET"])
+@jwt_required()
+def getActiveExecutionsState(script_name):
+ response = None
+
+ if running_execution := db_get_running_executions_by_name(current_user.id, script_name):
+ response = SubmissionResponse()
+ execution_uuid = running_execution[0].get("execution_uuid")
+ venjix_response = wait_for_venjix_response(execution_uuid)
+ response.update(venjix_response)
+ response = response.__dict__
return jsonify(response)
@@ -85,19 +180,19 @@ def uploadFile():
# Check if 'file' is in the request.files dictionary, else return error message
if "file" not in request.files:
- response.msg = "File missing"
+ response.status_msg = "File missing"
return jsonify(response.__dict__)
# If filename is empty, return error message
file = request.files["file"]
if file.filename == "":
- response.msg = "No file selected"
+ response.status_msg = "No file selected"
return jsonify(response.__dict__)
# Generate a new file name and check if the file type is allowed, else return error message
filename = f"{current_user.name}_{secure_filename(file.filename)}"
if not allowed_file(filename):
- response.msg = "File type not allowed"
+ response.status_msg = "File type not allowed"
return jsonify(response.__dict__)
try:
@@ -108,13 +203,13 @@ def uploadFile():
# Update the response object with success status and other details
response.completed = True
response.executed = True
- response.msg = "File successfully uploaded"
+ response.status_msg = "File successfully uploaded"
response.filename = filename
# Catch any server errors and return error message
except Exception as e:
logger.exception(f"ERROR: {e}")
- response.msg = "Server error"
+ response.status_msg = "Server error"
# Return the JSON representation of the response object
return jsonify(response.__dict__)
@@ -126,99 +221,26 @@ def downloadFile(filename):
return send_from_directory(os.path.join(os.getcwd(), cfg.upload_folder), filename)
-@executions_api.route("/submissions", methods=["GET"])
+@executions_api.route("/time", methods=["GET"])
@admin_required()
-def getAllSubmissions():
- submissions = []
+def getTime():
+ if stored_time := db_get_time():
+ return jsonify(convert_to_dict(stored_time))
+ return jsonify({})
- for user in db_get_all_users():
- executions = {"user_id": user.id, "username": user.name}
- for exercise in db_get_all_exercises():
- completed_state = [state[0] for state in db_get_completed_state(user.id, exercise.id)]
- execution_ids = [
- execution.get("execution_uuid") for execution in convert_to_dict(db_get_executions_by_user_exercise(user.id, exercise.id))
- ]
- executions[exercise.global_exercise_id] = {
- "completed": int(any(completed_state)) if completed_state else -1,
- "executions": execution_ids,
- }
- submissions.append(executions)
-
- return jsonify(submissions=submissions)
-
-
-@executions_api.route("/submissions/form/", methods=["POST"])
-@jwt_required()
-def postFormExercise(global_exercise_id):
- response = SubmissionResponse()
-
- data = request.get_json()
- if db_create_execution("form", global_exercise_id, data, current_user.id, None):
- response.executed = True
- response.completed = True
-
- sse_create_and_publish(_type="submission", user=current_user, exercise=db_get_exercise_by_global_exercise_id(global_exercise_id))
-
- return jsonify(response.__dict__)
-
-
-@executions_api.route("/submissions/", methods=["GET"])
-@jwt_required()
-def getExerciseSubmissions(global_exercise_id):
- response = None
-
- if executions := db_get_current_submissions(current_user.id, global_exercise_id):
- response = SubmissionResponse()
- response.update(convert_to_dict(executions))
- response = response.__dict__
-
- return jsonify(response)
+@executions_api.route("/time/", methods=["POST"])
+@admin_required()
+def setTimer(action):
-@executions_api.route("/submissions//", methods=["GET"])
-@only_self_or_admin()
-def getExerciseSubmissionsByUser(user_id, global_exercise_id):
- exercise = db_get_exercise_by_global_exercise_id(global_exercise_id)
- db_submissions = db_get_executions_by_user_exercise(user_id, exercise.id)
-
- return jsonify(
- exercise_name=exercise.exercise_name, user_name=db_get_user_by_id(user_id).name, submissions=convert_to_dict(db_submissions)
- )
-
-
-@executions_api.route("/executions/", methods=["POST"])
-@jwt_required()
-def runExecution(script_name):
- execution_uuid = f"{str(current_user.name)}_{uuid.uuid4().int & (1 << 64) - 1}"
- response = SubmissionResponse(uuid=execution_uuid)
-
- if db_create_venjix_execution(execution_uuid, current_user.id, script_name):
- response.connected, response.executed = call_venjix(current_user.name, script_name, execution_uuid)
-
- return jsonify(response.__dict__)
-
-
-@executions_api.route("/executions/", methods=["GET"])
-@jwt_required()
-def getExecutionState(execution_uuid):
- response = SubmissionResponse()
- venjix_response = wait_for_venjix_response(execution_uuid)
- response.update(venjix_response)
- response = response.__dict__
-
- return jsonify(response)
-
+ offset = 0
-@executions_api.route("/executions/active/", methods=["GET"])
-@jwt_required()
-def getActiveExecutionsState(script_name):
- response = None
+ if request.is_json:
+ offset = (request.get_json()).get("offset", 0)
- if running_execution := db_get_running_executions_by_name(current_user.id, script_name):
- response = SubmissionResponse()
- execution_uuid = running_execution[0].get("execution_uuid")
- venjix_response = wait_for_venjix_response(execution_uuid)
- response.update(venjix_response)
- response = response.__dict__
+ if timer_event := db_set_time(action, offset):
+ sse_create_and_publish(_type="timer", timer=convert_to_dict(timer_event))
+ return jsonify(updated=True)
- return jsonify(response)
+ else:
+ return jsonify(updated=False)
diff --git a/backend/routes/questionnaires.py b/backend/routes/questionnaires.py
index 7ed8de6..8132a2c 100644
--- a/backend/routes/questionnaires.py
+++ b/backend/routes/questionnaires.py
@@ -5,11 +5,11 @@
from backend.functions.database import (
db_activate_questioniare_question,
db_create_questionnaire_answer,
+ db_get_participants_userids,
db_get_questionnaire_question_answers_by_user,
- db_get_all_userids,
db_get_grouped_questionnaires,
- db_get_questionnaire_question_by_global_question_id,
- db_get_questionnaire_results_by_global_question_id,
+ db_get_questionnaire_question_by_id,
+ db_get_questionnaire_results_by_question_id,
db_get_admin_users,
)
from backend.functions.helpers import sse_create_and_publish
@@ -31,14 +31,18 @@ def getQuestions():
active_questions = []
+ # Avoid promting admins
+ if current_user.role == "instructor":
+ return jsonify(questions=[]), 200
+
# Filter for active questions
for questionnaire in grouped_questionnaires:
for question in questionnaire.get("questions"):
- question["global_questionnaire_id"] = questionnaire.get("global_questionnaire_id")
+ question["questionnaire_id"] = questionnaire.get("questionnaire_id")
question["page_title"] = questionnaire.get("page_title")
if question.get("active"):
# Check if user has already answerd the questionnaire
- answers = db_get_questionnaire_question_answers_by_user(question["global_question_id"], current_user.id)
+ answers = db_get_questionnaire_question_answers_by_user(question["id"], current_user.id)
if not len(answers):
active_questions.append(question)
@@ -46,11 +50,11 @@ def getQuestions():
# Activate Question
-@questionnaires_api.route("/questionnaires/questions/", methods=["PUT"])
+@questionnaires_api.route("/questionnaires/questions/", methods=["PUT"])
@admin_required()
-def activateQuestion(global_question_id):
- if question := db_activate_questioniare_question(global_question_id=global_question_id):
- user_list = db_get_all_userids()
+def activateQuestion(question_id):
+ if question := db_activate_questioniare_question(question_id=question_id):
+ user_list = db_get_participants_userids()
sse_create_and_publish(
event="questionnaire",
@@ -63,18 +67,17 @@ def activateQuestion(global_question_id):
return jsonify(success=False), 500
-@questionnaires_api.route("/questionnaires/questions/", methods=["POST"])
+@questionnaires_api.route("/questionnaires/questions/", methods=["POST"])
@jwt_required()
-def submitQuestion(global_question_id):
+def submitQuestion(question_id):
data = request.get_json()
answers = data.get("answers")
- if db_create_questionnaire_answer(global_question_id=global_question_id, answers=answers, user_id=current_user.id):
+ if db_create_questionnaire_answer(question_id=question_id, answers=answers, user_id=current_user.id):
# Send SSE event
-
sse_create_and_publish(
event="questionnaireSubmission",
- question=global_question_id,
+ question=question_id,
recipients=[admin_user.id for admin_user in db_get_admin_users()],
)
@@ -83,10 +86,10 @@ def submitQuestion(global_question_id):
return jsonify(success=False), 500
-@questionnaires_api.route("/questionnaires/questions/", methods=["GET"])
+@questionnaires_api.route("/questionnaires/questions/", methods=["GET"])
@admin_required()
-def getQuestionnaireResults(global_question_id):
- labels, results = db_get_questionnaire_results_by_global_question_id(global_question_id)
- question = db_get_questionnaire_question_by_global_question_id(global_question_id).question
+def getQuestionnaireResults(question_id):
+ labels, results = db_get_questionnaire_results_by_question_id(question_id)
+ question = db_get_questionnaire_question_by_id(question_id).question
return jsonify(question=question, labels=labels, results=results), 200
diff --git a/backend/routes/results.py b/backend/routes/results.py
new file mode 100644
index 0000000..b40b178
--- /dev/null
+++ b/backend/routes/results.py
@@ -0,0 +1,52 @@
+from flask import Blueprint, make_response
+from backend.jwt_manager import admin_required
+from backend.logger import logger
+from backend.functions.database import (
+ db_get_all_exercises,
+ db_get_all_users,
+ db_get_submissions_by_user_exercise,
+)
+
+results_api = Blueprint("results_api", __name__)
+
+
+@results_api.route("/md_results", methods=["GET"])
+@admin_required()
+def getResults():
+
+ results_md = ""
+ import json
+
+ for exercise in db_get_all_exercises():
+
+ results_md += f"# {exercise.page_title}\n"
+ results_md += f"## {exercise.exercise_name}\n"
+
+ for user in db_get_all_users():
+ _submissions = db_get_submissions_by_user_exercise(user.id, exercise.id)
+
+ if _submissions and _submissions[0]:
+ results_md += f"### { user.name }\n"
+
+ submission_content = _submissions[0].form_data
+ submission_content = json.loads(submission_content)
+
+ for input_group, input_fields in submission_content.items():
+ if input_fields:
+ results_md += f"#### {input_group}\n"
+ results_md += "| Label | Content |\n"
+ results_md += "| ---- | ---- |\n"
+ for input_label, input_field in input_fields.items():
+ if "divider" in input_label:
+ results_md += f"| ---- | ---- |\n"
+ else:
+ if isinstance(input_field, str):
+ input_field = input_field.replace("\n", "
")
+ results_md += f"| {input_label.replace('_', ' ')} | { input_field } |\n"
+
+ results_md += "---\n"
+
+ response = make_response(results_md)
+ response.headers["Content-Disposition"] = "attachment; filename=sample.md"
+ response.mimetype = "text/markdown"
+ return response
diff --git a/backend/routes/setup.py b/backend/routes/setup.py
index 435210a..4c9002e 100644
--- a/backend/routes/setup.py
+++ b/backend/routes/setup.py
@@ -38,12 +38,12 @@ def getSidebar():
usergroups = db_get_usergroups_by_user(current_user)
if "admins" in usergroups:
- tabs.append(Tab(name="admin", _type="admin").__dict__)
+ tabs.append(Tab(name="admin", _type="admin", user_role=current_user.role).__dict__)
landingpage = "admin"
for tab_id, tab_details in cfg.tabs.items():
if "all" in tab_details.get("show", []) or any(group in usergroups for group in tab_details.get("show", [])):
- tabs.append(Tab(name=tab_id, **tab_details).__dict__)
+ tabs.append(Tab(name=tab_id, **tab_details, user_role=current_user.role).__dict__)
if vnc_clients := cfg.users.get(current_user.name).get("vnc_clients"):
multiple = len(vnc_clients) > 1
diff --git a/dev-backend.sh b/dev-backend.sh
index f502b0d..a143326 100644
--- a/dev-backend.sh
+++ b/dev-backend.sh
@@ -1,3 +1,36 @@
#!/usr/bin/env bash
+
+cleanup() {
+ echo "Stopping processes..."
+ kill -TERM "$gunicorn_pid" "$yarn_pid"
+ wait
+ echo "Processes stopped."
+ exit
+}
+
+trap cleanup INT
+
+cd frontend
+yarn dev &
+yarn_pid=$!
+cd ..
+
+python -m venv venv
+source venv/bin/activate
+
+while [ "$#" -gt 0 ]; do
+ case "$1" in
+ -init)
+ pip install -e .
+ esac
+done
+
mkdir /tmp/learners
-gunicorn backend:app --worker-class gevent --bind unix:/tmp/learners/learners.sock
+
+gunicorn backend:app --worker-class gevent --bind unix:/tmp/learners/learners.sock &
+gunicorn_pid=$!
+
+
+wait -n
+
+cleanup
\ No newline at end of file
diff --git a/frontend/src/components/LoginForm.vue b/frontend/src/components/LoginForm.vue
index 910ec4e..8a81075 100644
--- a/frontend/src/components/LoginForm.vue
+++ b/frontend/src/components/LoginForm.vue
@@ -96,6 +96,7 @@ import { store } from "@/store";
import axios from "axios";
import { setStyles } from "@/helpers";
import Loader from "@/components/sub-components/Loader.vue";
+import { jwtDecode } from "jwt-js-decode";
export default {
name: "LoginForm",
@@ -141,6 +142,7 @@ export default {
store.dispatch("setJwt", jwt);
axios.defaults.headers.common["Authorization"] = "Bearer " + jwt;
store.dispatch("getTabsFromServer");
+ if (jwtDecode(jwt).payload.admin) this.landingpage = "admin";
this.$router.push(`/#${this.landingpage}`);
} else {
store.dispatch("unsetJwt");
diff --git a/frontend/src/components/Mainpage.vue b/frontend/src/components/Mainpage.vue
index df419d9..be6950f 100644
--- a/frontend/src/components/Mainpage.vue
+++ b/frontend/src/components/Mainpage.vue
@@ -19,7 +19,11 @@
:currentView="currentView"
@loaded="iframeLoaded"
/>
-
+
@@ -34,6 +38,9 @@ import { ITabObject } from "@/types";
import { jwtDecode } from "jwt-js-decode";
import { store } from "@/store";
import { setStyles, initSSE, initVisibility } from "@/helpers";
+import axios from "axios";
+import { VueCookies } from "vue-cookies";
+import { inject } from "vue";
// TODO: Add UserArea
export default {
@@ -48,10 +55,11 @@ export default {
return {
notificationClosed: false,
questionnaireClosed: false,
- sse_error: true,
+ sse_error: false,
evtSource: EventSource as any,
serverEvent: Object as any,
iframes: [] as any,
+ intervalTracker: null as any | null,
};
},
props: {
@@ -64,15 +72,25 @@ export default {
},
computed: {
admin() {
- const jwt = jwtDecode(store.getters.getJwt);
- return jwt.payload.admin;
+ if (store.getters.getJwt) {
+ const jwt = jwtDecode(store.getters.getJwt);
+ return jwt.payload.admin;
+ } else {
+ return false;
+ }
},
filteredTabs() {
const tabsList = this.tabs || [];
return tabsList.filter((tab) => tab._type != "admin");
},
+ toggleNotifications() {
+ return store.getters.getShowNotifications;
+ },
+ currentNotificationIndex() {
+ const index = store.getters.getCurrentNotificationIndex;
+ if (index) this.notificationClosed = false;
+ },
showNotifications() {
- this.notificationClosed = false;
return (
store.getters.getShowNotifications &&
store.getters.getNotifications.length > 0 &&
@@ -98,25 +116,32 @@ export default {
let attemptCount = 1;
const connectToStream = (context) => {
- const jwt = store.getters.getJwt;
-
- const backend = store.getters.getBackendUrl;
- context.evtSource = new EventSource(`${backend}/stream?jwt=${jwt}`);
-
- context.evtSource.onopen = function () {
- console.log("Connected to SSE source.");
- context.sse_error = false;
- initSSE(context);
- };
-
- context.evtSource.onerror = function () {
- console.log("Unable to establish SSE connection.");
- context.sse_error = true;
- setTimeout(() => {
- attemptCount++;
- connectToStream(context);
- }, 5000);
- };
+ if (attemptCount > 2) {
+ console.log(
+ "Unable to establish SSE connection. Using fallback polling function."
+ );
+ this.streamFallbackMethod();
+ } else {
+ const jwt = store.getters.getJwt;
+
+ const backend = store.getters.getBackendUrl;
+ context.evtSource = new EventSource(`${backend}/stream?jwt=${jwt}`);
+
+ context.evtSource.onopen = function () {
+ console.log("Connected to SSE source.");
+ context.sse_error = false;
+ initSSE(context);
+ };
+
+ context.evtSource.onerror = function () {
+ console.log("Unable to establish SSE connection.");
+ context.sse_error = true;
+ this.intervalTracker = setTimeout(() => {
+ attemptCount++;
+ connectToStream(context);
+ }, 5000);
+ };
+ }
};
connectToStream(ctx);
@@ -142,11 +167,42 @@ export default {
this.iframes = iframes_list;
return iframes_list;
},
+ streamFallbackMethod() {
+ const gatherUpdates = () => {
+ // Get full list of notifications from server
+ store.dispatch("getNotificationsFromServer");
+
+ // Get full list of questionnaires from server
+ store.dispatch("getQuestionnairesFromServer");
+
+ // Trigger visibility control
+ initVisibility(this.iFramesGather());
+
+ // Call setTimeout again to repeat after 10 seconds
+ this.intervalTracker = setTimeout(gatherUpdates, 15000);
+ };
+
+ clearInterval(this.intervalTracker);
+
+ // Initial call to start the loop
+ gatherUpdates();
+ },
},
async beforeMount() {
setStyles(this);
+ clearInterval(this.intervalTracker);
},
mounted() {
+ // Verify local jwt token
+ if (!store.getters.getJwt) {
+ const $cookies = inject("$cookies");
+ $cookies?.remove("jwt_cookie");
+ store.dispatch("resetTabs");
+ store.dispatch("unsetJwt");
+ axios.defaults.headers.common["Authorization"] = "";
+ this.$router.push("/login");
+ }
+
this.startSseSession(this);
// Get full list of notifications from server
@@ -163,10 +219,21 @@ export default {
},
beforeUnmount() {
this.closeSSE();
+ clearInterval(this.intervalTracker);
},
beforeDestroy() {
window.removeEventListener("message", this.iFrameHandle);
},
+ watch: {
+ toggleNotifications: {
+ handler(new_state, old_state) {
+ if (new_state === true || old_state === undefined) {
+ this.notificationClosed = false;
+ }
+ },
+ immediate: true,
+ },
+ },
};
diff --git a/frontend/src/components/admin/AdminArea.vue b/frontend/src/components/admin/AdminArea.vue
index e6b60f0..2ba47d4 100644
--- a/frontend/src/components/admin/AdminArea.vue
+++ b/frontend/src/components/admin/AdminArea.vue
@@ -19,7 +19,7 @@
>
Submissions Overview
Exercises
- Pages
+ Visibility
Notifications
Questionnaire
Feedback
@@ -39,9 +39,10 @@
class="tab-container"
/>
-
-
+
@@ -74,7 +75,7 @@
+
+
diff --git a/frontend/src/components/sub-components/Questionnaire.vue b/frontend/src/components/sub-components/Questionnaire.vue
index d76cb8f..51d5166 100644
--- a/frontend/src/components/sub-components/Questionnaire.vue
+++ b/frontend/src/components/sub-components/Questionnaire.vue
@@ -9,7 +9,7 @@
- Q{{ currentQuestionnaire?.id }}
+ Quiz
@@ -57,6 +57,16 @@
>
submit
+
+ dismiss
+
@@ -97,15 +107,12 @@ export default {
async submitHandler() {
if (this.selectedAnswers === undefined) return;
const response = await axios.post(
- `questionnaires/questions/${this.currentQuestionnaire?.global_question_id}`,
+ `questionnaires/questions/${this.currentQuestionnaire?.id}`,
{
answers: this.selectedAnswers,
}
);
- store.dispatch(
- "removeQuestionnaire",
- this.currentQuestionnaire?.global_question_id
- );
+ store.dispatch("removeQuestionnaire", this.currentQuestionnaire?.id);
},
triggerAnimation() {
this.contentChanging = true;
@@ -113,6 +120,9 @@ export default {
this.contentChanging = false;
}, 600);
},
+ dismissHandler() {
+ store.dispatch("removeQuestionnaire", this.currentQuestionnaire?.id);
+ },
},
watch: {
currentQuestionnaire: function (newVal, oldVal) {
diff --git a/frontend/src/components/sub-components/ScenarioTimer.vue b/frontend/src/components/sub-components/ScenarioTimer.vue
new file mode 100644
index 0000000..5a96e18
--- /dev/null
+++ b/frontend/src/components/sub-components/ScenarioTimer.vue
@@ -0,0 +1,177 @@
+
+
+
{{ time }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/helpers/index.ts b/frontend/src/helpers/index.ts
index 83ac39b..b839c2e 100644
--- a/frontend/src/helpers/index.ts
+++ b/frontend/src/helpers/index.ts
@@ -80,8 +80,7 @@ export const extractQuestionnaires = (responseData) => {
multiple: newQuestionnaire?.multiple,
answers: JSON.parse(newQuestionnaire?.answer_options),
language: newQuestionnaire?.language,
- global_question_id: newQuestionnaire?.global_question_id,
- global_questionnaire_id: newQuestionnaire?.global_questionnaire_id,
+ questionnaire_id: newQuestionnaire?.questionnaire_id,
page_title: newQuestionnaire?.page_title,
};
});
@@ -92,10 +91,7 @@ export const extractQuestionnaires = (responseData) => {
multiple: (JSON.parse(responseData)?.question).multiple,
answers: JSON.parse((JSON.parse(responseData)?.question).answer_options),
language: (JSON.parse(responseData)?.question).language,
- global_question_id:
- (JSON.parse(responseData)?.question).global_question_id,
- global_questionnaire_id:
- (JSON.parse(responseData)?.question).global_questionnaire_id,
+ questionnaire_id: (JSON.parse(responseData)?.question).questionnaire_id,
page_title: (JSON.parse(responseData)?.question).page_title,
};
}
@@ -250,13 +246,18 @@ export const initVisibility = async (iframe_list) => {
data: pages,
};
- try {
- _iframe.contentWindow.postMessage(
- functionCall,
- new URL(_iframe.src).origin
- );
- } catch (error) {
- console.log(error);
+ const current_location = document.location.origin;
+ const iframe_location = new URL(_iframe.src).origin;
+
+ if (current_location == iframe_location) {
+ try {
+ _iframe.contentWindow.postMessage(
+ functionCall,
+ new URL(_iframe.src).origin
+ );
+ } catch (error) {
+ console.log(error);
+ }
}
});
};
@@ -268,10 +269,20 @@ export const sseHandlerContent = (ctx, event) => {
function: "visibility",
data: JSON.parse(event.data).message,
};
- _iframe.contentWindow.postMessage(
- functionCall,
- new URL(_iframe.src).origin
- );
+
+ const current_location = document.location.origin;
+ const iframe_location = new URL(_iframe.src).origin;
+
+ if (current_location == iframe_location) {
+ try {
+ _iframe.contentWindow.postMessage(
+ functionCall,
+ new URL(_iframe.src).origin
+ );
+ } catch (error) {
+ console.log(error);
+ }
+ }
});
};
@@ -294,6 +305,13 @@ export const sseHandlerNotification = (ctx, event) => {
}
};
+export const sseHandlerTimer = (ctx, event) => {
+ ctx.notificationClosed = false;
+ let message_json = JSON.parse(event.data)?.message;
+ let timer = JSON.parse(message_json);
+ store.dispatch("setTimer", timer);
+};
+
export const sseHandlerQuestionnaire = (ctx, event) => {
ctx.questionnaireClosed = false;
const newQuestionnaire = extractQuestionnaires(event.data);
@@ -307,12 +325,17 @@ export const sseHandlerQuestionnaireSubmission = (ctx, event) => {
store.dispatch("setAdminForceReload", "questionnaire");
};
export const initSSE = (ctx) => {
+ ctx.sse_error = false;
+
ctx.evtSource.addEventListener("content", (event) =>
sseHandlerContent(ctx, event)
);
ctx.evtSource.addEventListener("notification", (event) =>
sseHandlerNotification(ctx, event)
);
+ ctx.evtSource.addEventListener("timer", (event) =>
+ sseHandlerTimer(ctx, event)
+ );
ctx.evtSource.addEventListener("questionnaire", (event) =>
sseHandlerQuestionnaire(ctx, event)
);
@@ -328,6 +351,9 @@ export const initSSE = (ctx) => {
ctx.evtSource.removeEventListener("notification", (event) =>
sseHandlerNotification(ctx, event)
);
+ ctx.evtSource.removeEventListener("timer", (event) =>
+ sseHandlerTimer(ctx, event)
+ );
ctx.evtSource.removeEventListener("questionnaire", (event) =>
sseHandlerQuestionnaire(ctx, event)
);
diff --git a/frontend/src/plugins/index.ts b/frontend/src/plugins/index.ts
index f2586b7..80633b5 100644
--- a/frontend/src/plugins/index.ts
+++ b/frontend/src/plugins/index.ts
@@ -2,6 +2,7 @@
import { loadFonts } from "./webfontloader";
import vuetify from "./vuetify";
import router from "../router";
+import VueCookies from "vue-cookies";
// Types
import type { App } from "vue";
@@ -10,4 +11,5 @@ export function registerPlugins(app: App) {
loadFonts();
app.use(vuetify);
app.use(router);
+ app.use(VueCookies);
}
diff --git a/frontend/src/plugins/webfontloader.ts b/frontend/src/plugins/webfontloader.ts
index fed11cf..c66505b 100644
--- a/frontend/src/plugins/webfontloader.ts
+++ b/frontend/src/plugins/webfontloader.ts
@@ -14,6 +14,7 @@ export async function loadFonts() {
families: [
"Rubik:100,300,400,500,700,900&display=swap",
"Montserrat:100,300,400,500,700,900&display=swap",
+ "Roboto+Mono:400,500,700&display=swap",
],
},
});
diff --git a/frontend/src/scss/main.scss b/frontend/src/scss/main.scss
index 26533f0..e7b37e5 100644
--- a/frontend/src/scss/main.scss
+++ b/frontend/src/scss/main.scss
@@ -218,6 +218,10 @@ button.v-btn.resume-btn span {
overflow-y: auto;
}
+.v-snackbar__content {
+ margin: auto;
+}
+
@media (min-width: 2560px) {
.v-main .v-container {
max-width: 1800px !important;
diff --git a/frontend/src/store/actions.ts b/frontend/src/store/actions.ts
index a43f8b6..24efe6a 100644
--- a/frontend/src/store/actions.ts
+++ b/frontend/src/store/actions.ts
@@ -29,6 +29,10 @@ export default {
commit("SET_BACKEND_URL", backendUrl);
},
+ // Timer
+ setTimer: ({ commit }: { commit: Commit }, timer: any) =>
+ commit("SET_TIMER", timer),
+
// Tabs
async getTabsFromServer({ commit }) {
await axios
@@ -112,9 +116,9 @@ export default {
removeQuestionnaire: (
{ commit }: { commit: Commit },
- global_question_id: Number
+ question_id: Number
) => {
- commit("REMOVE_QUESTIONNAIRE", global_question_id);
+ commit("REMOVE_QUESTIONNAIRE", question_id);
commit("SET_CURRENT_QUESTIONNAIRE_INDEX_TO_LAST");
},
diff --git a/frontend/src/store/getters.ts b/frontend/src/store/getters.ts
index daa2a15..0412ffc 100644
--- a/frontend/src/store/getters.ts
+++ b/frontend/src/store/getters.ts
@@ -8,6 +8,9 @@ export default {
getCurrentView: (state) => state.currentView || "",
getTheme: (state) => state.theme,
+ // Timer
+ getTimer: (state) => state.timer,
+
// Admin View
getAdminForceReload: (state) => (tab) => {
return state.adminForceReload[tab];
diff --git a/frontend/src/store/mutations.ts b/frontend/src/store/mutations.ts
index a5c707b..99bfae2 100644
--- a/frontend/src/store/mutations.ts
+++ b/frontend/src/store/mutations.ts
@@ -18,6 +18,8 @@ export default {
},
SET_THEME: (state: { theme: any }, theme: any) => (state.theme = theme),
+ SET_TIMER: (state: { timer: any }, timer: any) => (state.timer = timer),
+
SET_CURRENT_VIEW: (state: { currentView: string }, currentView: string) =>
(state.currentView = currentView),
@@ -73,12 +75,9 @@ export default {
if (state.questionnaires.length) state.showQuestionnaires = true;
},
- REMOVE_QUESTIONNAIRE: (
- state: { questionnaires: any },
- global_question_id: Number
- ) =>
+ REMOVE_QUESTIONNAIRE: (state: { questionnaires: any }, question_id: String) =>
(state.questionnaires = state.questionnaires.filter(
- (q) => q.global_question_id != global_question_id
+ (q) => q.id != question_id
)),
SET_SHOW_QUESTIONNAIRE_STATE: (
@@ -91,7 +90,7 @@ export default {
questionnaires: any;
showQuestionnaires: boolean;
}) => {
- state.currentQuestionnaireIndex = state.questionnaires.length - 1;
+ state.currentQuestionnaireIndex = 0;
if (state.questionnaires.length) {
state.showQuestionnaires = true;
} else {
@@ -101,4 +100,5 @@ export default {
SET_QUESTIONNAIRES: (state: { questionnaires: any }, payload: any) =>
(state.questionnaires = payload),
+
};
diff --git a/frontend/src/store/state.ts b/frontend/src/store/state.ts
index 8962714..6b87eb3 100644
--- a/frontend/src/store/state.ts
+++ b/frontend/src/store/state.ts
@@ -24,6 +24,9 @@ export default {
currentQuestionnaireIndex: 0,
showQuestionnaires: true,
+ // Timer
+ timer: null,
+
// Admin Reloads
adminForceReload: {
submissions: false,
@@ -31,5 +34,6 @@ export default {
notifications: false,
feedback: false,
questionnaire: false,
+ timer: false,
},
};
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index 6e53b32..c6cb68d 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -9,13 +9,12 @@ export interface ITabObject {
}
export interface IExerciseObject {
- id: number;
+ id: string;
exercise_type: string;
page_title: string;
root_weight: number;
child_weight: number;
local_exercise_id: number;
- global_exercise_id: string;
exercise_name: string;
parent_page_title: string;
parent_weight: number;
@@ -37,7 +36,7 @@ export interface IQuestionnaireQuestionObject {
multiple: boolean;
language: string;
answers: Array;
- global_question_id: string;
- global_questionnaire_id: string;
+ question_id: string;
+ questionnaire_id: string;
page_title: string;
}