Merge branch 'development' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test into development
This commit is contained in:
		| @@ -88,6 +88,19 @@ $('.test-action').click(function(event) { | ||||
|         }) | ||||
|     } else if (action == 'edit') { | ||||
|         window.location.href = `/admin/test/${id}/` | ||||
|     } else if (action == 'analyse') { | ||||
|         $.ajax({ | ||||
|             url: `/admin/analysis/`, | ||||
|             type: 'POST', | ||||
|             data: JSON.stringify({'id': id, 'class': 'test'}), | ||||
|             contentType: 'application/json', | ||||
|             success: function(response) { | ||||
|                 window.location.href = response | ||||
|             }, | ||||
|             error: function(response){ | ||||
|                 error_response(response) | ||||
|             }, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     event.preventDefault() | ||||
| @@ -123,6 +136,19 @@ $('.edit-question-dataset').click(function(event) { | ||||
|             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() | ||||
|   | ||||
| @@ -108,7 +108,7 @@ | ||||
|                                             {% for tag, scores in entry.result.tags.items() %} | ||||
|                                                 <tr> | ||||
|                                                     <td> | ||||
|                                                         {{ tag }} | ||||
|                                                         {{ tag|safe }} | ||||
|                                                     </td> | ||||
|                                                     <td> | ||||
|                                                         {{ scores.scored }} | ||||
|   | ||||
| @@ -52,6 +52,15 @@ | ||||
|                             {{ element.tests|length }} | ||||
|                         </td> | ||||
|                         <td class="row-actions"> | ||||
|                             <a | ||||
|                                 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" | ||||
| @@ -63,7 +72,7 @@ | ||||
|                             </a> | ||||
|                             <a | ||||
|                                 href="javascript:void(0)" | ||||
|                                 class="btn btn-primary view-question-dataset" | ||||
|                                 class="btn btn-primary edit-question-dataset" | ||||
|                                 data-id="{{ element.id }}" | ||||
|                                 data-action="view" | ||||
|                                 title="View Questions" | ||||
|   | ||||
| @@ -162,7 +162,7 @@ | ||||
|                     {% include "admin/components/client-alerts.html" %} | ||||
|                 </div> | ||||
|                 <div class="container justify-content-center"> | ||||
|                     <div class="row"> | ||||
|                     <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> | ||||
| @@ -174,6 +174,16 @@ | ||||
|                                 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 | ||||
|   | ||||
| @@ -58,6 +58,15 @@ | ||||
|                             {{ 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" | ||||
|   | ||||
| @@ -251,7 +251,6 @@ def _questions(): | ||||
|             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}') | ||||
| @@ -281,7 +280,7 @@ def _download(id:str): | ||||
|         return abort(500) | ||||
|     if not dataset: return abort(404) | ||||
|     data_path = path.abspath(dataset.get_file()) | ||||
|     return send_file(data_path, as_attachment=True, attachment_filename=f'{dataset.get_name()}.json') | ||||
|     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']) | ||||
| @@ -435,10 +434,7 @@ def _view_entry(id:str=None): | ||||
|         flash('Invalid entry ID.', 'error') | ||||
|         return redirect(url_for('admin._view_entries')) | ||||
|     test = entry.test | ||||
|     dataset = test.dataset | ||||
|     dataset_path = dataset.get_file() | ||||
|     with open(dataset_path, 'r') as _dataset: | ||||
|         data = loads(_dataset.read()) | ||||
|     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) | ||||
|   | ||||
| @@ -1,30 +1,8 @@ | ||||
| .info-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; | ||||
| .cell-percentage::after { | ||||
|     content: '%'; | ||||
| } | ||||
| @@ -1,130 +1,27 @@ | ||||
| // Variable Declarations | ||||
| const $control_panel = $('.control-panel') | ||||
| const $info_panel = $('.info-panel') | ||||
| const $viewer_panel = $('.viewer-panel') | ||||
| // Analyse Button Listener | ||||
| $('.button-analyse').click(function(event) { | ||||
|  | ||||
| var element_index = 0 | ||||
|     let buttonClass = $(this).data('class') | ||||
|     let id = null | ||||
|  | ||||
| // 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') | ||||
|     if (buttonClass == 'test' ) { | ||||
|         id = $('#select-test').children('option:selected').val() | ||||
|     } else if (buttonClass == 'dataset' ) { | ||||
|         id = $('#select-dataset').children('option:selected').val() | ||||
|     } | ||||
|     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 = `<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) | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Fetch data once page finishes loading | ||||
| $(window).on('load', function() { | ||||
|     $.ajax({ | ||||
|         url: target, | ||||
|         url: `/admin/analysis/`, | ||||
|         type: 'POST', | ||||
|         data: JSON.stringify({ | ||||
|             'id': id, | ||||
|             'action': 'fetch' | ||||
|         }), | ||||
|         data: JSON.stringify({'id': id, 'class': buttonClass}), | ||||
|         contentType: 'application/json', | ||||
|         success: function(response) { | ||||
|             parse_data(response['data']) | ||||
|             window.location.href = response | ||||
|         }, | ||||
|         error: function(response){ | ||||
|             error_response(response) | ||||
|         }, | ||||
|         error: function(response) { | ||||
|             console.log(response) | ||||
|         } | ||||
|     }) | ||||
|  | ||||
|     event.preventDefault() | ||||
| }) | ||||
| @@ -1,74 +1,22 @@ | ||||
| {% extends "view/components/base.html" %} | ||||
| {% extends "analysis/components/datatable.html" %} | ||||
|  | ||||
| {% block style %} | ||||
| <link  | ||||
|     rel="stylesheet" | ||||
|     href="{{ url_for('.static', filename='css/view.css') }}" | ||||
|     href="{{ url_for('.static', filename='css/analysis.css') }}" | ||||
| /> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block content %} | ||||
|     <h1>View Questions</h1> | ||||
|     <h1>Analysis</h1> | ||||
|     <div class="container"> | ||||
|         <p class="lead"> | ||||
|             This page lists all the questions in the selected dataset. | ||||
|             Analysis for {{ type }} {{ subject }}. | ||||
|         </p> | ||||
|     </div> | ||||
|     <div class="container control-panel"> | ||||
|         <button class="btn btn-primary" aria-title="Information" title="Information"><i class="bi bi-info-circle-fill"></i></button> | ||||
|     </div> | ||||
|     <div class="container info-panel"> | ||||
|     <div class="container"> | ||||
|         <h3> | ||||
|             Information | ||||
|         </h3> | ||||
|         <p> | ||||
|             Questions in the test are arranged in blocks. 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> | ||||
|             Questions can also be categorised using <strong>tags</strong>. | ||||
|         </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 ‘the next n questions are about a specific scenario’, the app uses the placeholder <code><block_remaining_questions></code>. | ||||
|         </p> | ||||
|     </div> | ||||
|     <div class="container viewer-panel"> | ||||
|         <h3> | ||||
|             Question Dataset | ||||
|             Question List | ||||
|         </h3> | ||||
|         <div class="container dataset-metadata"> | ||||
|             <div class="input-group mb-3"> | ||||
| @@ -100,7 +48,65 @@ | ||||
|                 </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 %} | ||||
|  | ||||
| @@ -111,6 +117,54 @@ | ||||
| </script> | ||||
| <script | ||||
|     type="text/javascript" | ||||
|     src="{{ url_for('.static', filename='js/view.js') }}" | ||||
|     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 %} | ||||
| @@ -22,24 +22,24 @@ | ||||
|         {% block style %} | ||||
|         {% endblock %} | ||||
|         <title>{% block title %} SKA Referee Test | Admin Console {% endblock %}</title> | ||||
|         {% include "view/components/og-meta.html" %} | ||||
|         {% include "analysis/components/og-meta.html" %} | ||||
|     </head> | ||||
|     <body class="bg-light"> | ||||
|  | ||||
|         {% block navbar %} | ||||
|             {% include "view/components/navbar.html" %} | ||||
|             {% include "analysis/components/navbar.html" %} | ||||
|         {% endblock %} | ||||
|  | ||||
|         <div class="container"> | ||||
|             {% block top_alerts %} | ||||
|                 {% include "view/components/server-alerts.html" %} | ||||
|                 {% include "analysis/components/server-alerts.html" %} | ||||
|             {% endblock %} | ||||
|             {% block content %}{% endblock %} | ||||
|         </div> | ||||
|  | ||||
|         <footer class="container site-footer mt-5"> | ||||
|             {% block footer %} | ||||
|                 {% include "view/components/footer.html" %} | ||||
|                 {% include "analysis/components/footer.html" %} | ||||
|             {% endblock %} | ||||
|         </footer> | ||||
|  | ||||
| @@ -78,7 +78,15 @@ | ||||
|             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> | ||||
| @@ -1,4 +1,4 @@ | ||||
| {% extends "view/components/base.html" %} | ||||
| {% 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"/> | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| {% extends "view/components/base.html" %} | ||||
| {% extends "analysis/components/base.html" %} | ||||
| {% import "bootstrap/wtf.html" as wtf %} | ||||
| {% block top_alerts %} | ||||
| {% endblock %} | ||||
| @@ -1,4 +1,4 @@ | ||||
| {% extends "view/components/input-forms.html" %} | ||||
| {% extends "analysis/components/input-forms.html" %} | ||||
|  | ||||
| {% block content %} | ||||
|     <h1>Analysis</h1> | ||||
| @@ -9,7 +9,19 @@ | ||||
|                     <div class="card-body"> | ||||
|                         <h5 class="card-title">Exams</h5> | ||||
|                         <div class="card-text"> | ||||
|                             {{ tests }} | ||||
|                             <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> | ||||
| @@ -19,11 +31,24 @@ | ||||
|                     <div class="card-body"> | ||||
|                         <h5 class="card-title">Datasets</h5> | ||||
|                         <div class="card-text"> | ||||
|                             {{ datasets }} | ||||
|                             <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 %} | ||||
| @@ -1,8 +1,9 @@ | ||||
| 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, jsonify, render_template, request | ||||
| from flask import Blueprint, render_template, request, jsonify | ||||
| from flask.helpers import abort, flash, redirect, url_for | ||||
| from flask_login import login_required | ||||
|  | ||||
| @@ -18,10 +19,33 @@ analysis = Blueprint( | ||||
| @check_dataset_exists | ||||
| @check_test_exists | ||||
| def _analysis(): | ||||
|     _tests = Test.query.all() | ||||
|     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.query.all() | ||||
|     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>') | ||||
| @@ -40,8 +64,7 @@ def _test(id:str=None): | ||||
|     if not test: | ||||
|         flash('Invalid exam.', 'error') | ||||
|         return redirect(url_for('analysis._analysis')) | ||||
|     return jsonify(analyse(test)) | ||||
|     return render_template('/analysis/analysis.html', analysis=None, text='Exam') | ||||
|     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/') | ||||
| @@ -59,4 +82,4 @@ def _dataset(id:str=None): | ||||
|     if not dataset: | ||||
|         flash('Invalid dataset.', 'error') | ||||
|         return redirect(url_for('analysis._analysis')) | ||||
|     return jsonify(analyse(dataset)) | ||||
|     return render_template('/analysis/analysis.html', analysis=analyse(dataset), subject=dataset.get_name(), type='dataset', dataset=dataset, questions=parse_questions(dataset.get_data())) | ||||
| @@ -8,7 +8,7 @@ from flask_login import current_user | ||||
| from werkzeug.utils import secure_filename | ||||
|  | ||||
| from datetime import datetime | ||||
| from json import dump | ||||
| from json import dump, loads | ||||
| from os import path, remove | ||||
| from pathlib import Path | ||||
| from uuid import uuid4 | ||||
| @@ -116,6 +116,12 @@ class Dataset(db.Model): | ||||
|         file_path = path.join(data, 'questions', filename) | ||||
|         return file_path | ||||
|      | ||||
|     def get_data(self): | ||||
|         dataset_path = self.get_file() | ||||
|         with open(dataset_path, 'r') as _dataset: | ||||
|             data = loads(_dataset.read()) | ||||
|         return data | ||||
|  | ||||
|     def update(self, data:list=None, default:bool=False): | ||||
|         self.date = datetime.now() | ||||
|         if default: self.make_default() | ||||
|   | ||||
| @@ -117,7 +117,6 @@ def analyse(subject:Union[Dataset,Test]) -> dict: | ||||
|         } | ||||
|     } | ||||
|     scores_raw = [] | ||||
|     dataset = subject if isinstance(subject, Dataset) else subject.dataset | ||||
|     if isinstance(subject, Test): | ||||
|         for entry in subject.entries: | ||||
|             if entry.answers: | ||||
| @@ -131,7 +130,6 @@ def analyse(subject:Union[Dataset,Test]) -> dict: | ||||
|                 scores_raw.append(int(entry.result['score']))         | ||||
|     else: | ||||
|         for test in subject.tests: | ||||
|             output['entries'] += len(test.entries) | ||||
|             for entry in test.entries: | ||||
|                 if entry.answers: | ||||
|                     for question, answer in entry.answers.items(): | ||||
| @@ -145,4 +143,26 @@ def analyse(subject:Union[Dataset,Test]) -> dict: | ||||
|     output['scores']['mean'] = mean(scores_raw) | ||||
|     output['scores']['median'] = median(scores_raw) | ||||
|     output['scores']['stdev'] = stdev(scores_raw, output['scores']['mean']) if len(scores_raw) > 1 else None | ||||
|     return output | ||||
|  | ||||
| def parse_questions(dataset:list): | ||||
|     output = [] | ||||
|     for block in dataset: | ||||
|         if block['type'] == 'question': | ||||
|             question = { | ||||
|                 'q_no': block['q_no'], | ||||
|                 'tags': block['tags'], | ||||
|                 'correct': block['correct'] | ||||
|             } | ||||
|             question['options'] = [*enumerate(block['options'])] | ||||
|             output.append(question) | ||||
|         elif block['type'] == 'block': | ||||
|             for _question in block['questions']: | ||||
|                 question = { | ||||
|                     'q_no': _question['q_no'], | ||||
|                     'tags': _question['tags'], | ||||
|                     'correct': _question['correct'] | ||||
|                 } | ||||
|                 question['options'] = [*enumerate(_question['options'])] | ||||
|                 output.append(question) | ||||
|     return output | ||||
| @@ -10,9 +10,10 @@ from functools import wraps | ||||
| def parse_test_code(code): | ||||
|     return code.replace('—', '').lower() | ||||
|  | ||||
| def generate_questions(dataset:list): | ||||
| def generate_questions(dataset:list, randomise:bool=True): | ||||
|     output = [] | ||||
|     for block in randomise_list(dataset): | ||||
|     question_dataset = randomise_list(dataset) if randomise else dataset | ||||
|     for block in question_dataset: | ||||
|         if block['type'] == 'question': | ||||
|             question = { | ||||
|                 'type': 'question', | ||||
| @@ -20,11 +21,12 @@ def generate_questions(dataset:list): | ||||
|                 'question_header': '', | ||||
|                 'text': block['text'] | ||||
|             } | ||||
|             if block['q_type'] == 'Multiple Choice': question['options'] = randomise_list([*enumerate(block['options'])]) | ||||
|             if block['q_type'] == 'Multiple Choice' and randomise: question['options'] = randomise_list([*enumerate(block['options'])]) | ||||
|             else: question['options'] = [*enumerate(block['options'])] | ||||
|             output.append(question) | ||||
|         elif block['type'] == 'block': | ||||
|             for key, _question in enumerate(randomise_list(block['questions'])): | ||||
|             block_questions = randomise_list(block['questions']) if randomise else block['questions'] | ||||
|             for key, _question in enumerate(block_questions): | ||||
|                 question = { | ||||
|                     'type': 'block', | ||||
|                     'q_no': _question['q_no'], | ||||
| @@ -33,7 +35,7 @@ def generate_questions(dataset:list): | ||||
|                     'block_q_no': key, | ||||
|                     'text': _question['text'] | ||||
|                 } | ||||
|                 if _question['q_type'] == 'Multiple Choice': question['options'] = randomise_list([*enumerate(_question['options'])]) | ||||
|                 if _question['q_type'] == 'Multiple Choice' and randomise: question['options'] = randomise_list([*enumerate(_question['options'])]) | ||||
|                 else: question['options'] = [*enumerate(_question['options'])] | ||||
|                 output.append(question) | ||||
|     return output | ||||
|   | ||||
| @@ -110,6 +110,27 @@ function parse_data(data) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Analyse Button | ||||
| $('.dataset-analyse').click(function(event) { | ||||
|      | ||||
|     let id = $(this).data('id') | ||||
|  | ||||
|     $.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() | ||||
| }) | ||||
|  | ||||
| // Fetch data once page finishes loading | ||||
| $(window).on('load', function() { | ||||
|     $.ajax({ | ||||
|   | ||||
| @@ -100,7 +100,18 @@ | ||||
|                 </div> | ||||
|             {% endif %} | ||||
|         </div> | ||||
|          | ||||
|         <div class="d-flex justify-content-center"> | ||||
|             <a | ||||
|                 href="#" | ||||
|                 class="btn btn-success dataset-analyse {% if not dataset.entries %} disabled {% endif %}" | ||||
|                 data-id="{{dataset.id}}" | ||||
|                 title="Analyse Answers" | ||||
|                 data-action="analyse" | ||||
|             > | ||||
|                 <i class="bi bi-search button-icon"></i> | ||||
|                 Analyse Answers | ||||
|             </a> | ||||
|         </div> | ||||
|     </div> | ||||
| {% endblock %} | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user