Coverage for app/views.py : 90%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
2from flask import render_template, redirect, flash, request, url_for, send_from_directory, jsonify, abort
3from flask_login import login_required, current_user, login_user, logout_user
4from flask.logging import default_handler
5from app import app, db, models, forms, utils
6from app.utils.xlsx_handler import importXLSX
7from app.charts import DashboardCharts, SummaryCharts
8from datetime import datetime
9from sqlalchemy import func, desc
10import requests
11import json
12import uuid
13import os
15# handle favicon requests
16@app.route('/favicon.ico')
17def favicon():
18 return send_from_directory('static/media', 'favicon.ico', mimetype='image/vnd.microsoft.icon')
20@app.route('/')
21@login_required
22def index():
23 return render_template('testrun/index.html', items=utils.getItemCategories())
25@app.route('/<string:item_type>')
26@login_required
27def item_list(item_type):
28 # placeholder for import form
29 form = None
30 # get item list by type
31 if item_type == 'testrun':
32 items = models.Testrun.query.order_by(desc('created')).all()
33 # build form for importing a testrun
34 form = forms.TestrunImportForm()
36 elif item_type == 'testcase_sequence':
37 items = models.TestCaseSequence.query.order_by(desc('created')).all()
38 # DataFile update form
39 form = forms.DataFileUpdateForm()
40 elif item_type == 'testcase':
41 items = models.TestCase.query.order_by(desc('created')).all()
42 elif item_type == 'teststep_sequence':
43 items = models.TestStepSequence.query.order_by(desc('created')).all()
44 elif item_type == 'teststep':
45 items = models.TestStepExecution.query.order_by(desc('created')).all()
46 else:
47 app.logger.warning(f'Item type "{item_type}" does not exist. Requested by "{current_user}".')
48 flash(f'Item type "{item_type}" does not exist.', 'warning')
49 return redirect(url_for('index'))
51 return render_template('testrun/item_list.html', type=item_type, items=items, form=form)
54@app.route('/<string:item_type>/<string:item_id>/delete', methods=['POST', 'DELETE'])
55@login_required
56def delete_item(item_type, item_id):
57 #
58 # delete item
59 #
61 # cascade delete
62 if request.method == 'POST':
63 app.logger.info(f'Cascade deletion of {utils.getItemType(item_type)} {item_id} requested by "{current_user}".')
64 try:
65 utils.deleteCascade(item_type, uuid.UUID(item_id).bytes)
66 flash(f'Item {utils.getItemType(item_type)} {item_id} and its children have been successfully deleted.', 'success')
67 app.logger.info(
68 f'Item {utils.getItemType(item_type)} {item_id} and its children have been successfully deleted by "{current_user}".'
69 )
70 return jsonify({'success': 'OK'}), 200
71 except Exception as error:
72 app.logger.error(f'Failed to cascade delete {utils.getItemType(item_type)} {item_id} requested by "{current_user}": {error}')
73 return jsonify({'error': 'Bad Request'}), 400
75 # delete single item
76 elif request.method == 'DELETE':
77 # get item by type and id
78 idAsBytes = uuid.UUID(item_id).bytes
79 app.logger.info(f'Deletion of {utils.getItemType(item_type)} {item_id} requested by "{current_user}".')
80 if item_type == 'testrun':
81 item = models.Testrun.query.get(idAsBytes)
82 elif item_type == 'testcase_sequence':
83 item = models.TestCaseSequence.query.get(idAsBytes)
84 elif item_type == 'testcase':
85 item = models.TestCase.query.get(idAsBytes)
86 elif item_type == 'teststep_sequence':
87 item = models.TestStepSequence.query.get(idAsBytes)
88 elif item_type == 'teststep':
89 item = models.TestStepExecution.query.get(idAsBytes)
90 else:
91 app.logger.warning(f'Item type "{item_type}" does not exist. Requested by "{current_user}".')
92 flash(f'Item type "{item_type}" does not exist.', 'warning')
93 #return redirect(url_for('index'))
94 return jsonify({'error': 'Bad Request'}), 400
96 if item is None:
97 app.logger.error(f'Item {utils.getItemType(item_type)} {item_id} does not exist.')
98 return jsonify({'error': 'Bad Request'}), 400
100 db.session.delete(item)
101 db.session.commit()
102 app.logger.info(f'Item {utils.getItemType(item_type)} {item_id} successfully deleted by {current_user}.')
103 flash(f'Item "{item.name}" has been successfully deleted.', 'success')
104 return jsonify({'success': 'OK'}), 200
106 return jsonify({'error': 'Method Not Allowed'}), 405
109@app.route('/<string:item_type>/<string:item_id>/edit', methods=['GET', 'POST'])
110@login_required
111def edit_item(item_type, item_id):
112 #
113 # edit item
114 #
116 idAsBytes = uuid.UUID(item_id).bytes
117 if item_type == 'testrun':
118 item = models.Testrun.query.get(idAsBytes)
119 form = forms.TestrunCreateForm.new()
120 if request.method == 'GET':
121 form.global_settings.data = item.global_settings
122 form.testcase_sequences.data = [f'{x.uuid}' for x in item.testcase_sequences]
123 elif item_type == 'testcase_sequence':
124 item = models.TestCaseSequence.query.get(idAsBytes)
125 form = forms.TestCaseSequenceCreateForm.new()
126 if request.method == 'GET':
127 form.classname.data = f'{item.classname.id}'
128 form.datafiles.data = [f'{x.uuid}' for x in item.datafiles]
129 form.testcases.data = [f'{x.uuid}' for x in item.testcases]
130 elif item_type == 'testcase':
131 item = models.TestCase.query.get(idAsBytes)
132 form = forms.TestCaseCreateForm.new()
133 if request.method == 'GET':
134 form.classname.data = f'{item.classname.id}'
135 form.browser_type.data = f'{item.browser_type.id}'
136 form.testcase_type.data = f'{item.testcase_type.id}'
137 form.teststep_sequences.data = [f'{x.uuid}' for x in item.teststep_sequences]
138 elif item_type == 'teststep_sequence':
139 item = models.TestStepSequence.query.get(idAsBytes)
140 form = forms.TestStepSequenceCreateForm.new(item_id=item_id)
141 if request.method == 'GET':
142 form.classname.data = f'{item.classname.id}'
143 form.teststeps.data =[f'{x.uuid}' for x in item.teststeps]
144 elif item_type == 'teststep':
145 item = models.TestStepExecution.query.get(idAsBytes)
146 form = forms.TestStepCreateForm.new()
147 if request.method == 'GET':
148 form.activity_type.data = f'{item.activity_type.id}'
149 form.locator_type.data = f'{item.locator_type.id}' if item.locator_type else '0'
150 # model extension
151 form.locator.data = item.locator or ''
152 if item.optional:
153 form.optional.data = '1'
154 else:
155 form.optional.data = '0'
156 if item.timeout:
157 form.timeout.data = f'{item.timeout}'
158 else:
159 form.timeout.data = ''
160 form.release.data = item.release or ''
161 form.value.data = item.value or ''
162 form.value2.data = item.value2 or ''
163 form.comparison.data = utils.getComparisonId(item.comparison)
165 else:
166 app.logger.warning(f'Item type "{item_type}" does not exist. Requested by "{current_user}".')
167 flash(f'Item type "{item_type}" does not exist.', 'warning')
168 return redirect(url_for('index'))
170 if request.method == 'GET':
171 form.name.data = item.name
172 form.description.data = item.description
174 if form.validate_on_submit():
175 # update item data
176 item.name = form.name.data
177 item.description = form.description.data
178 # testrun
179 if item_type == 'testrun':
180 item.editor = current_user
181 item.edited = datetime.utcnow()
182 item.testcase_sequences=[models.TestCaseSequence.query.get(uuid.UUID(x).bytes) for x in form.testcase_sequences.data]
183 item.global_settings = form.global_settings.data or None
184 # testcase sequence
185 elif item_type == 'testcase_sequence':
186 item.edited = datetime.utcnow()
187 item.classname = models.ClassName.query.get(int(form.classname.data))
188 item.datafiles = [models.DataFile.query.get(uuid.UUID(x).bytes) for x in form.datafiles.data]
189 item.testcases = [models.TestCase.query.get(uuid.UUID(x).bytes) for x in form.testcases.data]
190 # testcase
191 elif item_type == 'testcase':
192 item.editor = current_user
193 item.edited = datetime.utcnow()
194 item.classname = models.ClassName.query.get(int(form.classname.data))
195 item.browser_type = models.BrowserType.query.get(int(form.browser_type.data))
196 item.testcase_type = models.TestCaseType.query.get(int(form.testcase_type.data))
197 item.teststep_sequences = [models.TestStepSequence.query.get(uuid.UUID(x).bytes) for x in form.teststep_sequences.data]
198 # step sequence
199 elif item_type == 'teststep_sequence':
200 item.editor = current_user
201 item.edited = datetime.utcnow()
202 item.classname = models.ClassName.query.get(int(form.classname.data))
203 item.teststeps = [models.TestStepExecution.query.get(uuid.UUID(x).bytes) for x in form.teststeps.data]
204 # test step
205 elif item_type == 'teststep':
206 item.editor = current_user
207 item.edited = datetime.utcnow()
208 item.activity_type = models.ActivityType.query.get(int(form.activity_type.data))
209 item.locator_type = models.LocatorType.query.get(int(form.locator_type.data))
210 # model extension
211 item.locator = form.locator.data
212 item.optional = [False, True][int(form.optional.data)]
213 try:
214 item.timeout = float(form.timeout.data)
215 except ValueError:
216 item.timeout = None
217 item.release = form.release.data or None
218 item.value = form.value.data or None
219 item.value2 = form.value2.data or None
220 if form.comparison.data == '0':
221 item.comparison = None
222 else:
223 item.comparison = utils.COMPARISONS[int(form.comparison.data)]
226 # update item in db
227 db.session.commit()
228 app.logger.info(f'Edited {item_type} {item_id} by {current_user}.')
229 flash(f'Item "{item.name}" successfully updated.', 'success')
230 return redirect(url_for('item_list', item_type=item_type))
233 return render_template('testrun/edit_item.html', type=item_type, item=item, form=form)
236@app.route('/<string:item_type>/new', methods=['GET', 'POST'])
237@login_required
238def new_item(item_type):
239 #
240 # create new item
241 #
242 if item_type == 'testrun':
243 form = forms.TestrunCreateForm.new()
244 chips = ['testcase_sequences']
245 elif item_type == 'testcase_sequence':
246 form = forms.TestCaseSequenceCreateForm.new(defaults=(request.method == 'GET'))
247 chips = ['datafiles', 'testcases']
248 elif item_type == 'testcase':
249 form = forms.TestCaseCreateForm.new(defaults=(request.method == 'GET'))
250 chips = ['teststep_sequences']
251 elif item_type == 'teststep_sequence':
252 form = forms.TestStepSequenceCreateForm.new(defaults=(request.method == 'GET'))
253 chips = ['teststeps']
254 elif item_type == 'teststep':
255 form = forms.TestStepCreateForm.new(defaults=(request.method == 'GET'))
256 chips = []
257 else:
258 app.logger.warning(f'Item type "{item_type}" does not exist. Requested by "{current_user}".')
259 flash(f'Item type "{item_type}" does not exist.', 'warning')
260 return redirect(url_for('index'))
262 if form.validate_on_submit():
263 # create new item
264 # testrun
265 if item_type == 'testrun':
266 item = models.Testrun(
267 name=form.name.data,
268 description=form.description.data,
269 creator=current_user,
270 testcase_sequences=[models.TestCaseSequence.query.get(uuid.UUID(x).bytes) for x in form.testcase_sequences.data],
271 global_settings=form.global_settings.data or None,
272 )
273 # testcase sequence
274 elif item_type == 'testcase_sequence':
275 item = models.TestCaseSequence(
276 name=form.name.data,
277 description=form.description.data,
278 creator=current_user,
279 classname=models.ClassName.query.get(int(form.classname.data)),
280 datafiles=[models.DataFile.query.get(uuid.UUID(x).bytes) for x in form.datafiles.data],
281 testcases=[models.TestCase.query.get(uuid.UUID(x).bytes) for x in form.testcases.data],
282 )
283 # testcase
284 elif item_type == 'testcase':
285 item = models.TestCase(
286 name=form.name.data,
287 description=form.description.data,
288 creator=current_user,
289 classname=models.ClassName.query.get(int(form.classname.data)),
290 browser_type=models.BrowserType.query.get(int(form.browser_type.data)),
291 testcase_type=models.TestCaseType.query.get(int(form.testcase_type.data)),
292 teststep_sequences=[models.TestStepSequence.query.get(uuid.UUID(x).bytes) for x in form.teststep_sequences.data],
293 )
294 # step sequence
295 elif item_type == 'teststep_sequence':
296 item = models.TestStepSequence(
297 name=form.name.data,
298 description=form.description.data,
299 creator=current_user,
300 classname=models.ClassName.query.get(int(form.classname.data)),
301 teststeps=[models.TestStepExecution.query.get(uuid.UUID(x).bytes) for x in form.teststeps.data],
302 )
303 # test step
304 elif item_type == 'teststep':
305 item = models.TestStepExecution(
306 name=form.name.data,
307 description=form.description.data,
308 creator=current_user,
309 activity_type=models.ActivityType.query.get(int(form.activity_type.data)),
310 locator_type=models.LocatorType.query.get(int(form.locator_type.data)),
311 # model extension
312 locator=form.locator.data or None,
313 optional=[False, True][int(form.optional.data)],
314 )
315 try:
316 item.timeout = float(form.timeout.data)
317 except ValueError:
318 item.timeout = None
319 item.release = form.release.data or None
320 item.value = form.value.data or None
321 item.value2 = form.value2.data or None
322 if form.comparison.data == '0':
323 item.comparison = None
324 else:
325 item.comparison = utils.COMPARISONS[int(form.comparison.data)]
327 # save item to db
328 db.session.add(item)
329 db.session.commit()
330 app.logger.info(f'Created {item_type} {item.uuid} by {current_user}.')
331 flash(f'Item "{item.name}" successfully created.', 'success')
332 return redirect(url_for('item_list', item_type=item_type))
334 return render_template('testrun/create_item.html', type=item_type, chips=chips, form=form)
337@app.route('/testrun/<string:item_id>/run')
338@login_required
339def run_testrun(item_id):
340 #
341 # runs Testrun via API Web Service
342 #
344 # check for complete set of Testrun's layers
345 try:
346 utils.getTestrunIfCorrect(item_id)
347 except Exception as error:
348 app.logger.error(f'Failed to execute Testrun {item_id} by {current_user}. {error}.')
349 flash(f'ERROR: Testrun {item_id} cannot be executed. {error}', 'danger')
350 return redirect(url_for('item_list', item_type='testrun'))
352 # export to JSON
353 try:
354 json_testrun = utils.exportJSON(item_id, shouldBeSaved=False)
355 except Exception as error:
356 app.logger.error(f'Failed to export Testrun {item_id} by {current_user}. {error}')
357 flash(f'ERROR: Failed to export Testrun {item_id}. {error}', 'danger')
358 return redirect(url_for('item_list', item_type='testrun'))
360 # call API
361 url = '/'.join(('http:/', app.config.get('BAANGT_API_HOST'), app.config.get('BAANGT_API_EXECUTE')))
362 app.logger.info(f'Call execution of Testrun {item_id} by {current_user}. API URL: {url}')
363 try:
364 r = requests.post(url, json=json_testrun)
365 except Exception as error:
366 app.logger.error(f'Failed to connect to {app.config.get("BAANGT_API_HOST")} by {current_user}. {error}')
367 flash(f'ERROR: Cannot establish connection: {app.config.get("BAANGT_API_HOST")}', 'danger')
368 return redirect(url_for('item_list', item_type='testrun'))
370 # API server error handler
371 if r.status_code == 500:
372 app.logger.error('ERROR: API Server internal error')
373 flash('ERROR: API Server internal error', 'danger')
374 return redirect(url_for('item_list', item_type='testrun'))
376 app.logger.info(f'API response: status code {r.status_code}\n {r.text}')
378 if r.status_code == 202:
379 # get results
380 jsonResult = json.loads(r.text)
381 # store to db
382 tr_call = models.TestRunCall(
383 id=uuid.UUID(jsonResult['id']).bytes,
384 creator=current_user,
385 testrun_id=utils.idAsBytes(item_id),
386 )
387 db.session.add(tr_call)
388 db.session.commit()
389 app.logger.info(f'Created Testrun Call {jsonResult["id"]} by {current_user}.')
390 return render_template('testrun/testrun_status.html', call=tr_call)
391 else:
392 flash(f'ERROR: {r.text}', 'danger')
393 return redirect(url_for('item_list', item_type='testrun'))
396@app.route('/testrun/<string:item_id>/export/<string:export_format>')#, methods=['GET', 'POST'])
397@login_required
398def export_testrun(item_id, export_format):
399 #
400 # export Testrun object to <export_format>
401 # <export_format> is one of the following:
402 # XLSX
403 # JSON
404 #
406 if export_format.upper() == 'XLSX':
407 # export to XLSX
408 try:
409 result = utils.exportXLSX(item_id)
410 except Exception as e:
411 app.logger.error(f'Failed to export Testrun {item_id} to XLSX by {current_user}. {e}')
412 return jsonify({'error': f'Failed to export Testrun: {e}'}), 400
414 elif export_format.upper() == 'JSON':
415 # export to JSON
416 try:
417 result = utils.exportJSON(item_id)
418 except Exception as e:
419 app.logger.error(f'Failed to export Testrun {item_id} to JSON by {current_user}. {e}')
420 return jsonify({'error': f'Failed to export Testrun: {e}'}), 400
422 else:
423 # invalid format
424 app.logger.error(f'Failed to export Testrun {item_id} by {current_user}. Invalid format: {export_format}')
425 return jsonify({'error': f'format {export_format} is not supported'}), 400
427 # successful export
428 url = url_for('static', filename=f'files/{result}')
429 app.logger.info(f'Testrun {item_id} exported to {export_format} by {current_user}. Target: static/files/{result}')
431 #return f'Success: <a href="{url}">{result}</a>'
432 return jsonify({
433 'success': 'OK',
434 'filename': result,
435 'url': url,
436 }), 200
438@app.route('/testrun/import', methods=['POST'])
439@login_required
440def import_testsun():
441 #
442 # imports testrun from file
443 #
445 # import only from XLSX is available now
446 form = forms.TestrunImportForm()
448 if form.validate_on_submit():
449 try:
450 #utils.importXLSX(form.file.data, datafiles=form.dataFiles)
451 importXLSX(form.file.data, datafiles=form.dataFiles)
452 app.logger.info(f'Testrun successfully imported from "{form.file.data.filename}" by {current_user}.')
453 flash(f'Testrun successfully imported from "{form.file.data.filename}"', 'success')
454 except Exception as error:
455 # discard changes
456 db.session.rollback()
457 app.logger.error(f'Failed to import Testrun from "{form.file.data.filename}" by {current_user}. {error}.')
458 flash(f'ERROR: Cannot import Testrun from "{form.file.data.filename}". {error}.', 'danger')
459 else:
460 flash(f'File is required for import', 'warning')
462 return redirect(url_for('item_list', item_type='testrun'))
464@app.route('/datafile/upload', methods=['POST'])
465@login_required
466def upload_datafile():
467 # get datafile
468 file = request.files.get('datafile')
469 if file is None:
470 return jsonify({'error': 'Request does not contain dataFile'}), 400
472 # upload data file
473 try:
474 file_id = utils.importDataFile(file)
475 except Exception as error:
476 app.logger.error(f'Failed to uplod DataFile "{file.filename}" by {current_user}. {error}.')
477 return jsonify({'error': f'Failed upload DataFile "{file.filename}". {error}.'}), 400
479 return jsonify({'id': file_id, 'name': file.filename}), 200
481@app.route('/datafile/update', methods=['POST'])
482@login_required
483def update_datafile():
484 #
485 # update datafile with specified id
486 #
488 form = forms.DataFileUpdateForm()
490 if form.validate_on_submit():
491 file = form.datafile.data
493 # upload data file
494 try:
495 file_id = utils.importDataFile(
496 file,
497 datafile_id=form.datafile_id.data,
498 )
499 except Exception as error:
500 app.logger.error(f'Failed to uplod DataFile "{file.filename}" by {current_user}. {error}.')
501 flash(f'ERROR: Failed to upload DataFile "{file.filename}". {error}.', 'danger')
502 return redirect(url_for('item_list', item_type='testcase_sequence'))
504 return redirect(url_for(
505 'item_list',
506 item_type='testcase_sequence',
507 show=form.testcaseseq_id.data,
508 _anchor=f'item{form.testcaseseq_id.data}',
509 ))
511 flash(f'ERROR: Failed to upload DataFile. {form.datafile.errors}.', 'danger')
512 return redirect(url_for('item_list', item_type='testcase_sequence'))
517@app.route('/testrun/<string:item_id>/import', methods=['POST'])
518@login_required
519def update_testsun(item_id):
520 #
521 # imports testrun from file
522 #
524 # update only from XLSX is available now
525 form = forms.TestrunImportForm()
527 if form.validate_on_submit():
529 # update items
530 try:
531 #utils.importXLSX(form.file.data, item_id=uuid.UUID(item_id).bytes)
532 importXLSX(form.file.data, testrun_id=item_id)
533 app.logger.info(f'Testrun {item_id} successfully updated from "{form.file.data.filename}" by {current_user}.')
534 flash(f'Testrun {item_id} has been successfully updated from "{form.file.data.filename}"', 'success')
535 except Exception as error:
536 # discard changes
537 db.session.rollback()
538 app.logger.error(f'Failed to update Testrun {item_id} from "{form.file.data.filename}" by {current_user}. {error}.')
539 flash(f'ERROR: Cannot update Testrun {item_id} from "{form.file.data.filename}". {error}.', 'danger')
540 else:
541 flash(f'File is required for import', 'warning')
543 return redirect(url_for('item_list', item_type='testrun'))
546#
547# Testrun Reports
548#
550@app.route('/summary/<string:call_id>', methods=['GET', 'POST'])
551@login_required
552def get_results(call_id):
553 #
554 # show Testrun call results
555 #
557 # fetch call data
558 if request.method == 'POST':
560 # call API
561 url = '/'.join(('http:/', app.config.get('BAANGT_API_HOST'), app.config.get('BAANGT_API_STATUS'), call_id))
562 app.logger.info(f'Call results by {current_user}. API URL: {url}')
563 try:
564 r = requests.get(url)
565 except Exception as error:
566 app.logger.error(f'Failed to connect to {app.config.get("TESTRUN_SERVICE_HOST")} by {current_user}. {error}')
567 return jsonify({'error': f'Cannot establish connection: {app.config.get("TESTRUN_SERVICE_HOST")}'}), 400
569 app.logger.info(f'API response: status code {r.status_code} - JSON {r.text}')
571 # API server error handler
572 if r.status_code == 500:
573 app.logger.error('ERROR: API Server internal error')
574 return jsonify({'error': 'API Server internal error'}), 400
576 if r.status_code == 200:
577 # build charts
578 try:
579 charts = SummaryCharts(json.loads(r.text))
580 return jsonify(charts.get_charts()), 200
581 except Exception as e:
582 app.logger.error(f'ERROR: {e}')
583 return jsonify({'error': f'{e}'}), 400
585 return jsonify({'error': f'Response: {r.status_code}'}), 400
587 # show results
588 call = models.TestRunCall.query.get(uuid.UUID(call_id).bytes)
589 if not call:
590 abort(404)
592 if not call.is_finished and not call.is_failed:
593 # update Testrun calls
594 try:
595 utils.update_call(call)
596 except Exception as e:
597 flash(f'{e}', 'warning')
599 if not call.is_finished or call.is_failed:
600 return render_template('testrun/testrun_status.html', call=call)
602 return render_template('testrun/summary.html', call=call)
605@app.route('/chart/<string:testrun_id>/<string:stage>', methods=['POST'])
606@login_required
607def get_charts(testrun_id, stage):
608 #
609 # fetches chart data
610 #
612 testrun = models.Testrun.query.get(uuid.UUID(testrun_id).bytes)
614 # build charts
615 try:
616 charts = DashboardCharts(testrun.finished_calls(stage=stage))
617 return jsonify(charts.get_charts()), 200
618 except Exception as e:
619 print(f'ERROR: {e}')
620 return jsonify({'error': 'error'}), 400
623@app.route('/dashboard')
624@login_required
625def dashboard():
626 #
627 # Dashboard view
628 #
630 # update Testrun calls
631 try:
632 utils.update_all_calls()
633 except Exception as e:
634 flash(f'{e}', 'warning')
636 testruns = [tr for tr in models.Testrun.query.order_by(func.lower(models.Testrun.name)).all() if tr.finished_calls()]
637 stages = []
638 stage_set = set()
639 for tr in testruns:
640 stages.append({call.stage for call in tr.calls if call.is_finished})
641 stage_set.update(stages[-1])
643 return render_template('testrun/dashboard.html', testruns=testruns, stages=stages, stage_set=stage_set)
646#
647# user authentication
648#
650@app.route('/signup', methods=['GET', 'POST'])
651def register():
652 if current_user.is_authenticated:
653 return redirect(url_for('index'))
655 form = forms.SingupForm()
656 if form.validate_on_submit():
657 # create user
658 user = models.User(username=form.username.data)
659 user.set_password(form.password.data)
660 db.session.add(user)
661 db.session.commit()
662 # login
663 login_user(user, remember=True)
664 flash(f'User {user.username.capitalize()} successfully created!', 'success')
665 app.logger.info(f"User '{user.username}' successfully created")
666 return redirect(url_for('index'))
668 return render_template('auth/signup.html', form=form)
670@app.route('/login', methods=['GET', 'POST'])
671def login():
672 if current_user.is_authenticated:
673 return redirect(url_for('index'))
675 form = forms.LoginForm()
676 if form.validate_on_submit():
677 user = models.User.query.filter_by(username=form.username.data).first()
678 if user and user.verify_password(form.password.data):
679 login_user(user, remember=True)
680 flash(f'Welcome {user.username.capitalize()}!', 'success')
681 app.logger.info(f"User '{user.username}' logged in")
682 return redirect(url_for('index'))
684 return render_template('auth/login.html', form=form)
686@app.route('/logout')
687@login_required
688def logout():
689 app.logger.info(f"User '{current_user.username}' logged out")
690 logout_user()
692 return redirect(url_for('login'))
695#
696# BAANGT documentations
697#
699@app.route('/docs/')
700def help_index():
701 return render_template('docs/index.html')
703@app.route('/docs/<string:topic>')
704def help(topic):
705 if topic == 'testrun':
706 try:
707 with open(app.config.get('BAANGT_TESTRUN_GLOBALS_DEFAULT'), 'r') as f:
708 global_settings = json.load(f)
709 return render_template('docs/index.html', topic=topic, globals=json.dumps(global_settings, indent=2))
710 except Exception as e:
711 app.logger.error(f'Failed to load Testrun global settings: {e}')
713 return render_template('docs/index.html', topic=topic)