routes.py 18.5 KB
Newer Older
1
2
3
import os
import random
import secrets
4
from datetime import datetime, date
Ossi Laine's avatar
Ossi Laine committed
5
import json
6

Ossi Laine's avatar
Ossi Laine committed
7
8
9
10
11
12
from flask import (render_template,
                   request,
                   session,
                   flash,
                   redirect,
                   url_for)
13
14
15
from sqlalchemy import and_
from flask_login import current_user, login_user, logout_user, login_required

Ossi Laine's avatar
Ossi Laine committed
16
from app import app, db
Timo Heikkilä's avatar
Timo Heikkilä committed
17
18
from app.models import background_question, experiment
from app.models import background_question_answer
Ossi Laine's avatar
Ossi Laine committed
19
from app.models import page, question, embody_question, embody_answer
Timo Heikkilä's avatar
Timo Heikkilä committed
20
from app.models import background_question_option
Timo Heikkilä's avatar
Timo Heikkilä committed
21
from app.models import answer_set, answer, forced_id
Timo Heikkilä's avatar
Timo Heikkilä committed
22
from app.models import user, trial_randomization
23
from app.forms import LoginForm, RegisterForm, StartWithIdForm
Ossi Laine's avatar
Ossi Laine committed
24
from app.utils import saved_data_as_file, map_answers_to_questions
Timo Heikkilä's avatar
Timo Heikkilä committed
25

26
# Stimuli upload folder setting
Timo Heikkilä's avatar
Timo Heikkilä committed
27
28
APP_ROOT = os.path.dirname(os.path.abspath(__file__))

29

Timo Heikkilä's avatar
Timo Heikkilä committed
30
31
32
33
@app.route('/')
@app.route('/index')
def index():
    experiments = experiment.query.all()
34

Timo Heikkilä's avatar
Timo Heikkilä committed
35
36
37
    if session:
        flash("")
    else:
Timo Heikkilä's avatar
Timo Heikkilä committed
38
39
        #flash("sessio ei voimassa")
        session['language'] = "English"
40

Timo Heikkilä's avatar
Timo Heikkilä committed
41
42
43
44
45
46
    return render_template('index.html', title='Home', experiments=experiments)


@app.route('/consent')
def consent():
    exp_id = request.args.get('exp_id', None)
Timo Heikkilä's avatar
Timo Heikkilä committed
47
    experiment_info = experiment.query.filter_by(idexperiment=exp_id).first()
48

49
50
    instruction_paragraphs = str(experiment_info.short_instruction)
    instruction_paragraphs = instruction_paragraphs.split('<br>')
51

52
53
    consent_paragraphs = str(experiment_info.consent_text)
    consent_paragraphs = consent_paragraphs.split('<br>')
Timo Heikkilä's avatar
Timo Heikkilä committed
54
55
56
57

    if experiment_info.use_forced_id == 'On':
        return redirect(url_for('begin_with_id', exp_id=exp_id))

58
59
60
61
62
    return render_template('consent.html',
                           exp_id=exp_id,
                           experiment_info=experiment_info,
                           instruction_paragraphs=instruction_paragraphs,
                           consent_paragraphs=consent_paragraphs)
Timo Heikkilä's avatar
Timo Heikkilä committed
63
64


Timo Heikkilä's avatar
Timo Heikkilä committed
65
66
67
@app.route('/set_language')
def set_language():
    session['language'] = request.args.get('language', None)
Timo Heikkilä's avatar
Timo Heikkilä committed
68
69
    lang = request.args.get('lang', None)
    return redirect(url_for('index', lang=lang))
Timo Heikkilä's avatar
Timo Heikkilä committed
70

71

Timo Heikkilä's avatar
Timo Heikkilä committed
72
73
74
75
76
77
@app.route('/remove_language')
def remove_language():
    experiments = experiment.query.all()
    return render_template('index.html', title='Home', experiments=experiments)


Timo Heikkilä's avatar
Timo Heikkilä committed
78
79
@app.route('/session')
def participant_session():
80
    '''Set up session variables and create answer_set (database level sessions)'''
81

82
    # start session
Timo Heikkilä's avatar
Timo Heikkilä committed
83
84
    session['exp_id'] = request.args.get('exp_id', None)
    session['agree'] = request.args.get('agree', None)
85
86

    # 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
Timo Heikkilä's avatar
Timo Heikkilä committed
87
88
89
90
    if 'begin_with_id' in session:
        session['user'] = session['begin_with_id']
        session.pop('begin_with_id', None)
    else:
