diff --git a/README.md b/README.md index 4b77a35..0427bcb 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ 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 diff --git a/nginx/conf.d/ref-test-app.conf b/nginx/conf.d/ref-test-app.conf index 36c1247..24c2694 100644 --- a/nginx/conf.d/ref-test-app.conf +++ b/nginx/conf.d/ref-test-app.conf @@ -19,6 +19,7 @@ server { include /etc/nginx/ssl.conf; include /etc/nginx/certbot-challenge.conf; + # Define locations for static files to be served by Nginx location ^~ /quiz/static/ { include /etc/nginx/mime.types; alias /usr/share/nginx/html/quiz/static/; @@ -34,6 +35,12 @@ server { alias /usr/share/nginx/html/admin/editor/static/; } + location ^~ /admin/view/static/ { + include /etc/nginx/mime.types; + alias /usr/share/nginx/html/admin/view/static/; + } + + # Proxy to the main app for all other requests location / { include /etc/nginx/conf.d/proxy_headers.conf; proxy_pass http://reftest; diff --git a/ref-test/app/__init__.py b/ref-test/app/__init__.py index 4122f2c..66dfc25 100644 --- a/ref-test/app/__init__.py +++ b/ref-test/app/__init__.py @@ -46,11 +46,13 @@ def create_app(): from .quiz.views import quiz from .views import views from .editor.views import editor + from .view.views import view 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') return app \ No newline at end of file diff --git a/ref-test/app/admin/static/js/script.js b/ref-test/app/admin/static/js/script.js index 1fd1391..caaee59 100644 --- a/ref-test/app/admin/static/js/script.js +++ b/ref-test/app/admin/static/js/script.js @@ -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).prop('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,21 +57,21 @@ $('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' || action == 'start' || action == 'end') { $.ajax({ @@ -80,25 +80,25 @@ $('.test-action').click(function(event) { 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}/` } - event.preventDefault(); -}); + event.preventDefault() +}) // Edit Dataset Button Handlers $('.edit-question-dataset').click(function(event) { - var id = $(this).data('id'); - 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') { @@ -111,25 +111,27 @@ $('.edit-question-dataset').click(function(event) { }), contentType: 'application/json', success: function(response) { - window.location.reload(); + window.location.reload() }, error: function(response){ - error_response(response); + error_response(response) }, - }); + }) } 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}/` } - }; - event.preventDefault(); -}); + } + 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(` @@ -138,18 +140,18 @@ function error_response(response) { ${response.responseJSON.error} - `); + `) } 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 += ` - `; - $alert.html(output); + ` + $alert.html(output) } } @@ -167,20 +169,20 @@ $('#dismiss-cookie-alert').click(function(event){ }, 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({ @@ -190,13 +192,13 @@ $('.result-action-buttons').click(function(event){ 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({ @@ -206,23 +208,23 @@ $('.result-action-buttons').click(function(event){ 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({ @@ -231,12 +233,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(); -}); \ No newline at end of file + 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() +}) \ No newline at end of file diff --git a/ref-test/app/admin/templates/admin/components/navbar.html b/ref-test/app/admin/templates/admin/components/navbar.html index 48686d4..4dbe334 100644 --- a/ref-test/app/admin/templates/admin/components/navbar.html +++ b/ref-test/app/admin/templates/admin/components/navbar.html @@ -77,10 +77,13 @@ Users
  • - Question Datasets + Manage Questions
  • - Question Editor + View Questions +
  • +
  • + Edit Questions
  • diff --git a/ref-test/app/admin/templates/admin/result-detail.html b/ref-test/app/admin/templates/admin/result-detail.html index 4c60d80..6a8bd6b 100644 --- a/ref-test/app/admin/templates/admin/result-detail.html +++ b/ref-test/app/admin/templates/admin/result-detail.html @@ -114,7 +114,7 @@ {{ scores.scored }} - {{scores.max}} + {{ scores.max }} {% endfor %} @@ -131,6 +131,7 @@
    + View Questions @@ -146,7 +147,7 @@ {% for question, answer in entry.answers.items() %} {% endfor %} diff --git a/ref-test/app/admin/views.py b/ref-test/app/admin/views.py index 39a2599..d8463ef 100644 --- a/ref-test/app/admin/views.py +++ b/ref-test/app/admin/views.py @@ -2,7 +2,7 @@ from ..forms.admin import AddTimeAdjustment, CreateTest, CreateUser, DeleteUser, from ..models import Dataset, Entry, Test, User from ..tools.auth import disable_if_logged_in, require_account_creation from ..tools.forms import get_dataset_choices, get_time_options, send_errors_to_client -from ..tools.data import check_is_json, validate_json +from ..tools.data import check_dataset_exists, check_is_json, validate_json from ..tools.test import answer_options, get_correct_answers from flask import abort, Blueprint, jsonify, render_template, redirect, request, send_file, session @@ -247,15 +247,12 @@ def _download(id:str): @admin.route('/tests//', methods=['GET']) @admin.route('/tests/', methods=['GET']) @login_required +@check_dataset_exists def _tests(filter:str=None): - datasets = Dataset.query.all() tests = None _tests = Test.query.all() form = None now = datetime.now() - if not 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._questions')) if filter not in ['create','active','scheduled','expired','all']: return redirect(url_for('admin._tests', filter='active')) if filter == 'create': form = CreateTest() diff --git a/ref-test/app/editor/static/css/editor.css b/ref-test/app/editor/static/css/editor.css index 84544fd..133e762 100644 --- a/ref-test/app/editor/static/css/editor.css +++ b/ref-test/app/editor/static/css/editor.css @@ -71,7 +71,7 @@ margin: 30pt auto; } -.info-panel { +.info-panel, .viewer-panel { display: none; } @@ -84,4 +84,20 @@ #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; } \ No newline at end of file diff --git a/ref-test/app/editor/static/js/editor.js b/ref-test/app/editor/static/js/editor.js index 85e9fd1..f7eb64e 100644 --- a/ref-test/app/editor/static/js/editor.js +++ b/ref-test/app/editor/static/js/editor.js @@ -5,24 +5,69 @@ 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 Button Listener +// Info and Viewer Button Listener $control_panel.find('button').click(function(event){ - if ($info_panel.is(":hidden")) { - $editor_panel.hide() - $info_panel.fadeIn() - $(this).addClass('active') - } else { - $info_panel.hide() - $editor_panel.fadeIn() - $(this).removeClass('active') + 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() }) @@ -496,4 +541,102 @@ $(window).on('load', function() { console.log(response) } }) -}) \ No newline at end of file +}) + +// 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 = `Question ${block['q_no'] + 1}. ${block['text']}` + obj.append(text) + question_body = document.createElement('div') + question_body.className ='question-body' + type = document.createElement('p') + type.innerHTML = `Question Type: ${block['q_type']}` + question_body.append(type) + options = document.createElement('p') + options.innerHTML = 'Options:' + 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 += ' Correct' + } + option_list.append(option) + } + options.append(option_list) + question_body.append(options) + tags = document.createElement('p') + tags.innerHTML = `Tags:` + 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 = `Block ${i+1}. ${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 = `Question ${question['q_no'] + 1}. ${question['text']}` + block_question.append(text) + question_body = document.createElement('div') + question_body.className ='question-body' + type = document.createElement('p') + type.innerHTML = `Question Type: ${question['q_type']}` + question_body.append(type) + options = document.createElement('p') + options.innerHTML = 'Options:' + 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 += ' Correct' + } + option_list.append(option) + } + options.append(option_list) + question_body.append(options) + tags = document.createElement('p') + tags.innerHTML = `Tags:` + 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) + } +} \ No newline at end of file diff --git a/ref-test/app/editor/static/js/script.js b/ref-test/app/editor/static/js/script.js index bba3bde..f9a08ad 100644 --- a/ref-test/app/editor/static/js/script.js +++ b/ref-test/app/editor/static/js/script.js @@ -57,7 +57,7 @@ function error_response(response) { `); } 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 += `
    - {{ question }} + {{ question|int + 1 }} {{ answers[question|int][answer|int] }} diff --git a/ref-test/app/admin/templates/admin/settings/questions.html b/ref-test/app/admin/templates/admin/settings/questions.html index eed4611..d9e941a 100644 --- a/ref-test/app/admin/templates/admin/settings/questions.html +++ b/ref-test/app/admin/templates/admin/settings/questions.html @@ -57,28 +57,37 @@ class="btn btn-primary edit-question-dataset" data-id="{{ element.id }}" data-action="download" - title="Download Dataset" + title="Download Questions" > - + + + + - - + + - +
    ","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
    ",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 + + ${response.responseJSON.error} + +
    + `) + } else if (response.responseJSON.error instanceof Array) { + var output = '' + for (let i = 0; i < response.responseJSON.error.length; i ++) { + output += ` + + ` + $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() +}) \ No newline at end of file diff --git a/ref-test/app/view/static/js/view.js b/ref-test/app/view/static/js/view.js new file mode 100644 index 0000000..0d78304 --- /dev/null +++ b/ref-test/app/view/static/js/view.js @@ -0,0 +1,130 @@ +// Variable Declarations +const $control_panel = $('.control-panel') +const $info_panel = $('.info-panel') +const $viewer_panel = $('.viewer-panel') + +var element_index = 0 + +// Info Button Listener +$control_panel.find('button').click(function(event){ + if ($info_panel.is(":hidden")) { + $viewer_panel.hide() + $info_panel.fadeIn() + $(this).addClass('active') + } else { + $info_panel.hide() + $viewer_panel.fadeIn() + $(this).removeClass('active') + } + event.preventDefault() +}) + +function parse_data(data) { + 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 = `Question ${block['q_no'] + 1}. ${block['text']}` + obj.append(text) + question_body = document.createElement('div') + question_body.className ='question-body' + type = document.createElement('p') + type.innerHTML = `Question Type: ${block['q_type']}` + question_body.append(type) + options = document.createElement('p') + options.innerHTML = 'Options:' + 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 += ' Correct' + } + option_list.append(option) + } + options.append(option_list) + question_body.append(options) + tags = document.createElement('p') + tags.innerHTML = `Tags:` + 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 = `Block ${i+1}. ${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 = `Question ${question['q_no'] + 1}. ${question['text']}` + block_question.append(text) + question_body = document.createElement('div') + question_body.className ='question-body' + type = document.createElement('p') + type.innerHTML = `Question Type: ${question['q_type']}` + question_body.append(type) + options = document.createElement('p') + options.innerHTML = 'Options:' + 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 += ' Correct' + } + option_list.append(option) + } + options.append(option_list) + question_body.append(options) + tags = document.createElement('p') + tags.innerHTML = `Tags:` + 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) + } +} + +// 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) + } + }) +}) \ No newline at end of file diff --git a/ref-test/app/view/templates/view/components/base.html b/ref-test/app/view/templates/view/components/base.html new file mode 100644 index 0000000..887b851 --- /dev/null +++ b/ref-test/app/view/templates/view/components/base.html @@ -0,0 +1,84 @@ + + + + + + + + + + {% block style %} + {% endblock %} + {% block title %} SKA Referee Test | Admin Console {% endblock %} + {% include "view/components/og-meta.html" %} + + + + {% block navbar %} + {% include "view/components/navbar.html" %} + {% endblock %} + +
    + {% block top_alerts %} + {% include "view/components/server-alerts.html" %} + {% endblock %} + {% block content %}{% endblock %} +
    + + + + + + + + + + + + {% block script %} + {% endblock %} + + \ No newline at end of file diff --git a/ref-test/app/view/templates/view/components/client-alerts.html b/ref-test/app/view/templates/view/components/client-alerts.html new file mode 100644 index 0000000..48a43e2 --- /dev/null +++ b/ref-test/app/view/templates/view/components/client-alerts.html @@ -0,0 +1 @@ +
    \ No newline at end of file diff --git a/ref-test/app/view/templates/view/components/datatable.html b/ref-test/app/view/templates/view/components/datatable.html new file mode 100644 index 0000000..5eb0cb6 --- /dev/null +++ b/ref-test/app/view/templates/view/components/datatable.html @@ -0,0 +1,28 @@ +{% extends "view/components/base.html" %} +{% block datatable_css %} + + + + + + + +{% endblock %} +{% block datatable_scripts %} + + + + + + + + + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/ref-test/app/view/templates/view/components/footer.html b/ref-test/app/view/templates/view/components/footer.html new file mode 100644 index 0000000..e2cea14 --- /dev/null +++ b/ref-test/app/view/templates/view/components/footer.html @@ -0,0 +1,2 @@ +

    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 Vivek’s personal GIT repository under an MIT License.

    +

    All questions in the test are © The Scottish Korfball Association {{ now.year }}. All rights are reserved.

    \ No newline at end of file diff --git a/ref-test/app/view/templates/view/components/input-forms.html b/ref-test/app/view/templates/view/components/input-forms.html new file mode 100644 index 0000000..1136a28 --- /dev/null +++ b/ref-test/app/view/templates/view/components/input-forms.html @@ -0,0 +1,4 @@ +{% extends "view/components/base.html" %} +{% import "bootstrap/wtf.html" as wtf %} +{% block top_alerts %} +{% endblock %} \ No newline at end of file diff --git a/ref-test/app/view/templates/view/components/navbar.html b/ref-test/app/view/templates/view/components/navbar.html new file mode 100644 index 0000000..4dbe334 --- /dev/null +++ b/ref-test/app/view/templates/view/components/navbar.html @@ -0,0 +1,117 @@ + \ No newline at end of file diff --git a/ref-test/app/view/templates/view/components/og-meta.html b/ref-test/app/view/templates/view/components/og-meta.html new file mode 100644 index 0000000..45453e1 --- /dev/null +++ b/ref-test/app/view/templates/view/components/og-meta.html @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ref-test/app/view/templates/view/components/secondary-navs/tests.html b/ref-test/app/view/templates/view/components/secondary-navs/tests.html new file mode 100644 index 0000000..a007e23 --- /dev/null +++ b/ref-test/app/view/templates/view/components/secondary-navs/tests.html @@ -0,0 +1,23 @@ + \ No newline at end of file diff --git a/ref-test/app/view/templates/view/components/server-alerts.html b/ref-test/app/view/templates/view/components/server-alerts.html new file mode 100644 index 0000000..bcec7d1 --- /dev/null +++ b/ref-test/app/view/templates/view/components/server-alerts.html @@ -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" %} + + {% elif category == "success" %} + + {% elif category == "warning" %} + + {% elif category == "cookie_alert" %} + {% if not cookie_flash_flag.value %} + + {% set cookie_flash_flag.value = True %} + {% endif %} + {% else %} + + {% endif %} + {% endfor %} + {% endif %} +{% endwith %} \ No newline at end of file diff --git a/ref-test/app/view/templates/view/console.html b/ref-test/app/view/templates/view/console.html new file mode 100644 index 0000000..5de1fed --- /dev/null +++ b/ref-test/app/view/templates/view/console.html @@ -0,0 +1,116 @@ +{% extends "view/components/base.html" %} + +{% block style %} + +{% endblock %} + +{% block content %} +

    View Questions

    +
    +

    + This page lists all the questions in the selected dataset. +

    +
    +
    + +
    +
    +

    + Information +

    +

    + Questions in the test are arranged in blocks. Blocks can be of two types: Blocks of multiple related questions, and Single Questions that are not part of a block. + You can add, remove, or edit both Blockss and Questions through this editor. +

    +

    + Blocks 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. +

    +

    + Blocks can contain any number of questions within them, but cannot contain nested blocks. +

    +

    + When you set up a block, you can also add header text that will be displayed with each question. + You can use this to provide common information for a scenario across a series of questions. +

    +

    + Questions come in three types: +

    +

    +

    + Normally, multiple choice questions will have the order of the options randomised. +

    +

    + 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. +

    +

    + Questions can also be categorised using tags. +

    +

    + Placeholder for Questions Remaining in a Block +

    +

    + In order to show how many questions are remaining inside a block, e.g. to say ‘the next n questions are about a specific scenario’, the app uses the placeholder <block_remaining_questions>. +

    +
    +
    +

    + Question Dataset +

    + + +
    +{% endblock %} + +{% block script %} + + +{% endblock %} \ No newline at end of file diff --git a/ref-test/app/view/templates/view/index.html b/ref-test/app/view/templates/view/index.html new file mode 100644 index 0000000..78d06c9 --- /dev/null +++ b/ref-test/app/view/templates/view/index.html @@ -0,0 +1,27 @@ +{% extends "view/components/input-forms.html" %} + +{% block content %} +
    +
    + {% include "admin/components/server-alerts.html" %} +

    View Questions

    + {{ form.hidden_tag() }} +
    + {{ form.dataset(placeholder="Select Question Dataset") }} + {{ form.dataset.label }} +
    + {% include "admin/components/client-alerts.html" %} +
    +
    +
    + +
    +
    +
    +
    + +
    +{% endblock %} \ No newline at end of file diff --git a/ref-test/app/view/views.py b/ref-test/app/view/views.py new file mode 100644 index 0000000..b13a8ef --- /dev/null +++ b/ref-test/app/view/views.py @@ -0,0 +1,41 @@ +from ..forms.admin import EditDataset +from ..models import Dataset, User +from ..tools.forms import get_dataset_choices, send_errors_to_client +from ..tools.data import check_dataset_exists + +from flask import Blueprint, flash, jsonify, redirect, render_template, request +from flask.helpers import url_for +from flask_login import login_required + +view = Blueprint( + name='view', + import_name=__name__, + template_folder='templates', + static_folder='static' +) + +@view.route('/', methods=['GET','POST']) +@login_required +@check_dataset_exists +def _view(): + 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('view._view_console', id=id)}),200 + return send_errors_to_client(form=form) + form.process() + return render_template('/view/index.html', form=form) + +@view.route('//') +@login_required +@check_dataset_exists +def _view_console(id:str=None): + dataset = Dataset.query.filter_by(id=id).first() + datasets = Dataset.query.count() + users = User.query.all() + if not dataset: + flash('Invalid dataset ID.', 'error') + return redirect(url_for('admin._questions')) + return render_template('/view/console.html', dataset=dataset, datasets=datasets, users=users) \ No newline at end of file