Compare commits

...

1262 Commits

Author SHA1 Message Date
viveksantayana 25fb3fdda4 Update python version number 2025-10-06 16:14:04 +01:00
viveksantayana fa104d7d1b Updated for before_first_request decorator deprecation 2025-10-06 16:11:57 +01:00
viveksantayana cb747b4832 Removed deprecated cryptography method 2025-10-06 16:11:07 +01:00
viveksantayana 845fdcdf8d Updated dependencies 2025-10-06 16:09:57 +01:00
viveksantayana 54e1653cb5 Fixed typo 2025-10-06 15:14:07 +01:00
viveksantayana 716206dc65 Disable randomisation 2025-10-06 14:50:18 +01:00
viveksantayana a8eda4078d Added Barrowland Bears Korfball Club to list of clubs dropdown 2025-10-06 14:50:00 +01:00
viveksantayana d28cd6daed Updated instructions for the test 2023-10-20 20:58:50 +01:00
viveksantayana 57b25cd214 Formatted DataTable date to ISO-8601 for sorting 2023-07-01 21:48:36 +01:00
viveksantayana 8013a776a9 Merge branch 'development' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test into development 2023-07-01 21:26:24 +01:00
viveksantayana aa1f46ee62 Bugfix: response to invalid exam codes 2023-07-01 21:24:29 +01:00
viveksantayana ba851cb7dc Added analysis UI 2023-03-05 00:33:15 +00:00
viveksantayana fcc4d55947 Delete redundant lines 2023-03-05 00:32:15 +00:00
viveksantayana a56358b8dd Added question parser for analysis 2023-03-05 00:31:33 +00:00
viveksantayana 179a608089 Made randomising of question order optional 2023-03-05 00:29:25 +00:00
viveksantayana a1289da09c Added analysis button and scripting 2023-03-05 00:28:54 +00:00
viveksantayana ea86fd9ae6 Updated parameter for new library version 2023-03-05 00:27:30 +00:00
viveksantayana 76d60546e2 Cleared whitespace 2023-03-05 00:27:03 +00:00
viveksantayana 9a02048199 Added get_file method to datasets 2023-03-05 00:26:39 +00:00
viveksantayana c9ad8e87cd Bugfix: rendering htm elements in results page 2023-03-04 19:48:49 +00:00
viveksantayana 3714919ba5 Add analysis function 2023-03-04 18:56:34 +00:00
viveksantayana 1026cc71a9 Add dataset to entry on creation 2023-03-04 18:56:10 +00:00
viveksantayana 07fb170656 Add dataset and entry relation to database models 2023-03-04 18:55:30 +00:00
viveksantayana 1ea93994ab Load and register analysis module blueprints 2023-03-04 18:54:47 +00:00
viveksantayana 607b132996 Removed passing of unused values 2023-03-04 18:54:02 +00:00
viveksantayana 7aa4d81e65 Corrected indentation 2023-03-04 18:53:02 +00:00
viveksantayana 0ef39dcfbe Update navbars 2023-03-04 18:52:13 +00:00
viveksantayana e1517b89c0 Add analysis module 2023-03-04 18:51:50 +00:00
viveksantayana d0ed228824 Updated libraries 2023-03-04 18:49:35 +00:00
viveksantayana a2c52a4261 Corrected value to id property of entry 2023-02-28 20:59:12 +00:00
viveksantayana b2c9bdd7d2 Removed CSRF time limit 2023-02-28 20:37:23 +00:00
viveksantayana 7536c33a48 Tidied up form. Removed help text. 2023-02-03 17:08:50 +00:00
viveksantayana 850c2b13b7 Data use disclaimer for UI choices 2023-02-03 17:02:32 +00:00
viveksantayana eb69979f59 Spelling consistency advice 2023-02-03 17:02:16 +00:00
viveksantayana 95cea46a8f Merge branch 'master' into development 2023-02-03 16:31:41 +00:00
viveksantayana 02a1129390 Adding jquery ui css
Nesting script inside jquery function call
2023-02-03 16:26:31 +00:00
viveksantayana 438e09f1ec Bugfix: club field selector 2023-02-03 16:15:50 +00:00
viveksantayana 9241e1c0f7 Added club suggestion auto-complete 2023-02-03 16:06:06 +00:00
viveksantayana 8deefb9035 Bugfix: displaying scores for incomplete entries 2023-02-02 22:44:19 +00:00
viveksantayana 4f2984deea Bugfix: exception for incomplete entry dates 2023-02-02 22:38:49 +00:00
viveksantayana 70d2325579 Bugfix: datetime reference 2023-02-02 22:26:19 +00:00
viveksantayana 36d840c752 Typo 2023-02-02 22:16:29 +00:00
viveksantayana 4400446718 Bugfix: Sorting for empty dates 2023-02-02 22:16:09 +00:00
viveksantayana adead30a77 Updated privacy policy 2022-11-01 08:51:03 +00:00
viveksantayana 487f24732d Copy edit privacy notice. 2022-11-01 08:50:46 +00:00
viveksantayana 3c06cebddf Updated credit in footer to identify maintainer. 2022-11-01 08:48:54 +00:00
viveksantayana d1d52fa4b6 source /home/vivek/Git/ska-referee-test/ref-test/env/bin/activateMerge branch 'development' 2022-09-13 12:05:37 +01:00
viveksantayana 80dc8b3cff Fixed docker-compose depends_on mappings 2022-09-13 12:03:40 +01:00
viveksantayana a9ccd64de2 Updated dependency list 2022-09-13 11:17:17 +01:00
viveksantayana f5b9758bb1 Removed unused imports 2022-09-13 11:17:03 +01:00
viveksantayana 84570d5974 Added indices to various database fields 2022-09-13 11:01:28 +01:00
viveksantayana edb8241ad3 Removed the word Beta from site title 2022-09-13 11:00:53 +01:00
viveksantayana 644a539ed9 Changed to conventional extension for sqlite db 2022-09-13 11:00:07 +01:00
viveksantayana f05568b0de source /home/vivek/Git/ska-referee-test/ref-test/env/bin/activateMerge branch 'development' 2022-08-27 09:44:07 +01:00
viveksantayana da4a3e41c6 Bugfix: Wrong account password for updating user 2022-08-27 09:42:48 +01:00
viveksantayana 77f86f7102 Bugfix: Corrected dataset name in test editor 2022-08-23 11:03:18 +01:00
viveksantayana 358695977f Docker compose glitches 2022-08-20 18:21:52 +01:00
viveksantayana ddfd75c1f8 Added selecting database to Readme 2022-08-20 17:46:45 +01:00
viveksantayana f4642767ac Tweaking formatting of docker-compose file 2022-08-20 17:28:45 +01:00
viveksantayana 2f729de40b mysql compose 2022-08-20 17:25:07 +01:00
viveksantayana d68beb938f Tweaking docker-compose 2022-08-20 17:21:21 +01:00
viveksantayana ca667f7896 Create database before first request 2022-08-20 16:51:13 +01:00
viveksantayana 0cc00ef911 Updated install script to only create SQLite file 2022-08-20 16:50:34 +01:00
viveksantayana 5ec2a86d08 Added certbot directory for nginx to serve renewal 2022-08-20 15:46:19 +01:00
viveksantayana cd57eca7d3 Restructure install script 2022-08-20 15:40:41 +01:00
viveksantayana a46338fdcb Update gitignore and dockerignore 2022-08-20 15:39:50 +01:00
viveksantayana 40f1cebb7b Unsaved files 2022-08-20 14:58:31 +01:00
viveksantayana 2a6478f3cf Clean up unnecessary exception imports 2022-08-20 14:53:49 +01:00
viveksantayana b6e250a7cd Generate random root password for MySQL 2022-08-20 14:48:56 +01:00
viveksantayana bcee2eedd0 Generalise exception handling 2022-08-20 14:47:46 +01:00
viveksantayana d9837246de Updated SQL Json support 2022-08-20 13:01:32 +01:00
viveksantayana 62fac48904 Making logs accessible from install root 2022-08-20 13:00:09 +01:00
viveksantayana 2bf0eeb33d Bugfix: variable definition for different actions 2022-08-20 12:59:26 +01:00
viveksantayana 72f2af1df8 Include connection errors in exception handling 2022-08-20 12:58:47 +01:00
viveksantayana 168b2b288a Added mysql-related database variables
Added options for different database engines
2022-08-20 12:01:08 +01:00
viveksantayana 9a5f69f889 Added database-related env variables 2022-08-20 11:59:33 +01:00
viveksantayana 7d6f256392 Added PyMySQL driver dependency 2022-08-20 11:59:02 +01:00
viveksantayana 866c9b10cf Exception handling for database queries 2022-08-20 10:56:43 +01:00
viveksantayana b8fd65d856 Added command line password reset tool. 2022-08-19 15:29:27 +01:00
viveksantayana 5490bd083f Make reset script executable during image creation 2022-08-19 15:28:27 +01:00
viveksantayana 3cb78055ff Added check for password reset from command line 2022-08-19 15:28:05 +01:00
viveksantayana f9d85a8028 Updated .env variable for future Flask versions
FLASK_ENV has been deprecated
2022-08-19 15:27:25 +01:00
viveksantayana 4f193e7fa5 Corrected password length prompt 2022-08-19 15:26:51 +01:00
viveksantayana df3149abba Exception to cookie consent check for view/static 2022-08-19 13:29:29 +01:00
viveksantayana 7ab87c2966 Exception handling for database commit operations 2022-08-19 13:25:20 +01:00
viveksantayana f4f501def5 Deleted redundant line 2022-08-19 13:24:54 +01:00
viveksantayana 1c57950558 Exception handling and logging for SMTP errors
Should mitigate internal server errors if SMTP server fails.
2022-08-19 12:07:38 +01:00
viveksantayana f132cdbeef Updated dependencies 2022-08-19 12:03:20 +01:00
viveksantayana 0387c05055 Updated readme formatting 2022-08-19 12:02:54 +01:00
viveksantayana 552b2ffc47 Updating some of the references, deleting old ones 2022-08-19 11:17:19 +01:00
viveksantayana a2e859af5d Tidied up file mounting locations and server alias 2022-08-18 17:17:56 +01:00
viveksantayana 81b09190de Corrected erroneous mounting 2022-08-18 17:14:15 +01:00
viveksantayana ed100ee9e5 Added server directive for root folder 2022-08-18 17:08:03 +01:00
viveksantayana 5dc6c4998d Corrected typo 2022-08-18 17:07:48 +01:00
viveksantayana 0d68233d41 Update Dockerfile mounting,fix Nginx config typo 2022-08-18 17:00:51 +01:00
viveksantayana 4caac25b14 Merge remote-tracking branch 'refs/remotes/origin/master' 2022-08-18 16:33:42 +01:00
viveksantayana 3defe020f5 Bugfix: Dockerfile mounting static directory 2022-08-18 16:32:27 +01:00
viveksantayana f14085f4c1 Typo correction 2022-08-17 16:38:31 +01:00
viveksantayana be5343a4bd Added decorator to test availability of datasets
Used decorator tool to validate dataset exists on views
2022-08-17 16:37:03 +01:00
viveksantayana 2da8eb7712 Added cross-reference to question viewer
Changed question number countint to be consistent with viewer
2022-08-17 16:36:16 +01:00
viveksantayana 3a0abaac6a Stylistic change of name dataset to questions 2022-08-17 16:35:22 +01:00
viveksantayana b15f76701e Code clean up: redundant semicolons
Made variable declaration style in for loops consistent
2022-08-17 16:34:59 +01:00
viveksantayana 02290e968c Added question viewer functionality
Added view questions panel to editor interface
Added view questions section of web site
Added links to navbars
2022-08-17 16:32:58 +01:00
viveksantayana 294f1e42f7 Added timezone env variable 2022-08-11 17:31:20 +01:00
viveksantayana 070ce19fcc Added instructions on updating 2022-08-11 17:19:05 +01:00
viveksantayana 615e59fc6d Updated form error handling 2022-08-11 16:58:00 +01:00
viveksantayana 68314a4ed2 Add handling of anonymous user when updating account 2022-08-11 16:28:47 +01:00
viveksantayana b90761fd2c Simplify variabe nale 2022-08-11 16:28:13 +01:00
viveksantayana af03193217 Change user variable name 2022-08-11 16:13:31 +01:00
viveksantayana 730a75c44d Bugfix: reset password 2022-08-11 16:05:28 +01:00
viveksantayana 70883db5ad Changed dockerignore stricture 2022-08-11 13:09:41 +01:00
viveksantayana 7cefb487da Bugfix: reset password 2022-08-11 13:09:34 +01:00
viveksantayana 2e1b01ec9b Bugfix: reset password 2022-08-11 13:02:55 +01:00
viveksantayana a7a5a03991 Consistency in paths for templates 2022-08-11 13:02:41 +01:00
viveksantayana b36c6bfd18 Bugfix: reset password 2022-08-11 12:51:17 +01:00
viveksantayana a613b0006b Bugfix: password reset 2022-08-11 12:44:42 +01:00
viveksantayana d4db8692e7 Remove debug line 2022-08-11 12:13:33 +01:00
viveksantayana 37ad36da31 Add debug for email reset 2022-08-11 11:42:50 +01:00
viveksantayana d140f93d25 Bugfix: hude club field when empty 2022-08-11 11:41:57 +01:00
viveksantayana 26a6248a61 Tidied up nnecessary imports 2022-08-11 11:39:53 +01:00
viveksantayana 9f8ea16974 Bugfix: button display 2022-08-11 11:01:29 +01:00
viveksantayana bc5ec44145 Bugfix default datetime 2022-08-11 10:59:12 +01:00
viveksantayana ff5b19fa0b Editing text: remove repetition 2022-08-11 10:24:15 +01:00
viveksantayana 6c50be49c6 Bugfix: default time for exam creation 2022-08-11 10:23:40 +01:00
viveksantayana 8bfe028e2c Make certbot silent 2022-06-22 15:05:56 +01:00
viveksantayana 519394a656 Store data in docker volume instead of project dir 2022-06-22 15:05:44 +01:00
viveksantayana 9e1c9caec6 Updated config to have defaults for keys
Removed abstraction of data location for image build
2022-06-22 11:56:36 +01:00
viveksantayana ea850c9ae2 Added defaults for config keys to avoid exceptions 2022-06-22 11:45:37 +01:00
viveksantayana 591b868920 Separated install script to avoid launch errors 2022-06-22 11:20:30 +01:00
viveksantayana 91dc93758a Added nginx static serving editor files 2022-06-22 11:18:53 +01:00
viveksantayana 5d27baee08 Editor flash message bugfix 2022-06-22 10:46:43 +01:00
viveksantayana 1254cf3698 Bugfix install script dhparam 2022-06-22 09:55:59 +01:00
viveksantayana efab086057 Gitignore bugfix 2022-06-22 09:31:16 +01:00
viveksantayana 06db47c597 Push production version to Master 2022-06-22 02:01:34 +01:00
viveksantayana c04c824585 Editor client javascript and css 2022-06-22 01:58:36 +01:00
viveksantayana 8eb7fb6869 Cleaned unnecessary import 2022-06-22 01:58:17 +01:00
viveksantayana db88b84ecb Added editor home page form 2022-06-22 01:57:58 +01:00
viveksantayana 13c587b7da Added Editor api views 2022-06-22 01:57:45 +01:00
viveksantayana 2b2a6ddd25 Updated json structure validation
Only works with the data list
File parsed in the View layer
2022-06-22 01:57:03 +01:00
viveksantayana 26a6b45d75 Added dataset name support 2022-06-22 01:56:13 +01:00
viveksantayana c6c62fc34c Changed the layer at which json files are parsed
Updated dataset database model
Updated create and edit function to use data list instead of file
2022-06-22 01:55:55 +01:00
viveksantayana 6bbdb8fced Corrected timestamping 2022-06-22 01:54:53 +01:00
viveksantayana c633a474b5 Updated dataset edit button handlers 2022-06-22 01:54:40 +01:00
viveksantayana 5af99d85b5 Added editor link to navbar 2022-06-22 01:54:00 +01:00
viveksantayana 1e7124262e Added support for dataset names 2022-06-22 01:53:46 +01:00
viveksantayana 2f509af1de Added redirect on login to previous page 2022-06-22 01:53:06 +01:00
viveksantayana 3c8c1b5c16 Finished making editor console 2022-06-22 01:52:40 +01:00
viveksantayana 3988559920 Cleaned up unused file 2022-06-22 01:47:07 +01:00
viveksantayana 8988fee55d Merge branch 'editor' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test into editor 2022-06-21 02:50:09 +01:00
viveksantayana 86d1522ca1 Rectified editor script 2022-06-21 02:47:09 +01:00
viveksantayana ed53b771ef Finished designing the editor console 2022-06-21 02:44:23 +01:00
viveksantayana bc3b811fc9 Finished designing the editor console 2022-06-21 02:44:23 +01:00
viveksantayana f314566591 Merge branch 'master' into editor 2022-06-20 13:55:39 +01:00
viveksantayana 4b6dbd4441 Merge branch 'master' into editor 2022-06-20 13:55:39 +01:00
viveksantayana 1ef34465c2 Debug install script 2022-06-20 12:53:40 +01:00
viveksantayana 8b0ea1fec3 Make config production ready 2022-06-20 12:28:31 +01:00
viveksantayana 39acebb3a6 Make config production ready 2022-06-20 12:28:31 +01:00
viveksantayana d9962f18ed Production ready v.0.2.1 2022-06-20 12:28:00 +01:00
viveksantayana d8044a7c76 Make config production ready 2022-06-20 12:27:32 +01:00
viveksantayana 3025e83b66 Editor styling 2022-06-20 12:22:29 +01:00
viveksantayana a02a58a8db Editor styling 2022-06-20 12:22:29 +01:00
viveksantayana de6910b4bf Merge branch 'master' into editor 2022-06-20 12:16:25 +01:00
viveksantayana 7bb93afacb Merge branch 'master' into editor 2022-06-20 12:16:25 +01:00
viveksantayana 2663d5e3b7 Tidied up unused imports 2022-06-20 12:15:28 +01:00
viveksantayana 500beed4cc Merge branch 'master' into editor 2022-06-20 12:13:09 +01:00
viveksantayana d83999aa43 Merge branch 'master' into editor 2022-06-20 12:13:09 +01:00
viveksantayana 6a09559b70 Database URI absolute path fix 2022-06-20 12:10:52 +01:00
viveksantayana 26227a66c5 App Factory pattern 2022-06-20 12:10:37 +01:00
viveksantayana d6836915bb Prevent edit user from duplicating email address 2022-06-20 12:09:31 +01:00
viveksantayana 49a7fb1007 More elegant error handling 2022-06-20 11:27:05 +01:00
viveksantayana 90bc30757a Added local server for development 2022-06-20 11:26:44 +01:00
viveksantayana fac3839ea3 Merge branch 'master' into editor 2022-06-19 13:25:02 +01:00
viveksantayana d8d5e92453 Merge branch 'master' into editor 2022-06-19 13:25:02 +01:00
viveksantayana 12207d1159 Changed modules to extensions 2022-06-19 13:22:24 +01:00
viveksantayana ac02f4dee1 Changed structure of referencing data 2022-06-19 13:22:05 +01:00
viveksantayana a050a1eccf Merge branch 'master' into editor 2022-06-19 11:21:22 +01:00
viveksantayana 8d91dd1d30 Merge branch 'master' into editor 2022-06-19 11:21:22 +01:00
viveksantayana 76fa1e1dd9 Removed todo tags 2022-06-19 11:17:21 +01:00
viveksantayana 6d5f74bd62 Tidied up code 2022-06-19 11:17:00 +01:00
viveksantayana 2e00d503c8 Added detailed data validation 2022-06-19 11:13:47 +01:00
viveksantayana 43cc0a5652 Added detailed data validation 2022-06-19 10:48:17 +01:00
viveksantayana 4ce6536e33 Added detailed data validation 2022-06-19 10:48:17 +01:00
viveksantayana 1f60054d46 Edited base template to set up Editor scripts/css 2022-06-19 10:47:50 +01:00
viveksantayana 33bc7993fa Edited base template to set up Editor scripts/css 2022-06-19 10:47:50 +01:00
viveksantayana 418dfe7a70 Added templates and static files for editor 2022-06-18 09:53:36 +01:00
viveksantayana 645f69440f Added templates and static files for editor 2022-06-18 09:53:36 +01:00
viveksantayana e1e279e939 Base editor template 2022-06-18 09:43:07 +01:00
viveksantayana c197f6cb76 Base editor template 2022-06-18 09:43:07 +01:00
viveksantayana 7fe1afb348 Create editor files 2022-06-18 09:39:31 +01:00
viveksantayana bed186f6b5 Create editor files 2022-06-18 09:39:31 +01:00
viveksantayana 516c2cdf81 Buxfix: static folders bypass cookie consent 2022-06-18 09:26:05 +01:00
viveksantayana 8f9b78ac32 Merge branch 'editor' 2022-06-18 02:18:45 +01:00
viveksantayana 17b985d238 Bugfix: 404 errors with request.endpoint
Fixed static folder 404 errors
2022-06-18 02:18:07 +01:00
viveksantayana 69a0791a6d Bug fixes to main branch 2022-06-18 02:11:29 +01:00
viveksantayana 4414d1720e Typo 2022-06-17 13:16:40 +01:00
viveksantayana 43895bead0 Renamed containers 2022-06-17 13:01:27 +01:00
viveksantayana 067ef4fd7f Production debug 2022-06-17 12:58:46 +01:00
viveksantayana 73f31016fd Updated to newest Docker version syntax 2022-06-17 10:28:34 +01:00
viveksantayana 25115a6fae Wrote installation instructions in the Readme 2022-06-17 02:01:06 +01:00
viveksantayana 6028ac2d3c Renamed services
Made configs and scripts consistent
2022-06-17 02:00:37 +01:00
viveksantayana 225ef71518 Added conditional env loading.
Making it work in both dev and production without changing .env files.
2022-06-17 01:10:11 +01:00
viveksantayana fbae88eed1 Production Ready 2022-06-17 01:09:15 +01:00
viveksantayana 647d156802 Dotenv production setting 2022-06-16 15:20:54 +01:00
viveksantayana 08a140a73b Finished common section of app 2022-06-16 15:19:26 +01:00
viveksantayana a8a01e17da Updated wsgi 2022-06-16 14:19:04 +01:00
viveksantayana 3f59d1b1b7 Debug time limit handling 2022-06-16 14:15:18 +01:00
viveksantayana 5123365567 Debug password reset methods 2022-06-16 14:14:21 +01:00
viveksantayana d0166f0901 Debug html formatting 2022-06-16 14:13:25 +01:00
viveksantayana f6231dc779 Debug password reset url 2022-06-16 14:13:07 +01:00
viveksantayana 5c8435d39e Added cookie consent 2022-06-16 13:22:06 +01:00
viveksantayana e4e07c43b4 Updated nginx configs 2022-06-16 13:21:27 +01:00
viveksantayana d202e83189 Updated server files 2022-06-16 12:52:55 +01:00
viveksantayana e264b808fc Added email notifications 2022-06-16 12:46:03 +01:00
viveksantayana 4b08c830a1 Finished quiz and debugging 2022-06-16 10:44:48 +01:00
viveksantayana b9d45f94fe Finished Quiz Console 2022-06-16 01:03:06 +01:00
viveksantayana 2ea778143e Finished admin console 2022-06-15 23:54:44 +01:00
viveksantayana 62160beab2 Restored static and template files 2022-06-15 11:43:04 +01:00
viveksantayana 1a7983052f Finished common views 2022-06-15 11:33:09 +01:00
viveksantayana a1bee61679 Completed admin views
Corrected model method return values
2022-06-15 11:23:38 +01:00
viveksantayana 126bf9203c Added a whole lot of views.
Finished quiz API views
Finished question generator and answer eval
2022-06-14 22:55:11 +01:00
viveksantayana a58f267586 Added more views 2022-06-12 22:48:13 +01:00
viveksantayana 22878b5398 Added relationships between database models 2022-06-12 21:20:09 +01:00
viveksantayana 52b44128fa Tool function to parse test codes 2022-06-12 21:04:21 +01:00
viveksantayana 8439d99949 Added models and views 2022-06-12 21:03:51 +01:00
viveksantayana 66e7b2b9f8 Views into module 2022-06-12 21:02:22 +01:00
viveksantayana 9459b93c9b Re-organised admin views into single module 2022-06-12 21:01:03 +01:00
viveksantayana 09e444344d Importing models to create database 2022-06-12 20:53:03 +01:00
viveksantayana 767dcede54 Changed location of views to avoid circular import 2022-06-12 20:51:36 +01:00
viveksantayana 4431564304 Created config module to avoid circular import 2022-06-12 20:50:57 +01:00
viveksantayana da821bcadb moved user model 2022-06-11 18:26:53 +01:00
viveksantayana b58a23cf13 Added new models 2022-06-11 18:26:39 +01:00
viveksantayana dc126459bc Made db.create_all() conditional 2022-06-11 18:19:03 +01:00
viveksantayana 2c5ed21011 Fixed the weird database issue 2022-06-11 18:08:24 +01:00
viveksantayana 59281db9cb merged 2022-06-11 15:39:53 +01:00
viveksantayana 2a3927a140 Merge branch 'sqlite-ground-up' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test into sqlite-ground-up 2022-06-11 15:38:24 +01:00
viveksantayana 9a225543c6 db create all the time 2022-06-11 15:27:27 +01:00
viveksantayana dd8685b103 Fixed database connection issue 2022-06-11 15:16:35 +01:00
viveksantayana 625ef8883b Fixed database connection issue 2022-06-11 15:16:35 +01:00
viveksantayana f903f9d060 Update reqs 2022-06-11 13:30:28 +01:00
viveksantayana eac9ee7ab1 Update reqs 2022-06-11 13:30:28 +01:00
viveksantayana 8946e3eaf3 Progress
Problems with database access and subdirectories still
2022-06-11 13:26:50 +01:00
viveksantayana b27016aaf4 Progress
Problems with database access and subdirectories still
2022-06-11 13:26:50 +01:00
viveksantayana 89788550fb Name correction 2022-06-11 11:33:06 +01:00
viveksantayana 6992a75855 Name correction 2022-06-11 11:33:06 +01:00
viveksantayana 9539ba22fe Progress 2022-06-11 11:29:15 +01:00
viveksantayana 85ced0cc20 Progress 2022-06-11 11:29:15 +01:00
viveksantayana eac6cac7bc Restore 2022-06-11 02:56:38 +01:00
viveksantayana fcfde34c72 Restore 2022-06-11 02:56:38 +01:00
viveksantayana 1b111727be Started from scratch and failed
Issue with register_blueprint
2022-06-11 02:39:47 +01:00
viveksantayana 436c8e0e2d Started from scratch and failed
Issue with register_blueprint
2022-06-11 02:39:47 +01:00
viveksantayana 9c0c7f6ba1 Added new files 2022-06-10 22:11:29 +01:00
viveksantayana 7af588da6c Added new files 2022-06-10 22:11:29 +01:00
viveksantayana f170ff5e52 Whitespace corrections 2022-04-17 18:42:40 +01:00
viveksantayana cfd750894a Whitespace corrections 2022-04-17 18:42:40 +01:00
viveksantayana 3bd16ae563 Bugfix change event not triggering 2021-12-08 13:25:50 +00:00
viveksantayana ede71f7d82 Bugfix change event not triggering 2021-12-08 13:25:50 +00:00
viveksantayana 2f6de34051 Added click area to select background colour 2021-12-08 13:21:18 +00:00
viveksantayana 27706572ed Added click area to select background colour 2021-12-08 13:21:18 +00:00
viveksantayana b9c4edeb48 update function call from attr to prop 2021-12-08 13:20:40 +00:00
viveksantayana 08da6d71c4 update function call from attr to prop 2021-12-08 13:20:40 +00:00
viveksantayana 587415c5db Local jQuery library fallback 2021-12-08 13:20:07 +00:00
viveksantayana c5a0bbb827 Local jQuery library fallback 2021-12-08 13:20:07 +00:00
viveksantayana c2d7dc7fe2 Changed answer option object semantics to indices
Evaluating answer should no longer require string matching
Answers evaluated based on matching index value integers
2021-12-08 12:49:32 +00:00
viveksantayana 8680c73e86 Changed answer option object semantics to indices
Evaluating answer should no longer require string matching
Answers evaluated based on matching index value integers
2021-12-08 12:49:32 +00:00
viveksantayana 0059ec5270 Merge branch 'master' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test 2021-12-08 12:46:49 +00:00
viveksantayana ff74e92297 Merge branch 'master' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test 2021-12-08 12:46:49 +00:00
viveksantayana ce07bdf8b2 Only show topics to revise if failed 2021-12-08 12:46:33 +00:00
viveksantayana 6b3b255cfd Only show topics to revise if failed 2021-12-08 12:46:33 +00:00
viveksantayana 7db0d055e5 Merge 2021-12-08 11:33:27 +00:00
viveksantayana ecdb5df561 Merge 2021-12-08 11:33:27 +00:00
viveksantayana 7c70da4b5c Removed personal information 2021-12-08 11:29:22 +00:00
viveksantayana c5b4d948f5 Removed personal information 2021-12-08 11:29:22 +00:00
viveksantayana b3791dfcf8 Removed personal information 2021-12-08 11:27:54 +00:00
viveksantayana c40ef7d070 Removed personal information 2021-12-08 11:27:54 +00:00
viveksantayana cd5fd686e9 Cookie bugfix, removed 'session' string from expiry/age 2021-12-08 11:26:18 +00:00
viveksantayana b8081bc1c8 Cookie bugfix, removed 'session' string from expiry/age 2021-12-08 11:26:18 +00:00
viveksantayana 6e1f7c6df1 Debug form error handlers 2021-12-07 16:17:59 +00:00
viveksantayana efec599225 Debug form error handlers 2021-12-07 16:17:59 +00:00
viveksantayana de0b24b042 Debug form error handlers 2021-12-07 16:17:59 +00:00
viveksantayana 614ad91e3d Debug form error handlers 2021-12-07 16:17:59 +00:00
viveksantayana ab4496f06d Named image 2021-12-07 16:03:56 +00:00
viveksantayana 6605620d9c Named image 2021-12-07 16:03:56 +00:00
viveksantayana 150224c1d5 Named image 2021-12-07 16:03:56 +00:00
viveksantayana cd4d52692c Named image 2021-12-07 16:03:56 +00:00
viveksantayana a836e0c9e3 Bug fix and data persistence 2021-12-07 15:52:58 +00:00
viveksantayana 2038965dcb Bug fix and data persistence 2021-12-07 15:52:58 +00:00
viveksantayana 88a41de647 Bug fix and data persistence 2021-12-07 15:52:58 +00:00
viveksantayana b4c94a7ddb Bug fix and data persistence 2021-12-07 15:52:58 +00:00
viveksantayana fc099dbbf7 Bugfix: security key location 2021-12-07 15:25:22 +00:00
viveksantayana f144097c5d Bugfix: security key location 2021-12-07 15:25:22 +00:00
viveksantayana a84c1037a2 Bugfix: security key location 2021-12-07 15:25:22 +00:00
viveksantayana 63f72e35d2 Bugfix: security key location 2021-12-07 15:25:22 +00:00
viveksantayana deab85289b Bugfix: encryption lockout 2021-12-07 15:15:16 +00:00
viveksantayana 57ee0bf971 Bugfix: encryption lockout 2021-12-07 15:15:16 +00:00
viveksantayana 866267dc5f Bugfix: encryption lockout 2021-12-07 15:15:16 +00:00
viveksantayana 735cdec139 Bugfix: encryption lockout 2021-12-07 15:15:16 +00:00
viveksantayana d76c8a5fed Bug fix: evaluating question 97 2021-12-07 15:03:21 +00:00
viveksantayana 8591184da6 Bug fix: evaluating question 97 2021-12-07 15:03:21 +00:00
viveksantayana b87f99e138 Bug fix: evaluating question 97 2021-12-07 15:03:21 +00:00
viveksantayana 38d3420e4d Bug fix: evaluating question 97 2021-12-07 15:03:21 +00:00
viveksantayana ecf18a70a8 Merge 2021-12-07 13:37:12 +00:00
viveksantayana 7b5861ade6 Merge 2021-12-07 13:37:12 +00:00
viveksantayana ce5be3a53e Merge 2021-12-07 13:37:12 +00:00
viveksantayana f0437dceaa Merge 2021-12-07 13:37:12 +00:00
viveksantayana 7206ca6203 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-07 13:34:02 +00:00
viveksantayana fa4640840b Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-07 13:34:02 +00:00
viveksantayana 84cb483ee0 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-07 13:34:02 +00:00
viveksantayana ca30b002ed Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-07 13:34:02 +00:00
viveksantayana b612b39e73 Typo 2021-12-07 13:33:31 +00:00
viveksantayana 05a564f41d Typo 2021-12-07 13:33:31 +00:00
viveksantayana 0b12b43621 Typo 2021-12-07 13:33:31 +00:00
viveksantayana 7b2f155b14 Typo 2021-12-07 13:33:31 +00:00
viveksantayana 9462d7aa7f Some more minor fixes to containerising and ignore 2021-12-07 13:29:25 +00:00
viveksantayana f9628df8c7 Some more minor fixes to containerising and ignore 2021-12-07 13:29:25 +00:00
viveksantayana 0cbdfba45f Some more minor fixes to containerising and ignore 2021-12-07 13:29:25 +00:00
viveksantayana a10bb0384f Some more minor fixes to containerising and ignore 2021-12-07 13:29:25 +00:00
viveksantayana 1d91e6d6ee Re-wrote compose and conf removing personal info 2021-12-07 13:26:24 +00:00
viveksantayana b5443c1331 Re-wrote compose and conf removing personal info 2021-12-07 13:26:24 +00:00
viveksantayana 96c42f1ee1 Re-wrote compose and conf removing personal info 2021-12-07 13:26:24 +00:00
viveksantayana fe83a47dae Re-wrote compose and conf removing personal info 2021-12-07 13:26:24 +00:00
viveksantayana ec52ebffa5 Finesse log in form css 2021-12-07 12:39:29 +00:00
viveksantayana 3d7e144d12 Finesse log in form css 2021-12-07 12:39:29 +00:00
viveksantayana 9da654f235 Finesse log in form css 2021-12-07 12:39:29 +00:00
viveksantayana 3c9fcae9f8 Finesse log in form css 2021-12-07 12:39:29 +00:00
viveksantayana d9f967811f Finesse log in form css 2021-12-07 12:39:29 +00:00
viveksantayana d093c4e636 Finesse log in form css 2021-12-07 12:39:29 +00:00
viveksantayana 355c049937 Finesse log in form css 2021-12-07 12:39:29 +00:00
viveksantayana 1d5dfaa5ee Finesse log in form css 2021-12-07 12:39:29 +00:00
viveksantayana 7d287874cd Finessed remaining block question counter 2021-12-07 12:27:06 +00:00
viveksantayana 57f233f20f Finessed remaining block question counter 2021-12-07 12:27:06 +00:00
viveksantayana 55a11d80d2 Finessed remaining block question counter 2021-12-07 12:27:06 +00:00
viveksantayana a35d0ef7f1 Finessed remaining block question counter 2021-12-07 12:27:06 +00:00
viveksantayana 6fb8b62e5b Finessed remaining block question counter 2021-12-07 12:27:06 +00:00
viveksantayana 4a5bc48889 Finessed remaining block question counter 2021-12-07 12:27:06 +00:00
viveksantayana 0453de9f62 Finessed remaining block question counter 2021-12-07 12:27:06 +00:00
viveksantayana 0bdd50f432 Finessed remaining block question counter 2021-12-07 12:27:06 +00:00
viveksantayana 78e4afdc71 Correcting an error 2021-12-07 07:24:39 +00:00
viveksantayana f2fb52aeca Correcting an error 2021-12-07 07:24:39 +00:00
viveksantayana eaf8197ae6 Correcting an error 2021-12-07 07:24:39 +00:00
viveksantayana 52afd249b7 Correcting an error 2021-12-07 07:24:39 +00:00
viveksantayana 79073f3d92 Correcting an error 2021-12-07 07:24:39 +00:00
viveksantayana 4a8080f0c8 Correcting an error 2021-12-07 07:24:39 +00:00
viveksantayana 99a4539559 Correcting an error 2021-12-07 07:24:39 +00:00
viveksantayana 443568f8ff Correcting an error 2021-12-07 07:24:39 +00:00
viveksantayana 11e740ea44 Finesse cookie dismiss button 2021-12-07 07:22:11 +00:00
viveksantayana 5ab2e7e608 Finesse cookie dismiss button 2021-12-07 07:22:11 +00:00
viveksantayana cbf66d8768 Finesse cookie dismiss button 2021-12-07 07:22:11 +00:00
viveksantayana 7b1ae3b354 Finesse cookie dismiss button 2021-12-07 07:22:11 +00:00
viveksantayana b9a83a436e Finesse cookie dismiss button 2021-12-07 07:22:11 +00:00
viveksantayana bae8d1e6f8 Finesse cookie dismiss button 2021-12-07 07:22:11 +00:00
viveksantayana 0e28e3dc48 Finesse cookie dismiss button 2021-12-07 07:22:11 +00:00
viveksantayana 36ed23564d Finesse cookie dismiss button 2021-12-07 07:22:11 +00:00
viveksantayana cc995925bb Finesse cookie consent display 2021-12-07 07:09:28 +00:00
viveksantayana 4585b93136 Finesse cookie consent display 2021-12-07 07:09:28 +00:00
viveksantayana f15b7da648 Finesse cookie consent display 2021-12-07 07:09:28 +00:00
viveksantayana 14272ba0b8 Finesse cookie consent display 2021-12-07 07:09:28 +00:00
viveksantayana 706ba8409e Finesse cookie consent display 2021-12-07 07:09:28 +00:00
viveksantayana 0130f7412d Finesse cookie consent display 2021-12-07 07:09:28 +00:00
viveksantayana a5f1177d8f Finesse cookie consent display 2021-12-07 07:09:28 +00:00
viveksantayana 8b4ca65122 Finesse cookie consent display 2021-12-07 07:09:28 +00:00
viveksantayana 757425494f removed fake link 2021-12-07 07:04:58 +00:00
viveksantayana f3f8ac955c removed fake link 2021-12-07 07:04:58 +00:00
viveksantayana b2e708cde0 removed fake link 2021-12-07 07:04:58 +00:00
viveksantayana 8bfc8e119c removed fake link 2021-12-07 07:04:58 +00:00
viveksantayana 103093c886 removed fake link 2021-12-07 07:04:58 +00:00
viveksantayana 0ccb62ce3c removed fake link 2021-12-07 07:04:58 +00:00
viveksantayana 440a5aad09 removed fake link 2021-12-07 07:04:58 +00:00
viveksantayana 2507a1d00b removed fake link 2021-12-07 07:04:58 +00:00
viveksantayana 6d887f1bfd Fixed subject-verb disagreement 2021-12-07 07:00:44 +00:00
viveksantayana fed4b6739f Fixed subject-verb disagreement 2021-12-07 07:00:44 +00:00
viveksantayana 3fc9d4c3c1 Fixed subject-verb disagreement 2021-12-07 07:00:44 +00:00
viveksantayana dd22b51fe1 Fixed subject-verb disagreement 2021-12-07 07:00:44 +00:00
viveksantayana 89e8267a29 Fixed subject-verb disagreement 2021-12-07 07:00:44 +00:00
viveksantayana f2b261f0b0 Fixed subject-verb disagreement 2021-12-07 07:00:44 +00:00
viveksantayana 9c8c0d85a5 Fixed subject-verb disagreement 2021-12-07 07:00:44 +00:00
viveksantayana 526d940c54 Fixed subject-verb disagreement 2021-12-07 07:00:44 +00:00
viveksantayana 485e51f239 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-07 06:55:50 +00:00
viveksantayana f74df18af1 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-07 06:55:50 +00:00
viveksantayana 9f4e9637c9 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-07 06:55:50 +00:00
viveksantayana c85fa69713 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-07 06:55:50 +00:00
viveksantayana 1adb4867d5 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-07 06:55:50 +00:00
viveksantayana ff6468cce3 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-07 06:55:50 +00:00
viveksantayana 55aa5496db Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-07 06:55:50 +00:00
viveksantayana b7ef513870 Technical issues help email 2021-12-07 06:46:34 +00:00
viveksantayana af07a152ad Technical issues help email 2021-12-07 06:46:34 +00:00
viveksantayana 331e49a6bc Technical issues help email 2021-12-07 06:46:34 +00:00
viveksantayana 9bdff0d729 Technical issues help email 2021-12-07 06:46:34 +00:00
viveksantayana 2027e525e2 Technical issues help email 2021-12-07 06:46:34 +00:00
viveksantayana 7fdc4bc26c Technical issues help email 2021-12-07 06:46:34 +00:00
viveksantayana 59fc703bcb Technical issues help email 2021-12-07 06:46:34 +00:00
viveksantayana c466f06384 Remove personal data from document 2021-12-07 06:42:46 +00:00
viveksantayana ab3312d45c Remove personal data from document 2021-12-07 06:42:46 +00:00
viveksantayana 8d80666ed8 Remove personal data from document 2021-12-07 06:42:46 +00:00
viveksantayana f867022207 Remove personal data from document 2021-12-07 06:42:46 +00:00
viveksantayana 3d9a3ecdff Remove personal data from document 2021-12-07 06:42:46 +00:00
viveksantayana e311c66abc Remove personal data from document 2021-12-07 06:42:46 +00:00
viveksantayana a8e938e802 Remove personal data from document 2021-12-07 06:42:46 +00:00
viveksantayana 4c4927df31 Removed email address 2021-12-07 06:40:57 +00:00
viveksantayana 0d8450f667 Removed email address 2021-12-07 06:40:57 +00:00
viveksantayana f8126b42fe Removed email address 2021-12-07 06:40:57 +00:00
viveksantayana b151de39df Removed email address 2021-12-07 06:40:57 +00:00
viveksantayana 407ee49bff Removed email address 2021-12-07 06:40:57 +00:00
viveksantayana e7a82d2437 Removed email address 2021-12-07 06:40:57 +00:00
viveksantayana b0bb600e12 Removed email address 2021-12-07 06:40:57 +00:00
viveksantayana 0e8fbf148a Merge branch 'master' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test
Sync
2021-12-07 06:38:54 +00:00
viveksantayana 46aeee416a Merge branch 'master' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test
Sync
2021-12-07 06:38:54 +00:00
viveksantayana 0ef72ec338 Merge branch 'master' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test
Sync
2021-12-07 06:38:54 +00:00
viveksantayana 80391229a3 Merge branch 'master' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test
Sync
2021-12-07 06:38:54 +00:00
viveksantayana 721af501d1 Merge branch 'master' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test
Sync
2021-12-07 06:38:54 +00:00
viveksantayana 07395833e8 Merge branch 'master' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test
Sync
2021-12-07 06:38:54 +00:00
viveksantayana e6f1338ee4 Merge branch 'master' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test
Sync
2021-12-07 06:38:54 +00:00
viveksantayana 0e50e2c1b9 Started drafting documentation 2021-12-07 06:38:43 +00:00
viveksantayana a33ed7a94d Started drafting documentation 2021-12-07 06:38:43 +00:00
viveksantayana b0980b1871 Started drafting documentation 2021-12-07 06:38:43 +00:00
viveksantayana 23ddf6601b Started drafting documentation 2021-12-07 06:38:43 +00:00
viveksantayana ea9132542f Started drafting documentation 2021-12-07 06:38:43 +00:00
viveksantayana 19d12226f5 Started drafting documentation 2021-12-07 06:38:43 +00:00
viveksantayana b7fb30ce36 Started drafting documentation 2021-12-07 06:38:43 +00:00
viveksantayana fe75fa1a49 Better precision in expiring quizzes 2021-12-07 00:08:01 +00:00
viveksantayana 8726d4335c Better precision in expiring quizzes 2021-12-07 00:08:01 +00:00
viveksantayana f86fa6f4b5 Better precision in expiring quizzes 2021-12-07 00:08:01 +00:00
viveksantayana a5f8cba71b Better precision in expiring quizzes 2021-12-07 00:08:01 +00:00
viveksantayana 6c293c2ce6 Better precision in expiring quizzes 2021-12-07 00:08:01 +00:00
viveksantayana e1b2bd20f7 Better precision in expiring quizzes 2021-12-07 00:08:01 +00:00
viveksantayana d3ed32183c Better precision in expiring quizzes 2021-12-07 00:08:01 +00:00
viveksantayana e8090f30d7 Better precision in expiring quizzes 2021-12-07 00:08:01 +00:00
viveksantayana 3b3656209a Better precision in expiring quizzes 2021-12-07 00:08:01 +00:00
viveksantayana 176a0f069f Better precision in expiring quizzes 2021-12-07 00:08:01 +00:00
viveksantayana 58f40e221f Better precision in expiring quizzes 2021-12-07 00:08:01 +00:00
viveksantayana 302d8a933a Better precision in expiring quizzes 2021-12-07 00:08:01 +00:00
viveksantayana 7e9b5eada0 Better precision in expiring quizzes 2021-12-07 00:08:01 +00:00
viveksantayana c5587fcb73 Better precision in expiring quizzes 2021-12-07 00:08:01 +00:00
viveksantayana a4b4bfe0ee Updated test expiry 2021-12-06 23:37:16 +00:00
viveksantayana c915ae1182 Updated test expiry 2021-12-06 23:37:16 +00:00
viveksantayana 0faef8651a Updated test expiry 2021-12-06 23:37:16 +00:00
viveksantayana 973bafcdb2 Updated test expiry 2021-12-06 23:37:16 +00:00
viveksantayana 4f925eae2f Updated test expiry 2021-12-06 23:37:16 +00:00
viveksantayana b821d40e85 Updated test expiry 2021-12-06 23:37:16 +00:00
viveksantayana a9f5ba51c4 Updated test expiry 2021-12-06 23:37:16 +00:00
viveksantayana 5b0fd0ced3 Updated test expiry 2021-12-06 23:37:16 +00:00
viveksantayana fba539f933 Updated test expiry 2021-12-06 23:37:16 +00:00
viveksantayana eca786d444 Updated test expiry 2021-12-06 23:37:16 +00:00
viveksantayana 220a378c63 Updated test expiry 2021-12-06 23:37:16 +00:00
viveksantayana affb309ffc Updated test expiry 2021-12-06 23:37:16 +00:00
viveksantayana b013ffec47 Updated test expiry 2021-12-06 23:37:16 +00:00
viveksantayana 0e1db9d21d Updated test expiry 2021-12-06 23:37:16 +00:00
viveksantayana 003d998b72 Corrected bug in exam display 2021-12-06 23:24:57 +00:00
viveksantayana b84654d931 Corrected bug in exam display 2021-12-06 23:24:57 +00:00
viveksantayana dccc85370e Corrected bug in exam display 2021-12-06 23:24:57 +00:00
viveksantayana d3f116e1ca Corrected bug in exam display 2021-12-06 23:24:57 +00:00
viveksantayana 355a6bff5e Corrected bug in exam display 2021-12-06 23:24:57 +00:00
viveksantayana 9bd1f29aad Corrected bug in exam display 2021-12-06 23:24:57 +00:00
viveksantayana 98638e803a Corrected bug in exam display 2021-12-06 23:24:57 +00:00
viveksantayana 6c4ab2e1e3 Corrected bug in exam display 2021-12-06 23:24:57 +00:00
viveksantayana 991313e8b2 Corrected bug in exam display 2021-12-06 23:24:57 +00:00
viveksantayana e13069bed6 Corrected bug in exam display 2021-12-06 23:24:57 +00:00
viveksantayana e2a0bc7b4e Corrected bug in exam display 2021-12-06 23:24:57 +00:00
viveksantayana 5b6f83c294 Corrected bug in exam display 2021-12-06 23:24:57 +00:00
viveksantayana 9a4c2962a5 Corrected bug in exam display 2021-12-06 23:24:57 +00:00
viveksantayana 7295a2751c Corrected bug in exam display 2021-12-06 23:24:57 +00:00
viveksantayana dd72da6ae6 I am bad at debugging. 2021-12-06 23:19:13 +00:00
viveksantayana 54121a4fda I am bad at debugging. 2021-12-06 23:19:13 +00:00
viveksantayana 36cdeb15ad I am bad at debugging. 2021-12-06 23:19:13 +00:00
viveksantayana 0f1f79e237 I am bad at debugging. 2021-12-06 23:19:13 +00:00
viveksantayana eb6f5b876c I am bad at debugging. 2021-12-06 23:19:13 +00:00
viveksantayana d5f7ab0488 I am bad at debugging. 2021-12-06 23:19:13 +00:00
viveksantayana 14500434d7 I am bad at debugging. 2021-12-06 23:19:13 +00:00
viveksantayana 35dffd358b I am bad at debugging. 2021-12-06 23:19:13 +00:00
viveksantayana cc7712ccec I am bad at debugging. 2021-12-06 23:19:13 +00:00
viveksantayana fafb3fcc2e I am bad at debugging. 2021-12-06 23:19:13 +00:00
viveksantayana ed6360e7a3 I am bad at debugging. 2021-12-06 23:19:13 +00:00
viveksantayana 4131dd054a I am bad at debugging. 2021-12-06 23:19:13 +00:00
viveksantayana 3b92dc3005 I am bad at debugging. 2021-12-06 23:19:13 +00:00
viveksantayana f370496780 I am bad at debugging. 2021-12-06 23:19:13 +00:00
viveksantayana 667ad4ebc2 Close Quiz function 2021-12-06 23:16:33 +00:00
viveksantayana 4c3805cfe7 Close Quiz function 2021-12-06 23:16:33 +00:00
viveksantayana 52e3ce4c93 Close Quiz function 2021-12-06 23:16:33 +00:00
viveksantayana 045f3aec0a Close Quiz function 2021-12-06 23:16:33 +00:00
viveksantayana ca0e6c82cb Close Quiz function 2021-12-06 23:16:33 +00:00
viveksantayana 1f767da365 Close Quiz function 2021-12-06 23:16:33 +00:00
viveksantayana 860c18c5fd Close Quiz function 2021-12-06 23:16:33 +00:00
viveksantayana 46cef8cd1e Close Quiz function 2021-12-06 23:16:33 +00:00
viveksantayana f069556afd Close Quiz function 2021-12-06 23:16:33 +00:00
viveksantayana 421445d8d5 Close Quiz function 2021-12-06 23:16:33 +00:00
viveksantayana 6d931bdf6c Close Quiz function 2021-12-06 23:16:33 +00:00
viveksantayana b0d3ff3fc1 Close Quiz function 2021-12-06 23:16:33 +00:00
viveksantayana 0cb88390b8 Close Quiz function 2021-12-06 23:16:33 +00:00
viveksantayana 68aef968e2 Close Quiz function 2021-12-06 23:16:33 +00:00
viveksantayana 8d29944d5d Remove redundant file 2021-12-06 22:54:40 +00:00
viveksantayana 33314e2bc4 Remove redundant file 2021-12-06 22:54:40 +00:00
viveksantayana 8fbb52d366 Remove redundant file 2021-12-06 22:54:40 +00:00
viveksantayana 8749c6e590 Remove redundant file 2021-12-06 22:54:40 +00:00
viveksantayana 1dbe4215ec Remove redundant file 2021-12-06 22:54:40 +00:00
viveksantayana c10c31c4b6 Remove redundant file 2021-12-06 22:54:40 +00:00
viveksantayana 101f6786f5 Remove redundant file 2021-12-06 22:54:40 +00:00
viveksantayana fe5cf189cc Remove redundant file 2021-12-06 22:54:40 +00:00
viveksantayana 167bee38d6 Remove redundant file 2021-12-06 22:54:40 +00:00
viveksantayana cefb5fe849 Remove redundant file 2021-12-06 22:54:40 +00:00
viveksantayana bac083411c Remove redundant file 2021-12-06 22:54:40 +00:00
viveksantayana f0c7873257 Remove redundant file 2021-12-06 22:54:40 +00:00
viveksantayana 6dcef4885a Remove redundant file 2021-12-06 22:54:40 +00:00
viveksantayana 0cb8ff9991 Remove redundant file 2021-12-06 22:54:40 +00:00
viveksantayana 4d77021d58 Merge branch 'master' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test 2021-12-06 22:54:02 +00:00
viveksantayana 5ab8be93ec Merge branch 'master' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test 2021-12-06 22:54:02 +00:00
viveksantayana fa05a17508 Merge branch 'master' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test 2021-12-06 22:54:02 +00:00
viveksantayana 920287b7ae Merge branch 'master' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test 2021-12-06 22:54:02 +00:00
viveksantayana 5960d0103d Merge branch 'master' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test 2021-12-06 22:54:02 +00:00
viveksantayana f44fbb24da Merge branch 'master' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test 2021-12-06 22:54:02 +00:00
viveksantayana 3535622380 Merge branch 'master' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test 2021-12-06 22:54:02 +00:00
viveksantayana 86abae01c0 Merge branch 'master' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test 2021-12-06 22:54:02 +00:00
viveksantayana 8328f6fb1a Merge branch 'master' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test 2021-12-06 22:54:02 +00:00
viveksantayana 7c2adc9cac Merge branch 'master' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test 2021-12-06 22:54:02 +00:00
viveksantayana e9a5e72959 Merge branch 'master' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test 2021-12-06 22:54:02 +00:00
viveksantayana e119c344dd Merge branch 'master' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test 2021-12-06 22:54:02 +00:00
viveksantayana 12480b32a5 Merge branch 'master' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test 2021-12-06 22:54:02 +00:00
viveksantayana c7b54d2119 Merge branch 'master' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test 2021-12-06 22:54:02 +00:00
viveksantayana e6841b7744 This fixes it, hopefully 2021-12-06 22:47:54 +00:00
viveksantayana 28f944a6cf This fixes it, hopefully 2021-12-06 22:47:54 +00:00
viveksantayana 6835232698 This fixes it, hopefully 2021-12-06 22:47:54 +00:00
viveksantayana 00fb8e13fe This fixes it, hopefully 2021-12-06 22:47:54 +00:00
viveksantayana 5392ff86ed This fixes it, hopefully 2021-12-06 22:47:54 +00:00
viveksantayana 1b2b4f8dc6 This fixes it, hopefully 2021-12-06 22:47:54 +00:00
viveksantayana 328a78a923 This fixes it, hopefully 2021-12-06 22:47:54 +00:00
viveksantayana 9810577c5d This fixes it, hopefully 2021-12-06 22:47:54 +00:00
viveksantayana 2d29e6ac97 This fixes it, hopefully 2021-12-06 22:47:54 +00:00
viveksantayana 2c93b0d3a7 This fixes it, hopefully 2021-12-06 22:47:54 +00:00
viveksantayana c462b76cb7 This fixes it, hopefully 2021-12-06 22:47:54 +00:00
viveksantayana 343cb3f8b1 This fixes it, hopefully 2021-12-06 22:47:54 +00:00
viveksantayana c2d95c1d52 This fixes it, hopefully 2021-12-06 22:47:54 +00:00
viveksantayana 961e8629cb This fixes it, hopefully 2021-12-06 22:47:54 +00:00
viveksantayana 378e8eeae3 And again 2021-12-06 22:26:48 +00:00
viveksantayana 28789c72f1 And again 2021-12-06 22:26:48 +00:00
viveksantayana fe898aaf7d And again 2021-12-06 22:26:48 +00:00
viveksantayana 57e5a21ffa And again 2021-12-06 22:26:48 +00:00
viveksantayana a010d7d290 And again 2021-12-06 22:26:48 +00:00
viveksantayana 13121b3037 And again 2021-12-06 22:26:48 +00:00
viveksantayana 8b962c53a9 And again 2021-12-06 22:26:48 +00:00
viveksantayana bceb91b406 And again 2021-12-06 22:26:48 +00:00
viveksantayana b299f4ae55 And again 2021-12-06 22:26:48 +00:00
viveksantayana a14b7bf305 And again 2021-12-06 22:26:48 +00:00
viveksantayana e935524552 And again 2021-12-06 22:26:48 +00:00
viveksantayana 3622baf988 And again 2021-12-06 22:26:48 +00:00
viveksantayana b8a7182f98 And again 2021-12-06 22:26:48 +00:00
viveksantayana 24545feea0 And again 2021-12-06 22:26:48 +00:00
viveksantayana bb9233eeae Trying to fix it again 2021-12-06 22:24:34 +00:00
viveksantayana 3035ac6687 Trying to fix it again 2021-12-06 22:24:34 +00:00
viveksantayana 60b8aad419 Trying to fix it again 2021-12-06 22:24:34 +00:00
viveksantayana 8d2a84a071 Trying to fix it again 2021-12-06 22:24:34 +00:00
viveksantayana 6e541c6a7b Trying to fix it again 2021-12-06 22:24:34 +00:00
viveksantayana a77faa7eed Trying to fix it again 2021-12-06 22:24:34 +00:00
viveksantayana 685b1b928d Trying to fix it again 2021-12-06 22:24:34 +00:00
viveksantayana e0c2570515 Trying to fix it again 2021-12-06 22:24:34 +00:00
viveksantayana b4e7efdbe5 Trying to fix it again 2021-12-06 22:24:34 +00:00
viveksantayana 5163914875 Trying to fix it again 2021-12-06 22:24:34 +00:00
viveksantayana f61c12afd1 Trying to fix it again 2021-12-06 22:24:34 +00:00
viveksantayana 467b6d9ce7 Trying to fix it again 2021-12-06 22:24:34 +00:00
viveksantayana 8a0e93e3e5 Trying to fix it again 2021-12-06 22:24:34 +00:00
viveksantayana e5aab6268d Trying to fix it again 2021-12-06 22:24:34 +00:00
viveksantayana 383ae11cd3 I am really bad at fixing bugs 2021-12-06 22:20:27 +00:00
viveksantayana 37d6d3a003 I am really bad at fixing bugs 2021-12-06 22:20:27 +00:00
viveksantayana 348ee95d1c I am really bad at fixing bugs 2021-12-06 22:20:27 +00:00
viveksantayana 1b2209d97c I am really bad at fixing bugs 2021-12-06 22:20:27 +00:00
viveksantayana 9db80c9148 I am really bad at fixing bugs 2021-12-06 22:20:27 +00:00
viveksantayana 2e340cce00 I am really bad at fixing bugs 2021-12-06 22:20:27 +00:00
viveksantayana 20b447adbb I am really bad at fixing bugs 2021-12-06 22:20:27 +00:00
viveksantayana 669bbd2f7b I am really bad at fixing bugs 2021-12-06 22:20:27 +00:00
viveksantayana 9cef9819fe I am really bad at fixing bugs 2021-12-06 22:20:27 +00:00
viveksantayana 22b483b021 I am really bad at fixing bugs 2021-12-06 22:20:27 +00:00
viveksantayana 9e4e874401 I am really bad at fixing bugs 2021-12-06 22:20:27 +00:00
viveksantayana 21ad8b2f94 I am really bad at fixing bugs 2021-12-06 22:20:27 +00:00
viveksantayana 47f996b19c I am really bad at fixing bugs 2021-12-06 22:20:27 +00:00
viveksantayana a3a13d4eb6 I am really bad at fixing bugs 2021-12-06 22:20:27 +00:00
viveksantayana a357ffe28d More Bug Fixes 2021-12-06 22:17:52 +00:00
viveksantayana cdc19e69b8 More Bug Fixes 2021-12-06 22:17:52 +00:00
viveksantayana e00e2b17b0 More Bug Fixes 2021-12-06 22:17:52 +00:00
viveksantayana a2e05d39e6 More Bug Fixes 2021-12-06 22:17:52 +00:00
viveksantayana 65d679afbb More Bug Fixes 2021-12-06 22:17:52 +00:00
viveksantayana 72a068c975 More Bug Fixes 2021-12-06 22:17:52 +00:00
viveksantayana 891ec2fd38 More Bug Fixes 2021-12-06 22:17:52 +00:00
viveksantayana 4be21a2ca2 More Bug Fixes 2021-12-06 22:17:52 +00:00
viveksantayana cfc62ee21f More Bug Fixes 2021-12-06 22:17:52 +00:00
viveksantayana efd4dc440d More Bug Fixes 2021-12-06 22:17:52 +00:00
viveksantayana 7150e679c8 More Bug Fixes 2021-12-06 22:17:52 +00:00
viveksantayana 935b465a19 More Bug Fixes 2021-12-06 22:17:52 +00:00
viveksantayana bce76d808d More Bug Fixes 2021-12-06 22:17:52 +00:00
viveksantayana 05fa5bf274 More Bug Fixes 2021-12-06 22:17:52 +00:00
viveksantayana 1d1e2acf62 Bugfix: lack of submission time in detailed result 2021-12-06 22:11:16 +00:00
viveksantayana 700daa51ef Bugfix: lack of submission time in detailed result 2021-12-06 22:11:16 +00:00
viveksantayana c742edb57c Bugfix: lack of submission time in detailed result 2021-12-06 22:11:16 +00:00
viveksantayana cdd35117bf Bugfix: lack of submission time in detailed result 2021-12-06 22:11:16 +00:00
viveksantayana 529504509e Bugfix: lack of submission time in detailed result 2021-12-06 22:11:16 +00:00
viveksantayana abcd4cbec5 Bugfix: lack of submission time in detailed result 2021-12-06 22:11:16 +00:00
viveksantayana 852b2664ce Bugfix: lack of submission time in detailed result 2021-12-06 22:11:16 +00:00
viveksantayana 8b1b0162cc Bugfix: lack of submission time in detailed result 2021-12-06 22:11:16 +00:00
viveksantayana 715c7856fa Bugfix: lack of submission time in detailed result 2021-12-06 22:11:16 +00:00
viveksantayana 56e5d29416 Bugfix: lack of submission time in detailed result 2021-12-06 22:11:16 +00:00
viveksantayana 19521c3f23 Bugfix: lack of submission time in detailed result 2021-12-06 22:11:16 +00:00
viveksantayana ee50306370 Bugfix: lack of submission time in detailed result 2021-12-06 22:11:16 +00:00
viveksantayana d7ef628640 Bugfix: lack of submission time in detailed result 2021-12-06 22:11:16 +00:00
viveksantayana 559e5b96c4 Bugfix: lack of submission time in detailed result 2021-12-06 22:11:16 +00:00
viveksantayana 4c2a6e7f74 OG Meta and navbar bug fix 2021-12-06 21:58:51 +00:00
viveksantayana 4ae587da12 OG Meta and navbar bug fix 2021-12-06 21:58:51 +00:00
viveksantayana daaf173ff6 OG Meta and navbar bug fix 2021-12-06 21:58:51 +00:00
viveksantayana fa43ab1879 OG Meta and navbar bug fix 2021-12-06 21:58:51 +00:00
viveksantayana 05de6d716b OG Meta and navbar bug fix 2021-12-06 21:58:51 +00:00
viveksantayana a3101503d4 OG Meta and navbar bug fix 2021-12-06 21:58:51 +00:00
viveksantayana f740ee7f1b OG Meta and navbar bug fix 2021-12-06 21:58:51 +00:00
viveksantayana c56c0dc822 OG Meta and navbar bug fix 2021-12-06 21:58:51 +00:00
viveksantayana df94b9b486 OG Meta and navbar bug fix 2021-12-06 21:58:51 +00:00
viveksantayana 0c446b9ae7 OG Meta and navbar bug fix 2021-12-06 21:58:51 +00:00
viveksantayana 9a5f073170 OG Meta and navbar bug fix 2021-12-06 21:58:51 +00:00
viveksantayana 9ebec5000c OG Meta and navbar bug fix 2021-12-06 21:58:51 +00:00
viveksantayana 700c5ff39d OG Meta and navbar bug fix 2021-12-06 21:58:51 +00:00
viveksantayana ce32b33eaa OG Meta and navbar bug fix 2021-12-06 21:58:51 +00:00
viveksantayana 45e0d37f81 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 21:53:00 +00:00
viveksantayana 21565592cc Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 21:53:00 +00:00
viveksantayana d353a80269 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 21:53:00 +00:00
viveksantayana d2cd8316da Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 21:53:00 +00:00
viveksantayana 8e7a09edca Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 21:53:00 +00:00
viveksantayana c3fb5a9b0c Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 21:53:00 +00:00
viveksantayana 616bd3f578 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 21:53:00 +00:00
viveksantayana 108297cbfd Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 21:53:00 +00:00
viveksantayana 5954c1a68e Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 21:53:00 +00:00
viveksantayana 9e03db595b Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 21:53:00 +00:00
viveksantayana de969e0028 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 21:53:00 +00:00
viveksantayana 3bfd08411b Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 21:53:00 +00:00
viveksantayana 19d16ab7d9 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 21:53:00 +00:00
viveksantayana a4affa72a9 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 21:53:00 +00:00
viveksantayana 12c424be08 OG and Cookie settings 2021-12-06 21:51:29 +00:00
viveksantayana 06aee6fa6d OG and Cookie settings 2021-12-06 21:51:29 +00:00
viveksantayana e00b4a9045 OG and Cookie settings 2021-12-06 21:51:29 +00:00
viveksantayana 2472323103 OG and Cookie settings 2021-12-06 21:51:29 +00:00
viveksantayana 0ad7089722 OG and Cookie settings 2021-12-06 21:51:29 +00:00
viveksantayana 5d334e4da5 OG and Cookie settings 2021-12-06 21:51:29 +00:00
viveksantayana 707890ce3a OG and Cookie settings 2021-12-06 21:51:29 +00:00
viveksantayana 7bdca9b895 OG and Cookie settings 2021-12-06 21:51:29 +00:00
viveksantayana 848f39aa66 OG and Cookie settings 2021-12-06 21:51:29 +00:00
viveksantayana bd1ac46942 OG and Cookie settings 2021-12-06 21:51:29 +00:00
viveksantayana 5fad0cda1e OG and Cookie settings 2021-12-06 21:51:29 +00:00
viveksantayana 11f965e20f OG and Cookie settings 2021-12-06 21:51:29 +00:00
viveksantayana bfd8c8fa1e OG and Cookie settings 2021-12-06 21:51:29 +00:00
viveksantayana ee99dd9038 OG and Cookie settings 2021-12-06 21:51:29 +00:00
viveksantayana 65ec27b35b Bugfixes: case sensitivity, percent sign rendering
Temporary work-around: disabled client navbar home link to prevent exit
2021-12-06 21:47:02 +00:00
viveksantayana 5a3aca732b Bugfixes: case sensitivity, percent sign rendering
Temporary work-around: disabled client navbar home link to prevent exit
2021-12-06 21:47:02 +00:00
viveksantayana 63ca5e33de Bugfixes: case sensitivity, percent sign rendering
Temporary work-around: disabled client navbar home link to prevent exit
2021-12-06 21:47:02 +00:00
viveksantayana 54653e82f2 Bugfixes: case sensitivity, percent sign rendering
Temporary work-around: disabled client navbar home link to prevent exit
2021-12-06 21:47:02 +00:00
viveksantayana 1f228c7f1c Bugfixes: case sensitivity, percent sign rendering
Temporary work-around: disabled client navbar home link to prevent exit
2021-12-06 21:47:02 +00:00
viveksantayana fa290a2713 Bugfixes: case sensitivity, percent sign rendering
Temporary work-around: disabled client navbar home link to prevent exit
2021-12-06 21:47:02 +00:00
viveksantayana 56191f5e7a Bugfixes: case sensitivity, percent sign rendering
Temporary work-around: disabled client navbar home link to prevent exit
2021-12-06 21:47:02 +00:00
viveksantayana cbc8d276eb Bugfixes: case sensitivity, percent sign rendering
Temporary work-around: disabled client navbar home link to prevent exit
2021-12-06 21:47:02 +00:00
viveksantayana 396400e7c0 Bugfixes: case sensitivity, percent sign rendering
Temporary work-around: disabled client navbar home link to prevent exit
2021-12-06 21:47:02 +00:00
viveksantayana cd68a60001 Bugfixes: case sensitivity, percent sign rendering
Temporary work-around: disabled client navbar home link to prevent exit
2021-12-06 21:47:02 +00:00
viveksantayana cb39592bd3 Bugfixes: case sensitivity, percent sign rendering
Temporary work-around: disabled client navbar home link to prevent exit
2021-12-06 21:47:02 +00:00
viveksantayana dd7e3cad7a Bugfixes: case sensitivity, percent sign rendering
Temporary work-around: disabled client navbar home link to prevent exit
2021-12-06 21:47:02 +00:00
viveksantayana 5dbf3be732 Bugfixes: case sensitivity, percent sign rendering
Temporary work-around: disabled client navbar home link to prevent exit
2021-12-06 21:47:02 +00:00
viveksantayana 32908bde7d Bugfixes: case sensitivity, percent sign rendering
Temporary work-around: disabled client navbar home link to prevent exit
2021-12-06 21:47:02 +00:00
viveksantayana 835c5e2aa6 Proxy Fix 2021-12-06 20:10:27 +00:00
viveksantayana 672888c5d9 Proxy Fix 2021-12-06 20:10:27 +00:00
viveksantayana 6823c12b2d Proxy Fix 2021-12-06 20:10:27 +00:00
viveksantayana cda7ac480f Proxy Fix 2021-12-06 20:10:27 +00:00
viveksantayana c7907dc24d Proxy Fix 2021-12-06 20:10:27 +00:00
viveksantayana 8926960a18 Proxy Fix 2021-12-06 20:10:27 +00:00
viveksantayana e4d97869da Proxy Fix 2021-12-06 20:10:27 +00:00
viveksantayana dfbf10e2dd Proxy Fix 2021-12-06 20:10:27 +00:00
viveksantayana e499d1d54a Proxy Fix 2021-12-06 20:10:27 +00:00
viveksantayana dbd25ddf38 Proxy Fix 2021-12-06 20:10:27 +00:00
viveksantayana 724ffbfdf4 Proxy Fix 2021-12-06 20:10:27 +00:00
viveksantayana 11d839aada Proxy Fix 2021-12-06 20:10:27 +00:00
viveksantayana 6d90ec4aa6 Proxy Fix 2021-12-06 20:10:27 +00:00
viveksantayana 3980be3701 Proxy Fix 2021-12-06 20:10:27 +00:00
Vivek Santayana 43cb31849a Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Fix config
2021-12-06 19:22:06 +00:00
Vivek Santayana 59e7d3d112 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Fix config
2021-12-06 19:22:06 +00:00
Vivek Santayana 39cdafc847 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Fix config
2021-12-06 19:22:06 +00:00
Vivek Santayana 727779f054 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Fix config
2021-12-06 19:22:06 +00:00
Vivek Santayana bdeb026a7c Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Fix config
2021-12-06 19:22:06 +00:00
Vivek Santayana b244fb34f7 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Fix config
2021-12-06 19:22:06 +00:00
Vivek Santayana 73f4825bbe Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Fix config
2021-12-06 19:22:06 +00:00
Vivek Santayana e1ecb5bcb6 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Fix config
2021-12-06 19:22:06 +00:00
Vivek Santayana da5e115bbc Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Fix config
2021-12-06 19:22:06 +00:00
Vivek Santayana 1651f63577 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Fix config
2021-12-06 19:22:06 +00:00
Vivek Santayana 1edd25d3ea Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Fix config
2021-12-06 19:22:06 +00:00
Vivek Santayana a01d486d99 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Fix config
2021-12-06 19:22:06 +00:00
Vivek Santayana 3c64240842 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Fix config
2021-12-06 19:22:06 +00:00
Vivek Santayana 2b71c77c6c Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Fix config
2021-12-06 19:22:06 +00:00
viveksantayana 112c097d69 Updated config 2021-12-06 19:21:45 +00:00
viveksantayana 79e015191a Updated config 2021-12-06 19:21:45 +00:00
viveksantayana b6af6d5c15 Updated config 2021-12-06 19:21:45 +00:00
viveksantayana 529ac35bdb Updated config 2021-12-06 19:21:45 +00:00
viveksantayana 6c4ca715f6 Updated config 2021-12-06 19:21:45 +00:00
viveksantayana 8d6ca515dc Updated config 2021-12-06 19:21:45 +00:00
viveksantayana 972673f5d1 Updated config 2021-12-06 19:21:45 +00:00
viveksantayana cb1bc69f47 Updated config 2021-12-06 19:21:45 +00:00
viveksantayana 7550deab89 Updated config 2021-12-06 19:21:45 +00:00
viveksantayana a4058c475b Updated config 2021-12-06 19:21:45 +00:00
viveksantayana 81b3e3a142 Updated config 2021-12-06 19:21:45 +00:00
viveksantayana 0004d2714f Updated config 2021-12-06 19:21:45 +00:00
viveksantayana 8527976007 Updated config 2021-12-06 19:21:45 +00:00
viveksantayana 20efd4444c Updated config 2021-12-06 19:21:45 +00:00
Vivek Santayana 13465859ab Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Pull icons
2021-12-06 18:59:05 +00:00
Vivek Santayana 0f3a84b54d Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Pull icons
2021-12-06 18:59:05 +00:00
Vivek Santayana 53050f1358 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Pull icons
2021-12-06 18:59:05 +00:00
Vivek Santayana c8ccd002fa Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Pull icons
2021-12-06 18:59:05 +00:00
Vivek Santayana f025eee4a6 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Pull icons
2021-12-06 18:59:05 +00:00
Vivek Santayana 6acf3fa204 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Pull icons
2021-12-06 18:59:05 +00:00
Vivek Santayana 506a6cf6c2 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Pull icons
2021-12-06 18:59:05 +00:00
Vivek Santayana 97db70abff Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Pull icons
2021-12-06 18:59:05 +00:00
Vivek Santayana 4d642cc1e9 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Pull icons
2021-12-06 18:59:05 +00:00
Vivek Santayana 1a1d763d67 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Pull icons
2021-12-06 18:59:05 +00:00
Vivek Santayana fc765c0177 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Pull icons
2021-12-06 18:59:05 +00:00
Vivek Santayana 598dfa45e8 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Pull icons
2021-12-06 18:59:05 +00:00
Vivek Santayana 0d8200193d Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Pull icons
2021-12-06 18:59:05 +00:00
Vivek Santayana ca36772f29 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Pull icons
2021-12-06 18:59:05 +00:00
viveksantayana bd3205f06e Favicons and OG Meta 2021-12-06 18:58:42 +00:00
viveksantayana fa9d08b10e Favicons and OG Meta 2021-12-06 18:58:42 +00:00
viveksantayana ab7a25182f Favicons and OG Meta 2021-12-06 18:58:42 +00:00
viveksantayana 720de1f2de Favicons and OG Meta 2021-12-06 18:58:42 +00:00
viveksantayana e3bb2895ae Favicons and OG Meta 2021-12-06 18:58:42 +00:00
viveksantayana bf64b78acc Favicons and OG Meta 2021-12-06 18:58:42 +00:00
viveksantayana 3e1e57a067 Favicons and OG Meta 2021-12-06 18:58:42 +00:00
viveksantayana 42f90c667d Favicons and OG Meta 2021-12-06 18:58:42 +00:00
viveksantayana 5f06c9b624 Favicons and OG Meta 2021-12-06 18:58:42 +00:00
viveksantayana b02277f12f Favicons and OG Meta 2021-12-06 18:58:42 +00:00
viveksantayana 7d90b6c7a2 Favicons and OG Meta 2021-12-06 18:58:42 +00:00
viveksantayana a9ad171249 Favicons and OG Meta 2021-12-06 18:58:42 +00:00
viveksantayana 861b61c1ca Favicons and OG Meta 2021-12-06 18:58:42 +00:00
viveksantayana bc42ae86d1 Favicons and OG Meta 2021-12-06 18:58:42 +00:00
Vivek Santayana cc3410a1f6 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Download fonts and remove private files
2021-12-06 18:06:47 +00:00
Vivek Santayana 6c048a9c48 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Download fonts and remove private files
2021-12-06 18:06:47 +00:00
Vivek Santayana 953d3658a8 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Download fonts and remove private files
2021-12-06 18:06:47 +00:00
Vivek Santayana 14bc50165b Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Download fonts and remove private files
2021-12-06 18:06:47 +00:00
Vivek Santayana 70f6875ac1 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Download fonts and remove private files
2021-12-06 18:06:47 +00:00
Vivek Santayana 4c40240346 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Download fonts and remove private files
2021-12-06 18:06:47 +00:00
Vivek Santayana 5da08d5c37 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Download fonts and remove private files
2021-12-06 18:06:47 +00:00
Vivek Santayana 534247ece3 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Download fonts and remove private files
2021-12-06 18:06:47 +00:00
Vivek Santayana ba9ed0ca40 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Download fonts and remove private files
2021-12-06 18:06:47 +00:00
Vivek Santayana 9525694e39 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Download fonts and remove private files
2021-12-06 18:06:47 +00:00
Vivek Santayana bcc9e9c609 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Download fonts and remove private files
2021-12-06 18:06:47 +00:00
Vivek Santayana 31903626f0 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Download fonts and remove private files
2021-12-06 18:06:47 +00:00
Vivek Santayana c6adac8288 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Download fonts and remove private files
2021-12-06 18:06:47 +00:00
Vivek Santayana 0111547676 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test
Download fonts and remove private files
2021-12-06 18:06:47 +00:00
viveksantayana e70592b276 Uploading Fonts 2021-12-06 18:06:11 +00:00
viveksantayana b2a71bf51b Uploading Fonts 2021-12-06 18:06:11 +00:00
viveksantayana 22a0d58996 Uploading Fonts 2021-12-06 18:06:11 +00:00
viveksantayana 13b7249f2a Uploading Fonts 2021-12-06 18:06:11 +00:00
viveksantayana 3d6a1dc7ba Uploading Fonts 2021-12-06 18:06:11 +00:00
viveksantayana bdffce21b7 Uploading Fonts 2021-12-06 18:06:11 +00:00
viveksantayana 51d468fb44 Uploading Fonts 2021-12-06 18:06:11 +00:00
viveksantayana 164d43be8b Uploading Fonts 2021-12-06 18:06:11 +00:00
viveksantayana 7ae8cba851 Uploading Fonts 2021-12-06 18:06:11 +00:00
viveksantayana cdf47e0b88 Uploading Fonts 2021-12-06 18:06:11 +00:00
viveksantayana 73c00ac333 Uploading Fonts 2021-12-06 18:06:11 +00:00
viveksantayana 2427d55310 Uploading Fonts 2021-12-06 18:06:11 +00:00
viveksantayana b0f2d89956 Uploading Fonts 2021-12-06 18:06:11 +00:00
viveksantayana 757cc94f33 Uploading Fonts 2021-12-06 18:06:11 +00:00
Vivek Santayana 0cfac25ed3 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 18:02:59 +00:00
Vivek Santayana 33e8d8482f Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 18:02:59 +00:00
Vivek Santayana 0443e348ac Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 18:02:59 +00:00
Vivek Santayana a7e3a5fe47 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 18:02:59 +00:00
Vivek Santayana f2c0090aa3 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 18:02:59 +00:00
Vivek Santayana 4ff62d3b36 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 18:02:59 +00:00
Vivek Santayana ae75498edb Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 18:02:59 +00:00
Vivek Santayana 7f3e251ac4 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 18:02:59 +00:00
Vivek Santayana da2ac4c0ae Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 18:02:59 +00:00
Vivek Santayana 233e173735 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 18:02:59 +00:00
Vivek Santayana 36334ef186 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 18:02:59 +00:00
Vivek Santayana c5686fbd40 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 18:02:59 +00:00
Vivek Santayana 40f3bb3b20 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 18:02:59 +00:00
Vivek Santayana 94556d0731 Merge branch 'master' of https://git.vsnt.uk/viveksantayana/ska-referee-test 2021-12-06 18:02:59 +00:00
viveksantayana ccab358464 Correct error 2021-12-06 16:56:54 +00:00
viveksantayana a0aaa6b035 Correct error 2021-12-06 16:56:54 +00:00
viveksantayana 79b0e83eba Correct error 2021-12-06 16:56:54 +00:00
viveksantayana 9b34fb8f73 Correct error 2021-12-06 16:56:54 +00:00
viveksantayana 22e163f036 Correct error 2021-12-06 16:56:54 +00:00
viveksantayana c90a37a99b Correct error 2021-12-06 16:56:54 +00:00
viveksantayana 511eccac99 Correct error 2021-12-06 16:56:54 +00:00
viveksantayana 8ec0967f40 Correct error 2021-12-06 16:56:54 +00:00
viveksantayana 2f611d43cc Correct error 2021-12-06 16:56:54 +00:00
viveksantayana ae1380407c Correct error 2021-12-06 16:56:54 +00:00
viveksantayana 1a290e3bd6 Correct error 2021-12-06 16:56:54 +00:00
viveksantayana 1e7222c781 Correct error 2021-12-06 16:56:54 +00:00
viveksantayana 39af68cf36 Correct error 2021-12-06 16:56:54 +00:00
viveksantayana b65b71df7a Correct error 2021-12-06 16:56:54 +00:00
viveksantayana 9a4820c725 Added correct answer view 2021-12-06 13:44:40 +00:00
viveksantayana 1cc5b9cf57 Added correct answer view 2021-12-06 13:44:40 +00:00
viveksantayana 6c327c7978 Added correct answer view 2021-12-06 13:44:40 +00:00
viveksantayana 3d03fb79a7 Added correct answer view 2021-12-06 13:44:40 +00:00
viveksantayana c730fca3eb Added correct answer view 2021-12-06 13:44:40 +00:00
viveksantayana f4dbc55f88 Added correct answer view 2021-12-06 13:44:40 +00:00
viveksantayana ba106ff684 Added correct answer view 2021-12-06 13:44:40 +00:00
viveksantayana 738f4eae86 Added correct answer view 2021-12-06 13:44:40 +00:00
viveksantayana 1015148d4d Added correct answer view 2021-12-06 13:44:40 +00:00
viveksantayana d114b061b4 Added correct answer view 2021-12-06 13:44:40 +00:00
viveksantayana 6db6baab50 Added correct answer view 2021-12-06 13:44:40 +00:00
viveksantayana 9b5b97eb1d Added correct answer view 2021-12-06 13:44:40 +00:00
viveksantayana acfcca13b3 Added correct answer view 2021-12-06 13:44:40 +00:00
viveksantayana 52ab3af1f2 Added correct answer view 2021-12-06 13:44:40 +00:00
viveksantayana 79ca8fc932 Added individual result correct/incorrect flag 2021-12-06 13:42:26 +00:00
viveksantayana 10a524b7b8 Added individual result correct/incorrect flag 2021-12-06 13:42:26 +00:00
viveksantayana 3a380c9f50 Added individual result correct/incorrect flag 2021-12-06 13:42:26 +00:00
viveksantayana 41a7129959 Added individual result correct/incorrect flag 2021-12-06 13:42:26 +00:00
viveksantayana b9bff4812b Added individual result correct/incorrect flag 2021-12-06 13:42:26 +00:00
viveksantayana 33f555f847 Added individual result correct/incorrect flag 2021-12-06 13:42:26 +00:00
viveksantayana dedd2d3449 Added individual result correct/incorrect flag 2021-12-06 13:42:26 +00:00
viveksantayana bf7e0a2a18 Added individual result correct/incorrect flag 2021-12-06 13:42:26 +00:00
viveksantayana 9bf39107fb Added individual result correct/incorrect flag 2021-12-06 13:42:26 +00:00
viveksantayana d34aa82e86 Added individual result correct/incorrect flag 2021-12-06 13:42:26 +00:00
viveksantayana 349dd030d6 Added individual result correct/incorrect flag 2021-12-06 13:42:26 +00:00
viveksantayana af9b5210fa Added individual result correct/incorrect flag 2021-12-06 13:42:26 +00:00
viveksantayana 03d419c3fc Added individual result correct/incorrect flag 2021-12-06 13:42:26 +00:00
viveksantayana 389fbf99aa Added individual result correct/incorrect flag 2021-12-06 13:42:26 +00:00
viveksantayana 1cafa04763 Added Certbot and Nginx to Docker stack 2021-12-06 13:39:06 +00:00
viveksantayana ac21f571b4 Added Certbot and Nginx to Docker stack 2021-12-06 13:39:06 +00:00
viveksantayana bc68089f87 Added Certbot and Nginx to Docker stack 2021-12-06 13:39:06 +00:00
viveksantayana 8cb4435517 Added Certbot and Nginx to Docker stack 2021-12-06 13:39:06 +00:00
viveksantayana 9b7a3b3ec0 Added Certbot and Nginx to Docker stack 2021-12-06 13:39:06 +00:00
viveksantayana 87c15070bd Added Certbot and Nginx to Docker stack 2021-12-06 13:39:06 +00:00
viveksantayana 23136b7e40 Added Certbot and Nginx to Docker stack 2021-12-06 13:39:06 +00:00
viveksantayana 2e4035d8a4 Added Certbot and Nginx to Docker stack 2021-12-06 13:39:06 +00:00
viveksantayana 9a7758f208 Added Certbot and Nginx to Docker stack 2021-12-06 13:39:06 +00:00
viveksantayana 7063fe271e Added Certbot and Nginx to Docker stack 2021-12-06 13:39:06 +00:00
viveksantayana ce31c3e691 Added Certbot and Nginx to Docker stack 2021-12-06 13:39:06 +00:00
viveksantayana 8d65b0c089 Added Certbot and Nginx to Docker stack 2021-12-06 13:39:06 +00:00
viveksantayana 8ead32c34d Added Certbot and Nginx to Docker stack 2021-12-06 13:39:06 +00:00
viveksantayana 9988a989a6 Added Certbot and Nginx to Docker stack 2021-12-06 13:39:06 +00:00
viveksantayana 20e418aeae Nginx Server 2021-12-06 13:29:20 +00:00
viveksantayana 0fce697095 Nginx Server 2021-12-06 13:29:20 +00:00
viveksantayana 9affa657c4 Nginx Server 2021-12-06 13:29:20 +00:00
viveksantayana 1a20f1ec67 Nginx Server 2021-12-06 13:29:20 +00:00
viveksantayana 395ddbd460 Nginx Server 2021-12-06 13:29:20 +00:00
viveksantayana 0a1a9b007d Nginx Server 2021-12-06 13:29:20 +00:00
viveksantayana 93b8ac40df Nginx Server 2021-12-06 13:29:20 +00:00
viveksantayana 09f71fc5a7 Nginx Server 2021-12-06 13:29:20 +00:00
viveksantayana e6d22f2a89 Nginx Server 2021-12-06 13:29:20 +00:00
viveksantayana e694119a58 Nginx Server 2021-12-06 13:29:20 +00:00
viveksantayana f242413911 Nginx Server 2021-12-06 13:29:20 +00:00
viveksantayana 67bbab0061 Nginx Server 2021-12-06 13:29:20 +00:00
viveksantayana 41a9892538 Nginx Server 2021-12-06 13:29:20 +00:00
viveksantayana 9992138bc4 Nginx Server 2021-12-06 13:29:20 +00:00
viveksantayana f548221a10 Dockerised. Restructured to remove circular import
Moved most of app definitions out of guard function to use wsgi
Updated configuration files and referencing of .env values.
Local version needs dotenv or exporting of env variables.
Dockerised version works fine without load_dotenv.
Ready to test now!
2021-12-05 03:49:31 +00:00
viveksantayana 347378d785 Dockerised. Restructured to remove circular import
Moved most of app definitions out of guard function to use wsgi
Updated configuration files and referencing of .env values.
Local version needs dotenv or exporting of env variables.
Dockerised version works fine without load_dotenv.
Ready to test now!
2021-12-05 03:49:31 +00:00
viveksantayana 4d883e8dce Dockerised. Restructured to remove circular import
Moved most of app definitions out of guard function to use wsgi
Updated configuration files and referencing of .env values.
Local version needs dotenv or exporting of env variables.
Dockerised version works fine without load_dotenv.
Ready to test now!
2021-12-05 03:49:31 +00:00
viveksantayana db59e6c85c Dockerised. Restructured to remove circular import
Moved most of app definitions out of guard function to use wsgi
Updated configuration files and referencing of .env values.
Local version needs dotenv or exporting of env variables.
Dockerised version works fine without load_dotenv.
Ready to test now!
2021-12-05 03:49:31 +00:00
viveksantayana 92e2462bb9 Dockerised. Restructured to remove circular import
Moved most of app definitions out of guard function to use wsgi
Updated configuration files and referencing of .env values.
Local version needs dotenv or exporting of env variables.
Dockerised version works fine without load_dotenv.
Ready to test now!
2021-12-05 03:49:31 +00:00
viveksantayana 281575bbf7 Dockerised. Restructured to remove circular import
Moved most of app definitions out of guard function to use wsgi
Updated configuration files and referencing of .env values.
Local version needs dotenv or exporting of env variables.
Dockerised version works fine without load_dotenv.
Ready to test now!
2021-12-05 03:49:31 +00:00
viveksantayana 6ea02c28d4 Dockerised. Restructured to remove circular import
Moved most of app definitions out of guard function to use wsgi
Updated configuration files and referencing of .env values.
Local version needs dotenv or exporting of env variables.
Dockerised version works fine without load_dotenv.
Ready to test now!
2021-12-05 03:49:31 +00:00
viveksantayana 05a8a78ed9 Dockerised. Restructured to remove circular import
Moved most of app definitions out of guard function to use wsgi
Updated configuration files and referencing of .env values.
Local version needs dotenv or exporting of env variables.
Dockerised version works fine without load_dotenv.
Ready to test now!
2021-12-05 03:49:31 +00:00
viveksantayana 4b62ff6e80 Dockerised. Restructured to remove circular import
Moved most of app definitions out of guard function to use wsgi
Updated configuration files and referencing of .env values.
Local version needs dotenv or exporting of env variables.
Dockerised version works fine without load_dotenv.
Ready to test now!
2021-12-05 03:49:31 +00:00
viveksantayana ac5d17fc66 Dockerised. Restructured to remove circular import
Moved most of app definitions out of guard function to use wsgi
Updated configuration files and referencing of .env values.
Local version needs dotenv or exporting of env variables.
Dockerised version works fine without load_dotenv.
Ready to test now!
2021-12-05 03:49:31 +00:00
viveksantayana 79c23471ee Dockerised. Restructured to remove circular import
Moved most of app definitions out of guard function to use wsgi
Updated configuration files and referencing of .env values.
Local version needs dotenv or exporting of env variables.
Dockerised version works fine without load_dotenv.
Ready to test now!
2021-12-05 03:49:31 +00:00
viveksantayana 37d7e5010f Dockerised. Restructured to remove circular import
Moved most of app definitions out of guard function to use wsgi
Updated configuration files and referencing of .env values.
Local version needs dotenv or exporting of env variables.
Dockerised version works fine without load_dotenv.
Ready to test now!
2021-12-05 03:49:31 +00:00
viveksantayana 350c67ab10 Dockerised. Restructured to remove circular import
Moved most of app definitions out of guard function to use wsgi
Updated configuration files and referencing of .env values.
Local version needs dotenv or exporting of env variables.
Dockerised version works fine without load_dotenv.
Ready to test now!
2021-12-05 03:49:31 +00:00
viveksantayana ce40568870 Dockerised. Restructured to remove circular import
Moved most of app definitions out of guard function to use wsgi
Updated configuration files and referencing of .env values.
Local version needs dotenv or exporting of env variables.
Dockerised version works fine without load_dotenv.
Ready to test now!
2021-12-05 03:49:31 +00:00
viveksantayana f4234f57b1 dockerise 2021-12-05 00:17:54 +00:00
viveksantayana e715d07bf1 dockerise 2021-12-05 00:17:54 +00:00
viveksantayana b8c652e78a dockerise 2021-12-05 00:17:54 +00:00
viveksantayana 8d76ecb78a dockerise 2021-12-05 00:17:54 +00:00
viveksantayana 9d760aafef dockerise 2021-12-05 00:17:54 +00:00
viveksantayana f9d16b3608 dockerise 2021-12-05 00:17:54 +00:00
viveksantayana 4da025d50f dockerise 2021-12-05 00:17:54 +00:00
viveksantayana 787b741687 dockerise 2021-12-05 00:17:54 +00:00
viveksantayana 21f54c9789 dockerise 2021-12-05 00:17:54 +00:00
viveksantayana 2aca8015af dockerise 2021-12-05 00:17:54 +00:00
viveksantayana c4f088f29c dockerise 2021-12-05 00:17:54 +00:00
viveksantayana 89ae75050b dockerise 2021-12-05 00:17:54 +00:00
viveksantayana 0c01bff022 dockerise 2021-12-05 00:17:54 +00:00
viveksantayana efa83d2bf8 dockerise 2021-12-05 00:17:54 +00:00
viveksantayana 388d89d95d Deleted redundant variable assignment 2021-12-04 21:29:15 +00:00
viveksantayana 3856d5fa84 Deleted redundant variable assignment 2021-12-04 21:29:15 +00:00
viveksantayana 8a368dbd16 Deleted redundant variable assignment 2021-12-04 21:29:15 +00:00
viveksantayana 0318ddbf21 Deleted redundant variable assignment 2021-12-04 21:29:15 +00:00
viveksantayana 4f842223cd Deleted redundant variable assignment 2021-12-04 21:29:15 +00:00
viveksantayana 78746b5e1a Deleted redundant variable assignment 2021-12-04 21:29:15 +00:00
viveksantayana 81eac4b880 Deleted redundant variable assignment 2021-12-04 21:29:15 +00:00
viveksantayana f03c92082e Deleted redundant variable assignment 2021-12-04 21:29:15 +00:00
viveksantayana aafde86012 Deleted redundant variable assignment 2021-12-04 21:29:15 +00:00
viveksantayana 3a63c72bbb Deleted redundant variable assignment 2021-12-04 21:29:15 +00:00
viveksantayana 31d7e978f4 Deleted redundant variable assignment 2021-12-04 21:29:15 +00:00
viveksantayana c3f6d45883 Deleted redundant variable assignment 2021-12-04 21:29:15 +00:00
viveksantayana 3dd7739a16 Deleted redundant variable assignment 2021-12-04 21:29:15 +00:00
viveksantayana 27cead22ad Deleted redundant variable assignment 2021-12-04 21:29:15 +00:00
viveksantayana 3a39ff6fc3 Updated footer, added question counters. 2021-12-04 21:25:43 +00:00
viveksantayana a0a33b81c4 Updated footer, added question counters. 2021-12-04 21:25:43 +00:00
viveksantayana 8ab0a5e164 Updated footer, added question counters. 2021-12-04 21:25:43 +00:00
viveksantayana 486aeb86a2 Updated footer, added question counters. 2021-12-04 21:25:43 +00:00
viveksantayana c3c6e5084a Updated footer, added question counters. 2021-12-04 21:25:43 +00:00
viveksantayana 21a7eeea21 Updated footer, added question counters. 2021-12-04 21:25:43 +00:00
viveksantayana ef7de71a5b Updated footer, added question counters. 2021-12-04 21:25:43 +00:00
viveksantayana 1a1dff2c5d Updated footer, added question counters. 2021-12-04 21:25:43 +00:00
viveksantayana 9b8f0f3d8e Updated footer, added question counters. 2021-12-04 21:25:43 +00:00
viveksantayana da6d380786 Updated footer, added question counters. 2021-12-04 21:25:43 +00:00
viveksantayana 77267f944b Updated footer, added question counters. 2021-12-04 21:25:43 +00:00
viveksantayana a1ed557dc2 Updated footer, added question counters. 2021-12-04 21:25:43 +00:00
viveksantayana a3cdc42fab Updated footer, added question counters. 2021-12-04 21:25:43 +00:00
viveksantayana 3ffb4a68e1 Updated footer, added question counters. 2021-12-04 21:25:43 +00:00
viveksantayana 12d9cd39be Finished making dashboards 2021-12-04 20:47:43 +00:00
viveksantayana 9fa515cf9b Finished making dashboards 2021-12-04 20:47:43 +00:00
viveksantayana 0fd7ac7f1f Finished making dashboards 2021-12-04 20:47:43 +00:00
viveksantayana e313df57d6 Finished making dashboards 2021-12-04 20:47:43 +00:00
viveksantayana 66d8fb7d93 Finished making dashboards 2021-12-04 20:47:43 +00:00
viveksantayana 0ad4ae38fa Finished making dashboards 2021-12-04 20:47:43 +00:00
viveksantayana cca2633f1a Finished making dashboards 2021-12-04 20:47:43 +00:00
viveksantayana e1fcad3b42 Finished making dashboards 2021-12-04 20:47:43 +00:00
viveksantayana 031d18e922 Finished making dashboards 2021-12-04 20:47:43 +00:00
viveksantayana 4aad0c1213 Finished making dashboards 2021-12-04 20:47:43 +00:00
viveksantayana 85efd755d8 Finished making dashboards 2021-12-04 20:47:43 +00:00
viveksantayana ef1cad1995 Finished making dashboards 2021-12-04 20:47:43 +00:00
viveksantayana 292c642e73 Finished making dashboards 2021-12-04 20:47:43 +00:00
viveksantayana ab2ca04ceb Finished making dashboards 2021-12-04 20:47:43 +00:00
viveksantayana c88c142f7f Added question progress bar 2021-12-04 18:50:09 +00:00
viveksantayana 85460b7192 Added question progress bar 2021-12-04 18:50:09 +00:00
viveksantayana ff6865c7ca Added question progress bar 2021-12-04 18:50:09 +00:00
viveksantayana ecaa4fa95f Added question progress bar 2021-12-04 18:50:09 +00:00
viveksantayana 488389057c Added question progress bar 2021-12-04 18:50:09 +00:00
viveksantayana 6442c7f678 Added question progress bar 2021-12-04 18:50:09 +00:00
viveksantayana 186e83f92a Added question progress bar 2021-12-04 18:50:09 +00:00
viveksantayana da6ae3c826 Added question progress bar 2021-12-04 18:50:09 +00:00
viveksantayana efb89e7626 Added question progress bar 2021-12-04 18:50:09 +00:00
viveksantayana 23d6f833d7 Added question progress bar 2021-12-04 18:50:09 +00:00
viveksantayana fdb5cc1cf9 Added question progress bar 2021-12-04 18:50:09 +00:00
viveksantayana 17f9ef79b7 Added question progress bar 2021-12-04 18:50:09 +00:00
viveksantayana 335c42f924 Added question progress bar 2021-12-04 18:50:09 +00:00
viveksantayana 231f1d97bc Added question progress bar 2021-12-04 18:50:09 +00:00
viveksantayana dbc0c782c0 Added custom 404 display and login redirect 2021-12-04 17:40:01 +00:00
viveksantayana 383f303127 Added custom 404 display and login redirect 2021-12-04 17:40:01 +00:00
viveksantayana 27bb07a942 Added custom 404 display and login redirect 2021-12-04 17:40:01 +00:00
viveksantayana 6331dda37b Added custom 404 display and login redirect 2021-12-04 17:40:01 +00:00
viveksantayana 0d63413835 Added custom 404 display and login redirect 2021-12-04 17:40:01 +00:00
viveksantayana eb812a9ebb Added custom 404 display and login redirect 2021-12-04 17:40:01 +00:00
viveksantayana a126d1f91d Added custom 404 display and login redirect 2021-12-04 17:40:01 +00:00
viveksantayana 30e298aa02 Added custom 404 display and login redirect 2021-12-04 17:40:01 +00:00
viveksantayana 196c4774a2 Added custom 404 display and login redirect 2021-12-04 17:40:01 +00:00
viveksantayana cc8db3fea4 Added custom 404 display and login redirect 2021-12-04 17:40:01 +00:00
viveksantayana 727fc2d8c0 Added custom 404 display and login redirect 2021-12-04 17:40:01 +00:00
viveksantayana 7c2b9df0d0 Added custom 404 display and login redirect 2021-12-04 17:40:01 +00:00
viveksantayana f0bfecaad3 Added custom 404 display and login redirect 2021-12-04 17:40:01 +00:00
viveksantayana 3b605c3340 Added custom 404 display and login redirect 2021-12-04 17:40:01 +00:00
viveksantayana d8e7bf6ae8 Completed client side time adjustment handling
Corrected error display bug
Removed redundant auth and models in quiz client app
2021-12-04 17:14:28 +00:00
viveksantayana 9ec9a5e80f Completed client side time adjustment handling
Corrected error display bug
Removed redundant auth and models in quiz client app
2021-12-04 17:14:28 +00:00
viveksantayana 3c903424fb Completed client side time adjustment handling
Corrected error display bug
Removed redundant auth and models in quiz client app
2021-12-04 17:14:28 +00:00
viveksantayana 8b2daf400a Completed client side time adjustment handling
Corrected error display bug
Removed redundant auth and models in quiz client app
2021-12-04 17:14:28 +00:00
viveksantayana 766487b669 Completed client side time adjustment handling
Corrected error display bug
Removed redundant auth and models in quiz client app
2021-12-04 17:14:28 +00:00
viveksantayana cf5ac8b221 Completed client side time adjustment handling
Corrected error display bug
Removed redundant auth and models in quiz client app
2021-12-04 17:14:28 +00:00
viveksantayana 0e52c12b35 Completed client side time adjustment handling
Corrected error display bug
Removed redundant auth and models in quiz client app
2021-12-04 17:14:28 +00:00
viveksantayana 3a1abe5157 Completed client side time adjustment handling
Corrected error display bug
Removed redundant auth and models in quiz client app
2021-12-04 17:14:28 +00:00
viveksantayana eff7b25d71 Completed client side time adjustment handling
Corrected error display bug
Removed redundant auth and models in quiz client app
2021-12-04 17:14:28 +00:00
viveksantayana 9a2d738653 Completed client side time adjustment handling
Corrected error display bug
Removed redundant auth and models in quiz client app
2021-12-04 17:14:28 +00:00
viveksantayana 93bf9e94fb Completed client side time adjustment handling
Corrected error display bug
Removed redundant auth and models in quiz client app
2021-12-04 17:14:28 +00:00
viveksantayana 5c6f56f1c3 Completed client side time adjustment handling
Corrected error display bug
Removed redundant auth and models in quiz client app
2021-12-04 17:14:28 +00:00
viveksantayana efb69efa10 Completed client side time adjustment handling
Corrected error display bug
Removed redundant auth and models in quiz client app
2021-12-04 17:14:28 +00:00
viveksantayana 329538f7f5 Completed client side time adjustment handling
Corrected error display bug
Removed redundant auth and models in quiz client app
2021-12-04 17:14:28 +00:00
viveksantayana cfdb4db0c3 Added server and admin-side time limit adjustments 2021-12-04 15:41:47 +00:00
viveksantayana c46facdf8b Added server and admin-side time limit adjustments 2021-12-04 15:41:47 +00:00
viveksantayana 5151b98f97 Added server and admin-side time limit adjustments 2021-12-04 15:41:47 +00:00
viveksantayana f97b2c7cbb Added server and admin-side time limit adjustments 2021-12-04 15:41:47 +00:00
viveksantayana b102dc86aa Added server and admin-side time limit adjustments 2021-12-04 15:41:47 +00:00
viveksantayana c8c93dc721 Added server and admin-side time limit adjustments 2021-12-04 15:41:47 +00:00
viveksantayana d9dc2e209f Added server and admin-side time limit adjustments 2021-12-04 15:41:47 +00:00
viveksantayana 86f8c12279 Added server and admin-side time limit adjustments 2021-12-04 15:41:47 +00:00
viveksantayana be63eed81d Added server and admin-side time limit adjustments 2021-12-04 15:41:47 +00:00
viveksantayana c71e91326f Added server and admin-side time limit adjustments 2021-12-04 15:41:47 +00:00
viveksantayana cca01a6c2f Added server and admin-side time limit adjustments 2021-12-04 15:41:47 +00:00
viveksantayana 41d92b97a0 Added server and admin-side time limit adjustments 2021-12-04 15:41:47 +00:00
viveksantayana 6855ddfdcb Added server and admin-side time limit adjustments 2021-12-04 15:41:47 +00:00
viveksantayana 2f6ccd530a Added server and admin-side time limit adjustments 2021-12-04 15:41:47 +00:00
viveksantayana 5d9dba0e3d Refactored form model for custom validators 2021-12-04 15:41:24 +00:00
viveksantayana 0eda083bf2 Refactored form model for custom validators 2021-12-04 15:41:24 +00:00
viveksantayana ee159402d0 Refactored form model for custom validators 2021-12-04 15:41:24 +00:00
viveksantayana 56b3e6a2f5 Refactored form model for custom validators 2021-12-04 15:41:24 +00:00
viveksantayana 82ed0cf7cc Refactored form model for custom validators 2021-12-04 15:41:24 +00:00
viveksantayana 49b0ea14f0 Refactored form model for custom validators 2021-12-04 15:41:24 +00:00
viveksantayana 66f2da31b6 Refactored form model for custom validators 2021-12-04 15:41:24 +00:00
viveksantayana cf39f83243 Refactored form model for custom validators 2021-12-04 15:41:24 +00:00
viveksantayana 8f8a12b609 Refactored form model for custom validators 2021-12-04 15:41:24 +00:00
viveksantayana 5bd04d8dc0 Refactored form model for custom validators 2021-12-04 15:41:24 +00:00
viveksantayana 4b603b70a0 Refactored form model for custom validators 2021-12-04 15:41:24 +00:00
viveksantayana 48624584fe Refactored form model for custom validators 2021-12-04 15:41:24 +00:00
viveksantayana 4902d40787 Refactored form model for custom validators 2021-12-04 15:41:24 +00:00
viveksantayana fb7f9e328d Refactored form model for custom validators 2021-12-04 15:41:24 +00:00
viveksantayana c7ddf034a3 Typo correction 2021-12-04 12:48:01 +00:00
viveksantayana f47e22ccae Typo correction 2021-12-04 12:48:01 +00:00
viveksantayana e001ccfa01 Typo correction 2021-12-04 12:48:01 +00:00
viveksantayana 121dd32bfb Typo correction 2021-12-04 12:48:01 +00:00
viveksantayana b6179430be Typo correction 2021-12-04 12:48:01 +00:00
viveksantayana 4b671242ff Typo correction 2021-12-04 12:48:01 +00:00
viveksantayana 8924232a93 Typo correction 2021-12-04 12:48:01 +00:00
viveksantayana ac36309527 Typo correction 2021-12-04 12:48:01 +00:00
viveksantayana ba082d4ed7 Typo correction 2021-12-04 12:48:01 +00:00
viveksantayana 7eddcabb7f Typo correction 2021-12-04 12:48:01 +00:00
viveksantayana d890a45f2b Typo correction 2021-12-04 12:48:01 +00:00
viveksantayana f66d62db37 Typo correction 2021-12-04 12:48:01 +00:00
viveksantayana b23d583bfb Typo correction 2021-12-04 12:48:01 +00:00
viveksantayana 567b272161 Typo correction 2021-12-04 12:48:01 +00:00
viveksantayana 2f04671ec5 Added results CRUD and result detailed view 2021-12-04 12:20:03 +00:00
viveksantayana 5f9b30cc01 Added results CRUD and result detailed view 2021-12-04 12:20:03 +00:00
viveksantayana c375576436 Added results CRUD and result detailed view 2021-12-04 12:20:03 +00:00
viveksantayana 9fac4ebd82 Added results CRUD and result detailed view 2021-12-04 12:20:03 +00:00
viveksantayana c536fb95b2 Added results CRUD and result detailed view 2021-12-04 12:20:03 +00:00
viveksantayana 018be71ed3 Added results CRUD and result detailed view 2021-12-04 12:20:03 +00:00
viveksantayana fbe3a59847 Added results CRUD and result detailed view 2021-12-04 12:20:03 +00:00
viveksantayana 6472241dfc Added results CRUD and result detailed view 2021-12-04 12:20:03 +00:00
viveksantayana 72aa7696ac Added results CRUD and result detailed view 2021-12-04 12:20:03 +00:00
viveksantayana 998ec597b1 Added results CRUD and result detailed view 2021-12-04 12:20:03 +00:00
viveksantayana 237aabf4ba Added results CRUD and result detailed view 2021-12-04 12:20:03 +00:00
viveksantayana 3470f7422c Added results CRUD and result detailed view 2021-12-04 12:20:03 +00:00
viveksantayana 37367cecc3 Added results CRUD and result detailed view 2021-12-04 12:20:03 +00:00
viveksantayana 9be3b1a487 Added results CRUD and result detailed view 2021-12-04 12:20:03 +00:00
viveksantayana c00ffd3ed0 Streamlined post form handlers for admin console 2021-12-01 08:26:08 +00:00
viveksantayana a341974ebc Streamlined post form handlers for admin console 2021-12-01 08:26:08 +00:00
viveksantayana f17ba4f6bf Streamlined post form handlers for admin console 2021-12-01 08:26:08 +00:00
viveksantayana 4d734dbbe8 Streamlined post form handlers for admin console 2021-12-01 08:26:08 +00:00
viveksantayana 700850434a Streamlined post form handlers for admin console 2021-12-01 08:26:08 +00:00
viveksantayana 6a4fe535e1 Streamlined post form handlers for admin console 2021-12-01 08:26:08 +00:00
viveksantayana 019622bd85 Streamlined post form handlers for admin console 2021-12-01 08:26:08 +00:00
viveksantayana fe61456922 Streamlined post form handlers for admin console 2021-12-01 08:26:08 +00:00
viveksantayana 989d6900df Streamlined post form handlers for admin console 2021-12-01 08:26:08 +00:00
viveksantayana 64f1da772a Streamlined post form handlers for admin console 2021-12-01 08:26:08 +00:00
viveksantayana e69f60d813 Streamlined post form handlers for admin console 2021-12-01 08:26:08 +00:00
viveksantayana 6b79fb8ebe Streamlined post form handlers for admin console 2021-12-01 08:26:08 +00:00
viveksantayana da35da2b76 Streamlined post form handlers for admin console 2021-12-01 08:26:08 +00:00
viveksantayana 8963e5461e Streamlined post form handlers for admin console 2021-12-01 08:26:08 +00:00
viveksantayana a780b2330e Added footer text to quiz site. 2021-12-01 01:00:53 +00:00
viveksantayana f068c6c937 Added footer text to quiz site. 2021-12-01 01:00:53 +00:00
viveksantayana a3a1c2ab2f Added footer text to quiz site. 2021-12-01 01:00:53 +00:00
viveksantayana a0fc1653e7 Added footer text to quiz site. 2021-12-01 01:00:53 +00:00
viveksantayana dcd047a5ae Added footer text to quiz site. 2021-12-01 01:00:53 +00:00
viveksantayana 3f5d0feedb Added footer text to quiz site. 2021-12-01 01:00:53 +00:00
viveksantayana 268fa36507 Added footer text to quiz site. 2021-12-01 01:00:53 +00:00
viveksantayana f0ba8777e3 Added footer text to quiz site. 2021-12-01 01:00:53 +00:00
viveksantayana 1d778a6bdd Added footer text to quiz site. 2021-12-01 01:00:53 +00:00
viveksantayana 43989af1f1 Added footer text to quiz site. 2021-12-01 01:00:53 +00:00
viveksantayana cf82a85070 Added footer text to quiz site. 2021-12-01 01:00:53 +00:00
viveksantayana 0a6a14f8d0 Added footer text to quiz site. 2021-12-01 01:00:53 +00:00
viveksantayana 4cfe7c2cba Added footer text to quiz site. 2021-12-01 01:00:53 +00:00
viveksantayana 5dfc3379fc Added footer text to quiz site. 2021-12-01 01:00:53 +00:00
viveksantayana c08e1c7010 Added result page. 2021-12-01 00:48:47 +00:00
viveksantayana e84fb91452 Added result page. 2021-12-01 00:48:47 +00:00
viveksantayana 2479fd193b Added result page. 2021-12-01 00:48:47 +00:00
viveksantayana 4da20115d2 Added result page. 2021-12-01 00:48:47 +00:00
viveksantayana a6ad184447 Added result page. 2021-12-01 00:48:47 +00:00
viveksantayana 01d1a35238 Added result page. 2021-12-01 00:48:47 +00:00
viveksantayana ff9ede6cce Added result page. 2021-12-01 00:48:47 +00:00
viveksantayana 05b68fdd95 Added result page. 2021-12-01 00:48:47 +00:00
viveksantayana 0725b8b490 Added result page. 2021-12-01 00:48:47 +00:00
viveksantayana 900929b875 Added result page. 2021-12-01 00:48:47 +00:00
viveksantayana 7e65416f80 Added result page. 2021-12-01 00:48:47 +00:00
viveksantayana 8cf9629bf1 Added result page. 2021-12-01 00:48:47 +00:00
viveksantayana ecc5780604 Added result page. 2021-12-01 00:48:47 +00:00
viveksantayana 40926c1063 Added result page. 2021-12-01 00:48:47 +00:00
viveksantayana ba47f79d44 Finessing of client. 2021-12-01 00:48:38 +00:00
viveksantayana 4d81172deb Finessing of client. 2021-12-01 00:48:38 +00:00
viveksantayana 6f4353266c Finessing of client. 2021-12-01 00:48:38 +00:00
viveksantayana d1cf44fd18 Finessing of client. 2021-12-01 00:48:38 +00:00
viveksantayana abfa7b21ba Finessing of client. 2021-12-01 00:48:38 +00:00
viveksantayana 23aee7abed Finessing of client. 2021-12-01 00:48:38 +00:00
viveksantayana 2536e595f0 Finessing of client. 2021-12-01 00:48:38 +00:00
viveksantayana bda9946859 Finessing of client. 2021-12-01 00:48:38 +00:00
viveksantayana 93552023f6 Finessing of client. 2021-12-01 00:48:38 +00:00
viveksantayana a67ea9951b Finessing of client. 2021-12-01 00:48:38 +00:00
viveksantayana 61ac4c1cb0 Finessing of client. 2021-12-01 00:48:38 +00:00
viveksantayana 756af0a064 Finessing of client. 2021-12-01 00:48:38 +00:00
viveksantayana 3907ede872 Finessing of client. 2021-12-01 00:48:38 +00:00
viveksantayana 7caf54a5ba Finessing of client. 2021-12-01 00:48:38 +00:00
viveksantayana 222b8e8a8b Finessing display on index and instruction pages 2021-12-01 00:48:05 +00:00
viveksantayana 29d015cdb4 Finessing display on index and instruction pages 2021-12-01 00:48:05 +00:00
viveksantayana 2875c59460 Finessing display on index and instruction pages 2021-12-01 00:48:05 +00:00
viveksantayana 19054a9c67 Finessing display on index and instruction pages 2021-12-01 00:48:05 +00:00
viveksantayana bb09930116 Finessing display on index and instruction pages 2021-12-01 00:48:05 +00:00
viveksantayana e2a9d79484 Finessing display on index and instruction pages 2021-12-01 00:48:05 +00:00
viveksantayana 31736bfbaf Finessing display on index and instruction pages 2021-12-01 00:48:05 +00:00
viveksantayana b5625a5fb2 Finessing display on index and instruction pages 2021-12-01 00:48:05 +00:00
viveksantayana aab1a2815e Finessing display on index and instruction pages 2021-12-01 00:48:05 +00:00
viveksantayana 6103010169 Finessing display on index and instruction pages 2021-12-01 00:48:05 +00:00
viveksantayana e326729ddb Finessing display on index and instruction pages 2021-12-01 00:48:05 +00:00
viveksantayana 283dfe8ecf Finessing display on index and instruction pages 2021-12-01 00:48:05 +00:00
viveksantayana ae4c418ed7 Finessing display on index and instruction pages 2021-12-01 00:48:05 +00:00
viveksantayana faeaeb8b2c Finessing display on index and instruction pages 2021-12-01 00:48:05 +00:00
viveksantayana 75db9fde3c Set timer to be hidden by default
Swapped default behaviour of timer around.
2021-12-01 00:47:19 +00:00
viveksantayana bbc2af5962 Set timer to be hidden by default
Swapped default behaviour of timer around.
2021-12-01 00:47:19 +00:00
viveksantayana 91621625e6 Set timer to be hidden by default
Swapped default behaviour of timer around.
2021-12-01 00:47:19 +00:00
viveksantayana fb19b12e7f Set timer to be hidden by default
Swapped default behaviour of timer around.
2021-12-01 00:47:19 +00:00
viveksantayana d23d3ca6d1 Set timer to be hidden by default
Swapped default behaviour of timer around.
2021-12-01 00:47:19 +00:00
viveksantayana 516200d881 Set timer to be hidden by default
Swapped default behaviour of timer around.
2021-12-01 00:47:19 +00:00
viveksantayana 8969505383 Set timer to be hidden by default
Swapped default behaviour of timer around.
2021-12-01 00:47:19 +00:00
viveksantayana e9ff14d63e Set timer to be hidden by default
Swapped default behaviour of timer around.
2021-12-01 00:47:19 +00:00
viveksantayana da251b57b0 Set timer to be hidden by default
Swapped default behaviour of timer around.
2021-12-01 00:47:19 +00:00
viveksantayana 10b325ad29 Set timer to be hidden by default
Swapped default behaviour of timer around.
2021-12-01 00:47:19 +00:00
viveksantayana 8a2d81ec23 Set timer to be hidden by default
Swapped default behaviour of timer around.
2021-12-01 00:47:19 +00:00
viveksantayana a15844f52d Set timer to be hidden by default
Swapped default behaviour of timer around.
2021-12-01 00:47:19 +00:00
viveksantayana 6ad73aa3c9 Set timer to be hidden by default
Swapped default behaviour of timer around.
2021-12-01 00:47:19 +00:00
viveksantayana e0cac3c800 Set timer to be hidden by default
Swapped default behaviour of timer around.
2021-12-01 00:47:19 +00:00
viveksantayana be26a19f2e Added text to index and instruction pages. 2021-12-01 00:46:41 +00:00
viveksantayana b9ea8dffa3 Added text to index and instruction pages. 2021-12-01 00:46:41 +00:00
viveksantayana 218090d1e5 Added text to index and instruction pages. 2021-12-01 00:46:41 +00:00
viveksantayana 51f40311e0 Added text to index and instruction pages. 2021-12-01 00:46:41 +00:00
viveksantayana f65e5b122f Added text to index and instruction pages. 2021-12-01 00:46:41 +00:00
viveksantayana 038a4e44ba Added text to index and instruction pages. 2021-12-01 00:46:41 +00:00
viveksantayana f3cb7deaf4 Added text to index and instruction pages. 2021-12-01 00:46:41 +00:00
viveksantayana 1745299e12 Added text to index and instruction pages. 2021-12-01 00:46:41 +00:00
viveksantayana 7904c52671 Added text to index and instruction pages. 2021-12-01 00:46:41 +00:00
viveksantayana b17e04de71 Added text to index and instruction pages. 2021-12-01 00:46:41 +00:00
viveksantayana 779b06b4bf Added text to index and instruction pages. 2021-12-01 00:46:41 +00:00
viveksantayana b66b94fd83 Added text to index and instruction pages. 2021-12-01 00:46:41 +00:00
viveksantayana 2672f9e45f Added text to index and instruction pages. 2021-12-01 00:46:41 +00:00
viveksantayana 2af61ca986 Added text to index and instruction pages. 2021-12-01 00:46:41 +00:00
viveksantayana 7269cec73d Added automated email notification of results. 2021-12-01 00:46:21 +00:00
viveksantayana 26350a7eb6 Added automated email notification of results. 2021-12-01 00:46:21 +00:00
viveksantayana 68a6507c1b Added automated email notification of results. 2021-12-01 00:46:21 +00:00
viveksantayana f49b2f7df1 Added automated email notification of results. 2021-12-01 00:46:21 +00:00
viveksantayana e48ab4b58a Added automated email notification of results. 2021-12-01 00:46:21 +00:00
viveksantayana 3c7b1a70ce Added automated email notification of results. 2021-12-01 00:46:21 +00:00
viveksantayana f38e9df6b9 Added automated email notification of results. 2021-12-01 00:46:21 +00:00
viveksantayana 1f661a7038 Added automated email notification of results. 2021-12-01 00:46:21 +00:00
viveksantayana ab4290a706 Added automated email notification of results. 2021-12-01 00:46:21 +00:00
viveksantayana 66b4c50221 Added automated email notification of results. 2021-12-01 00:46:21 +00:00
viveksantayana a4a3b6de1b Added automated email notification of results. 2021-12-01 00:46:21 +00:00
viveksantayana 9f8a6e1a27 Added automated email notification of results. 2021-12-01 00:46:21 +00:00
viveksantayana 2ca0929fe8 Added automated email notification of results. 2021-12-01 00:46:21 +00:00
viveksantayana d9b72bce0c Added automated email notification of results. 2021-12-01 00:46:21 +00:00
viveksantayana e829514e91 Corrected missing assignment of variable 2021-12-01 00:45:56 +00:00
viveksantayana 6355afef59 Corrected missing assignment of variable 2021-12-01 00:45:56 +00:00
viveksantayana a1d19b4474 Corrected missing assignment of variable 2021-12-01 00:45:56 +00:00
viveksantayana 636c1fdadb Corrected missing assignment of variable 2021-12-01 00:45:56 +00:00
viveksantayana d29a5984f1 Corrected missing assignment of variable 2021-12-01 00:45:56 +00:00
viveksantayana 7df7465012 Corrected missing assignment of variable 2021-12-01 00:45:56 +00:00
viveksantayana 0b2a74ddd3 Corrected missing assignment of variable 2021-12-01 00:45:56 +00:00
viveksantayana a1c3e79e90 Corrected missing assignment of variable 2021-12-01 00:45:56 +00:00
viveksantayana 7358c4440c Corrected missing assignment of variable 2021-12-01 00:45:56 +00:00
viveksantayana 7b1b789644 Corrected missing assignment of variable 2021-12-01 00:45:56 +00:00
viveksantayana e7ca3ac0d7 Corrected missing assignment of variable 2021-12-01 00:45:56 +00:00
viveksantayana 963453d2d6 Corrected missing assignment of variable 2021-12-01 00:45:56 +00:00
viveksantayana 99f1a8d681 Corrected missing assignment of variable 2021-12-01 00:45:56 +00:00
viveksantayana 46ab5d620b Corrected missing assignment of variable 2021-12-01 00:45:56 +00:00
viveksantayana 6593d372e0 Corrected doubled import 2021-12-01 00:45:20 +00:00
viveksantayana df503cf810 Corrected doubled import 2021-12-01 00:45:20 +00:00
viveksantayana cffafa82d9 Corrected doubled import 2021-12-01 00:45:20 +00:00
viveksantayana 6495904cf1 Corrected doubled import 2021-12-01 00:45:20 +00:00
viveksantayana dc432c4ac9 Corrected doubled import 2021-12-01 00:45:20 +00:00
viveksantayana b6171637af Corrected doubled import 2021-12-01 00:45:20 +00:00
viveksantayana f0c4f237de Corrected doubled import 2021-12-01 00:45:20 +00:00
viveksantayana 99bd4df741 Corrected doubled import 2021-12-01 00:45:20 +00:00
viveksantayana 1d38d77c57 Corrected doubled import 2021-12-01 00:45:20 +00:00
viveksantayana a866699f5d Corrected doubled import 2021-12-01 00:45:20 +00:00
viveksantayana 594354e459 Corrected doubled import 2021-12-01 00:45:20 +00:00
viveksantayana 75b43f8993 Corrected doubled import 2021-12-01 00:45:20 +00:00
viveksantayana 268d3a371e Corrected doubled import 2021-12-01 00:45:20 +00:00
viveksantayana e50ad9430e Corrected doubled import 2021-12-01 00:45:20 +00:00
viveksantayana 173b1e329b Exam Code Time Controls 2021-11-30 18:16:52 +00:00
viveksantayana 4eb29750ab Exam Code Time Controls 2021-11-30 18:16:52 +00:00
viveksantayana 346238dab8 Exam Code Time Controls 2021-11-30 18:16:52 +00:00
viveksantayana 6f57a4b24c Exam Code Time Controls 2021-11-30 18:16:52 +00:00
viveksantayana 9913c9e084 Exam Code Time Controls 2021-11-30 18:16:52 +00:00
viveksantayana ae583bd2ef Exam Code Time Controls 2021-11-30 18:16:52 +00:00
viveksantayana ad16311941 Exam Code Time Controls 2021-11-30 18:16:52 +00:00
viveksantayana 493f71ac20 Exam Code Time Controls 2021-11-30 18:16:52 +00:00
viveksantayana ae3d34e0f2 Exam Code Time Controls 2021-11-30 18:16:52 +00:00
viveksantayana 3f29b504b2 Exam Code Time Controls 2021-11-30 18:16:52 +00:00
viveksantayana 2e5c87f0b1 Exam Code Time Controls 2021-11-30 18:16:52 +00:00
viveksantayana 565486aef3 Exam Code Time Controls 2021-11-30 18:16:52 +00:00
viveksantayana 17ba7a8bdd Exam Code Time Controls 2021-11-30 18:16:52 +00:00
viveksantayana e5cecd6102 Exam Code Time Controls 2021-11-30 18:16:52 +00:00
viveksantayana 795545e8af Finished client result API.
Need to work on adjustment user codes and server email notifications.
2021-11-30 18:06:24 +00:00
viveksantayana 535fe31054 Finished client result API.
Need to work on adjustment user codes and server email notifications.
2021-11-30 18:06:24 +00:00
viveksantayana b4f021bb8b Finished client result API.
Need to work on adjustment user codes and server email notifications.
2021-11-30 18:06:24 +00:00
viveksantayana 1f4848cc83 Finished client result API.
Need to work on adjustment user codes and server email notifications.
2021-11-30 18:06:24 +00:00
viveksantayana dcafde1158 Finished client result API.
Need to work on adjustment user codes and server email notifications.
2021-11-30 18:06:24 +00:00
viveksantayana 288ecb60e1 Finished client result API.
Need to work on adjustment user codes and server email notifications.
2021-11-30 18:06:24 +00:00
viveksantayana 9b038dc8e4 Finished client result API.
Need to work on adjustment user codes and server email notifications.
2021-11-30 18:06:24 +00:00
viveksantayana 4a201f3f9d Finished client result API.
Need to work on adjustment user codes and server email notifications.
2021-11-30 18:06:24 +00:00
viveksantayana ee71044421 Finished client result API.
Need to work on adjustment user codes and server email notifications.
2021-11-30 18:06:24 +00:00
viveksantayana a57f5476c0 Finished client result API.
Need to work on adjustment user codes and server email notifications.
2021-11-30 18:06:24 +00:00
viveksantayana 71b39d467d Finished client result API.
Need to work on adjustment user codes and server email notifications.
2021-11-30 18:06:24 +00:00
viveksantayana 240bcc6dd4 Finished client result API.
Need to work on adjustment user codes and server email notifications.
2021-11-30 18:06:24 +00:00
viveksantayana 2fc8523bfe Finished client result API.
Need to work on adjustment user codes and server email notifications.
2021-11-30 18:06:24 +00:00
viveksantayana add2001ba3 Finished client result API.
Need to work on adjustment user codes and server email notifications.
2021-11-30 18:06:24 +00:00
viveksantayana 70f362015c Built client interface 2021-11-30 03:11:28 +00:00
viveksantayana 8ad71d3a06 Built client interface 2021-11-30 03:11:28 +00:00
viveksantayana 459c630db7 Built client interface 2021-11-30 03:11:28 +00:00
viveksantayana dc0e3bf11c Built client interface 2021-11-30 03:11:28 +00:00
viveksantayana 89bb802e45 Built client interface 2021-11-30 03:11:28 +00:00
viveksantayana dd7df3080e Built client interface 2021-11-30 03:11:28 +00:00
viveksantayana 475fdfcca7 Built client interface 2021-11-30 03:11:28 +00:00
viveksantayana db755334d0 Built client interface 2021-11-30 03:11:28 +00:00
viveksantayana 42f9cd9ea8 Built client interface 2021-11-30 03:11:28 +00:00
viveksantayana 1980363c12 Built client interface 2021-11-30 03:11:28 +00:00
viveksantayana 2bd07bf598 Built client interface 2021-11-30 03:11:28 +00:00
viveksantayana 07c8b62dc1 Built client interface 2021-11-30 03:11:28 +00:00
viveksantayana d20a25f261 Built client interface 2021-11-30 03:11:28 +00:00
viveksantayana 4c14c85a47 Built client interface 2021-11-30 03:11:28 +00:00
viveksantayana 40119c9e9c Added CSRF protection to all ajax requests 2021-11-29 09:13:21 +00:00
viveksantayana 5b819c5e52 Added CSRF protection to all ajax requests 2021-11-29 09:13:21 +00:00
viveksantayana 8432884479 Added CSRF protection to all ajax requests 2021-11-29 09:13:21 +00:00
viveksantayana 9e9ceab81f Added CSRF protection to all ajax requests 2021-11-29 09:13:21 +00:00
viveksantayana 82b16ec9fb Added CSRF protection to all ajax requests 2021-11-29 09:13:21 +00:00
viveksantayana 97719badef Added CSRF protection to all ajax requests 2021-11-29 09:13:21 +00:00
viveksantayana 11a0dc3a4a Added CSRF protection to all ajax requests 2021-11-29 09:13:21 +00:00
viveksantayana 2348c76ee8 Added CSRF protection to all ajax requests 2021-11-29 09:13:21 +00:00
viveksantayana 59f10c789a Added CSRF protection to all ajax requests 2021-11-29 09:13:21 +00:00
viveksantayana 6518458768 Added CSRF protection to all ajax requests 2021-11-29 09:13:21 +00:00
viveksantayana 7c325e7c9e Added CSRF protection to all ajax requests 2021-11-29 09:13:21 +00:00
viveksantayana aab5325255 Added CSRF protection to all ajax requests 2021-11-29 09:13:21 +00:00
viveksantayana 6a898bc2dc Added CSRF protection to all ajax requests 2021-11-29 09:13:21 +00:00
viveksantayana af8ea5ddc3 Added CSRF protection to all ajax requests 2021-11-29 09:13:21 +00:00
viveksantayana e730607c66 Added question generating API 2021-11-28 18:17:50 +00:00
viveksantayana ac1ad771f0 Added question generating API 2021-11-28 18:17:50 +00:00
viveksantayana 87f60e1826 Added question generating API 2021-11-28 18:17:50 +00:00
viveksantayana 6285014938 Added question generating API 2021-11-28 18:17:50 +00:00
viveksantayana 0c3199515b Added question generating API 2021-11-28 18:17:50 +00:00
viveksantayana 272cd1441c Added question generating API 2021-11-28 18:17:50 +00:00
viveksantayana 7c5e3c1e43 Added question generating API 2021-11-28 18:17:50 +00:00
viveksantayana 274eb2d214 Added question generating API 2021-11-28 18:17:50 +00:00
viveksantayana 9fa553bd4a Added question generating API 2021-11-28 18:17:50 +00:00
viveksantayana 7aa5be57cd Added question generating API 2021-11-28 18:17:50 +00:00
viveksantayana 3cedfcaeaf Added question generating API 2021-11-28 18:17:50 +00:00
viveksantayana 2e77b1a216 Added question generating API 2021-11-28 18:17:50 +00:00
viveksantayana 2cf3329131 Added question generating API 2021-11-28 18:17:50 +00:00
viveksantayana e3fdf08b2c Added question generating API 2021-11-28 18:17:50 +00:00
viveksantayana 2d1cdd5e94 Added functionality for default datasets.
Incorporated dataset selector into test creation.
2021-11-28 17:28:14 +00:00
viveksantayana e6b37ce453 Added functionality for default datasets.
Incorporated dataset selector into test creation.
2021-11-28 17:28:14 +00:00
viveksantayana af5e6172e9 Added functionality for default datasets.
Incorporated dataset selector into test creation.
2021-11-28 17:28:14 +00:00
viveksantayana fdc68079dc Added functionality for default datasets.
Incorporated dataset selector into test creation.
2021-11-28 17:28:14 +00:00
viveksantayana 88a4fc02d1 Added functionality for default datasets.
Incorporated dataset selector into test creation.
2021-11-28 17:28:14 +00:00
viveksantayana f8d97314d3 Added functionality for default datasets.
Incorporated dataset selector into test creation.
2021-11-28 17:28:14 +00:00
viveksantayana d6bc6df86b Added functionality for default datasets.
Incorporated dataset selector into test creation.
2021-11-28 17:28:14 +00:00
viveksantayana 2fce2e0c80 Added functionality for default datasets.
Incorporated dataset selector into test creation.
2021-11-28 17:28:14 +00:00
viveksantayana 69139b9ac5 Added functionality for default datasets.
Incorporated dataset selector into test creation.
2021-11-28 17:28:14 +00:00
viveksantayana bf1d53d07d Added functionality for default datasets.
Incorporated dataset selector into test creation.
2021-11-28 17:28:14 +00:00
viveksantayana 3797adfc95 Added functionality for default datasets.
Incorporated dataset selector into test creation.
2021-11-28 17:28:14 +00:00
viveksantayana 2482242f20 Added functionality for default datasets.
Incorporated dataset selector into test creation.
2021-11-28 17:28:14 +00:00
viveksantayana 6929136f90 Added functionality for default datasets.
Incorporated dataset selector into test creation.
2021-11-28 17:28:14 +00:00
viveksantayana 0d7fa41261 Added functionality for default datasets.
Incorporated dataset selector into test creation.
2021-11-28 17:28:14 +00:00
viveksantayana 2e9e15be95 Finished data upload
Refactored to move security package inside common
Moved data folder to process root.
2021-11-28 02:30:46 +00:00
viveksantayana e0eda9df49 Finished data upload
Refactored to move security package inside common
Moved data folder to process root.
2021-11-28 02:30:46 +00:00
viveksantayana 08f2585def Finished data upload
Refactored to move security package inside common
Moved data folder to process root.
2021-11-28 02:30:46 +00:00
viveksantayana 53cc25b4ce Finished data upload
Refactored to move security package inside common
Moved data folder to process root.
2021-11-28 02:30:46 +00:00
viveksantayana f8d05f2cec Finished data upload
Refactored to move security package inside common
Moved data folder to process root.
2021-11-28 02:30:46 +00:00
viveksantayana e37d287397 Finished data upload
Refactored to move security package inside common
Moved data folder to process root.
2021-11-28 02:30:46 +00:00
viveksantayana fd89626172 Finished data upload
Refactored to move security package inside common
Moved data folder to process root.
2021-11-28 02:30:46 +00:00
viveksantayana e1967bcd7e Finished data upload
Refactored to move security package inside common
Moved data folder to process root.
2021-11-28 02:30:46 +00:00
viveksantayana 6d5f8bc00c Finished data upload
Refactored to move security package inside common
Moved data folder to process root.
2021-11-28 02:30:46 +00:00
viveksantayana 79193d897e Finished data upload
Refactored to move security package inside common
Moved data folder to process root.
2021-11-28 02:30:46 +00:00
viveksantayana c7252d0f7b Finished data upload
Refactored to move security package inside common
Moved data folder to process root.
2021-11-28 02:30:46 +00:00
viveksantayana 2064ac508a Finished data upload
Refactored to move security package inside common
Moved data folder to process root.
2021-11-28 02:30:46 +00:00
viveksantayana 35b0d30739 Finished data upload
Refactored to move security package inside common
Moved data folder to process root.
2021-11-28 02:30:46 +00:00
viveksantayana 66a950f757 Finished data upload
Refactored to move security package inside common
Moved data folder to process root.
2021-11-28 02:30:46 +00:00
viveksantayana 95abec2b4b Refactor to have all models in the models package. 2021-11-25 23:21:48 +00:00
viveksantayana 0fff71b1fb Refactor to have all models in the models package. 2021-11-25 23:21:48 +00:00
viveksantayana 3bde83cf92 Refactor to have all models in the models package. 2021-11-25 23:21:48 +00:00
viveksantayana 5b2e6dda67 Refactor to have all models in the models package. 2021-11-25 23:21:48 +00:00
viveksantayana 408aa965fd Refactor to have all models in the models package. 2021-11-25 23:21:48 +00:00
viveksantayana 39e80c64fa Refactor to have all models in the models package. 2021-11-25 23:21:48 +00:00
viveksantayana 52019e61c1 Quiz registration form 2021-11-25 23:12:20 +00:00
viveksantayana c745e3c27c Quiz registration form 2021-11-25 23:12:20 +00:00
viveksantayana 9b1d8fca71 Finished delete and data table fiew for tests 2021-11-25 20:12:50 +00:00
viveksantayana 2d0bb883bd Finished delete and data table fiew for tests 2021-11-25 20:12:50 +00:00
viveksantayana d6d809a60e Working version of test tables 2021-11-25 10:34:01 +00:00
viveksantayana c0d79d1bc7 Working version of test tables 2021-11-25 10:34:01 +00:00
viveksantayana 610b6a5766 Building new test form
Added CRUD for tests
2021-11-24 17:17:56 +00:00
viveksantayana a862a0f03a Building new test form
Added CRUD for tests
2021-11-24 17:17:56 +00:00
viveksantayana f2943e4bc1 Deleted user views registration 2021-11-23 14:53:58 +00:00
viveksantayana 9f198ed133 Deleted user views registration 2021-11-23 14:53:58 +00:00
viveksantayana 43b5973dbe Finished most of admin console
Basic CRUD operations for managing registered admin users
Encrypted personal information
Still missing sections on managing tests and results
Also missing dashboards/index/category landing pages
2021-11-23 13:00:03 +00:00
viveksantayana 0002c15e94 Finished most of admin console
Basic CRUD operations for managing registered admin users
Encrypted personal information
Still missing sections on managing tests and results
Also missing dashboards/index/category landing pages
2021-11-23 13:00:03 +00:00
188 changed files with 8443 additions and 2996 deletions
+33
View File
@@ -0,0 +1,33 @@
SERVER_NAME= # URL where this will be hosted.
FLASK_DEBUG=False
TZ=Europe/London # Time Zone
## App Configuration
SECRET_KEY= # Long, secure, secret string.
DATA=./data/
DATABASE_TYPE=SQLite # SQLite or MySQL, defaults to SQLite
DATABASE_HOST= # Required if MySQL. Must match name of Docker service, or provide host if database is on an external server. Defaults to localhost.
DATABASE_PORT= # Required if MySQL. Defaults to 3306
## MySQL Database Configuration (Required if configured to MySQL Database.)
# Note that if using the Docker service, these configuration values will also be used when creating the database in the mysql container.
MYSQL_RANDOM_ROOT_PASSWORD=True
MYSQL_DATABASE= # Required if MySQL.
MYSQL_USER= # Required if MySQL
MYSQL_PASSWORD= # Required if MySQL. Create secure password string. Note '@' character cannot be used.
## Flask Mail Configuration
MAIL_SERVER=postfix # Must match name of the Docker service
MAIL_PORT=25
MAIL_USE_TLS=False
MAIL_USE_SSL=False
MAIL_USERNAME= # Username@domain, must match config values below
MAIL_PASSWORD= # Must match config value below
MAIL_DEFAULT_SENDER= # NoReply@domain or some such.
MAIL_MAX_EMAILS=25
MAIL_ASCII_ATTACHMENTS=True
# Postfix
maildomain= # Domain must match the section of username above
smtp_user= # username:password. Must match config values above.
+16
View File
@@ -145,5 +145,21 @@ dev/
out/
ref-test/testing.py
# Ignore Database
database/data/
# Ignore Encryption Keyfile
.encryption.key
# Ignore Data Dir
**/data/*
# Ignore Logs Dir
logs/*
# Ignore Certbot Dir
certbot/*
# Ignore src dir (exception for robots.txt)
src/html/*
src/html/robots.txt
+205 -12
View File
@@ -10,31 +10,224 @@ The exam client is made with accessibility in mind, and has been designed to be
## Set Up and Installation
The clien is designed to work on a server.
The app is designed to be hosted on a server.
### Pre-Requisites
Server
Docker
Docker-Compose
Git
- A Debian- or Ubuntu-based server, preferably the latest distribution.
- Docker (specifically, Docker Engine)
- Docker Compose
- Git
### Installation
#### Install all the pre-requisites
The first step is to ensure all the prerequisites are available on the server.
To set up the server, consult some of the comprehensive guides on various hosting platforms like Linode or DigitalOcean.
Here is a [good starting point on setting up a server](https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu-22-04).
To install Docker and Docker Compose, consult the respective documentation:
- [Install on Ubuntu](https://docs.docker.com/engine/install/ubuntu/) or [Install on Debian](https://docs.docker.com/engine/install/debian/)
- Docker Compose should be installed as part of that.
> At the time of writing, there has been an upgrade to Docker and Docker Compose, meaning the syntax below might be different between versions.
Check if Git is installed on your server using the `git --version` command.
If it isn't installed, install it.
This should normally come pre-packaged with your OS distribution.
But if it doesn't, look up how to for whatever OS you use.
If you are using Ubuntu or Debian, it should be as easy as using the command:
```$ sudo apt-get install git -y```
#### Preliminary Set-Up: Clone repos and Configure Values
#### Set Up Web Server
Open a terminal and navigate to the folder where you want to install this app.
I would suggest using a subfolder within your Home folder:
#### Incorporate SSL
```$ cd ~ && mkdir ska-referee-test && cd ska-referee-test```
#### Set Up Auto-Renew
That way, you will ensure you can read and write all the necessary files during installation.
Once in the destination folder, clone all the relevant files you will need for the installation:
### Alterations
```$ git clone https://git.vsnt.uk/viveksantayana/ska-referee-test.git .```
## Use
(Remember to include the trailing dot at the end, as that indicates to Git to download the files in the current directory.)
## Compatibility
#### Choose What Database Engine You Will Use
### iOS Limitations
This app is designed to use an SQLite database by default.
You can set it up to use a MySQL database by configuring the environment variables accordingly.
If your database is being hosted remotely, make sure the MySQL database has the proper authentication for the user from a remote server.
Alternatively, you can also use the second `docker-compose-mysql.yml` file which provides a MySQL database as part of the cluster.
To use the second `docker-compose-mysql.yml` file, use the following command at the last step of the installation:
```sudo docker compose -f docker-compose-mysql.yml up```
#### Populate Environment Variables
Configuration values for the app are stored in the environment variables file.
To set it up, make a copy of the example file and populate it with appropriate values.
```$ cp .env.example .env```
Make sure to use complex, secure strings for passwords.
Also make sure that the various entries for usernames and passwords match.
#### Input Specific Values for Your Installation
There are some values in the following four files you will need to configure to reflect the domain you are installing this app.
```sh
# .env
SERVER_NAME= # URL where this will be hosted.
```
```sh
# install-script.sh
domains=(example.org www.example.org)
email="" # Adding a valid address is strongly recommended
```
Substitute the domain name `domain_name` in the two file paths in the following file:
```sh
# nginx/ssl.conf
ssl_certificate /etc/letsencrypt/live/domain_name/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/domain_name/privkey.pem;
...
```
And **six** locations in the following file, two for the regular version of the domain and four for the www version (remember to keep the www. prefix where present):
```nginx
# nginx/conf.d/ref-test-app.conf
server {
server_name domain_name;
listen 80 default_server;
...
}
server {
server_name domain_name;
listen 443 ssl http2 default_server;
...
}
server {
server_name www.domain_name;
listen 80;
listen [::]:80;
# Redirect to non-www
return 301 $scheme://domain_name$request_uri;
...
}
server {
server_name www.domain_name;
listen 443 ssl http2;
listen [::]:443 ssl http2;
...
# Redirect to non-www
return 301 $scheme://domain_name$request_uri;
}
```
#### Installing SSL Certificates
The app will use SSL certificates to operate through a secure, `https` connection.
This will be set up automatically.
However, there is a specific chicken-and-egg problem as the web server, Nginx, won't run without certificates, Certbot, the certificate generator, won't run without the web server.
So to solve this, there is an automation script we can run that will set up a dummy certificate and then issue the appropriate certificates for us.
```sh
chmod +x install-script.sh
sudo ./install-script.sh
```
This will take a long time to run the first time because it will try and generate a fairly sizeable cypher.
When we later run the server, Certbot will check for renewals of the SSL certificates every 12 hours, and Nginx will reload the configurations every 6 hours, to make sure everything runs smoothly and stays live.
#### Run the Stack
Everything should be good to run on autopilot at this point.
Navigate to the root folder of the app, the folder where you have `install-script.sh` and `docker-compose.yml`.
Run the following command:
```sudo docker compose up -d```
And you should have the stack running.
You can register in the app and begin using it.
### Fonts
The app uses [OpenDyslexic](https://opendyslexic.org/), which is available on-line.
It also has the option of rendering in other system fonts, but this can vary depending on your operating system.
Because these are proprietary fonts, they cannot be made available on-line in the same way as open source ones should your system not have them.
Some fonts may not display correctly as a result.
## Updating the Installation
If the app is updated, you can update the version on your installation using the following method:
### Navigate to the root folder
This will be the root folder into which you cloned the git repository when you set the app up.
### Stash your local changes
When you update the code, there is a risk the changes you made to your configuration will be overwritten.
To avoid this, use the following command:
```git stash```
This will stash the changes you made, and we can re-apply the changes once the new code has been downloaded.
If you do not have any other changes stashed, the index number of these changes should be `0` in a later step.
If there are other changes, make sure to note what the correct index number for the stashed changes is.
### Take down the Docker containers
We will need to stop the current containers with the following command:
```sudo docker compose down```
This may take a few seconds.
### Pull the updated code
Download the updated code from the Git repository:
```git pull```
This step might fail if you have any un-stashed local changed.
### Re-Apply your local configurations
Because we stashed our local configurations, we can re-apply them once again:
```git stash pop 0```
The index number (`0`) is assuming there were no other changes saved on your git repository.
If you have a different index number for the relevant changes from the above step, change this accordingly.
### Re-build the docker image
Now that we have the base code downloaded, we just need to update the docker image:
```sudo docker compose build app```
### Re-build the containers
This is the same last step as running the containers in the last step of the installation:
```sudo docker compose up -d```
+2 -16
View File
@@ -6,11 +6,6 @@
- [Docker Compose Reference](https://docs.docker.com/compose/compose-file/compose-file-v3/)
### MongoDB/PyMongo
- [MongoDB Shell Commands](https://docs.mongodb.com/manual/reference/)
- [PyMongo Driver](https://pymongo.readthedocs.io/en/stable/)
## Source Code
- [MongoDB Docker Image entrypoint shell script](https://github.com/docker-library/mongo/blob/master/5.0/docker-entrypoint.sh) (Context: Tried to replicate the command to create a new user in the original entrypoint script in the custom initialisation script in this app.)
@@ -23,15 +18,6 @@
- [Tables](https://www.blog.pythonlibrary.org/2017/12/14/flask-101-adding-editing-and-displaying-data/)
- [Tables, but interactive](https://blog.miguelgrinberg.com/post/beautiful-interactive-tables-for-your-flask-templates)
## Stack Exchange/Overflow
### MongoDB
- [Creating MongoDB Database on Container Start](https://stackoverflow.com/questions/42912755/how-to-create-a-db-for-mongodb-container-on-start-up)
- [Passing Environment Variables to Docker Container Entrypoint](https://stackoverflow.com/questions/64606674/how-can-i-pass-environment-variables-to-mongo-docker-entrypoint-initdb-d)
- [Integrating Flask-Login with MongoDB](https://stackoverflow.com/questions/54992412/flask-login-usermixin-class-with-a-mongodb) (**This does not work with the app as is, and is possibly something that needs more research and development in the future**)
- [Setting up a Postfix email notification system](https://medium.com/@vietgoeswest/a-simple-outbound-email-service-for-your-app-in-15-minutes-cc4da70a2af7)
## YouTube Tutorials
### General Flask Introduction
@@ -72,7 +58,7 @@ A much simpler and more rudimentary introduction to Flask and MongoDB.
- [Build a User Login System with `flask-login`, `flask-wtforms`, `flask-bootstrap`, and `flask-sqlalchemy`](https://www.youtube.com/watch?v=8aTnmsDMldY)
A much more robust method that uses the various Flask modules to make a more powerful framework.
Uses SQL rather than MongoDB.
Uses SQL.
### Flask techniques
@@ -80,4 +66,4 @@ Uses SQL rather than MongoDB.
### Flask handling file uploads
- [Handlin File Uploads](https://blog.miguelgrinberg.com/post/handling-file-uploads-with-flask)
- [Handling File Uploads](https://blog.miguelgrinberg.com/post/handling-file-uploads-with-flask)
-2
View File
@@ -1,2 +0,0 @@
*
!.gitignore
-14
View File
@@ -1,14 +0,0 @@
set -e
mongo=( mongo --host 127.0.0.1 --port 27017 --quiet )
if [ "$MONGO_INITDB_ROOT_USERNAME" ] && [ "$MONGO_INITDB_ROOT_PASSWORD" ] && [ "$MONGO_INITDB_USERNAME" ] && [ "$MONGO_INITDB_PASSWORD" ]; then
rootAuthDatabase='admin'
"${mongo[@]}" "$rootAuthDatabase" <<-EOJS
db.createUser({
user: $(_js_escape "$MONGO_INITDB_USERNAME"),
pwd: $(_js_escape "$MONGO_INITDB_PASSWORD"),
roles: [ { role: 'readWrite', db: $(_js_escape "$MONGO_INITDB_DATABASE") } ]
})
EOJS
fi
+90
View File
@@ -0,0 +1,90 @@
version: '3.9'
volumes:
app:
mysql:
services:
nginx:
container_name: reftest_server
image: nginx:alpine
volumes:
- ./certbot:/etc/letsencrypt:ro
- ./nginx:/etc/nginx
- ./src/html/certbot:/usr/share/nginx/html/certbot:ro
- ./src/html/robots.txt:/usr/share/nginx/html/robots.txt:ro
- ./ref-test/app/root:/usr/share/nginx/html/root:ro
- ./ref-test/app/admin/static:/usr/share/nginx/html/admin/static:ro
- ./ref-test/app/editor/static:/usr/share/nginx/html/editor/static:ro
- ./ref-test/app/quiz/static:/usr/share/nginx/html/quiz/static:ro
- ./ref-test/app/view/static:/usr/share/nginx/html/view/static:ro
ports:
- 80:80
- 443:443
restart: unless-stopped
networks:
- frontend
depends_on:
- app
command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
app:
container_name: reftest_app
image: reftest
build: ./ref-test
env_file:
- ./.env
ports:
- 5000
volumes:
- app:/ref-test/data
- ./logs:/ref-test/data/logs
restart: unless-stopped
networks:
- frontend
- backend
depends_on:
postfix:
mysql:
condition: service_healthy
postfix:
container_name: reftest_postfix
image: catatnight/postfix:latest
restart: unless-stopped
env_file:
- ./.env
ports:
- 25
networks:
- backend
certbot:
container_name: reftest_certbot
image: certbot/certbot
volumes:
- ./certbot:/etc/letsencrypt
- ./src/html/certbot:/var/www/html
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
mysql:
container_name: reftest_db
image: mysql:8.0
env_file:
- ./.env
volumes:
- mysql:/var/lib/mysql
ports:
- 3306
networks:
- backend
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
timeout: 10s
retries: 10
networks:
frontend:
external: false
backend:
external: false
+29 -36
View File
@@ -1,15 +1,22 @@
version: '3.9'
volumes:
app:
services:
ref_test_server:
container_name: ref_test_server
image: nginx:1.21.4-alpine
nginx:
container_name: reftest_server
image: nginx:alpine
volumes:
- ./certbot:/etc/letsencrypt:ro
- ./nginx:/etc/nginx
- ./src/html:/usr/share/nginx/html/
- ./ref-test/admin/static:/usr/share/nginx/html/admin/static
- ./ref-test/quiz/static:/usr/share/nginx/html/quiz/static
- ./src/html/certbot:/usr/share/nginx/html/certbot:ro
- ./src/html/robots.txt:/usr/share/nginx/html/robots.txt:ro
- ./ref-test/app/root:/usr/share/nginx/html/root:ro
- ./ref-test/app/admin/static:/usr/share/nginx/html/admin/static:ro
- ./ref-test/app/editor/static:/usr/share/nginx/html/editor/static:ro
- ./ref-test/app/quiz/static:/usr/share/nginx/html/quiz/static:ro
- ./ref-test/app/view/static:/usr/share/nginx/html/view/static:ro
ports:
- 80:80
- 443:443
@@ -17,41 +24,29 @@ services:
networks:
- frontend
depends_on:
- ref_test_app
- app
command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
ref_test_app:
container_name: ref_test_app
app:
container_name: reftest_app
image: reftest
build: ./ref-test
volumes:
- ./ref-test:/ref-test
env_file:
- ./.env
ports:
- 5000
volumes:
- app:/ref-test/data
- ./logs:/ref-test/data/logs
restart: unless-stopped
networks:
- frontend
- backend
depends_on:
- ref_test_db
- ref_test_postfix
- postfix
ref_test_db:
container_name: ref_test_db
image: mongo:5.0.4-focal
restart: unless-stopped
volumes:
- ./database/data:/data
- ./database/initdb.d/:/docker-entrypoint-initdb.d/
env_file:
- ./.env
ports:
- 27017
networks:
- backend
ref_test_postfix:
container_name: ref_test_postfix
postfix:
container_name: reftest_postfix
image: catatnight/postfix:latest
restart: unless-stopped
env_file:
@@ -61,15 +56,13 @@ services:
networks:
- backend
ref_test_certbot:
container_name: ref_test_certbot
image: certbot/certbot:v1.21.0
certbot:
container_name: reftest_certbot
image: certbot/certbot
volumes:
- ./certbot:/etc/letsencrypt
- ./src/html:/var/www/html
depends_on:
- ref_test_server
# command: certonly --webroot --webroot-path=/var/www/html --email vsdomainmanager@gmail.com --agree-tos --no-eff-email -d reftest.vsnt.uk
- ./src/html/certbot:/var/www/html
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
networks:
frontend:
+90
View File
@@ -0,0 +1,90 @@
#!/bin/bash
# Source https://pentacent.medium.com/nginx-and-lets-encrypt-with-docker-in-less-than-5-minutes-b4b8a60d3a71
if ! [ -x "$(command -v docker)" ]; then
echo 'Error: docker is not installed.' >&2
exit 1
fi
if ! [ -x "$(command -v compose)" ]; then
echo 'Error: docker compose is not installed.' >&2
exit 1
fi
domains=(example.org www.example.org)
rsa_key_size=4096
data_path="./certbot"
email="" # Adding a valid address is strongly recommended
staging=0 # Set to 1 if you're testing your setup to avoid hitting request limits
if [ -d "$data_path" ]; then
read -p "Existing data found for $domains. Continue and replace existing certificate? (y/N) " decision
if [ "$decision" != "Y" ] && [ "$decision" != "y" ]; then
exit
fi
fi
if [ ! -e "$data_path/ssl-dhparams.pem" ]; then
echo "### Generating ssl-dhparams.pem ..."
docker compose run --rm --entrypoint "\
openssl dhparam -out /etc/letsencrypt/ssl-dhparams.pem 4096" certbot
echo
fi
echo "### Creating dummy certificate for $domains ..."
path="/etc/letsencrypt/live/$domains"
mkdir -p "$data_path/live/$domains"
docker compose run --rm --entrypoint "\
openssl req -x509 -nodes -newkey rsa:$rsa_key_size -days 1\
-keyout '$path/privkey.pem' \
-out '$path/fullchain.pem' \
-subj '/CN=localhost'" certbot
echo
if [ ! -e "$data_path/lets-encrypt-x3-cross-signed.pem" ]; then
echo "### Downloading lets-encrypt-x3-cross-signed.pem ..."
wget -O $data_path/lets-encrypt-x3-cross-signed.pem \
"https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem"
echo
fi
echo "### Starting nginx ..."
docker compose up --force-recreate -d nginx
echo
echo "### Deleting dummy certificate for $domains ..."
docker compose run --rm --entrypoint "\
rm -Rf /etc/letsencrypt/live/$domains && \
rm -Rf /etc/letsencrypt/archive/$domains && \
rm -Rf /etc/letsencrypt/renewal/$domains.conf" certbot
echo
echo "### Requesting Let's Encrypt certificate for $domains ..."
#Join $domains to -d args
domain_args=""
for domain in "${domains[@]}"; do
domain_args="$domain_args -d $domain"
done
# Select appropriate email arg
case "$email" in
"") email_arg="--register-unsafely-without-email" ;;
*) email_arg="--email $email" ;;
esac
# Enable staging mode if needed
if [ $staging != "0" ]; then staging_arg="--staging"; fi
docker compose run --rm --entrypoint "\
certbot certonly --non-interactive --webroot -w /var/www/html \
$staging_arg \
$email_arg \
$domain_args \
--rsa-key-size $rsa_key_size \
--agree-tos \
--force-renewal" certbot
echo
echo "### Reloading nginx ..."
docker compose exec nginx nginx -s reload
+1 -1
View File
@@ -1,6 +1,6 @@
# Certbot Renewal
location ^~ /.well-known/acme-challenge/ {
root /usr/share/nginx/html;
root /usr/share/nginx/html/certbot;
allow all;
default_type "text/plain";
}
-33
View File
@@ -1,33 +0,0 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
access_log /var/log/nginx/host.access.log main;
# SSL configuration
include /etc/nginx/ssl.conf;
# Add index.php to the list if you are using PHP
index index.html index.htm index.nginx-debian.html;
# Default catch all to 404
# Added from Serverfault support https://serverfault.com/questions/994141/nginx-redirecting-the-wrong-subdomains
server_name _;
server_name_in_redirect off;
location / {
return 404;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
+74
View File
@@ -0,0 +1,74 @@
upstream reftest {
server app:5000;
}
server {
server_name domain_name;
listen 80 default_server;
listen [::]:80 default_server;
# Redirect to ssl
return 301 https://$host$request_uri;
}
server {
server_name domain_name;
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
# SSL configuration
include /etc/nginx/ssl.conf;
include /etc/nginx/certbot-challenge.conf;
# Define locations for static files to be served by Nginx
location ^~ /root/ {
include /etc/nginx/mime.types;
alias /usr/share/nginx/html/root/;
}
location ^~ /quiz/static/ {
include /etc/nginx/mime.types;
alias /usr/share/nginx/html/quiz/static/;
}
location ^~ /admin/static/ {
include /etc/nginx/mime.types;
alias /usr/share/nginx/html/admin/static/;
}
location ^~ /admin/editor/static/ {
include /etc/nginx/mime.types;
alias /usr/share/nginx/html/editor/static/;
}
location ^~ /admin/view/static/ {
include /etc/nginx/mime.types;
alias /usr/share/nginx/html/view/static/;
}
# Proxy to the main app for all other requests
location / {
include /etc/nginx/conf.d/proxy_headers.conf;
proxy_pass http://reftest;
}
}
server {
server_name www.domain_name;
listen 80;
listen [::]:80;
# Redirect to non-www
return 301 $scheme://domain_name$request_uri;
}
server {
server_name www.domain_name;
listen 443 ssl http2;
listen [::]:443 ssl http2;
# SSL configuration
include /etc/nginx/ssl.conf;
include /etc/nginx/certbot-challenge.conf;
# Redirect to non-www
return 301 $scheme://domain_name$request_uri;
}
+13
View File
@@ -0,0 +1,13 @@
ssl_certificate /etc/letsencrypt/live/domain_name/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/domain_name/privkey.pem;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
ssl_prefer_server_ciphers on;
ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/lets-encrypt-x3-cross-signed.pem;
add_header Strict-Transport-Security "max-age=31536000" always;
ssl_session_cache shared:SSL:40m;
ssl_session_timeout 4h;
ssl_session_tickets on;
+1
View File
@@ -1,2 +1,3 @@
env/
__pycache__/
data/
+4 -1
View File
@@ -1,5 +1,8 @@
FROM python:3.10-slim
FROM python:3.13-slim
ARG DATA=./data/
ENV DATA=$DATA
WORKDIR /ref-test
COPY . .
RUN pip install --upgrade pip && pip install -r requirements.txt
RUN chmod +x install.py reset.py && ./install.py
CMD [ "gunicorn", "-b", "0.0.0.0:5000", "-w", "5", "wsgi:app" ]
-147
View File
@@ -1,147 +0,0 @@
from flask import Blueprint, render_template, request, session, redirect
from flask.helpers import flash, url_for
from flask.json import jsonify
from .models.users import User
from uuid import uuid4
from common.security.database import decrypt_find_one, encrypted_update
from werkzeug.security import check_password_hash
from .views import admin_account_required, disable_on_registration, login_required, disable_if_logged_in, get_id_from_cookie
auth = Blueprint(
'admin_auth',
__name__,
template_folder='templates',
static_folder='static'
)
@auth.route('/account/', methods = ['GET', 'POST'])
@admin_account_required
@login_required
def account():
from .models.forms import UpdateAccountForm
from main import db
form = UpdateAccountForm()
_id = get_id_from_cookie()
user = decrypt_find_one(db.users, {'_id': _id})
if request.method == 'GET':
return render_template('/admin/auth/account.html', form = form, user = user)
if request.method == 'POST':
if form.validate_on_submit():
password_confirm = request.form.get('password_confirm')
if not check_password_hash(user['password'], password_confirm):
return jsonify({ 'error': 'The password you entered is incorrect.' }), 401
entry = User(
_id = _id,
password = request.form.get('password'),
email = request.form.get('email')
)
return entry.update()
else:
errors = [*form.password_confirm.errors, *form.password_reenter.errors, *form.password.errors, *form.email.errors]
return jsonify({ 'error': errors}), 400
@auth.route('/login/', methods=['GET','POST'])
@admin_account_required
@disable_if_logged_in
def login():
from .models.forms import LoginForm
form = LoginForm()
if request.method == 'GET':
return render_template('/admin/auth/login.html', form=form)
if request.method == 'POST':
if form.validate_on_submit():
entry = User(
username = request.form.get('username').lower(),
password = request.form.get('password'),
remember = request.form.get('remember')
)
return entry.login()
else:
errors = [*form.username.errors, *form.password.errors]
return jsonify({ 'error': errors}), 400
@auth.route('/logout/')
@admin_account_required
@login_required
def logout():
_id = get_id_from_cookie()
return User(_id=_id).logout()
@auth.route('/register/', methods=['GET','POST'])
@disable_on_registration
def register():
from .models.forms import RegistrationForm
form = RegistrationForm()
if request.method == 'GET':
return render_template('/admin/auth/register.html', form=form)
if request.method == 'POST':
if form.validate_on_submit():
entry = User(
_id = uuid4().hex,
username = request.form.get('username').lower(),
email = request.form.get('email'),
password = request.form.get('password'),
)
return entry.register()
else:
errors = [*form.username.errors, *form.email.errors, *form.password.errors, *form.password_reenter.errors]
return jsonify({ 'error': errors}), 400
@auth.route('/reset/', methods = ['GET', 'POST'])
@admin_account_required
@disable_if_logged_in
def reset():
from .models.forms import ResetPasswordForm
form = ResetPasswordForm()
if request.method == 'GET':
return render_template('/admin/auth/reset.html', form=form)
if request.method == 'POST':
if form.validate_on_submit():
entry = User(
username = request.form.get('username').lower(),
email = request.form.get('email'),
)
return entry.reset_password()
else:
errors = [*form.username.errors, *form.email.errors]
return jsonify({ 'error': errors}), 400
@auth.route('/reset/<token1>/<token2>/', methods = ['GET'])
@admin_account_required
@disable_if_logged_in
def reset_gateway(token1,token2):
from main import db
user = decrypt_find_one( db.users, {'reset_token' : token1} )
if not user:
return redirect(url_for('admin_auth.login'))
encrypted_update( db.users, {'reset_token': token1}, {'$unset': {'reset_token' : '', 'verification_token': ''}})
if not user['verification_token'] == token2:
flash('The verification of your password reset request failed and the token has been invalidated. Please make a new reset password request.', 'error'), 401
return redirect(url_for('admin_auth.reset'))
session['_id'] = user['_id']
session['reset_validated'] = True
return redirect(url_for('admin_auth.update_password_'))
@auth.route('/reset/update/', methods = ['GET','POST'])
@admin_account_required
@disable_if_logged_in
def update_password_():
from .models.forms import UpdatePasswordForm
form = UpdatePasswordForm()
if request.method == 'GET':
if 'reset_validated' not in session:
return redirect(url_for('admin_auth.login'))
session.pop('reset_validated')
return render_template('/admin/auth/update-password.html', form=form)
if request.method == 'POST':
if form.validate_on_submit():
entry = User(
_id = session['_id'],
password = request.form.get('password')
)
session.pop('_id')
return entry.update()
else:
errors = [*form.password.errors, *form.password_reenter.errors]
return jsonify({ 'error': errors}), 400
-11
View File
@@ -1,11 +0,0 @@
from wtforms.validators import ValidationError
def value(min=0, max=None):
message = f'Value must be between {min} and {max}.'
def _length(form, field):
value = field.data or 0
if value < min or max != None and value > max:
raise ValidationError(message)
return _length
-122
View File
@@ -1,122 +0,0 @@
import secrets
from datetime import datetime
from uuid import uuid4
from flask import flash, jsonify
import secrets
import os
from json import dump, loads
from common.security import encrypt
class Test:
def __init__(self, _id=None, start_date=None, expiry_date=None, time_limit=None, creator=None, dataset=None):
self._id = _id
self.start_date = start_date
self.expiry_date = expiry_date
self.time_limit = None if time_limit == 'none' or time_limit == '' or time_limit == None else int(time_limit)
self.creator = creator
self.dataset = dataset
def create(self):
from main import app, db
test = {
'_id': self._id,
'date_created': datetime.today(),
'start_date': self.start_date,
'expiry_date': self.expiry_date,
'time_limit': self.time_limit,
'creator': encrypt(self.creator),
'test_code': secrets.token_hex(6).upper(),
'dataset': self.dataset
}
if db.tests.insert_one(test):
dataset_file_path = os.path.join(app.config["DATA_FILE_DIRECTORY"],self.dataset)
with open(dataset_file_path, 'r') as dataset_file:
data = loads(dataset_file.read())
data['meta']['tests'].append(self._id)
with open(dataset_file_path, 'w') as dataset_file:
dump(data, dataset_file, indent=2)
flash(f'Created a new exam with Exam Code <strong>{self.render_test_code(test["test_code"])}</strong>.', 'success')
return jsonify({'success': test}), 200
return jsonify({'error': f'Could not create exam. An error occurred.'}), 400
def add_time_adjustment(self, time_adjustment):
from main import db
user_code = secrets.token_hex(3).upper()
adjustment = {
user_code: time_adjustment
}
if db.tests.find_one_and_update({'_id': self._id}, {'$set': {'time_adjustments': adjustment}},upsert=False):
flash(f'Time adjustment for {time_adjustment} minutes has been added. This can be enabled using the user code {user_code}.')
return jsonify({'success': adjustment})
return jsonify({'error': 'Failed to add the time adjustment. An error occurred.'}), 400
def remove_time_adjustment(self, user_code):
from main import db
if db.tests.find_one_and_update({'_id': self._id}, {'$unset': {f'time_adjustments.{user_code}': {}}}):
message = 'Time adjustment has been deleted.'
flash(message, 'success')
return jsonify({'success': message})
return jsonify({'error': 'Failed to delete the time adjustment. An error occurred.'}), 400
def render_test_code(self, test_code):
return ''.join([test_code[:4], test_code[4:8], test_code[8:]])
def parse_test_code(self, test_code):
return test_code.replace('', '')
def delete(self):
from main import app, db
test = db.tests.find_one({'_id': self._id})
if 'entries' in test:
if test['entries']:
return jsonify({'error': 'Cannot delete an exam that has entries submitted to it.'}), 400
if self.dataset is None:
self.dataset = db.tests.find_one({'_id': self._id})['dataset']
if db.tests.delete_one({'_id': self._id}):
dataset_file_path = os.path.join(app.config["DATA_FILE_DIRECTORY"],self.dataset)
with open(dataset_file_path, 'r') as dataset_file:
data = loads(dataset_file.read())
data['meta']['tests'].remove(self._id)
with open(dataset_file_path, 'w') as dataset_file:
dump(data, dataset_file, indent=2)
message = 'Deleted exam.'
flash(message, 'alert')
return jsonify({'success': message}), 200
return jsonify({'error': f'Could not create exam. An error occurred.'}), 400
def update(self):
from main import db
test = {}
updated = []
if not self.start_date == '' and self.start_date is not None:
test['start_date'] = self.start_date
updated.append('start date')
if not self.expiry_date == '' and self.expiry_date is not None:
test['expiry_date'] = self.expiry_date
updated.append('expiry date')
if not self.time_limit == '' and self.time_limit is not None:
test['time_limit'] = int(self.time_limit)
updated.append('time limit')
output = ''
if len(updated) == 0:
flash(f'There were no changes requested for your account.', 'alert'), 200
return jsonify({'success': 'There were no changes requested for your account.'}), 200
elif len(updated) == 1:
output = updated[0]
elif len(updated) == 2:
output = ' and '.join(updated)
elif len(updated) > 2:
output = updated[0]
for index in range(1,len(updated)):
if index < len(updated) - 2:
output = ', '.join([output, updated[index]])
elif index == len(updated) - 2:
output = ', and '.join([output, updated[index]])
else:
output = ''.join([output, updated[index]])
db.tests.find_one_and_update({'_id': self._id}, {'$set': test})
_output = f'The {output} of the test {"has" if len(updated) == 1 else "have"} been updated.'
flash(_output)
return jsonify({'success': _output}), 200
-205
View File
@@ -1,205 +0,0 @@
from flask import flash, make_response, Response, session
from flask.helpers import url_for
from flask.json import jsonify
from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.utils import redirect
from flask_mail import Message
import secrets
from common.security import encrypt, decrypt
from common.security.database import decrypt_find_one, encrypted_update
from datetime import datetime, timedelta
class User:
def __init__(self, _id=None, username=None, password=None, email=None, remember=False):
self._id = _id
self.username = username
self.email = email
self.password = password
self.remember = remember
def start_session(self, resp:Response):
resp.set_cookie(
key = '_id',
value = self._id,
max_age = timedelta(days=14) if self.remember else 'Session',
path = '/',
expires = datetime.utcnow() + timedelta(days=14) if self.remember else 'Session',
domain = '.reftest.vsnt.uk',
secure = True
)
if self.remember:
resp.set_cookie (
key = 'remember',
value = 'True',
max_age = timedelta(days=14),
path = '/',
expires = datetime.utcnow() + timedelta(days=14),
domain = '.reftest.vsnt.uk',
secure = True
)
def register(self):
from main import db
from ..views import get_id_from_cookie
user = {
'_id': self._id,
'email': encrypt(self.email),
'password': generate_password_hash(self.password, method='sha256'),
'username': encrypt(self.username)
}
if decrypt_find_one(db.users, { 'username': self.username }):
return jsonify({ 'error': f'Username {self.username} is not available.' }), 400
if db.users.insert_one(user):
flash(f'User {self.username} has been created successfully. You may now use it to log in and administer the tests.', 'success')
resp = make_response(jsonify(user), 200)
if not get_id_from_cookie:
self.start_session(resp)
return resp
return jsonify({ 'error': f'Registration failed. An error occurred.' }), 400
def login(self):
from main import db
user = decrypt_find_one( db.users, { 'username': self.username })
if not user:
return jsonify({ 'error': f'Username {self.username} does not exist.' }), 401
if not check_password_hash( user['password'], self.password ):
return jsonify({ 'error': f'The password you entered is incorrect.' }), 401
response = {
'success': f'Successfully logged in user {self.username}.'
}
if 'prev_page' in session:
response['redirect_to'] = session['prev_page']
session.pop('prev_page')
resp = make_response(jsonify(response), 200)
self._id = user['_id']
self.start_session(resp)
return resp
def logout(self):
resp = make_response(redirect(url_for('admin_auth.login')))
resp.set_cookie(
key = '_id',
value = '',
max_age = timedelta(days=-1),
path = '/',
expires= datetime.utcnow() + timedelta(days=-1),
domain = '.reftest.vsnt.uk',
secure = True
)
resp.set_cookie (
key = 'cookie_consent',
value = 'True',
max_age = 'Session',
path = '/',
expires = 'Session',
domain = '.reftest.vsnt.uk',
secure = True
)
resp.set_cookie (
key = 'remember',
value = 'True',
max_age = timedelta(days=-1),
path = '/',
expires = datetime.utcnow() + timedelta(days=-1),
domain = '.reftest.vsnt.uk',
secure = True
)
flash('You have been logged out. All cookies pertaining to your account have been deleted. Have a nice day.', 'alert')
return resp
def reset_password(self):
from main import db, mail
user = decrypt_find_one(db.users, { 'username': self.username })
if not user:
return jsonify({ 'error': f'Username {self.username} does not exist.' }), 401
if not user['email'] == self.email:
return jsonify({ 'error': f'The email address {self.email} does not match the user account {self.username}.' }), 401
new_password = secrets.token_hex(12)
reset_token = secrets.token_urlsafe(16)
verification_token = secrets.token_urlsafe(16)
user['password'] = generate_password_hash(new_password, method='sha256')
if encrypted_update(db.users, { 'username': self.username }, { '$set': {'password': user['password'], 'reset_token': reset_token, 'verification_token': verification_token } } ):
flash(f'Your password has been reset. Instructions to recover your account have been sent to {self.email}. Please be sure to check your spam folder in case you have not received the email.', 'alert')
email = Message(
subject = 'RefTest | Password Reset',
recipients = [self.email],
body = f"""
Hello {user['username']}, \n\n
This email was generated because we received a request to reset the password for your administrator account for the SKA RefTest app.\n\n
If you did not make this request, please ignore this email.\n\n
If you did make this request, then you have two options to recover your account.\n\n
For the time being, your password has been reset to the following:\n\n
{new_password}\n\n
You may use this to log back in to your account, and subsequently change your password to something more suitable.\n\n
Alternatively, you may visit the following private link using your unique token to override your password. Copy and paste the following link in a web browser. Please note that this token is only valid once:\n\n
{url_for('admin_auth.reset_gateway', token1 = reset_token, token2 = verification_token, _external = True)}\n\n
Have a nice day.
""",
html = f"""
<p>Hello {user['username']},</p>
<p>This email was generated because we received a request to reset the password for your administrator account for the SKA RefTest app. </p>
<p>If you did not make this request, please ignore this email.</p>
<p>If you did make this request, then you have two options to recover your account.</p>
<p>For the time being, your password has been reset to the following:</p>
<strong>{new_password}</strong>
<p>You may use this to log back in to your account, and subsequently change your password to something more suitable.</p>
<p>Alternatively, you may visit the following private link using your unique token to override your password. Click on the following link, or copy and paste it into a browser. <strong>Please note that this token is only valid once</strong>:</p>
<p><a href='{url_for('admin_auth.reset_gateway', token1 = reset_token, token2 = verification_token, _external = True)}'>{url_for('admin_auth.reset_gateway', token1 = reset_token, token2 = verification_token, _external = True)}</a></p>
<p>Have a nice day.</p>
"""
)
mail.send(email)
return jsonify({ 'success': 'Password reset request has been processed.'}), 200
def update(self):
from main import db
from ..views import get_id_from_cookie
retrieved_user = decrypt_find_one(db.users, { '_id': self._id })
if not retrieved_user:
return jsonify({ 'error': f'User {retrieved_user["username"]} does not exist.' }), 401
user = {}
updated = []
if not self.email == '' and self.email is not None:
user['email'] = self.email
updated.append('email')
if not self.password == '' and self.password is not None:
user['password'] = generate_password_hash(self.password, method='sha256')
updated.append('password')
output = ''
if len(updated) == 0:
flash(f'There were no changes requested for your account.', 'alert'), 200
return jsonify({'success': 'There were no changes requested for your account.'}), 200
elif len(updated) == 1:
output = updated[0]
elif len(updated) == 2:
output = ' and '.join(updated)
elif len(updated) > 2:
output = updated[0]
for index in range(1,len(updated)):
if index < len(updated) - 2:
output = ', '.join([output, updated[index]])
elif index == len(updated) - 2:
output = ', and '.join([output, updated[index]])
else:
output = ''.join([output, updated[index]])
encrypted_update(db.users, {'_id': self._id}, { '$set': user })
if self._id == get_id_from_cookie():
_output = 'Your '
elif retrieved_user['username'][-1] == 's':
_output = '&rsquo;'.join([retrieved_user['username'], ''])
else:
_output = '&rsquo;'.join([retrieved_user['username'], 's'])
_output = f'{_output} {output} {"has" if len(updated) == 1 else "have"} been updated.'
flash(_output)
return jsonify({'success': _output}), 200
def delete(self):
from main import db
retrieved_user = decrypt_find_one(db.users, { '_id': self._id })
if not retrieved_user:
return jsonify({ 'error': f'User does not exist.' }), 401
db.users.find_one_and_delete({'_id': self._id})
flash(f'User {retrieved_user["username"]} has been deleted.')
return jsonify({'success': f'User {retrieved_user["username"]} has been deleted.'}), 200
-15
View File
@@ -1,15 +0,0 @@
from flask import Blueprint, render_template
from .views import login_required, admin_account_required
results = Blueprint(
'results',
__name__,
template_folder='templates',
static_folder='static'
)
@results.route('/')
@admin_account_required
@login_required
def _results():
return render_template('/admin/results.html')
@@ -1,2 +0,0 @@
<p>This web app was developed by Vivek Santayana. The source code for the web app, excluding any data pertaining to the questions in the quiz, is freely available at <a href="https://git.vsnt.uk/viveksantayana/ska-referee-test">Vivek&rsquo;s personal GIT repository</a> under an MIT License.</p>
<p>All questions in the test are &copy; The Scottish Korfball Association {{ now.year }}. All rights are reserved.</p>
@@ -1,79 +0,0 @@
<nav class="navbar fixed-top navbar-expand-md navbar-dark bg-dark">
<div class="container">
<a href="{{ url_for('admin_views.home') }}" class="navbar-brand mb-0 h1">RefTest (Beta) | Admin</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbar"
aria-controls="navbar"
aria-expanded="false"
aria-label="Toggle Navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse justify-content-end" id="navbar">
<ul class="navbar-nav">
{% if not check_login() %}
<li class="nav-item" id="nav-login">
<a href="{{ url_for('admin_auth.login') }}" id="link-login" class="nav-link">Log In</a>
</li>
{% endif %}
{% if check_login() %}
<li class="nav-item" id="nav-results">
<a href="{{ url_for('results._results') }}" id="link-results" class="nav-link">View Results</a>
</li>
<li class="nav-item" id="nav-tests">
<a href="{{ url_for('admin_views.tests') }}" id="link-tests" class="nav-link">Manage Exams</a>
</li>
<li class="nav-item dropdown" id="nav-settings">
<a
class="nav-link dropdown-toggle"
id="dropdown-account"
role="button"
href="{{ url_for('admin_views.settings') }}"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Settings
</a>
<ul
class="dropdown-menu"
aria-labelledby="dropdown-account"
>
<li>
<a href="{{ url_for('admin_views.users') }}" id="link-users" class="dropdown-item">Users</a>
</li>
<li>
<a href="{{ url_for('admin_views.questions') }}" id="link-questions" class="dropdown-item">Question Datasets</a>
</li>
</ul>
</li>
<li class="nav-item dropdown" id="nav-account">
<a
class="nav-link dropdown-toggle"
id="dropdown-account"
role="button"
href="{{ url_for('admin_auth.account') }}"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Account
</a>
<ul
class="dropdown-menu"
aria-labelledby="dropdown-account"
>
<li>
<a href="{{ url_for('admin_auth.account') }}" id="link-account" class="dropdown-item">Account Settings</a>
</li>
<li>
<a href="{{ url_for('admin_auth.logout') }}" id="link-logout" class="dropdown-item">Log Out</a>
</li>
</ul>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
@@ -1,23 +0,0 @@
<div class="navbar navbar-expand-sm navbar-light bg-light">
<div class="container-fluid">
<div class="expand navbar-expand justify-content-center" id="navbar_secondary">
<ul class="nav">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin_views.tests', filter='active') }}">Active</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin_views.tests', filter='scheduled') }}">Scheduled</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin_views.tests', filter='expired') }}">Expired</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin_views.tests', filter='all') }}">All</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin_views.tests', filter='create') }}">Create</a>
</li>
</ul>
</div>
</div>
</div>
@@ -1 +0,0 @@
{% extends "admin/components/base.html" %}
-509
View File
@@ -1,509 +0,0 @@
from flask import Blueprint, render_template, flash, redirect, request, jsonify, abort, session
from flask.helpers import url_for
from functools import wraps
from datetime import datetime, timedelta
import os
from glob import glob
from json import loads
from werkzeug.security import check_password_hash
from common.security.database import decrypt_find, decrypt_find_one
from .models.users import User
from flask_mail import Message
from uuid import uuid4
import secrets
from datetime import datetime, date
from .models.tests import Test
from common.data_tools import get_default_dataset, get_time_options, available_datasets, get_datasets, get_correct_answers
views = Blueprint(
'admin_views',
__name__,
template_folder='templates',
static_folder='static'
)
def admin_account_required(function):
@wraps(function)
def decorated_function(*args, **kwargs):
from main import db
from main import db
if not db.users.find_one({}):
flash('No administrator accounts have been registered. Please register an administrator account.', 'alert')
return redirect(url_for('admin_auth.register'))
return function(*args, **kwargs)
return decorated_function
def disable_on_registration(function):
@wraps(function)
def decorated_function(*args, **kwargs):
from main import db
if db.users.find_one({}):
return abort(404)
return function(*args, **kwargs)
return decorated_function
def get_id_from_cookie():
return request.cookies.get('_id')
def get_user_from_db(_id):
from main import db
return db.users.find_one({'_id': _id})
def check_login():
_id = get_id_from_cookie()
return True if get_user_from_db(_id) else False
def login_required(function):
@wraps(function)
def decorated_function(*args, **kwargs):
if not check_login():
session['prev_page'] = request.url
flash('Please log in to view this page.', 'alert')
return redirect(url_for('admin_auth.login'))
return function(*args, **kwargs)
return decorated_function
def disable_if_logged_in(function):
@wraps(function)
def decorated_function(*args, **kwargs):
if check_login():
return abort(404)
return function(*args, **kwargs)
return decorated_function
@views.route('/')
@views.route('/home/')
@views.route('/dashboard/')
@admin_account_required
@login_required
def home():
from main import db
tests = db.tests.find()
results = decrypt_find(db.entries, {})
current_tests = [ test for test in tests if test['expiry_date'] >= datetime.utcnow() and test['start_date'].date() <= date.today() ]
current_tests.sort(key= lambda x: x['expiry_date'], reverse=True)
upcoming_tests = [ test for test in tests if test['start_date'] > datetime.utcnow()]
upcoming_tests.sort(key= lambda x: x['start_date'])
recent_results = [result for result in results if 'submission_time' in result ]
recent_results.sort(key= lambda x: x['submission_time'], reverse=True)
for result in recent_results:
result['percent'] = round(100*result['results']['score']/result['results']['max'])
return render_template('/admin/index.html', current_tests = current_tests[:5], upcomimg_tests = upcoming_tests[:5], recent_results = recent_results[:5])
@views.route('/settings/')
@admin_account_required
@login_required
def settings():
from main import db
users = decrypt_find(db.users, {})
users.sort(key= lambda x: x['username'])
datasets = get_datasets()
return render_template('/admin/settings/index.html', users=users[:5], datasets=datasets[:5])
@views.route('/settings/users/', methods=['GET','POST'])
@admin_account_required
@login_required
def users():
from main import db, mail
from .models.forms import CreateUserForm
form = CreateUserForm()
if request.method == 'GET':
users_list = decrypt_find(db.users, {})
return render_template('/admin/settings/users.html', users = users_list, form = form)
if request.method == 'POST':
if form.validate_on_submit():
entry = User(
_id = uuid4().hex,
username = request.form.get('username').lower(),
email = request.form.get('email'),
password = request.form.get('password') if not request.form.get('password') == '' else secrets.token_hex(12),
)
email = Message(
subject = 'RefTest | Registration Confirmation',
recipients = [entry.email],
body = f"""
Hello {entry.username}, \n\n
You have been registered as an administrator for the SKA RefTest App!\n\n
You can access your account using the username '{entry.username}'.\n\n
Your password is as follows:\n\n
{entry.password}\n\n
You can change your password by logging in to the admin console by copying the following URL into a web browser:\n\n
{url_for('admin_views.home', _external = True)}\n\n
Have a nice day.
""",
html = f"""
<p>Hello {entry.username},</p>
<p>You have been registered as an administrator for the SKA RefTest App!</p>
<p>You can access your account using the username '{entry.username}'.</p>
<p>Your password is as follows:</p>
<strong>{entry.password}</strong>
<p>You can change your password by logging in to the admin console at the link below:</p>
<p><a href='{url_for('admin_views.home', _external = True)}'>{url_for('admin_views.home', _external = True)}</a></p>
<p>Have a nice day.</p>
"""
)
mail.send(email)
return entry.register()
else:
errors = [*form.username.errors, *form.email.errors, *form.password.errors]
return jsonify({ 'error': errors}), 400
@views.route('/settings/users/delete/<string:_id>', methods = ['GET', 'POST'])
@admin_account_required
@login_required
def delete_user(_id:str):
from main import db, mail
if _id == get_id_from_cookie():
flash('Cannot delete your own user account.', 'error')
return redirect(url_for('admin_views.users'))
from .models.forms import DeleteUserForm
form = DeleteUserForm()
user = decrypt_find_one(db.users, {'_id': _id})
if request.method == 'GET':
if not user:
return abort(404)
return render_template('/admin/settings/delete-user.html', form = form, _id = _id, user = user)
if request.method == 'POST':
if not user:
return jsonify({ 'error': 'User does not exist.' }), 404
if form.validate_on_submit():
_user = decrypt_find_one(db.users, {'_id': get_id_from_cookie()})
password = request.form.get('password')
if not check_password_hash(_user['password'], password):
return jsonify({ 'error': 'The password you entered is incorrect.' }), 401
if request.form.get('notify'):
email = Message(
subject = 'RefTest | Account Deletion',
recipients = [user['email']],
bcc = [_user['email']],
body = f"""
Hello {user['username']}, \n\n
Your administrator account for the SKA RefTest App has been deleted by {_user['username']}. All data about your account has been deleted.\n\n
If you believe this was done in error, please contact them immediately.\n\n
If you would like to register to administer the app, please ask an existing administrator to create a new account.\n\n
Have a nice day.
""",
html = f"""
<p>Hello {user['username']},</p>
<p>Your administrator account for the SKA RefTest App has been deleted by {_user['username']}. All data about your account has been deleted.</p>
<p>If you believe this was done in error, please contact them immediately.</p>
<p>If you would like to register to administer the app, please ask an existing administrator to create a new account.</p>
<p>Have a nice day.</p>
"""
)
mail.send(email)
user = User(
_id = user['_id']
)
return user.delete()
else: return abort(400)
@views.route('/settings/users/update/<string:_id>', methods = ['GET', 'POST'])
@admin_account_required
@login_required
def update_user(_id:str):
from main import db, mail
if _id == get_id_from_cookie():
flash('Cannot delete your own user account.', 'error')
return redirect(url_for('admin_views.users'))
from .models.forms import UpdateUserForm
form = UpdateUserForm()
user = decrypt_find_one( db.users, {'_id': _id})
if request.method == 'GET':
if not user:
return abort(404)
return render_template('/admin/settings/update-user.html', form = form, _id = _id, user = user)
if request.method == 'POST':
if not user:
return jsonify({ 'error': 'User does not exist.' }), 404
if form.validate_on_submit():
_user = decrypt_find_one(db.users, {'_id': get_id_from_cookie()})
password = request.form.get('password')
if not check_password_hash(_user['password'], password):
return jsonify({ 'error': 'The password you entered is incorrect.' }), 401
if request.form.get('notify'):
recipient = request.form.get('email') if not request.form.get('email') == '' else user['email']
email = Message(
subject = 'RefTest | Account Update',
recipients = [recipient],
bcc = [_user['email']],
body = f"""
Hello {user['username']}, \n\n
Your administrator account for the SKA RefTest App has been updated by {_user['username']}.\n\n
Your new account details are as follows:\n\n
Email: {recipient}\n
Password: {request.form.get('password')}\n\n
You can update your email and password by logging in to the app.\n\n
Have a nice day.
""",
html = f"""
<p>Hello {user['username']},</p>
<p>Your administrator account for the SKA RefTest App has been updated by {_user['username']}.</p>
<p>Your new account details are as follows:</p>
<p>Email: {recipient} <br/>Password: {request.form.get('password')}</p>
<p>You can update your email and password by logging in to the app.</p>
<p>Have a nice day.</p>
"""
)
mail.send(email)
entry = User(
_id = _id,
email = request.form.get('email'),
password = request.form.get('password')
)
return entry.update()
else:
errors = [*form.user_password.errors, *form.email.errors, *form.password.errors, *form.password_reenter.errors]
return jsonify({ 'error': errors}), 400
@views.route('/settings/questions/', methods=['GET', 'POST'])
@admin_account_required
@login_required
def questions():
from .models.forms import UploadDataForm
from common.data_tools import check_json_format, validate_json_contents, store_data_file
form = UploadDataForm()
if request.method == 'GET':
data = get_datasets()
default = get_default_dataset()
return render_template('/admin/settings/questions.html', form=form, data=data, default=default)
if request.method == 'POST':
if form.validate_on_submit():
upload = form.data_file.data
default = True if request.form.get('default') else False
if not check_json_format(upload):
return jsonify({ 'error': 'Invalid file selected. Please upload a JSON file.'}), 400
if not validate_json_contents(upload):
return jsonify({'error': 'The data in the file is invalid.'}), 400
filename = store_data_file(upload, default=default)
flash(f'Dataset {form.data_file.data.filename} has been uploaded as {filename}.', 'success')
return jsonify({ 'success': f'Dataset {form.data_file.data.filename} has been uploaded as {filename}.'}), 200
errors = [*form.errors]
return jsonify({ 'error': errors}), 400
@views.route('/settings/questions/delete/', methods=['POST'])
@admin_account_required
@login_required
def delete_questions():
from main import db, app
filename = request.get_json()['filename']
data_files = glob(os.path.join(app.config["DATA_FILE_DIRECTORY"],'*.json'))
if any(filename in file for file in data_files):
default = get_default_dataset()
if default == filename:
return jsonify({'error': 'Cannot delete the default question dataset.'}), 400
data_file = os.path.join(app.config["DATA_FILE_DIRECTORY"],filename)
with open(data_file, 'r') as _data_file:
data = loads(_data_file.read())
if data['meta']['tests']:
return jsonify({'error': 'Cannot delete a dataset that is in use by an exam.'}), 400
if len(data_files) == 1:
return jsonify({'error': 'Cannot delete the only question dataset.'}), 400
os.remove(data_file)
flash(f'Question dataset {filename} has been deleted.', 'success')
return jsonify({'success': f'Question dataset {filename} has been deleted.'}), 200
return abort(404)
@views.route('/settings/questions/default/', methods=['POST'])
@admin_account_required
@login_required
def make_default_questions():
from main import app
filename = request.get_json()['filename']
data_files = glob(os.path.join(app.config["DATA_FILE_DIRECTORY"],'*.json'))
default_file_path = os.path.join(app.config['DATA_FILE_DIRECTORY'], '.default.txt')
if any(filename in file for file in data_files):
with open(default_file_path, 'r') as default_file:
default = default_file.read()
if default == filename:
return jsonify({'error': 'Cannot delete default question dataset.'}), 400
with open(default_file_path, 'w') as default_file:
default_file.write(filename)
flash(f'Set dataset f{filename} as the default.', 'success')
return jsonify({'success': f'Set dataset {filename} as the default.'})
return abort(404)
@views.route('/tests/<filter>/', methods=['GET'])
@views.route('/tests/', methods=['GET'])
@admin_account_required
@login_required
def tests(filter=''):
from main import db
if not available_datasets():
flash('There are no available question datasets. Please upload a question dataset in order to set up an exam.', 'error')
return redirect(url_for('admin_views.questions'))
if filter not in ['', 'create', 'active', 'scheduled', 'expired', 'all']:
return abort(404)
if filter == 'create':
from .models.forms import CreateTest
form = CreateTest()
form.time_limit.choices = get_time_options()
form.dataset.choices = available_datasets()
form.time_limit.default='none'
form.dataset.default=get_default_dataset()
form.process()
display_title = ''
error_none = ''
return render_template('/admin/tests.html', form = form, display_title=display_title, error_none=error_none, filter=filter)
_tests = db.tests.find({})
if filter == 'active' or filter == '':
tests = [ test for test in _tests if test['expiry_date'] >= datetime.utcnow() and test['start_date'].date() <= date.today() ]
display_title = 'Active Exams'
error_none = 'There are no exams that are currently active. You can create one using the Creat Exam form.'
if filter == 'expired':
tests = [ test for test in _tests if test['expiry_date'] < datetime.utcnow()]
display_title = 'Expired Exams'
error_none = 'There are no expired exams. Exams will appear in this category after their expiration date has passed.'
if filter == 'scheduled':
tests = [ test for test in _tests if test['start_date'].date() > date.today()]
display_title = 'Scheduled Exams'
error_none = 'There are no scheduled exams pending. You can schedule an exam for the future using the Create Exam form.'
if filter == 'all':
tests = _tests
display_title = 'All Exams'
error_none = 'There are no exams set up. You can create one using the Create Exam form.'
return render_template('/admin/tests.html', tests = tests, display_title=display_title, error_none=error_none, filter=filter)
@views.route('/tests/create/', methods=['POST'])
@admin_account_required
@login_required
def create_test():
from main import db
from .models.forms import CreateTest
form = CreateTest()
form.dataset.choices = available_datasets()
form.time_limit.choices = get_time_options()
if form.validate_on_submit():
start_date = request.form.get('start_date')
start_date = datetime.strptime(start_date, '%Y-%m-%d')
expiry_date = request.form.get('expiry_date')
expiry_date = datetime.strptime(expiry_date, '%Y-%m-%d') + timedelta(days= 1) - timedelta(milliseconds = 1)
dataset = request.form.get('dataset')
errors = []
if start_date.date() < date.today():
errors.append('The start date cannot be in the past.')
if expiry_date.date() < date.today():
errors.append('The expiry date cannot be in the past.')
if expiry_date < start_date:
errors.append('The expiry date cannot be before the start date.')
if errors:
return jsonify({'error': errors}), 400
creator_id = get_id_from_cookie()
creator = decrypt_find_one(db.users, { '_id': creator_id } )['username']
test = Test(
_id = uuid4().hex,
start_date = start_date,
expiry_date = expiry_date,
time_limit = request.form.get('time_limit'),
creator = creator,
dataset = dataset
)
test.create()
return jsonify({'success': 'New exam created.'}), 200
else:
errors = [*form.expiry.errors, *form.time_limit.errors]
return jsonify({ 'error': errors}), 400
@views.route('/tests/delete/', methods=['POST'])
@admin_account_required
@login_required
def delete_test():
from main import db
_id = request.get_json()['_id']
if db.tests.find_one({'_id': _id}):
return Test(_id = _id).delete()
return jsonify({'error': 'Could not find the corresponding test to delete.'}), 404
@views.route('/tests/close/', methods=['POST'])
@admin_account_required
@login_required
def close_test():
from main import db
_id = request.get_json()['_id']
if db.tests.find_one({'_id': _id}):
return Test(_id = _id, expiry_date= datetime.utcnow()).update()
return jsonify({'error': 'Could not find the corresponding test to delete.'}), 404
@views.route('/test/<_id>/', methods=['GET','POST'])
@admin_account_required
@login_required
def view_test(_id):
from main import db
from .models.forms import AddTimeAdjustment
form = AddTimeAdjustment()
test = decrypt_find_one(db.tests, {'_id': _id})
if request.method == 'GET':
if not test:
return abort(404)
return render_template('/admin/test.html', test = test, form = form)
if request.method == 'POST':
if form.validate_on_submit():
time = int(request.form.get('time'))
return Test(_id=_id).add_time_adjustment(time)
return jsonify({'error': form.time.errors }), 400
@views.route('/test/<_id>/delete-adjustment/', methods = ['POST'])
@admin_account_required
@login_required
def delete_adjustment(_id):
user_code = request.get_json()['user_code']
return Test(_id=_id).remove_time_adjustment(user_code)
@views.route('/results/')
@admin_account_required
@login_required
def view_entries():
from main import db
entries = decrypt_find(db.entries, {})
return render_template('/admin/results.html', entries = entries)
@views.route('/results/<_id>/', methods = ['GET', 'POST'])
@admin_account_required
@login_required
def view_entry(_id=''):
from main import app, db
entry = decrypt_find_one(db.entries, {'_id': _id})
if request.method == 'GET':
if not entry:
return abort(404)
test_code = entry['test_code']
test = db.tests.find_one({'test_code' : test_code})
dataset = test['dataset']
dataset_path = os.path.join(app.config['DATA_FILE_DIRECTORY'], dataset)
with open(dataset_path, 'r') as _dataset:
data = loads(_dataset.read())
correct = get_correct_answers(dataset=data)
print(correct.values())
return render_template('/admin/result-detail.html', entry = entry, correct = correct)
if request.method == 'POST':
if not entry:
return jsonify({'error': 'A valid entry could no be found.'}), 404
action = request.get_json()['action']
if action == 'override':
late_ignore = db.entries.find_one_and_update({'_id': _id}, {'$set': {'status': 'late (allowed)'}})
if late_ignore:
flash('Late status for the entry has been allowed.', 'success')
return jsonify({'success': 'Late status allowed.'}), 200
return jsonify({'error': 'An error occurred.'}), 400
if action == 'delete':
test_code = entry['test_code']
test = db.tests.find_one_and_update({'test_code': test_code}, {'$pull': {'entries': _id}})
if not test:
return jsonify({'error': 'A valid exam could not be found.'}), 404
delete = db.entries.delete_one({'_id': _id})
if delete:
flash('Entry has been deleted.', 'success')
return jsonify({'success': 'Entry has been deleted.'}), 200
return jsonify({'error': 'An error occurred.'}), 400
@views.route('/certificate/', methods=['POST'])
@admin_account_required
@login_required
def generate_certificate():
from main import db
_id = request.get_json()['_id']
entry = decrypt_find_one(db.entries, {'_id': _id})
if not entry:
return abort(404)
return render_template('/admin/components/certificate.html', entry = entry)
+68
View File
@@ -0,0 +1,68 @@
from .config import Production as Config
from .models import *
from .extensions import bootstrap, csrf, db, login_manager, mail
from .tools.logs import write
from flask import flash, Flask, render_template, request
from flask.helpers import abort, url_for
from flask.json import jsonify
from flask_wtf.csrf import CSRFError
from werkzeug.middleware.proxy_fix import ProxyFix
from datetime import datetime
def create_app():
app = Flask(__name__)
app.config.from_object(Config())
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto= 1, x_host= 1)
bootstrap.init_app(app)
csrf.init_app(app)
db.init_app(app)
login_manager.init_app(app)
mail.init_app(app)
login_manager.login_view = 'admin._login'
@login_manager.user_loader
def _load_user(id):
try: return User.query.filter_by(id=id).first()
except Exception as exception:
write('system.log', f'Database error when loading user fo login manager: {exception}')
return abort(500)
@app.before_request
def _check_cookie_consent():
if request.cookies.get('cookie_consent'):
return
if any([ request.path.startswith(x) for x in [ '/admin/static/', '/root/', '/quiz/static', '/cookies/', '/admin/editor/static', '/admin/view/static' ] ]):
return
flash(f'<strong>Cookie Consent</strong>: This web site only stores minimal, functional cookies. It does not store any tracking information. By using this site, you consent to this use of cookies. For more information, see our <a href="{url_for("views._privacy")}">privacy policy</a>.', 'cookie_alert')
@app.errorhandler(404)
def _404_handler(error): return render_template('404.html')
@app.errorhandler(CSRFError)
def _csrf_handler(): return jsonify({'error':'Could not validate a secure connection.'}), 403
@app.context_processor
def _now(): return {'now': datetime.now()}
from .admin.views import admin
from .api.views import api
from .quiz.views import quiz
from .views import views
from .editor.views import editor
from .view.views import view
from .analysis.views import analysis
app.register_blueprint(admin, url_prefix='/admin')
app.register_blueprint(api, url_prefix='/api')
app.register_blueprint(views)
app.register_blueprint(quiz)
app.register_blueprint(editor, url_prefix='/admin/editor')
app.register_blueprint(view, url_prefix='/admin/view')
app.register_blueprint(analysis, url_prefix='/admin/analysis')
"""Create Database Tables when creating app"""
with app.app_context():
db.create_all()
return app
@@ -158,7 +158,7 @@ table.dataTable {
#dismiss-cookie-alert {
margin-top: 16px;
width: 100%;
width: fit-content;
}
.alert-db-empty {
@@ -223,6 +223,19 @@ table.dataTable {
background-color: unset;
}
/* Change Autocomplete styles in Chrome*/
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:focus,
select:-webkit-autofill,
select:-webkit-autofill:hover,
select:-webkit-autofill:focus {
transition: background-color 5000s ease-in-out 0s;
}
/* Fallback for Edge
-------------------------------------------------- */
@supports (-ms-ime-align: auto) {

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

@@ -1,25 +1,25 @@
// Menu Highlight Scripts
const menuItems = document.getElementsByClassName('nav-link');
const menuItems = document.getElementsByClassName('nav-link')
for(let i = 0; i < menuItems.length; i++) {
if(menuItems[i].pathname == window.location.pathname) {
menuItems[i].classList.add('active');
menuItems[i].classList.add('active')
}
}
const dropdownItems = document.getElementsByClassName('dropdown-item');
const dropdownItems = document.getElementsByClassName('dropdown-item')
for(let i = 0; i< dropdownItems.length; i++) {
if(dropdownItems[i].pathname == window.location.pathname) {
dropdownItems[i].classList.add('active');
$( "#" + dropdownItems[i].id ).closest( '.dropdown' ).find('.dropdown-toggle').addClass('active');
dropdownItems[i].classList.add('active')
$( "#" + dropdownItems[i].id ).closest( '.dropdown' ).find('.dropdown-toggle').addClass('active')
}
}
// General Post Method Form Processing Script
$('form.form-post').submit(function(event) {
var $form = $(this);
var data = $form.serialize();
var url = $(this).attr('action');
var rel_success = $(this).data('rel-success');
var $form = $(this)
var data = $form.serialize()
var url = $(this).prop('action')
var rel_success = $(this).data('rel-success')
$.ajax({
url: url,
@@ -28,25 +28,25 @@ $('form.form-post').submit(function(event) {
dataType: 'json',
success: function(response) {
if (response.redirect_to) {
window.location.href = response.redirect_to;
window.location.href = response.redirect_to
}
else {
window.location.href = rel_success;
window.location.href = rel_success
}
},
error: function(response) {
error_response(response);
error_response(response)
}
});
})
event.preventDefault();
});
event.preventDefault()
})
// Form Upload Questions - Special case, needs to handle files.
$('form[name=form-upload-questions]').submit(function(event) {
var $form = $(this);
var data = new FormData($form[0]);
var $form = $(this)
var data = new FormData($form[0])
var file = $('input[name=data_file]')[0].files[0]
data.append('file', file)
@@ -57,84 +57,107 @@ $('form[name=form-upload-questions]').submit(function(event) {
processData: false,
contentType: false,
success: function(response) {
window.location.reload();
window.location.reload()
},
error: function(response) {
error_response(response);
error_response(response)
}
});
})
event.preventDefault();
});
event.preventDefault()
})
// Edit and Delete Test Button Handlers
$('.test-action').click(function(event) {
let _id = $(this).data('_id');
let action = $(this).data('action');
let id = $(this).data('id')
let action = $(this).data('action')
if (action == 'delete') {
if (action == 'delete' || action == 'start' || action == 'end') {
$.ajax({
url: `/admin/tests/delete/`,
url: `/admin/tests/edit/`,
type: 'POST',
data: JSON.stringify({'_id': _id}),
data: JSON.stringify({'id': id, 'action': action}),
contentType: 'application/json',
success: function(response) {
window.location.href = '/admin/tests/';
window.location.href = '/admin/tests/'
},
error: function(response){
error_response(response);
error_response(response)
},
});
})
} else if (action == 'edit') {
window.location.href = `/admin/test/${_id}/`
} else if (action == 'close'){
window.location.href = `/admin/test/${id}/`
} else if (action == 'analyse') {
$.ajax({
url: `/admin/tests/close/`,
url: `/admin/analysis/`,
type: 'POST',
data: JSON.stringify({'_id': _id}),
data: JSON.stringify({'id': id, 'class': 'test'}),
contentType: 'application/json',
success: function(response) {
$(window).scrollTop(0);
window.location.reload();
window.location.href = response
},
error: function(response){
error_response(response);
error_response(response)
},
});
})
}
event.preventDefault();
});
event.preventDefault()
})
// Edit Dataset Button Handlers
$('.edit-question-dataset').click(function(event) {
var filename = $(this).data('filename');
var action = $(this).data('action');
var disabled = $(this).hasClass('disabled');
var id = $(this).data('id')
var action = $(this).data('action')
var disabled = $(this).hasClass('disabled')
if ( !disabled ) {
if (action == 'delete') {
$.ajax({
url: `/admin/settings/questions/${action}/`,
type: 'POST',
data: JSON.stringify({'filename': filename}),
data: JSON.stringify({
'id': id,
'action': action,
}),
contentType: 'application/json',
success: function(response) {
window.location.reload();
window.location.reload()
},
error: function(response){
error_response(response);
error_response(response)
},
});
};
event.preventDefault();
});
})
} else if (action == 'edit') {
window.location.href = `/admin/editor/${id}/`
} else if (action == 'view') {
window.location.href = `/admin/view/${id}`
} else if (action == 'download') {
window.location.href = `/admin/settings/questions/download/${id}/`
} else if (action == 'analyse') {
$.ajax({
url: `/admin/analysis/`,
type: 'POST',
data: JSON.stringify({'id': id, 'class': 'dataset'}),
contentType: 'application/json',
success: function(response) {
window.location.href = response
},
error: function(response){
error_response(response)
},
})
}
}
event.preventDefault()
})
function error_response(response) {
const $alert = $("#alert-box");
$alert.html('');
const $alert = $("#alert-box")
$alert.html('')
if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) {
$alert.html(`
@@ -143,18 +166,18 @@ function error_response(response) {
${response.responseJSON.error}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`);
`)
} else if (response.responseJSON.error instanceof Array) {
var output = ''
for (var i = 0; i < response.responseJSON.error.length; i ++) {
for (let i = 0; i < response.responseJSON.error.length; i ++) {
output += `
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
${response.responseJSON.error[i]}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`;
$alert.html(output);
`
$alert.html(output)
}
}
@@ -166,68 +189,68 @@ $('#dismiss-cookie-alert').click(function(event){
$.ajax({
url: '/cookies/',
type: 'GET',
type: 'POST',
data: {
time: Date.now()
},
dataType: 'json',
success: function(response){
console.log(response);
console.log(response)
},
error: function(response){
console.log(response);
console.log(response)
}
})
event.preventDefault();
event.preventDefault()
})
// Script for Result Actions
$('.result-action-buttons').click(function(event){
var _id = $(this).data('_id');
var id = $(this).data('id')
if ($(this).data('result-action') == 'generate') {
$.ajax({
url: '/admin/certificate/',
type: 'POST',
data: JSON.stringify({'_id': _id}),
data: JSON.stringify({'id': id}),
contentType: 'application/json',
dataType: 'html',
success: function(response) {
var display_window = window.open();
display_window.document.write(response);
var display_window = window.open()
display_window.document.write(response)
},
error: function(response){
error_response(response);
error_response(response)
},
});
})
} else {
var action = $(this).data('result-action')
$.ajax({
url: window.location.href,
type: 'POST',
data: JSON.stringify({'_id': _id, 'action': action}),
data: JSON.stringify({'id': id, 'action': action}),
contentType: 'application/json',
success: function(response) {
if (action == 'delete') {
window.location.href = '/admin/results/';
} else window.location.reload();
window.location.href = '/admin/results/'
} else window.location.reload()
},
error: function(response){
error_response(response);
error_response(response)
},
});
})
}
event.preventDefault();
});
event.preventDefault()
})
// Script for Deleting Time Adjustment
$('.adjustment-delete').click(function(event){
var user_code = $(this).data('user_code');
var location = window.location.href;
var user_code = $(this).data('user_code')
var location = window.location.href
location = location.replace('#', '')
$.ajax({
@@ -236,12 +259,19 @@ $('.adjustment-delete').click(function(event){
data: JSON.stringify({'user_code': user_code}),
contentType: 'application/json',
success: function(response) {
window.location.reload();
window.location.reload()
},
error: function(response){
error_response(response);
error_response(response)
},
});
})
event.preventDefault();
});
event.preventDefault()
})
// Detailed Results view questions
$('.view-full-questions').click(function(event) {
var dataset = $(this).data('dataset')
window.open(`/admin/view/${dataset}`, '_blank')
event.preventDefault()
})
@@ -2,7 +2,7 @@
{% block content %}
<div class="form-container">
<form name="form-update-account" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin_views.home') }}">
<form name="form-update-account" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin._home') }}">
{% include "admin/components/server-alerts.html" %}
<h2 class="form-heading">Update Your Account</h2>
{{ form.hidden_tag() }}
@@ -32,7 +32,7 @@
<div class="container form-submission-button">
<div class="row">
<div class="col text-center">
<a href="{{ url_for('admin_views.users') }}" class="btn btn-md btn-danger btn-block" type="button">
<a href="{{ url_for('admin._users') }}" class="btn btn-md btn-danger btn-block" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-x-circle-fill" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>
</svg>
@@ -2,7 +2,7 @@
{% block content %}
<div class="form-container">
<form name="form-login" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin_views.home') }}">
<form name="form-login" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ next or url_for('admin._home') }}">
{% include "admin/components/server-alerts.html" %}
<h2 class="form">Log In</h2>
{{ form.hidden_tag() }}
@@ -26,7 +26,7 @@
</div>
</div>
</div>
<a href="{{ url_for('admin_auth.reset') }}" class="signin-forgot-password">Reset Password</a>
<a href="{{ url_for('admin._reset') }}" class="signin-forgot-password">Reset Password</a>
</form>
</div>
{% endblock %}
@@ -3,14 +3,14 @@
{% block navbar %}
<nav class="navbar fixed-top navbar-expand-md navbar-dark bg-dark">
<div class="container">
<a href="{{ url_for('admin_views.home') }}" class="navbar-brand mb-0 h1">RefTest | Admin</a>
<a href="{{ url_for('admin._home') }}" class="navbar-brand mb-0 h1">RefTest | Admin</a>
</div>
</nav>
{% endblock %}
{% block content %}
<div class="form-container">
<form name="form-register" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin_auth.login') }}">
<form name="form-register" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin._login') }}">
{% include "admin/components/server-alerts.html" %}
<h2 class="form-heading">Register an Account</h2>
{{ form.hidden_tag() }}
@@ -2,7 +2,7 @@
{% block content %}
<div class="form-container">
<form name="form-reset" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin_auth.login') }}">
<form name="form-reset" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin._login') }}">
{% include "admin/components/server-alerts.html" %}
<h2 class="form-heading">Reset Password</h2>
{{ form.hidden_tag() }}
@@ -2,7 +2,7 @@
{% block content %}
<div class="form-container">
<form name="form-update-password" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin_auth.login') }}">
<form name="form-update-password" class="form-display form-post" action="{{ url_for('admin._update_password', **request.view_args) }}" data-rel-success="{{ url_for('admin._login') }}">
{% include "admin/components/server-alerts.html" %}
<h2 class="form-heading">Update Password</h2>
{{ form.hidden_tag() }}
@@ -45,6 +45,9 @@
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
crossorigin="anonymous">
</script>
<script>
window.jQuery || document.write(`<script src="{{ url_for('.static', filename='js/jquery-3.6.0.min.js') }}"><\/script>`)
</script>
<script
src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.10.2/dist/umd/popper.min.js"
integrity="sha384-7+zCNj/IqJ95wo16oMtfsKbZ9ccEh31eOz1HGyDuCQ6wgnyJNSYdrPa03rtR1zdB"
@@ -15,30 +15,30 @@
<h5 class="mb-1">Candidate</h5>
</div>
<h2>
{{ entry.name.surname}}, {{ entry.name.first_name }}
{{ entry.get_surname()}}, {{ entry.get_first_name() }}
</h2>
</li>
<li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Email Address</h5>
</div>
{{ entry.email }}
{{ entry.get_email() }}
</li>
{% if entry['club'] %}
{% if entry.get_club() %}
<li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Club</h5>
</div>
{{ entry.club }}
{{ entry.get_club() }}
</li>
{% endif %}
<li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Exam Code</h5>
</div>
{{ '—'.join([entry.test_code[:4], entry.test_code[4:8], entry.test_code[8:]]) }}
{{ entry.test.get_code() }}
</li>
{% if entry['user_code'] %}
{% if entry.user_code %}
<li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">User Code</h5>
@@ -50,7 +50,7 @@
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Start Time</h5>
</div>
{{ entry.start_time.strftime('%d %b %Y %H:%M:%S') }}
{{ entry.start_time.strftime('%d %b %Y %H:%M:%S') if entry.start_time else None }}
</li>
<li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
@@ -59,19 +59,19 @@
<span class="badge bg-danger">Late</span>
{% endif %}
</div>
{{ entry.submission_time.strftime('%d %b %Y %H:%M:%S') }}
{{ entry.end_time.strftime('%d %b %Y %H:%M:%S') if entry.end_time else None }}
</li>
<li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Score</h5>
</div>
{{ entry.results.score }}&percnt;
{{ entry.result.score }}&percnt;
</li>
<li class="list-group-item list-group-item-action {% if entry.results.grade == 'fail' %}list-group-item-danger {% elif entry.results.grade == 'merit' %} list-group-item-success {% endif %}">
<li class="list-group-item list-group-item-action {% if entry.result.grade == 'fail' %}list-group-item-danger {% elif entry.result.grade == 'merit' %} list-group-item-success {% endif %}">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Grade</h5>
</div>
{{ entry.results.grade[0]|upper }}{{ entry.results.grade[1:]}}
{{ entry.result.grade[0]|upper }}{{ entry.result.grade[1:]}}
</li>
</ul>
<div class="site-footer mt-5">
@@ -0,0 +1,2 @@
<p>This web app was developed and is maintained by Vivek Santayana. The source code for the web app, excluding any data pertaining to the questions in the quiz, is freely available at <a href="https://git.vsnt.uk/viveksantayana/ska-referee-test">Vivek&rsquo;s personal GIT repository</a> under an MIT License.</p>
<p>All questions in the test are &copy; The Scottish Korfball Association {{ now.year }}. All rights are reserved.</p>
@@ -0,0 +1,137 @@
<nav class="navbar fixed-top navbar-expand-md navbar-dark bg-dark">
<div class="container">
<a href="{{ url_for('admin._home') }}" class="navbar-brand mb-0 h1">RefTest | Admin</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbar"
aria-controls="navbar"
aria-expanded="false"
aria-label="Toggle Navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse justify-content-end" id="navbar">
<ul class="navbar-nav">
{% if not current_user.is_authenticated %}
<li class="nav-item" id="nav-login">
<a href="{{ url_for('admin._login') }}" id="link-login" class="nav-link">Log In</a>
</li>
{% endif %}
{% if current_user.is_authenticated %}
<li class="nav-item dropdown" id="nav-results">
<a
class="nav-link dropdown-toggle"
id="dropdown-results"
role="button"
href="{{ url_for('admin._view_entries') }}"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Results
</a>
<ul
class="dropdown-menu"
aria-labelledby="dropdown-results"
>
<li>
<a href="{{ url_for('admin._view_entries', filter='active') }}" id="link-results" class="dropdown-item">View Results</a>
</li>
<li>
<a href="{{ url_for('analysis._analysis') }}" id="link-analysis" class="dropdown-item">Analysis</a>
</li>
</ul>
</li>
<li class="nav-item dropdown" id="nav-tests">
<a
class="nav-link dropdown-toggle"
id="dropdown-tests"
role="button"
href="{{ url_for('admin._tests') }}"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Exams
</a>
<ul
class="dropdown-menu"
aria-labelledby="dropdown-tests"
>
<li>
<a href="{{ url_for('admin._tests', filter='active') }}" id="link-active" class="dropdown-item">Active</a>
</li>
<li>
<a href="{{ url_for('admin._tests', filter='scheduled') }}" id="link-scheduled" class="dropdown-item">Scheduled</a>
</li>
<li>
<a href="{{ url_for('admin._tests', filter='expired') }}" id="link-expired" class="dropdown-item">Expired</a>
</li>
<li>
<a href="{{ url_for('admin._tests', filter='all') }}" id="link-all" class="dropdown-item">All</a>
</li>
<li>
<a href="{{ url_for('admin._tests', filter='create') }}" id="link-create" class="dropdown-item">Create</a>
</li>
</ul>
</li>
<li class="nav-item dropdown" id="nav-settings">
<a
class="nav-link dropdown-toggle"
id="dropdown-settings"
role="button"
href="{{ url_for('admin._settings') }}"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Settings
</a>
<ul
class="dropdown-menu"
aria-labelledby="dropdown-settings"
>
<li>
<a href="{{ url_for('admin._settings') }}" id="link-settings" class="dropdown-item">View Settings</a>
</li>
<li>
<a href="{{ url_for('admin._users') }}" id="link-users" class="dropdown-item">Users</a>
</li>
<li>
<a href="{{ url_for('admin._questions') }}" id="link-questions" class="dropdown-item">Manage Questions</a>
</li>
<li>
<a href="{{ url_for('view._view') }}" id="link-editor" class="dropdown-item">View Questions</a>
</li>
<li>
<a href="{{ url_for('editor._editor') }}" id="link-editor" class="dropdown-item">Edit Questions</a>
</li>
</ul>
</li>
<li class="nav-item dropdown" id="nav-account">
<a
class="nav-link dropdown-toggle"
id="dropdown-account"
role="button"
href="{{ url_for('admin._update_user', id=current_user.id) }}"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Account
</a>
<ul
class="dropdown-menu"
aria-labelledby="dropdown-account"
>
<li>
<a href="{{ url_for('admin._update_user', id=current_user.id) }}" id="link-account" class="dropdown-item">Account Settings</a>
</li>
<li>
<a href="{{ url_for('admin._logout') }}" id="link-logout" class="dropdown-item">Log Out</a>
</li>
</ul>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
@@ -2,16 +2,17 @@
<meta property="og:locale" content="en_UK" />
<meta property="og:type" content="website" />
<meta property="og:description" content="A web app for taking the Scottish Korfball Association Refereeing Theory Exam on-line." />
<meta property="og:url" content="{{ url_for(request.endpoint, _external = True, **request.view_args) }}" />
<meta property="og:url" content="{{ url_for(request.endpoint, _external = True, **(request.view_args or {})) }}" />
<meta property="og:site_name" content="Scottish Korfball Association Referee Theory Exam" />
<meta property="og:image" content="{{ url_for('static', filename='favicon.png', _external = True) }}" />
<meta property="og:image" content="{{ url_for('.static', filename='favicon.png', _external = True) }}" />
<meta property="og:image:alt" content="Logo of the SKA Refereeing Exam App" />
<meta property="og:image:width" content="512" />
<meta property="og:image:height" content="512" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:description" content="A web app for taking the Scottish Korfball Association Refereeing Theory Exam on-line." />
<meta name="twitter:image" content="{{ url_for('static', filename='favicon.png', _external = True) }}" />
<meta name="twitter:image" content="{{ url_for('.static', filename='favicon.png', _external = True) }}" />
<meta name="twitter:image:alt" content="Logo of the SKA Refereeing Exam App" />
<meta name="twitter:creator" content="@viveksantayana" />
<meta name="twitter:site" content="@viveksantayana" />
<meta name="theme-color" content="#343a40" />
<link rel="shortcut icon" href="{{ url_for('.static', filename='favicon.ico') }}">
@@ -0,0 +1,23 @@
<div class="navbar navbar-expand-sm navbar-light bg-light">
<div class="container-fluid">
<div class="expand navbar-expand justify-content-center" id="navbar_secondary">
<ul class="nav nav-pills">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='active') }}">Active</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='scheduled') }}">Scheduled</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='expired') }}">Expired</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='all') }}">All</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='create') }}">Create</a>
</li>
</ul>
</div>
</div>
</div>
@@ -25,8 +25,10 @@
<div class="alert alert-primary alert-dismissible fade show" id="cookie-alert" role="alert">
<i class="bi bi-info-circle-fill" title="Cookie Alert" aria-title="Cookie Alert"></i>
{{ message|safe }}
<div class="d-flex justify-content-center w-100">
<button type="button" id="dismiss-cookie-alert" class="btn btn-success" data-bs-dismiss="alert" aria-label="Close">Accept</button>
</div>
</div>
{% set cookie_flash_flag.value = True %}
{% endif %}
{% else %}
@@ -25,22 +25,22 @@
{% for test in current_tests %}
<tr>
<td>
<a href="{{ url_for('admin_views.view_test', _id=test._id) }}">{{ '—'.join([test.test_code[:4], test.test_code[4:8], test.test_code[8:]]) }}</a>
<a href="{{ url_for('admin._view_test', id=test.id) }}">{{ test.get_code() }}</a>
</td>
<td>
{{ test.expiry_date.strftime('%d %b %Y') }}
{{ test.end_date.strftime('%d %b %Y') if test.end_date else None }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<a href="{{ url_for('admin_views.tests', filter='active') }}" class="btn btn-primary">View Exams</a>
<a href="{{ url_for('admin._tests', filter='active') }}" class="btn btn-primary">View Exams</a>
{% else %}
<div class="alert alert-primary">
There are currently no active exams.
</div>
<a href="{{ url_for('admin_views.tests', filter='create') }}" class="btn btn-primary">Create Exam</a>
<a href="{{ url_for('admin._tests', filter='create') }}" class="btn btn-primary">Create Exam</a>
{% endif %}
</div>
</div>
@@ -69,20 +69,24 @@
{% for result in recent_results %}
<tr>
<td>
<a href="{{ url_for('admin_views.view_entry', _id=result._id) }}">{{ result.name.surname }}, {{ result.name.first_name }}</a>
<a href="{{ url_for('admin._view_entry', id=result.id) }}">{{ result.get_surname() }}, {{ result.get_first_name() }}</a>
</td>
<td>
{{ result.submission_time.strftime('%d %b %Y %H:%M') }}
{{ result.end_time.strftime('%d %b %Y %H:%M') if result.end_time else None }}
</td>
<td>
{{ result.percent }}&percnt; ({{ result.results.grade }})
{% if result.result %}
{{ (100*result.result['score']/result.result['max'])|round|int }}&percnt; ({{ result.result.grade }})
{% else %}
Incomplete
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<a href="{{ url_for('admin_views.view_entries') }}" class="btn btn-primary">View Results</a>
<a href="{{ url_for('admin._view_entries') }}" class="btn btn-primary">View Results</a>
{% else %}
<div class="alert alert-primary">
There are currently no exam results to preview.
@@ -114,22 +118,22 @@
{% for test in upcoming_tests %}
<tr>
<td>
<a href="{{ url_for('admin_views.view_test', _id=test._id) }}">{{ '—'.join([test.test_code[:4], test.test_code[4:8], test.test_code[8:]]) }}</a>
<a href="{{ url_for('admin._view_test', id=test.id) }}">{{ test.get_code() }}</a>
</td>
<td>
{{ test.expiry_date.strftime('%d %b %Y') }}
{{ test.end_date.strftime('%d %b %Y') if test.end_date else None }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<a href="{{ url_for('admin_views.tests', filter='scheduled') }}" class="btn btn-primary">View Exams</a>
<a href="{{ url_for('admin._tests', filter='scheduled') }}" class="btn btn-primary">View Exams</a>
{% else %}
<div class="alert alert-primary">
There are currently no upcoming exams.
</div>
<a href="{{ url_for('admin_views.tests', filter='create') }}" class="btn btn-primary">Create Exam</a>
<a href="{{ url_for('admin._tests', filter='create') }}" class="btn btn-primary">Create Exam</a>
{% endif %}
</div>
</div>
@@ -138,7 +142,7 @@
<div class="card m-3">
<div class="card-body">
<h5 class="card-title">Help</h5>
<p class="card-text">This web app was developed by Vivek Santayana. If there are any issues with the app, any bugs you need to report, or any features you would like to request, please feel free to <a href="https://git.vsnt.uk/viveksantayana/ska-referee-test/issues">open an issue at the Git Repository</a>.</p>
<p class="card-text">This web app was developed and is maintained by Vivek Santayana. If there are any issues with the app, any bugs you need to report, or any features you would like to request, please feel free to <a href="https://git.vsnt.uk/viveksantayana/ska-referee-test/issues">open an issue at the Git Repository</a>.</p>
<a href="https://git.vsnt.uk/viveksantayana/ska-referee-test/issues" class="btn btn-primary">Open an Issue</a>
</div>
</div>
@@ -13,30 +13,30 @@
<h5 class="mb-1">Candidate</h5>
</div>
<h2>
{{ entry.name.surname }}, {{ entry.name.first_name }}
{{ entry.get_surname() }}, {{ entry.get_first_name() }}
</h2>
</li>
<li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Email Address</h5>
</div>
{{ entry.email }}
{{ entry.get_email() }}
</li>
{% if entry['club'] %}
{% if entry.get_club() %}
<li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Club</h5>
</div>
{{ entry.club }}
{{ entry.get_club() }}
</li>
{% endif %}
<li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Exam Code</h5>
</div>
{{ '—'.join([entry.test_code[:4], entry.test_code[4:8], entry.test_code[8:]]) }}
{{ entry.test.get_code() }}
</li>
{% if entry['user_code'] %}
{% if entry.user_code %}
<li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">User Code</h5>
@@ -44,12 +44,14 @@
{{ entry.user_code }}
</li>
{% endif %}
{% if entry.start_time %}
<li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Start Time</h5>
</div>
{{ entry.start_time.strftime('%d %b %Y %H:%M:%S') }}
{{ entry.start_time.strftime('%d %b %Y %H:%M:%S') if entry.start_time else None }}
</li>
{% endif %}
<li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Submission Time</h5>
@@ -57,28 +59,28 @@
<span class="badge bg-danger">Late</span>
{% endif %}
</div>
{% if 'submission_time' in entry %}
{{ entry.submission_time.strftime('%d %b %Y %H:%M:%S') }}
{% if entry.end_time %}
{{ entry.end_time.strftime('%d %b %Y %H:%M:%S') }}
{% else %}
Incomplete
{% endif %}
</li>
{% if 'results' in entry %}
{% if entry.result %}
<li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Score</h5>
</div>
{{ entry.results.score }}&percnt;
{{ entry.result.score }}&percnt;
</li>
<li class="list-group-item list-group-item-action {% if entry.results.grade == 'fail' %}list-group-item-danger {% elif entry.results.grade == 'merit' %} list-group-item-success {% endif %}">
<li class="list-group-item list-group-item-action {% if entry.result.grade == 'fail' %}list-group-item-danger {% elif entry.result.grade == 'merit' %} list-group-item-success {% endif %}">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Grade</h5>
</div>
{{ entry.results.grade[0]|upper }}{{ entry.results.grade[1:]}}
{{ entry.result.grade[0]|upper }}{{ entry.result.grade[1:]}}
</li>
{% endif %}
</ul>
{% if 'results' in entry %}
{% if entry.result %}
<div class="accordion" id="results-breakdown">
<div class="accordion-item">
<h2 class="accordion-header" id="by-category">
@@ -103,16 +105,16 @@
</tr>
</thead>
<tbody>
{% for tag, scores in entry.results.tags.items() %}
{% for tag, scores in entry.result.tags.items() %}
<tr>
<td>
{{ tag }}
{{ tag|safe }}
</td>
<td>
{{ scores.scored }}
</td>
<td>
{{scores.max}}
{{ scores.max }}
</td>
</tr>
{% endfor %}
@@ -129,6 +131,7 @@
</h2>
<div id="by-question-breakdown" class="accordion-collapse collapse" aria-labelledby="by-question" data-bs-parent="#results-breakdown">
<div class="accordion-body">
<a class="view-full-questions" data-dataset="{{ entry.test.dataset.id }}">View Questions</a>
<table class="table table-striped">
<thead>
<tr>
@@ -144,11 +147,11 @@
{% for question, answer in entry.answers.items() %}
<tr>
<td>
{{ question }}
{{ question|int + 1 }}
</td>
<td>
{{ answer }}
{% if not correct[question] == answer %}
{{ answers[question|int][answer|int] }}
{% if not correct[question] == answer|int %}
<span class="badge badge-pill bg-danger badge-danger">Incorrect</span>
{% endif %}
</td>
@@ -162,19 +165,19 @@
{% endif %}
<div class="container justify-content-center">
<div class="row">
<a href="#" class="btn btn-primary result-action-buttons" data-result-action="generate" data-_id="{{ entry._id }}">
<a href="#" class="btn btn-primary result-action-buttons" data-result-action="generate" data-id="{{ entry.id }}">
<i class="bi bi-printer-fill button-icon"></i>
Printable Version
</a>
</div>
<div class="row">
{% if entry.status == 'late' %}
<a href="#" class="btn btn-warning result-action-buttons" data-result-action="override" data-_id="{{ entry._id }}">
<a href="#" class="btn btn-warning result-action-buttons" data-result-action="override" data-id="{{ entry.id }}">
<i class="bi bi-clock-history button-icon"></i>
Allow Late Entry
</a>
{% endif %}
<a href="#" class="btn btn-danger result-action-buttons" data-result-action="delete" data-_id="{{ entry._id }}">
<a href="#" class="btn btn-danger result-action-buttons" data-result-action="delete" data-id="{{ entry.id }}">
<i class="bi bi-trash-fill button-icon"></i>
Delete Result
</a>
@@ -37,41 +37,41 @@
{% for entry in entries %}
<tr class="table-row">
<td>
{{ entry.name.surname }}, {{ entry.name.first_name }}
{{ entry.get_surname() }}, {{ entry.get_first_name() }}
</td>
<td>
{% if 'club' in entry %}
{{ entry.club }}
{% if entry.get_club() %}
{{ entry.get_club() }}
{% endif %}
</td>
<td>
{{ '—'.join([entry.test_code[:4], entry.test_code[4:8], entry.test_code[8:]]) }}
{{ entry.test.get_code() }}
</td>
<td>
{% if 'status' in entry %}
{% if entry.status %}
{{ entry.status }}
{% endif %}
</td>
<td>
{% if 'submission_time' in entry %}
{{ entry.submission_time.strftime('%d %b %Y') }}
{% if entry.end_time %}
{{ entry.end_time.strftime('%Y-%m-%d %H:%M') }}
{% endif %}
</td>
<td>
{% if 'results' in entry %}
{{ entry.results.score }}&percnt;
{% if entry.result %}
{{ entry.result.score }}&percnt;
{% endif %}
</td>
<td>
{% if 'results' in entry %}
{{ entry.results.grade }}
{% if entry.result %}
{{ entry.result.grade }}
{% endif %}
</td>
<td class="row-actions">
<a
href="{{ url_for('admin_views.view_entry', _id = entry._id ) }}"
href="{{ url_for('admin._view_entry', id = entry.id ) }}"
class="btn btn-primary entry-details"
data-_id="{{entry._id}}"
data-id="{{entry.id}}"
title="View Details"
>
<i class="bi bi-file-medical-fill button-icon"></i>
@@ -2,11 +2,11 @@
{% block content %}
<div class="form-container">
<form name="form-delete-user" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin_views.users') }}">
<form name="form-delete-user" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin._users') }}">
{% include "admin/components/server-alerts.html" %}
<h2 class="form-heading">Delete User &lsquo;{{ user.username }}&rsquo;?</h2>
<h2 class="form-heading">Delete User &lsquo;{{ user.get_username() }}&rsquo;?</h2>
{{ form.hidden_tag() }}
<p>This action cannot be undone. Deleting an account will mean {{ user.username }} will no longer be able to log in to the admin console.</p>
<p>This action cannot be undone. Deleting an account will mean {{ user.get_username() }} will no longer be able to log in to the admin console.</p>
<p>Are you sure you want to proceed?</p>
<div class="form-label-group">
{{ form.password(class_="form-control", placeholder="Confirm Your Password", autofocus=true) }}
@@ -20,7 +20,7 @@
<div class="container form-submission-button">
<div class="row">
<div class="col text-center">
<a href="{{ url_for('admin_views.users') }}" autofocus="true" class="btn btn-md btn-primary btn-block" type="button">
<a href="{{ url_for('admin._users') }}" autofocus="true" class="btn btn-md btn-primary btn-block" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-x-circle-fill" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>
</svg>
@@ -28,22 +28,22 @@
<tr>
<td>
<a href="
{% if user._id == get_id_from_cookie() %}
{{ url_for('admin_auth.account') }}
{% if user == current_user %}
{{ url_for('admin._update_user', id=current_user.id) }}
{% else %}
{{ url_for('admin_views.update_user', _id=user._id) }}
{{ url_for('admin._update_user', id=user.id) }}
{% endif%}
">{{ user.username }}</a>
">{{ user.get_username() }}</a>
</td>
<td>
<a href="mailto:{{ user.email }}">{{ user.email }}</a>
<a href="mailto:{{ user.get_email() }}">{{ user.get_email() }}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<a href="{{ url_for('admin_views.users') }}" class="btn btn-primary">Manage Users</a>
<a href="{{ url_for('admin._users') }}" class="btn btn-primary">Manage Users</a>
</div>
</div>
</div>
@@ -57,7 +57,7 @@
<thead>
<tr>
<th>
File Name
Name
</th>
<th>
Exams
@@ -68,22 +68,24 @@
{% for dataset in datasets %}
<tr>
<td>
{{ dataset.filename }}
<a href="{{ url_for('editor._editor_console', id=dataset.id) }}">
{{ dataset.get_name() }}
</a>
</td>
<td>
{{ dataset.use }}
{{ dataset.tests|length }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<a href="{{ url_for('admin_views.questions') }}" class="btn btn-primary">Manage Datasets</a>
<a href="{{ url_for('admin._questions') }}" class="btn btn-primary">Manage Datasets</a>
{% else %}
<div class="alert alert-primary">
There are currently no question datasets uploaded.
</div>
<a href="{{ url_for('admin_views.questions') }}" class="btn btn-primary">Upload Dataset</a>
<a href="{{ url_for('admin._questions') }}" class="btn btn-primary">Upload Dataset</a>
{% endif %}
</div>
</div>
@@ -11,10 +11,10 @@
</th>
<th data-priority="1">
File Name
Name
</th>
<th data-priority="2">
Uploaded
Updated
</th>
<th data-priority="3">
Author
@@ -31,7 +31,7 @@
{% for element in data %}
<tr class="table-row">
<td>
{% if element.filename == default %}
{% if element.default %}
<div class="text-success" title="Default Dataset">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi success bi-caret-right-fill" viewBox="0 0 16 16">
<path d="m12.14 8.753-5.482 4.796c-.646.566-1.658.106-1.658-.753V3.204a1 1 0 0 1 1.659-.753l5.48 4.796a1 1 0 0 1 0 1.506z"/>
@@ -40,36 +40,63 @@
{% endif %}
</td>
<td>
{{ element.filename }}
{{ element.get_name() }}
</td>
<td>
{{ element.timestamp.strftime('%d %b %Y') }}
{{ element.date.strftime('%Y-%m-%d %H:%M') }}
</td>
<td>
{{ element.author }}
{{ element.creator.get_username() }}
</td>
<td>
{{ element.use }}
{{ element.tests|length }}
</td>
<td class="row-actions">
<a
href="#"
class="btn btn-primary edit-question-dataset {% if element.filename == default %}disabled{% endif %}"
data-filename="{{ element.filename }}"
data-action="default"
title="Make Default"
href="javascript:void(0)"
class="btn btn-success edit-question-dataset {% if not element.entries %} disabled {% endif %}"
data-id="{{ element.id }}"
data-action="analyse"
title="Analyse Answers"
>
<i class="bi bi-search button-icon"></i>
</a>
<a
href="javascript:void(0)"
class="btn btn-primary edit-question-dataset"
data-id="{{ element.id }}"
data-action="download"
title="Download Questions"
>
<i class="bi bi-cloud-arrow-down-fill button-icon"></i>
</a>
<a
href="javascript:void(0)"
class="btn btn-primary edit-question-dataset"
data-id="{{ element.id }}"
data-action="view"
title="View Questions"
>
<i class="bi bi-file-earmark-text-fill button-icon"></i>
</button>
</a>
<a
href="#"
class="btn btn-danger edit-question-dataset {% if element.filename == default %}disabled{% endif %}"
data-filename="{{ element.filename }}"
href="javascript:void(0)"
class="btn btn-primary edit-question-dataset"
data-id="{{ element.id }}"
data-action="edit"
title="Edit Questions"
>
<i class="bi bi-pencil-fill button-icon"></i>
</a>
<a
href="javascript:void(0)"
class="btn btn-danger edit-question-dataset {% if element.default %}disabled{% endif %}"
data-id="{{ element.id }}"
data-action="delete"
title="Delete Dataset"
title="Delete Questions"
>
<i class="bi bi-file-earmark-excel-fill button-icon"></i>
</button>
</a>
</td>
</tr>
{% endfor %}
@@ -78,13 +105,23 @@
{% else %}
<div class="alert alert-primary alert-db-empty">
<i class="bi bi-info-circle-fill" aria-title="Alert" title="Alert"></i>
There are no question datasets uploaded. Please use the panel below to upload a new question dataset.
There are no question datasets uploaded. Please use the panel below to upload a new question dataset or create a new dataset using the editor console.
</div>
{% endif %}
<div class="col text-center">
<button title="Create New" class="btn btn-md btn-primary btn-block create-new-dataset">
<i class="bi bi-cloud-plus-fill button-icon"></i>
Create New Dataset
</button>
</div>
<div class="form-container">
<form name="form-upload-questions" class="form-display" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="" enctype="multipart/form-data">
<h2 class="form-heading">Upload Question Dataset</h2>
{{ form.hidden_tag() }}
<div class="form-label-group">
{{ form.name(class_="form-control", autofocus=true, placeholder="Enter Name of Dataset") }}
{{ form.name.label }}
</div>
<div class="form-upload">
{{ form.data_file() }}
</div>
@@ -95,8 +132,8 @@
<div class="container form-submission-button">
<div class="row">
<div class="col text-center">
<button title="Create User" class="btn btn-md btn-success btn-block" type="submit">
<i class="bi bi-file-earmark-arrow-up-fill button-icon"></i>
<button title="Upload Dataset" class="btn btn-md btn-success btn-block" type="submit">
<i class="bi bi-cloud-arrow-up-fill button-icon"></i>
Upload Dataset
</button>
</div>
@@ -113,9 +150,9 @@
$('#question-datasets-table').DataTable({
'columnDefs': [
{'sortable': false, 'targets': [0,5]},
{'searchable': false, 'targets': [0,4,5]}
{'searchable': false, 'targets': [1,2,3]}
],
'order': [[2, 'desc'], [3, 'asc']],
'order': [[1, 'asc'], [2, 'desc'], [3, 'asc']],
'responsive': 'true',
'fixedHeader': 'true',
});
@@ -2,12 +2,12 @@
{% block content %}
<div class="form-container">
<form name="form-update-user" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin_views.users') }}">
<form name="form-update-user" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin._users') }}">
{% include "admin/components/server-alerts.html" %}
<h2 class="form-heading">Update User &lsquo;{{ user.username }}&rsquo;</h2>
<h2 class="form-heading">Update User &lsquo;{{ user.get_username() }}&rsquo;</h2>
{{ form.hidden_tag() }}
<div class="form-label-group">
{{ form.email(class_="form-control", placeholder="Email Address", value = user.email) }}
{{ form.email(class_="form-control", placeholder="Email Address", value = user.get_email()) }}
{{ form.email.label }}
</div>
<div class="form-label-group">
@@ -23,17 +23,17 @@
{{ form.notify.label }}
</div>
<div class="form-label-group">
Please confirm <strong>your password</strong> before committing any changes to a user account.
Please confirm <strong>your current password</strong> before committing any changes to a user account.
</div>
<div class="form-label-group">
{{ form.user_password(class_="form-control", placeholder="Your Password", value = user.email) }}
{{ form.user_password.label }}
{{ form.confirm_password(class_="form-control", placeholder="Your Password", value = user.email) }}
{{ form.confirm_password.label }}
</div>
{% include "admin/components/client-alerts.html" %}
<div class="container form-submission-button">
<div class="row">
<div class="col text-center">
<a href="{{ url_for('admin_views.users') }}" class="btn btn-md btn-danger btn-block" type="button">
<a href="{{ url_for('admin._users') }}" class="btn btn-md btn-danger btn-block" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-x-circle-fill" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>
</svg>
@@ -23,7 +23,7 @@
{% for user in users %}
<tr class="table-row">
<td>
{% if user._id == get_id_from_cookie() %}
{% if user == current_user %}
<div class="text-success" title="Current User">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi success bi-caret-right-fill" viewBox="0 0 16 16">
<path d="m12.14 8.753-5.482 4.796c-.646.566-1.658.106-1.658-.753V3.204a1 1 0 0 1 1.659-.753l5.48 4.796a1 1 0 0 1 0 1.506z"/>
@@ -32,18 +32,18 @@
{% endif %}
</td>
<td>
{{ user.username }}
{{ user.get_username() }}
</td>
<td>
{{ user.email }}
{{ user.get_email() }}
</td>
<td class="row-actions">
<a
href="
{% if not user._id == get_id_from_cookie() %}
{{ url_for('admin_views.update_user', _id = user._id ) }}
{% if not user == current_user %}
{{ url_for('admin._update_user', id = user.id ) }}
{% else %}
{{ url_for('admin_auth.account') }}
{{ url_for('admin._update_user', id=current_user.id) }}
{% endif %}
"
class="btn btn-primary"
@@ -53,15 +53,15 @@
</a>
<a
href="
{% if not user._id == get_id_from_cookie() %}
{{ url_for('admin_views.delete_user', _id = user._id ) }}
{% if not user == current_user %}
{{ url_for('admin._delete_user', id = user.id ) }}
{% else %}
#
{% endif %}
"
class="btn btn-danger {% if user._id == get_id_from_cookie() %} disabled {% endif %}"
class="btn btn-danger {% if user == current_user %} disabled {% endif %}"
title="Delete User"
{% if user._id == get_id_from_cookie() %} onclick="return false" {% endif %}
{% if user == current_user %} onclick="return false" {% endif %}
>
<i class="bi bi-person-x-fill button-icon"></i>
</button>
@@ -12,38 +12,33 @@
<h5 class="mb-1">Exam Code</h5>
</div>
<h2>
{{ '—'.join([test.test_code[:4], test.test_code[4:8], test.test_code[8:]]) }}
{{ test.get_code() }}
</h2>
</li>
<li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Dataset</h5>
</div>
{{ test.dataset }}
<a href="{{ url_for('view._view_console', id=test.dataset.id) }}">{{ test.dataset.get_name() }}</a>
</li>
<li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Created By</h5>
</div>
{{ test.creator }}
</li>
<li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Date Created</h5>
</div>
{{ test.date_created.strftime('%d %b %Y') }}
{{ test.creator.get_username() }}
</li>
<li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Start Date</h5>
</div>
{{ test.start_date.strftime('%d %b %Y') }}
{{ test.start_date.strftime('%d %b %Y %H:%M') if test.start_date else None }}
</li>
<li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Expiry Date</h5>
</div>
{{ test.expiry_date.strftime('%d %b %Y') }}
{{ test.end_date.strftime('%d %b %Y %H:%M') if test.end_date else None }}
</li>
<li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
@@ -62,7 +57,7 @@
{% endif %}
</li>
<div class="accordion" id="test-info-detail">
{% if 'entries' in test and test.entries|length > 0 %}
{% if test.entries|length > 0 %}
<div class="accordion-item">
<h2 class="accordion-header" id="test-entries">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#test-entries-list" aria-expanded="false" aria-controls="test-entries-list">
@@ -76,7 +71,7 @@
{% for entry in test.entries %}
<tr>
<td>
<a href="{{ url_for('admin_views.view_entry', _id=entry) }}" >Entry {{ loop.index }}</a>
<a href="{{ url_for('admin._view_entry', id=entry.id) }}" >Entry {{ loop.index }}</a>
</td>
</tr>
{% endfor %}
@@ -86,7 +81,7 @@
</div>
</div>
{% endif %}
{% if 'time_adjustments' in test and test.time_adjustments|length > 0 %}
{% if test.adjustments %}
<div class="accordion-item">
<h2 class="accordion-header" id="test-adjustments">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#test-adjustments-list" aria-expanded="false" aria-controls="test-adjustments-list">
@@ -110,10 +105,10 @@
</tr>
</thead>
<tbody>
{% for key, value in test.time_adjustments.items() %}
{% for key, value in test.adjustments.items() %}
<tr>
<td>
{{ key }}
{{ key.upper() }}
</td>
<td>
{{ value }}
@@ -143,7 +138,7 @@
<form name="form-add-adjustment" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="">
{{ form.hidden_tag() }}
<div class="form-label-group">
{{ form.time(class_="form-control", placeholder="Enter Username") }}
{{ form.time(class_="form-control", placeholder="Enter Time") }}
{{ form.time.label }}
</div>
<div class="container form-submission-button">
@@ -167,12 +162,29 @@
{% include "admin/components/client-alerts.html" %}
</div>
<div class="container justify-content-center">
<div class="row">
<a href="#" class="btn btn-warning test-action" data-action="close" data-_id="{{ test._id }}">
<i class="bi bi-hourglass button-icon"></i>
<div class="my-3 row">
{% if test.start_date <= now %}
<a href="#" class="btn btn-warning test-action {% if test.end_date < now %}disabled{% endif %}" data-action="end" data-id="{{ test.id }}">
<i class="bi bi-hourglass-bottom button-icon"></i>
Close Exam
</a>
<a href="#" class="btn btn-danger test-action" data-action="delete" data-_id="{{ test._id }}">
{% else %}
<a href="#" class="btn btn-success test-action {% if test.start_date < now %}disabled{% endif %}" data-action="start" data-id="{{ test.id }}">
<i class="bi bi-hourglass-top button-icon"></i>
Start Exam
</a>
{% endif %}
<a
href="#"
class="btn btn-success test-analyse test-action {% if not test.entries %} disabled {% endif %}"
data-id="{{test.id}}"
title="Analyse Exam"
data-action="analyse"
>
<i class="bi bi-search button-icon"></i>
Analyse Exam
</a>
<a href="#" class="btn btn-danger test-action" data-action="delete" data-id="{{ test.id }}">
<i class="bi bi-file-earmark-excel-fill button-icon"></i>
Delete Exam
</a>
@@ -33,13 +33,13 @@
{% for test in tests %}
<tr class="table-row">
<td>
{{ test.start_date.strftime('%d %b %Y') }}
{{ test.start_date.strftime('%Y-%m-%d %H:%M') }}
</td>
<td>
{{ '—'.join([test.test_code[:4], test.test_code[4:8], test.test_code[8:]]) }}
{{ test.get_code() }}
</td>
<td>
{{ test.expiry_date.strftime('%d %b %Y') }}
{{ test.end_date.strftime('%Y-%m-%d %H:%M') }}
</td>
<td>
{% if test.time_limit == None -%}
@@ -58,10 +58,19 @@
{{ test.entries|length }}
</td>
<td class="row-actions">
<a
href="#"
class="btn btn-success test-analyse test-action {% if not test.entries %} disabled {% endif %}"
data-id="{{test.id}}"
title="Analyse Exam"
data-action="analyse"
>
<i class="bi bi-search button-icon"></i>
</a>
<a
href="#"
class="btn btn-primary test-action"
data-_id="{{test._id}}"
data-id="{{test.id}}"
title="Edit Exam"
data-action="edit"
>
@@ -70,7 +79,7 @@
<a
href="#"
class="btn btn-danger test-action"
data-_id="{{test._id}}"
data-id="{{test.id}}"
title="Delete Exam"
data-action="delete"
>
+452
View File
@@ -0,0 +1,452 @@
from ..forms.admin import AddTimeAdjustment, CreateTest, CreateUser, DeleteUser, Login, Register, ResetPassword, UpdatePassword, UpdateUser, UploadData
from ..models import Dataset, Entry, Test, User
from ..tools.auth import disable_if_logged_in, require_account_creation
from ..tools.data import check_dataset_exists, check_is_json, validate_json
from ..tools.forms import get_dataset_choices, get_time_options, send_errors_to_client
from ..tools.logs import write
from ..tools.test import answer_options, get_correct_answers
from flask import abort, Blueprint, jsonify, render_template, request, send_file, session
from flask.helpers import abort, flash, redirect, url_for
from flask_login import current_user, login_required
from datetime import date, datetime, MINYEAR, timedelta
from json import loads
from os import path
import secrets
admin = Blueprint(
name='admin',
import_name=__name__,
template_folder='templates',
static_folder='static'
)
@admin.route('/')
@admin.route('/home/')
@admin.route('/dashboard/')
@login_required
def _home():
try:
tests = Test.query.all()
results = Entry.query.all()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
current_tests = [ test for test in tests if test.end_date >= datetime.now() and test.start_date.date() <= date.today() ]
current_tests.sort(key= lambda x: x.end_date or datetime(MINYEAR,1,1), reverse=True)
upcoming_tests = [ test for test in tests if test.start_date.date() > datetime.now().date()]
upcoming_tests.sort(key= lambda x: x.start_date or datetime(MINYEAR,1,1))
recent_results = [result for result in results if not result.status == 'started' ]
recent_results.sort(key= lambda x: x.end_time or datetime(MINYEAR,1,1), reverse=True)
return render_template('/admin/index.html', current_tests = current_tests, upcomimg_tests = upcoming_tests, recent_results = recent_results)
@admin.route('/settings/')
@login_required
def _settings():
try:
users = User.query.all()
datasets = Dataset.query.all()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
return render_template('/admin/settings/index.html', users=users, datasets=datasets)
@admin.route('/login/', methods=['GET','POST'])
@disable_if_logged_in
@require_account_creation
def _login():
form = Login()
if request.method == 'POST':
if form.validate_on_submit():
try: users = User.query.all()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
user = None
for _user in users:
if _user.get_username() == request.form.get('username').lower():
user = _user
break
if user:
if user.verify_password(request.form.get('password')):
user.login(remember=request.form.get('remember'))
return jsonify({'success': f'Successfully logged in.'}), 200
return jsonify({'error': f'The password you entered is incorrect.'}), 401
return jsonify({'error': f'The username you entered does not exist.'}), 401
return send_errors_to_client(form=form)
if 'remembered_username' in session: form.username.data = session.pop('remembered_username')
next = request.args.get('next')
return render_template('/admin/auth/login.html', form=form, next=next)
@admin.route('/logout/')
@login_required
def _logout():
current_user.logout()
return redirect(url_for('admin._login'))
@admin.route('/register/', methods=['GET','POST'])
@disable_if_logged_in
def _register():
from ..models.user import User
form = Register()
if request.method == 'POST':
if form.validate_on_submit():
new_user = User()
new_user.set_username(request.form.get('username').lower())
new_user.set_email(request.form.get('email').lower())
success, message = new_user.register(password=request.form.get('password'))
if success:
flash(message=f'{message} Please log in to continue.', category='success')
session['remembered_username'] = request.form.get('username').lower()
return jsonify({'success': message}), 200
flash(message=message, category='error')
return jsonify({'error': message}), 401
return send_errors_to_client(form=form)
return render_template('/admin/auth/register.html', form=form)
@admin.route('/reset/', methods=['GET','POST'])
def _reset():
form = ResetPassword()
if request.method == 'POST':
if form.validate_on_submit():
user = None
try: users = User.query.all()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
for _user in users:
if _user.get_username() == request.form.get('username'):
user = _user
break
if not user: return jsonify({'error': 'The user account does not exist.'}), 400
if not user.get_email() == request.form.get('email'): return jsonify({'error': 'The email address does not match the user account.'}), 400
return user.reset_password()
return send_errors_to_client(form=form)
token = request.args.get('token')
if token:
try: user = User.query.filter_by(reset_token=token).first()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
if not user: return redirect(url_for('admin._reset'))
verification_token = user.verification_token
user.clear_reset_tokens()
if request.args.get('verification') == verification_token:
form = UpdatePassword()
session['user'] = user.id
return render_template('/admin/auth/update-password.html', form=form)
flash('The verification of your password reset request failed and the token has been invalidated. Please make a new reset password request.', 'error')
return render_template('/admin/auth/reset.html', form=form)
@admin.route('/update_password/', methods=['POST'])
def _update_password():
form = UpdatePassword()
if form.validate_on_submit():
user = session.pop('user')
try: user = User.query.filter_by(id=user).first()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
user.update(password=request.form.get('password'))
session['remembered_username'] = user.get_username()
flash('Your password has been reset.', 'success')
return jsonify({'success':'Your password has been reset'}), 200
return send_errors_to_client(form=form)
@admin.route('/settings/users/', methods=['GET', 'POST'])
@login_required
def _users():
form = CreateUser()
try: users = User.query.all()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
if request.method == 'POST':
if form.validate_on_submit():
password = request.form.get('password')
password = secrets.token_hex(12) if not password else password
new_user = User()
new_user.set_username(request.form.get('username').lower())
new_user.set_email(request.form.get('email'))
success, message = new_user.register(notify=request.form.get('notify'), password=password)
if success: return jsonify({'success': message}), 200
return jsonify({'error': message}), 401
return send_errors_to_client(form=form)
return render_template('/admin/settings/users.html', form = form, users = users)
@admin.route('/settings/users/delete/<string:id>', methods=['GET', 'POST'])
@login_required
def _delete_user(id:str):
try: user = User.query.filter_by(id=id).first()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
form = DeleteUser()
if request.method == 'POST':
if not user: return jsonify({'error': 'User does not exist.'}), 400
if id == current_user.id: return jsonify({'error': 'Cannot delete your own account.'}), 400
if form.validate_on_submit():
password = request.form.get('password')
if not current_user.verify_password(password): return jsonify({'error': 'The password you entered is incorrect.'}), 401
success, message = user.delete(notify=request.form.get('notify'))
if success: return jsonify({'success': message}), 200
return jsonify({'error': message}), 400
return send_errors_to_client(form=form)
if id == current_user.id:
flash('Cannot delete your own user account.', 'error')
return redirect(url_for('admin._users'))
if not user:
flash('User not found.', 'error')
return redirect(url_for('admin._users'))
return render_template('/admin/settings/delete_user.html', form=form, id = id, user = user)
@admin.route('/settings/users/update/<string:id>', methods=['GET', 'POST'])
@login_required
def _update_user(id:str):
try: user = User.query.filter_by(id=id).first()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
form = UpdateUser()
if request.method == 'POST':
if not user: return jsonify({'error': 'User does not exist.'}), 400
if form.validate_on_submit():
if not current_user.verify_password(request.form.get('confirm_password')): return jsonify({'error': 'Invalid password for your account.'}), 401
success, message = user.update(
password = request.form.get('password'),
email = request.form.get('email'),
notify = request.form.get('notify')
)
if success:
flash(message, 'success')
return jsonify({'success': message}), 200
return jsonify({'error': message}), 400
return send_errors_to_client(form=form)
if not user:
flash('User not found.', 'error')
return redirect(url_for('admin._users'))
return render_template('/admin/settings/update_user.html', form=form, id = id, user = user)
@admin.route('/settings/questions/', methods=['GET', 'POST'])
@login_required
def _questions():
form = UploadData()
if request.method == 'POST':
if form.validate_on_submit():
upload = form.data_file.data
if not check_is_json(upload): return jsonify({'error': 'Invalid file. Please upload a JSON file.'}), 400
upload.stream.seek(0)
data = loads(upload.read())
if not validate_json(data=data): return jsonify({'error': 'The data in the file is invalid.'}), 400
new_dataset = Dataset()
new_dataset.set_name(request.form.get('name'))
success, message = new_dataset.create(
data = data,
default = request.form.get('default')
)
if success: return jsonify({'success': message}), 200
return jsonify({'error': message}), 400
return send_errors_to_client(form=form)
try: data = Dataset.query.all()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
return render_template('/admin/settings/questions.html', form=form, data=data)
@admin.route('/settings/questions/delete/', methods=['POST'])
@login_required
def _edit_questions():
id = request.get_json()['id']
action = request.get_json()['action']
if not action == 'delete': return jsonify({'error': 'Invalid action.'}), 400
try: dataset = Dataset.query.filter_by(id=id).first()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
if action == 'delete': success, message = dataset.delete()
if success: return jsonify({'success': message}), 200
return jsonify({'error': message}), 400
@admin.route('/settings/questions/download/<string:id>/')
@login_required
def _download(id:str):
try: dataset = Dataset.query.filter_by(id=id).first()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
if not dataset: return abort(404)
data_path = path.abspath(dataset.get_file())
return send_file(data_path, as_attachment=True, download_name=f'{dataset.get_name()}.json')
@admin.route('/tests/<string:filter>/', methods=['GET'])
@admin.route('/tests/', methods=['GET'])
@login_required
@check_dataset_exists
def _tests(filter:str=None):
tests = None
try: _tests = Test.query.all()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
form = None
now = datetime.now()
if filter not in ['create','active','scheduled','expired','all']: return redirect(url_for('admin._tests', filter='active'))
if filter == 'create':
form = CreateTest()
form.start_date.default = datetime.now()
form.expiry_date.default = date.today() + timedelta(days=1)
form.time_limit.choices = get_time_options()
form.dataset.choices = get_dataset_choices()
form.time_limit.default='none'
form.process()
display_title = ''
error_none = ''
if filter in [None, '', 'active']:
tests = [ test for test in _tests if test.end_date >= now and test.start_date <= now ]
display_title = 'Active Exams'
error_none = 'There are no exams that are currently active. You can create one using the Create Exam form.'
if filter == 'expired':
tests = [ test for test in _tests if test.end_date < now ]
display_title = 'Expired Exams'
error_none = 'There are no expired exams. Exams will appear in this category after their expiration date has passed.'
if filter == 'scheduled':
tests = [ test for test in _tests if test.start_date > now]
display_title = 'Scheduled Exams'
error_none = 'There are no scheduled exams pending. You can schedule an exam for the future using the Create Exam form.'
if filter == 'all':
tests = _tests
display_title = 'All Exams'
error_none = 'There are no exams set up. You can create one using the Create Exam form.'
return render_template('/admin/tests.html', form = form, tests=tests, display_title=display_title, error_none=error_none, filter=filter)
@admin.route('/tests/create/', methods=['POST'])
@login_required
def _create_test():
form = CreateTest()
form.dataset.choices = get_dataset_choices()
form.time_limit.choices = get_time_options()
if form.validate_on_submit():
new_test = Test()
new_test.start_date = request.form.get('start_date')
new_test.start_date = datetime.strptime(new_test.start_date, '%Y-%m-%dT%H:%M')
new_test.end_date = request.form.get('expiry_date')
new_test.end_date = datetime.strptime(new_test.end_date, '%Y-%m-%dT%H:%M')
new_test.time_limit = None if request.form.get('time_limit') == 'none' else int(request.form.get('time_limit'))
dataset = request.form.get('dataset')
try: new_test.dataset = Dataset.query.filter_by(id=dataset).first()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
success, message = new_test.create()
if success:
flash(message=message, category='success')
return jsonify({'success': message}), 200
return jsonify({'error': message}), 400
return send_errors_to_client(form=form)
@admin.route('/tests/edit/', methods=['POST'])
@login_required
def _edit_test():
id = request.get_json()['id']
action = request.get_json()['action']
if action not in ['start', 'delete', 'end']: return jsonify({'error': 'Invalid action.'}), 400
try: test = Test.query.filter_by(id=id).first()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
if not test: return jsonify({'error': 'Could not find the corresponding test to delete.'}), 404
if action == 'delete': success, message = test.delete()
if action == 'start': success, message = test.start()
if action == 'end': success, message = test.end()
if success:
flash(message=message, category='success')
return jsonify({'success': message}), 200
return jsonify({'error': message}), 400
@admin.route('/test/<string:id>/', methods=['GET','POST'])
@login_required
def _view_test(id:str=None):
form = AddTimeAdjustment()
try: test = Test.query.filter_by(id=id).first()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
if request.method == 'POST':
if not test: return jsonify({'error': 'Invalid test ID.'}), 404
if form.validate_on_submit():
time = int(request.form.get('time'))
success, message = test.add_adjustment(time)
if success: return jsonify({'success': message}), 200
return jsonify({'error': message}), 400
return jsonify({'error': form.time.errors }), 400
if not test:
flash('Invalid test ID.', 'error')
return redirect(url_for('admin._tests', filter='active'))
return render_template('/admin/test.html', test = test, form = form)
@admin.route('/test/<string:id>/delete-adjustment/', methods=['POST'])
@login_required
def _delete_adjustment(id:str=None):
try: test = Test.query.filter_by(id=id).first()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
if not test: return jsonify({'error': 'Invalid test ID.'}), 404
user_code = request.get_json()['user_code'].lower()
success, message = test.remove_adjustment(user_code)
if success: return jsonify({'success': message}), 200
return jsonify({'error': message}), 400
@admin.route('/results/')
@login_required
def _view_entries():
try: entries = Entry.query.all()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
return render_template('/admin/results.html', entries = entries)
@admin.route('/results/<string:id>/', methods = ['GET', 'POST'])
@login_required
def _view_entry(id:str=None):
try: entry = Entry.query.filter_by(id=id).first()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
if request.method == 'POST':
if not entry: return jsonify({'error': 'Invalid entry ID.'}), 404
action = request.get_json()['action']
if action not in ['validate', 'delete']: return jsonify({'error': 'Invalid action.'}), 400
if action == 'validate':
success, message = entry.validate()
if action == 'delete':
success, message = entry.delete()
if success:
flash(message, 'success')
entry.notify_result()
return jsonify({'success': message}), 200
return jsonify({'error': message}),400
if not entry:
flash('Invalid entry ID.', 'error')
return redirect(url_for('admin._view_entries'))
test = entry.test
data = test.dataset.get_data()
correct = get_correct_answers(dataset=data)
answers = answer_options(dataset=data)
return render_template('/admin/result-detail.html', entry = entry, correct = correct, answers = answers)
@admin.route('/certificate/',methods=['POST'])
@login_required
def _generate_certificate():
from ..extensions import db
id = request.get_json()['id']
try: entry = Entry.query.filter_by(id=id).first()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
if not entry: return jsonify({'error': 'Invalid entry ID.'}), 404
return render_template('/admin/components/certificate.html', entry = entry)
@@ -0,0 +1,8 @@
#alert-box {
margin: 30px auto;
max-width: 460px;
}
.cell-percentage::after {
content: '%';
}
+260
View File
@@ -0,0 +1,260 @@
body {
padding: 80px 0;
}
.site-footer {
background-color: lightgray;
font-size: small;
}
.site-footer p {
margin: 0;
}
.form-container {
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
padding-top: 40px;
padding-bottom: 40px;
}
.form-display {
width: 100%;
max-width: 420px;
padding: 15px;
margin: auto;
}
.form-heading {
margin-bottom: 2rem;
}
.form-label-group {
position: relative;
margin-bottom: 2rem;
}
.form-label-group input,
.form-label-group label {
padding: var(--input-padding-y) var(--input-padding-x);
font-size: 16pt;
}
.form-label-group label {
position: absolute;
top: 0;
left: 0;
display: block;
width: 100%;
margin-bottom: 0; /* Override default `<label>` margin */
line-height: 1.5;
color: #495057;
cursor: text; /* Match the input under the label */
border: 1px solid transparent;
border-radius: .25rem;
transition: all .1s ease-in-out;
z-index: -1;
}
.form-label-group input {
background-color: transparent;
border: none;
border-radius: 0%;
border-bottom: 2px solid #585858;
}
.form-label-group input:active, .form-label-group input:focus {
background-color: transparent;
}
.form-label-group input::-webkit-input-placeholder {
color: transparent;
}
.form-label-group input:-ms-input-placeholder {
color: transparent;
}
.form-label-group input::-ms-input-placeholder {
color: transparent;
}
.form-label-group input::-moz-placeholder {
color: transparent;
}
.form-label-group input::placeholder {
color: transparent;
}
.form-label-group input:not(:placeholder-shown) {
padding-top: calc(var(--input-padding-y) + var(--input-padding-y) * (2 / 3));
padding-bottom: calc(var(--input-padding-y) / 3);
}
.form-label-group input:not(:placeholder-shown) ~ label {
padding-top: calc(var(--input-padding-y) / 3);
padding-bottom: calc(var(--input-padding-y) / 3);
font-size: 12px;
color: #777;
}
.form-check {
margin-bottom: 2rem;
}
.checkbox input {
transform: scale(1.5);
margin-right: 1rem;
}
.signin-forgot-password {
font-size: 14pt;
}
.form-submission-button {
margin-bottom: 2rem;
}
.form-submission-button button, .form-submission-button a {
margin: 1rem;
vertical-align: middle;
}
.form-submission-button button span, .form-submission-button button svg, .form-submission-button a span, .form-submission-button a svg {
margin: 0 2px;
}
table.dataTable {
border-collapse: collapse;
width: 100%;
}
.table-row {
vertical-align: middle;
}
.row-actions {
text-align: center;
white-space: nowrap;
}
.dataTables_wrapper .dt-buttons {
left: 50%;
transform: translateX(-50%);
float:none;
text-align:center;
}
.row-actions button, .row-actions a {
margin: 0px 5px;
}
#cookie-alert {
padding-right: 16px;
}
#dismiss-cookie-alert {
margin-top: 16px;
width: fit-content;
}
.alert-db-empty {
width: 100%;
max-width: 720px;
font-size: 14pt;
margin: 20px auto;
}
.form-date-input, .form-select-input {
position: relative;
margin: 2rem 0;
}
.form-date-input input,
.form-date-input label, .form-select-input select, .form-select-input label {
padding: var(--input-padding-y) var(--input-padding-x);
font-size: 16pt;
width: 100%;
background-color: transparent;
border: none;
border-bottom: 2px solid #585858;
}
.datepicker::-webkit-calendar-picker-indicator {
border: 1px;
border-color: gray;
border-radius: 10%;
}
.form-date-input label, .form-select-input label {
/* position: absolute; */
/* top: 0;
left: 0; */
display: block;
width: 100%;
margin-bottom: 0; /* Override default `<label>` margin */
line-height: 1.5;
color: #495057;
cursor: text; /* Match the input under the label */
border: 1px solid transparent;
border-radius: .25rem;
transition: all .1s ease-in-out;
z-index: -1;
}
.button-icon {
font-size: 20px;
}
.form-upload {
margin: 2rem 0;
font-size: 14pt;
}
.result-action-buttons, .test-action {
margin: 5px auto;
width: fit-content;
}
.accordion-item {
background-color: unset;
}
/* Change Autocomplete styles in Chrome*/
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:focus,
select:-webkit-autofill,
select:-webkit-autofill:hover,
select:-webkit-autofill:focus {
transition: background-color 5000s ease-in-out 0s;
}
/* Fallback for Edge
-------------------------------------------------- */
@supports (-ms-ime-align: auto) {
.form-label-group label {
display: none;
}
.form-label-group input::-ms-input-placeholder {
color: #777;
}
}
/* Fallback for IE
-------------------------------------------------- */
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
.form-label-group label {
display: none;
}
.form-label-group input:-ms-input-placeholder {
color: #777;
}
}
@@ -0,0 +1,27 @@
// Analyse Button Listener
$('.button-analyse').click(function(event) {
let buttonClass = $(this).data('class')
let id = null
if (buttonClass == 'test' ) {
id = $('#select-test').children('option:selected').val()
} else if (buttonClass == 'dataset' ) {
id = $('#select-dataset').children('option:selected').val()
}
$.ajax({
url: `/admin/analysis/`,
type: 'POST',
data: JSON.stringify({'id': id, 'class': buttonClass}),
contentType: 'application/json',
success: function(response) {
window.location.href = response
},
error: function(response){
error_response(response)
},
})
event.preventDefault()
})
File diff suppressed because one or more lines are too long
+115
View File
@@ -0,0 +1,115 @@
// Menu Highlight Scripts
const menuItems = document.getElementsByClassName('nav-link')
for(let i = 0; i < menuItems.length; i++) {
if(menuItems[i].pathname == window.location.pathname) {
menuItems[i].classList.add('active')
}
}
const dropdownItems = document.getElementsByClassName('dropdown-item')
for(let i = 0; i< dropdownItems.length; i++) {
if(dropdownItems[i].pathname == window.location.pathname) {
dropdownItems[i].classList.add('active')
$( "#" + dropdownItems[i].id ).closest( '.dropdown' ).find('.dropdown-toggle').addClass('active')
}
}
// General Post Method Form Processing Script
$('form.form-post').submit(function(event) {
var $form = $(this)
var data = $form.serialize()
var url = $(this).prop('action')
var rel_success = $(this).data('rel-success')
$.ajax({
url: url,
type: 'POST',
data: data,
dataType: 'json',
success: function(response) {
if (response.redirect_to) {
window.location.href = response.redirect_to
}
else {
window.location.href = rel_success
}
},
error: function(response) {
error_response(response)
}
})
event.preventDefault()
})
function error_response(response) {
const $alert = $("#alert-box")
$alert.html('')
if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) {
$alert.html(`
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
${response.responseJSON.error}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`)
} else if (response.responseJSON.error instanceof Array) {
var output = ''
for (let i = 0; i < response.responseJSON.error.length; i ++) {
output += `
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
${response.responseJSON.error[i]}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`
$alert.html(output)
}
}
$alert.focus()
}
// Dismiss Cookie Alert
$('#dismiss-cookie-alert').click(function(event){
$.ajax({
url: '/cookies/',
type: 'POST',
data: {
time: Date.now()
},
dataType: 'json',
success: function(response){
console.log(response)
},
error: function(response){
console.log(response)
}
})
event.preventDefault()
})
// Create New Dataset
$('.create-new-dataset').click(function(event){
$.ajax({
url: '/api/editor/new/',
type: 'POST',
data: {
time: Date.now()
},
dataType: 'json',
success: function(response){
if (response.redirect_to) {
window.location.href = response.redirect_to
}
},
error: function(response){
console.log(response)
}
})
event.preventDefault()
})
@@ -0,0 +1,170 @@
{% extends "analysis/components/datatable.html" %}
{% block style %}
<link
rel="stylesheet"
href="{{ url_for('.static', filename='css/analysis.css') }}"
/>
{% endblock %}
{% block content %}
<h1>Analysis</h1>
<div class="container">
<p class="lead">
Analysis for {{ type }} {{ subject }}.
</p>
</div>
<div class="container">
<h3>
Question List
</h3>
<div class="container dataset-metadata">
<div class="input-group mb-3">
<span class="input-group-text">Dataset Name</span>
<span class="form-control">
{{ dataset.get_name() }}
</span>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Author</span>
<span class="form-control">
{{ dataset.creator.get_username() }}
</span>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Last Updated</span>
<span class="form-control">
{{ dataset.date.strftime('%d %b %Y %H:%M') }}
</span>
</div>
{% if dataset.default %}
<div class="input-group mb-3">
<span class="input-group-text">
<input type="checkbox" aria-label="Default" class="dataset-default" checked disabled>
</span>
<span class="form-control">
Default Dataset
</select>
</div>
{% endif %}
</div>
<div class="container">
<table id="analysis-table" class="table table-striped" style="width:100%">
<thead>
<th data-priority="1">
Question
</th>
<th data-priority="1">
Percent Correct
</th>
<th data-priority="2">
Answers
</th>
<th data-priority="3">
Tags
</th>
</thead>
<tbody>
{% for question in questions %}
<tr class="table-row">
<td>
{{ question.q_no + 1 }}
</td>
<td class="cell-percentage">
{{ ((analysis.answers[question.q_no][question.correct] or 0)*100/(analysis.answers[question.q_no].values())|sum())|round(2) }}
</td>
<td>
<table style="width:100%">
{% for option in question.options %}
<tr>
<td style="width:50%">
{{ option[1] }}
</td>
<td>
{% if question.correct == option[0] %}
<div class="progress">
<div class="progress-bar bg-success progress-bar-striped" role="progressbar" style="width: {{ (analysis.answers[question.q_no][option[0]] or 0)*100/(analysis.answers[question.q_no].values())|sum() }}%;" aria-valuenow="{{ (analysis.answers[question.q_no][option[0]] or 0)*100/(analysis.answers[question.q_no].values())|sum() }}" aria-valuemin="0" aria-valuemax="100">{{ ((analysis.answers[question.q_no][option[0]] or 0)*100/(analysis.answers[question.q_no].values())|sum())|round(2) }}%</div>
</div>
{% else %}
<div class="progress">
<div class="progress-bar bg-danger progress-bar-striped" role="progressbar" style="width: {{ (analysis.answers[question.q_no][option[0]] or 0)*100/(analysis.answers[question.q_no].values())|sum() }}%;" aria-valuenow="{{ (analysis.answers[question.q_no][option[0]] or 0)*100/(analysis.answers[question.q_no].values())|sum() }}" aria-valuemin="0" aria-valuemax="100">{{ ((analysis.answers[question.q_no][option[0]] or 0)*100/(analysis.answers[question.q_no].values())|sum())|round(2) }}%</div>
</div>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</td>
<td>
<ul>
{% for tag in question.tags %}
<li>{{ tag|safe }}</li>
{% endfor %}
</ul>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
{% block script %}
<script>
const target = "{{ url_for('api._editor') }}"
const id = "{{ dataset.id }}"
</script>
<script
type="text/javascript"
src="{{ url_for('.static', filename='js/analysis.js') }}"
></script>
{% endblock %}
{% block custom_data_script %}
<script>
console.log($('#analysis-table'))
$(document).ready(function() {
$('#analysis-table').DataTable({
'searching': true,
'columnDefs': [
{'sortable': true, 'targets': [0,1]},
{'sortable': false, 'targets': [2,3]},
{'searchable': true, 'targets': [0,2,3]}
],
'order': [[0, 'asc'], [1, 'desc']],
'buttons': [
{
extend: 'print',
exportOptions: {
columns: [0, 1, 2, 3]
}
},
{
extend: 'excel',
exportOptions: {
columns: [0, 1, 2, 3]
}
},
{
extend: 'pdf',
exportOptions: {
columns: [0, 1, 2, 3]
}
}
],
'responsive': 'true',
'colReorder': 'true',
'fixedHeader': 'true',
'searchBuilder': {
depthLimit: 2,
columns: [2, 3],
},
dom: 'BQlfrtip'
});
// $('.buttons-pdf').html('<span class="glyphicon glyphicon-file" data-toggle="tooltip" title="Export To Excel"/>') -->
} );
$('#analysis-table').show();
$(window).trigger('resize');
</script>
{% endblock %}
@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
crossorigin="anonymous">
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.1/font/bootstrap-icons.css">
<link
rel="stylesheet"
href="{{ url_for('.static', filename='css/style.css') }}"
/>
<link
rel="stylesheet"
href="{{ url_for('.static', filename='css/view.css') }}"
/>
{% block style %}
{% endblock %}
<title>{% block title %} SKA Referee Test | Admin Console {% endblock %}</title>
{% include "analysis/components/og-meta.html" %}
</head>
<body class="bg-light">
{% block navbar %}
{% include "analysis/components/navbar.html" %}
{% endblock %}
<div class="container">
{% block top_alerts %}
{% include "analysis/components/server-alerts.html" %}
{% endblock %}
{% block content %}{% endblock %}
</div>
<footer class="container site-footer mt-5">
{% block footer %}
{% include "analysis/components/footer.html" %}
{% endblock %}
</footer>
<!-- JQuery, Popper, and Bootstrap js dependencies -->
<script
src="https://code.jquery.com/jquery-3.6.0.min.js"
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
crossorigin="anonymous">
</script>
<script>
window.jQuery || document.write(`<script src="{{ url_for('.static', filename='js/jquery-3.6.0.min.js') }}"><\/script>`)
</script>
<script
src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.10.2/dist/umd/popper.min.js"
integrity="sha384-7+zCNj/IqJ95wo16oMtfsKbZ9ccEh31eOz1HGyDuCQ6wgnyJNSYdrPa03rtR1zdB"
crossorigin="anonymous">
</script>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.min.js"
integrity="sha384-QJHtvGhmr9XOIpI6YVutG+2QOK9T+ZnN4kzFN1RtK3zEFEIsxhlmWl5/YESvpZ13"
crossorigin="anonymous"
></script>
<!-- Custom js -->
<script type="text/javascript">
var csrf_token = "{{ csrf_token() }}";
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrf_token);
}
}
});
</script>
<script
type="text/javascript"
src="{{ url_for('.static', filename='js/script.js') }}"
></script>
<script
type="text/javascript"
src="{{ url_for('.static', filename='js/analysis.js') }}"
></script>
{% block script %}
{% endblock %}
{% block datatable_scripts %}
{% endblock %}
{% block custom_data_script %}
{% endblock %}
</body>
</html>
@@ -0,0 +1 @@
<div id="alert-box" tabindex="-1"></div>
@@ -0,0 +1,28 @@
{% extends "analysis/components/base.html" %}
{% block datatable_css %}
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.11.3/css/dataTables.bootstrap5.min.css"/>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/buttons/2.0.1/css/buttons.bootstrap5.min.css"/>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/colreorder/1.5.5/css/colReorder.bootstrap5.min.css"/>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/fixedheader/3.2.0/css/fixedHeader.bootstrap5.min.css"/>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/keytable/2.6.4/css/keyTable.bootstrap5.min.css"/>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/responsive/2.2.9/css/responsive.bootstrap5.min.css"/>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/searchbuilder/1.3.0/css/searchBuilder.dataTables.min.css"/>
{% endblock %}
{% block datatable_scripts %}
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jszip/2.5.0/jszip.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.36/pdfmake.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.36/vfs_fonts.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/1.11.3/js/jquery.dataTables.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/1.11.3/js/dataTables.bootstrap5.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/dataTables.buttons.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/buttons.bootstrap5.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/buttons.colVis.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/buttons.html5.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/buttons.print.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/colreorder/1.5.5/js/dataTables.colReorder.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/fixedheader/3.2.0/js/dataTables.fixedHeader.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/keytable/2.6.4/js/dataTables.keyTable.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/responsive/2.2.9/js/dataTables.responsive.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/responsive/2.2.9/js/responsive.bootstrap5.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/searchbuilder/1.3.0/js/dataTables.searchBuilder.min.js"></script>
{% endblock %}
@@ -0,0 +1,2 @@
<p>This web app was developed and is maintained by Vivek Santayana. The source code for the web app, excluding any data pertaining to the questions in the quiz, is freely available at <a href="https://git.vsnt.uk/viveksantayana/ska-referee-test">Vivek&rsquo;s personal GIT repository</a> under an MIT License.</p>
<p>All questions in the test are &copy; The Scottish Korfball Association {{ now.year }}. All rights are reserved.</p>
@@ -0,0 +1,4 @@
{% extends "analysis/components/base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block top_alerts %}
{% endblock %}
@@ -0,0 +1,137 @@
<nav class="navbar fixed-top navbar-expand-md navbar-dark bg-dark">
<div class="container">
<a href="{{ url_for('admin._home') }}" class="navbar-brand mb-0 h1">RefTest | Admin</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbar"
aria-controls="navbar"
aria-expanded="false"
aria-label="Toggle Navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse justify-content-end" id="navbar">
<ul class="navbar-nav">
{% if not current_user.is_authenticated %}
<li class="nav-item" id="nav-login">
<a href="{{ url_for('admin._login') }}" id="link-login" class="nav-link">Log In</a>
</li>
{% endif %}
{% if current_user.is_authenticated %}
<li class="nav-item dropdown" id="nav-results">
<a
class="nav-link dropdown-toggle"
id="dropdown-results"
role="button"
href="{{ url_for('admin._view_entries') }}"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Results
</a>
<ul
class="dropdown-menu"
aria-labelledby="dropdown-results"
>
<li>
<a href="{{ url_for('admin._view_entries', filter='active') }}" id="link-results" class="dropdown-item">View Results</a>
</li>
<li>
<a href="{{ url_for('analysis._analysis') }}" id="link-analysis" class="dropdown-item">Analysis</a>
</li>
</ul>
</li>
<li class="nav-item dropdown" id="nav-tests">
<a
class="nav-link dropdown-toggle"
id="dropdown-tests"
role="button"
href="{{ url_for('admin._tests') }}"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Exams
</a>
<ul
class="dropdown-menu"
aria-labelledby="dropdown-tests"
>
<li>
<a href="{{ url_for('admin._tests', filter='active') }}" id="link-active" class="dropdown-item">Active</a>
</li>
<li>
<a href="{{ url_for('admin._tests', filter='scheduled') }}" id="link-scheduled" class="dropdown-item">Scheduled</a>
</li>
<li>
<a href="{{ url_for('admin._tests', filter='expired') }}" id="link-expired" class="dropdown-item">Expired</a>
</li>
<li>
<a href="{{ url_for('admin._tests', filter='all') }}" id="link-all" class="dropdown-item">All</a>
</li>
<li>
<a href="{{ url_for('admin._tests', filter='create') }}" id="link-create" class="dropdown-item">Create</a>
</li>
</ul>
</li>
<li class="nav-item dropdown" id="nav-settings">
<a
class="nav-link dropdown-toggle"
id="dropdown-settings"
role="button"
href="{{ url_for('admin._settings') }}"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Settings
</a>
<ul
class="dropdown-menu"
aria-labelledby="dropdown-settings"
>
<li>
<a href="{{ url_for('admin._settings') }}" id="link-settings" class="dropdown-item">View Settings</a>
</li>
<li>
<a href="{{ url_for('admin._users') }}" id="link-users" class="dropdown-item">Users</a>
</li>
<li>
<a href="{{ url_for('admin._questions') }}" id="link-questions" class="dropdown-item">Manage Questions</a>
</li>
<li>
<a href="{{ url_for('view._view') }}" id="link-editor" class="dropdown-item">View Questions</a>
</li>
<li>
<a href="{{ url_for('editor._editor') }}" id="link-editor" class="dropdown-item">Edit Questions</a>
</li>
</ul>
</li>
<li class="nav-item dropdown" id="nav-account">
<a
class="nav-link dropdown-toggle"
id="dropdown-account"
role="button"
href="{{ url_for('admin._update_user', id=current_user.id) }}"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Account
</a>
<ul
class="dropdown-menu"
aria-labelledby="dropdown-account"
>
<li>
<a href="{{ url_for('admin._update_user', id=current_user.id) }}" id="link-account" class="dropdown-item">Account Settings</a>
</li>
<li>
<a href="{{ url_for('admin._logout') }}" id="link-logout" class="dropdown-item">Log Out</a>
</li>
</ul>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
@@ -2,16 +2,17 @@
<meta property="og:locale" content="en_UK" />
<meta property="og:type" content="website" />
<meta property="og:description" content="A web app for taking the Scottish Korfball Association Refereeing Theory Exam on-line." />
<meta property="og:url" content="{{ url_for(request.endpoint, _external = True, **request.view_args) }}" />
<meta property="og:url" content="{{ url_for(request.endpoint, _external = True, **(request.view_args or {})) }}" />
<meta property="og:site_name" content="Scottish Korfball Association Referee Theory Exam" />
<meta property="og:image" content="{{ url_for('static', filename='favicon.png', _external = True) }}" />
<meta property="og:image" content="{{ url_for('.static', filename='favicon.png', _external = True) }}" />
<meta property="og:image:alt" content="Logo of the SKA Refereeing Exam App" />
<meta property="og:image:width" content="512" />
<meta property="og:image:height" content="512" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:description" content="A web app for taking the Scottish Korfball Association Refereeing Theory Exam on-line." />
<meta name="twitter:image" content="{{ url_for('static', filename='favicon.png', _external = True) }}" />
<meta name="twitter:image" content="{{ url_for('.static', filename='favicon.png', _external = True) }}" />
<meta name="twitter:image:alt" content="Logo of the SKA Refereeing Exam App" />
<meta name="twitter:creator" content="@viveksantayana" />
<meta name="twitter:site" content="@viveksantayana" />
<meta name="theme-color" content="#343a40" />
<link rel="shortcut icon" href="{{ url_for('.static', filename='favicon.ico') }}">
@@ -0,0 +1,23 @@
<div class="navbar navbar-expand-sm navbar-light bg-light">
<div class="container-fluid">
<div class="expand navbar-expand justify-content-center" id="navbar_secondary">
<ul class="nav nav-pills">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='active') }}">Active</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='scheduled') }}">Scheduled</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='expired') }}">Expired</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='all') }}">All</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='create') }}">Create</a>
</li>
</ul>
</div>
</div>
</div>
@@ -25,8 +25,10 @@
<div class="alert alert-primary alert-dismissible fade show" id="cookie-alert" role="alert">
<i class="bi bi-info-circle-fill" title="Cookie Alert" aria-title="Cookie Alert"></i>
{{ message|safe }}
<div class="d-flex justify-content-center w-100">
<button type="button" id="dismiss-cookie-alert" class="btn btn-success" data-bs-dismiss="alert" aria-label="Close">Accept</button>
</div>
</div>
{% set cookie_flash_flag.value = True %}
{% endif %}
{% else %}
@@ -0,0 +1,54 @@
{% extends "analysis/components/input-forms.html" %}
{% block content %}
<h1>Analysis</h1>
<div class="container">
<div class="row">
<div class="col-sm">
<div class="card m-3">
<div class="card-body">
<h5 class="card-title">Exams</h5>
<div class="card-text">
<div class="form-select-input">
<select name="select-test" id="select-test">
{% for test in tests %}
<option value="{{ test.id }}">{{ test.get_code() }}</option>
{% endfor %}
</select>
</div>
<div class="my-3">
<a href="{{ url_for('analysis._test') }}" class="btn btn-primary button-analyse" data-class="test">
<i class="bi bi-search button-icon"></i>
Analyse
</a>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm">
<div class="card m-3">
<div class="card-body">
<h5 class="card-title">Datasets</h5>
<div class="card-text">
<div class="form-select-input">
<select name="select-dataset" id="select-dataset">
{% for dataset in datasets %}
<option value="{{ dataset.id }}">{{ dataset.get_name() }}</option>
{% endfor %}
</select>
</div>
<div class="my-3">
<a href="{{ url_for('analysis._dataset') }}" class="btn btn-primary button-analyse" data-class="dataset">
<i class="bi bi-search button-icon"></i>
Analyse
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% include "analysis/components/client-alerts.html" %}
{% endblock %}
+85
View File
@@ -0,0 +1,85 @@
from ..models import Dataset, Test
from ..tools.data import analyse, check_dataset_exists, check_test_exists
from ..tools.logs import write
from ..tools.data import parse_questions
from flask import Blueprint, render_template, request, jsonify
from flask.helpers import abort, flash, redirect, url_for
from flask_login import login_required
analysis = Blueprint(
name='analysis',
import_name=__name__,
template_folder='templates',
static_folder='static'
)
@analysis.route('/', methods=['GET','POST'])
@login_required
@check_dataset_exists
@check_test_exists
def _analysis():
try:
_tests = Test.query.all()
_datasets = Dataset.query.all()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
tests = [ test for test in _tests if test.entries ]
datasets = [ dataset for dataset in _datasets if dataset.entries ]
if request.method == 'POST':
selection = request.get_json()
if selection['class'] == 'test':
try:
test = Test.query.filter_by(id=selection['id']).first()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
if not test: return jsonify({'error': 'Invalid entry ID.'}), 404
return url_for('analysis._test', id=selection['id']), 200
if selection['class'] == 'dataset':
try:
dataset = Dataset.query.filter_by(id=selection['id']).first()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
if not dataset: return jsonify({'error': 'Invalid entry ID.'}), 404
return url_for('analysis._dataset', id=selection['id']), 200
return jsonify({'error': 'Invalid entry ID.'}), 404
return render_template('/analysis/index.html', tests=tests, datasets=datasets)
@analysis.route('/test/<string:id>')
@analysis.route('/test/')
@login_required
@check_test_exists
def _test(id:str=None):
if id in [None, '']:
flash(message='Please select a valid exam.', category='error')
return redirect(url_for('analysis._analysis'))
try:
test = Test.query.filter_by(id=id).first()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
if not test:
flash('Invalid exam.', 'error')
return redirect(url_for('analysis._analysis'))
return render_template('/analysis/analysis.html', analysis=analyse(test), subject=test.get_code(), type='exam', dataset=test.dataset, questions=parse_questions(test.dataset.get_data()))
@analysis.route('/dataset/<string:id>')
@analysis.route('/dataset/')
@login_required
@check_dataset_exists
def _dataset(id:str=None):
if id in [None, '']:
flash(message='Please select a valid dataset.', category='error')
return redirect(url_for('analysis._analysis'))
try:
dataset = Dataset.query.filter_by(id=id).first()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
if not dataset:
flash('Invalid dataset.', 'error')
return redirect(url_for('analysis._analysis'))
return render_template('/analysis/analysis.html', analysis=analyse(dataset), subject=dataset.get_name(), type='dataset', dataset=dataset, questions=parse_questions(dataset.get_data()))
+115
View File
@@ -0,0 +1,115 @@
from ..models import Dataset, Entry, User
from ..tools.data import validate_json
from ..tools.logs import write
from ..tools.test import evaluate_answers, generate_questions
from flask import Blueprint, jsonify, request
from flask.helpers import abort, flash, url_for
from flask_login import login_required
from datetime import datetime, timedelta
from json import loads
api = Blueprint(
name='api',
import_name=__name__
)
@api.route('/questions/', methods=['POST'])
def _fetch_questions():
id = request.get_json()['id']
try: entry = Entry.query.filter_by(id=id).first()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
if not entry: return jsonify({'error': 'Invalid entry ID.'}), 400
test = entry.test
user_code = entry.user_code
time_limit = test.time_limit
time_adjustment = 0
if time_limit:
_time_limit = int(time_limit)
if user_code:
time_adjustment = test.adjustments[user_code]
_time_limit += time_adjustment
end_delta = timedelta(minutes=_time_limit)
end_time = datetime.now() + end_delta
else:
end_time = None
entry.start()
dataset = test.dataset
success, message = dataset.check_file()
if not success: return jsonify({'error': message}), 500
data_path = dataset.get_file()
with open(data_path, 'r') as data_file:
data = loads(data_file.read())
questions = generate_questions(dataset=data, randomise=False)
return jsonify({
'time_limit': end_time,
'questions': questions,
'start_time': entry.start_time,
'time_adjustment': time_adjustment
}), 200
@api.route('/submit/', methods=['POST'])
def _submit_quiz():
id = request.get_json()['id']
answers = request.get_json()['answers']
try: entry = Entry.query.filter_by(id=id).first()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
if not entry: return jsonify({'error': 'Unrecognised Entry.'}), 400
test = entry.test
dataset = test.dataset
success, message = dataset.check_file()
if not success: return jsonify({'error': message}), 500
data_path = dataset.get_file()
with open(data_path, 'r') as data_file:
data = loads(data_file.read())
result = evaluate_answers(answers=answers, key=data)
entry.complete(answers=answers, result=result)
return jsonify({
'success': 'Your submission has been processed. Redirecting you to receive your results.',
'id': id
}), 200
@api.route('/editor/', methods=['POST'])
@login_required
def _editor(id:str=None):
request_data = request.get_json()
id = request_data['id']
try: dataset = Dataset.query.filter_by(id=id).first()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
if not dataset: return jsonify({'error': 'Invalid request. Dataset not found.'}), 404
data_path = dataset.get_file()
if request_data['action'] == 'fetch':
with open(data_path, 'r') as data_file:
data = loads(data_file.read())
return jsonify({'success': 'Successfully downloaded dataset', 'data': data}), 200
default = request_data['default']
creator = request_data['creator']
try: user = User.query.filter_by(id=creator).first()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
name = request_data['name']
data = request_data['data']
if not validate_json(data): return jsonify({'error': 'The data you submitted was invalid.'}), 400
dataset.set_name(name)
dataset.creator = user
success, message = dataset.update(data=data, default=default)
if not success: return jsonify({'error': message}), 400
return jsonify({'success': message}), 200
@api.route('/editor/new/', methods=['POST'])
@login_required
def _editor_new():
new_dataset = Dataset()
new_dataset.set_name('New Dataset')
success, message = new_dataset.create(data=[], default=False)
if not success: return jsonify({'error':message}), 400
flash(message, 'success')
return jsonify({'success': message, 'redirect_to': url_for('editor._editor_console', id=new_dataset.id)}), 200
+60
View File
@@ -0,0 +1,60 @@
import os
from pathlib import Path
from dotenv import load_dotenv
load_dotenv('../.env')
class Config(object):
"""Basic App Configuration"""
APP_HOST = '0.0.0.0'
DATA = './data/'
DEBUG = False
TESTING = False
SECRET_KEY = os.getenv('SECRET_KEY')
SERVER_NAME = os.getenv('SERVER_NAME')
SESSION_COOKIE_SECURE = True
WTF_CSRF_TIME_LIMIT = None
"""Email Engine Configuration"""
MAIL_SERVER = os.getenv('MAIL_SERVER')
MAIL_PORT = int(os.getenv('MAIL_PORT') or 25)
MAIL_USE_TLS = False
MAIL_USE_SSL = False
MAIL_DEBUG = False
MAIL_USERNAME = os.getenv('MAIL_USERNAME')
MAIL_PASSWORD = os.getenv('MAIL_PASSWORD')
MAIL_DEFAULT_SENDER = os.getenv('MAIL_DEFAULT_SENDER')
MAIL_MAX_EMAILS = int(os.getenv('MAIL_MAX_EMAILS') or 25)
MAIL_SUPPRESS_SEND = False
MAIL_ASCII_ATTACHMENTS = bool(os.getenv('MAIL_ASCII_ATTACHMENTS') or True)
"""Database Driver Configuration"""
DATABASE_TYPE = os.getenv('DATABASE_TYPE') or 'SQLite'
SQLALCHEMY_TRACK_MODIFICATIONS = False
if DATABASE_TYPE.lower() == 'mysql' and os.getenv('MYSQL_DATABASE') and os.getenv('MYSQL_USER') and os.getenv('MYSQL_PASSWORD'):
DATABASE_HOST = os.getenv('DATABASE_HOST') or 'localhost'
DATABASE_PORT = int(os.getenv('DATABASE_PORT') or 3306)
MYSQL_DATABASE = os.getenv('MYSQL_DATABASE')
MYSQL_USER = os.getenv('MYSQL_USER')
MYSQL_PASSWORD = os.getenv('MYSQL_PASSWORD')
SQLALCHEMY_DATABASE_URI = f'mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{MYSQL_DATABASE}'
else: SQLALCHEMY_DATABASE_URI = f'sqlite:///{Path(os.path.abspath(f"{DATA}/db.sqlite"))}'
class Production(Config):
pass
class Development(Config):
APP_HOST = '127.0.0.1'
DEBUG = True
SERVER_NAME = '127.0.0.1:5000'
SESSION_COOKIE_SECURE = False
MAIL_SERVER = 'localhost'
MAIL_DEBUG = True
MAIL_SUPPRESS_SEND = False
class Testing(Development):
TESTING = True
SESSION_COOKIE_SECURE = False
MAIL_SERVER = os.getenv('MAIL_SERVER')
MAIL_DEBUG = True
MAIL_SUPPRESS_SEND = False
+103
View File
@@ -0,0 +1,103 @@
.accordion-button {
color: inherit;
background-color: inherit;
display: block;
border: 1px solid rgb(0 0 0 / .3);
height: 60px;
}
.editor-controls {
width: fit-content;
display: block;
margin: 10px auto;
}
.editor-controls a {
margin: 10px 10px;
}
.editor-controls a i {
font-size: larger;
margin: 2px;
}
.option-controls, .block-controls {
width: fit-content;
display: block;
margin: 10px auto;
}
.option-controls a, .block-controls a {
margin: 0 10px;
z-index: 10;
}
.option-controls a i, .block-controls a i {
font-size: larger;
margin: 2px;
}
.accordion-button div {
margin: 0;
position: relative;
top: 50%;
transform: translate(0, -50%);
}
.accordion-button::after {
content: none;
}
.accordion-error {
background-color: #bb2d3b;
color: white;
}
.accordion-error:not(.collapsed) {
background-color: #bb2d3b;
color: white;
}
.panel-button {
padding: 6px;
margin: 0px 2px;
}
.panel-button i {
font-size: larger;
}
.editor-panel, .info-panel {
margin: 30pt auto;
}
.info-panel, .viewer-panel {
display: none;
}
.control-panel {
margin-left: auto;
margin-right: 0;
width:fit-content;
}
#alert-box {
margin: 30px auto;
max-width: 460px;
}
.block {
border: 2px solid black;
border-radius: 10px;
margin: 10px;
padding: 5px;
}
.question-body, .question-block {
padding: 0px 2em;
}
blockquote {
padding: 0px 2em;
font-style: italic;
}
+260
View File
@@ -0,0 +1,260 @@
body {
padding: 80px 0;
}
.site-footer {
background-color: lightgray;
font-size: small;
}
.site-footer p {
margin: 0;
}
.form-container {
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
padding-top: 40px;
padding-bottom: 40px;
}
.form-display {
width: 100%;
max-width: 420px;
padding: 15px;
margin: auto;
}
.form-heading {
margin-bottom: 2rem;
}
.form-label-group {
position: relative;
margin-bottom: 2rem;
}
.form-label-group input,
.form-label-group label {
padding: var(--input-padding-y) var(--input-padding-x);
font-size: 16pt;
}
.form-label-group label {
position: absolute;
top: 0;
left: 0;
display: block;
width: 100%;
margin-bottom: 0; /* Override default `<label>` margin */
line-height: 1.5;
color: #495057;
cursor: text; /* Match the input under the label */
border: 1px solid transparent;
border-radius: .25rem;
transition: all .1s ease-in-out;
z-index: -1;
}
.form-label-group input {
background-color: transparent;
border: none;
border-radius: 0%;
border-bottom: 2px solid #585858;
}
.form-label-group input:active, .form-label-group input:focus {
background-color: transparent;
}
.form-label-group input::-webkit-input-placeholder {
color: transparent;
}
.form-label-group input:-ms-input-placeholder {
color: transparent;
}
.form-label-group input::-ms-input-placeholder {
color: transparent;
}
.form-label-group input::-moz-placeholder {
color: transparent;
}
.form-label-group input::placeholder {
color: transparent;
}
.form-label-group input:not(:placeholder-shown) {
padding-top: calc(var(--input-padding-y) + var(--input-padding-y) * (2 / 3));
padding-bottom: calc(var(--input-padding-y) / 3);
}
.form-label-group input:not(:placeholder-shown) ~ label {
padding-top: calc(var(--input-padding-y) / 3);
padding-bottom: calc(var(--input-padding-y) / 3);
font-size: 12px;
color: #777;
}
.form-check {
margin-bottom: 2rem;
}
.checkbox input {
transform: scale(1.5);
margin-right: 1rem;
}
.signin-forgot-password {
font-size: 14pt;
}
.form-submission-button {
margin-bottom: 2rem;
}
.form-submission-button button, .form-submission-button a {
margin: 1rem;
vertical-align: middle;
}
.form-submission-button button span, .form-submission-button button svg, .form-submission-button a span, .form-submission-button a svg {
margin: 0 2px;
}
table.dataTable {
border-collapse: collapse;
width: 100%;
}
.table-row {
vertical-align: middle;
}
.row-actions {
text-align: center;
white-space: nowrap;
}
.dataTables_wrapper .dt-buttons {
left: 50%;
transform: translateX(-50%);
float:none;
text-align:center;
}
.row-actions button, .row-actions a {
margin: 0px 5px;
}
#cookie-alert {
padding-right: 16px;
}
#dismiss-cookie-alert {
margin-top: 16px;
width: fit-content;
}
.alert-db-empty {
width: 100%;
max-width: 720px;
font-size: 14pt;
margin: 20px auto;
}
.form-date-input, .form-select-input {
position: relative;
margin: 2rem 0;
}
.form-date-input input,
.form-date-input label, .form-select-input select, .form-select-input label {
padding: var(--input-padding-y) var(--input-padding-x);
font-size: 16pt;
width: 100%;
background-color: transparent;
border: none;
border-bottom: 2px solid #585858;
}
.datepicker::-webkit-calendar-picker-indicator {
border: 1px;
border-color: gray;
border-radius: 10%;
}
.form-date-input label, .form-select-input label {
/* position: absolute; */
/* top: 0;
left: 0; */
display: block;
width: 100%;
margin-bottom: 0; /* Override default `<label>` margin */
line-height: 1.5;
color: #495057;
cursor: text; /* Match the input under the label */
border: 1px solid transparent;
border-radius: .25rem;
transition: all .1s ease-in-out;
z-index: -1;
}
.button-icon {
font-size: 20px;
}
.form-upload {
margin: 2rem 0;
font-size: 14pt;
}
.result-action-buttons, .test-action {
margin: 5px auto;
width: fit-content;
}
.accordion-item {
background-color: unset;
}
/* Change Autocomplete styles in Chrome*/
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:focus,
select:-webkit-autofill,
select:-webkit-autofill:hover,
select:-webkit-autofill:focus {
transition: background-color 5000s ease-in-out 0s;
}
/* Fallback for Edge
-------------------------------------------------- */
@supports (-ms-ime-align: auto) {
.form-label-group label {
display: none;
}
.form-label-group input::-ms-input-placeholder {
color: #777;
}
}
/* Fallback for IE
-------------------------------------------------- */
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
.form-label-group label {
display: none;
}
.form-label-group input:-ms-input-placeholder {
color: #777;
}
}

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

+642
View File
@@ -0,0 +1,642 @@
// Variable Declarations
const $root = $('#editor-root')
const target = $root.data('target')
const id = $root.data('id')
const $control_panel = $('.control-panel')
const $info_panel = $('.info-panel')
const $viewer_panel = $('.viewer-panel')
const $editor_panel = $('.editor-panel')
var toggle_info = false
var toggle_viewer = false
var element_index = 0
// Initialise Sortable and trigger renumbering on end of drag
Sortable.create($root.get(0), {handle: '.move-handle', onEnd: function(evt) {renumber_blocks()}})
// Info and Viewer Button Listener
$control_panel.find('button').click(function(event){
var action = $(this).data('action');
if (action == 'info') {
if ($info_panel.is(":hidden")) {
if ($viewer_panel.is(":visible")) {
toggle_viewer = true
$viewer_panel.hide()
}
$editor_panel.hide()
$info_panel.fadeIn()
$(window).scrollTop(0)
toggle_info = false
$(this).addClass('active')
} else {
$info_panel.hide()
if (toggle_viewer) {
render_viewer()
$(window).scrollTop(0)
toggle_viewer = false
} else {
$editor_panel.fadeIn()
$(window).scrollTop(0)
}
$(this).removeClass('active')
}
} else if (action == 'view') {
if ($viewer_panel.is(":hidden")) {
if ($info_panel.is(':visible')) {
toggle_info = true
$info_panel.hide()
}
$editor_panel.hide()
render_viewer()
$(window).scrollTop(0)
toggle_viewer = false
$(this).addClass('active')
} else {
$viewer_panel.hide()
if (toggle_info) {
$info_panel.fadeIn()
$(window).scrollTop(0)
toggle_info = false
} else {
$editor_panel.fadeIn()
$(window).scrollTop(0)
}
$(this).removeClass('active')
}
}
event.preventDefault()
})
// Control Button Listeners
$root.on('click', '.block-controls > a', function(event){
event.preventDefault()
var action = $(this).data('action')
var root_accordion = $(this).closest('div').siblings('.accordion')
if (action == 'add-question') {
var question = generate_single_question(root_container_id=`#${root_accordion.attr('id')}`)
$(question).appendTo(root_accordion).hide().fadeIn()
if (root_accordion.children().length > 1 ) {
root_accordion.find('.panel-controls > a[data-action="delete"]').removeClass('disabled')
} else {
root_accordion.find('.panel-controls > a[data-action="delete"]').addClass('disabled')
}
renumber_blocks()
}
})
$root.on('click', '.panel-controls > a', function(event) {
event.preventDefault()
event.stopPropagation()
var action = $(this).data('action')
var element = $(this).closest('.accordion-item')
var root_container = $(this).closest('.accordion')
if (action == 'delete') {
element.fadeOut(function(){
$(this).remove()
renumber_blocks()
if (root_container.get(0) != $root.get(0) && root_container.children().length < 2 ) {
root_container.find('.panel-controls > a[data-action="delete"]').addClass('disabled')
}
})
} else if (action == 'add-question') {
var question = generate_single_question(root_container_id=`#${root_container.attr('id')}`)
$(question).insertBefore(element).hide().fadeIn()
if (root_container.get(0) != $root.get(0) && root_container.children().length > 1 ) {
root_container.find('.panel-controls > a[data-action="delete"]').removeClass('disabled')
}
} else if (action == 'add-block') {
var block = generate_block(root_container_id=`#${root_container.attr('id')}`)
$(block).insertBefore(element).hide().fadeIn()
var block_container = $(`#element${element_index-1}`).children().find('.accordion')
Sortable.create(block_container.get(0), {handle: '.move-handle', onEnd: function(evt) {renumber_blocks()}})
var question = generate_single_question(root_container_id=`#${block_container.attr('id')}`)
block_container.append(question)
block_container.find('.panel-controls > a[data-action="delete"]').addClass('disabled')
}
renumber_blocks()
})
$root.on('click', '.option-controls > a', function(event) {
event.preventDefault()
var action = $(this).data('action')
var options = $(this).closest('div.option-controls').siblings('.options')
var length = options.children().length
var correct = $(this).closest('div.option-controls').siblings().find('.question-correct')
if (action == 'delete') {
if (length > 2) {
options.children().last().fadeOut(function(){
$(this).remove()
length = options.children().length
if (length <= 2) {
options.siblings('div.option-controls').children('a[data-action="delete"]').addClass('disabled')
} else {
options.siblings('div.option-controls').children('a[data-action="delete"]').removeClass('disabled')
}
})
correct.children().last().fadeOut(function(){
$(this).remove()
})
}
} else {
var opt = `
<div class="input-group mb-3">
<span class="input-group-text">${length}</span>
<input type="text" class="form-control" value="Option ${length}">
</div>
`
$(opt).appendTo(options).hide().fadeIn()
var cor = `<option value="${length}">${length}</option>`
correct.append(cor)
}
length = options.children().length
if (length <= 2) {
$(this).closest('div.option-controls').children('a[data-action="delete"]').addClass('disabled')
} else {
$(this).closest('div.option-controls').children('a[data-action="delete"]').removeClass('disabled')
}
})
$('.editor-controls > a').click(function(event){
event.preventDefault()
var action = $(this).data('action')
var root_accordion = $(this).closest('div').siblings('.accordion')
if (action == 'add-question') {
var obj = generate_single_question(root_container_id=`#${root_accordion.attr('id')}`)
$(obj).appendTo($root).hide().fadeIn()
} else if (action == 'add-block') {
var obj = generate_block(root_container_id=`#${root_accordion.attr('id')}`)
$(obj).appendTo($root).hide().fadeIn()
var block_container = $(`#element${element_index-1}`).children().find('.accordion')
Sortable.create(block_container.get(0), {handle: '.move-handle', onEnd: function(evt) {renumber_blocks()}})
var question = generate_single_question(root_container_id=`#${block_container.attr('id')}`)
block_container.append(question)
block_container.find('.panel-controls > a[data-action="delete"]').addClass('disabled')
} else if (action == 'discard') {
window.location.href = '/admin/settings/questions/'
} else if (action == 'delete') {
$.ajax({
url: '/admin/settings/questions/delete/',
type: 'POST',
data: JSON.stringify({
'id': id,
'action': action
}),
contentType: 'application/json',
success: function(response) {
window.location.href = '/admin/settings/questions/'
},
error: function(response) {
error_response(response)
}
})
} else if (action == 'save') {
var input = parse_input()
var def = $('.dataset-default').is(':checked')
var name = $('.dataset-name').val()
var creator = $('.dataset-creator').val()
console.log([def, name, creator])
$.ajax({
url: target,
type: 'POST',
data: JSON.stringify({
'id': id,
'action': 'upload',
'data': input,
'default': def,
'name': name,
'creator': creator
}),
contentType: 'application/json',
success: function(response) {
window.location.href = '/admin/settings/questions/'
},
error: function(response) {
error_response(response)
}
})
}
renumber_blocks()
})
// Question Type Select Menu Listener
$root.on('change', '.form-select.question-type', function(event) {
event.preventDefault()
var type = $(this).val()
var options = $(this).closest('div.input-group').siblings('.options')
var option_controls = $(this).closest('div.input-group').siblings('.option-controls')
var correct = $(this).closest('div.input-group').siblings().find('.question-correct')
if (type == 'Yes/No') {
options.empty()
correct.empty()
var opt = `
<div class="input-group mb-3">
<span class="input-group-text">0</span>
<input type="text" class="form-control" value="Yes" disabled>
</div>
<div class="input-group mb-3">
<span class="input-group-text">1</span>
<input type="text" class="form-control" value="No" disabled>
</div>
`
$(opt).appendTo(options).hide().fadeIn()
option_controls.children('a').addClass('disabled')
var cor = `
<option value ="0" default>0</option>
<option value="1">1</option>
`
correct.append(cor)
} else {
option_controls.children('a').removeClass('disabled')
options.find('input').removeAttr('disabled')
if (options.children().length <= 2 ){
option_controls.children('a[data-action="delete"]').addClass('disabled')
}
}
})
// Data and Rendering Functions
function renumber_blocks () {
$( ".block-number" ).each(function(index) {
$( this ).text($( this ).closest('.accordion-item').index() + 1)
})
}
function parse_input() {
var data = []
var element = {}
var question = {}
var block_container
var q_no = 0
$root.children().each(function(index) {
element = {}
if ($(this).data('type') == 'block') {
element['type'] = 'block'
element['question_header'] = $(this).find('.block-header-text').val()
element['questions'] = []
block_container = $(this).children().find('.accordion')
block_container.children().each(function(index) {
question = {}
question['q_no'] = q_no
question['text'] = $(this).find('.question-text').val()
question['q_type'] = $(this).find('.question-type').val()
question['correct'] = parseInt($(this).find('.question-correct').val())
question['options'] = []
$(this).find('.options').find('input').each(function(index) {
question['options'].push($(this).val())
})
question['tags'] = $(this).find('.question-tags').val().split('\r\n')
element['questions'].push(question)
q_no ++
})
} else if ( $(this).data('type') == 'question') {
element['type'] = 'question'
element['q_no'] = q_no
element['text'] = $(this).find('.question-text').val()
element['q_type'] = $(this).find('.question-type').val()
element['correct'] = parseInt($(this).find('.question-correct').val())
element['options'] = []
$(this).find('.options').find('input').each(function(index) {
element['options'].push($(this).val())
})
element['tags'] = $(this).find('.question-tags').val().split('\r\n')
q_no ++
}
data.push(element)
})
return data
}
function parse_data(data) {
var block, obj, new_block, block_container, question, _question, new_question, options, correct, opt, tags
for (let c = 0; c < data.length; c++) {
block = data[c]
if (block['type'] == 'block') {
obj = generate_block(root_container_id=`#${$root.attr('id')}`)
$root.append(obj)
new_block = $(`#element${element_index-1}`)
new_block.find('.block-header-text').val(block['question_header']).trigger('change')
block_container = $(`#element${element_index-1}`).children().find('.accordion')
Sortable.create(block_container.get(0), {handle: '.move-handle', onEnd: function(evt) {renumber_blocks()}})
for (let _c = 0; _c < block['questions'].length; _c ++) {
question = block['questions'][_c]
_question = generate_single_question(root_container_id=`#${block_container.attr('id')}`)
block_container.append(_question)
if (block_container.children().length <= 1) {
block_container.find('.panel-controls > a[data-action="delete"]').addClass('disabled')
} else {
block_container.find('.panel-controls > a[data-action="delete"]').removeClass('disabled')
}
new_question = $(`#element${element_index-1}`)
new_question.find('.question-text').val(question['text']).trigger('change')
new_question.find('.question-type').val(question['q_type']).trigger('change')
correct = new_question.find('.question-correct')
if (question['q_type'] != 'Yes/No') {
options = new_question.find('.options')
options.empty()
correct.empty()
for ( var __c = 0; __c < question['options'].length; __c++) {
option = question['options'][__c]
opt = `
<div class="input-group mb-3">
<span class="input-group-text">${__c}</span>
<input type="text" class="form-control" value="${option}">
</div>
`
options.append(opt)
correct.append(`<option value="${__c}">${__c}</option>`)
}
}
correct.val(String(question['correct']))
tags = question['tags'].join('\r\n')
new_question.find('.question-tags').val(tags)
}
} else {
question = block
obj = generate_single_question(root_container_id=`#${$root.attr('id')}`)
$root.append(obj)
new_question = $(`#element${element_index-1}`)
new_question.find('.question-text').val(question['text']).trigger('change')
new_question.find('.question-type').val(question['q_type']).trigger('change')
correct = new_question.find('.question-correct')
if (question['q_type'] != 'Yes/No') {
options = new_question.find('.options')
options.empty()
correct.empty()
for ( var _c = 0; _c < question['options'].length; _c++) {
option = question['options'][_c]
opt = `
<div class="input-group mb-3">
<span class="input-group-text">${_c}</span>
<input type="text" class="form-control" value="${option}">
</div>
`
options.append(opt)
correct.append(`<option value="${_c}">${_c}</option>`)
}
}
correct.val(String(question['correct']))
tags = question['tags'].join('\r\n')
new_question.find('.question-tags').val(tags)
}
}
renumber_blocks()
}
// Content Generator Functions
function generate_single_question(root_container_id) {
if (root_container_id == `#${$root.attr('id')}`) {
var block_button = `
<a href="javascript:void(0)" class="btn btn-primary" data-action="add-block" title="Add Block Above" aria-title="Add Block Above" data-bs-toggle="collapse" data-bs-target>
<i class="bi bi-folder-plus"></i>
</a>
`
} else {
var block_button = ''
}
var question = `
<div class="accordion-item" id="element${element_index}" data-type="question">
<h2 class="accordion-header" id="element${element_index}-header">
<div class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#element${element_index}-content" aria-expanded="true" aria-controls="element${element_index}-content">
<div class="float-start">
<div class="accordion-caption">
<span class="block-number"></span>.
Question
</div>
</div>
<div class="panel-controls float-end">
<a href="javascript:void(0)" class="btn btn-success move-handle" data-action="move-question" title="Move Question" aria-title="Move Question" data-bs-toggle="collapse" data-bs-target>
<i class="bi bi-arrows-move"></i>
</a>
${block_button}
<a href="javascript:void(0)" class="btn btn-primary" data-action="add-question" title="Add Question Above" aria-title="Add Question Above" data-bs-toggle="collapse" data-bs-target>
<i class="bi bi-file-plus-fill"></i>
</a>
<a href="javascript:void(0)" class="btn btn-danger" data-action="delete" title="Delete Block" aria-title="Delete Block" data-bs-toggle="collapse" data-bs-target>
<i class="bi bi-trash-fill"></i>
</a>
</div>
</div>
</h2>
<div id="element${element_index}-content" class="accordion-collapse collapse" aria-labelledby="element${element_index}-header" data-bs-parent="${root_container_id}">
<div class="accordion-body">
<div class="input-group mb-3">
<span class="input-group-text">Question</span>
<textarea type="text" class="form-control question-text">Enter question here.</textarea>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Question Type</span>
<select class="form-select question-type">
<option value ="Multiple Choice" default>Multiple Choice</option>
<option value="Yes/No">Yes/No</option>
<option value="List">Ordered List</option>
</select>
</div>
<label class="form-label">Options</label>
<ul class="options">
<div class="input-group mb-3">
<span class="input-group-text">0</span>
<input type="text" class="form-control" value="Option 0">
</div>
<div class="input-group mb-3">
<span class="input-group-text">1</span>
<input type="text" class="form-control" value="Option 1">
</div>
</ul>
<div class="option-controls">
<a href="javascript:void(0)" class="btn btn-danger disabled" data-action="delete" title="Delete Question" aria-title="Delete Question">
<i class="bi bi-patch-minus-fill"></i>
Delete
</a>
<a href="javascript:void(0)" class="btn btn-success" data-action="add" title="Add Question" aria-title="Add Question">
<i class="bi bi-patch-plus-fill"></i>
Add
</a>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Correct</span>
<select class="form-select question-correct">
<option value ="0" default>0</option>
<option value="1">1</option>
</select>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Tags</span>
<textarea type="text" class="form-control question-tags"></textarea>
</div>
</div>
</div>
</div>
`
element_index ++
return question
}
function generate_block(root_container_id) {
var block = `
<div class="accordion-item" id="element${element_index}" data-type="block">
<h2 class="accordion-header" id="element${element_index}-header">
<div class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#element${element_index}-content" aria-expanded="true" aria-controls="element${element_index}-content">
<div class="float-start">
<div class="accordion-caption">
<span class="block-number"></span>.
Block
</div>
</div>
<div class="panel-controls float-end">
<a href="javascript:void(0)" class="btn btn-success move-handle" data-action="move-question" title="Move Question" aria-title="Move Question" data-bs-toggle="collapse" data-bs-target>
<i class="bi bi-arrows-move"></i>
</a>
<a href="javascript:void(0)" class="btn btn-primary" data-action="add-block" title="Add Block Above" aria-title="Add Block Above" data-bs-toggle="collapse" data-bs-target>
<i class="bi bi-folder-plus"></i>
</a>
<a href="javascript:void(0)" class="btn btn-primary" data-action="add-question" title="Add Question Above" aria-title="Add Question Above" data-bs-toggle="collapse" data-bs-target>
<i class="bi bi-file-plus-fill"></i>
</a>
<a href="javascript:void(0)" class="btn btn-danger" data-action="delete" title="Delete Block" aria-title="Delete Block" data-bs-toggle="collapse" data-bs-target>
<i class="bi bi-trash-fill"></i>
</a>
</div>
</div>
</h2>
<div id="element${element_index}-content" class="accordion-collapse collapse" aria-labelledby="element${element_index}-header" data-bs-parent="${root_container_id}">
<div class="accordion-body">
<div class="input-group mb-3">
<span class="input-group-text">Block Header</span>
<textarea type="text" class="form-control block-header-text">Enter the header text for this block of questions.</textarea>
</div>
<div class="accordion" id="element${element_index}-questions">
</div>
<div class="block-controls">
<a href="javascript:void(0)" class="btn btn-success" data-action="add-question" title="Add Question" aria-title="Add Question">
<i class="bi bi-file-plus-fill"></i>
Add Question
</a>
</div>
</div>
</div>
</div>
`
element_index ++
return block
}
// Fetch data once page finishes loading
$(window).on('load', function() {
$.ajax({
url: target,
type: 'POST',
data: JSON.stringify({
'id': id,
'action': 'fetch'
}),
contentType: 'application/json',
success: function(response) {
parse_data(response['data'])
},
error: function(response) {
console.log(response)
}
})
})
// Viewer Render Function
function render_viewer() {
$viewer_panel.fadeIn()
$viewer_panel.empty()
var heading = document.createElement('h3')
heading.innerText = 'View Questions'
$viewer_panel.append(heading)
var data = parse_input()
var block
var obj
for (let i = 0; i < data.length; i++) {
block = data[i]
obj = document.createElement('div')
obj.classList = 'block'
if (block['type'] == 'question') {
text = document.createElement('p')
text.innerHTML = `<strong>Question ${block['q_no'] + 1}.</strong> ${block['text']}`
obj.append(text)
question_body = document.createElement('div')
question_body.className ='question-body'
type = document.createElement('p')
type.innerHTML = `<strong>Question Type:</strong> ${block['q_type']}`
question_body.append(type)
options = document.createElement('p')
options.innerHTML = '<strong>Options:</strong>'
option_list = document.createElement('ul')
for (let _i = 0; _i < block['options'].length; _i++) {
option = document.createElement('li')
option.innerHTML = block['options'][_i]
if (block['correct'] == _i) {
option.innerHTML += ' <span class="badge rounded-pill bg-success">Correct</span>'
}
option_list.append(option)
}
options.append(option_list)
question_body.append(options)
tags = document.createElement('p')
tags.innerHTML = `<strong>Tags:</strong>`
tag_list = document.createElement('ul')
for (let _i = 0; _i < block['tags'].length; _i++) {
tag = document.createElement('li')
tag.innerHTML = block['tags'][_i]
tag_list.append(tag)
}
tags.append(tag_list)
question_body.append(tags)
obj.append(question_body)
} else if (block['type'] == 'block') {
meta = document.createElement('p')
meta.innerHTML = `<strong>Block ${i+1}.</strong> ${block['questions'].length} questions.`
obj.append(meta)
header = document.createElement('blockquote')
header.innerText = block['question_header']
obj.append(header)
var block_question = document.createElement('div')
var question
block_question.className = 'question-block'
for (let _i = 0; _i < block['questions'].length; _i++) {
question = block['questions'][_i]
text = document.createElement('p')
text.innerHTML = `<strong>Question ${question['q_no'] + 1}.</strong> ${question['text']}`
block_question.append(text)
question_body = document.createElement('div')
question_body.className ='question-body'
type = document.createElement('p')
type.innerHTML = `<strong>Question Type:</strong> ${question['q_type']}`
question_body.append(type)
options = document.createElement('p')
options.innerHTML = '<strong>Options:</strong>'
option_list = document.createElement('ul')
for (let __i = 0; __i < question['options'].length; __i++) {
option = document.createElement('li')
option.innerHTML = question['options'][__i]
if (question['correct'] == __i) {
option.innerHTML += ' <span class="badge rounded-pill bg-success">Correct</span>'
}
option_list.append(option)
}
options.append(option_list)
question_body.append(options)
tags = document.createElement('p')
tags.innerHTML = `<strong>Tags:</strong>`
tag_list = document.createElement('ul')
for (let __i = 0; __i < question['tags'].length; __i++) {
tag = document.createElement('li')
tag.innerHTML = question['tags'][__i]
tag_list.append(tag)
}
tags.append(tag_list)
question_body.append(tags)
block_question.append(question_body)
obj.append(block_question)
}
}
$viewer_panel.append(obj)
}
}
File diff suppressed because one or more lines are too long
+115
View File
@@ -0,0 +1,115 @@
// Menu Highlight Scripts
const menuItems = document.getElementsByClassName('nav-link');
for(let i = 0; i < menuItems.length; i++) {
if(menuItems[i].pathname == window.location.pathname) {
menuItems[i].classList.add('active');
}
}
const dropdownItems = document.getElementsByClassName('dropdown-item');
for(let i = 0; i< dropdownItems.length; i++) {
if(dropdownItems[i].pathname == window.location.pathname) {
dropdownItems[i].classList.add('active');
$( "#" + dropdownItems[i].id ).closest( '.dropdown' ).find('.dropdown-toggle').addClass('active');
}
}
// General Post Method Form Processing Script
$('form.form-post').submit(function(event) {
var $form = $(this);
var data = $form.serialize();
var url = $(this).prop('action');
var rel_success = $(this).data('rel-success');
$.ajax({
url: url,
type: 'POST',
data: data,
dataType: 'json',
success: function(response) {
if (response.redirect_to) {
window.location.href = response.redirect_to;
}
else {
window.location.href = rel_success;
}
},
error: function(response) {
error_response(response);
}
});
event.preventDefault();
});
function error_response(response) {
const $alert = $("#alert-box");
$alert.html('');
if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) {
$alert.html(`
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
${response.responseJSON.error}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`);
} else if (response.responseJSON.error instanceof Array) {
var output = ''
for (let i = 0; i < response.responseJSON.error.length; i ++) {
output += `
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
${response.responseJSON.error[i]}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`;
$alert.html(output);
}
}
$alert.focus()
}
// Dismiss Cookie Alert
$('#dismiss-cookie-alert').click(function(event){
$.ajax({
url: '/cookies/',
type: 'POST',
data: {
time: Date.now()
},
dataType: 'json',
success: function(response){
console.log(response);
},
error: function(response){
console.log(response);
}
})
event.preventDefault();
})
// Create New Dataset
$('.create-new-dataset').click(function(event){
$.ajax({
url: '/api/editor/new/',
type: 'POST',
data: {
time: Date.now()
},
dataType: 'json',
success: function(response){
if (response.redirect_to) {
window.location.href = response.redirect_to;
}
},
error: function(response){
console.log(response);
}
})
event.preventDefault()
})
@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
crossorigin="anonymous">
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.1/font/bootstrap-icons.css">
<link
rel="stylesheet"
href="{{ url_for('.static', filename='css/style.css') }}"
/>
{% block style %}
{% endblock %}
<title>{% block title %} SKA Referee Test | Admin Console {% endblock %}</title>
{% include "editor/components/og-meta.html" %}
</head>
<body class="bg-light">
{% block navbar %}
{% include "editor/components/navbar.html" %}
{% endblock %}
<div class="container">
{% block top_alerts %}
{% include "editor/components/server-alerts.html" %}
{% endblock %}
{% block content %}{% endblock %}
</div>
<footer class="container site-footer mt-5">
{% block footer %}
{% include "editor/components/footer.html" %}
{% endblock %}
</footer>
<!-- JQuery, Popper, and Bootstrap js dependencies -->
<script
src="https://code.jquery.com/jquery-3.6.0.min.js"
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
crossorigin="anonymous">
</script>
<script>
window.jQuery || document.write(`<script src="{{ url_for('.static', filename='js/jquery-3.6.0.min.js') }}"><\/script>`)
</script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
<script
src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.10.2/dist/umd/popper.min.js"
integrity="sha384-7+zCNj/IqJ95wo16oMtfsKbZ9ccEh31eOz1HGyDuCQ6wgnyJNSYdrPa03rtR1zdB"
crossorigin="anonymous">
</script>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.min.js"
integrity="sha384-QJHtvGhmr9XOIpI6YVutG+2QOK9T+ZnN4kzFN1RtK3zEFEIsxhlmWl5/YESvpZ13"
crossorigin="anonymous"
></script>
<!-- Custom js -->
<script type="text/javascript">
var csrf_token = "{{ csrf_token() }}";
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrf_token);
}
}
});
</script>
<script
type="text/javascript"
src="{{ url_for('.static', filename='js/script.js') }}"
></script>
{% block script %}
{% endblock %}
</body>
</html>
@@ -0,0 +1 @@
<div id="alert-box" tabindex="-1"></div>
@@ -0,0 +1,28 @@
{% extends "editor/components/base.html" %}
{% block datatable_css %}
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.11.3/css/dataTables.bootstrap5.min.css"/>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/buttons/2.0.1/css/buttons.bootstrap5.min.css"/>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/colreorder/1.5.5/css/colReorder.bootstrap5.min.css"/>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/fixedheader/3.2.0/css/fixedHeader.bootstrap5.min.css"/>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/keytable/2.6.4/css/keyTable.bootstrap5.min.css"/>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/responsive/2.2.9/css/responsive.bootstrap5.min.css"/>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/searchbuilder/1.3.0/css/searchBuilder.dataTables.min.css"/>
{% endblock %}
{% block datatable_scripts %}
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jszip/2.5.0/jszip.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.36/pdfmake.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.36/vfs_fonts.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/1.11.3/js/jquery.dataTables.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/1.11.3/js/dataTables.bootstrap5.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/dataTables.buttons.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/buttons.bootstrap5.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/buttons.colVis.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/buttons.html5.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/buttons.print.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/colreorder/1.5.5/js/dataTables.colReorder.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/fixedheader/3.2.0/js/dataTables.fixedHeader.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/keytable/2.6.4/js/dataTables.keyTable.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/responsive/2.2.9/js/dataTables.responsive.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/responsive/2.2.9/js/responsive.bootstrap5.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/searchbuilder/1.3.0/js/dataTables.searchBuilder.min.js"></script>
{% endblock %}
@@ -0,0 +1,2 @@
<p>This web app was developed and is maintained by Vivek Santayana. The source code for the web app, excluding any data pertaining to the questions in the quiz, is freely available at <a href="https://git.vsnt.uk/viveksantayana/ska-referee-test">Vivek&rsquo;s personal GIT repository</a> under an MIT License.</p>
<p>All questions in the test are &copy; The Scottish Korfball Association {{ now.year }}. All rights are reserved.</p>
@@ -0,0 +1,4 @@
{% extends "admin/components/base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block top_alerts %}
{% endblock %}
@@ -0,0 +1,137 @@
<nav class="navbar fixed-top navbar-expand-md navbar-dark bg-dark">
<div class="container">
<a href="{{ url_for('admin._home') }}" class="navbar-brand mb-0 h1">RefTest | Admin</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbar"
aria-controls="navbar"
aria-expanded="false"
aria-label="Toggle Navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse justify-content-end" id="navbar">
<ul class="navbar-nav">
{% if not current_user.is_authenticated %}
<li class="nav-item" id="nav-login">
<a href="{{ url_for('admin._login') }}" id="link-login" class="nav-link">Log In</a>
</li>
{% endif %}
{% if current_user.is_authenticated %}
<li class="nav-item dropdown" id="nav-results">
<a
class="nav-link dropdown-toggle"
id="dropdown-results"
role="button"
href="{{ url_for('admin._view_entries') }}"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Results
</a>
<ul
class="dropdown-menu"
aria-labelledby="dropdown-results"
>
<li>
<a href="{{ url_for('admin._view_entries', filter='active') }}" id="link-results" class="dropdown-item">View Results</a>
</li>
<li>
<a href="{{ url_for('analysis._analysis') }}" id="link-analysis" class="dropdown-item">Analysis</a>
</li>
</ul>
</li>
<li class="nav-item dropdown" id="nav-tests">
<a
class="nav-link dropdown-toggle"
id="dropdown-tests"
role="button"
href="{{ url_for('admin._tests') }}"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Exams
</a>
<ul
class="dropdown-menu"
aria-labelledby="dropdown-tests"
>
<li>
<a href="{{ url_for('admin._tests', filter='active') }}" id="link-active" class="dropdown-item">Active</a>
</li>
<li>
<a href="{{ url_for('admin._tests', filter='scheduled') }}" id="link-scheduled" class="dropdown-item">Scheduled</a>
</li>
<li>
<a href="{{ url_for('admin._tests', filter='expired') }}" id="link-expired" class="dropdown-item">Expired</a>
</li>
<li>
<a href="{{ url_for('admin._tests', filter='all') }}" id="link-all" class="dropdown-item">All</a>
</li>
<li>
<a href="{{ url_for('admin._tests', filter='create') }}" id="link-create" class="dropdown-item">Create</a>
</li>
</ul>
</li>
<li class="nav-item dropdown" id="nav-settings">
<a
class="nav-link dropdown-toggle"
id="dropdown-settings"
role="button"
href="{{ url_for('admin._settings') }}"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Settings
</a>
<ul
class="dropdown-menu"
aria-labelledby="dropdown-settings"
>
<li>
<a href="{{ url_for('admin._settings') }}" id="link-settings" class="dropdown-item">View Settings</a>
</li>
<li>
<a href="{{ url_for('admin._users') }}" id="link-users" class="dropdown-item">Users</a>
</li>
<li>
<a href="{{ url_for('admin._questions') }}" id="link-questions" class="dropdown-item">Manage Questions</a>
</li>
<li>
<a href="{{ url_for('view._view') }}" id="link-editor" class="dropdown-item">View Questions</a>
</li>
<li>
<a href="{{ url_for('editor._editor') }}" id="link-editor" class="dropdown-item">Edit Questions</a>
</li>
</ul>
</li>
<li class="nav-item dropdown" id="nav-account">
<a
class="nav-link dropdown-toggle"
id="dropdown-account"
role="button"
href="{{ url_for('admin._update_user', id=current_user.id) }}"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Account
</a>
<ul
class="dropdown-menu"
aria-labelledby="dropdown-account"
>
<li>
<a href="{{ url_for('admin._update_user', id=current_user.id) }}" id="link-account" class="dropdown-item">Account Settings</a>
</li>
<li>
<a href="{{ url_for('admin._logout') }}" id="link-logout" class="dropdown-item">Log Out</a>
</li>
</ul>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
@@ -0,0 +1,18 @@
<meta name="description" content="A web app for taking the Scottish Korfball Association Refereeing Theory Exam on-line." />
<meta property="og:locale" content="en_UK" />
<meta property="og:type" content="website" />
<meta property="og:description" content="A web app for taking the Scottish Korfball Association Refereeing Theory Exam on-line." />
<meta property="og:url" content="{{ url_for(request.endpoint, _external = True, **(request.view_args or {})) }}" />
<meta property="og:site_name" content="Scottish Korfball Association Referee Theory Exam" />
<meta property="og:image" content="{{ url_for('.static', filename='favicon.png', _external = True) }}" />
<meta property="og:image:alt" content="Logo of the SKA Refereeing Exam App" />
<meta property="og:image:width" content="512" />
<meta property="og:image:height" content="512" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:description" content="A web app for taking the Scottish Korfball Association Refereeing Theory Exam on-line." />
<meta name="twitter:image" content="{{ url_for('.static', filename='favicon.png', _external = True) }}" />
<meta name="twitter:image:alt" content="Logo of the SKA Refereeing Exam App" />
<meta name="twitter:creator" content="@viveksantayana" />
<meta name="twitter:site" content="@viveksantayana" />
<meta name="theme-color" content="#343a40" />
<link rel="shortcut icon" href="{{ url_for('.static', filename='favicon.ico') }}">
@@ -0,0 +1,23 @@
<div class="navbar navbar-expand-sm navbar-light bg-light">
<div class="container-fluid">
<div class="expand navbar-expand justify-content-center" id="navbar_secondary">
<ul class="nav nav-pills">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='active') }}">Active</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='scheduled') }}">Scheduled</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='expired') }}">Expired</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='all') }}">All</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='create') }}">Create</a>
</li>
</ul>
</div>
</div>
</div>
@@ -0,0 +1,43 @@
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% set cookie_flash_flag = namespace(value=False) %}
{% for category, message in messages %}
{% if category == "error" %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill" title="Error" aria-title="Error"></i>
{{ message|safe }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% elif category == "success" %}
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check2-circle" title="Success" aria-title="Success"></i>
{{ message|safe }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% elif category == "warning" %}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
<i class="bi bi-info-circle-fill" aria-title="Warning" title="Warning"></i>
{{ message|safe }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% elif category == "cookie_alert" %}
{% if not cookie_flash_flag.value %}
<div class="alert alert-primary alert-dismissible fade show" id="cookie-alert" role="alert">
<i class="bi bi-info-circle-fill" title="Cookie Alert" aria-title="Cookie Alert"></i>
{{ message|safe }}
<div class="d-flex justify-content-center w-100">
<button type="button" id="dismiss-cookie-alert" class="btn btn-success" data-bs-dismiss="alert" aria-label="Close">Accept</button>
</div>
</div>
{% set cookie_flash_flag.value = True %}
{% endif %}
{% else %}
<div class="alert alert-primary alert-dismissible fade show" role="alert">
<i class="bi bi-info-circle-fill" title="Alert"></i>
{{ message|safe }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
{% endfor %}
{% endif %}
{% endwith %}
@@ -0,0 +1,150 @@
{% extends "editor/components/base.html" %}
{% block style %}
<link
rel="stylesheet"
href="{{ url_for('.static', filename='css/editor.css') }}"
/>
{% endblock %}
{% block content %}
<h1>Editor</h1>
<div class="container">
<p class="lead">
Use this console to edit the questions in this dataset. For more information on using the editor console, click on the the blue Information button. To preview the questions in the current dataset, click on the green View Questions button.
</p>
</div>
<div class="container control-panel">
<button class="btn btn-primary" aria-title="Information" title="Information" data-action="info"><i class="bi bi-info-circle-fill"></i></button>
<button class="btn btn-success" aria-title="View Questions" title="View Questions" data-action="view"><i class="bi bi-book-fill"></i></button>
</div>
<div class="container info-panel">
<h3>
About the Editor Console
</h3>
<p>
This console will allow you to edit the question data for the RefTest App.
All of the questions will be visually displayed as blocks on the screen that you can minimise, expand, and rearrange.
</p>
<p>
Blocks can be of two types: <strong>Blocks</strong> of multiple related questions, and <strong>Single Questions</strong> that are not part of a block.
You can add, remove, or edit both Blockss and Questions through this editor.
</p>
<p>
<strong>Blocks</strong> are useful when you have a section of the test that contains multiple questions that are related to each other, for example if there is a scenario-based section where a series of questions are about the same situation.
</p>
<p>
Blocks can contain any number of questions within them, but cannot contain nested blocks.
</p>
<p>
When you set up a block, you can also add <strong>header text</strong> that will be displayed with each question.
You can use this to provide common information for a scenario across a series of questions.
</p>
<p>
Questions come in three types:
<ul>
<li>
<strong>Yes/No</strong> for when there is only a yes or no option,
</li>
<li>
<strong>Multiple Choice</strong> for your regular multiple choice questions, and
</li>
<li>
<strong>Ordered List</strong> for multiple choice questions that will be displayed in the same order as listed here.
</li>
</ul>
</p>
<p>
Normally, multiple choice questions will have the order of the options randomised.
</p>
<p>
Questions will be displayed to candidates in a randomised order.
Blocks of questions will be kept together, but the order within the block will also be randomised.
</p>
<p><strong>Do not use language that will assume the flow of questions, such as saying &lsquo;the previous question&rsquo;, or &lsquo;the next question&rsquo;, etc. because of randomisation.</strong></p>
<p>
Each option will be referenced by an <strong>index number</strong>.
Make sure to select which index number represents the <strong>correct option</strong>.
</p>
<p>
You will also be able to define <strong>tags</strong> for each question.
Separate multiple tags in <strong>new lines</strong>.
Make sure to keep the spelling, capitalisation, and punctuation for tags consistent.
</p>
<p class="lead">
Placeholder for Questions Remaining in a Block
</p>
<p>
In order to show how many questions are remaining inside a block, e.g. to say &lsquo;the next n questions are about a specific scenario&rsquo;, use the placeholder <code>&lt;block_remaining_questions&gt;</code>.
</p>
</div>
<div class="container viewer-panel">
</div>
<div class="container editor-panel">
<h3>
Edit Questions
</h3>
<div class="container dataset-metadata">
<div class="input-group mb-3">
<span class="input-group-text">Dataset Name</span>
<input type="text" class="form-control dataset-name" value="{{ dataset.get_name() }}">
</div>
<div class="input-group mb-3">
<span class="input-group-text">Author</span>
<select class="form-select dataset-creator">
{% for user in users %}
<option value="{{ user.id }}" {{default if dataset.user == user else None }}>{{ user.get_username() }}</option>
{% endfor %}
</select>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Last Updated</span>
<span class="form-control">
{{ dataset.date.strftime('%d %b %Y %H:%M') }}
</span>
</div>
<div class="input-group mb-3">
<span class="input-group-text">
<input type="checkbox" aria-label="Default" class="dataset-default" {% if dataset.default %}checked{% endif %}>
</span>
<span class="form-control">
Make Dataset the Default
</select>
</div>
</div>
<div class="accordion" id="editor-root" data-target="{{ url_for('api._editor') }}" data-id="{{ dataset.id }}">
</div>
{% include "editor/components/client-alerts.html" %}
<div class="editor-controls container">
<a href="javascript:void(0);" class="btn btn-primary" data-action="add-block" title="Add Block" aria-title="Add Block">
<i class="bi bi-folder-plus"></i>
Add Block
</a>
<a href="javascript:void(0);" class="btn btn-primary" data-action="add-question" title="Add Question" aria-title="Add Question">
<i class="bi bi-file-plus-fill"></i>
Add Question
</a>
</div>
<div class="editor-controls container">
<a href="javascript:void(0);" class="btn btn-warning" data-action="discard" title="Discard Changes" aria-title="Discard Changes">
<i class="bi bi-x-circle-fill"></i>
Discard Changes
</a>
<a href="javascript:void(0);" class="btn btn-danger {% if datasets <=1 or dataset.default or dataset.tests|length > 0 %}disabled{% endif %}" data-action="delete" title="Delete" aria-title="Delete">
<i class="bi bi-trash-fill"></i>
Delete
</a>
<a href="javascript:void(0);" class="btn btn-success" data-action="save" title="Save" aria-title="Save">
<i class="bi bi-cloud-arrow-up-fill"></i>
Save Changes
</a>
</div>
</div>
{% endblock %}
{% block script %}
<script
type="text/javascript"
src="{{ url_for('.static', filename='js/editor.js') }}"
></script>
{% endblock %}
@@ -0,0 +1,31 @@
{% extends "editor/components/input-forms.html" %}
{% block content %}
<div class="form-container">
<form name="form-editor" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for(request.endpoint, **request.view_args) }}">
{% include "admin/components/server-alerts.html" %}
<h2 class="form">Edit Questions</h2>
{{ form.hidden_tag() }}
<div class="form-select-input">
{{ form.dataset(placeholder="Select Question Dataset") }}
{{ form.dataset.label }}
</div>
{% include "admin/components/client-alerts.html" %}
<div class="container form-submission-button">
<div class="row">
<div class="col text-center">
<button class="btn btn-md btn-success btn-block" type="submit">
<i class="bi bi-pencil-fill button-icon"></i>
Edit
</button>
<button title="New" class="btn btn-md btn-primary create-new-dataset">
<i class="bi bi-cloud-plus-fill button-icon"></i>
New
</button>
</div>
</div>
</div>
</form>
</div>
{% endblock %}
+45
View File
@@ -0,0 +1,45 @@
from ..forms.admin import EditDataset
from ..models import Dataset, User
from ..tools.data import check_dataset_exists
from ..tools.forms import get_dataset_choices, send_errors_to_client
from ..tools.logs import write
from flask import Blueprint, jsonify, render_template
from flask.helpers import abort, flash, redirect, request, url_for
from flask_login import login_required
editor = Blueprint(
name='editor',
import_name=__name__,
template_folder='templates',
static_folder='static'
)
@editor.route('/', methods=['GET','POST'])
@login_required
def _editor():
form = EditDataset()
form.dataset.choices = get_dataset_choices()
if request.method == 'POST':
if form.validate_on_submit():
id = request.form.get('dataset')
return jsonify({'success': 'Selected dataset', 'redirect_to': url_for('editor._editor_console', id=id)}),200
return send_errors_to_client(form=form)
form.process()
return render_template('/editor/index.html', form=form)
@editor.route('/<string:id>/')
@check_dataset_exists
@login_required
def _editor_console(id:str=None):
try:
dataset = Dataset.query.filter_by(id=id).first()
datasets = Dataset.query.count()
users = User.query.all()
except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500)
if not dataset:
flash('Invalid dataset ID.', 'error')
return redirect(url_for('admin._questions'))
return render_template('/editor/console.html', dataset=dataset, datasets=datasets, users=users)

Some files were not shown because too many files have changed in this diff Show More