diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96fca6d --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +.venv/ + +*.pyc +__pycache__/ + +instance/ + +.pytest_cache/ +.coverage +htmlcov/ + +dist/ +build/ +*.egg-info/ \ No newline at end of file diff --git a/SLAEForms Testing.code-workspace b/SLAEForms Testing.code-workspace new file mode 100644 index 0000000..4901684 --- /dev/null +++ b/SLAEForms Testing.code-workspace @@ -0,0 +1,10 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "liveServer.settings.multiRootWorkspaceName": "CSS Testing" + } +} \ No newline at end of file diff --git a/slaeforms/app.py b/slaeforms/app.py new file mode 100644 index 0000000..66d7574 --- /dev/null +++ b/slaeforms/app.py @@ -0,0 +1,623 @@ +import sys +import json +import random +import base64 +from flask import Flask, redirect, url_for, request, session, make_response, jsonify, send_from_directory +from flask import render_template +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import Integer, String, Column, Float +from datetime import datetime +import uuid +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy import inspect +from sqlalchemy.orm import DeclarativeBase +import os + +random_order = True +# activate environment: cd C:\Users\Jan\Google Drive\Master Stuff\Code\SLAEForms Testing\.venv\Scripts\ +# then this: activate + +#SETUP-------------------------------------------------- + +#Set up sqlalchemy +class Base(DeclarativeBase): + pass + +db = SQLAlchemy(model_class=Base) + +#create the app +app = Flask(__name__) +# configure the database, give it a path (it will be in the instances folder) +app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///database.db" +db.init_app(app) + +#set the secret key (TODO change this for final deployment) +app.secret_key = b"29fe9e8edd407c5491d4f1c05632d9fa33e26ed8734a3f5e080ebac3772a555a" + +UPLOAD_FOLDER = 'uploads' + + + + +#---------testing JSON Stuff +#open the json file with the config +testconfigfile = open("singleformconfig.json", encoding='utf-8') +#convert it to dict +testconfig = json.load(testconfigfile) +testconfigfile.close() +# get the questions: Questions is a list that contains the keys of the dictionary +questions = list(testconfig) + + + + + + +#------------------------------------------------------------------------ +# Setting up DB Models, response -> should be removed eventually and replaced by json files and automatic generation ---------------------------- + +# create the model for the response table +class Response(db.Model): + id = db.Column("id",db.UUID(as_uuid=True), primary_key=True, nullable=False) + user_id = db.Column("user_id",db.UUID(as_uuid=True), nullable=False) + question_title = db.Column("question_title",db.String(30)) + likert_result = db.Column("likert_result",db.Integer, nullable=False) + notes = db.Column("notes",db.String(200)) + date_created = db.Column("date_created",db.DateTime) + def __repr__(self) -> str: + return "" % self.id + +# This table is always created +class User(db.Model): + user_id = db.Column("user_id",db.UUID(as_uuid=True), primary_key=True, nullable=False) + device_id = db.Column("device_id",db.UUID(as_uuid=True), nullable=False) + question_order = db.Column("question_order",db.String(60)) + date_created = db.Column("date_created",db.DateTime, default=datetime.today()) # todo test if this default works + def __repr__(self) -> str: + return "" % self.user_id + +# create the table (existing tables are not overwritten) +try: + with app.app_context(): + db.create_all() +except SQLAlchemyError as e: + print("Error occurred during database creation:", str(e)) + + + +# -------Testing the actual final form from json------------------------------------------------ + + + + + + +#open the json file with the config +configfile = open("default.json", encoding='utf-8') #todo replace with other name +#convert it to dict +config = json.load(configfile) +configfile.close() +db_tables = {} # contains all dynamically created tables, key = table/classname + + +#TODO insert code to create all tables +def create_json_tables(): + print(config) + for block_key, block_content in config.items(): + if "database_table" in block_content: + print("debug") + if not (block_content["database_table"]["table_name"] in db_tables): + print("New table: \n {table}".format(table=block_content["database_table"]["table_name"])) + db_tables[block_content["database_table"]["table_name"]]=create_model_class(block_content["database_table"]) + print("created table: {table}".format(table=db_tables[block_content["database_table"]["table_name"]])) + + print("tables in db_tables: \n {db_tables}".format(db_tables=db_tables)) + +def create_model_class(schema): + class_name = schema["table_name"].capitalize() + print("creating table class: {class_name}".format(class_name=class_name)) + + # Define class attributes dynamically + attributes = {"__tablename__": schema["table_name"]} + + # id as key and date as standard fields + attributes["id"] = Column("id",db.UUID(as_uuid=True), primary_key=True, nullable=False) + attributes["user_id"] = Column("user_id",db.UUID(as_uuid=True), primary_key=True, nullable=False) + attributes["date_created"] = db.Column("date_created",db.DateTime) + attributes["stimulus_name"] = db.Column("stimulus_name",db.String(30)) + + + for column_name, column_info in schema["fields"].items(): + if column_info["type"] == "integer": + column_type = Integer + elif column_info["type"] == "string": + column_type = String(int(column_info["size"])) + if column_info["type"] == "float": + column_type = Float + + attributes[column_name] = Column(column_name,column_type, nullable=column_info["nullable"]) + + print("attributes of the table: ",attributes) + # Create the model class + return type(class_name, (db.Model,), attributes) + + + +create_json_tables() +# create the table (existing tables are not overwritten) +try: + print("try to create tables") + with app.app_context(): + db.create_all() + print("successfully created all tables") +except SQLAlchemyError as e: + print("Error occurred during database creation:", str(e)) + +@app.route("/teststart", methods=["GET", "POST"]) +def teststartpage(): + if not "slaeform_device_id" in session: + # If this device was not seen, remember it. + new_device_id = uuid.uuid4() + session["slaeform_device_id"] = new_device_id + session["agreed_to_tos"] = False + + + if request.method == "POST": + #right now if a user that has an active session goes to the startpage again and accepts tos + #it will just start another session and discard the previous session + + #config is the dict with the info + #get the block names + block_names = config.keys() + session["block_names"] = list(block_names) + session["block_order"] = {} # only for templates, for each block the list of keys for stimuli + session["current_block_index"] = 0 + session["current_stimulus_index"] = 0 + session["current_block_name"] = session["block_names"][session["current_block_index"]] + session["number_of_blocks"] = len(session["block_names"]) + current_block = config[session["current_block_name"]] + + # if the block has stimuli, get how many + if "stimuli" in current_block: + session["number_of_stimuli"] = len(list(current_block["stimuli"]["list"])) + + + print("number of blocks: ",len(session["block_names"])) + + print("Startpage post") + print(session["block_names"]) + + for name in block_names: + if config[name]["type"] == "TaskTemplate" and ("stimuli" in current_block): + match config[name]["stimuli"]["type"]: + case "single_video": + order = list(config[name]["stimuli"]["list"]) # order = list of simuli keys + print("order: ",order) + if config[name]["stimuli"]["order"] == "random": + random.shuffle(order) #in random order + session["block_order"][name] = order + case "empty": + order = list(config[name]["stimuli"]["list"]) # order = list of simuli keys + print("order: ",order) + session["block_order"][name] = order + + + if "stimuli" in current_block: + #get the name of the current stimulus + session["current_stimulus_name"] = session["block_order"][session["current_block_name"]][session["current_stimulus_index"]] + + #save the new user to the database and the session + session["agreed_to_tos"] = True + new_user_id = uuid.uuid4() + session["slaeform_user_id"] = new_user_id + device_id = session["slaeform_device_id"] + user_id = new_user_id + question_order = str(session["block_order"]) + date = datetime.today() + new_user = User(user_id=user_id, device_id=device_id,question_order=question_order,date_created = date) #,question_order=question_order + + db.session.add(new_user) + db.session.commit() + + print("block order: {order}".format(order=session["block_order"])) + try: + db.session.add(new_user) + db.session.commit() + + except: + return "There was a problem while adding the user to the Database" + return redirect("/jsonform") + + return render_template( + "teststartpage.html" + ) + + +@app.route("/jsonform") +def jsonform(): + #user is not yet registered and should not be here + if not "slaeform_user_id" in session: + return redirect("/teststart") #TODO replace this later with actual startpage + + + current_block = config[session["current_block_name"]] + print("jsonform") + print("current_block_name: {current_block_name}".format(current_block_name=session["current_block_name"])) + print("current_block_index: {current_block_order}".format(current_block_order=session["current_block_index"])) + print("current_stimulus: {current_stimulus}".format(current_stimulus=session["current_stimulus_index"])) + + #print("current Blockname: {blockname}, current block index: {blockindex}, current stim index: {stimulusindex}".format(blockname=session["current_block_name"], + # blockindex=session["current_block_index"], + # stimulusindex=session["current_stimulus_index"] ) ) + + # erster Fall: SinglePage + if current_block["type"] == "SinglePage": + return render_template(current_block["template"]) + + #zweiter Fall, empty TaskTemplate + if current_block["type"] == "TaskTemplate" and current_block["stimuli"]["type"] == "empty": + current_block_order = session["block_order"][session["current_block_name"]] + current_block_stimuli = current_block["stimuli"] + current_stimulus = current_block_order[session["current_stimulus_index"]] + stimulus_type=current_block["stimuli"]["type"] + return render_template( + "standard_template.html", + stimuli=current_block_stimuli, + stimulus_type=stimulus_type, + current_stimulus=current_stimulus, + questions=current_block["questions"] + ) + + + # ansonsten, templates: + current_block_order = session["block_order"][session["current_block_name"]] + current_block_stimuli = current_block["stimuli"] + current_stimulus = current_block_order[session["current_stimulus_index"]] + stimulus_type=current_block["stimuli"]["type"] + stimulus_configuration = current_block["stimuli"]["configuration"] # dict with the config + + + + if current_block["type"] == "TaskTemplate": + print("case: TaskTemplate") + match stimulus_type: + case "single_video": + stimulus_configuration["video_url"] = config[session["current_block_name"]]["stimuli"]["list"][current_stimulus] + print("-------------videourl: ", stimulus_configuration["video_url"]) + return render_template( + "standard_template.html", + stimuli=current_block_stimuli, + stimulus_type=stimulus_type, + current_stimulus=current_stimulus, + stimulus_configuration=stimulus_configuration, + questions=current_block["questions"] + ) + + + + + return "Error, none of the Blocks triggered" + + +@app.route("/send_json", methods=["POST"]) +def sendpage_json(): + print("send_json") + # Do I need to write to a table at all? + # I can figure it out by checking if the current block has a database field, that is best + if not ("database_table" in config[session["current_block_name"]]): #it has no database field, so nothing to receive + # so just move on + print("no database table") + update_session() + return redirect("/jsonform") + + # now to if it has a database field + + # find out which table we need to write to + table_name = config[session["current_block_name"]]["database_table"]["table_name"] + print("Form posted: {rqform}".format(rqform=request.form)) + print("Writing to table: {table_name}".format(table_name=table_name)) + session_user_id = session["slaeform_user_id"] + new_id = uuid.uuid4() + date = datetime.today() + stimulus_name = session["current_stimulus_name"] + + new_entry = db_tables[table_name](id=new_id,user_id = session_user_id,date_created = date,stimulus_name=stimulus_name) + + for key, value in request.form.items(): + print("hasattr key: ", key) + if hasattr(new_entry, key): + print("key exists: ", key) + setattr(new_entry, key, value) + print("setattr value: ", value) + print("entry: ", new_entry) + try: + db.session.add(new_entry) + db.session.commit() + except Exception as e: + print("Error occurred: {e}".format(e=str(e))) + return "There was a problem while adding the response to the Database" + + # handle possible Video that was send + if 'recordedVideo' in request.files: + video = request.files['recordedVideo'] + formatted_date = date.strftime("%Y.%m.%d %H-%M-%S") + print("date: ", date) + video_name = str(session_user_id) + "_" + session["current_block_name"] + "_" + session["current_stimulus_name"] + "_" + str(formatted_date) + ".webm" + path = os.path.join(UPLOAD_FOLDER, video_name) + print("path: ",path) + os.makedirs(UPLOAD_FOLDER, exist_ok=True) + video.save(path) + + + # Now move to the next stimulus or block + update_session() + print("now redirect and reload the page") + return redirect("/jsonform") + +def update_session(): + if "stimuli" in config[session["current_block_name"]]: + # if there are stimuli in this block + if session["current_stimulus_index"] < session["number_of_stimuli"]-1: + # if there are still stimuli left, keep going through them + session["current_stimulus_index"] += 1 + # set the name of the current stimulus + session["current_stimulus_name"] = session["block_order"][session["current_block_name"]][session["current_stimulus_index"]] + + else: + # if there are no stimuli left.. + if(session["current_block_index"] < session["number_of_blocks"]-1): + # go to next block if possible + session["current_block_index"] += 1 + session["current_block_name"] = session["block_names"][session["current_block_index"]] + session["current_stimulus_index"] = 0 + if "stimuli" in config[session["current_block_name"]]: + session["number_of_stimuli"] = len(list(config[session["current_block_name"]]["stimuli"]["list"])) + + else: + # if there arent any stimuli, go to the next block + session["number_of_stimuli"] = 0 + session["current_stimulus_index"] = 0 + if(session["current_block_index"] < session["number_of_blocks"]-1): + session["current_block_index"] += 1 + session["current_block_name"] = session["block_names"][session["current_block_index"]] + + + print("---Session updated---") + print("current_block_index / number_of_blocks: {current_block_index} / {number_of_blocks}".format(current_block_index=session["current_block_index"],number_of_blocks=session["number_of_blocks"])) + print("current_block_name: ", session["current_block_name"]) + print("current_stimulus_index: ", session["current_stimulus_index"]) + print("Current number_of_stimuli: ", session["number_of_stimuli"]) + + + + + + +# Actual main code for Form etc -------------------------------------------------------------- + +@app.route("/video") +def videopage(): + + return render_template( + #"videorecorder3.html" + "myvideotemplate.html" + ) + +@app.route("/send_video", methods=["POST"]) +def send_video(): + data_url = request.json['dataUrl'] + data = data_url.split(',')[1] + with open('video.webm', 'wb') as f: + f.write(base64.b64decode(data)) + return jsonify({'message': 'Video saved successfully'}) + + + +@app.route("/send", methods=["POST"]) +def sendpage(): + session_user_id = session["slaeform_user_id"] + likert_score = request.form["likertscale"] + text_input = request.form["feedback"] + question_title = session["current_question"] + new_id = uuid.uuid4() + date = datetime.today() + print("new idea: {new_id} ".format(new_id=new_id)) + new_response = Response(id=new_id,user_id = session_user_id, question_title = question_title,likert_result = likert_score,notes = text_input, date_created = date) + + try: + db.session.add(new_response) + db.session.commit() + return redirect("/form") + except: + return "There was a problem while adding the response to the Database" + + +#TODO this is bugged rn, it will skip to the next question when refreshed. +#Move the removal of the first element to the send function to fix this +@app.route("/form", methods=["GET", "POST"]) +def formpage(): + #user is not yet registered and should not be here + if not "slaeform_user_id" in session: + return redirect("/start") + + + #TODO fill in code that determins at which question the user is + print("form starts, the sessionorder rn:") + print(session["question_order"]) + if not session["question_order"]: + print("---------------question order is empty------------") + return redirect("/data") + + print("pop the first element") + current_question = session["question_order"].pop(0) + session["current_question"] = current_question + print(current_question) + print("the new sessionorder rn:") + print(session["question_order"]) + + return render_template( + "layout2.html", + config=testconfig, + current_question = current_question, + videotype=testconfig[current_question]["type"], + video_url= testconfig[current_question]["video1"], + blocks = testconfig[current_question]["blocks"] + ) + + +@app.route("/start", methods=["GET", "POST"]) +def startpage(): + if not "slaeform_device_id" in session: + # If this device was not seen, remember it. + new_device_id = uuid.uuid4() + session["slaeform_device_id"] = new_device_id + session["agreed_to_tos"] = False + + if request.method == "POST": + #right now if a user that has an active session goes to the startpage again and accepts tos + #it will just start another session and discard the previous session + + # get a random question order + if random_order: + order = questions + random.shuffle(order) + else: + order = questions + session["question_order"] = order + + + #save the new user to the database and the session + session["agreed_to_tos"] = True + new_user_id = uuid.uuid4() + session["slaeform_user_id"] = new_user_id + device_id = session["slaeform_device_id"] + user_id = new_user_id + question_order = str(order) + date = datetime.today() + new_user = User(user_id=user_id, device_id=device_id,question_order=question_order,date_created = date) #,question_order=question_order + + db.session.add(new_user) + db.session.commit() + return redirect("/form") + """ + try: + db.session.add(new_user) + db.session.commit() + return redirect("/form") + except: + return "There was a problem while adding the user to the Database" """ + + return render_template( + "startpage.html" + ) + + + + + + + +# Database stuff------------------------------------------------------------------------------ + +# the contents of all tables +@app.route("/table_contents") +def table_contents(): + meta = db.metadata + #meta.reflect(db.engine) # Uncomment this to also get the hidden tables, but this crashes rn + tables = meta.tables.keys() + table_contents = {} + #print(tables) + + for table_name in tables: + table = meta.tables[table_name] + columns = table.columns.keys() + #print(table) + rows = db.session.query(table) + try: + rows = rows.all() + except Exception as e: + print("Error occurred: {e}".format(e=str(e))) + print("Values:\n Table = {table}\n Tabletype = {tabletype}\n".format(table=table,tabletype=type(table))) + + table_contents[table_name] = { + 'columns': columns, + 'rows': rows + } + + return render_template( + "table_contents.html", + table_contents=table_contents, + ) + + + +@app.route('/show_tables') +def show_tables(): + meta = db.metadata + meta.reflect(db.engine) + tables = meta.tables + return render_template('show_tables.html', tables=tables) + +# Root page ----------------------------- + +def has_no_empty_params(rule): + defaults = rule.defaults if rule.defaults is not None else () + arguments = rule.arguments if rule.arguments is not None else () + return len(defaults) >= len(arguments) + +@app.route("/") +def all_links(): + links = [] + for rule in app.url_map.iter_rules(): + # Filter out rules we can't navigate to in a browser + # and rules that require parameters + if "GET" in rule.methods and has_no_empty_params(rule): + url = url_for(rule.endpoint, **(rule.defaults or {})) + links.append((url, rule.endpoint)) + return render_template("all_links.html", links=links) + +# delete all tables as last link -------------------------- + +# Route to delete all entries +@app.route('/delete_json_tables', methods=['GET']) +def delete_json_tables(): + try: + meta = db.metadata + meta.reflect(db.engine) + for table in reversed(meta.sorted_tables): # Iterate through tables in reverse order to handle foreign key constraints + if table.name != "user" and table.name != "response": + print("Deleting Table: {name}".format(name=table.name)) + db.session.execute(table.delete()) + + db.session.commit() + return 'All entries deleted successfully' + except Exception as e: + # Rollback changes if any error occurs + db.session.rollback() + return f'Error occurred: {str(e)}', 500 + finally: + # Close the session + db.session.close() + + +# Route to delete all entries +@app.route('/delete_all_entries', methods=['GET']) +def delete_all_entries(): + # here I could also use a "drop_all()", that works jsut like create all from the creation part + # this together with the reflect could drop actually all tables + try: + meta = db.metadata + for table in reversed(meta.sorted_tables): # Iterate through tables in reverse order to handle foreign key constraints + db.session.execute(table.delete()) + db.session.commit() + return 'All entries deleted successfully' + except Exception as e: + # Rollback changes if any error occurs + db.session.rollback() + return f'Error occurred: {str(e)}', 500 + finally: + # Close the session + db.session.close() + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/slaeforms/attributions.txt b/slaeforms/attributions.txt new file mode 100644 index 0000000..d76b728 --- /dev/null +++ b/slaeforms/attributions.txt @@ -0,0 +1,59 @@ +# Likert Scale +https://jamesalvarez.co.uk/blog/how-to-make-responsive-likert-scales-in-css-(like-qualtrics)/ + + +# Rootpage that shows all defined routes +https://stackoverflow.com/questions/13317536/get-list-of-all-routes-defined-in-the-flask-app + + +Licenses: + +Open Iconic Icon Set (https://github.com/iconic/open-iconic) + +The MIT License (MIT) + +Copyright (c) 2014 Waybury + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +Bootstrap Icons https://github.com/twbs/icons + +The MIT License (MIT) + +Copyright (c) 2019-2024 The Bootstrap Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/slaeforms/currenturls.txt b/slaeforms/currenturls.txt new file mode 100644 index 0000000..4caeb27 --- /dev/null +++ b/slaeforms/currenturls.txt @@ -0,0 +1,32 @@ +"/" Blank page +'/delete_all_entries' delete entries from database (not actually all, hardcoded) +"/data" Show Data of some tables (Responses and users) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/slaeforms/defaul_wip.json b/slaeforms/defaul_wip.json new file mode 100644 index 0000000..5d4b74f --- /dev/null +++ b/slaeforms/defaul_wip.json @@ -0,0 +1,21 @@ +{ + "Block_1":{ + "type": "SinglePage", + "template": "startpage.html", + "database_table" :{ + "TableName": "Datenschutzerklaerung", + "Fields": { + } + } + }, + "Block 2":{ + "type": "TaskTemplate", + "tempalte": "tempaltetest1.html", + "name" : "Block2Responses", + "database_table": { + "TableName": "Datenschutzerklaerung", + "Fields": { + } + } + } +} \ No newline at end of file diff --git a/slaeforms/default.json b/slaeforms/default.json new file mode 100644 index 0000000..44db713 --- /dev/null +++ b/slaeforms/default.json @@ -0,0 +1,230 @@ +{ + "Block 4":{ + "type": "TaskTemplate", + "tempalte": "standard_template.html", + "number_of_pages":"3", + "stimuli":{ + "type":"single_video", + "order": "random", + "list":{ + "video_1":"https://www.youtube-nocookie.com/embed/VtnwHmabyzo?si=H3rrG-GHtlSymR70", + "video_2":"https://www.youtube-nocookie.com/embed/EL76Ok4r0aQ?si=hqUm8eUUfX39NN4L", + "video_3":"https://www.youtube-nocookie.com/embed/XTMIomsXxKM?si=r2zB6OKERH6Jdpi6" + }, + "configuration":{ + "embed":"yt" + } + }, + "questions":{ + "question1":{ + "type": "likert", + "name": "likertscale", + "text": "How would you rate this video?", + "required": "true", + "points":{ + "p1":{ + "value":"1", + "text":"I dont like it at all" + }, + "p2":{ + "value":"2", + "text":"I dont like it" + }, + "p3":{ + "value":"3", + "text":"I am indifferent" + }, + "p4":{ + "value":"4", + "text":"I like it" + }, + "p5":{ + "value":"5", + "text":"I like it a lot" + } + } + }, + "question2":{ + "type": "textinput", + "name": "text_feedback", + "text": "Here you can give us Feedback", + "required": "false", + "size": "250" + }, + "question3":{ + "type": "videoinput", + "text": "Here you can give us Feedback as video", + "name": "video_feedback", + "required": "false" + } + }, + "database_table" :{ + "table_name": "default_block3_test", + "fields": { + "likertscale":{ + "type": "integer", + "nullable": "false" + }, + "text_feedback":{ + "type": "string", + "size": "250", + "nullable": "true" + } + } + } + }, + "Block 3":{ + "type": "TaskTemplate", + "tempalte": "standard_template.html", + "stimuli":{ + "type":"empty", + "list":{ + "empty_stimulus":"" + } + }, + "questions":{ + "question1_alter":{ + "type": "numberinput", + "name": "alter", + "text": "Alter:", + "required": "true", + "min": "1", + "max": "120" + }, + "question2_geschlecht":{ + "type": "dropdowninput", + "name": "geschlecht", + "text": "Geschlecht:", + "required": "true", + "defaulttext": "", + "points":{ + "männlich":{ + "value":"Männlich", + "text":"Männlich" + }, + "weiblich":{ + "value":"Weiblich", + "text":"Weiblich" + }, + "divers":{ + "value":"Divers", + "text":"Divers" + }, + "keine_angabe":{ + "value":"keine_angabe", + "text":"Keine Angabe" + } + } + }, + "question3_hoerstatus":{ + "type": "dropdowninput", + "name": "hoerstatus", + "text": "Hörstatus:", + "required": "true", + "defaulttext": "", + "points":{ + "hörend":{ + "value":"Hörend", + "text":"Hörend" + }, + "schwerhörig":{ + "value":"Schwerhörig", + "text":"Schwerhörig" + }, + "gehörlos":{ + "value":"Gehörlos", + "text":"Gehörlos" + } + } + }, + "question4_bevorzugte_kommunikation":{ + "type": "dropdowninput", + "name": "bevorzugte_kommunikation", + "text": "Bevorzugte Kommunikationsform:", + "required": "true", + "defaulttext": "", + "points":{ + "gesprochen":{ + "value":"Gesprochene Sprache", + "text":"Gesprochene Sprache" + }, + "text":{ + "value":"Text", + "text":"Text" + }, + "gebärdensprache":{ + "value":"Gebärdensprache", + "text":"Gebärdensprache" + } + } + }, + "question5_gebeardenzeitraum":{ + "type": "numberinput", + "name": "gebärdenzeitraum", + "text": "Wie viele Jahre verwenden sie schon Gebärdensprache:", + "required": "true", + "min": "0", + "max": "100", + "step": "0.5" + }, + "question6_sprachkompetenz":{ + "type": "numberinput", + "name": "gebärdensprachkompetenz", + "text": "Wie schätzen sie ihre Gebärdensprachkompetenz ein (1-10):", + "required": "true", + "min": "1", + "max": "10" + } + }, + "database_table" :{ + "table_name": "default_demographic_test", + "fields": { + "alter":{ + "type": "integer", + "nullable": "false" + }, + "geschlecht":{ + "type": "string", + "size": "14", + "nullable": "false" + }, + "hoerstatus":{ + "type": "string", + "size": "14", + "nullable": "false" + }, + "bevorzugte_kommunikation":{ + "type": "string", + "size": "22", + "nullable": "false" + }, + "gebärdenzeitraum":{ + "type": "float", + "nullable": "false" + }, + "gebärdensprachkompetenz":{ + "type": "integer", + "nullable": "false" + } + } + } + }, + "Block_1":{ + "type": "SinglePage", + "template": "test_page0.html" + }, + "Block_2":{ + "type": "SinglePage", + "template": "test_page1.html", + "database_table" :{ + "table_name": "Datenschutzerklaerung", + "fields": { + "accepted":{ + "type": "string", + "size": "7", + "nullable": "false" + } + } + } + } +} \ No newline at end of file diff --git a/slaeforms/dicttablecode.py b/slaeforms/dicttablecode.py new file mode 100644 index 0000000..6824e8e --- /dev/null +++ b/slaeforms/dicttablecode.py @@ -0,0 +1,133 @@ +import sys +import json +import random +import base64 +from flask import Flask, redirect, url_for, request, session, make_response, jsonify, send_from_directory +from flask import render_template +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import Integer, String, Column +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.dialects.postgresql import UUID +from datetime import datetime +import uuid +from sqlalchemy.exc import SQLAlchemyError + + + + +random_order = True + +# activate environment: cd C:\Users\Jan\Google Drive\Master Stuff\Code\SLAEForms Testing\.venv\Scripts\ +# then this: activate + +#create the app +app = Flask(__name__) +# configure the database, give it a path (it will be in the instances folder) +app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///database.db" +db = SQLAlchemy(app) + +#set the secret key (TODO change this for final deployment) +app.secret_key = b"29fe9e8edd407c5491d4f1c05632d9fa33e26ed8734a3f5e080ebac3772a555a" + + +#--------temporär der code für dict to db test +database_schema = { + "table_name": "userstest", + "id": {"type": "integer", "nullable": False}, + "username": {"type": "string", "nullable": False, "size": 20}, + "age": {"type": "integer", "nullable": True} +} + +tablename = database_schema["table_name"].capitalize() + +# test function to create tables from dicts +def create_model_class(schema): + class_name = schema["table_name"].capitalize() + + # Define class attributes dynamically + attributes = {"__tablename__": schema["table_name"]} + + attributes["uid"] = Column(db.UUID(as_uuid=True), primary_key=True, nullable=False) + + + for column_name, column_info in schema.items(): + if column_name != "table_name": + if column_info["type"] == "integer": + column_type = Integer + elif column_info["type"] == "string": + column_type = String(column_info["size"]) + + attributes[column_name] = Column(column_type, nullable=column_info["nullable"]) + + # Create the model class + return type(class_name, (db.Model,), attributes) + +print("creating the userstest table") +customtable = create_model_class(database_schema) +try: + with app.app_context(): + print("try to create tables") + db.create_all() +except SQLAlchemyError as e: + print("Error occurred during database creation:", str(e)) + +@app.route("/customsendtest", methods=["POST"]) +def customsendtest(): + # Extract form data + form_data = request.form + + # Create a new instance of the dynamically generated model + new_user = customtable() + + # Assign form data to model attributes + for key, value in form_data.items(): + if hasattr(new_user, key): + setattr(new_user, key, value) + new_id = uuid.uuid4() + setattr(new_user, "uid",new_id) + + # Add new user to the database session and commit changes + try: + #print("new idea: {new_id} ".format(new_id=new_id)) + db.session.add(new_user) + db.session.commit() + return 'Data submitted successfully!' + except SQLAlchemyError as e: + print("Error occurred during database commit:", str(e)) + return 'Data not submitted successfully!' + + +@app.route("/customsend/", methods=["POST"]) +def customsend(): + + session_user_id = session["slaeform_user_id"] + likert_score = request.form["likertscale"] + text_input = request.form["feedback"] + question_title = session["current_question"] + new_id = uuid.uuid4() + date = datetime.today() + print("new idea: {new_id} ".format(new_id=new_id)) + new_response = Response(id=new_id,user_id = session_user_id, question_title = question_title,likert_result = likert_score,notes = text_input, date_created = date) + + try: + db.session.add(new_response) + db.session.commit() + return redirect("/form") + except: + return "There was a problem while adding the response to the Database" + + +# End testing------------------------------------- + + + +# create the model for the response table +class Response(db.Model): + id = db.Column(db.UUID(as_uuid=True), primary_key=True, nullable=False) + user_id = db.Column(db.UUID(as_uuid=True), nullable=False) + question_title = db.Column(db.String(30)) + likert_result = db.Column(db.Integer, nullable=False) + notes = db.Column(db.String(200)) + date_created = db.Column(db.DateTime) + def __repr__(self) -> str: + return "" % self.id \ No newline at end of file diff --git a/slaeforms/documentation.json b/slaeforms/documentation.json new file mode 100644 index 0000000..6768177 --- /dev/null +++ b/slaeforms/documentation.json @@ -0,0 +1,61 @@ +{ + "Block_1":{ + "type": "single_page", + "html_template": "the jinja templat in the template folder that should be used", + "database_table" :{ + "table_name": "Name of the table in the database", + "fields": { + "answer_1": { + "type": "string or integer", + "nullable": "true (must have a value) or false (must not have a value)", + "size" : "max number of chars in the string (only usable with type:string)" + } + } + } + }, + "Block 2":{ + "type": "template", + "html_template": "the jinja templat in the template folder that should be used", + "template_blocks":{ + "likert_block_1":{ + "type":"likert", + "scale_size":"7", + "text_left":"bad", + "text_right":"good" + } + }, + "content":{ + "item that should be given to the template":{ + "type":"list", + "values":{ + "1":"whatever", + "2":"also whatever" + } + } + }, + "database_table" :{ + "table_name": "Name of the table in the database", + "fields": { + "answer_1": { + "type": "string or integer", + "nullable": "true (must have a value) or false (must not have a value)", + "size" : "max number of chars in the string (only usable with type:string)" + } + } + } + }, + "Block 3":{ + "type": "custom_template", + "html_template": "the jinja templat in the template folder that should be used", + "database_table" :{ + "table_name": "Name of the table in the database", + "fields": { + "answer_1": { + "type": "string or integer", + "nullable": "true (must have a value) or false (must not have a value)", + "size" : "max number of chars in the string (only usable with type:string)" + } + } + } + } +} \ No newline at end of file diff --git a/slaeforms/jsonparsecode.py b/slaeforms/jsonparsecode.py new file mode 100644 index 0000000..b9832a8 --- /dev/null +++ b/slaeforms/jsonparsecode.py @@ -0,0 +1,80 @@ +import sys +import json +import random +import base64 +from flask import Flask, redirect, url_for, request, session, make_response, jsonify, send_from_directory +from flask import render_template +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import Integer, String, Column +from datetime import datetime +import uuid +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy import inspect +from sqlalchemy.orm import DeclarativeBase + +random_order = True +# activate environment: cd C:\Users\Jan\Google Drive\Master Stuff\Code\SLAEForms Testing\.venv\Scripts\ +# then this: activate + +#SETUP-------------------------------------------------- + +#Set up sqlalchemy +class Base(DeclarativeBase): + pass + +db = SQLAlchemy(model_class=Base) + +#create the app +app = Flask(__name__) +# configure the database, give it a path (it will be in the instances folder) +app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///database.db" +db.init_app(app) + +#set the secret key (TODO change this for final deployment) +app.secret_key = b"29fe9e8edd407c5491d4f1c05632d9fa33e26ed8734a3f5e080ebac3772a555a" + + +#---------testing +#open the json file with the config +configfile = open("singleformconfig.json") +#convert it to dict +config = json.load(configfile) +configfile.close() +# get the questions: Questions is a list that contains the keys of the dictionary +questions = list(config) + + +@app.route("/form", methods=["GET", "POST"]) +def formpage(): + #user is not yet registered and should not be here + if not "slaeform_user_id" in session: + return redirect("/start") + + + #TODO fill in code that determins at which question the user is + print("form starts, the sessionorder rn:") + print(session["question_order"]) + if not session["question_order"]: + print("---------------question order is empty------------") + return redirect("/data") + + print("pop the first element") + current_question = session["question_order"].pop(0) + session["current_question"] = current_question + print(current_question) + print("the new sessionorder rn:") + print(session["question_order"]) + session["question_order"] = session["question_order"] + print("has this changed sth?:") + print(session["question_order"]) + + + + return render_template( + "layout2.html", + config=config, + current_question = current_question, + videotype=config[current_question]["type"], + video_url= config[current_question]["video1"], + blocks = config[current_question]["blocks"] + ) \ No newline at end of file diff --git a/slaeforms/pairwiseformconfig.json b/slaeforms/pairwiseformconfig.json new file mode 100644 index 0000000..89818ee --- /dev/null +++ b/slaeforms/pairwiseformconfig.json @@ -0,0 +1,17 @@ +{ + "question 1":{ + "type": "pairwise", + "video1": "https://www.youtube-nocookie.com/embed/VtnwHmabyzo?si=H3rrG-GHtlSymR70", + "video2": "https://www.youtube-nocookie.com/embed/PdvSPBdX2T0?si=8R2ZAMnPuoe50X-7" + }, + "question 2":{ + "type": "pairwise", + "video1": "https://www.youtube-nocookie.com/embed/EL76Ok4r0aQ?si=hqUm8eUUfX39NN4L", + "video2": "https://www.youtube-nocookie.com/embed/xIkdJeXkQIU?si=7a5WmlVtZy00JaNX" + }, + "question 3":{ + "type": "pairwise", + "video1": "https://www.youtube-nocookie.com/embed/XTMIomsXxKM?si=r2zB6OKERH6Jdpi6", + "video2": "https://www.youtube-nocookie.com/embed/keEKlr2dG-I?si=NZ4Q-aL56d3baz0t" + } +} \ No newline at end of file diff --git a/slaeforms/singleformconfig.json b/slaeforms/singleformconfig.json new file mode 100644 index 0000000..00ab628 --- /dev/null +++ b/slaeforms/singleformconfig.json @@ -0,0 +1,63 @@ +{ + "question 1":{ + "type": "single", + "video1": "https://www.youtube-nocookie.com/embed/VtnwHmabyzo?si=H3rrG-GHtlSymR70", + "blocks": { + "block1":{ + "type": "likert", + "numberofpoints": "5", + "points":{ + "point1": "1", + "point2": "2", + "point3": "3", + "point4": "4", + "point5": "5" + } + }, + "block2":{ + "type": "textinput", + "length": "200" + } + } + }, + "question 2":{ + "type": "single", + "video1": "https://www.youtube-nocookie.com/embed/EL76Ok4r0aQ?si=hqUm8eUUfX39NN4L", + "blocks": { + "block1":{ + "type": "likert", + "numberofpoints": "5", + "points":{ + "point1": "1", + "point2": "2", + "point3": "3", + "point4": "4", + "point5": "5" + } + }, + "block2":{ + "type": "textinput", + "length": "200" + } + } + }, + "question 3":{ + "type": "single", + "video1": "https://www.youtube-nocookie.com/embed/XTMIomsXxKM?si=r2zB6OKERH6Jdpi6", + "blocks": { + "block1":{ + "type": "likert", + "numberofpoints": "3", + "points":{ + "point1": "left", + "point2": "none", + "point3": "right" + } + }, + "block2":{ + "type": "textinput", + "length": "200" + } + } + } +} \ No newline at end of file diff --git a/slaeforms/static/icons/camera-icon.png b/slaeforms/static/icons/camera-icon.png new file mode 100644 index 0000000..7d15774 Binary files /dev/null and b/slaeforms/static/icons/camera-icon.png differ diff --git a/slaeforms/static/icons/camera-off-icon.png b/slaeforms/static/icons/camera-off-icon.png new file mode 100644 index 0000000..bf302a6 Binary files /dev/null and b/slaeforms/static/icons/camera-off-icon.png differ diff --git a/slaeforms/static/icons/camera-reels-icon.png b/slaeforms/static/icons/camera-reels-icon.png new file mode 100644 index 0000000..d948dc0 Binary files /dev/null and b/slaeforms/static/icons/camera-reels-icon.png differ diff --git a/slaeforms/static/icons/check-icon.png b/slaeforms/static/icons/check-icon.png new file mode 100644 index 0000000..5e3e8c2 Binary files /dev/null and b/slaeforms/static/icons/check-icon.png differ diff --git a/slaeforms/static/icons/favicon.ico b/slaeforms/static/icons/favicon.ico new file mode 100644 index 0000000..aea1c7c Binary files /dev/null and b/slaeforms/static/icons/favicon.ico differ diff --git a/slaeforms/static/icons/record-icon.png b/slaeforms/static/icons/record-icon.png new file mode 100644 index 0000000..10235a8 Binary files /dev/null and b/slaeforms/static/icons/record-icon.png differ diff --git a/slaeforms/static/icons/stop-icon.png b/slaeforms/static/icons/stop-icon.png new file mode 100644 index 0000000..7ac9c08 Binary files /dev/null and b/slaeforms/static/icons/stop-icon.png differ diff --git a/slaeforms/static/icons/trash-icon.png b/slaeforms/static/icons/trash-icon.png new file mode 100644 index 0000000..8c33162 Binary files /dev/null and b/slaeforms/static/icons/trash-icon.png differ diff --git a/slaeforms/static/icons/x-icon.png b/slaeforms/static/icons/x-icon.png new file mode 100644 index 0000000..d210e49 Binary files /dev/null and b/slaeforms/static/icons/x-icon.png differ diff --git a/slaeforms/static/styles.css b/slaeforms/static/styles.css new file mode 100644 index 0000000..acc8973 --- /dev/null +++ b/slaeforms/static/styles.css @@ -0,0 +1,236 @@ +html { + height: 100%; +} + +body { + width: 100%; + height: 100%; + margin: 0; + /*align-items: center; this will make the content a square, with edges up and bottom*/ + background-color: #a4b5ff; + color: #000000; + font-family: Tahoma; + font-size: 16px; +} + +.centercontent { + height: 100%; + display: flex; + justify-content: center; +} +.container { + height: 100%; + + width: 80%; /* You can adjust this width as needed */ + max-width: 1200px; /* Maximum width to keep it from getting too wide on large screens */ + padding: 20px; + background-color: #7b8cdb; /* Just for visual differentiation */ + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} + + + +.spacer { + clear: both; + box-sizing: border-box; +} + +.buttondisable { + filter: invert(65%); +} + +#submitbutton { + cursor: pointer; + padding: 10px 20px; /* Increased padding for a bigger button */ + font-size: 20px; /* Increased font size */ + border: none; /* Removes default border */ + border-radius: 8px; /* Optional: rounds the corners of the button */ +} +#submitbutton:hover { + background-color: #0056b3; /* Darker shade for hover effect */ +} +.button-container { + display: flex; + justify-content: flex-end; + margin-top: 20px; /* Adjust as needed for spacing */ +} + +.textarea-container { + display: flex; + flex-direction: column; + align-items: center; +} +.textarea-label{ + align-self: flex-start; /* Aligns the label to the start of the container */ +} + + +video { + max-width: 100%; + max-height: 100%; + width: auto; + height: auto; +} +.aspect-ratio-16-9 { + width: 100%; + aspect-ratio: 16 / 9; + } + +.videocontrols { + width: 100px; /* Set a specific width for the buttons */ + height: 70px; /* Set a specific height for the buttons */ + background-color: #cae4ff; + border: none; + color: white; + padding: 10px 20px; + margin: 0 10px; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s ease; + display: inline-flex; /* Display button contents as a flexbox */ + justify-content: center; /* Center contents horizontally */ + align-items: center; /* Center contents vertically */ + text-align: center; +} + +.videocontrols img { + max-width: 65%; + max-height: 65%; + text-align: center; + margin: auto; + width: auto; /* Make the image fill its container */ + height: auto; /* Make the image fill its container */ + display: block; /* Remove any extra space around the image */ +} + +.columncontainer { + display: flex; +} + +.columnleft { + width: 100%; + border: 3px solid black; +} +.columnright { + width: 100%; + border: 3px solid black; +} + +iframe { + display:block; + margin: auto; +} + +.video-container { + position: relative; + padding-bottom: 56.25%; /* 16:9 aspect ratio */ + height: 0; + overflow: hidden; + max-width: 100%; + background: #000; +} + +.video-container iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +form { + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + +} + +.centertext { + text-align: center; +} + +h2,h3 { + text-align: center; +} + + +.likert { + --likert-rows: 5; + margin: auto; + text-align: center; + display: inline-grid; + max-width: 900px; + grid-auto-rows: 1fr; + gap: 1em; + grid-template-columns: repeat(var(--likert-rows), minmax(0, 1fr)); +} + +@media only screen and (max-width: 680px) { + .likert { + grid-template-columns: minmax(0, 400px); + justify-content: center; + } +} + +input, label { + display: block; + margin: 0.5rem 0; + } + +.likert input { + max-width: 250px; + position: fixed; + opacity: 0; + pointer-events: none; +} + +.likercontainer{ + margin: 30px auto; + text-align: center; +} +.likert span { + border-radius: 5px; + display: flex; + justify-content: center; + align-items: center; + text-align: center; + box-sizing: border-box; + width: 100%; + height: 100%; + padding: 20px; + background: #dcdcdc; + transition: background .2s ease-in-out; +} + +.likert input:checked + span { + outline: black auto 1px; + background: transparent; +} + +.likert input:focus + span { + outline: auto 1px; /*-webkit-focus-ring-color*/ +} + +.likert span:hover { + background: #f1f1f1; + outline: lightgrey auto 0.5px; +} + +table { + border-collapse: collapse; + margin: 20px 0; + table-layout: auto; /* Allows columns to adjust to their content */ + width: auto; /* Adjusts the table width to the content */ +} + +th, td { + border: 1px solid #dddddd; + text-align: left; + padding: 8px; + word-wrap: break-word; /* Ensures content wraps and doesn't overflow */ +} + +th { + background-color: #f2f2f2; +} \ No newline at end of file diff --git a/slaeforms/static/videoscript.js b/slaeforms/static/videoscript.js new file mode 100644 index 0000000..49adb5d --- /dev/null +++ b/slaeforms/static/videoscript.js @@ -0,0 +1,159 @@ +const buttonCamera = document.getElementById('buttonCamera'); +const buttonRecord = document.getElementById('buttonRecord'); +const buttonDelete = document.getElementById('buttonDelete'); +const videoDisplay = document.getElementById('videoDisplay'); +const buttonCameraIcon = document.getElementById('buttonCameraIcon'); +const buttonRecordIcon = document.getElementById('buttonRecordIcon'); +const buttonDeleteIcon = document.getElementById('buttonDeleteIcon'); +const videoContainer = document.getElementById('videoContainer'); +var mediaRecorder = null; +var stream = null; +let recordedVideoBlob = null; +let isRecording = false; +let videoAccess = false; + +// Handle form submission +document.getElementById("question_form").addEventListener("submit", function (event) { + event.preventDefault(); // Prevent the default form submission + console.log("submit button pressed"); + // Create a FormData object + const formData = new FormData(event.target); + + console.log("form data: ",formData); + + // Append the recorded video Blob to the FormData object + if (recordedVideoBlob) { + console.log("video is available: ", recordedVideoBlob); + formData.append("recordedVideo", recordedVideoBlob, "recordedVideo.webm"); + } + + + console.log("Data to be submitted: ",formData); + + // Use fetch to send the form data and video to the backend + console.log("try to send the form and video"); + fetch("/send_json", { + method: "POST", + body: formData + }).then(response => { + console.log('Response:', response); + // Check if response is a redirect (HTTP 3xx status) + if (response.redirected) { + console.log("Redirecting to:", response.url); + window.location.href = response.url; // Redirect to the new page + } else { + console.log("Non-redirect response received."); + // Handle other responses if needed + } + }) + .catch(error => { + console.error('Error:', error); + // Handle errors if fetch fails + }); +}); + +async function cameraButton() { + if (!videoAccess) { + console.log("cameraButton case videoAccess = false"); + try { + stream = await navigator.mediaDevices.getUserMedia({ + video: true, + }); + } catch (error) { + console.log("Error: ", error); + return; + } + console.log("stream is active"); + videoAccess = true; + + videoDisplay.srcObject = stream; + + buttonCameraIcon.src = ICON_PATHS.cameraofficon; + buttonCameraIcon.alt = "Camera-off Icon"; + buttonRecord.style.display = 'inline-block'; + buttonDelete.style.display = 'inline-block'; + videoDisplay.style.display = 'inline-block'; + videoContainer.style.display = 'inline-block'; + + mediaRecorder = new MediaRecorder(stream, { + mimeType: "video/webm", // could use different video format + // videoBitsPerSecond: 5000000, // Standard bitrate for video is 2,5 mbps + }); + + mediaRecorder.addEventListener("dataavailable", (event) => { + console.log("Data available Event triggered"); + if (event.data.size > 0) { + recordedVideoBlob = event.data; + console.log("Data available:", recordedVideoBlob); + } else { + console.log("No Data available"); + } + }); + + mediaRecorder.addEventListener("stop", () => { + if (recordedVideoBlob) { + videoDisplay.srcObject = null; + videoDisplay.src = URL.createObjectURL(recordedVideoBlob); + videoDisplay.controls = true; + videoDisplay.pause(); + + buttonRecordIcon.src = ICON_PATHS.recordicon; + buttonRecordIcon.alt = 'Record Icon'; + isRecording = false; + console.log('Recording stopped'); + console.log("Src path:", videoDisplay.src); + } + }); + + } else { + console.log("cameraButton case videoAccess = true"); + + stream.getTracks().forEach(track => track.stop()); // Stop all tracks to release the camera + stream = null; + videoAccess = false; + + buttonCameraIcon.src = ICON_PATHS.cameraicon; + buttonCameraIcon.alt = "Camera Icon"; + buttonRecord.style.display = 'none'; + buttonDelete.style.display = 'none'; + videoDisplay.style.display = 'none'; + videoContainer.style.display = 'none'; + } + console.log("camera button function ends"); +} + +function recordButton() { + console.log("recordButton pressed"); + if (!isRecording) { + console.log("recordButton pressed case isRecording = false"); + deleteButton(); + buttonDelete.setAttribute("disabled", ""); + buttonDeleteIcon.classList.add("buttondisable"); + videoDisplay.srcObject = stream; + videoDisplay.src = null; + videoDisplay.controls = false; + mediaRecorder.start(); + console.log("video bitrate = ",mediaRecorder.videoBitsPerSecond); + + buttonRecordIcon.src = ICON_PATHS.stopicon; + buttonRecordIcon.alt = 'Stop Icon'; + isRecording = true; + console.log('Recording started'); + } else { + console.log("recordButton pressed case isRecording = true"); + mediaRecorder.stop(); + + console.log("recording stops"); + buttonDelete.removeAttribute("disabled"); + buttonDeleteIcon.classList.remove("buttondisable"); + } +} + +function deleteButton() { + // TODO delete data + videoDisplay.controls = false; + videoDisplay.srcObject = stream; + recordedVideoBlob = null + buttonDelete.setAttribute("disabled", ""); + buttonDeleteIcon.classList.add("buttondisable"); +} diff --git a/slaeforms/templates/all_links.html b/slaeforms/templates/all_links.html new file mode 100644 index 0000000..683be47 --- /dev/null +++ b/slaeforms/templates/all_links.html @@ -0,0 +1,31 @@ + + + + + + + all links + + + + + +
+

