/
views.py
462 lines (377 loc) · 16.7 KB
/
views.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
from flask import session, abort, flash, url_for, make_response, request, \
render_template, redirect, g, jsonify
from sqlalchemy.exc import IntegrityError
from flask.ext.login import login_user, logout_user, login_required, \
current_user
import uuid
import json
from models import Hunt, Participant, Item, Admin, Setting
from forms import HuntForm, AdminForm, AdminLoginForm, ParticipantForm, \
ItemForm, SettingForm
from hunt import app, login_manager, db, bcrypt
from utils import get_admin, get_settings, get_item, \
get_participant, item_path, validate_participant, get_intended_url, \
get_items, initialize_hunt, create_new_participant, \
valid_login, finished_setting, participant_registered,\
num_items_remaining, hunt_requirements_completed, found_ids_list
from xapi import WaxCommunicator
import logging
logger = logging.getLogger(__name__)
login_manager.login_view = "login"
def get_db():
return db
@app.errorhandler(500)
def internal_error(error):
logger.error("Problem!", error)
return "500 error"
@app.before_request
def before_request():
g.db = get_db()
@login_manager.user_loader
def load_user(userid):
return Admin.query.get(userid)
#################### ADMIN ROUTES ####################
@app.route('/login', methods=['GET', 'POST'])
def login():
errors = None
form = AdminLoginForm(request.form)
if request.method == 'POST' and form.validate():
admin = get_admin(g.db, form.email.data)
if valid_login(admin, form.email.data, form.password.data):
login_user(admin)
return redirect(url_for('hunts'))
flash('Invalid email and password combination')
else:
errors = form.errors
return make_response(render_template(
'homepage.html', form=form, display_login_link=True))
@app.route('/logout')
def logout():
logout_user()
return redirect(url_for('login'))
@app.route('/')
def root():
return login()
# create or list admins who can create hunts
@app.route('/admins', methods=['POST'])
def admins():
if request.method == 'POST':
form = AdminForm(request.form)
if form.validate():
admin = Admin()
form.populate_obj(admin)
admin.pw_hash = bcrypt.generate_password_hash(form.password.data)
g.db.session.add(admin)
g.db.session.commit()
login_user(get_admin(g.db, admin.email))
flash('Welcome to xAPI Scavenger Hunt', 'success')
logger.info(
'Admin registration form was submitted successfully for %s',
admin.email)
return make_response(render_template(
'settings.html', form=SettingForm()))
logger.info(
'Admin registration form was submitted with'
' invalid information. Errors: %s', form.errors)
flash(
'There was an error creating your admin profile.'
' Please try again.', 'warning')
return render_template(
'homepage.html', form=form, display_login_link=True)
return login()
# settings page primarily for connecting to Wax LRS
@app.route('/settings', methods=['GET', 'POST'])
@login_required
def settings():
errors = None
admin_settings = get_settings(
g.db, admin_id=current_user.admin_id) or Setting()
form = SettingForm(request.form)
if request.method == 'POST':
if form.validate():
already_completed = finished_setting(admin_settings)
form.populate_obj(admin_settings)
admin_settings.admin_id = current_user.admin_id
g.db.session.add(admin_settings)
g.db.session.commit()
url = 'hunts' if already_completed else 'new_hunt'
flash('Settings have been updated successfully', 'success')
return make_response(redirect(url_for(url)))
else:
logger.info(
'%s attempted to submit settings information'
' resulting in errors: %s', current_user.email, form.errors)
return make_response(render_template(
'settings.html', login=admin_settings.login, form=form,
password=admin_settings.password, wax_site=admin_settings.wax_site
))
# list hunts
@app.route('/hunts', methods=['GET'])
@login_required
def hunts():
hunts = Hunt.list_for_admin_id(g.db, current_user.admin_id)
return render_template('hunts.html', hunts=hunts)
# form to create new hunt
@app.route('/new_hunt', methods=['GET', 'POST'])
@login_required
def new_hunt():
setting = get_settings(g.db, admin_id=current_user.admin_id)
if not finished_setting(setting):
flash('You must complete your settings information before'
' creating a hunt', 'warning')
return redirect(url_for('settings'))
hunt = Hunt()
form = HuntForm(request.form)
if request.method == 'POST':
if form.validate():
hunt = initialize_hunt(form, hunt, current_user.admin_id, request)
try:
g.db.session.add(hunt)
g.db.session.commit()
except IntegrityError as e:
logger.warning(
'Exception found while creating hunt with an existing '
' name: %s\n Form data: %s ', e, form.data)
return jsonify(
{'hunt name': [{'name': ['hunt name already taken']}]}), 400
else:
flash('New scavenger hunt added', 'success')
logger.info('hunt, %s, created for admin with id, %s',
hunt.name, hunt.admin_id)
saved_hunt = g.db.session.query(Hunt).order_by(
Hunt.hunt_id.desc()).first()
return jsonify({'hunt_id': saved_hunt.hunt_id})
else:
logger.warning('Error creating hunt.\nForm errors: %s\nForm data: '
'%s ', form.errors, form.data)
return jsonify(form.errors), 400
domain = current_user.email.split('@')[-1]
return make_response(
render_template('new_hunt.html', form=form, domain=domain))
# page to view hunt
@app.route('/hunts/<int:hunt_id>', methods=['GET'])
@login_required
def hunt(hunt_id):
hunt = Hunt.find_by_id(g.db, hunt_id)
if hunt:
registered = []
unregistered = []
for participant in hunt.participants:
if participant.registered:
registered.append(participant)
else:
unregistered.append(participant)
return render_template(
'show_hunt.html', hunt=hunt, registered_participants=registered,
unregistered_participants=unregistered)
logger.info('Someone attempted to visit a hunt with id, %s, but it '
'does not exist', hunt_id)
abort(404)
# check googlecharts infographics api in April 2015 when they may or may
# not change the qrcode api
def get_qr_codes_response(hunt_id, item_id, condition):
hunt = Hunt.find_by_id(g.db, hunt_id)
if hunt:
item_paths = [
{'name': item.name, 'path': item_path(hunt_id, item.item_id)}
for item in hunt.items if condition(item, item_id)
]
return make_response(render_template(
'qrcodes.html', item_paths=item_paths))
abort(404)
@app.route('/hunts/<int:hunt_id>/qrcodes')
@login_required
def show_item_codes(hunt_id):
return get_qr_codes_response(hunt_id, '', lambda x, y: True)
@app.route('/hunts/<int:hunt_id>/items/<int:item_id>/qrcode', methods=['GET'])
@login_required
def show_item_code(hunt_id, item_id):
return get_qr_codes_response(
hunt_id, item_id, lambda item, item_id: item.item_id == item_id)
@app.route('/hunts/<int:hunt_id>/delete')
@login_required
def delete_hunt(hunt_id):
hunt = Hunt.find_by_id(g.db, hunt_id)
if hunt and hunt.admin_id == current_user.admin_id:
logger.info(
'preparing to delete hunt with hunt_id, {}'.format(hunt_id))
g.db.session.delete(hunt)
g.db.session.commit()
flash('Successfully deleted hunt: {}'.format(hunt.name), 'success')
hunts = Hunt.list_for_admin_id(g.db, current_user.admin_id)
return make_response(render_template('hunts.html', hunts=hunts))
abort(404)
################ SCAVENGER HUNT PARTICIPANT ROUTES ####################
# maybe just get rid of this
# form for scavenger hunt participant to enter email and name
@app.route('/get_started/hunts/<int:hunt_id>', methods=['GET'])
def get_started(hunt_id):
# todo: track duration
hunt = Hunt.find_by_id(g.db, hunt_id)
logger.info("Rendering getting started form for hunt, '%s'.", hunt.name)
return render_template('get_started.html', form=ParticipantForm(),
hunt_id=hunt_id, hunt=hunt)
# validate and register participant before redirecting back to hunt
@app.route('/register_participant', methods=['POST'])
def register_participant():
hunt_id = request.args['hunt_id']
hunt = Hunt.find_by_id(g.db, hunt_id)
if hunt:
form = ParticipantForm(request.form)
if form.validate():
email = form.email.data
logger.info(
'Participant registration form validated for hunt, "%s", and'
' email, %s.\nPreparing to validate participant against hunt'
' participation rules.', hunt.name, email)
participant_valid, err_msg = validate_participant(
g.db, email, hunt_id, hunt.participant_rule)
if participant_valid:
logger.info('The registering participant, %s, has been'
' validated against the hunt participation rules.'
' Preparing to find email in participant database'
' table.', email)
if not get_participant(g.db, email, hunt_id):
logger.info(
'Preparing to save new participant with email, %s,'
' to hunt, %s', email, hunt.name)
create_new_participant(g.db, form, hunt_id)
scavenger_info = {'email': email, 'name': form.name.data}
session.update(scavenger_info)
admin_settings = get_settings(g.db, hunt_id=hunt_id)
logger.info(
"Retrieved settings associated with hunt with id, %s: %s",
hunt_id, admin_settings)
try:
lrs = WaxCommunicator(
admin_settings, request.host_url, hunt, None,
scavenger_info=scavenger_info)
except Exception as e:
logger.exception(
"Error instantiating WaxCommunicator while registering"
" participant: %s", e)
raise e
try:
lrs.send_began_hunt_statement()
except Exception as e:
logger.exception(
"Error sending began hunt statement: %s", e)
raise e
logger.info(
"name and email set to %s, and %s\n"
"preparing requested item information.",
session['name'], email)
redirect_url = get_intended_url(session, hunt_id)
return make_response(redirect(redirect_url))
else:
logger.info('participant attempted to register for'
' hunt with invalid form information.\n'
'Error message: %s\n. Form data: %s',
err_msg, request.form)
return err_msg
else:
# i don't think this can happen ever in the app
logger.warning('A user attempted to register for hunt with id, %s,'
' but the hunt could not be found. Form data: %s',
hunt_id, request.form)
abort(400)
# list of items for scavengers to scavenge
@app.route('/hunts/<int:hunt_id>/items', methods=['GET'])
def index_items(hunt_id):
hunt = Hunt.find_by_id(g.db, hunt_id)
if hunt:
email = session.get('email')
if email:
admin_settings = get_settings(g.db, hunt_id=hunt_id)
lrs = WaxCommunicator(
admin_settings, request.host_url, hunt, None,
{'email': email, 'name': session.get('name')})
state = lrs.get_state()
logger.info(
'preparing to render items from hunt_id, %s, for user, %s',
hunt_id, email)
return make_response(render_template(
'items.html', state=state, hunt=hunt,
num_remaining=num_items_remaining(state, hunt.items)))
session['intended_url'] = '/hunts/{}/items'.format(hunt_id)
return make_response(
render_template('welcome.html', hunt=hunt,
welcome=hunt.welcome_message,
action_url="/get_started/hunts/{}".format(
hunt_id)))
logger.info('Someone attempted to visit the items list for hunt with id, '
'%s, but this hunt does not exist', hunt_id)
abort(404)
# information about one item for scavenger to read
@app.route('/hunts/<int:hunt_id>/items/<int:item_id>', methods=['GET'])
def find_item(hunt_id, item_id):
logger.info(
'Participant is visiting route: /hunts/%s/items/%s', hunt_id, item_id)
admin_settings = get_settings(g.db, hunt_id=hunt_id)
# admin_settings found through hunt_id means hunt exists
logger.info("Settings retrieved for hunt with id, %s", hunt_id)
if finished_setting(admin_settings):
logger.info(
"Settings are complete. Preparing to retrieve item with id, %s",
item_id)
item = get_item(g.db, item_id, hunt_id)
if item:
logger.info(
"Item found. Preparing to retrieve hunt with id, %s ", hunt_id)
hunt = Hunt.find_by_id(g.db, hunt_id)
if participant_registered(g.db, session.get('email'), hunt_id):
logger.info(
"Participant, %s, has registered. Preparing to"
" retrieve data from the state api.", session.get('email'))
lrs = WaxCommunicator(
admin_settings, request.host_url, hunt, item,
scavenger_info={
'email': session.get('email'),
'name': session.get('name')
})
state = lrs.get_state()
found_again = str(item_id) in state
lrs.send_found_item_statement(found_again=found_again)
updated_state = {str(item.item_id): True}
hunt_previously_completed = state.get('hunt_completed')
# TODO: Don't send the whole state object, as discussed
state.update(updated_state)
if hunt_requirements_completed(state, hunt):
logger.info(
'Requirements for hunt, "%s", have been completed.',
hunt.name)
if not hunt_previously_completed:
lrs.send_completed_hunt_statement()
updated_state['hunt_completed'] = True
state.update(updated_state)
lrs.update_state_api_doc(updated_state)
found_ids = found_ids_list(state)
return make_response(render_template(
'items.html', item=item, hunt=hunt,
username=session.get('name'), found_ids=found_ids,
hunt_now_completed=state.get('hunt_completed'),
num_found=len(found_ids), num_items=len(hunt.items),
num_remaining=num_items_remaining(state, hunt.items),
found_again=found_again,
previously_completed=hunt_previously_completed))
else:
logger.info(
"Page visitor is not yet registered for this hunt."
" Preparing to redirect to the getting started page.")
session['intended_url'] = '/hunts/{}/items/{}'.format(
hunt_id, item_id)
return make_response(render_template(
'welcome.html', hunt=hunt, welcome=hunt.welcome_message,
action_url="/get_started/hunts/{}".format(hunt_id)))
abort(404)
@app.route('/oops')
def oops():
session.clear()
return make_response(render_template('goodbye.html'))
@app.route('/failblog')
def failblog():
try:
return doesnotexistsowillerror
except Exception as e:
logger.exception("Error for the failblog: %s", e)
raise e