91
        # 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']
Timo Heikkilä's avatar
Timo Heikkilä committed
92
93
94
95
96
97
        random_id = secrets.token_hex(3)
        check_id = answer_set.query.filter_by(session=random_id).first()

        while check_id is not None:
            random_id = secrets.token_hex(3)
            check_id = answer_set.query.filter_by(session=random_id).first()
98

Timo Heikkilä's avatar
Timo Heikkilä committed
99
        session['user'] = random_id
100

101
    # Set session status variables
102
103
    exp_status = experiment.query.filter_by(
        idexperiment=session['exp_id']).first()
104

Ossi Laine's avatar
Ossi Laine committed
105
    # Create answer set for the participant in the database
Timo Heikkilä's avatar
Timo Heikkilä committed
106
107
    the_time = datetime.now()
    the_time = the_time.replace(microsecond=0)
108

Ossi Laine's avatar
Ossi Laine committed
109
    # Check which question type is the first in answer_set (embody is first if enabled)
110
111
112
113
    answer_set_type = 'slider'
    if exp_status.embody_enabled:
        answer_set_type = 'embody'

114
    participant_answer_set = answer_set(experiment_idexperiment=session['exp_id'],
115
116
117
118
119
                                        session=session['user'],
                                        agreement=session['agree'],
                                        answer_counter='0',
                                        answer_type=answer_set_type,
                                        registration_time=the_time,
120
                                        last_answer_time=the_time)
Timo Heikkilä's avatar
Timo Heikkilä committed
121
122
    db.session.add(participant_answer_set)
    db.session.commit()
123

124
125
    # 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
Timo Heikkilä's avatar
Timo Heikkilä committed
126
    if exp_status.randomization == 'On':
127

Timo Heikkilä's avatar
Timo Heikkilä committed
128
        session['randomization'] = 'On'
129
130
131
132

        # create a list of page id:s for the experiment
        experiment_pages = page.query.filter_by(
            experiment_idexperiment=session['exp_id']).all()
Timo Heikkilä's avatar
Timo Heikkilä committed
133
        original_id_order_list = [(int(o.idpage)) for o in experiment_pages]
134
135
136

        # create a randomized page id list
        helper_list = original_id_order_list
Timo Heikkilä's avatar
Timo Heikkilä committed
137
        randomized_order_list = []
138

Timo Heikkilä's avatar
Timo Heikkilä committed
139
140
141
142
        for i in range(len(helper_list)):
            element = random.choice(helper_list)
            helper_list.remove(element)
            randomized_order_list.append(element)
143
144
145
146

        # 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()
Timo Heikkilä's avatar
Timo Heikkilä committed
147
        original_id_order_list = [(int(o.idpage)) for o in experiment_pages]
148

Timo Heikkilä's avatar
Timo Heikkilä committed
149
        for c in range(len(original_id_order_list)):
150
151
            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'])
Timo Heikkilä's avatar
Timo Heikkilä committed
152
153
            db.session.add(random_page)
            db.session.commit()
154

Timo Heikkilä's avatar
Timo Heikkilä committed
155
156
157
    if exp_status.randomization == "Off":
        session['randomization'] = "Off"

158
159
160
    # 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()
Timo Heikkilä's avatar
Timo Heikkilä committed
161
    session['answer_set'] = session_id_for_participant.idanswer_set
162
163
164
165
    # 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()
Timo Heikkilä's avatar
Timo Heikkilä committed
166
167
168
169
170
    if mediatype:
        session['type'] = mediatype.type
    else:
        flash('No pages or mediatype set for experiment')
        return redirect('/')
171
172

    # Redirect user to register page
Timo Heikkilä's avatar
Timo Heikkilä committed
173
174
175
    if 'user' in session:
        user = session['user']
        return redirect('/register')
176

Timo Heikkilä's avatar
Timo Heikkilä committed
177
178
179
180
181
    return "Session start failed return <a href = '/login'></b>" + "Home</b></a>"


@app.route('/register', methods=['GET', 'POST'])
def register():
182

Timo Heikkilä's avatar
Timo Heikkilä committed
183
184
    form = RegisterForm(request.form)
    questions_and_options = {}
185
186
187
188
189
190
191
192
193
194
195
196
    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

Timo Heikkilä's avatar
Timo Heikkilä committed
197
    form.questions1 = questions_and_options