All available pages

+
    +

    --------------------------------------------

    + {% for url, endpoint in links %} + {% if endpoint == "delete_all_entries" %} +

    -

    +

    -

    +

    -

    +

    - make sure you really want this -

    + {% endif %} +
  • {{ endpoint }}
  • +

    --------------------------------------------

    + {% endfor %} +
+
+ + + \ No newline at end of file diff --git a/slaeforms/templates/layout2.html b/slaeforms/templates/layout2.html new file mode 100644 index 0000000..f3154dc --- /dev/null +++ b/slaeforms/templates/layout2.html @@ -0,0 +1,64 @@ + + + + + + + Testform + + + + +

Question: {{ current_question }}

+ {% if (videotype == "single")%} +
+

Video 1

+ +
+ {% elif (videotype == "pairwise")%} +
+
+

Video 1

+ +
+
+

Video 2

+ +
+
+ + {% else %} +

Error: No Videotype could be matched or was given!

+ {% endif %} +
+ {% for block in config["question 1"]["blocks"] %} + {% if (config["question 1"]["blocks"][block]["type"] == "likert") %} +
+
+ + + + + +
+
+ {% elif (config["question 1"]["blocks"][block]["type"] == "textinput") %} + + + {% else %} +

Error: A block could not be loaded!

+ {% endif %} + {% endfor %} +

