diff --git a/.gitignore b/.gitignore index 82ce638e24652fda99b4442de0cf70d8660d4f49..d3c652fd439d8383ba3a20db17a1b4500514e910 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,9 @@ config.py *.db /embody /app/static/lib +.env +/app/static/*.png +/app/static/embody_images +.vscode/ +documentation diff --git a/app/__init__.py b/app/__init__.py index 2fd3d82fefb5c8b25d7d0d93cae852662cfbc5c4..71c6ee7e0970ac6a6dea8fa8cde8bf4781bd880e 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -15,7 +15,7 @@ from flask_cors import CORS, cross_origin app = Flask(__name__) CORS(app) -#CORS(app, resources={r"/*": {"cors_allowed_origins":"*"} } ) +# CORS(app, resources={r"/*": {"cors_allowed_origins":"*"} } ) #app.config['BABEL_DEFAULT_LOCALE'] = 'fin' #app.config['BABEL_TRANSLATION_DIRECTORIES'] ='C:/Users/Timo/git/pet-rating/app/translations' @@ -68,9 +68,6 @@ def get_locale(): """ -# Run flask app with socketIO -socketio = SocketIO() -socketio.init_app(app) #mariabd mysql portti 3306 tarkista? @@ -81,6 +78,11 @@ migrate = Migrate(app, db) login = LoginManager(app) login.login_view = 'login' +# Run flask app with socketIO +socketio = SocketIO(app, cors_allowed_origins="*") +# socketio = SocketIO() +socketio.init_app(app) + # Register blueprints from .task.views import task_blueprint from .experiment.views import experiment_blueprint diff --git a/app/experiment/templates/add_embody.html b/app/experiment/templates/add_embody.html index 535fb830e4340b5cacaa79a2e660ec0002b141a4..b7bdc332f86ac5db4d4857a3bc31e90c7ba6e718 100644 --- a/app/experiment/templates/add_embody.html +++ b/app/experiment/templates/add_embody.html @@ -3,8 +3,7 @@ <h1 class="container mt-5 display-4 text-center"><br>Add new embody picture:</h1> <br> -<p class="lead"> -Upload new embody image... instructions here for admins.. +<p class="lead"> Upload new embody image. Submit without choosing file if you want to use default embody picture. </p> {% from "_formhelpers.html" import render_field %} @@ -12,8 +11,8 @@ Upload new embody image... instructions here for admins.. <form method="post" enctype="multipart/form-data"> <div class="form-group"> - <label for="Background questions">image question/explanation:</label> - <textarea class="form-control" rows="5" id="embody_picture_text" name="question"></textarea> + <label for="Background questions">Image question/explanation:</label> + <textarea class="form-control" rows="5" id="embody_picture_text" name="question">Color the regions whose activity you feel increasing or getting stronger</textarea> </div> <div class="custom-file"> @@ -23,7 +22,7 @@ Upload new embody image... instructions here for admins.. <hr> - <button type="submit" class="btn btn-primary submit-file" disabled>Submit</button> + <button type="submit" class="btn btn-primary">Submit</button> <a class="btn btn-primary" href="{{ request.referrer }}" role="button">Cancel</a> </form> diff --git a/app/experiment/templates/experiment_statistics.html b/app/experiment/templates/experiment_statistics.html index 817470eafe2cfc7e485c63e9c1c60db30827c24d..76b2e9edb2a6f0a0afcc6a6cbf341090860e6415 100644 --- a/app/experiment/templates/experiment_statistics.html +++ b/app/experiment/templates/experiment_statistics.html @@ -36,7 +36,27 @@ <tr> <td>Number of finished ratings:</td> <td>{{ finished_ratings }} - <a class="btn btn-primary btn-info float-right" href="{{ url_for('download_csv', exp_id=exp.idexperiment) }}" role="button">Export results (csv)</a> + + + <button data-value="{{ exp.idexperiment }}" class="btn btn-primary float-right get-csv-results"> + + export results + + </button> + + <div id="export-link-container" class="hidden"> + <a id="export-link" class="float-right" + href="{{ url_for('experiment.download_csv', exp_id=exp.idexperiment) }}" role="button"></a> + <p id="export-error"></p> + </div> + + + <div class="progress hidden"> + <div id="export-results-bar" class="progress-bar progress-bar-striped progress-bar-animated" + role="progressbar" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100" style="width: 0%"> + </div> + </div> + </td> </tr> </tbody> @@ -49,21 +69,21 @@ <table class="table"> <thead> <tr> - <th scope="col" nowrap>Question ID:</td> - <th scope="col" nowrap>Question:</th> - <th scope="col" nowrap>Left scale</th> - <th scope="col" nowrap>Right scale</th> + <th scope="col" nowrap>Question ID:</td> + <th scope="col" nowrap>Question:</th> + <th scope="col" nowrap>Left scale</th> + <th scope="col" nowrap>Right scale</th> </tr> </thead> <tbody> - {% for q in question_headers %} + {% for q in question_headers %} <tr> - <td>{{ q.idquestion }}</td> - <td>{{ q.question }}</td> - <td>{{ q.left }}</td> - <td>{{ q.right }}</td> + <td>{{ q.idquestion }}</td> + <td>{{ q.question }}</td> + <td>{{ q.left }}</td> + <td>{{ q.right }}</td> </tr> - {% endfor %} + {% endfor %} </tbody> </table> @@ -74,29 +94,29 @@ <table class="table"> <thead> - + <tr> - <th scope="col" nowrap>Participant ID:</th> - {% for page in pages_and_questions %} - - {% for p in pages_and_questions[page] %} - <th scope="col" nowrap>{{ p[0]}}/{{ p[1]}}</th> - {% endfor %} - - {% endfor %} + <th scope="col" nowrap>Participant ID:</th> + {% for page in pages_and_questions %} + + {% for p in pages_and_questions[page] %} + <th scope="col" nowrap>{{ p[0]}}/{{ p[1]}}</th> + {% endfor %} + + {% endfor %} </tr> </thead> <tbody> - {% for participant in participants_and_answers %} + {% for participant in participants_and_answers %} <tr> - {% if participant == 'mean' %} - <td><b>{{ participant }}</b></td> - {% else %} - <td>{{ participant }}</td> - {% endif %} - {% for answer in participants_and_answers[participant] %} - <td>{{ answer }}</td> - {% endfor %} + {% if participant == 'mean' %} + <td><b>{{ participant }}</b></td> + {% else %} + <td>{{ participant }}</td> + {% endif %} + {% for answer in participants_and_answers[participant] %} + <td>{{ answer }}</td> + {% endfor %} </tr> {% endfor %} @@ -109,56 +129,60 @@ <table class="table"> <thead> <tr> - <th style="width:25%;%" scope="col" nowrap>Stimulus:</td> - <th style="width:25%;%" scope="col" nowrap>Picture:</th> - <th style="width:25%;%" scope="col" nowrap>Description:</th> - <th style="width:25%;%" scope="col" nowrap></th> + <th style="width:25%;%" scope="col" nowrap>Stimulus:</td> + <th style="width:25%;%" scope="col" nowrap>Picture:</th> + <th style="width:25%;%" scope="col" nowrap>Description:</th> + <th style="width:25%;%" scope="col" nowrap></th> </tr> </thead> <tbody> - {% for s in stimulus_headers %} - {% for embody_picture in embody_questions %} - <tr> - - {% if s.type == 'text' %} - <td>{{ s.text }}</td> - {% elif s.type == 'picture' %} - <td><img src="/{{ s.media }}" class="thumbnail" /></td> - {% elif s.type == 'video' %} - <td> - <div class="embed-responsive embed-responsive-16by9 "> - <iframe class="embed-responsive-item thumbnail" src="/{{ s.media }}" allowFullScreen></iframe> - </div> - </td> - {% elif s.type == 'audio' %} - <td> - <div class="embed-responsive embed-responsive-16by9 "> - <iframe class="embed-responsive-item thumbnail" src="/{{ s.media }}" allowFullScreen></iframe> - </div> - </td> - {% else %} - <td>{{ s.text }}</td> - {% endif %} - - <td><img src="{{ embody_picture.picture }}" class="thumbnail" /></td> - <td>{{ embody_picture.question }}</td> - - <td> - <button data-value="{{ s.idpage }}-{{ embody_picture.idembody }}" class="btn btn-primary embody-get-drawing"> - <span class="spinner-border spinner-border-sm hidden"></span> - Draw - </button> - </td> - - </tr> - {% endfor %} - {% endfor %} + {% for s in stimulus_headers %} + {% for embody_picture in embody_questions %} + <tr> + + {% if s.type == 'text' %} + <td>{{ s.text }}</td> + {% elif s.type == 'picture' %} + <td><img src="/{{ s.media }}" class="thumbnail" /></td> + {% elif s.type == 'video' %} + <td> + <div class="embed-responsive embed-responsive-16by9 "> + <iframe class="embed-responsive-item thumbnail" src="/{{ s.media }}" allowFullScreen></iframe> + </div> + </td> + {% elif s.type == 'audio' %} + <td> + <div class="embed-responsive embed-responsive-16by9 "> + <audio class="embed-responsive-item thumbnail" controls> + <source src="/{{ s.media }}"> + Your browser does not support the audio element. + </audio> + </div> + </td> + {% else %} + <td>{{ s.text }}</td> + {% endif %} + + <td><img src="{{ embody_picture.picture }}" class="thumbnail" /></td> + <td>{{ embody_picture.question }}</td> + + <td> + <button data-value="{{ s.idpage }}-{{ embody_picture.idembody }}" class="btn btn-primary embody-get-drawing"> + <span class="spinner-border spinner-border-sm hidden"></span> + Draw + </button> + </td> + + </tr> + {% endfor %} + {% endfor %} </tbody> </table> <div class="progress hidden" id="plotted-image"> - <div id="image-loading-progress" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100" style="width: 0%"> + <div id="image-loading-progress" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" + aria-valuenow="50" aria-valuemin="0" aria-valuemax="100" style="width: 0%"> <!-- Creating image... --> </div> </div> @@ -172,20 +196,20 @@ <table class="table"> <thead> <tr> - <th scope="col" nowrap>Participant</th> - - {% for bg in bg_questions %} - <th scope="col" nowrap>{{ bg.background_question }}</th> - {% endfor %} + <th scope="col" nowrap>Participant</th> + + {% for bg in bg_questions %} + <th scope="col" nowrap>{{ bg.background_question }}</th> + {% endfor %} </tr> </thead> <tbody> - {% for p in bg_answers_for_participants %} + {% for p in bg_answers_for_participants %} <tr> - <td>{{ p }}</td> - {% for bg_answer in bg_answers_for_participants[p] %} - <td align="center">{{ bg_answer }}</td> - {% endfor %} + <td>{{ p }}</td> + {% for bg_answer in bg_answers_for_participants[p] %} + <td align="center">{{ bg_answer }}</td> + {% endfor %} </tr> {% endfor %} @@ -193,7 +217,9 @@ </tbody> </table> -<script src="{{ url_for('static', filename='lib/js/socket.io.js') }}" ></script> -<script src="{{ url_for('static', filename='js/getDrawing.js') }}" ></script> +<script src="{{ url_for('static', filename='lib/js/socket.io.js') }}"></script> +<script src="{{ url_for('static', filename='js/urls.js') }}"></script> +<script src="{{ url_for('static', filename='js/getDrawing.js') }}"></script> +<script src="{{ url_for('static', filename='js/getCSV.js') }}"></script> -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/app/experiment/templates/view_experiment.html b/app/experiment/templates/view_experiment.html index b3ee1a19262734852c5b61c91df1b477fd603e86..c0aee71c6757b95a426568161e9234cf3fac8779 100644 --- a/app/experiment/templates/view_experiment.html +++ b/app/experiment/templates/view_experiment.html @@ -211,7 +211,6 @@ </table> - <h1 class="container mt-5 display-4 text-left"><br>Add embody tool:</h1> <table class="table"> @@ -231,15 +230,13 @@ {% endif %} </td> <td> - <a class="btn btn-primary btn-block btn-sm btn-info" href="{{ url_for('experiment.add_embody', exp_id=exp_id, default=true) }}" role="button">Add default</a> <a class="btn btn-primary btn-block btn-sm btn-info" href="{{ url_for('experiment.add_embody', exp_id=exp_id) }}" role="button">Add new picture</a> </td> </tr> {% for embody_picture in embody_pictures %} - <tr> - <td>ID: {{ embody_picture.idembody }} <br> {{ embody_picture.question }}</td> + <td>ID: {{ embody_picture.idembody }} <br> {{ embody_picture.question }}</td> <td><img src="{{ embody_picture.picture }}" class="thumbnail" /></td> <td><a class="btn btn-primary btn-block btn-sm btn-dark" href="{{ url_for('experiment.remove_embody', exp_id=exp_id, idembody=embody_picture.idembody) }}" role="button">Remove</a></td> </tr> diff --git a/app/experiment/views.py b/app/experiment/views.py index 6b02e510950e60e156b686cefddd00646103b2a7..110dfe95dbc6c92a00ebd032e6a7a81dd06f8e33 100644 --- a/app/experiment/views.py +++ b/app/experiment/views.py @@ -1,69 +1,71 @@ - import os import secrets -import json +from datetime import date +from tempfile import mkstemp +from flask_socketio import emit +from sqlalchemy import and_ +from flask_login import login_required +from werkzeug import secure_filename from flask import ( - Flask, - render_template, - request, - session, - flash, - redirect, - url_for, + render_template, + request, + flash, + redirect, + url_for, Blueprint, - jsonify + send_file ) -from wtforms import Form -from sqlalchemy import and_, update -from flask_login import login_required - -from app import app, db +from app import app, db, socketio from app.routes import APP_ROOT from app.models import background_question, experiment from app.models import background_question_answer from app.models import page, question from app.models import background_question_option from app.models import answer_set, answer, forced_id -from app.models import user, trial_randomization +from app.models import trial_randomization from app.models import embody_answer, embody_question from app.forms import ( - CreateBackgroundQuestionForm, - CreateQuestionForm, UploadStimuliForm, EditBackgroundQuestionForm, - EditQuestionForm, EditExperimentForm, UploadResearchBulletinForm, - EditPageForm, RemoveExperimentForm, GenerateIdForm,CreateEmbodyForm + CreateBackgroundQuestionForm, + CreateQuestionForm, UploadStimuliForm, EditBackgroundQuestionForm, + EditQuestionForm, EditExperimentForm, UploadResearchBulletinForm, + EditPageForm, RemoveExperimentForm, GenerateIdForm, CreateEmbodyForm ) -from werkzeug import secure_filename +from app.utils import get_mean_from_slider_answers, map_answers_to_questions, \ + generate_csv + +import embody_plot -#Stimuli upload folder setting +# Stimuli upload folder setting #APP_ROOT = os.path.dirname(os.path.abspath(__file__)) -experiment_blueprint = Blueprint("experiment", __name__, - template_folder='templates', - #static_folder='static', - url_prefix='/experiment') +experiment_blueprint = Blueprint("experiment", __name__, + template_folder='templates', + # static_folder='static', + url_prefix='/experiment') # Set sliders/embody: DEFAULT_EMBODY_PICTURE = '/static/img/dummy_600.png' DEFAULT_EMBODY_QUESTION = 'Color the regions whose activity you feel increasing or getting stronger' + @experiment_blueprint.route('/view') @login_required def view(): - - #crap:3lines + + # crap:3lines exp_id = request.args.get('exp_id', None) media = page.query.filter_by(experiment_idexperiment=exp_id).all() - # stimulus type + # stimulus type mtype = page.query.filter_by(experiment_idexperiment=exp_id).first() - - #experiment info - experiment_info = experiment.query.filter_by(idexperiment = exp_id).all() - #background questions + # experiment info + experiment_info = experiment.query.filter_by(idexperiment=exp_id).all() + + # background questions questions_and_options = {} questions = background_question.query.filter_by( experiment_idexperiment=exp_id).all() @@ -79,7 +81,7 @@ def view(): questions1 = questions_and_options - #sliderset + # sliderset categories_and_scales = {} categories = question.query.filter_by(experiment_idexperiment=exp_id).all() @@ -107,51 +109,59 @@ def remove(): exp_status = experiment.query.filter_by(idexperiment=exp_id).first() if exp_status.status != 'Hidden': - + flash("Experiment is public. Cannot modify structure.") return redirect(url_for('experiment.view', exp_id=exp_id)) - + else: - + form = RemoveExperimentForm(request.form) - + if request.method == 'POST' and form.validate(): - + if form.remove.data == 'DELETE': - - #This removes all experiment data from the database! - - #Remove research bulletin if it exists - empty_filevariable = experiment.query.filter_by(idexperiment=exp_id).first() - + + # This removes all experiment data from the database! + + # Remove research bulletin if it exists + empty_filevariable = experiment.query.filter_by( + idexperiment=exp_id).first() + if empty_filevariable.research_notification_filename is not None: - target = os.path.join(APP_ROOT, empty_filevariable.research_notification_filename) - - if os.path.exists(target): + target = os.path.join( + APP_ROOT, empty_filevariable.research_notification_filename) + + if os.path.exists(target): os.remove(target) - - #Tables - remove_forced_id = forced_id.query.filter_by(experiment_idexperiment=exp_id).all() + + # Tables + remove_forced_id = forced_id.query.filter_by( + experiment_idexperiment=exp_id).all() remove_rows(remove_forced_id) - #background_question_option & background_question & background question answers: - remove_background_question = background_question.query.filter_by(experiment_idexperiment=exp_id).all() - - #Remove all background questions and all answers given to each bg question + # background_question_option & background_question & background question answers: + remove_background_question = background_question.query.filter_by( + experiment_idexperiment=exp_id).all() + + # Remove all background questions and all answers given to each bg question for a in range(len(remove_background_question)): - remove_background_question_option = background_question_option.query.filter_by(background_question_idbackground_question=remove_background_question[a].idbackground_question).all() + remove_background_question_option = background_question_option.query.filter_by( + background_question_idbackground_question=remove_background_question[a].idbackground_question).all() remove_rows(remove_background_question_option) - remove_background_question_answers = background_question_answer.query.filter_by(background_question_idbackground_question=remove_background_question[a].idbackground_question).all() + remove_background_question_answers = background_question_answer.query.filter_by( + background_question_idbackground_question=remove_background_question[a].idbackground_question).all() remove_rows(remove_background_question_answers) db.session.delete(remove_background_question[a]) db.session.commit() - - #Remove all questions and answers - remove_question = question.query.filter_by(experiment_idexperiment=exp_id).all() + + # Remove all questions and answers + remove_question = question.query.filter_by( + experiment_idexperiment=exp_id).all() for a in range(len(remove_question)): - remove_question_answers = answer.query.filter_by(question_idquestion=remove_question[a].idquestion).all() + remove_question_answers = answer.query.filter_by( + question_idquestion=remove_question[a].idquestion).all() remove_rows(remove_question_answers) db.session.delete(remove_question[a]) db.session.commit() @@ -160,54 +170,58 @@ def remove(): remove_embody_answers = embody_answer.query.filter(embody_answer.page_idpage.in_(list(map( lambda x: x[0], page.query.with_entities(page.idpage).filter_by(experiment_idexperiment=exp_id).all())))).all() remove_rows(remove_embody_answers) - remove_embody_questions = embody_question.query.filter_by(experiment_idexperiment=exp_id).all() + remove_embody_questions = embody_question.query.filter_by( + experiment_idexperiment=exp_id).all() for a in range(len(remove_embody_questions)): target = APP_ROOT + remove_embody_questions[a].picture - if os.path.exists(target) and DEFAULT_EMBODY_PICTURE != remove_embody_questions[a].picture: + if os.path.exists(target) and DEFAULT_EMBODY_PICTURE != remove_embody_questions[a].picture: os.remove(target) remove_rows(remove_embody_questions) - #Remove all pages and datafiles - remove_pages = page.query.filter_by(experiment_idexperiment=exp_id).all() - + # Remove all pages and datafiles + remove_pages = page.query.filter_by( + experiment_idexperiment=exp_id).all() + for a in range(len(remove_pages)): if remove_pages[a].type != 'text': target = os.path.join(APP_ROOT, remove_pages[a].media) - if os.path.exists(target): + if os.path.exists(target): os.remove(target) - - #Now that the files are removed we can delete the page + + # Now that the files are removed we can delete the page db.session.delete(remove_pages[a]) db.session.commit() - - #Remove all answer_sets and trial_randomization orders - remove_answer_set = answer_set.query.filter_by(experiment_idexperiment=exp_id).all() - + # Remove all answer_sets and trial_randomization orders + remove_answer_set = answer_set.query.filter_by( + experiment_idexperiment=exp_id).all() + for a in range(len(remove_answer_set)): - remove_trial_randomizations = trial_randomization.query.filter_by(answer_set_idanswer_set=remove_answer_set[a].idanswer_set).all() + remove_trial_randomizations = trial_randomization.query.filter_by( + answer_set_idanswer_set=remove_answer_set[a].idanswer_set).all() remove_rows(remove_trial_randomizations) db.session.delete(remove_answer_set[a]) db.session.commit() - - #Remove experiment table - remove_experiment = experiment.query.filter_by(idexperiment=exp_id).first() + + # Remove experiment table + remove_experiment = experiment.query.filter_by( + idexperiment=exp_id).first() db.session.delete(remove_experiment) db.session.commit() # Remove empty directories os.rmdir(APP_ROOT + '/static/embody_images/' + str(exp_id)) os.rmdir(APP_ROOT + '/static/experiment_stimuli/' + str(exp_id)) - + flash("Experiment was removed from database!") return redirect(url_for('index')) - + else: flash("Experiment was not removed!") return redirect(url_for('experiment.view', exp_id=exp_id)) - + return render_template('remove_experiment.html', form=form, exp_id=exp_id) @@ -215,7 +229,8 @@ def remove(): @login_required def publish(): exp_id = request.args.get('exp_id', None) - publish_experiment = experiment.query.filter_by(idexperiment = exp_id).first() + publish_experiment = experiment.query.filter_by( + idexperiment=exp_id).first() publish_experiment.status = 'Public' flash("Changed status to Public") db.session.commit() @@ -226,7 +241,7 @@ def publish(): @login_required def hide(): exp_id = request.args.get('exp_id', None) - hide_experiment = experiment.query.filter_by(idexperiment = exp_id).first() + hide_experiment = experiment.query.filter_by(idexperiment=exp_id).first() hide_experiment.status = 'Hidden' flash("Changed status to Hidden") db.session.commit() @@ -237,7 +252,8 @@ def hide(): @login_required def private(): exp_id = request.args.get('exp_id', None) - private_experiment = experiment.query.filter_by(idexperiment = exp_id).first() + private_experiment = experiment.query.filter_by( + idexperiment=exp_id).first() private_experiment.status = 'Private' flash("Changed status to Private") db.session.commit() @@ -255,9 +271,10 @@ def randomization(): elif status == 'Off': flash("Disabled trial randomization") - experiment.query.filter_by(idexperiment = exp_id).first().randomization = status + experiment.query.filter_by( + idexperiment=exp_id).first().randomization = status db.session.commit() - + return redirect(url_for('experiment.view', exp_id=exp_id)) @@ -275,9 +292,10 @@ def set_forced_id(): elif status == 'Off': flash("Disabled forced ID login") - experiment.query.filter_by(idexperiment = exp_id).first().use_forced_id = status + experiment.query.filter_by( + idexperiment=exp_id).first().use_forced_id = status db.session.commit() - + return redirect(url_for('experiment.view', exp_id=exp_id)) @@ -289,29 +307,34 @@ def view_forced_id_list(): exp_id = request.args.get('exp_id', None) id_list = forced_id.query.filter_by(experiment_idexperiment=exp_id).all() form = GenerateIdForm(request.form) - + if request.method == 'POST' and form.validate(): - for i in range(int(request.form['number'])): + for i in range(int(request.form['number'])): random_id = str(request.form['string']) + str(secrets.token_hex(3)) - check_answer_set = answer_set.query.filter_by(session=random_id).first() - check_forced_id = forced_id.query.filter_by(pregenerated_id=random_id).first() - - #here we check if the generated id is found from given answers from the whole database in answer_set table - #or from forced_id table. If so another id is generated instead to avoid a duplicate + check_answer_set = answer_set.query.filter_by( + session=random_id).first() + check_forced_id = forced_id.query.filter_by( + pregenerated_id=random_id).first() + + # here we check if the generated id is found from given answers from the whole database in answer_set table + # or from forced_id table. If so another id is generated instead to avoid a duplicate if check_answer_set is not None or check_forced_id is not None: - + #flash("ID already existed; generated a new one") random_id = secrets.token_hex(3) - check_answer_set = answer_set.query.filter_by(session=random_id).first() - check_forced_id = forced_id.query.filter_by(pregenerated_id=random_id).first() - - input_id = forced_id(experiment_idexperiment=exp_id, pregenerated_id=random_id) + check_answer_set = answer_set.query.filter_by( + session=random_id).first() + check_forced_id = forced_id.query.filter_by( + pregenerated_id=random_id).first() + + input_id = forced_id( + experiment_idexperiment=exp_id, pregenerated_id=random_id) db.session.add(input_id) db.session.commit() - + return redirect(url_for('experiment.view_forced_id_list', exp_id=exp_id)) - + return render_template('view_forced_id_list.html', exp_id=exp_id, id_list=id_list) @@ -322,27 +345,27 @@ def upload_research_notification(): exp_id = request.args.get('exp_id', None) form = UploadResearchBulletinForm(request.form) - + if request.method == 'POST': path = 'static/experiment_stimuli/' + str(exp_id) target = os.path.join(APP_ROOT, path) - + if not os.path.isdir(target): os.mkdir(target) - - #This returns a list of filenames: request.files.getlist("file") + + # This returns a list of filenames: request.files.getlist("file") for file in request.files.getlist("file"): - #save files in the correct folder + # save files in the correct folder filename = file.filename destination = "/".join([target, filename]) file.save(destination) - - #add pages to the db - db_path = path + str('/') + str(filename) + + # add pages to the db + db_path = path + str('/') + str(filename) bulletin = experiment.query.filter_by(idexperiment=exp_id).first() bulletin.research_notification_filename = db_path db.session.commit() - + return redirect(url_for('experiment.view', exp_id=exp_id)) return render_template('upload_research_notification.html', exp_id=exp_id, form=form) @@ -352,17 +375,19 @@ def upload_research_notification(): @login_required def remove_research_notification(): '''Remove research bulletin''' - + exp_id = request.args.get('exp_id', None) - empty_filevariable = experiment.query.filter_by(idexperiment=exp_id).first() - target = os.path.join(APP_ROOT, empty_filevariable.research_notification_filename) - - if os.path.exists(target): + empty_filevariable = experiment.query.filter_by( + idexperiment=exp_id).first() + target = os.path.join( + APP_ROOT, empty_filevariable.research_notification_filename) + + if os.path.exists(target): os.remove(target) - + empty_filevariable.research_notification_filename = None db.session.commit() - + return redirect(url_for('experiment.view', exp_id=exp_id)) @@ -372,22 +397,20 @@ def edit(): '''Edit experiment details''' exp_id = request.args.get('exp_id', None) - current_experiment = experiment.query.filter_by(idexperiment=exp_id).first() - + current_experiment = experiment.query.filter_by( + idexperiment=exp_id).first() + form = EditExperimentForm(request.form, obj=current_experiment) form.language.default = current_experiment.language - + if request.method == 'POST' and form.validate(): - + form.populate_obj(current_experiment) db.session.commit() - - return redirect(url_for('experiment.view', exp_id=exp_id)) - - return render_template('edit_experiment.html', form=form, exp_id=exp_id) - + return redirect(url_for('experiment.view', exp_id=exp_id)) + return render_template('edit_experiment.html', form=form, exp_id=exp_id) # Background questions: @@ -395,94 +418,100 @@ def edit(): @experiment_blueprint.route('/add_bg_question', methods=['GET', 'POST']) @login_required def add_bg_question(): - + exp_id = request.args.get('exp_id', None) exp_status = experiment.query.filter_by(idexperiment=exp_id).first() if exp_status.status != 'Hidden': flash("Experiment is public. Cannot modify structure.") return redirect(url_for('experiment.view', exp_id=exp_id)) - + else: form = CreateBackgroundQuestionForm(request.form) - + if request.method == 'POST' and form.validate(): - - #Split the form data into a list that separates questions followed by the corresponding options + + # Split the form data into a list that separates questions followed by the corresponding options str = form.bg_questions_and_options.data str_list = str.split('/n') - - #Iterate through the questions and options list + + # Iterate through the questions and options list for a in range(len(str_list)): - #Split the list cells further into questions and options + # Split the list cells further into questions and options list = str_list[a].split(';') - - #Input the first item of the list as a question in db and the items followed by that as options for that question + + # Input the first item of the list as a question in db and the items followed by that as options for that question for x in range(len(list)): if x == 0: - add_bgquestion = background_question(background_question=list[x], experiment_idexperiment=exp_id) + add_bgquestion = background_question( + background_question=list[x], experiment_idexperiment=exp_id) db.session.add(add_bgquestion) db.session.commit() else: - add_bgq_option = background_question_option(background_question_idbackground_question=add_bgquestion.idbackground_question, option=list[x]) + add_bgq_option = background_question_option( + background_question_idbackground_question=add_bgquestion.idbackground_question, option=list[x]) db.session.add(add_bgq_option) db.session.commit() - - return redirect(url_for('experiment.view', exp_id=exp_id)) - + + return redirect(url_for('experiment.view', exp_id=exp_id)) + return render_template('add_bg_question.html', form=form) @experiment_blueprint.route('/edit_bg_question', methods=['GET', 'POST']) @login_required def edit_bg_question(): - + bg_question_id = request.args.get('idbackground_question', None) - #Search for the right question and for the right options. Form a string of those separated with ";" and insert the - #formed string into the edit form - current_bg_question = background_question.query.filter_by(idbackground_question=bg_question_id).first() - exp_id=current_bg_question.experiment_idexperiment + # Search for the right question and for the right options. Form a string of those separated with ";" and insert the + # formed string into the edit form + current_bg_question = background_question.query.filter_by( + idbackground_question=bg_question_id).first() + exp_id = current_bg_question.experiment_idexperiment question_string = current_bg_question.background_question - options = background_question_option.query.filter_by(background_question_idbackground_question=bg_question_id).all() - - for o in range(len(options)): - question_string = str(question_string) + str("; ") + str(options[o].option) - + options = background_question_option.query.filter_by( + background_question_idbackground_question=bg_question_id).all() + + for o in range(len(options)): + question_string = str(question_string) + \ + str("; ") + str(options[o].option) + form = EditBackgroundQuestionForm(request.form) form.bg_questions_and_options.data = question_string - #After user chooses to update the question and options lets replace the old question and options with the ones from the form + # After user chooses to update the question and options lets replace the old question and options with the ones from the form if request.method == 'POST' and form.validate(): - #Explode the string with new values from the form + # Explode the string with new values from the form form_values = form.new_values.data form_values_list = form_values.split(';') - - #Check and remove possible whitespaces from string beginnings with lstrip + + # Check and remove possible whitespaces from string beginnings with lstrip for x in range(len(form_values_list)): form_values_list[x] = form_values_list[x].lstrip() - - #Cycle through strings and update db + + # Cycle through strings and update db for x in range(len(form_values_list)): - - #Replace question and update the object to database + + # Replace question and update the object to database if x == 0: - current_bg_question.background_question = form_values_list[x] + current_bg_question.background_question = form_values_list[x] db.session.commit() - - #Delete old options from db + + # Delete old options from db for o in options: db.session.delete(o) db.session.commit() - - #Insert new options to db + + # Insert new options to db else: - new_option = background_question_option(background_question_idbackground_question=current_bg_question.idbackground_question, option=form_values_list[x]) + new_option = background_question_option( + background_question_idbackground_question=current_bg_question.idbackground_question, option=form_values_list[x]) db.session.add(new_option) db.session.commit() - + return redirect(url_for('experiment.view', exp_id=exp_id)) - + return render_template('edit_bg_question.html', form=form, exp_id=exp_id) @@ -496,24 +525,24 @@ def remove_bg_question(): if exp_status.status != 'Hidden': flash("Experiment is public. Cannot modify structure.") return redirect(url_for('experiment.view', exp_id=exp_id)) - + else: remove_id = request.args.get('idbackground_question', None) - remove_options = background_question_option.query.filter_by(background_question_idbackground_question=remove_id).all() - - for a in range(len(remove_options)): + remove_options = background_question_option.query.filter_by( + background_question_idbackground_question=remove_id).all() + + for a in range(len(remove_options)): db.session.delete(remove_options[a]) db.session.commit() - - remove_question = background_question.query.filter_by(idbackground_question=remove_id).first() - + + remove_question = background_question.query.filter_by( + idbackground_question=remove_id).first() + db.session.delete(remove_question) db.session.commit() - - return redirect(url_for('experiment.view', exp_id=exp_id)) - + return redirect(url_for('experiment.view', exp_id=exp_id)) @experiment_blueprint.route('/set_embody') @@ -521,7 +550,7 @@ def remove_bg_question(): def set_embody(): '''Enable/disable embody tool''' exp_id = request.args.get('exp_id', None) - exp = experiment.query.filter_by(idexperiment = exp_id).first() + exp = experiment.query.filter_by(idexperiment=exp_id).first() exp.embody_enabled = (True if exp.embody_enabled == False else False) return redirect(url_for('experiment.view', exp_id=exp_id)) @@ -537,16 +566,6 @@ def add_embody(): if exp_info.status != 'Hidden': flash("Experiment is public. Cannot modify structure.") return redirect(url_for('experiment.view', exp_id=exp_id)) - elif default: - - # TODO: check if default image already added - - default_embody = embody_question(experiment_idexperiment=exp_id, picture=DEFAULT_EMBODY_PICTURE, question=DEFAULT_EMBODY_QUESTION) - db.session.add(default_embody) - exp_info.embody_enabled = 1 - db.session.commit() - return redirect(url_for('experiment.view', exp_id=exp_id)) - else: form = CreateEmbodyForm(request.form) @@ -554,10 +573,18 @@ def add_embody(): picture = request.files.get("picture") question = request.form.get("question") - # get filename + if not picture: + default_embody = embody_question( + experiment_idexperiment=exp_id, picture=DEFAULT_EMBODY_PICTURE, question=question) + db.session.add(default_embody) + exp_info.embody_enabled = 1 + db.session.commit() + return redirect(url_for('experiment.view', exp_id=exp_id)) + + # get filename filename = secure_filename(picture.filename) path = 'static/embody_images/' + str(exp_id) - db_path = '/' + path + str('/') + str(filename) + db_path = '/' + path + str('/') + str(filename) target = os.path.join(APP_ROOT, path) # create folder with experiment id (if it does not exist) @@ -568,14 +595,13 @@ def add_embody(): destination = "/".join([target, filename]) picture.save(destination) - #add pages to the db - new_embody = embody_question(experiment_idexperiment=exp_id, question=question, picture=db_path) + # add pages to the db + new_embody = embody_question( + experiment_idexperiment=exp_id, question=question, picture=db_path) db.session.add(new_embody) exp_info.embody_enabled = 1 db.session.commit() - - - return redirect(url_for('experiment.view', exp_id=exp_id)) + return redirect(url_for('experiment.view', exp_id=exp_id)) return render_template('add_embody.html', form=form) @@ -583,7 +609,7 @@ def add_embody(): @experiment_blueprint.route('/add_questions', methods=['GET', 'POST']) @login_required def add_questions(): - + exp_id = request.args.get('exp_id', None) exp_status = experiment.query.filter_by(idexperiment=exp_id).first() @@ -592,47 +618,49 @@ def add_questions(): return redirect(url_for('experiment.view', exp_id=exp_id)) else: form = CreateQuestionForm(request.form) - + if request.method == 'POST' and form.validate(): - + str = form.questions_and_options.data str_list = str.split('/n') - - for a in range(len(str_list)): + + for a in range(len(str_list)): list = str_list[a].split(';') - - #If there are the right amount of values for the slider input values + + # If there are the right amount of values for the slider input values if len(list) == 3: - add_question = question(experiment_idexperiment=exp_id, question=list[0], left=list[1], right=list[2]) + add_question = question( + experiment_idexperiment=exp_id, question=list[0], left=list[1], right=list[2]) db.session.add(add_question) db.session.commit() - - #If slider has too many or too litlle parameters give an error and redirect back to input form + + # If slider has too many or too litlle parameters give an error and redirect back to input form else: - flash("Error Each slider must have 3 parameters separated by ; Some slider has:") + flash( + "Error Each slider must have 3 parameters separated by ; Some slider has:") flash(len(list)) return redirect(url_for('create.experiment_questions', exp_id=exp_id)) - - return redirect(url_for('experiment.view', exp_id=exp_id)) - + + return redirect(url_for('experiment.view', exp_id=exp_id)) + return render_template('add_questions.html', form=form) @experiment_blueprint.route('/edit_question', methods=['GET', 'POST']) @login_required def edit_question(): - + question_id = request.args.get('idquestion', None) current_question = question.query.filter_by(idquestion=question_id).first() form = EditQuestionForm(request.form, obj=current_question) - + if request.method == 'POST' and form.validate(): - + form.populate_obj(current_question) db.session.commit() - + return redirect(url_for('experiment.view', exp_id=current_question.experiment_idexperiment)) - + return render_template('edit_question.html', form=form) @@ -646,21 +674,23 @@ def remove_question(): if exp_status.status != 'Hidden': flash("Experiment is public. Cannot modify structure.") return redirect(url_for('experiment.view', exp_id=exp_id)) - + else: remove_id = request.args.get('idquestion', None) - remove_question = question.query.filter_by(idquestion=remove_id).first() + remove_question = question.query.filter_by( + idquestion=remove_id).first() # remove answers before removing questions - remove_answers = answer.query.filter_by(question_idquestion=remove_question.idquestion).all() + remove_answers = answer.query.filter_by( + question_idquestion=remove_question.idquestion).all() for a in range(len(remove_answers)): db.session.delete(remove_answers[a]) db.session.commit() - + db.session.delete(remove_question) db.session.commit() - + return redirect(url_for('experiment.view', exp_id=exp_id)) @@ -674,29 +704,30 @@ def remove_embody(): if exp_status.status != 'Hidden': flash("Experiment is public. Cannot modify structure.") return redirect(url_for('experiment.view', exp_id=exp_id)) - + else: remove_id = request.args.get('idembody', None) - remove_question = embody_question.query.filter_by(idembody=remove_id).first() + remove_question = embody_question.query.filter_by( + idembody=remove_id).first() # remove embody image from server if DEFAULT_EMBODY_PICTURE != remove_question.picture: os.remove(APP_ROOT + remove_question.picture) - remove_answers = embody_answer.query.filter_by(embody_question_idembody=remove_question.idembody).all() + remove_answers = embody_answer.query.filter_by( + embody_question_idembody=remove_question.idembody).all() remove_rows(remove_answers) db.session.delete(remove_question) db.session.commit() - - question_count = embody_question.query.filter_by(experiment_idexperiment=exp_id).count() + question_count = embody_question.query.filter_by( + experiment_idexperiment=exp_id).count() if question_count == 0: exp_status.embody_enabled = 0 db.session.commit() - - return redirect(url_for('experiment.view', exp_id=exp_id)) + return redirect(url_for('experiment.view', exp_id=exp_id)) # Stimuli: @@ -707,56 +738,59 @@ def add_stimuli(): exp_id = request.args.get('exp_id', None) exp_status = experiment.query.filter_by(idexperiment=exp_id).first() - + if exp_status.status != 'Hidden': flash("Experiment is public. Cannot modify structure.") return redirect(url_for('experiment.view', exp_id=exp_id)) else: - #If there are no pages set for the experiment lets reroute user to create experiment stimuli upload instead - is_there_any_stimuli = page.query.filter_by(experiment_idexperiment = exp_id).first() - + # If there are no pages set for the experiment lets reroute user to create experiment stimuli upload instead + is_there_any_stimuli = page.query.filter_by( + experiment_idexperiment=exp_id).first() + if is_there_any_stimuli is None: return redirect(url_for('create.experiment_upload_stimuli', exp_id=exp_id)) - + stimulus_type = request.args.get('stimulus_type', None) form = UploadStimuliForm(request.form) - + if request.method == 'POST': if stimulus_type == 'text': string = form.text.data str_list = string.split('/n') - + for a in range(len(str_list)): - add_text_stimulus = page(experiment_idexperiment=exp_id, type='text', text=str_list[a], media='none') + add_text_stimulus = page( + experiment_idexperiment=exp_id, type='text', text=str_list[a], media='none') db.session.add(add_text_stimulus) db.session.commit() - - return redirect(url_for('experiment.view', exp_id=exp_id)) - + + return redirect(url_for('experiment.view', exp_id=exp_id)) + else: - #Upload stimuli into /static/experiment_stimuli/exp_id folder - #Create the pages for the stimuli by inserting experiment_id, stimulus type, text and names of the stimulus files (as a path to the folder) + # Upload stimuli into /static/experiment_stimuli/exp_id folder + # Create the pages for the stimuli by inserting experiment_id, stimulus type, text and names of the stimulus files (as a path to the folder) path = 'static/experiment_stimuli/' + str(exp_id) target = os.path.join(APP_ROOT, path) - + if not os.path.isdir(target): os.mkdir(target) - - #This returns a list of filenames: request.files.getlist("file") + + # This returns a list of filenames: request.files.getlist("file") for file in request.files.getlist("file"): - #save files in the correct folder + # save files in the correct folder filename = file.filename destination = "/".join([target, filename]) file.save(destination) - #add pages to the db - db_path = path + str('/') + str(filename) - new_page = page(experiment_idexperiment=exp_id, type=form.type.data, media=db_path) + # add pages to the db + db_path = path + str('/') + str(filename) + new_page = page(experiment_idexperiment=exp_id, + type=form.type.data, media=db_path) db.session.add(new_page) db.session.commit() - + return redirect(url_for('experiment.view', exp_id=exp_id)) - + return redirect(url_for('experiment.view', exp_id=exp_id)) return render_template('add_stimuli.html', form=form, stimulus_type=stimulus_type) @@ -776,38 +810,38 @@ def edit_stimuli(): if request.method == 'POST' and form.validate(): print("POST IMAGE") - #If the stimulus type is not text, then the old stimulus file is deleted from os and replaced + # If the stimulus type is not text, then the old stimulus file is deleted from os and replaced if edit_page.type != 'text': - #remove old file + # remove old file target = os.path.join(APP_ROOT, edit_page.media) os.remove(target) - #upload new file + # upload new file path = 'static/experiment_stimuli/' + str(exp_id) target = os.path.join(APP_ROOT, path) - + if not os.path.isdir(target): os.mkdir(target) - - #This returns a list of filenames: request.files.getlist("file") + + # This returns a list of filenames: request.files.getlist("file") for file in request.files.getlist("file"): - #save files in the correct folder + # save files in the correct folder filename = file.filename destination = "/".join([target, filename]) file.save(destination) - - #update db object - db_path = path + str('/') + str(filename) - edit_page.media=db_path + + # update db object + db_path = path + str('/') + str(filename) + edit_page.media = db_path db.session.commit() - #If editing text stimulus no need for filehandling + # If editing text stimulus no need for filehandling else: - form.populate_obj(edit_page) - db.session.commit() - + form.populate_obj(edit_page) + db.session.commit() + return redirect(url_for('experiment.view', exp_id=exp_id)) - + return render_template('edit_stimuli.html', form=form, edit_page=edit_page) @@ -819,65 +853,68 @@ def remove_stimuli(): exp_status = experiment.query.filter_by(idexperiment=exp_id).first() if exp_status.status != 'Hidden': - + flash("Experiment is public. Cannot modify structure.") return redirect(url_for('experiment.view', exp_id=exp_id)) - + else: - + remove_id = request.args.get('idpage', None) remove_page = page.query.filter_by(idpage=remove_id).first() - experiment_pages = page.query.filter_by(experiment_idexperiment=exp_id).all() - - #if stimulustype is text, the stimulus itself is text on the database, other stimulus types are real files - #on the server and need to be deleted + experiment_pages = page.query.filter_by( + experiment_idexperiment=exp_id).all() + + # if stimulustype is text, the stimulus itself is text on the database, other stimulus types are real files + # on the server and need to be deleted if remove_page.type != 'text': - - #helper variable + + # helper variable do_not_delete_file = 'False' - - #if the file to be deleted is in duplicate use of another page then we won't delete the file + + # if the file to be deleted is in duplicate use of another page then we won't delete the file for a in range(len(experiment_pages)): if experiment_pages[a].media == remove_page.media and experiment_pages[a].idpage != remove_page.idpage: do_not_delete_file = 'True' - - #If no other page is using the file then lets remove it + + # If no other page is using the file then lets remove it if do_not_delete_file == 'False': target = os.path.join(APP_ROOT, remove_page.media) os.remove(target) - + # remove slider answer from this page - remove_question_answers = answer.query.filter_by(page_idpage=remove_page.idpage).all() + remove_question_answers = answer.query.filter_by( + page_idpage=remove_page.idpage).all() remove_rows(remove_question_answers) # remove embody answer from this page - remove_embody_answers = embody_answer.query.filter_by(page_idpage=remove_page.idpage).all() + remove_embody_answers = embody_answer.query.filter_by( + page_idpage=remove_page.idpage).all() remove_rows(remove_embody_answers) db.session.delete(remove_page) db.session.commit() - + return redirect(url_for('experiment.view', exp_id=exp_id)) - - + if remove_page.type == 'text': - + # remove slider answer from this page - remove_question_answers = answer.query.filter_by(page_idpage=remove_page.idpage).all() + remove_question_answers = answer.query.filter_by( + page_idpage=remove_page.idpage).all() remove_rows(remove_question_answers) # remove embody answer from this page - remove_embody_answers = embody_answer.query.filter_by(page_idpage=remove_page.idpage).all() + remove_embody_answers = embody_answer.query.filter_by( + page_idpage=remove_page.idpage).all() remove_rows(remove_embody_answers) db.session.delete(remove_page) db.session.commit() - + return redirect(url_for('experiment.view', exp_id=exp_id)) - - return redirect(url_for('experiment.view', exp_id=exp_id)) + return redirect(url_for('experiment.view', exp_id=exp_id)) # Misc: @@ -887,13 +924,14 @@ def remove_stimuli(): def statistics(): # TODO: Answers are in normal order although questions might be in randomized order - + exp_id = request.args.get('exp_id', None) - - experiment_info = experiment.query.filter_by(idexperiment = exp_id).all() - participants = answer_set.query.filter_by(experiment_idexperiment= exp_id).all() - - #started and finished ratings counters + + experiment_info = experiment.query.filter_by(idexperiment=exp_id).all() + participants = answer_set.query.filter_by( + experiment_idexperiment=exp_id).all() + + # started and finished ratings counters started_ratings = answer_set.query.filter_by( experiment_idexperiment=exp_id).count() experiment_page_count = page.query.filter_by( @@ -901,66 +939,101 @@ def statistics(): finished_ratings = answer_set.query.filter(and_( answer_set.answer_counter == experiment_page_count, answer_set.experiment_idexperiment == exp_id)).count() - - #Rating task headers - question_headers = question.query.filter_by(experiment_idexperiment=exp_id).all() - stimulus_headers = page.query.filter_by(experiment_idexperiment=exp_id).all() - + # Rating task headers + question_headers = question.query.filter_by( + experiment_idexperiment=exp_id).all() + stimulus_headers = page.query.filter_by( + experiment_idexperiment=exp_id).all() + pages = page.query.filter_by(experiment_idexperiment=exp_id).all() questions = question.query.filter_by(experiment_idexperiment=exp_id).all() pages_and_questions = {} + ''' + for p in pages: questions_list = [(p.idpage, a.idquestion) for a in questions] - pages_and_questions[p.idpage] = questions_list - - #List of answers per participant in format question Stimulus ID/Question ID - #those are in answer table as page_idpage and question_idquestion respectively + pages_and_questions[p.idpage] = questions_list + # List of answers per participant in format question Stimulus ID/Question ID + # those are in answer table as page_idpage and question_idquestion respectively slider_answers = {} for participant in participants: - # list only finished answer sets - if experiment_page_count == participant.answer_counter: - answers = answer.query.filter_by(answer_set_idanswer_set=participant.idanswer_set).all() - slider_answers[participant.session] = [ a.answer for a in answers] + if int(participant.answer_counter) == 0: + continue + + answers = answer.query.filter_by( + answer_set_idanswer_set=participant.idanswer_set)\ + .order_by(answer.page_idpage)\ + .all() + + # flatten pages and questions to list of tuples (page_id, question_id) + _questions = [ + item for sublist in pages_and_questions.values() for item in sublist] + - # map slider_answers from str to int and calculate mean - a = [map(int,i) for i in list(slider_answers.values())] - slider_answers['mean'] = [round(float(sum(l))/len(l), 2) for l in zip(*a)] + slider_answers[participant.session] = map_answers_to_questions( + answers, _questions) + mean = get_mean_from_slider_answers(slider_answers) + # slider_answers['mean'] = get_mean_from_slider_answers(slider_answers) - #Background question answers + slider_answers = { + 'mean': mean + } + + ''' + + slider_answers = {} + + # Background question answers bg_questions = background_question.query.filter_by( experiment_idexperiment=exp_id).all() bg_answers_for_participants = {} for participant in participants: - - # list only finished answer sets - if experiment_page_count == participant.answer_counter: + if participant.answer_counter > 0: bg_answers = background_question_answer.query.filter_by( answer_set_idanswer_set=participant.idanswer_set).all() bg_answers_list = [(a.answer) for a in bg_answers] bg_answers_for_participants[participant.session] = bg_answers_list - # embody questions embody_questions = embody_question.query.filter_by( experiment_idexperiment=exp_id).all() - return render_template('experiment_statistics.html', - experiment_info=experiment_info, - participants_and_answers=slider_answers, - pages_and_questions=pages_and_questions, - bg_questions=bg_questions, - bg_answers_for_participants=bg_answers_for_participants, - started_ratings=started_ratings, - finished_ratings=finished_ratings, - question_headers=question_headers, - stimulus_headers=stimulus_headers, - embody_questions=embody_questions - ) + return render_template('experiment_statistics.html', + experiment_info=experiment_info, + participants_and_answers=slider_answers, + pages_and_questions=pages_and_questions, + bg_questions=bg_questions, + bg_answers_for_participants=bg_answers_for_participants, + started_ratings=started_ratings, + finished_ratings=finished_ratings, + question_headers=question_headers, + stimulus_headers=stimulus_headers, + embody_questions=embody_questions) + + +@experiment_blueprint.route('/download_csv') +def download_csv(): + exp_id = request.args.get('exp_id', None) + path = request.args.get('path', None) + + filename = "experiment_{}_{}.csv".format( + exp_id, date.today().strftime("%Y-%m-%d")) + + path = '/tmp/' + path + + try: + return send_file(path, + mimetype='text/csv', + as_attachment=True, + attachment_filename=filename) + + finally: + os.remove(path) def remove_rows(rows): @@ -970,29 +1043,56 @@ def remove_rows(rows): db.session.commit() -import embody_plot -from flask_cors import CORS,cross_origin -from flask_socketio import emit -from app import socketio - - @socketio.on('connect', namespace="/create_embody") -def create_embody(): +def start_create_embody(): emit('success', {'connection': 'on'}) -@socketio.on('draw', namespace="/create_embody") -def create_embody(page_id): - page = page_id["page"] - embody = page_id["embody"] +@socketio.on('draw', namespace="/create_embody") +def create_embody(meta): + page = meta["page"] + embody = meta["embody"] img_path = embody_plot.get_coordinates(page, embody) app.logger.info(img_path) - emit('end', {'path':img_path}) + emit('end', {'path': img_path}) -@socketio.on('end', namespace="/create_embody") -def create_embody(): - print("connection end") - emit('end', {'connection': 'off'}) +@socketio.on('connect', namespace="/download_csv") +def start_download_csv(): + emit('success', {'connection': 'Start generating CSV file'}) + + +@socketio.on('generate_csv', namespace="/download_csv") +def download_csv(meta): + exp_id = meta["exp_id"] + + data = generate_csv(exp_id) + + # error handling + if isinstance(data, Exception): + emit('timeout', {'exc': str(data)}) + return + + # create temporary file + fd, path = mkstemp() + with os.fdopen(fd, 'w') as tmp: + tmp.write(data) + tmp.flush() -# EOF + # return path and filename to front so user can start downloading + filename = "experiment_{}_{}".format( + exp_id, date.today().strftime("%Y-%m-%d")) + path = path.split('/')[-1] + emit('file_ready', {'path': path, 'filename': filename}) + + +@socketio.on('end', namespace="/download_csv") +def end_download_csv(): + # TODO: not working solution... db session keeps hanging after socket session has ended + # mysqld timeout is set to 180s, so it kills hanging connections, but this is not a good solution + db.session.close() + + +@socketio.on('end', namespace="/create_embody") +def end_create_embody(): + db.session.close() diff --git a/app/models.py b/app/models.py index 6920ffaa20a1c611e9d837b5c8985d944b5410db..c792add29192ab35999a8898e477eff576fd505b 100644 --- a/app/models.py +++ b/app/models.py @@ -1,7 +1,9 @@ +from sqlalchemy import and_ +from flask import session from app import db from sqlalchemy import Column, Integer, String, Text, Boolean from flask_wtf import FlaskForm -from wtforms_sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField +from wtforms_sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField from werkzeug.security import generate_password_hash, check_password_hash from flask_login import UserMixin from app import login @@ -15,9 +17,10 @@ class background_question(db.Model): __tablename__ = "background_question" idbackground_question = db.Column(db.Integer, primary_key=True) background_question = db.Column(db.String(120)) - answers = db.relationship('background_question_answer', backref='question', lazy='dynamic') + answers = db.relationship( + 'background_question_answer', backref='question', lazy='dynamic') experiment_idexperiment = db.Column(db.Integer) - + def __repr__(self): return "<idbackground_question = '%s', background_question = '%s'>" % (self.idbackground_question, self.background_question) @@ -25,11 +28,12 @@ class background_question(db.Model): class background_question_option(db.Model): __tablename__ = "background_question_option" idbackground_question_option = db.Column(db.Integer, primary_key=True) - background_question_idbackground_question = db.Column(db.Integer, db.ForeignKey('background_question.idbackground_question')) + background_question_idbackground_question = db.Column( + db.Integer, db.ForeignKey('background_question.idbackground_question')) option = db.Column(db.String(120)) - + def __repr__(self): - return "<idbackground_question_option = '%s', background_question_idbackground_question = '%s', option = '%s'>" % (self.idbackground_question_option, self.background_question_idbackground_question, self.option) + return "<idbackground_question_option = '%s', background_question_idbackground_question = '%s', option = '%s'>" % (self.idbackground_question_option, self.background_question_idbackground_question, self.option) class experiment (db.Model): @@ -51,7 +55,7 @@ class experiment (db.Model): consent_text = db.Column(db.Text, index=True) use_forced_id = db.Column(db.String(120)) embody_enabled = db.Column(db.Boolean, unique=False, default=False) - + def __repr__(self): return "<idexperiment = '%s', name='%s', instruction='%s', directoryname='%s', language='%s', status='%s', randomization='%s', short_instruction='%s', single_sentence_instruction='%s', is_archived='%s', creator_name='%s', research_notification_filename='%s', creation_time='%s', stimulus_size='%s', consent_text='%s', use_forced_id='%s', embody_enabled='%s'>" % (self.idexperiment, self.name, self.instruction, self.directoryname, self.language, self.status, self.randomization, self.short_instruction, self.single_sentence_instruction, self.is_archived, self.creator_name, self.research_notification_filename, self.creation_time, self.stimulus_size, self.consent_text, self.use_forced_id, self.embody_enabled) @@ -59,13 +63,16 @@ class experiment (db.Model): class answer_set (db.Model): __tablename__ = "answer_set" idanswer_set = db.Column(db.Integer, primary_key=True) - experiment_idexperiment = db.Column(db.Integer, db.ForeignKey('experiment.idexperiment')) + experiment_idexperiment = db.Column( + db.Integer, db.ForeignKey('experiment.idexperiment')) session = db.Column(db.String(120)) agreement = db.Column(db.String(120)) answer_counter = db.Column(db.Integer) answer_type = db.Column(db.String(120)) - registration_time = db.Column(db.DateTime, index=True, default=datetime.utcnow) - last_answer_time = db.Column(db.DateTime, index=True, default=datetime.utcnow) + registration_time = db.Column( + db.DateTime, index=True, default=datetime.utcnow) + last_answer_time = db.Column( + db.DateTime, index=True, default=datetime.utcnow) def __repr__(self): return "<idanswer_set = '%s', experiment_idexperiment = '%s', session = '%s', agreement = '%s', answer_counter = '%s', registration_time = '%s', last_answer_time = '%s'>" % (self.idanswer_set, self.experiment_idexperiment, self.session, self.agreement, self.answer_counter, self.registration_time, self.last_answer_time) @@ -74,17 +81,20 @@ class answer_set (db.Model): class background_question_answer(db.Model): __tablename__ = "background_question_answer" idbackground_question_answer = db.Column(db.Integer, primary_key=True) - answer_set_idanswer_set = db.Column(db.Integer, db.ForeignKey('answer_set.idanswer_set')) + answer_set_idanswer_set = db.Column( + db.Integer, db.ForeignKey('answer_set.idanswer_set')) answer = db.Column(db.String(120)) - background_question_idbackground_question = db.Column(db.Integer, db.ForeignKey('background_question.idbackground_question')) + background_question_idbackground_question = db.Column( + db.Integer, db.ForeignKey('background_question.idbackground_question')) def __repr__(self): - return "<idbackground_question_answer = '%s', answer_set_idanswer_set = '%s', answer = '%s', background_question_idbackground_question = '%s'>" % (self.idbackground_question_answer, self.answer_set_idanswer_set, self.answer, self.background_question_idbackground_question) + return "<idbackground_question_answer = '%s', answer_set_idanswer_set = '%s', answer = '%s', background_question_idbackground_question = '%s'>" % (self.idbackground_question_answer, self.answer_set_idanswer_set, self.answer, self.background_question_idbackground_question) def background_question_answer_query(): return background_question_answer.query + """ class ChoiceForm(FlaskForm): opts = QuerySelectField(query_factory=background_question_answer_query, allow_blank=True) @@ -98,30 +108,33 @@ vastaukset = u.answers.all() class question (db.Model): __tablename__ = "question" idquestion = db.Column(db.Integer, primary_key=True) - experiment_idexperiment = db.Column(db.Integer, db.ForeignKey('experiment.idexperiment')) + experiment_idexperiment = db.Column( + db.Integer, db.ForeignKey('experiment.idexperiment')) question = db.Column(db.String(120)) left = db.Column(db.String(120)) right = db.Column(db.String(120)) def __repr__(self): - return "<idquestion = '%s', experiment_idexperiment = '%s', question = '%s', left = '%s', right = '%s'>" % (self.idquestion, self.experiment_idexperiment, self.question, self.left, self.right) + return "<idquestion = '%s', experiment_idexperiment = '%s', question = '%s', left = '%s', right = '%s'>" % (self.idquestion, self.experiment_idexperiment, self.question, self.left, self.right) class embody_question (db.Model): __tablename__ = "embody_question" idembody = db.Column(db.Integer, primary_key=True) - experiment_idexperiment = db.Column(db.Integer, db.ForeignKey('experiment.idexperiment')) + experiment_idexperiment = db.Column( + db.Integer, db.ForeignKey('experiment.idexperiment')) picture = db.Column(db.Text) question = db.Column(db.Text) def __repr__(self): - return "<idembody = '%s', experiment_idexperiment = '%s', picture = '%s', question = '%s'>" % (self.idembody, self.experiment_idexperiment, self.picture, self.question ) + return "<idembody = '%s', experiment_idexperiment = '%s', picture = '%s', question = '%s'>" % (self.idembody, self.experiment_idexperiment, self.picture, self.question) class page (db.Model): __tablename__ = "page" idpage = db.Column(db.Integer, primary_key=True) - experiment_idexperiment = db.Column(db.Integer, db.ForeignKey('experiment.idexperiment')) + experiment_idexperiment = db.Column( + db.Integer, db.ForeignKey('experiment.idexperiment')) type = db.Column(db.String(120), index=True) text = db.Column(db.Text) media = db.Column(db.String(120), index=True) @@ -133,11 +146,19 @@ class page (db.Model): class answer (db.Model): __tablename__ = "answer" idanswer = db.Column(db.Integer, primary_key=True) - question_idquestion = db.Column(db.Integer, db.ForeignKey('question.idquestion')) - answer_set_idanswer_set = db.Column(db.Integer, db.ForeignKey('answer_set.idanswer_set')) + question_idquestion = db.Column( + db.Integer, db.ForeignKey('question.idquestion')) + answer_set_idanswer_set = db.Column( + db.Integer, db.ForeignKey('answer_set.idanswer_set')) answer = db.Column(db.String(120)) page_idpage = db.Column(db.Integer, db.ForeignKey('page.idpage')) + def question(self): + return int(self.question_idquestion) + + def result(self): + return int(self.answer) + def __repr__(self): return "<idanswer = '%s', question_idquestion = '%s', answer_set_idanswer_set = '%s', answer = '%s', page_idpage = '%s'>" % (self.idanswer, self.question_idquestion, self.answer_set_idanswer_set, self.answer, self.page_idpage) @@ -145,11 +166,19 @@ class answer (db.Model): class embody_answer (db.Model): __tablename__ = "embody_answer" idanswer = db.Column(db.Integer, primary_key=True) - answer_set_idanswer_set = db.Column(db.Integer, db.ForeignKey('answer_set.idanswer_set')) + answer_set_idanswer_set = db.Column( + db.Integer, db.ForeignKey('answer_set.idanswer_set')) page_idpage = db.Column(db.Integer, db.ForeignKey('page.idpage')) - embody_question_idembody = db.Column(db.Integer, db.ForeignKey('embody_question.idembody')) + embody_question_idembody = db.Column( + db.Integer, db.ForeignKey('embody_question.idembody')) coordinates = db.Column(db.Text) + def question(self): + return self.embody_question_idembody + + def result(self): + return self.coordinates + def __repr__(self): return "<idanswer = '%s', answer_set_idanswer_set = '%s', coordinates = '%s', page_idpage = '%s', embody_question_idembody='%s' >" % (self.idanswer, self.answer_set_idanswer_set, self.coordinates, self.page_idpage, self.embody_question_idembody) @@ -161,17 +190,24 @@ class trial_randomization (db.Model): randomized_idpage = db.Column(db.Integer) answer_set_idanswer_set = db.Column(db.Integer) experiment_idexperiment = db.Column(db.Integer) - + def __repr__(self): return "<idtrial_randomization = '%s', page_idpage = '%s', randomized_idpage = '%s', answer_set_idanswer_set = '%s', experiment_idexperiment = '%s'>" % (self.idtrial_randomization, self.page_idpage, self.randomized_idpage, self.answer_set_idanswer_set, self.experiment_idexperiment) + @classmethod + def get_randomized_page(cls, page_id): + return cls.query.filter(and_( + cls.answer_set_idanswer_set == session['answer_set'], + cls.page_idpage == page_id)).first() + class forced_id (db.Model): __tablename__ = "forced_id" idforced_id = db.Column(db.Integer, primary_key=True) - experiment_idexperiment = db.Column(db.Integer, db.ForeignKey('experiment.idexperiment')) - pregenerated_id = db.Column(db.String(120)) - + experiment_idexperiment = db.Column( + db.Integer, db.ForeignKey('experiment.idexperiment')) + pregenerated_id = db.Column(db.String(120)) + def __repr__(self): return "<idforced_id = '%s', experiment_idexperiment = '%s', pregenerated_id = '%s'>" % (self.idforced_id, self.experiment_idexperiment, self.pregenerated_id) @@ -184,16 +220,15 @@ class user(UserMixin, db.Model): password_hash = db.Column(db.String(128)) def __repr__(self): - return '<user {}>'.format(self.username) - + return '<user {}>'.format(self.username) + def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) - + @login.user_loader def load_user(id): return user.query.get(int(id)) - \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index 342fcb19825935a067d22e8d145fe361c2a48a86..9d64ba14306b391f4546579683f54c383f1e1b8f 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,52 +1,41 @@ - - -import csv import os import random import secrets from datetime import datetime -import tempfile -import json - -from flask import ( - Flask, - render_template, - request, - session, - flash, - redirect, - url_for, - Blueprint, - send_file -) +from flask import (render_template, + request, + session, + flash, + redirect, + url_for) from sqlalchemy import and_ from flask_login import current_user, login_user, logout_user, login_required -from flask_babel import Babel, _, lazy_gettext as _l -from app import app, db, babel +from app import app, db from app.models import background_question, experiment from app.models import background_question_answer -from app.models import page, question, embody_question, embody_answer +from app.models import page from app.models import background_question_option -from app.models import answer_set, answer, forced_id +from app.models import answer_set, forced_id from app.models import user, trial_randomization from app.forms import LoginForm, RegisterForm, StartWithIdForm -#Stimuli upload folder setting +# Stimuli upload folder setting APP_ROOT = os.path.dirname(os.path.abspath(__file__)) + @app.route('/') @app.route('/index') def index(): experiments = experiment.query.all() - + if session: flash("") else: #flash("sessio ei voimassa") session['language'] = "English" - + return render_template('index.html', title='Home', experiments=experiments) @@ -54,21 +43,21 @@ def index(): def consent(): exp_id = request.args.get('exp_id', None) experiment_info = experiment.query.filter_by(idexperiment=exp_id).first() - + instruction_paragraphs = str(experiment_info.short_instruction) instruction_paragraphs = instruction_paragraphs.split('<br>') - + consent_paragraphs = str(experiment_info.consent_text) consent_paragraphs = consent_paragraphs.split('<br>') if experiment_info.use_forced_id == 'On': return redirect(url_for('begin_with_id', exp_id=exp_id)) - return render_template('consent.html', - exp_id=exp_id, - experiment_info=experiment_info, - instruction_paragraphs=instruction_paragraphs, - consent_paragraphs=consent_paragraphs) + return render_template('consent.html', + exp_id=exp_id, + experiment_info=experiment_info, + instruction_paragraphs=instruction_paragraphs, + consent_paragraphs=consent_paragraphs) @app.route('/set_language') @@ -88,28 +77,28 @@ def remove_language(): def participant_session(): '''Set up session variables and create answer_set (database level sessions)''' - #start session + # start session session['exp_id'] = request.args.get('exp_id', None) session['agree'] = request.args.get('agree', None) - - #If user came via the route for "I have already a participant ID that I wish to use, Use that ID, otherwise generate a random ID + + # If user came via the route for "I have already a participant ID that I wish to use, Use that ID, otherwise generate a random ID if 'begin_with_id' in session: session['user'] = session['begin_with_id'] session.pop('begin_with_id', None) else: - #lets generate a random id. If the same id is allready in db, lets generate a new one and finally use that in session['user'] + # lets generate a random id. If the same id is allready in db, lets generate a new one and finally use that in session['user'] random_id = secrets.token_hex(3) check_id = answer_set.query.filter_by(session=random_id).first() while check_id is not None: random_id = secrets.token_hex(3) check_id = answer_set.query.filter_by(session=random_id).first() - + session['user'] = random_id # Set session status variables - exp_status = experiment.query.filter_by(idexperiment=session['exp_id']).first() - + exp_status = experiment.query.filter_by( + idexperiment=session['exp_id']).first() # Create answer set for the participant in the database the_time = datetime.now() @@ -121,55 +110,57 @@ def participant_session(): answer_set_type = 'embody' participant_answer_set = answer_set(experiment_idexperiment=session['exp_id'], - session=session['user'], - agreement = session['agree'], - answer_counter = '0', - answer_type = answer_set_type, - registration_time=the_time, + session=session['user'], + agreement=session['agree'], + answer_counter='0', + answer_type=answer_set_type, + registration_time=the_time, last_answer_time=the_time) db.session.add(participant_answer_set) db.session.commit() - - #If trial randomization is set to 'On' for the experiment, create a randomized trial order for this participant - #identification is based on the uniquie answer set id + # If trial randomization is set to 'On' for the experiment, create a randomized trial order for this participant + # identification is based on the uniquie answer set id if exp_status.randomization == 'On': - + session['randomization'] = 'On' - - #create a list of page id:s for the experiment - experiment_pages = page.query.filter_by(experiment_idexperiment=session['exp_id']).all() + + # create a list of page id:s for the experiment + experiment_pages = page.query.filter_by( + experiment_idexperiment=session['exp_id']).all() original_id_order_list = [(int(o.idpage)) for o in experiment_pages] - - #create a randomized page id list - helper_list = original_id_order_list + + # create a randomized page id list + helper_list = original_id_order_list randomized_order_list = [] - + for i in range(len(helper_list)): element = random.choice(helper_list) helper_list.remove(element) randomized_order_list.append(element) - - #Input values into trial_randomization table where the original page_ids have a corresponding randomized counterpart - experiment_pages = page.query.filter_by(experiment_idexperiment=session['exp_id']).all() + + # Input values into trial_randomization table where the original page_ids have a corresponding randomized counterpart + experiment_pages = page.query.filter_by( + experiment_idexperiment=session['exp_id']).all() original_id_order_list = [(int(o.idpage)) for o in experiment_pages] - + for c in range(len(original_id_order_list)): - random_page = trial_randomization(page_idpage=original_id_order_list[c], randomized_idpage=randomized_order_list[c], answer_set_idanswer_set = participant_answer_set.idanswer_set, experiment_idexperiment = session['exp_id']) + random_page = trial_randomization(page_idpage=original_id_order_list[c], randomized_idpage=randomized_order_list[ + c], answer_set_idanswer_set=participant_answer_set.idanswer_set, experiment_idexperiment=session['exp_id']) db.session.add(random_page) db.session.commit() - + if exp_status.randomization == "Off": session['randomization'] = "Off" - #store participants session id in session list as answer_set, based on experiment id and session id - session_id_for_participant = answer_set.query.filter(and_(answer_set.session==session['user'], answer_set.experiment_idexperiment==session['exp_id'])).first() + # store participants session id in session list as answer_set, based on experiment id and session id + session_id_for_participant = answer_set.query.filter(and_( + answer_set.session == session['user'], answer_set.experiment_idexperiment == session['exp_id'])).first() session['answer_set'] = session_id_for_participant.idanswer_set - - - #collect experiments mediatype from db to session['type']. - #This is later used in task.html to determine page layout based on stimulus type - mediatype = page.query.filter_by(experiment_idexperiment=session['exp_id']).first() + # collect experiments mediatype from db to session['type']. + # This is later used in task.html to determine page layout based on stimulus type + mediatype = page.query.filter_by( + experiment_idexperiment=session['exp_id']).first() if mediatype: session['type'] = mediatype.type else: @@ -180,124 +171,129 @@ def participant_session(): if 'user' in session: user = session['user'] return redirect('/register') - + return "Session start failed return <a href = '/login'></b>" + "Home</b></a>" @app.route('/register', methods=['GET', 'POST']) def register(): - + form = RegisterForm(request.form) questions_and_options = {} - questions = background_question.query.filter_by(experiment_idexperiment=session['exp_id']).all() - - for q in questions: - - options = background_question_option.query.filter_by(background_question_idbackground_question=q.idbackground_question).all() - options_list = [(o.option, o.idbackground_question_option) for o in options] - questions_and_options[q.idbackground_question, q.background_question] = options_list - - + questions = background_question.query.filter_by( + experiment_idexperiment=session['exp_id']).all() + + for q in questions: + + options = background_question_option.query.filter_by( + background_question_idbackground_question=q.idbackground_question).all() + options_list = [(o.option, o.idbackground_question_option) + for o in options] + questions_and_options[q.idbackground_question, + q.background_question] = options_list + form.questions1 = questions_and_options - - if request.method == 'POST'and form.validate(): - + + if request.method == 'POST' and form.validate(): + data = request.form.to_dict() for key, value in data.items(): - - #tähän db insertit + # tähän db insertit - #flash(key) - #flash(value) - #Input registration page answers to database - participant_background_question_answers = background_question_answer(answer_set_idanswer_set=session['answer_set'], answer=value, background_question_idbackground_question=key) + # flash(key) + # flash(value) + # Input registration page answers to database + participant_background_question_answers = background_question_answer( + answer_set_idanswer_set=session['answer_set'], answer=value, background_question_idbackground_question=key) db.session.add(participant_background_question_answers) db.session.commit() return redirect('/instructions') - - - return render_template('register.html', form=form) + return render_template('register.html', form=form) @app.route('/begin_with_id', methods=['GET', 'POST']) def begin_with_id(): '''Begin experiment with experiment ID. GET -method returns login page for starting the experiment and POST -method verifys users ID before starting new experiment''' - + exp_id = request.args.get('exp_id', None) form = StartWithIdForm() experiment_info = experiment.query.filter_by(idexperiment=exp_id).first() - + instruction_paragraphs = str(experiment_info.short_instruction) instruction_paragraphs = instruction_paragraphs.split('<br>') - + consent_paragraphs = str(experiment_info.consent_text) consent_paragraphs = consent_paragraphs.split('<br>') if form.validate_on_submit(): - + variable = form.participant_id.data - - #check if participant ID is found from db with this particular ID. If a match is found inform about error - participant = answer_set.query.filter(and_(answer_set.session==variable, answer_set.experiment_idexperiment==exp_id)).first() - is_id_valid = forced_id.query.filter(and_(forced_id.pregenerated_id==variable, forced_id.experiment_idexperiment==exp_id)).first() - + + # check if participant ID is found from db with this particular ID. If a match is found inform about error + participant = answer_set.query.filter(and_( + answer_set.session == variable, answer_set.experiment_idexperiment == exp_id)).first() + is_id_valid = forced_id.query.filter(and_( + forced_id.pregenerated_id == variable, forced_id.experiment_idexperiment == exp_id)).first() + if participant is not None: flash(_('ID already in use')) - return redirect(url_for('begin_with_id', exp_id=exp_id)) - - #if there was not a participant already in DB: + return redirect(url_for('begin_with_id', exp_id=exp_id)) + + # if there was not a participant already in DB: if participant is None: - + if is_id_valid is None: flash(_('No such ID set for this experiment')) - return redirect(url_for('begin_with_id', exp_id=exp_id)) + return redirect(url_for('begin_with_id', exp_id=exp_id)) else: - #save the participant ID in session list for now, this is deleted after the session has been started in participant_session-view + # save the participant ID in session list for now, this is deleted after the session has been started in participant_session-view session['begin_with_id'] = form.participant_id.data return render_template('consent.html', exp_id=exp_id, experiment_info=experiment_info, instruction_paragraphs=instruction_paragraphs, consent_paragraphs=consent_paragraphs) - + return render_template('begin_with_id.html', exp_id=exp_id, form=form) @app.route('/admin_dryrun', methods=['GET', 'POST']) @login_required def admin_dryrun(): - + exp_id = request.args.get('exp_id', None) form = StartWithIdForm() experiment_info = experiment.query.filter_by(idexperiment=exp_id).first() if form.validate_on_submit(): - - #check if participant ID is found from db with this particular ID. If a match is found inform about error - participant = answer_set.query.filter(and_(answer_set.session==form.participant_id.data, answer_set.experiment_idexperiment==exp_id)).first() + + # check if participant ID is found from db with this particular ID. If a match is found inform about error + participant = answer_set.query.filter(and_( + answer_set.session == form.participant_id.data, answer_set.experiment_idexperiment == exp_id)).first() if participant is not None: flash('ID already in use') - return redirect(url_for('admin_dryrun', exp_id=exp_id)) - - #if there was not a participant already in DB: + return redirect(url_for('admin_dryrun', exp_id=exp_id)) + + # if there was not a participant already in DB: if participant is None: - #save the participant ID in session list for now, this is deleted after the session has been started in participant_session-view + # save the participant ID in session list for now, this is deleted after the session has been started in participant_session-view session['begin_with_id'] = form.participant_id.data return render_template('consent.html', exp_id=exp_id, experiment_info=experiment_info) - + return render_template('admin_dryrun.html', exp_id=exp_id, form=form) @app.route('/instructions') def instructions(): - + participant_id = session['user'] - instructions = experiment.query.filter_by(idexperiment = session['exp_id']).first() - + instructions = experiment.query.filter_by( + idexperiment=session['exp_id']).first() + instruction_paragraphs = str(instructions.instruction) instruction_paragraphs = instruction_paragraphs.split('<br>') - + return render_template('instructions.html', instruction_paragraphs=instruction_paragraphs, participant_id=participant_id) @@ -308,13 +304,14 @@ def login(): return redirect(url_for('index')) form = LoginForm() if form.validate_on_submit(): - user_details = user.query.filter_by(username=form.username.data).first() + user_details = user.query.filter_by( + username=form.username.data).first() if user_details is None or not user_details.check_password(form.password.data): flash('Invalid username or password') return redirect(url_for('login')) - login_user(user_details, remember=form.remember_me.data) + login_user(user_details, remember=form.remember_me.data) return redirect(url_for('index')) - + # flash('Login requested for user {}, remember_me={}'.format( # form.username.data, form.remember_me.data)) # return redirect('/index') @@ -329,129 +326,15 @@ def logout(): @app.route('/view_research_notification') def view_research_notification(): - + exp_id = request.args.get('exp_id', None) image = experiment.query.filter_by(idexperiment=exp_id).first() research_notification_filename = image.research_notification_filename - - return render_template('view_research_notification.html', research_notification_filename=research_notification_filename) - - -@app.route('/download_csv') -@login_required -def download_csv(): - - exp_id = request.args.get('exp_id', None) - experiment_info = experiment.query.filter_by(idexperiment = exp_id).all() - - # answer sets with participant ids - participants = answer_set.query.filter_by(experiment_idexperiment= exp_id).all() - - # pages aka stimulants - pages = page.query.filter_by(experiment_idexperiment=exp_id).all() - - # background questions - bg_questions = background_question.query.filter_by( - experiment_idexperiment=exp_id).all() - - # question - questions = question.query.filter_by(experiment_idexperiment=exp_id).all() - - # embody questions - embody_questions = embody_question.query.filter_by(experiment_idexperiment=exp_id).all() - - #started and finished ratings counters - started_ratings = answer_set.query.filter_by( - experiment_idexperiment=exp_id).count() - experiment_page_count = page.query.filter_by( - experiment_idexperiment=exp_id).count() - finished_ratings = answer_set.query.filter(and_( - answer_set.answer_counter == experiment_page_count, answer_set.experiment_idexperiment == exp_id)).count() - - csv = '' - - # create CSV-header - header = 'participant id;' - header += ';'.join([str(count) +'. bg_question: '+ question.background_question.strip() for count,question in enumerate(bg_questions, 1)]) - for idx in range(1,len(pages) + 1): - if len(questions) > 0: - header += ';' + ';'.join(['page' + str(idx) + '_' + str(count) +'. slider_question: ' + question.question.strip() for count,question in enumerate(questions, 1)]) - for idx in range(1,len(pages) + 1): - if len(embody_questions) > 0: - header += ';' + ';'.join(['page' + str(idx) + '_' + str(count) +'. embody_question: '+ question.picture.strip() for count,question in enumerate(embody_questions, 1)]) - - csv += header + '\r\n' - answer_row = '' - - for participant in participants: - - # list only finished answer sets - if experiment_page_count == participant.answer_counter: - - try: - - # append user session id - answer_row += participant.session + ';' - - # append background question answers - bg_answers = background_question_answer.query.filter_by(answer_set_idanswer_set=participant.idanswer_set).all() - bg_answers_list = [ str(a.answer).strip() for a in bg_answers] - answer_row += ';'.join(bg_answers_list) + ';' - - # append slider answers - slider_answers = answer.query.filter_by(answer_set_idanswer_set=participant.idanswer_set).all() - answers_list = [ str(a.answer).strip() for a in slider_answers] - answer_row += ';'.join(answers_list) + ';' if slider_answers else len(questions) * len(pages) * ';' - - # append embody answers (coordinates) - # save embody answers as bitmap images - embody_answers = embody_answer.query.filter_by(answer_set_idanswer_set=participant.idanswer_set).all() - answers_list = [] - for embody_answer_data in embody_answers: - - try: - embody_answer_data = json.loads(embody_answer_data.coordinates) - coordinates_to_bitmap = [[0 for x in range(embody_answer_data['height'] + 2)] for y in range(embody_answer_data['width'] + 2)] - - for point in list(zip( embody_answer_data['x'], embody_answer_data['y'] )): - - try: - coordinates_to_bitmap[point[0]][point[1]] += 0.1 - except IndexError: - continue - - answers_list.append(json.dumps(coordinates_to_bitmap)) - - except ValueError as err: - app.logger.info(err) - - # old way to save only visited points: - # answers_list = [ json.dumps(list(zip( json.loads(a.coordinates)['x'], json.loads(a.coordinates)['y']))) for a in embody_answers] - answer_row += ';'.join(answers_list) if embody_answers else len(embody_questions) * len(pages) * ';' - - except TypeError as err: - print(err) - - csv += answer_row + '\r\n' - answer_row = '' - - try: - fd, path = tempfile.mkstemp() - with os.fdopen(fd, 'w') as tmp: - tmp.write(csv) - tmp.flush() - - return send_file(path, mimetype='text/csv') + return render_template('view_research_notification.html', research_notification_filename=research_notification_filename) - finally: - os.remove(path) - @app.route('/researcher_info') @login_required def researcher_info(): return render_template('researcher_info.html') - - -# EOF diff --git a/app/static/css/custom.css b/app/static/css/custom.css deleted file mode 100644 index 407f953044adc21af3f22e7b7565a6e864acf6ee..0000000000000000000000000000000000000000 --- a/app/static/css/custom.css +++ /dev/null @@ -1,5 +0,0 @@ -body{ - -background-color: #000000; - -} \ No newline at end of file diff --git a/app/static/css/main.css b/app/static/css/main.css index eeb85710389fc8101bdd970f30d073288b47bf9d..54685cd436f2eaed95f6280edc034423d1500284 100644 --- a/app/static/css/main.css +++ b/app/static/css/main.css @@ -75,4 +75,20 @@ body { max-width: 90%; } -} \ No newline at end of file +} + +.stimulus img { + height: 100%; + object-fit: contain; +} + + +#export-link-container { + margin-top: 20px; + padding: 10px; +} + +#export-error { + float:right; + color:red; +} diff --git a/app/static/js/canvas.js b/app/static/js/canvas.js index 0f8bf30e0a0979f4382582f4b61dd213899dc957..b456b80e5315f4334ca192bf0d6ad8c5617c8a01 100644 --- a/app/static/js/canvas.js +++ b/app/static/js/canvas.js @@ -59,8 +59,14 @@ $(document).ready(function() { // Click handlers canvas.mousedown(function(e){ - var mouseX = e.pageX - this.offsetLeft; - var mouseY = e.pageY - this.offsetTop; + + //var mouseX = e.pageX - this.offsetLeft; + //var mouseY = e.pageY - this.offsetTop; + + var parentOffset = $(this).offset(); + var mouseX = e.pageX - parentOffset.left; + var mouseY = e.pageY - parentOffset.top; + paint = true; if (pointInsideBaseImage([mouseX, mouseY])) { @@ -70,8 +76,10 @@ $(document).ready(function() { }); canvas.mousemove(function(e){ - var mouseX = e.pageX - this.offsetLeft; - var mouseY = e.pageY - this.offsetTop; + + var parentOffset = $(this).offset(); + var mouseX = e.pageX - parentOffset.left; + var mouseY = e.pageY - parentOffset.top; if (paint && pointInsideBaseImage([mouseX, mouseY])){ addClick(mouseX, mouseY, true); @@ -81,8 +89,13 @@ $(document).ready(function() { canvas.bind('touchmove', function(e){ e.preventDefault() - var mouseX = e.touches[0].pageX - this.offsetLeft; - var mouseY = e.touches[0].pageY - this.offsetTop; + + //var mouseX = e.touches[0].pageX - this.offsetLeft; + //var mouseY = e.touches[0].pageY - this.offsetTop; + + var parentOffset = $(this).offset(); + var mouseX = e.touches[0].pageX - parentOffset.left; + var mouseY = e.touches[0].pageY - parentOffset.top; [mouseX, mouseY] = scaleClickCoordinates($(this)[0], mouseX, mouseY) @@ -94,8 +107,11 @@ $(document).ready(function() { canvas.bind('touchstart', function(e){ e.preventDefault() - var mouseX = e.touches[0].pageX - this.offsetLeft; - var mouseY = e.touches[0].pageY - this.offsetTop; + + var parentOffset = $(this).offset(); + var mouseX = e.touches[0].pageX - parentOffset.left; + var mouseY = e.touches[0].pageY - parentOffset.top; + paint = true; [mouseX, mouseY] = scaleClickCoordinates($(this)[0], mouseX, mouseY) diff --git a/app/static/js/getCSV.js b/app/static/js/getCSV.js new file mode 100644 index 0000000000000000000000000000000000000000..4b0313bbc2f96e59041548022644136f886ac246 --- /dev/null +++ b/app/static/js/getCSV.js @@ -0,0 +1,77 @@ + + + +$(document).ready(function() { + + var exportButton = $(".get-csv-results"); + + var progressBarContainer = $(".progress") + var progressBar = $("#export-results-bar") + + var exportLinkContainer = $("#export-link-container"); + var exportLink = $("#export-link"); + var exportError = $("#export-error"); + + // With sockets + function initConnection(socket) { + + socket.on('success', function(msg) { + exportButton.text('Generating file...') + exportButton.addClass('disabled') + }); + + socket.on('progress', function(data) { + progressBar.width(100*(data.done/data.from) + '%') + }); + + socket.on('timeout', function(data) { + // kill connection + + socket.emit('end') + socket.disconnect() + + exportButton.text('Export results') + exportButton.removeClass('disabled') + progressBarContainer.addClass("hidden") + + // show error + exportLinkContainer.removeClass("hidden") + exportError.text('Error: ' + data.exc) + }); + + socket.on('file_ready', function(file) { + + socket.emit('end') + socket.disconnect() + + exportButton.text('File is ready!') + + // show link + exportLinkContainer.removeClass("hidden") + exportLink.text('Download: ' + file.filename + '.csv') + + // set filename to exportlink + var href = exportLink.attr('href'); + href += '&path=' + file.path + $(exportLink).attr('href', href); + + // Remove progress bar + progressBarContainer.addClass("hidden") + progressBar.width('0%') + }); + } + + + exportButton.click(function(event) { + event.preventDefault() + + // Init socket + var socket = io.connect(exportURL); + initConnection(socket) + + // start generating csv file... + socket.emit('generate_csv', {exp_id: this.dataset.value}) + + progressBarContainer.removeClass("hidden") + }) +}) diff --git a/app/static/js/getDrawing.js b/app/static/js/getDrawing.js index ed1a11d87e2dd09ac7b5760f1953ef3da571a88b..70db2ae5142c7cda61360b362877a3b2d410a20d 100644 --- a/app/static/js/getDrawing.js +++ b/app/static/js/getDrawing.js @@ -1,8 +1,5 @@ -const baseURI = 'http://onni.utu.fi/'; -var getDrawingURI = baseURI + 'create_embody'; - $(document).ready(function() { var drawButtons = $(".embody-get-drawing"); @@ -22,7 +19,8 @@ $(document).ready(function() { }); socket.on('end', function(img) { - + // kill connection + socket.emit('end') socket.disconnect() // Draw image to statistic -page @@ -44,32 +42,13 @@ $(document).ready(function() { var socket = io.connect(getDrawingURI); initConnection(socket) - // var pageId = this.dataset.value.split('-')[0] var embodyId = this.dataset.value.split('-')[1] + socket.emit('draw', {page:pageId, embody:embodyId}) progressBarContainer.removeClass("hidden") scrollTo('plotted-image') - - /* - With AJAX -calls - var spinner = $(event.target.firstElementChild) - spinner.removeClass("hidden") - - $.ajax({ - url: getDrawingURI, - method: 'POST', - data: {page:pageId} - }).done(function(data) { - var source = JSON.parse(data).path; - console.log(source) - d = new Date() - imageContainer.attr("src", "/static/" + source + "?" +d.getTime()) - spinner.addClass("hidden") - }) - */ - }) function scrollTo(hash) { @@ -77,5 +56,4 @@ $(document).ready(function() { 'scrollTop': $('#'+hash).offset().top - 250 }, 500); } - }) diff --git a/app/static/js/urls.js b/app/static/js/urls.js new file mode 100644 index 0000000000000000000000000000000000000000..d2349e01d48a4087afe894ff7d0d22e1c849c4de --- /dev/null +++ b/app/static/js/urls.js @@ -0,0 +1,6 @@ + +const baseURI = 'localhost/'; +//const baseURI = 'http://onni.utu.fi/'; + +var exportURL = baseURI + 'download_csv'; +var getDrawingURI = baseURI + 'create_embody'; \ No newline at end of file diff --git a/app/task/templates/task.html b/app/task/templates/task.html index 6e322ac105947dda8ac4796a6fa3e6fcf0b086b9..62f57d7179c6925d48aba68eb49bbe51d3ca6e53 100644 --- a/app/task/templates/task.html +++ b/app/task/templates/task.html @@ -1,117 +1,136 @@ {% extends "base.html" %} {% block content %} -<br> -{% if session['randomization']=='Off' %} +<div class="container stimulus"> + <div class="row justify-content-center"> - {% if session['type']=='text' %} + {% if session['randomization']=='Off' %} + + {% if session['type']=='text' %} <div class="container text-center mt-5 pt-5"> {% for page in pages.items %} <h{{ stimulus_size_text }} class="text-center mt-5"><br>{{ page.text }}</h{{ stimulus_size_text }}> {% endfor %} </div> - <br><br> - {% endif %} - - {% if session['type']=='picture' %} - <div class="container stimulus col-{{stimulus_size}} mt-5 pt-5"> + <br><br> + {% endif %} + + {% if session['type']=='picture' %} + <div class="stimulus col-{{stimulus_size}} mt-5 pt-5"> {% for page in pages.items %} - <img src="/{{ page.media }}" class="img-fluid"> + <img src="/{{ page.media }}" class="img-fluid"> {% endfor %} </div> - {% endif %} - - {% if session['type']=='video' %} + {% endif %} + + {% if session['type']=='video' %} <div class="col-{{stimulus_size}} container stimulus mt-5 pt-5"> {% for page in pages.items %} - <div class="embed-responsive embed-responsive-16by9"> - <iframe class="embed-responsive-item" src="/{{ page.media }}" allowfullscreen></iframe> - </div> + <div class="embed-responsive embed-responsive-16by9"> + <iframe class="embed-responsive-item" src="/{{ page.media }}" allowfullscreen></iframe> + </div> {% endfor %} </div> - {% endif %} + {% endif %} - {% if session['type']=='audio' %} + {% if session['type']=='audio' %} <div class="col-{{stimulus_size}} container stimulus mt-5 pt-5"> {% for page in pages.items %} - <div class="embed-responsive embed-responsive-16by9"> - <iframe class="embed-responsive-item" src="/{{ page.media }}" allowfullscreen></iframe> - </div> + <div class="embed-responsive embed-responsive-16by9"> + <audio class="embed-responsive-item" controls autoplay> + <source src="/{{ page.media }}"> + Your browser does not support the audio element. + </audio> + </div> {% endfor %} </div> - {% endif %} + {% endif %} -{% else %} + {% else %} - {% if session['type']=='text' %} + {% if session['type']=='text' %} <div class="container text-center mt-5 pt-5"> {% for page in pages.items %} - <h{{ stimulus_size_text }} class="text-center mt-5"><br>{{ randomized_stimulus.text }}</h{{ stimulus_size_text }}> + <h{{ stimulus_size_text }} class="text-center mt-5"><br>{{ randomized_stimulus.text }} + </h{{ stimulus_size_text }}> {% endfor %} </div> - <br><br> - {% endif %} - - {% if session['type']=='picture' %} - <div class="container stimulus col-{{stimulus_size}} mt-5 pt-5"> + <br><br> + {% endif %} + + {% if session['type']=='picture' %} + <div class="stimulus col-{{stimulus_size}} mt-5 pt-5"> {% for page in pages.items %} - <img src="/{{ randomized_stimulus.media }}" class="img-fluid"> + <img src="/{{ randomized_stimulus.media }}" class="img-fluid"> {% endfor %} + + </div> - {% endif %} - - {% if session['type']=='video' %} + {% endif %} + + {% if session['type']=='video' %} <div class="col-{{stimulus_size}} container stimulus mt-5 pt-5"> {% for page in pages.items %} - <div class="embed-responsive embed-responsive-16by9"> - <iframe class="embed-responsive-item" src="/{{ randomized_stimulus.media }}" allowfullscreen></iframe> - </div> + <div class="embed-responsive embed-responsive-16by9"> + <iframe class="embed-responsive-item" src="/{{ randomized_stimulus.media }}" allowfullscreen></iframe> + </div> {% endfor %} </div> - {% endif %} + {% endif %} - {% if session['type']=='audio' %} + {% if session['type']=='audio' %} <div class="col-{{stimulus_size}} container stimulus mt-5 pt-5"> {% for page in pages.items %} - <div class="embed-responsive embed-responsive-16by9"> - <iframe class="embed-responsive-item" src="/{{ randomized_stimulus.media }}" allowfullscreen></iframe> - </div> + <div class="embed-responsive embed-responsive-16by9"> + <audio class="embed-responsive-item" controls autoplay> + <source src="/{{ randomized_stimulus.media }}"> + Your browser does not support the audio element. + </audio> + </div> {% endfor %} </div> - {% endif %} + {% endif %} -{% endif %} - -<br> - - <h4 class="text-center">{{ rating_instruction }}</h4> + {% endif %} - <!-- Select form type --> + {% if form.__name__ == 'embody' %} + <div class="col-4 mt-5 pt-5"> + <div class="canvas-container"> + <span class="canvas-info"></span> + <canvas id="embody-canvas" class="crosshair" width="20" height="20"></canvas> + </div> + {% for embody_question in embody_questions %} + <img id="idembody-{{embody_question.idembody}}" + class="embody-image {% if loop.last %}last-embody{% endif %} {% if loop.index != 1 %}hidden{% else %}selected-embody{% endif %}" + src={{ embody_question.picture }} /> + {% endfor %} - {% if form.__name__ == 'embody' %} + {% for embody_question in embody_questions %} + <p id="idquestion-{{ embody_question.idembody }}" + class="embody-question {% if loop.index != 1 %}hidden{% else %}selected-embody{% endif %}"> + {{ embody_question.question }}</p> + {% endfor %} - <div class="canvas-container"> - <span class="canvas-info"></span> - <canvas id="embody-canvas" class="crosshair" width="20" height="20" style="border: 1px solid blue;" ></canvas> + <img id="baseImageMask" class="hidden" src={{ url_for('static', filename='img/dummy_600_mask.png') }} /> - </div> + </div> + {% endif %} + </div> +</div> - {% for embody_question in embody_questions %} - <img id="idembody-{{embody_question.idembody}}" class="embody-image {% if loop.last %}last-embody{% endif %} {% if loop.index != 1 %}hidden{% else %}selected-embody{% endif %}" src={{ embody_question.picture }} /> - {% endfor %} - {% for embody_question in embody_questions %} - <p id="idquestion-{{ embody_question.idembody }}" class="embody-question {% if loop.index != 1 %}hidden{% else %}selected-embody{% endif %}"> {{ embody_question.question }}</p> - {% endfor %} +<br> +<h4 class="text-center">{{ rating_instruction }}</h4> - <img id="baseImageMask" class="hidden" src={{ url_for('static', filename='img/dummy_600_mask.png') }} /> +<!-- Select form type --> +{% if form.__name__ == 'embody' %} - <form id="canvas-form" class="form-group mt-5" action="/task/embody/{{ page_num }}" method="post" > +<form id="canvas-form" class="form-group mt-5" action="/task/embody/{{ page_num }}" method="post"> <input id="canvas-data" type="hidden" value="" name="coordinates"> @@ -131,29 +150,29 @@ <p>{{ _("Reload the page if canvas didn't appear") }} </p> <p>{{ _('You can zoom in/out the page view by pressing ctrl+/ctrl- (Windows) or ⌘+/⌘- (Mac)') }} </p> </div> - </div> - </form> + </div> +</form> -<script src="{{ url_for('static', filename='js/canvas.js') }}" ></script> +<script src="{{ url_for('static', filename='js/canvas.js') }}"></script> {% elif form.__name__ == 'slider' %} - <form class="form-group mt-5" action="/task/question/{{ page_num }}" method="post"> +<form class="form-group mt-5" action="/task/question/{{ page_num }}" method="post"> {% for category in form.categories1 %} - {% for scale in form.categories1[category] %} - <div class="row form-group mt-0 mb-0"> - <h6 class="col-3 text-right mt-0 mb-0"> - {{ scale[0] }} - </h6> - <h6 class="col text-center mt-0 mb-0"> - <label for="customRange">{{ category[1] }}</label> - <input type="range" class="custom-range" id="customRange" name={{ category[0] }}> - </h6> - <h6 class="col-3 text-left mt-0 mb-0"> - {{ scale[1] }} - </h6> - </div> - {% endfor %} + {% for scale in form.categories1[category] %} + <div class="row form-group mt-0 mb-0"> + <h6 class="col-3 text-right mt-0 mb-0"> + {{ scale[0] }} + </h6> + <h6 class="col text-center mt-0 mb-0"> + <label for="customRange">{{ category[1] }}</label> + <input type="range" class="custom-range" id="customRange" name={{ category[0] }}> + </h6> + <h6 class="col-3 text-left mt-0 mb-0"> + {{ scale[1] }} + </h6> + </div> + {% endfor %} {% endfor %} <div class="form-row text-center"> <div class="col-12"> @@ -164,9 +183,9 @@ <br> <p>{{ _('You can zoom in/out the page view by pressing ctrl+/ctrl- (Windows) or ⌘+/⌘- (Mac)') }} </p> </div> - </div> - </form> + </div> +</form> {% endif %} - + {% endblock %} \ No newline at end of file diff --git a/app/task/views.py b/app/task/views.py index e72588f910ab45bce172f12aa3904bfcb8d7bfb8..6e3a2085c9913f48963d4b4fa4b5e10eec24e036 100644 --- a/app/task/views.py +++ b/app/task/views.py @@ -37,13 +37,7 @@ task_blueprint = Blueprint("task", __name__, def get_randomized_page(page_id): """if trial randomization is on we will still use the same functionality that is used otherwise but we will pass the randomized pair of the page_id from trial randomization table to the task.html""" - - randomized_page = trial_randomization.query.filter(and_( - trial_randomization.answer_set_idanswer_set==session['answer_set'], - trial_randomization.page_idpage==page_id - )).first() - - return randomized_page + return trial_randomization.get_randomized_page(page_id) def add_slider_answer(key, value, page_id=None): @@ -269,7 +263,6 @@ def task(page_num): @task_blueprint.route('/completed') def completed(): - session.pop('user', None) session.pop('exp_id', None) session.pop('agree', None) diff --git a/app/utils.py b/app/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..a06c23ceda44af401265805c3b9ac94b0600ba34 --- /dev/null +++ b/app/utils.py @@ -0,0 +1,306 @@ +import os +import tempfile +import time +import json +from itertools import zip_longest +import concurrent.futures +from flask import send_file +from flask_socketio import emit +from app import app +from app.models import background_question, background_question_answer, \ + page, question, answer_set, answer, embody_answer, embody_question + + +def timeit(method): + def timed(*args, **kw): + ts = time.time() + result = method(*args, **kw) + te = time.time() + if 'log_time' in kw: + name = kw.get('log_name', method.__name__.upper()) + kw['log_time'][name] = int((te - ts) * 1000) + else: + app.logger.info('{} {:2.2f} ms'.format(method.__name__, (te - ts) * 1000)) + return result + + return timed + + +def map_values_to_int(values: dict): + #values = [map(int, i) for i in list(values.values())] + return zip_longest(*values.values(), fillvalue=None) + + +def calculate_mean(values: list) -> float: + n_answers = sum(x is not None for x in values) + sum_of_answers = float(sum(filter(None, values))) + mean = sum_of_answers / n_answers + return round(mean, 2) + + +def get_mean_from_slider_answers(answers): + return [calculate_mean(values) for values in map_values_to_int(answers)] + + +def saved_data_as_file(filename, data): + """write CSV data to temporary file on host and send that file + to requestor""" + try: + fd, path = tempfile.mkstemp() + with os.fdopen(fd, 'w') as tmp: + tmp.write(data) + tmp.flush() + return send_file(path, + mimetype='text/csv', + as_attachment=True, + attachment_filename=filename) + finally: + os.remove(path) + + +def get_values_from_list_of_answers(page_question, answers): + page_id = page_question[0] + question_id = page_question[1] + for _answer in answers: + try: + if _answer.question_idquestion == question_id and \ + _answer.page_idpage == page_id: + return int(_answer.answer) + except AttributeError: + if _answer.embody_question_idembody == question_id and \ + _answer.page_idpage == page_id: + return _answer + return None + + +def question_matches_answer(question, answer): + if (answer.page_idpage == question[0] and answer.question() == question[1]): + return True + return False + + +def map_answers_to_questions(answers, questions): + ''' + questions = [(4, 1), (4, 2), (5, 1), (5, 2), (6, 1), (6, 2)] + + + answers = [{p:6, q:1, a:100}, {p:6, q:2, a:99}] + -> + partial_answer = [None, None, None, None, 100, 99] + ''' + + # results = [] + results = list(map(lambda x: None, questions)) + + nth_answer = 0 + + for nth_question, question in enumerate(questions): + + try: + current_answer = answers[nth_answer] + except IndexError: + break + + if question_matches_answer(question, current_answer): + results[nth_question] = current_answer.result() + nth_answer += 1 + + return results + + ''' + return list(map( + lambda x: get_values_from_list_of_answers(x, answers), + questions)) + ''' + + +''' +select sub.answer_set_idanswer_set, group_concat(concat( + COALESCE(sub.aa, ''), + COALESCE(sub.ab, ''), + COALESCE(sub.ba, ''), + COALESCE(sub.bb, ''), + COALESCE(sub.ca, ''), + COALESCE(sub.cb, '') +)) +FROM ( + select *, + case when page_idpage = 4 and question_idquestion = 1 then answer end as aa, + case when page_idpage = 4 and question_idquestion = 2 then answer end as ab, + case when page_idpage = 5 and question_idquestion = 1 then answer end as ba, + case when page_idpage = 5 and question_idquestion = 2 then answer end as bb, + case when page_idpage = 6 and question_idquestion = 1 then answer end as ca, + case when page_idpage = 6 and question_idquestion = 2 then answer end as cb + from answer where answer_set_idanswer_set in ( select idanswer_set from answer_set where experiment_idexperiment = 2 and answer_counter != 0 ) +) as sub +group by sub.answer_set_idanswer_set; + + + + +# all possible page/question comobs +select distinct p.idpage, q.idquestion from question q join page p on p.experiment_idexperiment=q.experiment_idexperiment where p.experiment_idexperiment = 2 order by p.idpage,q.idquestion; +''' + + + +@timeit +def generate_csv(exp_id): + + # answer sets with participant ids + participants = answer_set.query.filter_by( + experiment_idexperiment=exp_id).all() + + # pages aka stimulants + pages = page.query.filter_by(experiment_idexperiment=exp_id).all() + + # background questions + bg_questions = background_question.query.filter_by( + experiment_idexperiment=exp_id).all() + + # question + questions = question.query.filter_by(experiment_idexperiment=exp_id).all() + + # embody questions + embody_questions = embody_question.query.filter_by( + experiment_idexperiment=exp_id).all() + + csv = '' + + # create CSV-header + header = 'participant id;' + header += ';'.join([str(count) + '. bg_question: ' + q.background_question.strip() + for count, q in enumerate(bg_questions, 1)]) + + for idx in range(1, len(pages) + 1): + if len(questions) > 0: + header += ';' + ';'.join(['page' + str(idx) + '_' + str(count) + '. slider_question: ' + + question.question.strip() for count, question in enumerate(questions, 1)]) + + for idx in range(1, len(pages) + 1): + if len(embody_questions) > 0: + header += ';' + ';'.join(['page' + str(idx) + '_' + str(count) + '. embody_question: ' + + question.picture.strip() for count, question in enumerate(embody_questions, 1)]) + + csv += header + '\r\n' + + # filter empty answer_sets + participants = list(filter(lambda participant: True if int( + participant.answer_counter) > 0 else False, participants)) + + len_participants = len(participants) + + # We can use a with statement to ensure threads are cleaned up promptly + with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: + # Start the load operations and mark each future with its URL + future_to_answer = { + executor.submit(generate_answer_row, participant, pages, questions, embody_questions): participant + for participant in participants} + + for nth, future in enumerate(concurrent.futures.as_completed(future_to_answer)): + # for testing purpose + # answer_row = future_to_answer[future] + try: + emit('progress', {'done': nth, 'from': len_participants}) + data = future.result() + csv += data + '\r\n' + except Exception as exc: + print('generated an exception: {}'.format(exc)) + return exc + + return csv + + +def generate_answer_row(participant, pages, questions, embody_questions): + # TODO: refactor + + with app.app_context(): + + answer_row = '' + + # append user session id + answer_row += participant.session + ';' + + # append background question answers + bg_answers = background_question_answer.query.filter_by( + answer_set_idanswer_set=participant.idanswer_set).all() + bg_answers_list = [str(a.answer).strip() for a in bg_answers] + answer_row += ';'.join(bg_answers_list) + ';' + + # append slider answers + slider_answers = answer.query.filter_by( + answer_set_idanswer_set=participant.idanswer_set) \ + .order_by(answer.page_idpage, answer.question_idquestion) \ + .all() + + pages_and_questions = {} + + for p in pages: + questions_list = [(p.idpage, a.idquestion) for a in questions] + pages_and_questions[p.idpage] = questions_list + + _questions = [ + item for sublist in pages_and_questions.values() for item in sublist] + + answers_list = map_answers_to_questions(slider_answers, _questions) + + # typecast elemnts to string + answers_list = [str(a).strip() for a in answers_list] + + answer_row += ';'.join(answers_list) + \ + ';' if slider_answers else len( + questions) * len(pages) * ';' + + # append embody answers (coordinates) + # save embody answers as bitmap images + embody_answers = embody_answer.query.filter_by( + answer_set_idanswer_set=participant.idanswer_set) \ + .order_by(embody_answer.page_idpage) \ + .all() + + pages_and_questions = {} + + for p in pages: + questions_list = [(p.idpage, a.idembody) for a in embody_questions] + pages_and_questions[p.idpage] = questions_list + + _questions = [ + item for sublist in pages_and_questions.values() for item in sublist] + + _embody_answers = map_answers_to_questions(embody_answers, _questions) + + answers_list = [] + + for answer_data in _embody_answers: + if not answer_data: + answers_list.append('') + continue + + try: + coordinates = json.loads(answer_data) + em_height = coordinates.get('height', 600) + 2 + em_width = coordinates.get('width', 200) + 2 + + coordinates_to_bitmap = [ + [0 for x in range(em_height)] for y in range(em_width)] + + coordinates = list( + zip(coordinates.get('x'), coordinates.get('y'))) + + for point in coordinates: + + try: + # for every brush stroke, increment the pixel + # value for every brush stroke + coordinates_to_bitmap[int(point[0])][int(point[1])] += 0.1 + except IndexError: + continue + + answers_list.append(json.dumps(coordinates_to_bitmap)) + + except ValueError as err: + app.logger(err) + + answer_row += ';'.join(answers_list) if embody_answers else \ + len(embody_questions) * len(pages) * ';' + + return answer_row diff --git a/config.py b/config.py index 26d0769b571c8e7f552ff5588aef47003fa91695..632122a826069dbc4e2203de761d258a5c66a275 100644 --- a/config.py +++ b/config.py @@ -1,11 +1,12 @@ +from decouple import config import os basedir = os.path.abspath(os.path.dirname(__file__)) -class Config(object): - #seret key is set in __ini__.py - #SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess' +class Config(object): + # seret key is set in __ini__.py + #SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess' LANGUAGES = ['en', 'fi', 'fa', 'el', 'it', 'zh'] @@ -17,23 +18,25 @@ class Config(object): SQLALCHEMY_TRACK_MODIFICATIONS = False """ - #MariaDB mysql database settings + # MariaDB mysql database settings + + MYSQL_USER = config('MYSQL_USER') + MYSQL_PASSWORD = config('MYSQL_PASSWORD') + MYSQL_SERVER = config('MYSQL_SERVER') + MYSQL_DB = config('MYSQL_DB') + + SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://'+MYSQL_USER+':' + \ + MYSQL_PASSWORD+'@'+MYSQL_SERVER+'/'+MYSQL_DB+'?charset=utf8mb4' - MYSQL_USER = 'rating' - MYSQL_PASSWORD = 'rating_passwd' - MYSQL_SERVER = 'localhost' - MYSQL_DB = 'rating_db' - - SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://'+MYSQL_USER+':'+MYSQL_PASSWORD+'@'+MYSQL_SERVER+'/'+MYSQL_DB+'?charset=utf8mb4' - SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_ENGINE_OPTIONS = { - "pool_pre_ping": True, - "pool_recycle": 300, - "max_overflow": 30, - "pool_size": 20 + "pool_pre_ping": True, + "pool_recycle": 60, + "max_overflow": 30, + "pool_size": 20 } - + TEMPLATES_AUTO_RELOAD = True + DEBUG = False diff --git a/embody_plot.py b/embody_plot.py index e72e0656b25cbd656f05ff2366c5aa54ea5eca62..f540d77fafee5a5fbdf446d50d2732bc08b4be4d 100644 --- a/embody_plot.py +++ b/embody_plot.py @@ -119,13 +119,13 @@ def timeit(method): return timed -import sys @timeit def get_coordinates(idpage, idembody=None, select_clause=SELECT_BY_PAGE_AND_PICTURE): """Select all drawn points from certain stimulus and plot them onto the human body""" + # init db db = MyDB() db.query(select_clause, (idpage,idembody)) @@ -138,12 +138,13 @@ def get_coordinates(idpage, idembody=None, select_clause=SELECT_BY_PAGE_AND_PICT image_path = db._db_cur.fetchone()[0] image_path = './app' + image_path - # Draw image plt = plot_coordinates(coordinates, image_path) else: plt = plot_coordinates(coordinates, DEFAULT_IMAGE_PATH) + # close db connection + db.__del__() # Save image to ./app/static/ img_filename = 'PAGE-' + str(idpage) + '-' + DATE_STRING + '.png' @@ -187,6 +188,7 @@ def plot_coordinates(coordinates, image_path=DEFAULT_IMAGE_PATH): # Total amount of points points_count = len(coordinates['coordinates']) + step = 1 # Load image to a plot image = mpimg.imread(image_path) @@ -209,21 +211,26 @@ def plot_coordinates(coordinates, image_path=DEFAULT_IMAGE_PATH): for idx, point in enumerate(coordinates["coordinates"]): try: - frame[int(point[1]), int(point[0])] = 1 + frame[int(point[1]), int(point[0])] = 1 except IndexError as err: - app.logger.info(err) - - point = ndimage.gaussian_filter(frame, sigma=5) - ax2.imshow(point, cmap='hot', interpolation='none') + app.logger.info(err) # Try to send progress information to socket.io - try: - emit('progress', {'done':idx+1/points_count, 'from':points_count}) - socketio.sleep(0) - except RuntimeError as err: - print(err) + if idx == 0: continue + if round((idx / points_count) * 100) % (step * 5) == 0: + try: + emit('progress', {'done': step * 5, 'from': 100}) + socketio.sleep(0.05) + except RuntimeError: + continue + + step += 1 + + point = ndimage.gaussian_filter(frame, sigma=5) + ax2.imshow(point, cmap='hot', interpolation='none') + image_mask = mpimg.imread(IMAGE_PATH_MASK) ax2.imshow(image_mask) @@ -238,7 +245,7 @@ def plot_coordinates(coordinates, image_path=DEFAULT_IMAGE_PATH): ax1.plot(coordinates["x"],coordinates["y"], 'ro', alpha=0.2) ax1.imshow(image, alpha=0.6) - app.logger.info("iamge plotted") + app.logger.info("image plotted") # return figure for saving/etc... return fig diff --git a/plot_image.py b/plot_image.py new file mode 100644 index 0000000000000000000000000000000000000000..bc723397bcdf5b66a8bd2b2adb96da1cdbd9c2a9 --- /dev/null +++ b/plot_image.py @@ -0,0 +1,120 @@ +''' +Script for testing embody drawing results from onni.utu.fi + +Install requirements: +pip install numpy +pip install matplotlib + +Usage: +Export data from onni.utu.fi and after that run the script in the same folder +by passing exported_file as a parameter to the script + +$ python plot_image.py <exported_file>.csv + +Program prints header of the file, from which you must select column where the +image data is. After you have selected the right column, program prints the +drawing results from embody answers. + +If you want the program to draw default embody image to the background, then +you must put a copy of the 'dummy_6000.png' -file (this is the same that is used +in onni.utu.fu) to the same path as the script. + +''' + +import copy + +import numpy as np +import matplotlib.pyplot as plt +import csv +import sys +import json + +csv.field_size_limit(sys.maxsize) + + +def show_images(images, cols=1, titles=None): + """Display a list of images in a single figure with matplotlib. + Parameters + --------- + images: List of np.arrays compatible with plt.imshow. + + cols (Default = 1): Number of columns in figure (number of rows is + set to np.ceil(n_images/float(cols))). + + titles: List of titles corresponding to each image. Must have + the same length as titles. + """ + + # default embody image for the background + + try: + background = True + default_img = plt.imread("./dummy_600.png") + except FileNotFoundError: + background = False + + # get a copy of the gray color map + my_cmap = copy.copy(plt.cm.get_cmap('gray')) + # set how the colormap handles 'bad' values + my_cmap.set_bad(alpha=0) + + + assert((titles is None) or (len(images) == len(titles))) + n_images = len(images) + if titles is None: + titles = ['Image (%d)' % i for i in range(1, n_images + 1)] + fig = plt.figure() + for n, (image, title) in enumerate(zip(images, titles)): + + a = fig.add_subplot(cols, np.ceil(n_images/float(cols)), n + 1) + + # draw points from users answers + plt.imshow(image, cmap=my_cmap) + + # draw default background image with transparency on top of the points + if background: + plt.imshow(default_img, extent=[0, 200, 600, 0], alpha=0.33) + + a.set_title(title) + + fig.set_size_inches(np.array(fig.get_size_inches()) * n_images) + plt.show() + + +if __name__ == '__main__': + + images = [] + titles = [] + + # filename = 'experiment_1_2020-05-20.csv' + filename = sys.argv[1] + + with open(filename, 'r+') as csvfile: + for row_no, row in enumerate(csv.reader(csvfile, delimiter=';')): + + # parse header + if row_no == 0: + for column, title in enumerate(row): + print("Column (no. {}): {}".format(column, title)) + + print('Enter the column number which has image data:') + x = int(input()) + continue + + try: + np_array = np.array(eval(row[x])) + except NameError: + print( + "Column didn't contain image data. Try again with different column number.") + except SyntaxError: + continue + except IndexError: + continue + + np_array = np.transpose(np_array) + images.append(np_array) + + # add id of the answerer to the image + titles.append(row[0]) + + show_images(images, titles=titles) diff --git a/requirements.txt b/requirements.txt index 9e433b6c68e2ba465520156fdc3537bdd72ae72e..9c4419e6189f07406c5f3dfa1d9dee67b492a6b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,8 @@ Flask-Cors==3.0.7 Flask-Login==0.4.1 Flask-Migrate==2.2.1 Flask-Session==0.3.1 -Flask-SocketIO==3.3.2 +Flask-SocketIO==4.3.0 +# Flask-SocketIO==3.3.2 Flask-SQLAlchemy==2.3.2 Flask-Uploads==0.2.1 Flask-WTF==0.14.2 @@ -17,11 +18,19 @@ mysql-connector==2.2.9 networkx==2.2 numpy==1.16.2 PyMySQL==0.9.3 -python-engineio==3.5.1 -python-socketio==3.1.2 +pyparsing==2.3.1 +python-dateutil==2.7.3 +python-editor==1.0.3 +python-engineio==3.13.0 +# python-engineio==3.5.1 +# python-socketio==3.1.2 +python-socketio==4.6.0 +pytz==2018.7 +rope==0.12.0 scipy==1.2.1 SQLAlchemy==1.2.8 uuid==1.30 Werkzeug==0.14.1 WTForms==2.2.1 -WTForms-SQLAlchemy==0.1 \ No newline at end of file +WTForms-SQLAlchemy==0.1 +python-decouple