198
199
200

    if request.method == 'POST' and form.validate():

Timo Heikkilä's avatar
Timo Heikkilä committed
201
202
203
        data = request.form.to_dict()
        for key, value in data.items():

204
            # tähän db insertit
Timo Heikkilä's avatar
Timo Heikkilä committed
205

206
207
208
209
210
            # 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)
Timo Heikkilä's avatar
Timo Heikkilä committed
211
212
213
214
215
            db.session.add(participant_background_question_answers)
            db.session.commit()

        return redirect('/instructions')

216
    return render_template('register.html', form=form)
Timo Heikkilä's avatar
Timo Heikkilä committed
217
218
219
220


@app.route('/begin_with_id', methods=['GET', 'POST'])
def begin_with_id():
221
222
    '''Begin experiment with experiment ID. GET -method returns login page for starting the 
    experiment and POST -method verifys users ID before starting new experiment'''
223

Timo Heikkilä's avatar
Timo Heikkilä committed
224
225
    exp_id = request.args.get('exp_id', None)
    form = StartWithIdForm()
Timo Heikkilä's avatar
Timo Heikkilä committed
226
    experiment_info = experiment.query.filter_by(idexperiment=exp_id).first()
227

228
229
    instruction_paragraphs = str(experiment_info.short_instruction)
    instruction_paragraphs = instruction_paragraphs.split('<br>')
230

231
232
    consent_paragraphs = str(experiment_info.consent_text)
    consent_paragraphs = consent_paragraphs.split('<br>')
Timo Heikkilä's avatar
Timo Heikkilä committed
233
234

    if form.validate_on_submit():
235

Timo Heikkilä's avatar
Timo Heikkilä committed
236
        variable = form.participant_id.data
237
238
239
240
241
242
243

        # check if participant ID is found from db with this particular ID. If a match is found inform about error
        participant = answer_set.query.filter(and_(
            answer_set.session == variable, answer_set.experiment_idexperiment == exp_id)).first()
        is_id_valid = forced_id.query.filter(and_(
            forced_id.pregenerated_id == variable, forced_id.experiment_idexperiment == exp_id)).first()

Timo Heikkilä's avatar
Timo Heikkilä committed
244
245
        if participant is not None:
            flash(_('ID already in use'))
246
247
248
            return redirect(url_for('begin_with_id', exp_id=exp_id))

        # if there was not a participant already in DB:
Timo Heikkilä's avatar
Timo Heikkilä committed
249
        if participant is None:
250

Timo Heikkilä's avatar
Timo Heikkilä committed
251
252
            if is_id_valid is None:
                flash(_('No such ID set for this experiment'))
253
                return redirect(url_for('begin_with_id', exp_id=exp_id))
Timo Heikkilä's avatar
Timo Heikkilä committed
254
255

            else:
256
                # save the participant ID in session list for now, this is deleted after the session has been started in participant_session-view
Timo Heikkilä's avatar
Timo Heikkilä committed
257
                session['begin_with_id'] = form.participant_id.data
258
                return render_template('consent.html', exp_id=exp_id, experiment_info=experiment_info, instruction_paragraphs=instruction_paragraphs, consent_paragraphs=consent_paragraphs)
259

Timo Heikkilä's avatar
Timo Heikkilä committed
260
261
262
263
264
265
    return render_template('begin_with_id.html', exp_id=exp_id, form=form)


@app.route('/admin_dryrun', methods=['GET', 'POST'])
@login_required
def admin_dryrun():
266

Timo Heikkilä's avatar
Timo Heikkilä committed
267
268
269
    exp_id = request.args.get('exp_id', None)
    form = StartWithIdForm()
    experiment_info = experiment.query.filter_by(idexperiment=exp_id).first()
Timo Heikkilä's avatar
Timo Heikkilä committed
270
271

    if form.validate_on_submit():
272
273
274
275

        # 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()
Timo Heikkilä's avatar
Timo Heikkilä committed
276
277
        if participant is not None:
            flash('ID already in use')
278
279
280
            return redirect(url_for('admin_dryrun', exp_id=exp_id))

        # if there was not a participant already in DB:
Timo Heikkilä's avatar
Timo Heikkilä committed
281
        if participant is None:
282
            # save the participant ID in session list for now, this is deleted after the session has been started in participant_session-view
Timo Heikkilä's avatar
Timo Heikkilä committed
283
            session['begin_with_id'] = form.participant_id.data