+
+ + + \ No newline at end of file diff --git a/slaeforms/templates/myvideotemplate.html b/slaeforms/templates/myvideotemplate.html new file mode 100644 index 0000000..6e3cbf4 --- /dev/null +++ b/slaeforms/templates/myvideotemplate.html @@ -0,0 +1,142 @@ + + + + + + + + Testform + + + + +
+

Gib Feedback als Video

+
+ + + +
+ +
+ + + + + +
+ +
+ +
+ +
+ + + \ No newline at end of file diff --git a/slaeforms/templates/old_unused/Study1.html b/slaeforms/templates/old_unused/Study1.html new file mode 100644 index 0000000..62d6a8f --- /dev/null +++ b/slaeforms/templates/old_unused/Study1.html @@ -0,0 +1,64 @@ + + + + + + + Testform + + + + +

Question: {{ current_question }}

+ {% if (videotype == "single")%} +
+

Video 1

+ +
+ {% elif (videotype == "pairwise")%} +
+
+

Video 1

+ +
+
+

Video 2

+ +
+
+ + {% else %} +

Error: No Videotype could be matched or was given!

+ {% endif %} +
+ {% for block in config["question 1"]["blocks"] %} + {% if (config["question 1"]["blocks"][block]["type"] == "likert") %} +
+
+ + + + + +
+
+ {% elif (config["question 1"]["blocks"][block]["type"] == "textinput") %} + + + {% else %} +

