From e1cbc7e6bc22688dc2c7f556f2289ba7576291f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20Heikkil=C3=A4?= <timo.t.heikkila@utu.fi> Date: Mon, 5 Nov 2018 14:01:43 +0200 Subject: [PATCH] add initial commit --- .gitignore | 10 + app/__init__.py | 34 + app/forms.py | 154 ++ app/messages.pot | 19 + app/models.py | 171 ++ app/rating_tool.py | 45 + app/routes.py | 1471 +++++++++++++++++ app/static/css/custom.css | 7 + app/static/css/main.css | 28 + app/static/css/style.css | 1455 ++++++++++++++++ app/static/img/madam-300x250.jpg | Bin 0 -> 9456 bytes app/templates/_formhelpers.html | 12 + app/templates/add_bg_question.html | 32 + app/templates/add_questions.html | 36 + app/templates/add_stimuli.html | 31 + app/templates/base.html | 89 + app/templates/begin_with_id.html | 29 + app/templates/consent.html | 40 + app/templates/continue_task.html | 24 + app/templates/create_experiment.html | 104 ++ .../create_experiment_bgquestions.html | 31 + .../create_experiment_questions.html | 35 + .../create_experiment_upload_stimuli.html | 78 + app/templates/edit_bg_question.html | 19 + app/templates/edit_experiment.html | 36 + app/templates/edit_question.html | 34 + app/templates/edit_stimuli.html | 31 + app/templates/experiment_statistics.html | 110 ++ app/templates/index.html | 91 + app/templates/instructions.html | 25 + app/templates/quit_task.html | 16 + app/templates/register.html | 29 + app/templates/remove_experiment.html | 26 + app/templates/researcher_info.html | 19 + app/templates/researcher_login.html | 27 + app/templates/task.html | 136 ++ app/templates/task_completed.html | 19 + app/templates/view_experiment.html | 314 ++++ app/translations/fin/LC_MESSAGES/messages.mo | Bin 0 -> 484 bytes app/translations/fin/LC_MESSAGES/messages.po | 24 + babel.cfg | 3 + config.py | 11 + dump.sql | 95 ++ messages.pot | 23 + requirements.txt | 24 + 45 files changed, 5047 insertions(+) create mode 100644 .gitignore create mode 100644 app/__init__.py create mode 100644 app/forms.py create mode 100644 app/messages.pot create mode 100644 app/models.py create mode 100644 app/rating_tool.py create mode 100644 app/routes.py create mode 100644 app/static/css/custom.css create mode 100644 app/static/css/main.css create mode 100644 app/static/css/style.css create mode 100644 app/static/img/madam-300x250.jpg create mode 100644 app/templates/_formhelpers.html create mode 100644 app/templates/add_bg_question.html create mode 100644 app/templates/add_questions.html create mode 100644 app/templates/add_stimuli.html create mode 100644 app/templates/base.html create mode 100644 app/templates/begin_with_id.html create mode 100644 app/templates/consent.html create mode 100644 app/templates/continue_task.html create mode 100644 app/templates/create_experiment.html create mode 100644 app/templates/create_experiment_bgquestions.html create mode 100644 app/templates/create_experiment_questions.html create mode 100644 app/templates/create_experiment_upload_stimuli.html create mode 100644 app/templates/edit_bg_question.html create mode 100644 app/templates/edit_experiment.html create mode 100644 app/templates/edit_question.html create mode 100644 app/templates/edit_stimuli.html create mode 100644 app/templates/experiment_statistics.html create mode 100644 app/templates/index.html create mode 100644 app/templates/instructions.html create mode 100644 app/templates/quit_task.html create mode 100644 app/templates/register.html create mode 100644 app/templates/remove_experiment.html create mode 100644 app/templates/researcher_info.html create mode 100644 app/templates/researcher_login.html create mode 100644 app/templates/task.html create mode 100644 app/templates/task_completed.html create mode 100644 app/templates/view_experiment.html create mode 100644 app/translations/fin/LC_MESSAGES/messages.mo create mode 100644 app/translations/fin/LC_MESSAGES/messages.po create mode 100644 babel.cfg create mode 100644 config.py create mode 100644 dump.sql create mode 100644 messages.pot create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2e28ad5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +__pycache__/ +/migrations +/venv +app.db +cmd.txt +DB_inserts.txt +dumb.sql +scrap_script.py +*.pyc + diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..b403534 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,34 @@ +from flask import Flask +from flask_bootstrap import Bootstrap +from config import Config +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate +from flask_login import LoginManager +from flask_babel import Babel + + +app = Flask(__name__) +app.config['BABEL_DEFAULT_LOCALE'] = 'en' +babel = Babel(app) + + +@babel.localeselector +def get_locale(): + + return 'fin' + +#mariabd mysql portti 3306 tarkista? + +Bootstrap(app) +app.config.from_object(Config) +db = SQLAlchemy(app) +migrate = Migrate(app, db) +login = LoginManager(app) +login.login_view = 'login' + + +app.secret_key = 'random string' +"""app.secret_key = os.urandom(24)""" + + +from app import routes, models diff --git a/app/forms.py b/app/forms.py new file mode 100644 index 0000000..cbfc28b --- /dev/null +++ b/app/forms.py @@ -0,0 +1,154 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, PasswordField, BooleanField, SubmitField, SelectField +from wtforms.validators import DataRequired, InputRequired +from wtforms_sqlalchemy.fields import QuerySelectField +from flask_bootstrap import Bootstrap +from app.models import background_question +from wtforms import Form, TextField, TextAreaField, SubmitField, FieldList, FormField +from wtforms import Form, BooleanField, StringField, PasswordField, validators, RadioField +from flask_wtf.file import FileField, FileAllowed, FileRequired + + +class LoginForm(FlaskForm): + username = StringField('Username', validators=[DataRequired()]) + password = PasswordField('Password', validators=[DataRequired()]) + remember_me = BooleanField('Remember Me') + submit = SubmitField('Sign In') + + +class RegisterForm(Form): + + questions1 = FieldList(SelectField([validators.InputRequired()])) + submit = SubmitField("Send") + + +class TaskForm(Form): + + categories1 = FieldList(SelectField([validators.InputRequired()])) + submit = SubmitField("Send") + + +class ContinueTaskForm(FlaskForm): + + participant_id = StringField('participant_id', validators=[DataRequired()]) + submit = SubmitField('Continue rating') + + +class StartWithIdForm(FlaskForm): + + participant_id = StringField('participant_id', validators=[DataRequired()]) + submit = SubmitField('Start rating') + + +class Questions(FlaskForm): + + questions = StringField() + + +class Answers(FlaskForm): + + background_question_idbackground_question = SelectField(coerce=int, validators=[InputRequired]) + + +class BackgroundQuestionForm(Form): + + idbackground_question_option = StringField() + background_question_idbackground_question = StringField() + option = StringField() + submit = SubmitField('Register') + + +class TestForm(Form): + + question_name = StringField() + options1 = SelectField() + + +class TestForm1(Form): + + questions1 = FieldList(SelectField([validators.InputRequired()])) + submit = SubmitField("Send") + + +class TestForm2(Form): + + questions1 = SelectField() + +#Forms for editing functions + + +class CreateExperimentForm(Form): + + name = StringField('Name', [validators.DataRequired()]) + instruction = StringField('Instruction', [validators.DataRequired()]) + language = StringField('Language', [validators.DataRequired()]) + submit = SubmitField('Send') + + +class EditExperimentForm(Form): + + name = StringField('Name', [validators.DataRequired()]) + instruction = StringField('Instruction', [validators.DataRequired()]) + language = SelectField('Language', choices=[ + ('Afrikanns', 'Afrikanns'), ('Albanian', 'Albanian'), ('Arabic', 'Arabic'), ('Armenian', 'Armenian'), ('Basque', 'Basque'), ('Bengali', 'Bengali'), ('Bulgarian', 'Bulgarian'), + ('Catalan', 'Catalan'), ('Cambodian', 'Cambodian'), ('Chinese (Mandarin)', 'Chinese (Mandarin)'), ('Croation', 'Croation'), ('Czech', 'Czech'), ('Danish', 'Danish'), + ('Dutch', 'Dutch'), ('English', 'English'), ('Estonian', 'Estonian'), ('Fiji', 'Fiji'), ('Finnish', 'Finnish'), ('French', 'French'), ('Georgian', 'Georgian'), + ('German', 'German'), ('Greek', 'Greek'), ('Gujarati', 'Gujarati'), ('Hebrew', 'Hebrew'), ('Hindi', 'Hindi'), ('Hungarian', 'Hungarian'), ('Icelandic', 'Icelandic'), + ('Indonesian', 'Indonesian'), ('Irish', 'Irish'), ('Italian', 'Italian'), ('Japanese', 'Japanese'), ('Javanese', 'Javanese'), ('Korean', 'Korean'), ('Latin', 'Latin'), + ('Latvian', 'Latvian'), ('Lithuanian', 'Lithuanian'), ('Macedonian', 'Macedonian'), ('Malay', 'Malay'), ('Malayalam', 'Malayalam'), ('Maltese', 'Maltese'), ('Maori', 'Maori'), + ('Marathi', 'Marathi'), ('Mongolian', 'Mongolian'), ('Nepali', 'Nepali'), ('Norwegian', 'Norwegian'), ('Persian', 'Persian'), ('Polish', 'Polish'), ('Portuguese', 'Portuguese'), + ('Punjabi', 'Punjabi'), ('Quechua', 'Quechua'), ('Romanian', 'Romanian'), ('Russian', 'Russian'), ('Samoan', 'Samoan'), ('Serbian', 'Serbian'), ('Slovak', 'Slovak'), + ('Slovenian', 'Slovenian'), ('Spanish', 'Spanish'), ('Swahili', 'Swahili'), ('Swedish ', 'Swedish '), ('Tamil', 'Tamil'), ('Tatar', 'Tatar'), ('Telugu', 'Telugu'), + ('Thai', 'Thai'), ('Tibetan', 'Tibetan'), ('Tonga', 'Tonga'), ('Turkish', 'Turkish'), ('Ukranian', 'Ukranian'), ('Urdu', 'Urdu'), ('Uzbek', 'Uzbek'), ('Vietnamese', 'Vietnamese'), + ('Welsh', 'Welsh'), ('Xhosa', 'Xhosa')]) + submit = SubmitField('Send') + + +class CreateBackgroundQuestionForm(Form): + + bg_questions_and_options = TextAreaField('Background questions and options', [validators.DataRequired()]) + submit = SubmitField('Send') + + +class EditBackgroundQuestionForm(Form): + + bg_questions_and_options = TextAreaField('Background questions and options') + new_values = TextAreaField('New values', [validators.DataRequired()]) + submit = SubmitField('Send') + + +class CreateQuestionForm(Form): + + questions_and_options = TextAreaField('Questions and options', [validators.DataRequired()]) + submit = SubmitField('Send') + + +class EditQuestionForm(Form): + + left = StringField('left_scale', [validators.DataRequired()]) + right = StringField('right_scale', [validators.DataRequired()]) + question = StringField('question', [validators.DataRequired()]) + + +class UploadStimuliForm(Form): + + type = RadioField('type', choices=[('text', 'text'), ('picture', 'picture'), ('video', 'video'), ('audio', 'audio')]) + text = TextAreaField('Text stimulus') + media = TextAreaField('Media filename') + file = FileField('Upload file') + submit = SubmitField('Send') + + +class EditPageForm(Form): + + type = RadioField('type', choices=[('text', 'text'), ('picture', 'picture'), ('video', 'video'), ('audio', 'audio')]) + text = TextAreaField('Text stimulus') + media = TextAreaField('Media filename') + file = FileField('Upload file') + submit = SubmitField('Send') + + +class RemoveExperimentForm(Form): + remove = TextAreaField('Remove') + submit = SubmitField('Send') + diff --git a/app/messages.pot b/app/messages.pot new file mode 100644 index 0000000..bcd5f63 --- /dev/null +++ b/app/messages.pot @@ -0,0 +1,19 @@ +# Translations template for PROJECT. +# Copyright (C) 2018 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2018. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2018-11-04 17:30+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.6.0\n" + diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..6115937 --- /dev/null +++ b/app/models.py @@ -0,0 +1,171 @@ +from app import db +from sqlalchemy import Column, Integer, String +from flask_sqlalchemy import SQLAlchemy +from flask_wtf import FlaskForm +from wtforms_sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField +from flask_bootstrap import Bootstrap +from werkzeug.security import generate_password_hash, check_password_hash +from flask_login import UserMixin +from app import login + + +"""DATABASE CLASSES""" + + +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') + experiment_idexperiment = db.Column(db.Integer) + + def __repr__(self): + return "<idbackground_question = '%s', background_question = '%s'>" % (self.idbackground_question, self.background_question) + + +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')) + 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) + + +class experiment (db.Model): + __tablename__ = "experiment" + idexperiment = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(120), index=True) + instruction = db.Column(db.String(120), index=True) + directoryname = db.Column(db.String(120), index=True, unique=True) + language = db.Column(db.String(120)) + status = db.Column(db.String(120)) + randomization = db.Column(db.String(120)) + + def __repr__(self): + return "<idexperiment = '%s', name='%s', instruction='%s', directoryname='%s', language='%s', status='%s', randomization='%s'>" % (self.idexperiment, self.name, self.instruction, self.directoryname, self.language, self.status, self.randomization) + + +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')) + session = db.Column(db.String(120)) + agreement = db.Column(db.String(120)) + answer_counter = db.Column(db.Integer) + + def __repr__(self): + return "<idanswer_set = '%s', experiment_idexperiment = '%s', session = '%s', agreement = '%s', answer_counter = '%s'>" % (self.idanswer_set, self.experiment_idexperiment, self.session, self.agreement, self.answer_counter) + + +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 = db.Column(db.String(120)) + 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) + """ + + def __repr__(self): + return '<answer {}>'.format(self.answer) + + """ + + +def background_question_answer_query(): + return background_question_answer.query + +""" +class ChoiceForm(FlaskForm): + opts = QuerySelectField(query_factory=background_question_answer_query, allow_blank=True) +""" + + +""" +u = background_question.query.get(1) +vastaukset = u.answers.all() +## pitää sisällään kysymyksen 1 vastaukset +""" + + +class question (db.Model): + __tablename__ = "question" + idquestion = db.Column(db.Integer, primary_key=True) + 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) + + +class page (db.Model): + __tablename__ = "page" + idpage = db.Column(db.Integer, primary_key=True) + experiment_idexperiment = db.Column(db.Integer, db.ForeignKey('experiment.idexperiment')) + type = db.Column(db.String(120), index=True) + text = db.Column(db.String(120), index=True) + media = db.Column(db.String(120), index=True) + """ + def __repr__(self): + return "<idpage = '%s', experiment_idexperiment = '%s', type = '%s', text = '%s', media = '%s'>" % (self.idpage, self.experiment_idexperiment, self.type, self.text, self.media) + + def __repr__(self): + return '{}'.format(self.text) + """ + def __repr__(self): + return "<idpage = '%s', experiment_idexperiment = '%s', type = '%s', text = '%s', media = '%s'>" % (self.idpage, self.experiment_idexperiment, self.type, self.text, self.media) + + +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')) + answer = db.Column(db.String(120)) + page_idpage = db.Column(db.Integer, db.ForeignKey('page.idpage')) + + 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) + + +class trial_randomization (db.Model): + __tablename__ = "trial_randomization" + idtrial_randomization = db.Column(db.Integer, primary_key=True) + page_idpage = db.Column(db.Integer) + 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) + + +class user(UserMixin, db.Model): + __tablename__ = "user" + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(64), index=True, unique=True) + email = db.Column(db.String(120), index=True, unique=True) + password_hash = db.Column(db.String(128)) + + def __repr__(self): + 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/rating_tool.py b/app/rating_tool.py new file mode 100644 index 0000000..5223f6e --- /dev/null +++ b/app/rating_tool.py @@ -0,0 +1,45 @@ +from app import app, db, babel +from app.models import background_question, background_question_option +from app.models import experiment +from app.models import answer_set +from app.models import background_question_answer +from app.models import question, page, answer +from app.models import user, trial_randomization + + + +@app.shell_context_processor +def make_shell_context(): + return {'db': db, + 'background_question': background_question, + 'background_question_option': background_question_option, + 'experiment': experiment, + 'answer_set': answer_set, + 'background_question_answer': background_question_answer, + 'question': question, + 'page': page, + 'answer': answer, + 'user': user, + 'trial_randomization': trial_randomization + } + + +""" + +from app import app, db +from app.models import Background_question, Background_question_answer +from app.models import Experiment, Question, Answer_set +from app.models import Page, Answer + +@app.shell_context_processor +def make_shell_context(): + return {'db': db, + 'Background_question': Background_question, + 'Background_question_answer': Background_question_answer, + 'Experiment': Experiment, + 'Question': Question, + 'Answer_set': Answer_set, + 'Page': Page, + 'Answer': Answer, + } +""" \ No newline at end of file diff --git a/app/routes.py b/app/routes.py new file mode 100644 index 0000000..b29fcd3 --- /dev/null +++ b/app/routes.py @@ -0,0 +1,1471 @@ +from app import app, db +from flask import render_template, request +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 +from flask import session +from app.forms import LoginForm, RegisterForm +from flask import flash, redirect +from flask import url_for +from wtforms_sqlalchemy.fields import QuerySelectField +from app.forms import Answers, Questions +from flask_bootstrap import Bootstrap +from flask_sqlalchemy import SQLAlchemy, BaseQuery +from app.forms import BackgroundQuestionForm +from wtforms import Form, TextField, TextAreaField, validators, StringField, SubmitField +from app.forms import TestForm, TestForm1, TestForm2, TaskForm +from collections import OrderedDict +from sqlalchemy import func, desc +from app.forms import ContinueTaskForm +from sqlalchemy import and_ +from app.models import user, trial_randomization +from flask_login import current_user, login_user +from flask_login import logout_user +from flask_login import login_required +from sqlalchemy import update +from app.forms import StartWithIdForm +import secrets +from app.forms import CreateExperimentForm, CreateBackgroundQuestionForm, CreateQuestionForm, UploadStimuliForm, EditBackgroundQuestionForm, EditQuestionForm, EditExperimentForm +from app.forms import EditPageForm, RemoveExperimentForm +import os +import random +from flask import Flask, make_response +import pyexcel as pe +import io +from io import BytesIO +import csv +from flask import send_file +from flask import make_response +from flask_babel import Babel +from app import babel + + + +#Stimuli upload folder setting +APP_ROOT = os.path.dirname(os.path.abspath(__file__)) + + + + + + + + + +@app.route('/') +@app.route('/index') +def index(): + + experiments = experiment.query.all() + return render_template('index.html', title='Home', experiments=experiments) + + +@app.route('/consent') +def consent(): + exp_id = request.args.get('exp_id', None) + experiments = experiment.query.all() + return render_template('consent.html', exp_id=exp_id, experiments=experiments) + + +@app.route('/session') +def participant_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 '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'] + + random_id = secrets.token_hex(3) + check_id = answer_set.query.filter_by(session=random_id).first() + + while check_id is not None: + + #flash("ID already existed; generated a new one") + random_id = secrets.token_hex(3) + check_id = answer_set.query.filter_by(session=random_id).first() + + + + session['user'] = random_id + + + #create answer set for the participant in the database + participant_answer_set = answer_set(experiment_idexperiment=session['exp_id'], session=session['user'], agreement = session['agree'], answer_counter = '0') + 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 + + exp_status = experiment.query.filter_by(idexperiment=session['exp_id']).first() + + if exp_status.randomization == 'On': + + session['randomization'] = 'On' + + #flash("answer_set_id") + #flash(participant_answer_set.idanswer_set) + + #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] + + + #flash("original Page id order:") + #for a in range(len(original_id_order_list)): + + #flash(original_id_order_list[a]) + + #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() + 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']) + 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 + + #old: was missing experiment id so made duplicates + #session_id_for_participant = answer_set.query.filter_by(session=session['user']).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() + + if mediatype: + session['type'] = mediatype.type + else: + flash('No pages or mediatype set for experiment') + return redirect('/') + + + if 'user' in session: + user = session['user'] + #flash('Session started for user {}'.format(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 + + + form.questions1 = questions_and_options + + if request.method == 'POST'and form.validate(): + + data = request.form.to_dict() + for key, value in data.items(): + + + #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) + db.session.add(participant_background_question_answers) + db.session.commit() + + return redirect('/instructions') + + + return render_template('register.html', form=form) + + +@app.route('/task_completed') +def task_completed(): + + session.pop('user', None) + session.pop('exp_id', None) + session.pop('agree', None) + session.pop('answer_set', None) + session.pop('type', None) + session.pop('randomization', None) + + + return render_template('task_completed.html') + + +@app.route('/continue_task', methods=['GET', 'POST']) +def continue_task(): + + + exp_id = request.args.get('exp_id', None) + form = ContinueTaskForm() + + + if form.validate_on_submit(): + + #check if participant ID is found from db and that the answer set is linked to the correct experiment + participant = answer_set.query.filter(and_(answer_set.session==form.participant_id.data, answer_set.experiment_idexperiment==exp_id)).first() + if participant is None: + flash('Invalid ID') + return redirect(url_for('continue_task', exp_id=exp_id)) + + #flash('Login requested for participant {}'.format(form.participant_id.data)) + + #if correct participant_id is found with the correct experiment ID; start session for that user + session['exp_id'] = exp_id + session['user'] = form.participant_id.data + session['answer_set'] = participant.idanswer_set + mediatype = page.query.filter_by(experiment_idexperiment=session['exp_id']).first() + + rand = experiment.query.filter_by(idexperiment=session['exp_id']).first() + + session['randomization'] = rand.randomization + + if mediatype: + session['type'] = mediatype.type + else: + flash('No pages or mediatype set for experiment') + return redirect('/') + + + #If participant has done just the registration redirect to the first page of the experiment + if participant.answer_counter == 0: + #flash("Ei vastauksia ohjataan ekalle sivulle") + return redirect( url_for('task', page_num=1)) + + + redirect_to_page = participant.answer_counter + 1 + + + #flash("redirect to page:") + #flash(redirect_to_page) + + experiment_page_count = db.session.query(page).filter_by(experiment_idexperiment=session['exp_id']).count() + + #If participant has ansvered all pages allready redirect to task completed page + if experiment_page_count == participant.answer_counter: + + return redirect( url_for('task_completed')) + + + return redirect( url_for('task', page_num=redirect_to_page)) + + return render_template('continue_task.html', exp_id=exp_id, form=form) + + +@app.route('/begin_with_id', methods=['GET', 'POST']) +def begin_with_id(): + + + exp_id = request.args.get('exp_id', None) + form = StartWithIdForm() + + 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() + 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: + 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 + session['begin_with_id'] = form.participant_id.data + return render_template('consent.html', exp_id=exp_id) + + + return render_template('begin_with_id.html', exp_id=exp_id, form=form) + + +@app.route('/create_task') +def create_task(): + return render_template('create_task.html') + + +@app.route('/instructions') +def instructions(): + + participant_id = session['user'] + instructions = experiment.query.filter_by(idexperiment = session['exp_id']).all() + return render_template('instructions.html', instructions=instructions, participant_id=participant_id) + + +@app.route('/task/<int:page_num>', methods=['GET', 'POST']) +def task(page_num): + + pages = page.query.filter_by(experiment_idexperiment=session['exp_id']).paginate(per_page=1, page=page_num, error_out=True) + progress_bar_percentage = round((pages.page/pages.pages)*100) + + #this variable is feeded to the template as empty if trial randomization is set to "off" + randomized_stimulus = "" + + + + #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 + if session['randomization'] == 'On': + + + randomized_page_id = trial_randomization.query.filter(and_(trial_randomization.answer_set_idanswer_set==session['answer_set'], trial_randomization.page_idpage==pages.items[0].idpage)).first() + #answer.query.filter(and_(answer.answer_set_idanswer_set==session['answer_set'], answer.page_idpage==session['current_idpage'])).first() + #flash("randomized page:") + #flash(randomized_page_id.randomized_idpage) + #set the stimulus to be shown if randomization is on + randomized_stimulus = page.query.filter_by(idpage=randomized_page_id.randomized_idpage).first() + + + + for p in pages.items: + session['current_idpage'] = p.idpage + + #slider set + form = TaskForm(request.form) + categories_and_scales = {} + categories = question.query.filter_by(experiment_idexperiment=session['exp_id']).all() + + for cat in categories: + + scale_list = [(cat.left, cat.right)] + categories_and_scales[cat.idquestion, cat.question] = scale_list + + form.categories1 = categories_and_scales + + + #slider set form handling + if request.method == 'POST'and form.validate(): + + + #Lets check if there are answers in database already for this page_id (eg. if user returned to previous page and tried to answer again) + #If so flash ("Page has been answered already. Answers discarded"), else insert values in to db + #this has to be done separately for trial randomization "on" and "off" situations + + if session['randomization'] == 'On': + + check_answer = answer.query.filter(and_(answer.answer_set_idanswer_set==session['answer_set'], answer.page_idpage==randomized_page_id.randomized_idpage)).first() + + if session['randomization'] == 'Off': + + check_answer = answer.query.filter(and_(answer.answer_set_idanswer_set==session['answer_set'], answer.page_idpage==session['current_idpage'])).first() + + + + if check_answer is None: + + update_answer_counter = answer_set.query.filter_by(idanswer_set=session['answer_set']).first() + update_answer_counter.answer_counter = int(update_answer_counter.answer_counter) + 1 + + #flash("vastauksia:") + #flash(update_answer_counter.answer_counter) + db.session.commit() + + data = request.form.to_dict() + for key, value in data.items(): + + #flash(key) + #flash(value) + #flash(session['current_idpage']) + + + #Insert slider values to database + + + #If trial randomization is set to 'Off' the values are inputted for session['current_idpage'] + #Otherwise the values are set for the corresponding id found in the trial randomization table + + if session['randomization'] == 'Off': + + participant_answer = answer(question_idquestion=key, answer_set_idanswer_set=session['answer_set'], answer=value, page_idpage=session['current_idpage']) + db.session.add(participant_answer) + db.session.commit() + + else: + + participant_answer = answer(question_idquestion=key, answer_set_idanswer_set=session['answer_set'], answer=value, page_idpage=randomized_page_id.randomized_idpage) + db.session.add(participant_answer) + db.session.commit() + + + else: + flash("Page has been answered already. Answers discarded") + + page_num=pages.next_num + + if pages.has_next: + return redirect( url_for('task', page_num=pages.next_num)) + + return redirect ( url_for('task_completed')) + + + return render_template('task.html', pages=pages, progress_bar_percentage=progress_bar_percentage, form=form, randomized_stimulus=randomized_stimulus) + + + +@app.route('/quit_task') +def quit_task(): + + user_id = session['user'] + session.pop('user', None) + session.pop('exp_id', None) + session.pop('agree', None) + session.pop('answer_set', None) + session.pop('type', None) + + return render_template('quit_task.html', user_id=user_id) + + +@app.route('/researcher_login', methods=['GET', 'POST']) +def login(): + if current_user.is_authenticated: + flash("allready logged in") + return redirect(url_for('index')) + form = LoginForm() + if form.validate_on_submit(): + 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) + return redirect(url_for('index')) + +# flash('Login requested for user {}, remember_me={}'.format( +# form.username.data, form.remember_me.data)) +# return redirect('/index') + return render_template('researcher_login.html', title='Sign In', form=form) + + +@app.route('/logout') +def logout(): + logout_user() + return redirect(url_for('index')) + + +@app.route('/experiment_statistics') +@login_required +def experiment_statistics(): + + 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() + + participants_and_answers = {} + + for participant in participants: + + answers = answer.query.filter_by(answer_set_idanswer_set=participant.idanswer_set).all() + answers_list = [(a.idanswer, a.question_idquestion, a.answer_set_idanswer_set, a.answer, a.page_idpage) for a in answers] + participants_and_answers[participant.session] = answers_list + + + + pages = page.query.filter_by(experiment_idexperiment=exp_id).all() + pages_and_questions = {} + + for p in pages: + + questions = question.query.filter_by(experiment_idexperiment=exp_id).all() + questions_list = [(p.idpage, a.question) for a in questions] + pages_and_questions[p.idpage] = questions_list + + + bg_questions = background_question.query.filter_by(experiment_idexperiment=exp_id).all() + bg_answers_for_participants = {} + + for participant in participants: + + 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 + + + started_ratings = answer_set.query.filter_by(experiment_idexperiment=exp_id).count() + + + + return render_template('experiment_statistics.html', experiment_info=experiment_info, participants_and_answers=participants_and_answers, pages_and_questions=pages_and_questions, bg_questions=bg_questions, bg_answers_for_participants=bg_answers_for_participants, started_ratings=started_ratings) + + + +#EDIT FUNCTIONS + +@app.route('/create_experiment', methods=['GET', 'POST']) +@login_required +def create_experiment(): + + form = CreateExperimentForm(request.form) + + + if request.method == 'POST' and form.validate(): + + new_exp = experiment(name=request.form['name'], instruction=request.form['instruction'], language=request.form['language'], status='Hidden', randomization='Off') + db.session.add(new_exp) + db.session.commit() + + #flash("lol") + #flash(new_exp.idexperiment) + + exp_id = new_exp.idexperiment + + #data = request.form.to_dict() + #for key, value in data.items(): + #tähän db insertit + + #flash(key) + #flash(value) + #flash('{}'.format(form.name.data)) + + #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(url_for('create_experiment_bgquestions', exp_id=exp_id)) + + return render_template('create_experiment.html', form=form) + + + +@app.route('/create_experiment_bgquestions', methods=['GET', 'POST']) +@login_required +def create_experiment_bgquestions(): + + exp_id = request.args.get('exp_id', None) + form = CreateBackgroundQuestionForm(request.form) + + if request.method == 'POST' and form.validate(): + + #data = request.form.to_dict() + + #flash(data) + #flash(form.bg_questions_and_options.data) + + + str = form.bg_questions_and_options.data + + #Split the form data into a list that separates questions followed by the corresponding options + str_list = str.split('/n') + + #Iterate through the questions and options list + for a in range(len(str_list)): + + #Split the list cells further into questions and options + list = str_list[a].split(';') + + #flash(list[0]) + #flash("id oikein?") + #flash(add_bgquestion.idbackground_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: + #flash("Kysymys") + #flash(list[x]) + add_bgquestion = background_question(background_question=list[x], experiment_idexperiment=exp_id) + db.session.add(add_bgquestion) + db.session.commit() + + else: + #flash("optio") + #flash(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('create_experiment_questions', exp_id=exp_id)) + + return render_template('create_experiment_bgquestions.html', form=form, exp_id=exp_id) + + +@app.route('/create_experiment_questions', methods=['GET', 'POST']) +@login_required +def create_experiment_questions(): + + exp_id = request.args.get('exp_id', None) + + 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)): + + list = str_list[a].split(';') + + #If there are the right amount of values for the slider input values + if len(list) == 3: + + #flash("Question:") + #flash(list[0]) + #flash("Left:") + #flash(list[1]) + #flash("Right:") + #flash(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 + else: + 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('create_experiment_upload_stimuli', exp_id=exp_id)) + + return render_template('create_experiment_questions.html', form=form) + + +@app.route('/create_experiment_upload_stimuli', methods=['GET', 'POST']) +@login_required +def create_experiment_upload_stimuli(): + + exp_id = request.args.get('exp_id', None) + + form = UploadStimuliForm(request.form) + + if request.method == 'POST' and form.validate(): + + #flash("validated") + #flash(form.type.data) + #flash(form.text.data) + + + #If stimulus type is text lets parse the information and insert it to database + + if form.type.data == 'text': + + #flash("db insert text") + + string = form.text.data + str_list = string.split('/n') + + for a in range(len(str_list)): + + #flash("lisättiin:") + #flash(str_list[a]) + 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() + + #flash("Succes!") + + return redirect(url_for('view_experiment', 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) + path = 'static/experiment_stimuli/' + str(exp_id) + + target = os.path.join(APP_ROOT, path) + #flash(target) + + if not os.path.isdir(target): + os.mkdir(target) + #flash("make dir") + + + #This returns a list of filenames: request.files.getlist("file") + + for file in request.files.getlist("file"): + + #save files in the correct folder + #flash(file.filename) + filename = file.filename + destination = "/".join([target, filename]) + #flash("destination") + #flash(destination) + file.save(destination) + + #add pages to the db + db_path = path + str('/') + str(filename) + + #flash("db path") + #flash(db_path) + + new_page = page(experiment_idexperiment=exp_id, type=form.type.data, media=db_path) + + db.session.add(new_page) + db.session.commit() + + #flash("Succes!") + + return redirect(url_for('view_experiment', exp_id=exp_id)) + + return redirect(url_for('create_experiment_upload_stimuli', exp_id=exp_id)) + + return render_template('create_experiment_upload_stimuli.html', form=form) + + +@app.route('/view_experiment') +@login_required +def view_experiment(): + + #crap:3lines + exp_id = request.args.get('exp_id', None) + media = page.query.filter_by(experiment_idexperiment=exp_id).paginate(per_page=20, page=1, error_out=True) + mtype = page.query.filter_by(experiment_idexperiment=exp_id).first() + + #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() + + 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 + + questions1 = questions_and_options + + #sliderset + categories_and_scales = {} + categories = question.query.filter_by(experiment_idexperiment=exp_id).all() + + for cat in categories: + + scale_list = [(cat.left, cat.right)] + categories_and_scales[cat.idquestion, cat.question] = scale_list + + categories1 = categories_and_scales + + + return render_template('view_experiment.html', exp_id=exp_id, media=media, mtype=mtype, experiment_info=experiment_info, categories1=categories1, questions1=questions1) + + +@app.route('/edit_experiment', methods=['GET', 'POST']) +@login_required +def edit_experiment(): + + exp_id = request.args.get('exp_id', None) + + 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('view_experiment', exp_id=exp_id)) + + + return render_template('edit_experiment.html', form=form, exp_id=exp_id) + + + + + +@app.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 + 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) + + 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 + if request.method == 'POST' and form.validate(): + + + #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 + for x in range(len(form_values_list)): + + form_values_list[x] = form_values_list[x].lstrip() + + #Cycle through strings and update db + for x in range(len(form_values_list)): + + #Replace question and update the object to database + if x == 0: + + + #flash("delete kys:") + #flash(current_bg_question.background_question) + #flash("insert kys:") + current_bg_question.background_question = form_values_list[x] + #flash(current_bg_question.background_question) + #flash(current_bg_question.idbackground_question) + #flash(current_bg_question.experiment_idexperiment) + + db.session.commit() + + #Delete old options from db + for o in options: + + db.session.delete(o) + db.session.commit() + + #Insert new options to db + else: + + + #flash("insert opt:") + #flash(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('view_experiment', exp_id=exp_id)) + + + return render_template('edit_bg_question.html', form=form, exp_id=exp_id) + + + +@app.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('view_experiment', exp_id=current_question.experiment_idexperiment)) + + return render_template('edit_question.html', form=form) + + + +@app.route('/add_bg_question', methods=['GET', 'POST']) +@login_required +def add_bg_question(): + + exp_id = request.args.get('exp_id', None) + form = CreateBackgroundQuestionForm(request.form) + + if request.method == 'POST' and form.validate(): + + str = form.bg_questions_and_options.data + + #Split the form data into a list that separates questions followed by the corresponding options + str_list = str.split('/n') + + #Iterate through the questions and options list + for a in range(len(str_list)): + + #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 + for x in range(len(list)): + + if x == 0: + #flash("Kysymys") + #flash(list[x]) + add_bgquestion = background_question(background_question=list[x], experiment_idexperiment=exp_id) + db.session.add(add_bgquestion) + db.session.commit() + + else: + #flash("optio") + #flash(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('view_experiment', exp_id=exp_id)) + + return render_template('add_bg_question.html', form=form) + + + +@app.route('/add_questions', methods=['GET', 'POST']) +@login_required +def add_questions(): + + exp_id = request.args.get('exp_id', None) + 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)): + + list = str_list[a].split(';') + + #If there are the right amount of values for the slider input values + if len(list) == 3: + + #flash("Question:") + #flash(list[0]) + #flash("Left:") + #flash(list[1]) + #flash("Right:") + #flash(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 + else: + 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('view_experiment', exp_id=exp_id)) + + return render_template('add_questions.html', form=form) + + + +#Remove functions + + +@app.route('/remove_bg_question') +@login_required +def remove_bg_question(): + + exp_id = request.args.get('exp_id', None) + 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)): + + #flash(remove_options[a].idbackground_question_option) + + db.session.delete(remove_options[a]) + db.session.commit() + + + remove_question = background_question.query.filter_by(idbackground_question=remove_id).first() + + db.session.delete(remove_question) + db.session.commit() + + return redirect(url_for('view_experiment', exp_id=exp_id)) + + + + +@app.route('/remove_question') +@login_required +def remove_question(): + + exp_id = request.args.get('exp_id', None) + remove_id = request.args.get('idquestion', None) + + remove_question = question.query.filter_by(idquestion=remove_id).first() + + db.session.delete(remove_question) + db.session.commit() + + return redirect(url_for('view_experiment', exp_id=exp_id)) + + +@app.route('/remove_experiment', methods=['GET', 'POST']) +@login_required +def remove_experiment(): + + exp_id = request.args.get('exp_id', None) + + form = RemoveExperimentForm(request.form) + + if request.method == 'POST' and form.validate(): + + if form.remove.data == 'DELETE': + + + #This removes all experiment data from the database! + + + #Tables + + #background_question_option & background_question & background question answers: + remove_background_question = background_question.query.filter_by(experiment_idexperiment=exp_id).all() + + #cycle through all bg questions and delete their options + 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() + + for b in range(len(remove_background_question_option)): + + db.session.delete(remove_background_question_option[b]) + db.session.commit() + + + #Remove all background questions and all answers given to each bg question + for a in range(len(remove_background_question)): + + remove_background_question_answers = background_question_answer.query.filter_by(background_question_idbackground_question=remove_background_question[a].idbackground_question).all() + + for b in range(len(remove_background_question_answers)): + + db.session.delete(remove_background_question_answers[b]) + db.session.commit() + + 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() + + for a in range(len(remove_question)): + + remove_question_answers = answer.query.filter_by(question_idquestion=remove_question[a].idquestion).all() + + for b in range(len(remove_question_answers)): + + db.session.delete(remove_question_answers[b]) + db.session.commit() + + db.session.delete(remove_question[a]) + db.session.commit() + + + #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': + + db.session.delete(remove_page[a]) + db.session.commit() + + else: + + target = os.path.join(APP_ROOT, remove_pages[a].media) + + if os.path.exists(target): + os.remove(target) + + #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() + + 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() + + for b in range(len(remove_trial_randomizations)): + + db.session.delete(remove_trial_randomizations[b]) + db.session.commit() + + db.session.delete(remove_answer_set[a]) + db.session.commit() + + + #Remove experiment table + remove_experiment = experiment.query.filter_by(idexperiment=exp_id).first() + db.session.delete(remove_experiment) + db.session.commit() + + + flash("Experiment was removed from database!") + + return redirect(url_for('index')) + + else: + + flash("Experiment was not removed!") + + return redirect(url_for('view_experiment', exp_id=exp_id)) + + + + return render_template('remove_experiment.html', form=form, exp_id=exp_id) + + + +@app.route('/remove_page') +@login_required +def remove_page(): + + exp_id = request.args.get('exp_id', None) + 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 + if remove_page.type != 'text': + + #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 + for a in range(len(experiment_pages)): + + #flash("in da for") + + if experiment_pages[a].media == remove_page.media and experiment_pages[a].idpage != remove_page.idpage: + + #flash("in da if") + do_not_delete_file = 'True' + + #If no other page is using the file then lets remove it + if do_not_delete_file == 'False': + #remove old file + target = os.path.join(APP_ROOT, remove_page.media) + #flash("Remove:") + #flash(target) + os.remove(target) + + db.session.delete(remove_page) + db.session.commit() + + return redirect(url_for('view_experiment', exp_id=exp_id)) + + if remove_page.type == 'text': + + db.session.delete(remove_page) + db.session.commit() + + return redirect(url_for('view_experiment', exp_id=exp_id)) + + return redirect(url_for('view_experiment', exp_id=exp_id)) + + +@app.route('/publish_experiment') +@login_required +def publish_experiment(): + + exp_id = request.args.get('exp_id', None) + + publish_experiment = experiment.query.filter_by(idexperiment = exp_id).first() + + publish_experiment.status = 'Public' + + flash("Changed status to Public") + + db.session.commit() + + return redirect(url_for('view_experiment', exp_id=exp_id)) + + +@app.route('/hide_experiment') +@login_required +def hide_experiment(): + + exp_id = request.args.get('exp_id', None) + + hide_experiment = experiment.query.filter_by(idexperiment = exp_id).first() + + hide_experiment.status = 'Hidden' + + flash("Changed status to Hidden") + + db.session.commit() + + return redirect(url_for('view_experiment', exp_id=exp_id)) + + +@app.route('/enable_randomization') +@login_required +def enable_randomization(): + + exp_id = request.args.get('exp_id', None) + + enable_randomization = experiment.query.filter_by(idexperiment = exp_id).first() + + enable_randomization.randomization = 'On' + + flash("Enabled trial randomization") + + db.session.commit() + + return redirect(url_for('view_experiment', exp_id=exp_id)) + + +@app.route('/disable_randomization') +@login_required +def disable_randomization(): + + exp_id = request.args.get('exp_id', None) + + disable_randomization = experiment.query.filter_by(idexperiment = exp_id).first() + + disable_randomization.randomization = 'Off' + + flash("Disabled trial randomization") + + db.session.commit() + + return redirect(url_for('view_experiment', exp_id=exp_id)) + + +@app.route('/edit_stimuli', methods=['GET', 'POST']) +@login_required +def edit_stimuli(): + + exp_id = request.args.get('exp_id', None) + page_id = request.args.get('idpage', None) + edit_page = page.query.filter_by(idpage=page_id).first() + + + + form = EditPageForm(request.form, obj=edit_page) + + if request.method == 'POST' and form.validate(): + + + #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 + target = os.path.join(APP_ROOT, edit_page.media) + #flash("Remove:") + #flash(target) + os.remove(target) + + #upload new file + + path = 'static/experiment_stimuli/' + str(exp_id) + + target = os.path.join(APP_ROOT, path) + #flash(target) + + if not os.path.isdir(target): + os.mkdir(target) + #flash("make dir") + + + #This returns a list of filenames: request.files.getlist("file") + + for file in request.files.getlist("file"): + + #save files in the correct folder + #flash(file.filename) + filename = file.filename + destination = "/".join([target, filename]) + #flash("destination") + #flash(destination) + file.save(destination) + + #update db object + db_path = path + str('/') + str(filename) + + #flash("db path") + #flash(db_path) + + edit_page.media=db_path + + db.session.commit() + + #flash("Succes!") + + + #If editing text stimulus no need for filehandling + else: + + form.populate_obj(edit_page) + db.session.commit() + + + + return redirect(url_for('view_experiment', exp_id=exp_id)) + + return render_template('edit_stimuli.html', form=form, edit_page=edit_page) + + +@app.route('/add_stimuli', methods=['GET', 'POST']) +@login_required +def add_stimuli(): + + exp_id = request.args.get('exp_id', None) + stimulus_type = request.args.get('stimulus_type', None) + + form = UploadStimuliForm(request.form) + + if request.method == 'POST': + + + if stimulus_type == 'text': + + #flash("db insert text") + + string = form.text.data + str_list = string.split('/n') + + for a in range(len(str_list)): + + #flash("lisättiin:") + #flash(str_list[a]) + 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() + + #flash("Succes!") + + return redirect(url_for('view_experiment', 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) + path = 'static/experiment_stimuli/' + str(exp_id) + + target = os.path.join(APP_ROOT, path) + #flash(target) + + if not os.path.isdir(target): + os.mkdir(target) + #flash("make dir") + + + #This returns a list of filenames: request.files.getlist("file") + + for file in request.files.getlist("file"): + + #save files in the correct folder + #flash(file.filename) + filename = file.filename + destination = "/".join([target, filename]) + #flash("destination") + #flash(destination) + file.save(destination) + + #add pages to the db + db_path = path + str('/') + str(filename) + + #flash("db path") + #flash(db_path) + + new_page = page(experiment_idexperiment=exp_id, type=form.type.data, media=db_path) + + db.session.add(new_page) + db.session.commit() + + #flash("Succes!") + + return redirect(url_for('view_experiment', exp_id=exp_id)) + + return redirect(url_for('view_experiment', exp_id=exp_id)) + + + return render_template('add_stimuli.html', form=form, stimulus_type=stimulus_type) + + +@app.route('/download_csv') +@login_required +def download_csv(): + + exp_id = request.args.get('exp_id', None) + + """ + with open('export_new.csv', 'w', newline='') as f: + + thewriter = csv.writer(f) + + thewriter.writerow(['1','2','3','4']) + thewriter.writerow(['a','b','c','d']) + """ + + + + return redirect(url_for('experiment_statistics', exp_id=exp_id)) + + +@app.route('/researcher_info') +@login_required +def researcher_info(): + + + return render_template('researcher_info.html') + diff --git a/app/static/css/custom.css b/app/static/css/custom.css new file mode 100644 index 0000000..2389230 --- /dev/null +++ b/app/static/css/custom.css @@ -0,0 +1,7 @@ +.btn { + border-radius: 0px; +} +.navbar-custom{ + background-color:red; + border-color: green; +} diff --git a/app/static/css/main.css b/app/static/css/main.css new file mode 100644 index 0000000..73f519a --- /dev/null +++ b/app/static/css/main.css @@ -0,0 +1,28 @@ +body { + background-color: darkgrey; + font-family: Helvetica, Arial, sans-serif; +} + + +container { + + text-decoration-color: #4EB1BA; + margin-left: auto; + margin-right: auto; + +} + +#header{ + background-color: darkslategrey; + color: white; + text-align: center; + +} + +#nav{ + +} + +#main{ + +} \ No newline at end of file diff --git a/app/static/css/style.css b/app/static/css/style.css new file mode 100644 index 0000000..cebb222 --- /dev/null +++ b/app/static/css/style.css @@ -0,0 +1,1455 @@ +@import url('css/prettyPhoto.css'); + +/*-----------------------------------------------------------------------------------* +/* = Reset default browser CSS. Based on work by Eric Meyer: http://meyerweb.com/eric/tools/css/reset/index.html +/*-----------------------------------------------------------------------------------*/ +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, font, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td { + border: 0; + font-family: inherit; + font-size: 100%; + font-style: inherit; + font-weight: inherit; + margin: 0; + outline: 0; + padding: 0; + vertical-align: baseline; +} + +/* remember to define focus styles! */ +:focus { + outline: 0; +} +body { + background: #fff; + line-height: 1; +} +ol, ul { + list-style: none; +} + +/* tables still need 'cellspacing="0"' in the markup */ +table { + border-collapse: separate; + border-spacing: 0; +} +caption, th, td { + font-weight: normal; + text-align: left; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ""; +} +blockquote, q { + quotes: "" ""; +} +a img { + border: 0; +} + +/* Block elements */ +header, hgroup, footer, section, article, aside { + display: block; +} + +/*-----------------------------------------------------------------------------------* +/* = Body, Common Classes & Wrap +/*-----------------------------------------------------------------------------------*/ + +body { + background: #212121; + font-size: 12px; + line-height: 1.5em; + color: #444; + font-family: 'Helvetica Nue', Arial, Helvetica, sans-serif; +} +a { + text-decoration: none; + color: #7da1bc; +} +a:hover { + text-decoration: none; + color: #ef4034; +} +p { margin: 0 0 1em } +strong { font-weight: 700 } +em { font-style: italic } +pre{ + margin: 20px 0; + background: #eee; + border: 1px solid #ddd; + padding: 10px; + white-space: pre-wrap; /* css-3 */ + white-space: -moz-pre-wrap; /* Mozilla */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ +} +blockquote{ + border-left: 4px solid #ccc; + padding-left: 20px; + margin: 30px 0px; +} +.clear { clear: both } +.clearfix:after { + visibility: hidden; + display: block; + font-size: 0; + content: " "; + clear: both; + height: 0; +} +* html .clearfix{ zoom: 1; } /* IE6 */ +*:first-child+html .clearfix { zoom: 1; } /* IE7 */ +.remove-margin { margin-right: 0 !important } + +/*-----------------------------------------------------------------------------------* +/* = Structure +/*-----------------------------------------------------------------------------------*/ + +#wrap{ + overflow: hidden; + background:#fff; + margin: 0 auto; + margin-bottom: 30px; + width: 920px; + padding: 30px 30px 0; + /* + -moz-box-shadow: 0px 0px 8px rgb(0,0,0); + -webkit-box-shadow: 0px 0px 8px rgb(0,0,0); + box-shadow: 0px 0px 8px rgb(0,0,0); + */ +} + +/*-----------------------------------------------------------------------------------* +/* = Headings +/*-----------------------------------------------------------------------------------*/ + +h1, +h2, +h3, +h4 { + color: #333; + line-height: 1.5em; + font-weight: bold; +} +h1 { + font-size: 24px; + margin-bottom: 20px; + line-height: 1.2em; +} +h2 { font-size: 18px } +h3 { font-size: 16px } +h4 { font-size: 14px; } +h2, +h3, +h4 { + margin-top: 30px; + margin-bottom: 20px; +} +h1 a, +h2 a, +h3 a, +h4 a { color: #333 } +h2 a:hover, +h3 a:hover, +h4 a:hover { color: #ef4034; text-decoration: none; } + +.single-title{ margin-bottom: 5px; } + +/*-----------------------------------------------------------------------------------* +/* = WordPress Styles +/*-----------------------------------------------------------------------------------*/ + +/*sticky*/ +.sticky{} + +/*gallery captio*/ +.gallery-caption + +/*by post author*/ +.bypostauthor{} + +/*aligns*/ +.aligncenter{ display:block; margin:0 auto} +.alignright{ float:right; margin:10px 0 10px 10px} +.alignleft{ float:left; margin:10px 10px 10px 0} + +/*floats*/ +.floatleft{ float:left} +.floatright{ float:right} + +/*text style*/ +.textcenter{ text-align:center} +.textright{ text-align:right} +.textleft{ text-align:left} + +/*captions*/ +.wp-caption{ + border:1px solid #ddd; + text-align:center; + background-color:#f3f3f3; + padding-top:4px; + margin:10px; + -moz-border-radius:3px; + -khtml-border-radius:3px; + -webkit-border-radius:3px; + border-radius:3px; +} +.wp-caption img{ + margin:0; + padding:0; + border:0 none; +} +.wp-caption p.wp-caption-text{ + font-size:11px; + line-height:17px; + padding:0 4px 5px; + margin:0; +} + +/*smiley reset*/ +.wp-smiley{ + margin:0 !important; + max-height:1em; +} + +/*blockquote*/ +blockquote.left{ + margin-right:20px; + text-align:right; + margin-left:0; + width:33%; + float:left; +} +blockquote.right{ + margin-left:20px; + text-align:left; + margin-right:0; + width:33%; + float:right; +} + +/* tag-cloud widget */ +.tagcloud a { + float: left; + display: block; + margin-right: 5px; + margin-bottom: 5px; + padding: 4px 7px; + line-height: 1.3em; + color: #fff !important; + background: #ef4034; + font-weight: bold; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + -ms-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} +.tagcloud a:hover{ + background: #eee; + color: #333 !important; + text-decoration: none; +} +/* calendar widget */ +.widget_calendar {float: left;} +#wp-calendar {width: 100%; } +#wp-calendar caption { + text-align: right; + color: #333; + font-size: 12px; + margin-top: 10px; + margin-bottom: 15px; +} +#wp-calendar thead { font-size: 12px; } +#wp-calendar thead th { padding-bottom: 10px; } +#wp-calendar tbody { color: #aaa; } +#wp-calendar tbody td { background: #f5f5f5; border: 1px solid #fff; text-align: center; padding:8px;} +#wp-calendar tbody td:hover { background: #fff; } +#wp-calendar tbody .pad { background: none; } +#wp-calendar tfoot #next { font-size: 12px; text-transform: uppercase; text-align: right; } +#wp-calendar tfoot #prev { font-size: 12px; text-transform: uppercase; padding-top: 10px; } + +/*-----------------------------------------------------------------------------------* +/* = Headings +/*-----------------------------------------------------------------------------------*/ +#logo h2, +#logo h1 { + margin: 0 !important; + font-size: 36px; + font-weight: 800; + line-height: 1em !important; +} +#logo h2 a, +#logo h1 a { + color: #000; + text-decoration: none; +} +#logo h2 a:hover, +#logo h1 a:hover { color: #999 } + +#page-heading{ + margin: -30px -30px 30px; + padding: 0px 30px; + height: 65px; + background: #eee; + border-bottom: 1px solid #ddd; + position: relative; +} +#page-heading h1, +#page-heading h2{ + font-size: 21px; + line-height: 65px; + color: #000; + text-shadow: 0px 0px 0px #fff; + margin: 0px !important; +} + +/*-------------------------------------------------* +/* = Header +/*-------------------------------------------------*/ +#header { + margin: 0 auto; + position: relative; + width: 980px; + padding: 30px 0px; +} +#logo a{ + font-size: 24px; + font-weight: normal; + color: #E8E8E8; + letter-spacing: -1px; + line-height: 0em; + padding: 0px; + margin: 0px; + text-decoration: none; +} +#logo a:hover { color: #FFF; } + + +/*-------------------------------------------------* +/* = Navigation +/*-------------------------------------------------*/ +#navigation { + position: absolute; + right: 0px; + top: 30px; +} +/*** ESSENTIAL Navigation Style ***/ +.sf-menu, +.sf-menu * { + margin: 0; + padding: 0; + list-style: none; +} +.sf-menu { line-height: 1.0 } +.sf-menu ul { + position: absolute; + top: -999em; + width: 160px; /* left offset of submenus need to match (see below) */ +} +.sf-menu ul li { width: 100% } +.sf-menu li:hover { + visibility: inherit; /* fixes IE7 'sticky bug' */ +} +.sf-menu li { + float: left; + position: relative; +} +.sf-menu a { + display: block; + position: relative; +} +.sf-menu li:hover ul, +.sf-menu li.sfHover ul { + left: 0; + top: 48px; /* match top ul list item height */ + z-index: 99; +} +ul.sf-menu li:hover li ul, +ul.sf-menu li.sfHover li ul { top: -999em } +ul.sf-menu li li:hover ul, +ul.sf-menu li li.sfHover ul { + left: 180px; /* match ul width */ + top: 0; +} +/*** navigation skin ***/ +.sf-menu { + float: left; + margin-bottom: 1em; +} +.sf-menu a { + font-size: 12px; + font-weight: bold; + color: #999; + border-bottom: 1px solid #333; + padding: 0 10px 10px; + text-decoration: none; + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + -ms-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} +.sf-menu a:focus, +.sf-menu a:hover, +.sf-menu a:active { + color: #FFF; + border-bottom: 1px solid #fee825 !important; +} +#navigation .current-menu-item > a:first-child { + color: #FFF; + border-bottom: 1px solid #fee825; +} +/*Subs*/ +.sf-menu ul{ + background: #212121; + padding: 10px 10px 20px; + -moz-opacity: 0.98; + -khtml-opacity: 0.98; + opacity: 0.98; +} +.sf-menu ul a{ + padding: 10px 0px; + border-bottom: 1px solid #333; +} +.sf-menu ul a:focus, +.sf-menu ul a:hover, +.sf-menu ul a:active { + border-bottom: 1px solid #333 !important; +} +.sf-menu ul .current-menu-item a{ + border-bottom: 1px solid #333 !important; +} + +/*-----------------------------------------------------------------------------------*/ +/* = Home +/*-----------------------------------------------------------------------------------*/ + +.home-wrap{ + margin: -30px; +} + +#home-tagline{ + padding: 20px 30px; + background:#eee; + color: #666; + border-bottom: 1px solid #ddd; + margin-bottom: 30px; + margin-top: -30px; + font-size: 16px; + line-height: 1.4em; + font-weight: bold; + text-shadow: 1px 1px 1px #fff; +} +#home-tagline a{ + color: #ef4034; +} +#home-tagline a:hover{ + border-bottom: 1px dotted #ef4034; +} +#home-projects { + margin: 0px 30px 0px; +} +h2.home-projects-heading{ + font-size: 14px; + font-weight: bold; + border-bottom: double #eee; + padding-bottom: 5px; +} + +/*-----------------------------------------------------------------------------------*/ +/* = NivoSlider +/*-----------------------------------------------------------------------------------*/ +#slider_nivo { + position: relative; + width: 980px; + height: 400px; + overflow: hidden; + margin-bottom: 30px; +} +.nivoSlider { + position: relative; + width: 980px; + height: 400px; + background: #fff url(images/nivo-loader.gif) no-repeat 50% 50%; +} +.nivoSlider img { + position: absolute; + top: 0px; + left: 0px; + z-index: 6; + display:none; +} +.nivoSlider a.nivo-imageLink { + position: absolute; + top: 0px; + left: 0px; + width: 980px; + height: 100%; + border: 0; + padding: 0; + margin: 0; + z-index: 6; + display: none; +} +.nivo-slice { + display: block; + position: absolute; + z-index: 5; + height: 100%; +} +.nivo-box { + display: block; + position: absolute; + z-index: 5; +} +.nivo-directionNav a { + width: 48px; + height: 48px; + position: absolute; + z-index: 1000; + top: 178px; + cursor: pointer; +} +.nivo-prevNav, +.nivo-nextNav { + -moz-opacity: 0.4; + -khtml-opacity: 0.4; + opacity: 0.4; + text-indent: -9999px; + -webkit-transition: opacity 0.2s ease-in-out; + -moz-transition: opacity 0.2s ease-in-out; + -o-transition: opacity 0.2s ease-in-out; + -ms-transition: opacity 0.2s ease-in-out; + transition: opacity 0.2s ease-in-out; +} +.nivo-prevNav:hover, +.nivo-nextNav:hover { + -moz-opacity: 1.0; + -khtml-opacity: 1.0; + opacity: 1.0; +} +.nivo-nextNav { + background: url('images/sliders/nivo-right-arrow.png'); + right: 20px; +} +.nivo-prevNav { + background: url('images/sliders/nivo-left-arrow.png'); + left: 20px; +} +.nivo-controlNav a.active { font-weight: bold } +.nivo-controlNav { + position: absolute; + z-index: 1000; + list-style: none; + bottom: 10px; + right: 10px; + padding: 0; +} +.nivo-controlNav a { + float: left; + margin-left: 5px; + cursor: pointer; + color: #999; + text-indent: -9999px; + background: url('images/sliders/bullets.png') no-repeat 4px 0; + width: 13px; + height: 12px; + overflow: hidden; +} +.nivo-controlNav a.active { + background-position: -8px 0; + margin-right: -1px; + margin-left: 6px; +} + + +/*-----------------------------------------------------------------------------------* +/* = HP Highlights +/*-----------------------------------------------------------------------------------*/ + +#home-highlights{ + margin: 0px 30px; +} +.hp-highlight{ + float: left; + width: 215px; + margin-right: 20px; + margin-bottom: 20px; +} +.hp-highlight p:last-child{ + margin-bottom: 0px; +} +.hp-highlight h2, +.hp-highlight h3{ + font-weight: bold; + font-size: 14px; + margin-top: 0px; + margin-bottom: 15px; +} + +/*-----------------------------------------------------------------------------------* +/* = Portfolio +/*-----------------------------------------------------------------------------------*/ +.no-margin{ + margin-right: 0px !important; +} + +ul.filter{ + list-style: none; + margin: 40px 0px 15px; +} +ul.filter a{ + display: block; + float: left; + margin-right: 5px; + margin-left: 0px; +} + +/*filter links design*/ +ul.filter a, ul.filter a span { + display: inline-block; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +ul.filter a { + white-space: nowrap; + line-height:1em; + position:relative; + outline: none; + overflow: visible; /* removes extra side padding in IE */ + cursor: pointer; + border: 1px solid #999;/* IE */ + border: rgba(0, 0, 0, .2) 1px solid;/* Saf4+, Chrome, FF3.6 */ + border-bottom:rgba(0, 0, 0, .4) 1px solid; + -webkit-box-shadow: 0 1px 2px rgba(0,0,0,.2); + -moz-box-shadow: 0 1px 2px rgba(0,0,0,.2); + box-shadow: 0 1px 2px rgba(0,0,0,.2); + background: -moz-linear-gradient( + center top, + rgba(255, 255, 255, .1) 0%, + rgba(0, 0, 0, .1) 100% + );/* FF3.6 */ + background: -webkit-gradient( + linear, + center bottom, + center top, + from(rgba(0, 0, 0, .1)), + to(rgba(255, 255, 255, .1)) + );/* Saf4+, Chrome */ + filter: progid:DXImageTransform.Microsoft.gradient(startColorStr='#19FFFFFF', EndColorStr='#19000000'); /* IE6,IE7 */ + -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#19FFFFFF', EndColorStr='#19000000')"; /* IE8 */ + -moz-user-select: none; + -webkit-user-select:none; + -khtml-user-select: none; + user-select: none; + margin-bottom:10px; +} +ul.filter a:hover, ul.filter a.hover { + -moz-opacity: 0.8; + -khtml-opacity: 0.8; + opacity: 0.8; +} +ul.filter a:active, ul.filter a.active { + top:1px; +} +ul.filter a span { + position: relative; + color:#fff; + font-weight: bold; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.25); + border-top: rgba(255, 255, 255, .3) 1px solid; + padding:0.8em 1.3em; + line-height:1em; + text-decoration:none; + text-align:center; + white-space: nowrap; +} + + +/*filter-colors*/ +ul.filter a { + background-color: #666666; +} +ul.filter a { + background-color: #D5D2D2; + text-shadow: 1px 1px 0px #FFF; +} +ul.filter a span{ + color: #242424; + text-shadow: 1px 1px 0px #e7e7e7; + border-top: rgba(255, 255, 255, .6) 1px solid; +} +ul.filter .active a{ + background-color: #333333; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.25); +} +ul.filter .active a span{ + color: #fff; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.25); + border-top: rgba(255, 255, 255, .3) 1px solid; +} +ul.filter .active a:hover { + -moz-opacity: 1 !important; + -khtml-opacity: 1 !important; + opacity: 1 !important; +} + + +/*portfolio items*/ +.portfolio-item{ + position: relative; + float: left; + width: 300px; + margin-right: 10px; + margin-bottom: 10px; +} +.portfolio-item img{ + padding: 4px; + border: 1px solid #ddd; +} +.portfolio-item-details{ + text-align: center; + margin-bottom: 10px; +} +.portfolio-item-details h2{ + font-size: 12px; + margin-top: 0px; + margin-bottom: 5px; +} + +#portfolio-wrap{ + overflow: hidden; + margin-bottom: -15px; + margin-right: -10px; +} + + +/*single portfolio*/ +#single-portfolio{ + margin-bottom: 20px; +} +#single-portfolio-left{ + float: left; + width: 510px; +} +#single-portfolio-left img{ + margin-bottom: 20px; + padding: 4px; + border: 1px solid #ddd; + -webkit-transition: opacity 0.2s ease-in-out; + -moz-transition: opacity 0.2s ease-in-out; + -o-transition: opacity 0.2s ease-in-out; + -ms-transition: opacity 0.2s ease-in-out; + transition: opacity 0.2s ease-in-out; +} +#single-portfolio-left a:hover img{ + -moz-opacity: 0.8; + -khtml-opacity: 0.8; + opacity: 0.8; +} + +#single-portfolio-left img:last-child{ + margin-bottom: 0px; +} +#single-portfolio-right{ + float: right; + width: 380px; +} +#single-portfolio-meta{ + margin-top: -10px; + margin-bottom: 20px; + font-style: italic; + font-weight: bold; + color: #666; +} +#single-portfolio-meta a{ + margin-right: 5px; +} +#single-portfolio-nav{ + margin-top: 20px; +} +#single-portfolio-nav a{ + float: left; + margin-right: 10px; +} + + +/*-----------------------------------------------------------------------------------* +/* = Posts & Pages +/*-----------------------------------------------------------------------------------*/ +.post{ + float: left; + width: 670px; + overflow: hidden; +} +.full-width{ + float: none !important; + width: 100% !important; +} +.loop-entry{ + margin-bottom: 30px; + padding-bottom: 30px; + border-bottom: 1px dotted #ccc; +} +.loop-entry-left{ + float: left; + width: 150px; +} +.loop-entry-right{ + float: right; + width: 490px; +} + +.loop-entry-thumbnail{ + margin-bottom: 20px; + padding: 4px; + border: 1px solid #ddd; +} +.loop-entry-thumbnail img{ + display: block; + margin: 0px; + -webkit-transition: opacity 0.2s ease-in-out; + -moz-transition: opacity 0.2s ease-in-out; + -o-transition: opacity 0.2s ease-in-out; + -ms-transition: opacity 0.2s ease-in-out; + transition: opacity 0.2s ease-in-out; +} +.loop-entry-thumbnail:hover img{ + -moz-opacity: 0.8; + -khtml-opacity: 0.8; + opacity: 0.8; +} +.loop-entry h2{ + margin-top: -5px; + margin-bottom: 10px; + font-size: 16px; +} + +.loop-entry-date{ + text-align: center; + border: 1px solid #ddd; + background: #eee; + font-size: 14px; + padding: 5px; + font-weight: bold; + margin-bottom: 10px; +} + +.loop-entry-author, +.loop-entry-cat{ + text-align: right; + color: #999; + font-style: italic; +} + +.loop-entry-author a, +.loop-entry-cat a{ + font-weight: bold; + color: #999; +} +.loop-entry-author a:hover, +.loop-entry-cat a:hover{ + color: #666; +} + +.entry { + margin-bottom: 20px; +} +.entry ul, +.entry ol { + margin-left: 30px; + margin-bottom: 10px; + list-style: inherit; +} +.entry ol{ + list-style: decimal; +} +/*thumbnail*/ +.post-thumbnail{ + margin-top: -10px; + margin-bottom: 10px; +} +/*post meta*/ +.post-meta{ + color: #999; + font-style: italic; + padding-bottom: 10px; + border-bottom: 1px dotted #ccc; + margin-bottom: 30px; +} +.post-meta span{ + margin-right: 10px; +} +.post-meta a{ + font-weight: bold; + color: #999; +} +.post-meta a:hover{ + color:#666; +} +.meta-date{ background: url(images/date.png) left no-repeat; padding-left: 17px; } +.meta-category{ background: url(images/category.png) left no-repeat; padding-left: 17px; } +.meta-author{ background: url(images/author.png) left no-repeat; padding-left: 17px; } +.post-meta-single { + margin-bottom: 20px; +} +/*post tags*/ +.post-tags { + margin-right: 10px; + margin-top: 30px; + font-size: 12px; +} +.post-tags a { +} + +/*-----------------------------------------------------------------------------------* +/* = Pagination +/*-----------------------------------------------------------------------------------*/ +.pagination +{ + font-family: Arial, Helvetica, sans-serif; +} +.pagination a, +.pagination span +{ + font-size: 11px; + line-height: 20px; + height: 20px; + width: 20px; + text-align: center; + margin-right: 5px; + display: block; + float: left; + background: #FFF; + color: #666; + border-bottom: 1px solid #cdcdcd; + border-right: 1px solid #cdcdcd; +} +.pagination a:hover, +.pagination span.current +{ + text-decoration: none; + color: #FFF; + background: #333; +} + +/*-----------------------------------------------------------------------------------* +/* = Comments +/*-----------------------------------------------------------------------------------*/ +#commentsbox { + width: 100%; + overflow: hidden; + margin-top: 30px; +} +#comments{ + text-transform: none; + font-weight: bold; + color: #333; + font-size: 13px; + margin-top: 20px; + margin-bottom: 30px; + padding-bottom: 10px; + border-bottom: 1px dotted #d6d6d6; +} +#commentsbox ol, +#commentsbox ul { + list-style: none; + margin: 0 !important; +} +#commentsbox li { } +.children { + list-style: none; + margin: 30px 0 0; + text-indent: 0; +} +.children li.depth-2 { margin: 0 0 0px 65px } +.children li.depth-3 { margin: 0 0 0px 65px } +.children li.depth-4 { margin: 0 0 0px 65px } +.children li.depth-5 { margin: 0 0 0px 65px } +.children li.depth-6 { margin: 0 0 0px 65px } +.children li.depth-7 { margin: 0 0 0px 65px } +.children li.depth-8 { margin: 0 0 0px 65px } +.children li.depth-9 { margin: 0 0 0px 65px } +.children li.depth-10 { margin: 0 0 0px 65px } +.comment-body { + position: relative; + padding: 15px 15px 0; + margin-left: 65px; + margin-bottom: 30px; + background: #ffffff; + border: 1px solid #EBEBEB; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + transition: all 0.4s ease; + -webkit-transition: all 0.4s ease; + -o-transition: all 0.4s ease; + -moz-transition: all 0.4s ease; +} +.comment-body p{ + margin-bottom: 15px !important; +} +.comment-body:after { + content: ''; + position: absolute; + top: 10px; + left: -12px; + width: 12px; + height: 20px; + background: url("images/comment-arrow.png") no-repeat; +} +.comment-body:hover{ +} +.reply{ + display: none; + position: absolute; + top: 10px; + right: 10px; +} +.comment-reply-link { + font-size: 10px; +} +.comment-reply-link:hover{ +} +.comment-body:hover .reply{ + display: block; +} +#commentsbox .avatar { + position: absolute; + top: 0px; + left: -65px; + height: 40px; + width: 40px; + padding: 2px; + border: 1px solid #eee; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.comment-author { + font-weight: bold; + color: #000; +} +.says { display: none } +.comment-meta{ + margin-bottom: 10px; +} +.comment-meta a { + font-size: 11px; + font-style: italic; + color: #666; +} +.cancel-comment-reply { + margin-top: -10px; +} +.cancel-comment-reply a { + color: #F00; + line-height: 20px; + height: 20px; +} +.cancel-comment-reply a:hover{ + text-decoration: underline; +} +#comments-respond { + clear: left; + text-transform: none; + font-weight: bold; + color: #333; + font-size: 13px; + margin-top: 0px; + margin-bottom: 15px; +} +#comments-respond-meta{ + font-size: 11px; +} +#commentform label { + display: block; +} +#commentform input#author, +#commentform input#email, +#commentform input#url { + width: 45%; + color: #666; + text-shadow: 1px 1px 0px #FFF; + border: 1px solid #cecece; + outline: none; + padding: 10px 5px; + margin-bottom: 10px; + -webkit-border-radius: 1px; + -moz-border-radius: 1px; + border-radius: 1px; + background-color: #fff; + -webkit-box-shadow: inset 1px 1px 4px rgba(0,0,0,0.1); + -moz-box-shadow: inset 1px 1px 4px rgba(0,0,0,0.1); + box-shadow: inset 1px 1px 4px rgba(0,0,0,0.1); +} +#commentform textarea { + display: block; + padding: 10px; + width: 95%; + color: #666; + text-shadow: 1px 1px 0px #FFF; + border: 1px solid #cecece; + background-color: #fff; + -webkit-box-shadow: inset 1px 1px 4px rgba(0,0,0,0.1); + -moz-box-shadow: inset 1px 1px 4px rgba(0,0,0,0.1); + box-shadow: inset 1px 1px 4px rgba(0,0,0,0.1); + -webkit-transition: all 0.1s ease-in-out; + -moz-transition: all 0.1s ease-in-out; + -o-transition: all 0.1s ease-in-out; + -ms-transition: all 0.1s ease-in-out; + transition: all 0.1s ease-in-out; +} +#commentform input#author:focus, +#commentform input#email:focus, +#commentform input#url:focus, +#commentform textarea:focus { + border-color: #acacac !important; +} +#commentSubmit { + font-size: 12px; + margin-top: -5px; + margin-bottom: 20px; + outline: none; +} +#commentSubmit::-moz-focus-inner, #commentSubmit::-moz-focus-inner { border: 0; padding: 0; } +#commentSubmit span{ + display: block; + margin: 0px; + padding: 10px 15px !important; +} + +/*-----------------------------------------------------------------------------------* +/* = Sidebar +/*-----------------------------------------------------------------------------------*/ +#sidebar { + width: 220px; + float: right; +} +.sidebar-box { + padding-bottom: 20px; + margin-bottom: 20px; + border-bottom: 1px dotted #ccc; +} +.sidebar-box h4 { + margin-top: 0px !important; + margin-bottom: 15px; + text-transform: uppercase; + font-size: 12px; +} +.sidebar-box ul { + list-style: none; +} +.sidebar-box li{ + margin-bottom: 5px; +} +.sidebar-box a{ + color: #666; +} +.sidebar-box a:hover{ + color:#ef4034; +} + +/*-----------------------------------------------------------------------------------* +/* = Copyright +/*-----------------------------------------------------------------------------------*/ +#footer{ + background:#000; + padding: 20px 30px; + margin-top: 30px; + margin-left: -30px; + margin-right: -30px; +} +#footer a{ + color: #ccc; + border-bottom: 1px dotted #ccc; + text-decoration: none; +} +#footer a:hover{ + color: #fee825; +} +#copyright { + float: left; + width: 820px; + font-size: 10px; + color:#666; + text-transform: uppercase; +} +#back-to-top{ + width: 100px; + float: right; + text-align: right; +} + +/*-----------------------------------------------------------------------------------* +/* = Search Bar +/*-----------------------------------------------------------------------------------*/ +#searchbar { + display: block; + position: relative; + width: 220px; +} +#search { + position: relative; + width: 180px; + padding: 10px 30px 10px 10px; + outline: none; + border: 1px solid #ddd; + color: #666; +} +#search:focus { } +#searchsubmit { + position: absolute; + right: 10px; + top: 12px; + background: url(images/search.png) no-repeat; + text-indent: -9999px; + border: none; + outline: none; + width: 15px; + height: 15px; + cursor: pointer; +} + +.search-portfolio-thumb{ + float: left; + margin-right: 20px; + padding: 4px; + border: 1px solid #ddd; +} +.search-portfolio-thumb img{ + margin: 0px; + padding: 0px; + display: block; + -webkit-transition: opacity 0.2s ease-in-out; + -moz-transition: opacity 0.2s ease-in-out; + -o-transition: opacity 0.2s ease-in-out; + -ms-transition: opacity 0.2s ease-in-out; + transition: opacity 0.2s ease-in-out; +} +.search-portfolio-thumb:hover img{ + -moz-opacity: 0.8; + -khtml-opacity: 0.8; + opacity: 0.8; +} + +/*-----------------------------------------------------------------------------------* +/* Main Shortcodes +/*-----------------------------------------------------------------------------------*/ + +/*columns*/ +.one-half{ width:48%; } +.one-third{ width:30.66%; } +.two-third{ width:65.33%; } +.one-fourth{ width:22%; } +.three-fourth{ width:74%; } +.one-fifth{ width:16.8%; } +.one-sixth{ width:13.33%; } +.one-half, .one-third, .two-third, .one-fourth, .three-fourth, .one-fifth, .one-sixth { + position:relative; margin-right:4%; float:left; +} + + +.column-last{margin-right: 0px;} +.column-first{margin-left: 0px;} + +/*box shortcodes*/ +.box-shortcode { + margin: 5px 0px; + padding: 10px; + color: #fff; + font-size: 13px; + font-weight: bold; +} +.box-black { + background-color: #000; +} +.box-red { + background-color: #e62727; +} +.box-green { + background-color: #91bd09; +} +.box-blue { + background-color: #00ADEE; +} +/*highlights*/ +.text-highlight { padding: 2px } +.highlight-yellow, +.highlight-yellow a { + background-color: #FFF7A8; + color: #695D43; +} +.highlight-pink, +.highlight-pink a { + background-color: #F7DEEB; + color: #724473; +} +.highlight-purple, +.highlight-purple a { + background-color: #E0DBF6; + color: #5C5577; +} +.highlight-blue, +.highlight-blue a { + background-color: #D7F0FF; + color: #2A67A4; +} +.highlight-green, +.highlight-green a { + background-color: #E7FFCE; + color: #47630A; +} +.highlight-red, +.highlight-red a { + background: #FFCEBE; + color: #A22121; +} +.highlight-gray, +.highlight-gray a { + background-color: #EBEBEB; + color: #787777; +} + +/*Other buttons*/ +.button{ + margin-right: 5px; +} +.button, .button span { + display: inline-block; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.button { + white-space: nowrap; + line-height:1em; + position:relative; + outline: none; + overflow: visible; /* removes extra side padding in IE */ + cursor: pointer; + border: 1px solid #999;/* IE */ + border: rgba(0, 0, 0, .2) 1px solid;/* Saf4+, Chrome, FF3.6 */ + border-bottom:rgba(0, 0, 0, .4) 1px solid; + -webkit-box-shadow: 0 1px 2px rgba(0,0,0,.2); + -moz-box-shadow: 0 1px 2px rgba(0,0,0,.2); + box-shadow: 0 1px 2px rgba(0,0,0,.2); + background: -moz-linear-gradient( + center top, + rgba(255, 255, 255, .1) 0%, + rgba(0, 0, 0, .1) 100% + );/* FF3.6 */ + background: -webkit-gradient( + linear, + center bottom, + center top, + from(rgba(0, 0, 0, .1)), + to(rgba(255, 255, 255, .1)) + );/* Saf4+, Chrome */ + filter: progid:DXImageTransform.Microsoft.gradient(startColorStr='#19FFFFFF', EndColorStr='#19000000'); /* IE6,IE7 */ + -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#19FFFFFF', EndColorStr='#19000000')"; /* IE8 */ + -moz-user-select: none; + -webkit-user-select:none; + -khtml-user-select: none; + user-select: none; + margin-bottom:10px; +} +.button.full, .button.full span { + display: block; +} +.button:hover, .button.hover { + background: -moz-linear-gradient( + center top, + rgba(255, 255, 255, .2) 0%, + rgba(255, 255, 255, .1) 100% + );/* FF3.6 */ + background: -webkit-gradient( + linear, + center bottom, + center top, + from(rgba(255, 255, 255, .1)), + to(rgba(255, 255, 255, .2)) + );/* Saf4+, Chrome */ + filter: progid:DXImageTransform.Microsoft.gradient(startColorStr='#33FFFFFF', EndColorStr='#19FFFFFF'); /* IE6,IE7 */ + -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#33FFFFFF', EndColorStr='#19FFFFFF')"; /* IE8 */ +} +.button:active, .button.active { + top:1px; +} +.button span { + position: relative; + color:#fff; + font-weight: bold; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.25); + border-top: rgba(255, 255, 255, .3) 1px solid; + padding:0.8em 1.3em; + line-height:1em; + text-decoration:none; + text-align:center; + white-space: nowrap; +} +.button.black { + background-color: #333333; +} +.button.gray { + background-color: #666666; +} +.button.light-gray { + background-color: #D5D2D2; + text-shadow: 1px 1px 0px #FFF; +} +.button.light-gray span{ + color: #242424; + text-shadow: 1px 1px 0px #e7e7e7; + border-top: rgba(255, 255, 255, .6) 1px solid; +} +.button.red { + background-color: #e62727; +} +.button.orange { + background-color: #f24919; +} +.button.magenta { + background-color: #A9014B; +} +.button.yellow { + background-color: #ffb515; +} +.button.blue { + background-color: #00ADEE; +} +.button.pink { + background-color: #e22092; +} +.button.green { + background-color: #91bd09; +} +.button.rosy { + background-color: #F16C7C; +} +.button.brown { + background-color: #804000; +} +.button.purple { + background-color: #800080; +} +.button.cyan { + background-color: #46C7C7; +} +.button.gold { + background-color: #D4A017; +} \ No newline at end of file diff --git a/app/static/img/madam-300x250.jpg b/app/static/img/madam-300x250.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4613ea3c170247fb1c90e13bcfc4fb93d3355697 GIT binary patch literal 9456 zcmbW72T+qwzwd)ILAnGX6sb}INDEaEM7p#93B4B~w9tzny@PZlROu}gA#@PwRRltS zP?X+Ls)GLY`hU-R&OPrr_s*Pq_nGhP&a?YGJD>UO&d$#F_j~d8X8?_+s)j0nfPesC z{pSJv{soXzhN>tT>Oy7No$Q5NJRO|a{X~UD*!g_Do!y<-H8j=OHT6`~*!?8M_yyUK zzBaDTK7s7=?9yW7zn1~Z01_f1Vj?0EVq#(v5)x7}Dl)R$x5?-zDaomTbPNnYI(qth zAP$y$jQ5%7=~;PM?{jkT@bECO@C)*BgE_c)xc~kM!JkjbZj;fHk<oH9(lc`ZKbPNq z0NUGt8v;Q>0#3j!S^`2^g5QIH`v3p|@gLN`Y5!RWZV?g@laSsfBfs-!0EPx|i-3^u z77-!wABI1pL;mywh-iuFI7F35?&{l+aw6%)!qeW|=6cff9tfTJ$}Mi|6+uSMaF3A* z#Pfick6%CnA}J*;Bdh#WMO95*L({;}2nIJcF}1UIaCCa^?Bea?>*pU37!(;59TOWD zpOBuBnU$UMIu}(^T2@|BSyf&0wz&n<+ScCD**`EiG(0joHa<ItoyRRKF5%ZVHn+BS zcK7xVPS4ISF0a0QzrOjKivU3QZ&?39_FuSY|8U(RA|fOr{hN#6mj53S(h?DKh?3AL z>66+Z?{bQT-==?(_NM7Q8J9TpE6~<!hMa+0Vx8yoZ?u1q{m;N6{=dln1MGiu;Q<tc z1b>1@NDFujxb98hYiNAbSCYU7`0vsLnDR>n&$^fU$WfK#PMazhV)G*g&XsI*5@wx4 z4@B>4raCw0ki+krmNQM*^YWpWNDiQj=tK{y70q0uDV~K`cN;>!m*fXG(V=E%um5t) zI`+QrS^mLg67`~~(IRRW|0aO#VRsq)(Hp)l4N$epkz|5yxsZB)0$=g1zm=Yy0N^Y6 zH))#w^X0#H{-fq6yA{Ten#y1P7eAEA;St9OTDi!!j=6a@Q+4VCrStxcW-CZ1q}HhA z`=FXj!hU&7=A@I6n!#&hZa+d`phTdB6nU_!Z98ErqPPt(D1B@;`@RD2Xi)zFrsWlD z!-7l6C`S*5HI4!uZ$^9yt%2olN;n9j{ni@=yuM|}0Gl2)ee5Dm(yJm+tZFzic<#%R zfK4NmVioAw5DBkcsEm%1R0BSHT9$aj`c}h5dZ%S==r()R$ZTV`R27l;UC`ckf%@lJ z@2n3#Ys0K3X&&h>Fr@qDSCtiN0wQ?n`W*e<WRle-JA89RqKJ6ofw=KTV@->Do9x%` zS)}7vj}8Ko#Kbn=42gSAYtHpXzCfpx&EoflGG1hzL#oE}<b!<a({j2{zUJL?Q6^wR z1$${oL|S?5j5?LG%OJhJ3gG`L=-q@Dv=S#kH3<S{5YP+ZPRRzb%6IoLb-)#EClznh z(C06fD;SDv&kol13?p>3p2i0O_&e>x>G>fpTVJ50nqRW%Xv?RtJIR4E%Wq7_4Oa|$ zEYtdj5k*uVSJJ7O&@#M2r1C?));qJErumoi7p|qEl)`Fg*QILFguVlfZ|Gy5?$KDZ z&-#6NHE$N(t~(_I107T@bv(tc8?B~G3HIdib>~bgB8XY%$5H2-`bT4R9_fYxtIj!I zFOtOZ&DN?Zht_C*w?tlrOmKVec16kM;&93~dV8&s2sCdIxSeSIW2DD^%H{niAs58) z!!#+fD#;-jq?^7VD5%SIJxk||PbuMyoghu;cXv9o;hihD{kZ_$4(Ej!@lKqKAPpjd zdg4mMrWH1%(Yk^}ge>~>3dX~0E^ggOT>Ex}cgGBiX?%1|HI$x8`zgxv`PT{fZgk<X z_eapX?TWSe4Wse0t!tCLx#8ip)1fspW)o3-DQpGoWRj$=L#o+-R`x`PirCYzQb1>N zKPqzuLO<_%S*cer{3m9j#Ps@#|00Q25mHlwj6Nnv=zx~j)3?;cRhj4#evZ~4^Ni## zeHUmjLZvMOV6<!vSWcqV?ywU40^GO$dYb)KMidB|VUSF^MGP+Kwx<!Og9i==8O3A- z{VatX^d}_cg%8Q5XwHgFpKNi2xE#2r`JJO|TIJfZ4oxNfqLMaIYgzYqL~D(Be4JS2 z;n{~e=`>B@rWM$xEuX5L7U6?pSV|KAYAG<Tr`|!iA`o2|ff17U78cPl<g8}hC~&gT zFq}pV@HRQ0i|BrK0MESGlkI<;>A_TT#sfGV(q`i8=3w{PD9vWB{i;t>Quw{zHxAx2 z*(%~T1Ny;Rfq?m}_Fq)21bA)G3s&;(8*;|#aStc#wrQ?0My;Mm40a{kF^%4;6~e21 zG1dDXy0hIZ2THf1taD2)!@V<sV}?6|^8dmfdj>F?n$a`yvMfCHL4v~Qjilz1d;0o+ zqIDp4!L0_hCrg~SDxIs^Tn!2_jX1fxm8O&LA{}XbAq}nC7EB|};{7eN4EhFl0@a7L zUG}d8XzUF$p3*iF^Ng?NHnfCLFiCt|ff#hm&eN|4Ehuql|GcX4r`(~d!9>4+2sCX6 zqx1(qh)LHjdwcNeW?~Ve=n%)5i0l!~A%hgmS$l#`GMw;JsR3R|Op{-B7R&H2`*zsX z;Pq#tL?KD7?lvXpm$x86XV@W^hr9Z|1qI(dLRJJF9*@wtx<Wgq4!?<S&?Si8&B)J= z+Z=>%n>VdA+K~T<4MM*Vdwpmy^tiB{)x{n|`E6J`SiiraxoKCI=GRP)2bnc}3Z@D8 zc8E<BTAl*;YT#_)hzE(>2fQ*}lAe<hz<DoDh%)pIxtN$my$YtF@ULc1kqk*4*^#OT zYf{pgH0>&oepB(b6-C%P1izlu?SB0^shLgO6r~fV_QjFOLvz379rbcFKvkg~&HFY6 zF9rZ^!$vAkO5ANJSsipyqSpqWXE{~hC$Yfr!~ws|4c&qCN$2LecX^9D;x^{2eSbvM zjg<8PhKUtZ6aj3Mx*eH+9SlQrRC&F?1qzZp0G2tS3vd8-*Uc0zGgiTzlrBq7zx&q4 z&j{p$d^b)aaA$(}Q@V8R)>cZ$J}>d`38u_t0`-mcL(sv3^9N~Q7C*J{YR8Z-UEhLY z6(j4ElXf|`6JKEcjd4m+*<^01AOMo)O^(?!_y||_^$@7Wiy;cKWr)Yl35ZZ%N{!@i zJ!^F^hI54pM19*)<%ICO0(#;lO1V$9Sj&)&i`_R1&>WisSkqi2>o=nGV57v^=d__u zZ=vJ*&-Jy*?49I^r7^*oUN=+i>73p63%p3{Q@{GoFTR5#tl}DyDN|&QtSa6cUsDIW zF6F*1HrS>+(MxlKe2uYnc%0q7QFudMKC6th)|+EhO|9HJD)dlBeHD6uz6uu$Ilzne z^+9h%XI=o22)mwW!j4dBj;AkFHE2Q2A%N+r2d-1(BI~IGw315*sCr+7scI{{%$x$1 zM_9E}XUai2dT6B-RmzPPdIbiW%8hvSL{X%#XTCyvC_75aJI4cT#RRYhiQX~SuN1+i zSu()1T}|^3M;OHO;_XI2rAleuVfqjCxKNDkhuPq|NAEaa`r|r<6a*=2nfe-dY2!nP zmaKGV?2>9~2!;?U73}P&HaAy826z5K9~YfnLf|I0fxm|$kJ~VX=1D%SNt;wviqUIp zoK+lTtC<uLZXP|{<sWbzZTtgu)W3aR)sc&gbTSie12)r`+Z`Gf4KeJE2!qqI`ODoF z?p_}8CsGi~Q2g5IVi9RsEf><+bGWo`u&eR_KGmfnRxcD#WA|VQUf|(eb{ol7y1~A* z4`0~!SH7$<ct3Vl@%#*5ZT`jjRKgwEZun_gWH*Q9$3kuoX3Oohu;DA^AxJ$&y^$jL zulHqdqJlMty(SLoW2?<vPMb5@L2ZAPMA>n6znwGgO<G5<gs50r1RW@tu!I7w^r9}K zQUcd^S!ebV{c3tt1jd;23orYXHuMHbxkC^^%ZACHDZ5bP6(o9M$R-SuJc`>INBmP@ zk~NkcSB@FW)zM|FkxjTSlp+T$$+vjT*^ph6%Mxi+@Wf7j%DA}&r4=$6Q2pmXBvE5u zu&v(FH+(ZfQhgxa0^8yER+KR3BYKW+wo7ifr148nKsd+~G@;y6DCD+Ckz25^Ib5(G zDaLIXU!_07Shp+pkd1pU7-qTjNl}{muCuj!-xi!*uih{Pehsfh+?Ztwt0JZ$U>o9L z<dC<c1W>(Z=2P{d!{x=5>*S%9=0sD%4HTXd^hSkVk3b7_ogol}dg6jmN14Q|m4wNv z+o+cq0<6hVVCmH}60*9{dZ+M+(OfIj`PKppWh%;G8T*HFg`tvu$>DBg8ckU8kI|k= zT#V&y7K3-!>WiVG?;G5Hbg^-lZE;!f^{YzBpxvZY932V`I&O|H>I&EhcqY>u*LCh4 zTHw+=&Q9DBqeJtaPkXm}z}j~SipJYY+uGhfOC_k$(`=ncCARw>IVg%B=~V;3!}$yc zb+cTZ>T=6#UA`*awdt!TH9QrZ9kp#CUdH+WHFvId&AgUTV4F|43?JC(wPzJuWk-Wc z3SBk>%e?=QzfGf8_1=@2mddH_)bZbd)#+O$>9(H)C~Nlw3M=cDo%?c&9^xSfh%D?Q zCI1(i!)wp1RxK+}8{~k=)h1qQDqF!9BG^4N?d-~dUv}<!J<a`-6m7?ejjrcsoH1(O zc!3kW-Wk&!74`efF1Iv}z|m@11&<b_woaYxqA~iH5)Ttif|g(PVPR~H*_+gIaXE86 zu4={mev50%)7AB;0D)Qa)A*#?_X@C4H_QxjEGlXKySvc`C9XxR*mSumh0QzS8gB$X zC}hz4I~-yP<N|9O<A9v=b5cY}B;HV-0U!5J%#_MTfbx1;1a2NZ2}j0rT93O}XuMw& zEX!Ew9HSW6O>)Be)?L?^&z&&HZw-z_qS`ChYFtv|>&hMv7Kt<{-$5BBWm;Bk4O&-! z5Lz~<oy90^6LjSpE}$ab(spHU0{fjoZvw(r-ycRN3Tnw5u;Hb$!)b6~H>W;2-+ z<q(Ll)aa{n6J^QU^==Se>GvBgunR#28+l?oLAiL8!vsz&jl#SuG^?`Qxd}fkMj(GE zHE<woQqP>O3!ELey&@I>nTDFzW-^Q0XQit?1!h-s%f{xie_nuJ5r1l@z=gIs{K(); zq5(6GnQOehS*gJth15=B+P^U<WbMW9_Jn7>B23uAOi4|wfP0ey_uE++1JAU@<MSN| zQ%WQCBT+q&-qpiJoag7iz`HI=b9sph>2y%|(@ZGzqbPGZ;yIF^;;$Q+#=gPg>XhrX z5s$8^h1$mh7*DEo0;~-jqgm@#t;R_aL1~qu_}R9%l7R7tf@NiVVrIvQd4y7vG6kre zCz-21zspkqY@D^Q%c3WQyR)ahtpUWCNYn*ZYM1UivuNczRoAD_KV=65)M+vJ+q{2| z?(iF8l<#XO8X9YJ)H+hDcR}u7xq(FhewPgjjzwneQc_WMfS>V)Fic)GB*fTs>ss^- zJCUp4RwGi|cdKnDg(S8mXsj@WknGsOzcFJR_~gN@6aaAQ@D~Rt@Jwjbd35VHzz+kt zli}gU-P0%CAcgw8qI1?UAvy+G#}1Bnca%++BtwYp6*RcpGbhBey~iG;E%H^|(seb6 za^vV@0%{?i=IlJRca%2A?YsiSCGk1uz5C{p)my|hTFhU+*=Kn#2a3gUmThK3mLvPM z^6*CA^~Vy2RB2vxp?)!b^<8^A5J7+Wpo{U>y?rG0MPl;Q3KgPZ?uetYjp3uMgwl^C zu)^-3subA4sS_gouuY~R<wN^MSN+#(9|Qfvk4cs6LyrE{_Z(I>Aw|(5V|NvbL|(ik zYY<rImKV+yAK`W|2}-!J)9OkzjZYZnEKm*DgbcrU_H}%1KhhvUwD)oLGpgRpg@Jsy z;CStOJ3HqKN8=+ZE`ho?Myukx-eFsywXbzU?)H(7!NQ8)YGjj91EQnXx|J>sZb+f% ze)FgPZt@|BWwgdzUevT^J~lmFu2F@%?wwPPzjnpj^;6cUQKqGm(8Gl3dCWSJDPoy} z8YX{ONZb5zW<vW}`$M)hWla0jbNoTleYs|1*Qg7Lr;`<3Rdy~=XCIjZn{|kH2L?-< zO4na08@ZB3;RH4}UCk%RhYi1~;-(9m6bpjCQx-{fEES^WY$Z*T;hOE1w|S%=;m~)O zun(0fP+cP6>=3#KQ`5VXuz#_P@w?VRuoykab9z1FbbN;Y`L=-#1D?!DMUHY7NIZT{ zM4M)Ex4YBlwcazQi+aBPt&&83OuEVl(Zb{U;xsbge0P&YeZW{f!YX#+$-VL*X+%Yu z6QMuVk~hA|Q9Gj*T#O}<mq*YY-_C&SIvO6!mF{_FbT)I6O0-}r;f1-l{?0p$8#E6` zse#k;KeV3>0+}=jdmbFk5r#7J*_$5$u!-(7BKUv`6U(QEsYp~~8*NJ@>Ia3OZPn1F z!b@8IU^TbI)Mpd#fi6<#4K|E@Y%STyTcMcJE7l<gr5FQL($jQlf9&1`c9f76tofBB z7T}!(3w@nrq!Wbul+8krbZR9R7kW9k)NkT^-Q8s?vRiJ{+4g+#)FK&&`nbZhVcDZ% zSU7wqX5v=ze!@w5d16`dDoS3EK@>W3gq&Lpsd*5W6vJ2#`VHv+1`1i`QuWKoavy|u z?f<&p;>TPD{76miEJZ`y5pB=5(Y_t~em(18{AjNs&nN!e9&~41Q9yULt!0{YWrc>L z%{Daa_|w<;JxM1Y$ph+r*m}(DnC{YNBZXpka?-{m31$0x7o--@v|{Ad%YN#Y2<8cH zZdayZaMO1SKytZPI}&vvFShz{GgUJ@quil4O2X_^sMCgJD-2v86G$;ZCai`iO(|PF z=DZGDbv77b`fJBat*Tz@2$dv&n;3Lm`XEz}5eChAh>4~KHK27K9+$ZtyB9C4&kGp! z!3yN83@%(F<DB&d1Pt~JW&|@-F?Tgz1`LF+;9kZ(ytOYrJXR!Lg?8>f9P<~nPg$&T z_W>`5|0>)3m~b>IE+GBPc4&jGGQ42pa0lXETC#Y(JY-6rs%{FI^cXMNH_vZ{GhDk> zdIa6H{sz3y&cV&7A^$9#1C3z>A^f{WrTKCZcbxI7tnl&OwRigFHW{0anH8J7+iylR zjq4sgl=aB@cwHw}w7!$+y_|6H$m(<LJ1Wa==txV75-Vd+!cGu!wkICy*}N)W<lUR~ zeEyXL5xW8TZuk_qNT=mc=22Hkx~KX^#{o9UUn<FrW6Q7FHuVds^pL5s*8??6*+?Uy zb?2E}>eae^oLP#VtoB0_Q9#|#l*`5Es7*Bix@(f|3Yi3r$W*2JSWWDzXW3trArGio zL5EA&N3M<}YZtnn7;ID;RaBbZN}8~;9ct^RTrHDl-AcmVcD@)@AlgAW-IPs@KfZcI zzBX5AF`BhDAM2}eat~3c;&;zV!pRqjM+uC6CO<6Ixyb_7R}i0QM#%2G<#jvKwSi;q zB(acBI!$j{!W2rp-^w3JfRabUuajl-k|46(Wqu;)RR#RR=2Ec;U9=7Cq<{~ha>-(( zvb2(2kH9YveB=XqE<PRL@C>OH!#ToOU}a)*w9$~u*QN>O^hYnQgsBSDsv!yL4Q<Gc zP6SKl7LJ0`$n!<>E0>kIZM{&tk#d{3W|)kAlPw$9u*u^rkEJk0-u!tXF~;3jqJ<#> z%Ouf#%Pl*-FKG&n;?k-;o~MgK`OgA^WYn+KaCt<P+b@0@xmq-RzH4cIFeX=e&#Pvi z;R@Xm$XVgX92CZ{#b%Oh>jH}D>ti9`jpB{7>gKn+x#w;aHY!8E$3xYnzJ*iCZjWcu zN`W_({00o-L-PnSr5%SA8*(NCp45YB%GDhG<_4v~tib%YOU=<g>cCpZ2ybUhQwL)5 zLN~tuY#m*%FIm(%ziehc;tj}N9Ux(q2RGRVbXO!MpX4U1O+5^x^mFE3EJZE0%^0mG zmvd;;wFA>__{}#lQf8;zAp5kC-mNE$_I!^vdZ}8iUJY+p$a=aJwlcJWG6QQ|elW|T zv3Nr{Hyb?Q$Kuw4z%M>*;S(u&pHBA%k1Bj@gilv^tUwSpW=_p~Vw?5TDkRHX@ND;} z$)|3*dt5qJ*r&sRS;8BnT2S@9Ru6#;un~}=3EIGTJQA<{ZUQ>NZyw`n#3i07Sj(ls z*yho~21YZBf*=3ddv*E{Y3Bb+pt1Yw@zj<(gVJ5_;HN+{#*stIH7gc2vB*T&^NYhx z2aDkVpK={jh{xplH0ZhHFM*FgM*(7jj_S<|tB9Q`%b7Y4Hc93{8Mds8p`n|-LZ<UF zqR!>!=x_4iomaH0Jy$i}-wNKoylaK*$LyTN2g7}tAorC&yL?P$8AgL1lRJ7MuQHi7 zA)&tky3XucalJRbL%PVyvTPS4fc>6YP){X$`lpFU5h93%7$7c3d~rX^h?3mWKt4lb zrGgSVr#Ixl#n!#iYTF8Sld#Mux5e{d8qcXnO?Mh%JcB?_O43EQT7Cmm#NN?SX_Pg0 zodwMv$VR!wNWZ`Cn)6wPbUgV|P+D#Yde`hcf;6ls!^q0thd;N6!@gKdWj6z{Hf?}` zss?bt+R%I0IB2#7yQZL=51(HbovCIs%`0T%CHXQ%ICJ8WRXU8woOd8+%owX_Lez{a z1-%IjS`ZphvW%*oi;`dPPAOz_w>%SnsKng_(GJlzFZkx^Qv2B&K4lBZ5j~m1_v@<w z!;6hp#kSNqk~^hz(a>T#DiwgQNW1CB*6uqW0)bQbPvTXqX607V>T*l($S==+n!V}} zdi9to)&Q<%VN(UBxxIz!HzNc1N-NBd8n5u#{H(2+JNeq3PkzxvqwTGD>cCKt`{%B# zVXr8&*^Z8bZ0CplFb=+~_cT|v|A5q{NPYmnA03$fCPV`%r|t`OrUHQB9Gz`ngej4! z6WhX{hbv6jl)EPT>IGigk9x}1=3S5*G5hy;-C$?MY(?i#FWT-f8oYa~8F6+4iONXH z+*>cWP)>41;!`k4LJ22!IDbt*gtOFxvtTcTlVxvmSz3EN=)~=;=@PPUTN7f6(qFmw zD($O(uEF4jaV~9#os%b>AzEicxwI=Qu=%OdLxD5Q)dBmK_M#aY!=Y#A@&b#pzEWy+ zW-rWoU~WfDy3bf_J9Oy-`gcvcr07Pq>;O(-;0y}*n=)JD@O;!ebXCLZd)}4tLMt7y zj{TQZ-AKt?UwYTAYHRYHp8Zwf<I>L96ZMXf@)ut|Q{RRh%}jeP)1)e%Aoh;w#z;`v zdjU$rh@p*cjGZTP_th6_E+C%)I=~f<aZ1?`D@)3^ZTzY1I09==AnwDPWct^aADgcE z*oqb79>zbt0@Yd!=gnhZ^(|~(ICm>ZEn(-BFQ{&#{q-L}=Z{P0A4yXwTnN!#ah1Q= z4=HU4>NO6*u`kTxL>a#RjLu=+S|P18{rZx|e2A?9$-J)iTG2!EFYz#Qu+8DR%L~!0 znRTb?^=8e&iE7O|4rlmBUNS%yy*q)S`XfPwOxz}luD~Ogz|WkDeV&L5SwkBS-{*nN z)xr%>KDWMX<;8Oof812S*61>)CEyZWNl8JWc%AIMu;Hg*I@YOrBnOXuwKm3<&1z!N z{NMoC-OX_I`Sv;ss;6S8bt&u@3l<*|ySR~AXq=P*H8Fd<WYxN92`=U1^={ITPeA<2 zVcrds1{RjDdzD>Cqn#fl+}&1-2&om;&oxuP%*6Cy9DN0idHimn$13V3rKc3Tp-m5Z zi1pDap-N{H8Tv)9cT$CeW>W-M@(atkQ(OLNipcVt@>2DvxD3<`!gu8;?V+@3+L9P0 zL;+7>C9~A%rb1Si6Pj4B4rv(}A;S_P5YQmZ`52sWgs6|PgJx%U${eh77<kWR7$j9v zh`AsT#X9~8=Z3g?daq(^<lwzKi@$vL1aTSPOORT^AxsC4@o6PL&mQjTYj5NQq(-wB zp#u{S+o1e_;~J>>H2?LMEp?B|v;K(hTu5c%jL>s=lJnu2uqW-SM~I@{Q|-a6Uzd#` zhk4vrWini--E1c)E<t@i<rZo_$GrjNZu&%{?rH!d=LU%mysQ~7?M}ga>d)M*i7_Y8 zdz<BgS|;$+l^CIHzo_I1sNg<2V&B(kwfKgt-ZM6|&c`+?jLr1iu1?O+8WGApv7`%6 zS8r_?uOS5<2H797Xp(MPYJEm4S@N0NWdZlD+QzFpdmiDbycD$P^3rByOtAGz5KATc zVkLh@hdt)@x^|D%9U>O;rCitsWnbiozE$3-@RM5QWn~R6*RK0;VtviLCg|1pZ8w!N z%KQi~g{-rNWE-D%okRr_V8ZK#d4_}K;iG({mGn<&qh!$K4(SA!2XJ-$FH#B~U-@Nu zf%bjdre3^RbJ8qk>}XMi@7(S}Ud12u=;|gttXt$4!=a_)7tHr#3*OkDU$UJ%_|>?u z3o8x*7@g1$y2{P>RK_38sF~F4*Ll`R*OkwL#3ri<*>+lTjGSNMHxpvu_s0D?;H-)b zc0kRfUZD@5y9F)cmP54UV|;AwU$pI$kAJr75~7v;S?B8tZHpkJMqdm+_nJIFMlu~y zzF6Z-PsCn2>+IEk4K$!>%+J8$eDRqb4v-lwnX~t{VsZ-*--o$A$Ay=>(Aj5u*;UW< zNcc!4Ft|GV|D)^kpIsmhCi)gVLd8Yt@^ccI@(NkUjLya26XNz_k<Z^M+yq4U`yAg$ z8l~<1BEsn|MYv!8ad%LD72IKT-XogVhXX-HR}N>*EUG^tLA_bExBa%mRhdY7jvnD2 zq$l-q2fRKK?w*kws0jmDQu>$z_k7L@&b!J3^IS&lnYCT_Iv35N2fo!c1^PoC)CC%p zR>Z#9QWKd9q#k~Fr>oYoW*c5TVrSWSMrM~{?ZxO=9Z;&HZ8h*s)J~{zqxU}7WN7W{ zX)=g?KV|=%+K5+iThA?}vq;lVc;+mdu)Bp9z3g)o{?C#`-nSNgC?d(XC2M0(xDnJQ z({HF)rLHU0H>&i0sS`_6&6{uFMCm4-6?AtB*4Fj>E+W*|9d5GzC`BdMtAUwUJVl~S zpC`YphDqU?6<jc1SC*P<$INrTpVV~DBvFy^el8V1=(GB7fC#JXLRMnIYR1Y@SyufA z9c^exg%p(0)1}TS*d5|z<Pbc<-!}zIR=<H4Ft!}|NcpID2qK{dumTHxn5y6(%lgf~ zxO4wGPXDP*3DzVb7NV~c1($s6R(CUUt2ciA8xZ}j%ls0E%t-8hIvZS=!Fni7%^Yj+ zF%pKx$Z+^_#K|;>CY<d6m+3!5egqx*FwX^7Zs&=qdahm0v-G!s{5!=0g1lkRzZDS$ z#K;{Y5lHs2A00=CMigas29;>zLfCh2gLK@s8Ilol2vguhEK`F+H5d=gUmsOuxqKYa zSMazOo5e3(^f=&zn%ANOU`Mx8qpEJ?;j;IOXFnPEV+G*(vk2{ld&`{Yz<%+0LDfCq z&_#HpCu>ZTrL1{0{Q>D(jp|OYa*n~<20>)iiu#nafH^-@j2gJ1+H!C*ZIJbJIK+im z&Tv;=u}auC+lQY#Yx)#a>hH?lokgk`P-Zmb(@%NGC8Oa3y611{ypGgTaW6^Bvr(Zp zy8Z9j0RPh(Y&RgaG5cM0mv&q-{{|;duGwhqsTKsdsEUu#Q|hZ980egY)%Cu3qt@do zOHzg^NQFJ_O|pqaI}bDa^Clb**jn((x><-*sIo!YWT`<bONS;Y{J?O=(PK-v15tKR z(v^Xv%tjZC(Jo=@g;`#R4*y&X7&Q$vf@umpkEB-8gC$DX&81QooI@Gc<LG1w(h-uB zp-Y=2uC-Ka7-fS6%GuY	Z>a-sa=^oONj*x0VY0*-<+hq)%214fPcx{w3YvA1M$L zf1I1Y-JAa^)+#_r`m4W_Hu#E(OHwW}0WDB{#d|*JC)$#y51t_NMJ|DRNLlmYjA5@7 z@u9$%L0?{Q4(@Yt=d)evgCK>Hr1G^HrzOA6mb#81OKdUi??6QtaSAbcB;Gzy|B~V- NGyiQ$dV=3e{|jWbyC?tv literal 0 HcmV?d00001 diff --git a/app/templates/_formhelpers.html b/app/templates/_formhelpers.html new file mode 100644 index 0000000..5790894 --- /dev/null +++ b/app/templates/_formhelpers.html @@ -0,0 +1,12 @@ +{% macro render_field(field) %} + <dt>{{ field.label }} + <dd>{{ field(**kwargs)|safe }} + {% if field.errors %} + <ul class=errors> + {% for error in field.errors %} + <li>{{ error }}</li> + {% endfor %} + </ul> + {% endif %} + </dd> +{% endmacro %} \ No newline at end of file diff --git a/app/templates/add_bg_question.html b/app/templates/add_bg_question.html new file mode 100644 index 0000000..d01ac5e --- /dev/null +++ b/app/templates/add_bg_question.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{% block content %} + + + <h1 class="container mt-5 display-4 text-center"><br>Add background questions:</h1> + <br> + <p class="lead">Please paste the background questions followed by the list of options for that question separated by ; into the field below. + Mark the last option of the question with a /n after which you can again input a new question followed by a list of options. + <br> + <br> + <b>Here is an example input of two background questions:</b> + <br> + <br> + How many hours did you sleep last night?;6;7;8;9;10/n + Do you normally listen to audio books?;Yes;No + </p> + + {% from "_formhelpers.html" import render_field %} + + +<form action="" method="post" role="form"> +<div class="form-group"> + <label for="Background questions">Background questions and options:</label> + <textarea class="form-control" rows="15" id="bg_questions_and_options" name="bg_questions_and_options"></textarea> +</div> +<button type="submit" class="btn btn-primary">Submit</button> +<a class="btn btn-primary" href="{{ request.referrer }}" role="button">Cancel</a> +</form> + + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/add_questions.html b/app/templates/add_questions.html new file mode 100644 index 0000000..cea9d04 --- /dev/null +++ b/app/templates/add_questions.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} +{% block content %} + + + <h1 class="container mt-5 display-4 text-center"><br>Add sliders:</h1> + <br> + <p class="lead">Please paste the slider set questions followed by the left and right ends of the scales separated with a ; Place a /n after the right scale to input another question + followed by the corresponding scales. Make sure you do not input the /n after the last question. + + <br> + <br> + <b>Here is an example input of two questions followed by their left (first) and right (second) ends of the scales:</b> + <br> + <br> + + Did the stimulus make you feel angry?; No; Yes /n + Did the stimulus make you feel happy; Not at all; Yes, alot + </p> + + + + + +{% from "_formhelpers.html" import render_field %} + +<form action="" method="post" role="form"> +<div class="form-group"> + <label for="questions_and_options">Question;left scale;right scale/n</label> + <textarea class="form-control" rows="15" id="questions_and_options" name="questions_and_options"></textarea> +</div> +<button type="submit" class="btn btn-primary">Submit</button> +<a class="btn btn-primary" href="{{ request.referrer }}" role="button">Cancel</a> +</form> + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/add_stimuli.html b/app/templates/add_stimuli.html new file mode 100644 index 0000000..f8617a1 --- /dev/null +++ b/app/templates/add_stimuli.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} +{% block content %} +<h1 class="container mt-5 display-4 text-center"><br>Add more stimuli:</h1> +<br> +<p class="lead"></p> + {% from "_formhelpers.html" import render_field %} + <form id="EditPageForm" action="" method="POST" role="form" enctype="multipart/form-data"> + {% if stimulus_type == 'text' %} + <div class="form-group"> + <legend>Paste the new texts to the textarea and mark the end of each stimulus with a /n. Do not place the marker at the end of the last text. Example: Text1/n Text2/n Text3</legend> + <textarea class="form-control" rows="10" id="text" name="text"></textarea> + </div> + {% else %} + <div class="input-group mb-3"> + <legend>Add files:</legend> + <div class="input-group-prepend"> + <span class="input-group-text">Upload:</span> + </div> + <div class="custom-file"> + <input type="file" class="custom-file-input" id="file" name="file" accept="audio/*,video/*,image/*" multiple> + <label class="custom-file-label" for="upload_file">Choose file</label> + </div> + </div> + {% endif %} + <br> + <button type="submit" class="btn btn-primary">Submit</button> + <a class="btn btn-primary" href="{{ request.referrer }}" role="button">Cancel</a> +</form> +{% endblock %} + + diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..aa3814a --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,89 @@ + +{% import "bootstrap/wtf.html" as wtf %} + +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + <meta name="description" content=""> + <meta name="author" content=""> + <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous"> + <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script> + <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous"></script> + <title>Mega-fMRI Stimulus Rating Tool</title> + + <!-- Bootstrap core CSS --> + <link href="../../dist/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this template --> + <link href="sticky-footer-navbar.css" rel="stylesheet"> + </head> + <body> + <header> +<!-- Navigation --> +<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top"> + <div class="col navbar-brand"> + <img src="/static/img/madam-300x250.jpg" alt="Logo" style="width:70px;"> + {% if pages %} + <a class="navbar-brand pl-5 font-weight-light">Participant ID: <span class="text-success font-weight-bold">{{ session['user']}}</span></a> + {% else %} + <a class="navbar-brand pl-3 text-success font-weight-light" href="{{ url_for('index') }}" class="nav-link">MEGA-fMRI Stimulus Rating Tool</a> + {% endif %} + </div> + <div class="col-6 navbar-nav"> + {% if pages %} + <div class="progress-bar bg-success progress-bar-striped" role="progressbar" style="width: {{ progress_bar_percentage }}%" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100">Task progress: {{ progress_bar_percentage }}%</div> + {% endif %} + </div> + <div class="col text-success text-right"> + {% block navbar %} + {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} + <a class="text">{{ message }}</a> + {% endfor %} + {% endif %} + {% endwith %} + {% endblock %} + {% if current_user.is_authenticated %} + <a class="nav-item" href="{{ url_for('researcher_info') }}" class="nav-link">Data preparation info |</a> + <a class="nav-item" href="{{ url_for('create_experiment') }}" class="nav-link">Create experiment |</a> + {% endif %} + {% if current_user.is_anonymous %} + <a class="nav-item" href="{{ url_for('login') }}" class="nav-link">Researcher login</a> + {% else %} + <a class="nav-item" href="{{ url_for('logout') }}" class="nav-link">Logout</a> + {% endif %} + </div> + + + + +</nav> +</header> + +<main role="main" class="container"> + {% block content %}{% endblock %} +</main> + <footer class="footer"> + <div class="container"> + <br> + <span class="text-muted">Human Emotion Systems Laboratory | <a href="http://emotion.utu.fi">emotion.utu.fi</a></span> + </div> + </footer> + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> + <script>window.jQuery || document.write('<script src="../../assets/js/vendor/jquery-slim.min.js"><\/script>')</script> + <script src="../../assets/js/vendor/popper.min.js"></script> + <script src="../../dist/js/bootstrap.min.js"></script> + </body> +</html> + + + + diff --git a/app/templates/begin_with_id.html b/app/templates/begin_with_id.html new file mode 100644 index 0000000..3e9bb98 --- /dev/null +++ b/app/templates/begin_with_id.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} +{% block content %} + + +<div class="container text-center"> +<div class="row mt-5 display-4"><br></div> + +<h1 class="mt-5 display-4"><br>Please insert your ID-code below:</h1><br> +<br> + +<h3 class="display-5 text-center text-danger"> Notice!</h3> +<div class="row lead">This login is meant only for participants who have received an identification code from a researcher to be used in the rating task. If you have not received such please return to the previous page and select the "Begin task" function instead.</div> + +<br><br> + <form action="" method="post"> + {{ form.hidden_tag() }} + <p> + {{ form.participant_id(size=32) }}<br> + {% for error in form.participant_id.errors %} + <span style="color: red;">[{{ error }}]</span> + {% endfor %} + </p> + <p>{{ form.submit() }}</p> + + </form> +</div> + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/consent.html b/app/templates/consent.html new file mode 100644 index 0000000..ee2743f --- /dev/null +++ b/app/templates/consent.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} +{% block content %} + +<h1 class="row mt-5 display-4"><br>This is the consent page for experiment ID: {{ exp_id }}</h1> +<br> +<h4>Lue tutkimustiedote <a href="tiedote.pdf">tästä.</a></h4> +<br/> +<p class="lead">Ennen tämän suostumuksen antamista olen lukenut ja ymmärtänyt saamani tutkimustiedotteen, sekä saanut riittävästi tietoa tutkimuksen kulusta. Minulle on selvitetty, että minusta kerättäviä tutkimustietoja tullaan käsittelemään luottamuksellisina siten, että niistä ei voida tunnistaa henkilöllisyyttäni. Ymmärrän, että osallistumiseni tutkimukseen on täysin vapaaehtoista ja että voin missä tutkimuksen vaiheessa tahansa keskeyttää tutkimuksen antamatta perustetta. Minulle on lisäksi selvitetty, että halutessani saan tutkimustiedotteessa nimetyltä tutkijalta lisätietoja tutkimuksen yleisistä periaatteista ja edistymisestä. Ymmärrän, että aineistoa kerätään pelkästään tieteellistä tutkimusta varten eikä sitä luovuteta osittainkaan koehenkilölle itselleen, ja että tietokoneeni IP-osoitetta tai muita tunnistetietoja ei tallenneta. +</p> +<p class="lead">Klikkaamalla alla olevaa painiketta annan suostumukseni tutkimuksen yhteydessä tapahtuvaan tietojen keräämiseen ja niiden käsittelyyn tiedotteessa kuvatulla tavalla. +</p> +<p class="lead">Tästä elektronisesta suostumuksesta säilytetään digitaalinen merkintä tutkimuksesta vastaavan tutkijan tiedostoissa. +</p> +<p> + <a class="btn btn-primary" href="{{ url_for('participant_session', exp_id=exp_id, agree='true') }}" role="button">Agree</a> + <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#myModal">Disagree</button> + <!-- Modal --> + <div class="modal fade" id="myModal" role="dialog"> + <div class="modal-dialog modal-dialog-centered"> + <!-- Modal content--> + <div class="modal-content modal-dialog-centered"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal">×</button> + <h4 class="modal-title">Notice!</h4> + </div> + <div class="modal-body"> + <p>In order to participate for the study you need to agree with the terms presented.</p> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-default" data-dismiss="modal">Close Notice</button> + <a class="btn btn-primary" href="/" role="button">Return Home</a> + </div> + </div> + </div> + </div> + +{% endblock %} + + + diff --git a/app/templates/continue_task.html b/app/templates/continue_task.html new file mode 100644 index 0000000..ad2084d --- /dev/null +++ b/app/templates/continue_task.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% block content %} + + +<div class="container text-center"> +<div class="row mt-5 display-4"><br></div> +<br> + +<h1 class="mt-5 display-4"><br>Please insert your participant ID:</h1><br> + <form action="" method="post"> + {{ form.hidden_tag() }} + <p> + {{ form.participant_id(size=32) }}<br> + {% for error in form.participant_id.errors %} + <span style="color: red;">[{{ error }}]</span> + {% endfor %} + </p> + <p>{{ form.submit() }}</p> + + </form> +</div> + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/create_experiment.html b/app/templates/create_experiment.html new file mode 100644 index 0000000..d8ca0c7 --- /dev/null +++ b/app/templates/create_experiment.html @@ -0,0 +1,104 @@ +{% extends "base.html" %} +{% block content %} + <h1 class="container mt-5 display-4 text-center"><br>Create new experiment: (1/4)</h1> + <br> + <br> + <p class="lead">Please input the following information. All fields are required. + </p> + {% from "_formhelpers.html" import render_field %} + <div class=container"><br> + <div class="row align-items-center justify-content-center"> + <div class="col-12"> + <form action="" method="post" role="form"> + <div class="form-group"> + <label for="name">Experiment name:</label> + <input required type="text" class="form-control" id="name" name="name" placeholder="Name?"> + <br> + <label for="instruction">Instructions:</label> + <input required type="text" class="form-control" id="instruction" name="instruction" placeholder="Instructions?"> + <br> + + <div class="form-group"> + <label for="language">Language:</label> + <select class="form-control" id="language" name="language"> + <option disabled selected value></option> + <option value="Afrikanns">Afrikanns</option> + <option value="Albanian">Albanian</option> + <option value="Arabic">Arabic</option> + <option value="Armenian">Armenian</option> + <option value="Basque">Basque</option> + <option value="Bengali">Bengali</option> + <option value="Bulgarian">Bulgarian</option> + <option value="Catalan">Catalan</option> + <option value="Cambodian">Cambodian</option> + <option value="Chinese (Mandarin)">Chinese (Mandarin)</option> + <option value="Croation">Croation</option> + <option value="Czech">Czech</option> + <option value="Danish">Danish</option> + <option value="Dutch">Dutch</option> + <option value="English">English</option> + <option value="Estonian">Estonian</option> + <option value="Fiji">Fiji</option> + <option value="Finnish">Finnish</option> + <option value="French">French</option> + <option value="Georgian">Georgian</option> + <option value="German">German</option> + <option value="Greek">Greek</option> + <option value="Gujarati">Gujarati</option> + <option value="Hebrew">Hebrew</option> + <option value="Hindi">Hindi</option> + <option value="Hungarian">Hungarian</option> + <option value="Icelandic">Icelandic</option> + <option value="Indonesian">Indonesian</option> + <option value="Irish">Irish</option> + <option value="Italian">Italian</option> + <option value="Japanese">Japanese</option> + <option value="Javanese">Javanese</option> + <option value="Korean">Korean</option> + <option value="Latin">Latin</option> + <option value="Latvian">Latvian</option> + <option value="Lithuanian">Lithuanian</option> + <option value="Macedonian">Macedonian</option> + <option value="Malay">Malay</option> + <option value="Malayalam">Malayalam</option> + <option value="Maltese">Maltese</option> + <option value="Maori">Maori</option> + <option value="Marathi">Marathi</option> + <option value="Mongolian">Mongolian</option> + <option value="Nepali">Nepali</option> + <option value="Norwegian">Norwegian</option> + <option value="Persian">Persian</option> + <option value="Polish">Polish</option> + <option value="Portuguese">Portuguese</option> + <option value="Punjabi">Punjabi</option> + <option value="Quechua">Quechua</option> + <option value="Romanian">Romanian</option> + <option value="Russian">Russian</option> + <option value="Samoan">Samoan</option> + <option value="Serbian">Serbian</option> + <option value="Slovak">Slovak</option> + <option value="Slovenian">Slovenian</option> + <option value="Spanish">Spanish</option> + <option value="Swahili">Swahili</option> + <option value="Swedish ">Swedish </option> + <option value="Tamil">Tamil</option> + <option value="Tatar">Tatar</option> + <option value="Telugu">Telugu</option> + <option value="Thai">Thai</option> + <option value="Tibetan">Tibetan</option> + <option value="Tonga">Tonga</option> + <option value="Turkish">Turkish</option> + <option value="Ukranian">Ukranian</option> + <option value="Urdu">Urdu</option> + <option value="Uzbek">Uzbek</option> + <option value="Vietnamese">Vietnamese</option> + <option value="Welsh">Welsh</option> + <option value="Xhosa">Xhosa</option> + </select> + </div> + </div> + <br> + <button type="submit" class="btn btn-primary">Submit</button> + </form> + +{% endblock %} \ No newline at end of file diff --git a/app/templates/create_experiment_bgquestions.html b/app/templates/create_experiment_bgquestions.html new file mode 100644 index 0000000..6f32525 --- /dev/null +++ b/app/templates/create_experiment_bgquestions.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} +{% block content %} + + + <h1 class="container mt-5 display-4 text-center"><br>Create new experiment: (2/4)</h1> + <br> + <p class="lead">Please paste the background questions followed by the list of options for that question separated by ; into the field below. + Mark the last option of the question with a /n after which you can again input a new question followed by a list of options. + <br> + <br> + <b>Here is an example input of two background questions:</b> + <br> + <br> + How many hours did you sleep last night?;6;7;8;9;10/n + Do you normally listen to audio books?;Yes;No + </p> + + {% from "_formhelpers.html" import render_field %} + + +<form action="" method="post" role="form"> +<div class="form-group"> + <label for="Background questions">Background questions and options:</label> + <textarea class="form-control" rows="15" id="bg_questions_and_options" name="bg_questions_and_options"></textarea> +</div> +<button type="submit" class="btn btn-primary">Submit</button> +</form> + + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/create_experiment_questions.html b/app/templates/create_experiment_questions.html new file mode 100644 index 0000000..a3401e8 --- /dev/null +++ b/app/templates/create_experiment_questions.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} +{% block content %} + + + <h1 class="container mt-5 display-4 text-center"><br>Create new experiment: (3/4)</h1> + <br> + <p class="lead">Please paste the slider set questions followed by the left and right ends of the scales separated with a ; Place a /n after the right scale to input another question + followed by the corresponding scales. Make sure you do not input the /n after the last question. + + <br> + <br> + <b>Here is an example input of two questions followed by their left (first) and right (second) ends of the scales:</b> + <br> + <br> + + Did the stimulus make you feel angry?; No; Yes /n + Did the stimulus make you feel happy; Not at all; Yes, alot + </p> + + + + + +{% from "_formhelpers.html" import render_field %} + +<form action="" method="post" role="form"> +<div class="form-group"> + <label for="questions_and_options">Question;left scale;right scale/n</label> + <textarea class="form-control" rows="15" id="questions_and_options" name="questions_and_options"></textarea> +</div> +<button type="submit" class="btn btn-primary">Submit</button> +</form> + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/create_experiment_upload_stimuli.html b/app/templates/create_experiment_upload_stimuli.html new file mode 100644 index 0000000..c718d5f --- /dev/null +++ b/app/templates/create_experiment_upload_stimuli.html @@ -0,0 +1,78 @@ +{% extends "base.html" %} +{% block content %} + + + <h1 class="container mt-5 display-4 text-center"><br>Create new experiment: (4/4)</h1> + <br> + <p class="lead">Select the stimuli to be uploaded for the experiment. First select the stimulus type. If your are using text as a stimuli paste the texts to the textarea and mark + the end of each stimulus with a /n. If you are using images, audio or videofiles use the upload function and select all the stimuli at once from your computer and press submit.</p> + + + +{% from "_formhelpers.html" import render_field %} + + +<form id="UploadStimuliForm" action="" method="POST" role="form" enctype="multipart/form-data"> + + + + <fieldset class="form-group"> + <legend>1. Select stimulus type:</legend> + <div class="form-check"> + <label class="form-check-label"> + <input type="radio" class="form-check-input" name="type" id="text" value="text"> + Text + </label> + </div> + <div class="form-check"> + <label class="form-check-label"> + <input type="radio" class="form-check-input" name="type" id="picture" value="picture"> + Images + </label> + </div> + <div class="form-check"> + <label class="form-check-label"> + <input type="radio" class="form-check-input" name="type" id="video" value="video"> + Video + </label> + </div> + <div class="form-check"> + <label class="form-check-label"> + <input type="radio" class="form-check-input" name="type" id="audio" value="audio"> + Audio + </label> + </div> + </fieldset> + + + + + + <div class="form-group"> + <legend>2. Use this box to upload text stimuli:</legend> + <label for="text_stimulus font-weight-bold">Example: text1 /n text2 /n text3</label> + <textarea class="form-control" rows="10" id="text" name="text"></textarea> + </div> + + + <div class="input-group mb-3"> + <legend>OR</legend> + <legend>2. Upload images, audio or videofiles here:</legend> + <div class="input-group-prepend"> + <span class="input-group-text">Upload:</span> + </div> + <div class="custom-file"> + <input type="file" class="custom-file-input" id="file" name="file" accept="audio/*,video/*,image/*" multiple> + <label class="custom-file-label" for="upload_file">Choose file</label> + </div> +</div> + + + <br> + <button type="submit" class="btn btn-primary">Submit</button> + +</form> + + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/edit_bg_question.html b/app/templates/edit_bg_question.html new file mode 100644 index 0000000..83b8a84 --- /dev/null +++ b/app/templates/edit_bg_question.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% block content %} +<h1 class="container mt-5 display-4 text-center"><br>Edit experiment details:</h1> +<br> +<br> +<p class="lead">Please use following format: Question; Option1; Option2; Option3 +<br> +<br> +Note that there is no ; after the last option! +</p> +<form action="" method="post" role="form"> +<div class="form-group"> +<label for="Background questions">Background question and its options:</label> +<textarea class="form-control" rows="15" id="new_values" name="new_values" placeholder= {{ form.bg_questions_and_options }}</textarea> +</div> +<button type="submit" class="btn btn-primary">Submit</button> +<a class="btn btn-primary" href="{{ request.referrer }}" role="button">Cancel</a> +</form> +{% endblock %} \ No newline at end of file diff --git a/app/templates/edit_experiment.html b/app/templates/edit_experiment.html new file mode 100644 index 0000000..8815f36 --- /dev/null +++ b/app/templates/edit_experiment.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} +{% block content %} + + +<h1 class="container mt-5 display-4 text-center"><br>Edit experiment details:</h1> + + + + + <br> + <br> + <p class="lead">Now editing experiment ID: {{ exp_id }}. All fields are required. + </p> + {% from "_formhelpers.html" import render_field %} + <div class=container"><br> + <div class="row align-items-center justify-content-center"> + <div class="col-12"> + <form action="" method="post" role="form"> + <div class="form-group"> + <label for="Name">Name:</label> + <input type="text" class="form-control" id="name" name="name" placeholder={{ form.name }} + <br> + <label for="Instruction">Instructions:</label> + <input required type="text" class="form-control" id="instruction" name="instruction" placeholder= {{ form.instruction }} + <br> + {{ render_field(form.language) }} + </div> + <br> + <button type="submit" class="btn btn-primary">Update</button> + <a class="btn btn-primary" href="{{ url_for('view_experiment', exp_id=exp_id) }}" role="button">Cancel</a> + + + </form> + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/edit_question.html b/app/templates/edit_question.html new file mode 100644 index 0000000..2ac4513 --- /dev/null +++ b/app/templates/edit_question.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} +{% block content %} + + +<h1 class="container mt-5 display-4 text-center"><br>Edit question:</h1> + <br> + <br> + <p class="lead">Edit this question + </p> + + <div class=container"><br> + <div class="row align-items-center justify-content-center"> + <div class="col-12"> + <form action="" method="post" role="form"> + <div class="form-group"> + <label for="Question">Question:</label> + <input type="text" class="form-control" id="question" name="question" placeholder= {{ form.question }} + <br> + <label for="Left scale">Left end of the scale:</label> + <input required type="text" class="form-control" id="left" name="left" placeholder= {{ form.left }} + <br> + <label for="Right scale">Right end of the scale:</label> + <input required type="text" class="form-control" id="right" name="right" placeholder= {{ form.right }} + <br> + </div> + <br> + <button type="submit" class="btn btn-primary">Update</button> + <a class="btn btn-primary" href="{{ request.referrer }}" role="button">Cancel</a> + + + </form> + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/edit_stimuli.html b/app/templates/edit_stimuli.html new file mode 100644 index 0000000..e234e0d --- /dev/null +++ b/app/templates/edit_stimuli.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} +{% block content %} +<h1 class="container mt-5 display-4 text-center"><br>Edit stimulus:</h1> +<br> +<p class="lead"></p> + {% from "_formhelpers.html" import render_field %} + <form id="EditPageForm" action="" method="POST" role="form" enctype="multipart/form-data"> + {% if edit_page.type == 'text' %} + <div class="form-group"> + <legend>Modify this text:</legend> + <textarea class="form-control" rows="10" id="text" name="text" placeholder={{ form.text }} + </textarea> + </div> + {% else %} + <div class="input-group mb-3"> + <legend>Replace current file:<br> {{ edit_page.media }}</legend> + <div class="input-group-prepend"> + <span class="input-group-text">Upload:</span> + </div> + <div class="custom-file"> + <input type="file" class="custom-file-input" id="file" name="file" accept="audio/*,video/*,image/*" required> + <label class="custom-file-label" for="upload_file">Choose file</label> + </div> + </div> + {% endif %} + <br> + <button type="submit" class="btn btn-primary">Submit</button> + <a class="btn btn-primary" href="{{ request.referrer }}" role="button">Cancel</a> +</form> +{% endblock %} + diff --git a/app/templates/experiment_statistics.html b/app/templates/experiment_statistics.html new file mode 100644 index 0000000..45fe3c5 --- /dev/null +++ b/app/templates/experiment_statistics.html @@ -0,0 +1,110 @@ +{% extends "base.html" %} +{% block content %} + +<h1 class="container mt-5 display-4 text-left"><br>Experiment info:</h1> +<br> + + +{% for exp in experiment_info %} + +<table class="table"> + <tbody> + <tr> + <td>Name:</td> + <td>{{ exp.name }}</td> + </tr> + <tr> + <td>ID:</td> + <td>{{ exp.idexperiment }}</td> + </tr> + <tr> + <td>Language:</td> + <td>{{ exp.language }}</td> + </tr> + <tr> + <td>Status:</td> + <td>{{ exp.status }}</td> + </tr> + <tr> + <td>Instructions:</td> + <td>{{ exp.instruction }}</td> + </tr> + <tr> + <td>Number of started ratings:</td> + <td>{{ started_ratings }}</td> + </tr> + <tr> + <td>Number of finished ratings:</td> + <td>??</td> + </tr> + <tr> + <td><a class="btn btn-primary btn-info" href="{{ url_for('download_csv', exp_id=exp.idexperiment) }}" role="button">Export results (csv)</a></td> + <td></td> + </tr> + </tbody> +</table> + +{% endfor %} + +<h1 class="container mt-5 display-4 text-left"><br>Rating task values:</h1> +<br> + + +<table class="table"> + <thead> + <tr> + <th scope="col" nowrap>Page ID/Question:</td> + {% 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 %} + <tr> + <td>{{ participant }}</td> + {% for answer in participants_and_answers[participant] %} + <td align="center">{{ answer[3] }}</td> + {% endfor %} + </tr> + {% endfor %} + </tbody> +</table> + + +<h1 class="container mt-5 display-4 text-left"><br>Background question answers:</h1> +<br> + + +<table class="table"> + <thead> + <tr> + <th scope="col" nowrap>Question:</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 %} + <tr> + <td>{{ p }}</td> + {% for bg_answer in bg_answers_for_participants[p] %} + <td align="center">{{ bg_answer }}</td> + {% endfor %} + </tr> + {% endfor %} + </tbody> +</table> + + + + + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..55823d0 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,91 @@ +{% extends "base.html" %} +{% block content %} + + + <h1 class="container mt-5 display-4 text-center"><br>{{ _('Welcome') }}</h1> + <br> + <p class="lead">This is the Human Emotion Systems laboratorys stimulus rating tool. If you have previously started a rating task you can continue that task on this page. If you are a researcher you can create new rating tasks by <a href="{{ url_for('login') }}">logging in.</a> + Or you can start a new rating task and start rating by selecting a rating task from the database list below.</p> + <div class="row"> + <div class="col mt-5"> + <h3>List of experiments in database:</h3> + {% block attributes %} + {% for exp in experiments %} + + {% if exp.status == 'Public' %} + + <ul class="list-group mb-4"> + <li class="list-group-item active"><span class="font-weight-bold">Name:</span> {{ exp.name }} </li> + <li class="list-group-item"><span class="font-weight-bold">Instruction:</span> {{ exp.instruction }}</li> + {% if current_user.is_authenticated %} + <li class="list-group-item"><span class="font-weight-bold">ID number:</span> {{ exp.idexperiment }} </li> + <li class="list-group-item"><span class="font-weight-bold">Language:</span> {{ exp.language }}</li> + <li class="list-group-item"><span class="font-weight-bold">Status:</span> {{ exp.status }}</li> + {% endif %} + <li class="list-group-item"> + <button class="btn btn-outline-success dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + Begin task + </button> + <div class="dropdown-menu" aria-labelledby="dropdownMenuButton"> + <a class="dropdown-item" href="{{ url_for('consent', exp_id=exp.idexperiment) }}">As a new participant</a> + <a class="dropdown-item" href="{{ url_for('begin_with_id', exp_id=exp.idexperiment) }}">I have received an ID to use for this task</a> + </div> + <a class="btn btn-outline-success" href="{{ url_for('continue_task', exp_id=exp.idexperiment) }}" role="button">Continue task</a> + + {% if current_user.is_authenticated %} + + + <a class="btn btn-outline-info" href="{{ url_for('experiment_statistics', exp_id=exp.idexperiment) }}" role="button">Statistics</a> + <a class="btn btn-outline-info" href="{{ url_for('view_experiment', exp_id=exp.idexperiment) }}" role="button">View / Edit</a> + + {% endif %} + + + </ul> + <br> + <br> + {% endif %} + + {% if (exp.status == 'Hidden') and (current_user.is_authenticated) %} + <br> + <h3>Unpublished experiment:</h3> + <ul class="list-group mb-4"> + <li class="list-group-item list-group-item-dark"><span class="font-weight-bold">Name:</span> {{ exp.name }} </li> + <li class="list-group-item"><span class="font-weight-bold">Instruction:</span> {{ exp.instruction }}</li> + {% if current_user.is_authenticated %} + <li class="list-group-item"><span class="font-weight-bold">ID number:</span> {{ exp.idexperiment }} </li> + <li class="list-group-item"><span class="font-weight-bold">Language:</span> {{ exp.language }}</li> + <li class="list-group-item"><span class="font-weight-bold">Status:</span> {{ exp.status }}</li> + {% endif %} + <li class="list-group-item"> + <button class="btn btn-outline-success dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + Begin task + </button> + <div class="dropdown-menu" aria-labelledby="dropdownMenuButton"> + <a class="dropdown-item" href="{{ url_for('consent', exp_id=exp.idexperiment) }}">As a new participant</a> + <a class="dropdown-item" href="{{ url_for('begin_with_id', exp_id=exp.idexperiment) }}">I have received an ID to use for this task</a> + </div> + <a class="btn btn-outline-success" href="{{ url_for('continue_task', exp_id=exp.idexperiment) }}" role="button">Continue task</a> + {% if current_user.is_authenticated %} + + + <a class="btn btn-outline-info" href="{{ url_for('experiment_statistics', exp_id=exp.idexperiment) }}" role="button">Statistics</a> + <a class="btn btn-outline-info" href="{{ url_for('view_experiment', exp_id=exp.idexperiment) }}" role="button">View / Edit</a> + + + {% endif %} + </li> + </ul> + + {% endif %} + + + + + {% endfor %} + {% endblock %} + </div> + </div> + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/instructions.html b/app/templates/instructions.html new file mode 100644 index 0000000..6c2c320 --- /dev/null +++ b/app/templates/instructions.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% block content %} + +<h1 class="row mt-5 display-4"><br>Rating task instructions:</h1> + + +{% block attributes %} + {% for ins in instructions %} + <div class="row lead"> {{ ins.instruction }} </div> + {% endfor %} + {% endblock %} + +<br> +<h3 class="row display-5 text-danger"> Notice!</h3> +<div class="row lead">If you wish to quit a rating task before it is fully completed, you can return to finish the task later but you will need your participant ID-number in order to do that. Please save your participant ID before starting the rating task!</div> + +<br> +<h4 class="row">Your participant ID is: <div class="text-danger"> {{ session['user'] }} </div></h4> + +<br> +<a class="btn btn-primary" href="/task/1" role="button">I'm ready to start</a> + +{% endblock %} + + diff --git a/app/templates/quit_task.html b/app/templates/quit_task.html new file mode 100644 index 0000000..78efec3 --- /dev/null +++ b/app/templates/quit_task.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} +{% block content %} + +<h1 class="row mt-5 display-4"><br>Notice!</h1> + +<br> +<h3 class="row display-5 text-danger"> Notice!</h3> +<div class="row lead">Please write down your participant ID so you can return and finish the rating task later!</div> + +<br> +<h4 class="row">Your participant ID is: <div class="text-danger"> {{ user_id }} </div></h4> + +<br> +<a class="btn btn-primary" href="/" role="button">Quit!</a> + +{% endblock %} \ No newline at end of file diff --git a/app/templates/register.html b/app/templates/register.html new file mode 100644 index 0000000..9526075 --- /dev/null +++ b/app/templates/register.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} +{% import "bootstrap/wtf.html" as wtf %} +{% block content %} + +<div class="container text-center mt-5"> + <h1 class="display-4"><br>This is the registration page.</h1> + <p class="lead"> Please fill in these background questions before starting the rating task:</p> +</div> +<form class="form-group" action="" method="post"> + {% for options in form.questions1 %} + <div class="form-group"> + <label for="{{ options[0] }}">{{ options[1] }}</label> + <select required class="form-control" name="{{ options[0] }}"> + <option disabled selected value></option> + {% for op in form.questions1[options] %} + <option value="{{ op[0] }}" name="{{ op[0] }}">{{ op[0] }}</option> + {% endfor %} + </select> + </div> + {% endfor %} + <p> + <button type="submit" class="btn btn-primary">Submit</button> + </p> +</form> + + +{% endblock %} + + diff --git a/app/templates/remove_experiment.html b/app/templates/remove_experiment.html new file mode 100644 index 0000000..03af89c --- /dev/null +++ b/app/templates/remove_experiment.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} +{% block content %} + + +<h1 class="container mt-5 display-4 text-center"><br>Remove experiment:</h1> + <br> + <br> + <p class="lead text-center">Confirm that you wish to remove the experiment by writing "DELETE" on the text field below and press submit. <br><br> + Notice!- All experiment files will be lost, including data collected from participants. + Be sure to export your data before deleting experiments. + </p> + + <div class=container"><br> + <div class="row align-items-center justify-content-center"> + <div class="col-4 text-center"> + <form action="" method="post" role="form"> + <div class="form-group"> + <label for="Remove"></label> + <input type="text" class="form-control" id="remove" name="remove" required> + <br> + <button type="submit" class="btn btn-primary">Remove</button> + <a class="btn btn-primary" href="{{ url_for('view_experiment', exp_id=exp_id) }}" role="button">Cancel</a> + </form> + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/researcher_info.html b/app/templates/researcher_info.html new file mode 100644 index 0000000..0a79d0d --- /dev/null +++ b/app/templates/researcher_info.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% block content %} + +<h1 class="row mt-5 display-4"><br>How to prepare your stimuli before uploading:</h1> +<br> + +<h4 class="">How to split audiofiles into sections:</h4> + +<p class="">Audio segmentation is easy to do with eg. <a href="https://www.nch.com.au/wavepad/index.html">WavePad</a> ..... </p> + + +<h4 class="">Supported videoformats and how to convert videos before upload:</h4> + +<p class="">Info on how to use <a href="https://www.videolan.org/vlc/">VLC-player</a> conversion tool to convert files to WebM-format..... </p> + +{% endblock %} + + + diff --git a/app/templates/researcher_login.html b/app/templates/researcher_login.html new file mode 100644 index 0000000..051251f --- /dev/null +++ b/app/templates/researcher_login.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} +{% block content %} + +<div class="container text-center"> +<h1 class="mt-5 display-4"><br>Sign In</h1> + <form action="" method="post"> + {{ form.hidden_tag() }} + <p> + {{ form.username.label }}<br> + {{ form.username(size=32) }}<br> + {% for error in form.username.errors %} + <span style="color: red;">[{{ error }}]</span> + {% endfor %} + </p> + <p> + {{ form.password.label }}<br> + {{ form.password(size=32) }}<br> + {% for error in form.password.errors %} + <span style="color: red;">[{{ error }}]</span> + {% endfor %} + </p> + <p>{{ form.remember_me() }} {{ form.remember_me.label }}</p> + <p>{{ form.submit() }}</p> + </form> +</div> + +{% endblock %} \ No newline at end of file diff --git a/app/templates/task.html b/app/templates/task.html new file mode 100644 index 0000000..d0d8ade --- /dev/null +++ b/app/templates/task.html @@ -0,0 +1,136 @@ +{% extends "base.html" %} +{% block content %} + +<br> +<br> + +{% if session['randomization']=='Off' %} + + + {% if session['type']=='text' %} + <div class="container text-center mt-5 pt-5"> + {% for page in pages.items %} + <h3 class="text-center mt-5"><br>{{ page.text }}</h3> + {% endfor %} + </div> + <br><br><br><br><br> + {% endif %} + + + {% if session['type']=='picture' %} + <div class="container stimulus col-9 mt-5 pt-5"> + {% for page in pages.items %} + <img src="/{{ page.media }}" class="img-fluid"> + {% endfor %} + </div> + {% endif %} + + + {% if session['type']=='video' %} + <div class="col-9 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> + {% endfor %} + </div> + {% endif %} + + + {% if session['type']=='audio' %} + <div class="col-4 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> + {% endfor %} + </div> + {% endif %} + + +{% else %} + + + + {% if session['type']=='text' %} + <div class="container text-center mt-5 pt-5"> + {% for page in pages.items %} + <h3 class="text-center mt-5"><br>{{ randomized_stimulus.text }}</h3> + {% endfor %} + </div> + <br><br><br><br><br> + {% endif %} + + + {% if session['type']=='picture' %} + <div class="container stimulus col-9 mt-5 pt-5"> + {% for page in pages.items %} + <img src="/{{ randomized_stimulus.media }}" class="img-fluid"> + {% endfor %} + </div> + {% endif %} + + + {% if session['type']=='video' %} + <div class="col-9 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> + {% endfor %} + </div> + {% endif %} + + + {% if session['type']=='audio' %} + <div class="col-4 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> + {% endfor %} + </div> + {% endif %} + + +{% endif %} + + + + + + + + + + <br> + + <h4 class="text-center">Rate the above stimulus based on these categories</h4> + + <form class="form-group mt-5" action="" method="post"> + {% for category in form.categories1 %} + {% for scale in form.categories1[category] %} + <div class="row form-group"> + <div class="col-2 text-center"> + <p>{{ scale[0] }}</p> + </div> + <div class="col text-center"> + <label for="customRange">{{ category[1] }}</label> + <input type="range" class="custom-range" id="customRange" name={{ category[0] }}> + + </div> + <div class="col-2 text-center"> + <p>{{ scale[1] }}</p> + </div> + </div> + {% endfor %} + {% endfor %} + <div class="form-row text-center"> + <div class="col-12"> + <a class="btn btn-primary" href={{ url_for('quit_task') }} role="button">Quit task</a> + <button type="submit" class="btn btn-primary">Next page</button> + </div> + </div> + </form> + +{% endblock %} \ No newline at end of file diff --git a/app/templates/task_completed.html b/app/templates/task_completed.html new file mode 100644 index 0000000..a12ea8d --- /dev/null +++ b/app/templates/task_completed.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% block content %} + + + <h1 class="row mt-5 display-4"> + <div class="container text-center"> + <br>Task completed!</h1> + <br> + </div> + <div class="container text-center lead"> + <p>You have completed the rating task. Thank you for your participation :)</p> + </div> + <p> + <div class="container text-center"> + <a class="btn btn-primary" href="/" role="button">Return Home</a> + </div> + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/view_experiment.html b/app/templates/view_experiment.html new file mode 100644 index 0000000..512a1d8 --- /dev/null +++ b/app/templates/view_experiment.html @@ -0,0 +1,314 @@ +{% extends "base.html" %} +{% block content %} + +<h1 class="container mt-5 display-4 text-left"><br>Experiment info:</h1> +<br> + + +<div class="container col-12"> +<table class="table"> + <tbody> + + +{% for exp in experiment_info %} + <tr> + <td>Name:</td> + <td>{{ exp.name }}</td> + <td> + <button type="button" class="btn btn-primary btn-block btn-sm btn-dark" data-toggle="modal" data-target="#myModal-remove">Remove experiment</button> + <!-- Modal --> + <div class="modal fade" id="myModal-remove" role="dialog"> + <div class="modal-dialog modal-dialog-centered" id="modal-remove"> + <!-- Modal content--> + <div class="modal-content modal-dialog-centered"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal">×</button> + <h4 class="modal-title">Notice!</h4> + </div> + <div class="modal-body"> + <p>Are you sure you want to remove this experiment? All gathered ratings will be lost!</p> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> + <a class="btn btn-primary" href="{{ url_for('remove_experiment', exp_id=exp.idexperiment) }}" role="button">Yes, remove</a> + </div> + </div> + </div> + </div> + + </td> + </tr> + <tr> + <td>Status:</td> + <td>{{ exp.status }}</td> + <td> + {% if exp.status == 'Hidden' %} + <a class="btn btn-primary btn-block btn-sm btn-info" href="{{ url_for('publish_experiment', exp_id=exp.idexperiment) }}" role="button">Publish</a></td> + {% endif %} + {% if exp.status == 'Public' %} + <a class="btn btn-primary btn-block btn-sm btn-info" href="{{ url_for('hide_experiment', exp_id=exp.idexperiment) }}" role="button">Hide experiment</a></td> + {% endif %} + </tr> + <tr> + <td class"col-1">Instructions:</td> + <td class"col-10">{{ exp.instruction }}</td> + <td class"col-1"> + <a class="btn btn-primary btn-block btn-sm btn-info" href="{{ url_for('edit_experiment', exp_id=exp.idexperiment) }}" role="button">Edit info</a> + </td> + </tr> + <tr> + <td>Trial randomization:</td> + <td>{{ exp.randomization }}</td> + <td> + {% if exp.randomization == 'Off' %} + <a class="btn btn-primary btn-block btn-sm btn-info" href="{{ url_for('enable_randomization', exp_id=exp.idexperiment) }}" role="button">Enable</a></td> + {% endif %} + {% if exp.randomization == 'On' %} + <a class="btn btn-primary btn-block btn-sm btn-info" href="{{ url_for('disable_randomization', exp_id=exp.idexperiment) }}" role="button">Disable</a></td> + {% endif %} + + </td> + </tr> + <tr> + <td>Language:</td> + <td>{{ exp.language }}</td> + <td></td> + </tr> + <tr> + <td>Stimulus type:</td> + <td>{{ mtype.type }}</td> + <td></td> + </tr> + <tr> + <td>Experiment ID:</td> + <td>{{ exp.idexperiment }}</td> + <td></td> + </tr> + +{% endfor %} + </tbody> +</table> +</div> + + + +<h1 class="container mt-5 display-4 text-left"><br>Background questions:</h1> +<br> + + {% for options in questions1 %} + + <table class="table"> + <tbody> + <tr> + <td class="col-8"> + <label for="{{ options[0] }}">{{ options[1] }}</label> + <select required class="form-control" name="{{ options[0] }}"> + <option disabled selected value></option> + {% for op in questions1[options] %} + <option value="{{ op[0] }}" name="{{ op[0] }}">{{ op[0] }}</option> + {% endfor %} + </select> + </td> + <td class="text-nowrap align-bottom"> + + <button type="button" class="btn btn-primary btn-sm btn-dark" data-toggle="modal" data-target="#myModal{{options[0]}}">Remove</button> + <!-- Modal --> + <div class="modal fade" id="myModal{{options[0]}}" role="dialog"> + <div class="modal-dialog modal-dialog-centered" id="{{options[0]}}"> + <!-- Modal content--> + <div class="modal-content modal-dialog-centered"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal">×</button> + <h4 class="modal-title">Notice!</h4> + </div> + <div class="modal-body"> + <p>Are you sure you want to remove this?</p> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> + <a class="btn btn-primary" href="{{ url_for('remove_bg_question', idbackground_question=options[0], exp_id=exp_id) }}" role="button">Yes, remove</a> + </div> + </div> + </div> + </div> + <a class="btn btn-primary btn-sm btn-info" href="{{ url_for('edit_bg_question', idbackground_question=options[0]) }}" role="button">Edit</a> + + </td> + </tr> + + {% endfor %} + </tbody> + </table> + <table class="table"> + <tbody> + <td class="text-nowrap align-bottom text-right col-8"> + <a class="btn btn-primary btn-sm btn-info" href="{{ url_for('add_bg_question', exp_id=exp_id) }}" role="button">Add more</a> + </td> + </tbody> + </table> + + + +<h1 class="container mt-5 display-4 text-left"><br>Rating set:</h1> +<hr> +<br> + {% for category in categories1 %} + {% for scale in categories1[category] %} + <div class="row form-group"> + <div class="col-2 text-center"> + <p>{{ scale[0] }}</p> + </div> + <div class="col text-center"> + <label for="customRange">{{ category[1] }}</label> + <input type="range" class="custom-range" id="customRange" name={{ category[0] }}> + </div> + <div class="col-2 text-center"> + <p>{{ scale[1] }}</p> + </div> + <div class="col-2 text-center"> + + + <button type="button" class="btn btn-primary btn-sm btn-dark" data-toggle="modal" data-target="#mymodal{{category[0]}}">Remove</button> + <!-- Modal --> + <div class="modal fade" id="mymodal{{category[0]}}" role="dialog"> + <div class="modal-dialog modal-dialog-centered" id="{{category[0]}}"> + <!-- Modal content--> + <div class="modal-content modal-dialog-centered"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal">×</button> + <h4 class="modal-title">Notice!</h4> + </div> + <div class="modal-body"> + <p>Are you sure you want to remove this?</p> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> + <a class="btn btn-primary" href="{{ url_for('remove_question', idquestion=category[0], exp_id=exp_id) }}" role="button">Yes, remove</a> + </div> + </div> + </div> + </div> + <a class="btn btn-primary btn-sm btn-info" href="{{ url_for('edit_question', idquestion=category[0]) }}" role="button">Edit</a> + + </div> + + </div> + {% endfor %} + {% endfor %} + + </table> + <table class="table"> + <tbody> + <td class="text-nowrap align-bottom text-right col-8"> + <a class="btn btn-primary btn-sm btn-info" href="{{ url_for('add_questions', exp_id=exp_id) }}" role="button">Add more</a> + </td> + </tbody> + </table> + + + <h1 class="container mt-5 display-4 text-left"><br>Stimuli:</h1> + <hr> + + <p class="lead">Please notice that the Page ID is just the reference ID of the stimulus in the database. When stimulus randomization is set to "OFF" + the stimulus will be presented in the order below even if there would be numbers missing from the page ID sequence. + If randomization is set to "ON" the order will be randomized for each participant. + </p> + <br> +<div class="container col-12"> + <table class="table col-12"> + <tbody> + {% if mtype.type=='text' %} + {% for page in media.items %} + <tr class="col-12"> + <td class="text-nowrap">Page ID: {{ page.idpage }} + </td> + <td class="col-8">{{ page.text }} + </td> + <td class="col-2 text-nowrap"> + <button type="button" class="btn btn-primary btn-sm btn-dark" data-toggle="modal" data-target="#mymodal{{page.idpage}}">Remove</button> + <!-- Modal --> + <div class="modal fade" id="mymodal{{page.idpage}}" role="dialog"> + <div class="modal-dialog modal-dialog-centered" id="{{page.idpage}}"> + <!-- Modal content--> + <div class="modal-content modal-dialog-centered"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal">×</button> + <h4 class="modal-title">Notice!</h4> + </div> + <div class="modal-body"> + <p>Are you sure you want to remove this?</p> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> + <a class="btn btn-primary" href="{{ url_for('remove_page', idpage=page.idpage, exp_id=exp_id) }}" role="button">Yes, remove</a> + </div> + </div> + </div> + </div> + <a class="btn btn-primary btn-sm btn-info" href="{{ url_for('edit_stimuli', idpage=page.idpage, exp_id=exp_id) }}" role="button">Edit</a> + </td> + </tr> + {% endfor %} + + <table class="table"> + <tbody> + <td class="text-nowrap align-bottom text-right col-8"> + <a class="btn btn-primary btn-sm btn-info" href="{{ url_for('add_stimuli', exp_id=exp_id, stimulus_type=mtype.type) }}" role="button">Add more</a> + </td> + </tbody> + </table> + + {% else %} + <div class="container col-12"> + {% for page in media.items %} + <tr class="col-12 text-left"> + <td class="col-2 text-nowrap">Page ID: {{ page.idpage }} + </td> + <td class="col-8" >{{ page.media }} + </td> + <td class="col-2 text-nowrap"> + <button type="button" class="btn btn-primary btn-sm btn-dark" data-toggle="modal" data-target="#mymodal{{page.idpage}}">Remove</button> + <!-- Modal --> + <div class="modal fade" id="mymodal{{page.idpage}}" role="dialog"> + <div class="modal-dialog modal-dialog-centered" id="{{page.idpage}}"> + <!-- Modal content--> + <div class="modal-content modal-dialog-centered"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal">×</button> + <h4 class="modal-title">Notice!</h4> + </div> + <div class="modal-body"> + <p>Are you sure you want to remove this?</p> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> + <a class="btn btn-primary" href="{{ url_for('remove_page', idpage=page.idpage, exp_id=exp_id) }}" role="button">Yes, remove</a> + </div> + </div> + </div> + </div> + <a class="btn btn-primary btn-sm btn-info" href="{{ url_for('edit_stimuli', idpage=page.idpage, exp_id=exp_id) }}" role="button">Replace</a> + + </td> + </tr> + {% endfor %} + + <table class="table"> + <tbody> + <td class="text-nowrap align-bottom text-right col-8"> + <a class="btn btn-primary btn-sm btn-info" href="{{ url_for('add_stimuli', exp_id=exp_id, stimulus_type=mtype.type) }}" role="button">Add more</a> + </td> + </tbody> + </table> + + {% endif %} + + </tbody> + </table> + + </div> + <br> + + + +{% endblock %} \ No newline at end of file diff --git a/app/translations/fin/LC_MESSAGES/messages.mo b/app/translations/fin/LC_MESSAGES/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..699bab536246627e73eee1a2fea29e5490bf4da7 GIT binary patch literal 484 zcmaJ-O-sW-6pSJWIePZ+yeYahsb3{+O21NxB(|in9%b9U#z?vwHk)F9hxp^X_*;Bc zv3l{rurtf>W_RBAc3usdZNfg`WG#M8gZM7NCA@4IhDWDKcq4AyV>XowG=@q(;Z!q! z#>S|MR0;=%(cs4G#xVAxkw3UKBh01JEG&|A#yVwEuvj|ag>64*wR^qD8;#82AZA^K zT+;^D;~E{PT6T@uHnYybzH-hkj;*R?kXOxH>O`T|~P7Ar1_jBBYJ=-&ka+_pmx z8h@gjX23;K@`NJi=|kWD(JV&3_`ZZj5VW$iB305fhgqq3#`;n%3I{~~DGRq+hcD_L z3OI1VJ~Bf;^ftxV70c{Op;6G&RyhS~eAcIV##2#;=_6MKYPZyLR@?a2Uznp}o|wv6 lDiQ~3lT@46C{R&P)ac5A4xeBK)ygF$7%{3P>N1nu_y946huZ)E literal 0 HcmV?d00001 diff --git a/app/translations/fin/LC_MESSAGES/messages.po b/app/translations/fin/LC_MESSAGES/messages.po new file mode 100644 index 0000000..5d1d64e --- /dev/null +++ b/app/translations/fin/LC_MESSAGES/messages.po @@ -0,0 +1,24 @@ +# Finnish (Finland) translations for PROJECT. +# Copyright (C) 2018 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2018-11-04 17:35+0200\n" +"PO-Revision-Date: 2018-11-04 17:35+0200\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language: fi_FI\n" +"Language-Team: fi_FI <LL@li.org>\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.6.0\n" + +#: app/templates/index.html:5 +msgid "Welcome" +msgstr "Tervetuloa" + diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..5ce6e47 --- /dev/null +++ b/babel.cfg @@ -0,0 +1,3 @@ +[python: app/**.py] +[jinja2: app/templates/**.html] +extensions=jinja2.ext.autoescape,jinja2.ext.with_ \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..1b0d935 --- /dev/null +++ b/config.py @@ -0,0 +1,11 @@ +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' + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ + 'sqlite:///' + os.path.join(basedir, 'app.db') + SQLALCHEMY_TRACK_MODIFICATIONS = False + diff --git a/dump.sql b/dump.sql new file mode 100644 index 0000000..4daea50 --- /dev/null +++ b/dump.sql @@ -0,0 +1,95 @@ +CREATE TABLE background_question ( + idbackground_question INTEGER NOT NULL, + background_question VARCHAR(120), + experiment_idexperiment INTEGER, + PRIMARY KEY (idbackground_question) +); +CREATE TABLE experiment ( + idexperiment INTEGER NOT NULL, + name VARCHAR(120), + instruction VARCHAR(120), + directoryname VARCHAR(120), + language VARCHAR(120), + status VARCHAR(120), + randomization VARCHAR(120), + PRIMARY KEY (idexperiment) +); +CREATE TABLE trial_randomization ( + idtrial_randomization INTEGER NOT NULL, + page_idpage INTEGER, + randomized_idpage INTEGER, + answer_set_idanswer_set INTEGER, + experiment_idexperiment INTEGER, + PRIMARY KEY (idtrial_randomization) +); +CREATE TABLE user ( + id INTEGER NOT NULL, + username VARCHAR(64), + email VARCHAR(120), + password_hash VARCHAR(128), + PRIMARY KEY (id) +); +INSERT INTO user VALUES(1,'timo',NULL,'pbkdf2:sha256:50000$sctKb5R4$688ff9fd63df4a0883b9eb003b6738c6b7baa2010e1cd503c678b43c881c07bf'); +CREATE TABLE answer_set ( + idanswer_set INTEGER NOT NULL, + experiment_idexperiment INTEGER, + session VARCHAR(120), + agreement VARCHAR(120), + answer_counter INTEGER, + PRIMARY KEY (idanswer_set), + FOREIGN KEY(experiment_idexperiment) REFERENCES experiment (idexperiment) +); +CREATE TABLE background_question_option ( + idbackground_question_option INTEGER NOT NULL, + background_question_idbackground_question INTEGER, + option VARCHAR(120), + PRIMARY KEY (idbackground_question_option), + FOREIGN KEY(background_question_idbackground_question) REFERENCES background_question (idbackground_question) +); +CREATE TABLE page ( + idpage INTEGER NOT NULL, + experiment_idexperiment INTEGER, + type VARCHAR(120), + text VARCHAR(120), + media VARCHAR(120), + PRIMARY KEY (idpage), + FOREIGN KEY(experiment_idexperiment) REFERENCES experiment (idexperiment) +); +CREATE TABLE question ( + idquestion INTEGER NOT NULL, + experiment_idexperiment INTEGER, + question VARCHAR(120), + `left` VARCHAR(120), + `right` VARCHAR(120), + PRIMARY KEY (idquestion), + FOREIGN KEY(experiment_idexperiment) REFERENCES experiment (idexperiment) +); +CREATE TABLE answer ( + idanswer INTEGER NOT NULL, + question_idquestion INTEGER, + answer_set_idanswer_set INTEGER, + answer VARCHAR(120), + page_idpage INTEGER, + PRIMARY KEY (idanswer), + FOREIGN KEY(answer_set_idanswer_set) REFERENCES answer_set (idanswer_set), + FOREIGN KEY(page_idpage) REFERENCES page (idpage), + FOREIGN KEY(question_idquestion) REFERENCES question (idquestion) +); +CREATE TABLE background_question_answer ( + idbackground_question_answer INTEGER NOT NULL, + answer_set_idanswer_set INTEGER, + answer VARCHAR(120), + background_question_idbackground_question INTEGER, + PRIMARY KEY (idbackground_question_answer), + FOREIGN KEY(answer_set_idanswer_set) REFERENCES answer_set (idanswer_set), + FOREIGN KEY(background_question_idbackground_question) REFERENCES background_question (idbackground_question) +); +CREATE UNIQUE INDEX ix_experiment_directoryname ON experiment (directoryname); +CREATE INDEX ix_experiment_instruction ON experiment (instruction); +CREATE INDEX ix_experiment_name ON experiment (name); +CREATE UNIQUE INDEX ix_user_email ON user (email); +CREATE UNIQUE INDEX ix_user_username ON user (username); +CREATE INDEX ix_page_media ON page (media); +CREATE INDEX ix_page_text ON page (text); +CREATE INDEX ix_page_type ON page (type); + diff --git a/messages.pot b/messages.pot new file mode 100644 index 0000000..f6a2d81 --- /dev/null +++ b/messages.pot @@ -0,0 +1,23 @@ +# Translations template for PROJECT. +# Copyright (C) 2018 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2018. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2018-11-04 17:35+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.6.0\n" + +#: app/templates/index.html:5 +msgid "Welcome" +msgstr "" + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2bd4d95 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,24 @@ +alembic==0.9.9 +click==6.7 +dominate==2.3.1 +Flask==1.0.2 +Flask-Bootstrap==3.3.7.1 +Flask-Login==0.4.1 +Flask-Migrate==2.2.1 +Flask-Session==0.3.1 +Flask-SQLAlchemy==2.3.2 +Flask-Uploads==0.2.1 +Flask-WTF==0.14.2 +itsdangerous==0.24 +Jinja2==2.10 +Mako==1.0.7 +MarkupSafe==1.0 +python-dateutil==2.7.3 +python-editor==1.0.3 +six==1.11.0 +SQLAlchemy==1.2.8 +uuid==1.30 +visitor==0.1.3 +Werkzeug==0.14.1 +WTForms==2.2.1 +WTForms-SQLAlchemy==0.1 -- GitLab