Timo Heikkilä's avatar
Timo Heikkilä committed
284
            return render_template('consent.html', exp_id=exp_id, experiment_info=experiment_info)
285

Timo Heikkilä's avatar
Timo Heikkilä committed
286
287
    return render_template('admin_dryrun.html', exp_id=exp_id, form=form)

Timo Heikkilä's avatar
Timo Heikkilä committed
288
289
290

@app.route('/instructions')
def instructions():
291

Timo Heikkilä's avatar
Timo Heikkilä committed
292
    participant_id = session['user']
293
294
295
    instructions = experiment.query.filter_by(
        idexperiment=session['exp_id']).first()

296
297
    instruction_paragraphs = str(instructions.instruction)
    instruction_paragraphs = instruction_paragraphs.split('<br>')
298

299
    return render_template('instructions.html', instruction_paragraphs=instruction_paragraphs, participant_id=participant_id)
Timo Heikkilä's avatar
Timo Heikkilä committed
300
301
302
303
304


@app.route('/researcher_login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
Timo Heikkilä's avatar
Timo Heikkilä committed
305
        #flash("allready logged in")
Timo Heikkilä's avatar
Timo Heikkilä committed
306
307
308
        return redirect(url_for('index'))
    form = LoginForm()
    if form.validate_on_submit():
309
310
        user_details = user.query.filter_by(
            username=form.username.data).first()
Timo Heikkilä's avatar
Timo Heikkilä committed
311
312
313
        if user_details is None or not user_details.check_password(form.password.data):
            flash('Invalid username or password')
            return redirect(url_for('login'))
314
        login_user(user_details, remember=form.remember_me.data)
Timo Heikkilä's avatar
Timo Heikkilä committed
315
        return redirect(url_for('index'))
316

Timo Heikkilä's avatar
Timo Heikkilä committed
317
318
319
320
321
322
323
324
325
326
327
328
#        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'))


329
330
@app.route('/view_research_notification')
def view_research_notification():
331

Timo Heikkilä's avatar
Timo Heikkilä committed
332
    exp_id = request.args.get('exp_id', None)
333
334
    image = experiment.query.filter_by(idexperiment=exp_id).first()
    research_notification_filename = image.research_notification_filename
335

336
    return render_template('view_research_notification.html', research_notification_filename=research_notification_filename)
Timo Heikkilä's avatar
Timo Heikkilä committed
337
338


339
340
341
@app.route('/download_csv')
@login_required
def download_csv():
Timo Heikkilä's avatar
Timo Heikkilä committed
342

343
    exp_id = request.args.get('exp_id', None)
344
345
346
    experiment_info = experiment.query.filter_by(idexperiment=exp_id).all()

    print(experiment_info)
Ossi Laine's avatar
Ossi Laine committed
347
348

    # answer sets with participant ids
349
350
    participants = answer_set.query.filter_by(
        experiment_idexperiment=exp_id).all()
Ossi Laine's avatar
Ossi Laine committed
351
352
353
354
355
356
357
358
359
360
361
362

    # pages aka stimulants
    pages = page.query.filter_by(experiment_idexperiment=exp_id).all()

    # background questions
    bg_questions = background_question.query.filter_by(
        experiment_idexperiment=exp_id).all()

    # question
    questions = question.query.filter_by(experiment_idexperiment=exp_id).all()

    # embody questions
363
364
    embody_questions = embody_question.query.filter_by(
        experiment_idexperiment=exp_id).all()
Ossi Laine's avatar
Ossi Laine committed
365
366
367
368
369

    csv = ''

    # create CSV-header
    header = 'participant id;'
370
371
    header += ';'.join([str(count) + '. bg_question: ' + question.background_question.strip()
                        for count, question in enumerate(bg_questions, 1)])
Timo Heikkilä's avatar
Timo Heikkilä committed
372

373
    for idx in range(1, len(pages) + 1):
Ossi Laine's avatar
Ossi Laine committed
374
        if len(questions) > 0:
375
376
            header += ';' + ';'.join(['page' + str(idx) + '_' + str(count) + '. slider_question: ' +
                                      question.question.strip() for count, question in enumerate(questions, 1)])
377

378
    for idx in range(1, len(pages) + 1):
Ossi Laine's avatar
Ossi Laine committed
379
        if len(embody_questions) > 0:
380
381
            header += ';' + ';'.join(['page' + str(idx) + '_' + str(count) + '. embody_question: ' +
                                      question.picture.strip() for count, question in enumerate(embody_questions, 1)])
Ossi Laine's avatar
Ossi Laine committed
382
383
384
385
386

    csv += header + '\r\n'
    answer_row = ''

    for participant in participants:
387
        # list only finished answer sets
388
        if participant.answer_counter > 0:
389
390
391
392
393
            try:
                # append user session id
                answer_row += participant.session + ';'

                # append background question answers
394
395
396
                bg_answers = background_question_answer.query.filter_by(
                    answer_set_idanswer_set=participant.idanswer_set).all()
                bg_answers_list = [str(a.answer).strip() for a in bg_answers]
397
398
                answer_row += ';'.join(bg_answers_list) + ';'

399
                # append slider answers
Ossi Laine's avatar
Ossi Laine committed
400
401
402
                slider_answers = answer.query.filter_by(
                    answer_set_idanswer_set=participant.idanswer_set) \
                    .order_by(answer.page_idpage) \
403
404
                    .all()

Ossi Laine's avatar
Ossi Laine committed
405
406
407
408
409
410
411
412
413
414
415
416
417
418

                pages_and_questions = {}
                for p in pages:
                    questions_list = [(p.idpage, a.idquestion) for a in questions]
                    pages_and_questions[p.idpage] = questions_list

                _questions = [
                    item for sublist in pages_and_questions.values() for item in sublist]

                answers_list = map_answers_to_questions(slider_answers, _questions)

                # typecast elemnts to string
                answers_list = [str(a).strip() for a in answers_list]

419
420
421
                answer_row += ';'.join(answers_list) + \
                    ';' if slider_answers else len(
                        questions) * len(pages) * ';'
422
423

                # append embody answers (coordinates)
424
                # save embody answers as bitmap images
Ossi Laine's avatar
Ossi Laine committed
425
426
427
                embody_answers = embody_answer.query.filter_by(
                    answer_set_idanswer_set=participant.idanswer_set) \
                    .order_by(embody_answer.page_idpage) \
428
                    .all()
429

430
                answers_list = []
431

432
                for answer_data in embody_answers:
433

root's avatar
root committed
434
                    try:
435
436
437
438
439
440
                        coordinates = json.loads(answer_data.coordinates)
                        em_height = coordinates.get('height', 600) + 2
                        em_width = coordinates.get('width', 200) + 2

                        coordinates_to_bitmap = [
                            [0 for x in range(em_height)] for y in range(em_width)]
root's avatar
root committed
441

442
443
444
445
                        coordinates = list(
                            zip(coordinates.get('x'), coordinates.get('y')))

                        for point in coordinates:
root's avatar
root committed
446
447

                            try:
448
449
450
                                # for every brush stroke, increment the pixel 
                                # value for every brush stroke
                                coordinates_to_bitmap[point[0]][point[1]] += 0.1
root's avatar
root committed
451
                            except IndexError:
452
                                continue
root's avatar
root committed
453
454

                        answers_list.append(json.dumps(coordinates_to_bitmap))
455

root's avatar
root committed
456
                    except ValueError as err:
457
458
459
460
                        app.logger(err)

                answer_row += ';'.join(answers_list) if embody_answers else \
                    len(embody_questions) * len(pages) * ';'
461
462

                # old way to save only visited points:
463
464
465
                # answers_list = [json.dumps(
                #   list(zip( json.loads(a.coordinates)['x'],
                #   json.loads(a.coordinates)['y']))) for a in embody_answers]
466
467
468
469
470
471

            except TypeError as err:
                print(err)

            csv += answer_row + '\r\n'
            answer_row = ''
Ossi Laine's avatar
Ossi Laine committed
472

Ossi Laine's avatar
Ossi Laine committed
473
474
    filename = "experiment_{}_{}.csv".format(
        exp_id, date.today().strftime("%Y-%m-%d"))
Ossi Laine's avatar
Ossi Laine committed
475

Ossi Laine's avatar
Ossi Laine committed
476
    return saved_data_as_file(filename, csv)
477

Timo Heikkilä's avatar
Timo Heikkilä committed
478

479
@app.route('/researcher_info')
Timo Heikkilä's avatar
Timo Heikkilä committed
480
@login_required
481
482
def researcher_info():
    return render_template('researcher_info.html')
Timo Heikkilä's avatar
Timo Heikkilä committed
483

Timo Heikkilä's avatar
Timo Heikkilä committed
484

root's avatar
root committed
485
# EOF