Error: A block could not be loaded!

+ {% endif %} + {% endfor %} +

+
+ + + \ No newline at end of file diff --git a/slaeforms/templates/old_unused/layout1.html b/slaeforms/templates/old_unused/layout1.html new file mode 100644 index 0000000..3e45e1d --- /dev/null +++ b/slaeforms/templates/old_unused/layout1.html @@ -0,0 +1,49 @@ + + + + + + + Testform + + + + +

Task number 1

+
+ +
+

Video 1

+ +
+
+

Video 2

+ +
+
+
+
+
+ + + + + +
+
+ + + +

+
+ + + \ No newline at end of file diff --git a/slaeforms/templates/old_unused/templatetest1.html b/slaeforms/templates/old_unused/templatetest1.html new file mode 100644 index 0000000..f889f3d --- /dev/null +++ b/slaeforms/templates/old_unused/templatetest1.html @@ -0,0 +1,24 @@ + + + + + + + Testform + + + + +
+ + + + + + +

+
+ + + + \ No newline at end of file diff --git a/slaeforms/templates/old_unused/videorecorder.html b/slaeforms/templates/old_unused/videorecorder.html new file mode 100644 index 0000000..f31aa96 --- /dev/null +++ b/slaeforms/templates/old_unused/videorecorder.html @@ -0,0 +1,65 @@ + + + + + + Webcam Recorder + + + + + + + + + + + \ No newline at end of file diff --git a/slaeforms/templates/old_unused/videorecorder2.html b/slaeforms/templates/old_unused/videorecorder2.html new file mode 100644 index 0000000..c6cdf6a --- /dev/null +++ b/slaeforms/templates/old_unused/videorecorder2.html @@ -0,0 +1,84 @@ + + + + + + + Webcam Recorder + + + + + + + +
+ + + + + + diff --git a/slaeforms/templates/old_unused/videorecorder3.html b/slaeforms/templates/old_unused/videorecorder3.html new file mode 100644 index 0000000..1678d01 --- /dev/null +++ b/slaeforms/templates/old_unused/videorecorder3.html @@ -0,0 +1,177 @@ + + + + + + + + Videorecorder test + + + +

Videorecordertest

+
+ + + + +
+
+
+ +
+
+ +
+ + + \ No newline at end of file diff --git a/slaeforms/templates/show_tables.html b/slaeforms/templates/show_tables.html new file mode 100644 index 0000000..a106b69 --- /dev/null +++ b/slaeforms/templates/show_tables.html @@ -0,0 +1,26 @@ + + + + Database Tables + + + + + +

Database Tables

+
    + {% for key, value in tables.items() %} +
  • + {{ key }}:
    +
      + {% for column in value.columns %} +
    • {{ column.name }} - {{ column.type }}
    • + {% endfor %} +
    +
  • + {% endfor %} +
+ + + + \ No newline at end of file diff --git a/slaeforms/templates/standard_template.html b/slaeforms/templates/standard_template.html new file mode 100644 index 0000000..4c31468 --- /dev/null +++ b/slaeforms/templates/standard_template.html @@ -0,0 +1,181 @@ + + +{% macro single_video(video_url, embed="yt", title="",width="560", height="315", class="center", code="

No code given +

") -%} +{% if (embed == "yt") %} +
+ {% if (title != "") %} +

{{title}}

+ {% endif %} +
+ +
+
+{% else %} +{{code}} +{% endif %} +{%- endmacro %} + +{% macro required(question) -%} +{% if (question["required"] == "true") %} +required +{% endif %} +{%- endmacro %} + +{% macro inputconfig(question) -%} +{% if ("min" in question) %} +min={{question["min"]}} +{% endif %} +{% if ("max" in question) %} +max={{question["max"]}} +{% endif %} +{% if ("step" in question) %} +step={{question["step"]}} +{% endif %} +{%- endmacro %} + +{% macro input(name, value='', type='text', size=20) -%} + +{%- endmacro %} + + + + + + Testform + + + + + +
+
+

Stimulus part

+ {% if (stimulus_type == "single_video") %} + {{ single_video(**stimulus_configuration) }} + {% elif (stimulus_type == "empty") %} + + {% else %} +

Error: Block {{ stimulus["type"] }} could not be loaded!

+ {% endif %} + + + +

Questions

+
+ {% for question in questions %} + {% if (questions[question]["type"] == "likert") %} +
+
+ {{ questions[question]['text']}} + {% for point in questions[question]["points"] %} + + {% endfor %} +
+
+ + {% elif (questions[question]["type"] == "textinput") %} +
+ +
+ + {% elif (questions[question]["type"] == "dateinput") %} + + + {% elif (questions[question]["type"] == "numberinput") %} + + + {% elif (questions[question]["type"] == "emailinput") %} + + + {% elif (questions[question]["type"] == "dropdowninput") %} + + + {% elif (questions[question]["type"] == "videoinput") %} +

Gib Feedback als Video

+
+ {{ questions[question]['text']}} + + +
+ +
+ + + + + +
+ + + + + {% else %} +

Error: Block {{config["question 1"]["blocks"][block]["type"]}} could not be loaded!

+ {% endif %} + {% endfor %} +
+

+ +
+
+
+
+ + + \ No newline at end of file diff --git a/slaeforms/templates/startpage.html b/slaeforms/templates/startpage.html new file mode 100644 index 0000000..4f8dc99 --- /dev/null +++ b/slaeforms/templates/startpage.html @@ -0,0 +1,25 @@ + + + + + + + DGS Avatar Study + + + +
+
+

Hello! Thank you for participating in our study!

+
+ + +

+
+
+
+ + + \ No newline at end of file diff --git a/slaeforms/templates/table_contents.html b/slaeforms/templates/table_contents.html new file mode 100644 index 0000000..ac102aa --- /dev/null +++ b/slaeforms/templates/table_contents.html @@ -0,0 +1,33 @@ + + + + Database Tables + + + + +

Database Tables

+ {% for table_name, contents in table_contents.items() %} +

{{table_name}}

+ + + + {% for column in contents['columns'] %} + + {% endfor %} + + + + {% for row in contents['rows'] %} + + {% for column in contents['columns'] %} + + {% endfor %} + + {% endfor %} + +
{{column}}
{{row[column]}}
+ {% endfor %} + + + \ No newline at end of file diff --git a/slaeforms/templates/test_page0.html b/slaeforms/templates/test_page0.html new file mode 100644 index 0000000..37e9e68 --- /dev/null +++ b/slaeforms/templates/test_page0.html @@ -0,0 +1,22 @@ + + + + + + + Test Page 0 - Nothing + + + + +
+
+

This is just a test page for the single page option of the json configuration, but without something to submit

+
+ +

+
+
+ + + \ No newline at end of file diff --git a/slaeforms/templates/test_page1.html b/slaeforms/templates/test_page1.html new file mode 100644 index 0000000..b2f54c6 --- /dev/null +++ b/slaeforms/templates/test_page1.html @@ -0,0 +1,27 @@ + + + + + + + Test Page 1 - Datenschutzerklaerung + + + + +
+
+

This is just a test page for the single page option of the json configuration

+
+ + +

+
+
+
+ + \ No newline at end of file diff --git a/slaeforms/templates/teststartpage.html b/slaeforms/templates/teststartpage.html new file mode 100644 index 0000000..a194d1b --- /dev/null +++ b/slaeforms/templates/teststartpage.html @@ -0,0 +1,25 @@ + + + + + + + DGS Avatar Study + + + +
+
+

Hello! Thank you for participating in our study!

+
+ + +

+
+
+
+ + + \ No newline at end of file diff --git a/slaeforms/test.json b/slaeforms/test.json new file mode 100644 index 0000000..6981cf1 --- /dev/null +++ b/slaeforms/test.json @@ -0,0 +1,38 @@ +{ + "Block 1":{ + "type": "SinglePage", + "template": "startpage.html" + }, + "Block 2":{ + "type": "TaskTemplate", + "tempalte": "tempaltetest1.html", + "name" : "Block2Responses", + "databasetable": { + "question_title" : { + "type":"likert" + } + } + }, + "question 3":{ + "type": "single", + "video1": "https://www.youtube-nocookie.com/embed/XTMIomsXxKM?si=r2zB6OKERH6Jdpi6", + "scales": { + "block1":{ + "type": "likert", + "numberofpoints": "3", + "points":{ + "point1": "left", + "point2": "none", + "point3": "right" + } + }, + "block2":{ + "type": "textinput", + "length": "200" + }, + "block3":{ + "type": "video" + } + } + } +} \ No newline at end of file diff --git a/slaeforms/uploads/8fa17956-484e-4fed-9941-2b944114d3b0_Block 4_empty_stimulus_2024.06.15 18-56-34.webm b/slaeforms/uploads/8fa17956-484e-4fed-9941-2b944114d3b0_Block 4_empty_stimulus_2024.06.15 18-56-34.webm new file mode 100644 index 0000000..8a5e35f Binary files /dev/null and b/slaeforms/uploads/8fa17956-484e-4fed-9941-2b944114d3b0_Block 4_empty_stimulus_2024.06.15 18-56-34.webm differ diff --git a/slaeforms/uploads/8fa17956-484e-4fed-9941-2b944114d3b0_Block 4_video_3_2024.06.15 18-56-47.webm b/slaeforms/uploads/8fa17956-484e-4fed-9941-2b944114d3b0_Block 4_video_3_2024.06.15 18-56-47.webm new file mode 100644 index 0000000..8a5e35f Binary files /dev/null and b/slaeforms/uploads/8fa17956-484e-4fed-9941-2b944114d3b0_Block 4_video_3_2024.06.15 18-56-47.webm differ diff --git a/slaeforms/uploads/a72f05bb-d6a7-4f0d-940d-e286746b015c_Block 4_video_2_2024.06.15 19-49-35.webm b/slaeforms/uploads/a72f05bb-d6a7-4f0d-940d-e286746b015c_Block 4_video_2_2024.06.15 19-49-35.webm new file mode 100644 index 0000000..6c77ad6 Binary files /dev/null and b/slaeforms/uploads/a72f05bb-d6a7-4f0d-940d-e286746b015c_Block 4_video_2_2024.06.15 19-49-35.webm differ diff --git a/slaeforms/uploads/acd47b24-2a64-400a-be52-258fa6dbfee3_Block 4_video_1_2024.06.15 19-15-06.webm b/slaeforms/uploads/acd47b24-2a64-400a-be52-258fa6dbfee3_Block 4_video_1_2024.06.15 19-15-06.webm new file mode 100644 index 0000000..fff3622 Binary files /dev/null and b/slaeforms/uploads/acd47b24-2a64-400a-be52-258fa6dbfee3_Block 4_video_1_2024.06.15 19-15-06.webm differ diff --git a/slaeforms/uploads/adffef3d-4d54-4a4d-8230-ec1ef2ede482_Block 4_video_3_2024.06.15 19-25-08.webm b/slaeforms/uploads/adffef3d-4d54-4a4d-8230-ec1ef2ede482_Block 4_video_3_2024.06.15 19-25-08.webm new file mode 100644 index 0000000..a1f87f6 Binary files /dev/null and b/slaeforms/uploads/adffef3d-4d54-4a4d-8230-ec1ef2ede482_Block 4_video_3_2024.06.15 19-25-08.webm differ