Added results CRUD and result detailed view
This commit is contained in:
		@@ -64,6 +64,10 @@ class Test:
 | 
			
		||||
        return test_code.replace('—', '')
 | 
			
		||||
    
 | 
			
		||||
    def delete(self):
 | 
			
		||||
        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}):
 | 
			
		||||
 
 | 
			
		||||
@@ -214,6 +214,11 @@ table.dataTable {
 | 
			
		||||
    font-size: 14pt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.result-action-buttons {
 | 
			
		||||
    margin: 5px auto;
 | 
			
		||||
    width: fit-content;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Fallback for Edge
 | 
			
		||||
-------------------------------------------------- */
 | 
			
		||||
@supports (-ms-ime-align: auto) {
 | 
			
		||||
 
 | 
			
		||||
@@ -154,3 +154,42 @@ $('#dismiss-cookie-alert').click(function(event){
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// Script for Resutlt Actions
 | 
			
		||||
$('.result-action-buttons').click(function(event){
 | 
			
		||||
 | 
			
		||||
    var _id = $(this).data('_id')
 | 
			
		||||
 | 
			
		||||
    if ($(this).data('result-action') == 'generate') {
 | 
			
		||||
        $.ajax({
 | 
			
		||||
            url: '/admin/certificate/',
 | 
			
		||||
            type: 'POST',
 | 
			
		||||
            data: JSON.stringify({'_id': _id}),
 | 
			
		||||
            contentType: 'application/json',
 | 
			
		||||
            dataType: 'html',
 | 
			
		||||
            success: function(response) {
 | 
			
		||||
                var display_window = window.open();
 | 
			
		||||
                display_window.document.write(response);
 | 
			
		||||
            },
 | 
			
		||||
            error: function(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}),
 | 
			
		||||
            contentType: 'application/json',
 | 
			
		||||
            success: function(response) {
 | 
			
		||||
                if (action == 'delete') {
 | 
			
		||||
                    window.location.href = '/admin/results/';
 | 
			
		||||
                } else window.location.reload();
 | 
			
		||||
            },
 | 
			
		||||
            error: function(response){
 | 
			
		||||
                error_response(response);
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
@@ -32,8 +32,10 @@
 | 
			
		||||
            {% block content %}{% endblock %}
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <footer class="container site-footer">
 | 
			
		||||
        <footer class="container site-footer mt-5">
 | 
			
		||||
            {% block footer %}
 | 
			
		||||
                {% include "admin/components/footer.html" %}
 | 
			
		||||
            {% endblock %}
 | 
			
		||||
        </footer>
 | 
			
		||||
 | 
			
		||||
        <!-- JQuery, Popper, and Bootstrap js dependencies -->
 | 
			
		||||
@@ -70,5 +72,7 @@
 | 
			
		||||
        ></script>
 | 
			
		||||
        {% block datatable_scripts %}
 | 
			
		||||
        {% endblock %}
 | 
			
		||||
        {% block custom_data_script %}
 | 
			
		||||
        {% endblock %}
 | 
			
		||||
    </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										84
									
								
								ref-test/admin/templates/admin/components/certificate.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								ref-test/admin/templates/admin/components/certificate.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,84 @@
 | 
			
		||||
{% extends "admin/components/base.html" %}
 | 
			
		||||
{% block title %} SKA Referee Test | Detailed Results {% endblock %}
 | 
			
		||||
{% block navbar %}{% endblock %}
 | 
			
		||||
{% block top_alerts %}{% endblock %}
 | 
			
		||||
{% block content %}
 | 
			
		||||
    <div class="d-flex justify-content-center">
 | 
			
		||||
        <h1 class="center">SKA Referee Theory Exam Results</h1>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="container mt-5">
 | 
			
		||||
        <div class="row justify-content-center">
 | 
			
		||||
            <div class="col-lg-6">
 | 
			
		||||
                <ul class="list-group">
 | 
			
		||||
                    <li class="list-group-item list-group-item-action">
 | 
			
		||||
                        <div class="d-flex w-100 justify-content-between">
 | 
			
		||||
                            <h5 class="mb-1">Candidate</h5>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <h2>
 | 
			
		||||
                            {{ entry.name.surname}}, {{ entry.name.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 }}
 | 
			
		||||
                    </li>
 | 
			
		||||
                    {% if entry['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 }}
 | 
			
		||||
                        </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:]]) }}
 | 
			
		||||
                    </li>
 | 
			
		||||
                    {% 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>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            {{ entry.user_code }}
 | 
			
		||||
                        </li>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    <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') }}
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <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>
 | 
			
		||||
                            {% if entry.status == 'late' %}
 | 
			
		||||
                                <span class="badge bg-danger">Late</span>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {{ entry.submission_time.strftime('%d %b %Y %H:%M:%S') }}
 | 
			
		||||
                    </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 }}%
 | 
			
		||||
                    </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 %}">
 | 
			
		||||
                        <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:]}}
 | 
			
		||||
                    </li>
 | 
			
		||||
                </ul>
 | 
			
		||||
                <div class="site-footer mt-5">
 | 
			
		||||
                    These results were generated using the SKA RefTest web app on {{ now.strftime('%d %b %Y at %H:%M:%S') }}.
 | 
			
		||||
                </div>
 | 
			
		||||
                {% block footer %}{% endblock %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -6,6 +6,7 @@
 | 
			
		||||
    <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>
 | 
			
		||||
@@ -23,5 +24,5 @@
 | 
			
		||||
<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>
 | 
			
		||||
    {% block custom_data_script %}{% endblock %}
 | 
			
		||||
<script type="text/javascript" src="https://cdn.datatables.net/searchbuilder/1.3.0/js/dataTables.searchBuilder.min.js"></script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										175
									
								
								ref-test/admin/templates/admin/result-detail.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								ref-test/admin/templates/admin/result-detail.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,175 @@
 | 
			
		||||
{% extends "admin/components/base.html" %}
 | 
			
		||||
{% block title %} SKA Referee Test | Detailed Results {% endblock %}
 | 
			
		||||
{% block content %}
 | 
			
		||||
    {% include "admin/components/client-alerts.html" %}
 | 
			
		||||
    <h1>Exam Results</h1>
 | 
			
		||||
 | 
			
		||||
    <div class="container">
 | 
			
		||||
        <div class="row justify-content-center">
 | 
			
		||||
            <div class="col-lg-6">          
 | 
			
		||||
                <ul class="list-group">
 | 
			
		||||
                    <li class="list-group-item list-group-item-action">
 | 
			
		||||
                        <div class="d-flex w-100 justify-content-between">
 | 
			
		||||
                            <h5 class="mb-1">Candidate</h5>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <h2>
 | 
			
		||||
                            {{ entry.name.surname}}, {{ entry.name.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 }}
 | 
			
		||||
                    </li>
 | 
			
		||||
                    {% if entry['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 }}
 | 
			
		||||
                        </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:]]) }}
 | 
			
		||||
                    </li>
 | 
			
		||||
                    {% 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>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            {{ entry.user_code }}
 | 
			
		||||
                        </li>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    <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') }}
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <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>
 | 
			
		||||
                            {% if entry.status == 'late' %}
 | 
			
		||||
                                <span class="badge bg-danger">Late</span>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {{ entry.submission_time.strftime('%d %b %Y %H:%M:%S') }}
 | 
			
		||||
                    </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 }}%
 | 
			
		||||
                    </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 %}">
 | 
			
		||||
                        <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:]}}
 | 
			
		||||
                    </li>
 | 
			
		||||
                </ul>
 | 
			
		||||
                <div class="accordion" id="results-breakdown">
 | 
			
		||||
                    <div class="accordion-item">
 | 
			
		||||
                        <h2 class="accordion-header" id="by-category">
 | 
			
		||||
                            <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#by-category-breakdown" aria-expanded="false" aria-controls="by-category-breakdown">
 | 
			
		||||
                                Score By Categories
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </h2>
 | 
			
		||||
                        <div id="by-category-breakdown" class="accordion-collapse collapse" aria-labelledby="by-category" data-bs-parent="#results-breakdown">
 | 
			
		||||
                            <div class="accordion-body">
 | 
			
		||||
                                <table class="table table-striped">
 | 
			
		||||
                                    <thead>
 | 
			
		||||
                                        <tr>
 | 
			
		||||
                                            <th>
 | 
			
		||||
                                                Category
 | 
			
		||||
                                            </th>
 | 
			
		||||
                                            <th>
 | 
			
		||||
                                                Score
 | 
			
		||||
                                            </th>
 | 
			
		||||
                                            <th>
 | 
			
		||||
                                                Max
 | 
			
		||||
                                            </th>
 | 
			
		||||
                                        </tr>
 | 
			
		||||
                                    </thead>
 | 
			
		||||
                                    <tbody>
 | 
			
		||||
                                        {% for tag, scores in entry.results.tags.items() %}
 | 
			
		||||
                                            <tr>
 | 
			
		||||
                                                <td>
 | 
			
		||||
                                                    {{ tag }}
 | 
			
		||||
                                                </td>
 | 
			
		||||
                                                <td>
 | 
			
		||||
                                                    {{ scores.scored }}
 | 
			
		||||
                                                </td>
 | 
			
		||||
                                                <td>
 | 
			
		||||
                                                    {{scores.max}}
 | 
			
		||||
                                                </td>
 | 
			
		||||
                                            </tr>
 | 
			
		||||
                                        {% endfor %}
 | 
			
		||||
                                    </tbody>
 | 
			
		||||
                                </table>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="accordion-item">
 | 
			
		||||
                        <h2 class="accordion-header" id="by-question">
 | 
			
		||||
                            <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#by-question-breakdown" aria-expanded="false" aria-controls="by-question-breakdown">
 | 
			
		||||
                                View All Answers
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </h2>
 | 
			
		||||
                    <div id="by-question-breakdown" class="accordion-collapse collapse" aria-labelledby="by-question" data-bs-parent="#results-breakdown">
 | 
			
		||||
                        <div class="accordion-body">
 | 
			
		||||
                            <table class="table table-striped">
 | 
			
		||||
                                    <thead>
 | 
			
		||||
                                        <tr>
 | 
			
		||||
                                            <th>
 | 
			
		||||
                                                Question
 | 
			
		||||
                                            </th>
 | 
			
		||||
                                            <th>
 | 
			
		||||
                                                Answer
 | 
			
		||||
                                            </th>
 | 
			
		||||
                                        </tr>
 | 
			
		||||
                                    </thead>
 | 
			
		||||
                                    <tbody>
 | 
			
		||||
                                        {% for question, answer in entry.answers.items() %}
 | 
			
		||||
                                            <tr>
 | 
			
		||||
                                                <td>
 | 
			
		||||
                                                    {{ question }}
 | 
			
		||||
                                                </td>
 | 
			
		||||
                                                <td>
 | 
			
		||||
                                                    {{ answer }}
 | 
			
		||||
                                                </td>
 | 
			
		||||
                                            </tr>
 | 
			
		||||
                                        {% endfor %}
 | 
			
		||||
                                    </tbody>
 | 
			
		||||
                            </table>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <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 }}">
 | 
			
		||||
                            <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 }}">
 | 
			
		||||
                                    <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 }}">
 | 
			
		||||
                            <i class="bi bi-trash-fill button-icon"></i>
 | 
			
		||||
                            Delete Result
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -1 +1,138 @@
 | 
			
		||||
{% extends "admin/components/base.html" %}
 | 
			
		||||
{% extends "admin/components/datatable.html" %}
 | 
			
		||||
{% block title %} SKA Referee Test | View Results {% endblock %}
 | 
			
		||||
{% block content %}
 | 
			
		||||
    {% include "admin/components/client-alerts.html" %}
 | 
			
		||||
    <h1>View Results</h1>
 | 
			
		||||
    {% if entries %}
 | 
			
		||||
        <table id="results-table" class="table table-striped" style="width:100%">
 | 
			
		||||
            <thead>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <th data-priority="1">
 | 
			
		||||
                        Name
 | 
			
		||||
                    </th>
 | 
			
		||||
                    <th data-priority="4">
 | 
			
		||||
                        Club
 | 
			
		||||
                    </th>
 | 
			
		||||
                    <th data-priority="5">
 | 
			
		||||
                        Exam Code
 | 
			
		||||
                    </th>
 | 
			
		||||
                    <th data-priority="3">
 | 
			
		||||
                        Status
 | 
			
		||||
                    </th>
 | 
			
		||||
                    <th data-priority="4">
 | 
			
		||||
                        Submitted
 | 
			
		||||
                    </th>
 | 
			
		||||
                    <th data-priority="2">
 | 
			
		||||
                        Result
 | 
			
		||||
                    </th>
 | 
			
		||||
                    <th data-priority="3">
 | 
			
		||||
                        Grade
 | 
			
		||||
                    </th>
 | 
			
		||||
                    <th data-priority="1">
 | 
			
		||||
                        Details
 | 
			
		||||
                    </th>
 | 
			
		||||
                </tr>
 | 
			
		||||
            </thead>
 | 
			
		||||
            <tbody>
 | 
			
		||||
                {% for entry in entries %}
 | 
			
		||||
                    <tr class="table-row">
 | 
			
		||||
                        <td>
 | 
			
		||||
                            {{ entry.name.surname }}, {{ entry.name.first_name }}
 | 
			
		||||
                        </td>
 | 
			
		||||
                        <td>
 | 
			
		||||
                            {% if 'club' in entry %}
 | 
			
		||||
                                {{ entry.club }}
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </td>
 | 
			
		||||
                        <td>
 | 
			
		||||
                            {{ '—'.join([entry.test_code[:4], entry.test_code[4:8], entry.test_code[8:]]) }}
 | 
			
		||||
                        </td>
 | 
			
		||||
                        <td>
 | 
			
		||||
                            {% if 'status' in entry %}
 | 
			
		||||
                                {{ entry.status }}
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </td>
 | 
			
		||||
                        <td>
 | 
			
		||||
                            {% if 'submission_time' in entry %}
 | 
			
		||||
                                {{ entry.submission_time.strftime('%d %b %Y') }}
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </td>
 | 
			
		||||
                        <td>
 | 
			
		||||
                            {% if 'results' in entry %}
 | 
			
		||||
                                {{ entry.results.score }}%
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </td>
 | 
			
		||||
                        <td>
 | 
			
		||||
                            {% if 'results' in entry %}
 | 
			
		||||
                                {{ entry.results.grade }}
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </td>
 | 
			
		||||
                        <td class="row-actions">
 | 
			
		||||
                            <a
 | 
			
		||||
                                href="{{ url_for('admin_views.view_entry', _id = entry._id ) }}"
 | 
			
		||||
                                class="btn btn-primary entry-details"
 | 
			
		||||
                                data-_id="{{entry._id}}"
 | 
			
		||||
                                title="View Details"
 | 
			
		||||
                            >
 | 
			
		||||
                                <i class="bi bi-file-medical-fill button-icon"></i>
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
    {% 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 exam attempts to view.
 | 
			
		||||
            </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% if entries %}
 | 
			
		||||
    {% block custom_data_script %}
 | 
			
		||||
        <script>
 | 
			
		||||
            $(document).ready(function() {
 | 
			
		||||
                $('#results-table').DataTable({
 | 
			
		||||
                    'searching': false,
 | 
			
		||||
                    'columnDefs': [
 | 
			
		||||
                        {'sortable': false, 'targets': [7]},
 | 
			
		||||
                        {'searchable': false, 'targets': [7]}
 | 
			
		||||
                    ],
 | 
			
		||||
                    'order': [[4, 'desc'], [0, 'asc']],
 | 
			
		||||
                    'buttons': [
 | 
			
		||||
                        {
 | 
			
		||||
                            extend: 'print',
 | 
			
		||||
                            exportOptions: {
 | 
			
		||||
                                columns: [0, 1, 3, 4, 5, 6]
 | 
			
		||||
                            }
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            extend: 'excel',
 | 
			
		||||
                            exportOptions: {
 | 
			
		||||
                                columns: [0, 1, 3, 4, 5, 6]
 | 
			
		||||
                            }
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            extend: 'pdf',
 | 
			
		||||
                            exportOptions: {
 | 
			
		||||
                                columns: [0, 1, 3, 4, 5, 6]
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    ],
 | 
			
		||||
                    'responsive': 'true',
 | 
			
		||||
                    'colReorder': 'true',
 | 
			
		||||
                    'fixedHeader': 'true',
 | 
			
		||||
                    'searchBuilder': {
 | 
			
		||||
                        depthLimit: 2,
 | 
			
		||||
                        columns: [1, 5, 6],
 | 
			
		||||
                    },
 | 
			
		||||
                    dom: 'BQlfrtip'
 | 
			
		||||
                });
 | 
			
		||||
                // $('.buttons-pdf').html('<span class="glyphicon glyphicon-file" data-toggle="tooltip" title="Export To Excel"/>') -->
 | 
			
		||||
            } );
 | 
			
		||||
            $('#results-table').show();
 | 
			
		||||
            $(window).trigger('resize');
 | 
			
		||||
        </script>
 | 
			
		||||
    {% endblock %}
 | 
			
		||||
{% endif %}
 | 
			
		||||
@@ -20,7 +20,7 @@
 | 
			
		||||
                        Author
 | 
			
		||||
                    </th>
 | 
			
		||||
                    <th data-priority="3">
 | 
			
		||||
                        Use
 | 
			
		||||
                        Exams
 | 
			
		||||
                    </th>
 | 
			
		||||
                    <th data-priority="1">
 | 
			
		||||
                        Actions
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
{% extends "admin/components/datatable.html" %}
 | 
			
		||||
{% block title %} SKA Referee Test | Manage Exams {% endblock %}
 | 
			
		||||
{% block content %}
 | 
			
		||||
    {% include "admin/components/client-alerts.html" %}
 | 
			
		||||
    <h1>Manage Exams</h1>
 | 
			
		||||
    {% include "admin/components/secondary-navs/tests.html" %}
 | 
			
		||||
    <h2>{{ display_title }}</h2>
 | 
			
		||||
@@ -157,7 +158,7 @@
 | 
			
		||||
            });
 | 
			
		||||
            // $('.buttons-pdf').html('<span class="glyphicon glyphicon-file" data-toggle="tooltip" title="Export To Excel"/>') -->
 | 
			
		||||
        } );
 | 
			
		||||
        $('#test-table').show();
 | 
			
		||||
        $('#active-test-table').show();
 | 
			
		||||
        $(window).trigger('resize');
 | 
			
		||||
    </script>
 | 
			
		||||
    {% endblock %}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
from flask import Blueprint, render_template, flash, redirect, request, jsonify, abort
 | 
			
		||||
from flask import Blueprint, render_template, flash, redirect, request, jsonify, abort, make_response
 | 
			
		||||
from flask.helpers import url_for
 | 
			
		||||
from functools import wraps
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
@@ -14,7 +14,7 @@ from main import app, db
 | 
			
		||||
from uuid import uuid4
 | 
			
		||||
import secrets
 | 
			
		||||
from main import mail
 | 
			
		||||
from datetime import datetime, date, timedelta
 | 
			
		||||
from datetime import datetime, date
 | 
			
		||||
from .models.tests import Test
 | 
			
		||||
from common.data_tools import get_default_dataset, get_time_options, available_datasets
 | 
			
		||||
 | 
			
		||||
@@ -396,14 +396,65 @@ def create_test():
 | 
			
		||||
        return jsonify({ 'error': errors}), 400
 | 
			
		||||
 | 
			
		||||
@views.route('/tests/delete/', methods=['POST'])
 | 
			
		||||
@admin_account_required
 | 
			
		||||
@login_required
 | 
			
		||||
def delete_test():
 | 
			
		||||
    _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('/test/<id>/', methods=['GET','POST'])
 | 
			
		||||
@views.route('/test/<_id>/', methods=['GET','POST'])
 | 
			
		||||
@admin_account_required
 | 
			
		||||
@login_required
 | 
			
		||||
def view_test(_id, filter=''):
 | 
			
		||||
    test = db.tests.find_one({'_id':_id})
 | 
			
		||||
    if not test:
 | 
			
		||||
        return abort(404)
 | 
			
		||||
 | 
			
		||||
@views.route('/results/')
 | 
			
		||||
@admin_account_required
 | 
			
		||||
@login_required
 | 
			
		||||
def view_entries():
 | 
			
		||||
    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=''):
 | 
			
		||||
    entry = decrypt_find_one(db.entries, {'_id': _id})
 | 
			
		||||
    if request.method == 'GET':
 | 
			
		||||
        if not entry:
 | 
			
		||||
            return abort(404)
 | 
			
		||||
        return render_template('/admin/result-detail.html', entry = entry)
 | 
			
		||||
    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():
 | 
			
		||||
    _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)
 | 
			
		||||
@@ -172,6 +172,11 @@ body {
 | 
			
		||||
    font-size: 26pt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.button-icon {
 | 
			
		||||
    font-size: 20px;
 | 
			
		||||
    margin-right: 2px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Change Autocomplete styles in Chrome*/
 | 
			
		||||
input:-webkit-autofill,
 | 
			
		||||
input:-webkit-autofill:hover, 
 | 
			
		||||
 
 | 
			
		||||
@@ -363,7 +363,7 @@ function render_question() {
 | 
			
		||||
function check_answered() {
 | 
			
		||||
    var question = questions[current_question];
 | 
			
		||||
    var name = question.q_no;
 | 
			
		||||
    if (question_status[current_question] == 0) {
 | 
			
		||||
    if (question_status[current_question] == 0 || question_status[current_question] == -1) {
 | 
			
		||||
        if (!$(`input[name='${name}']:checked`).val()) {
 | 
			
		||||
            question_status[current_question] = -1;
 | 
			
		||||
        } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,9 @@
 | 
			
		||||
        The presentation of the questions is just a start, and we acknowledge we still have a long way to go. We welcome any feedback on how we can further improve this test.
 | 
			
		||||
    </p>
 | 
			
		||||
    <div class="button-container">
 | 
			
		||||
        <a href="{{ url_for('quiz_views.instructions') }}" class="btn btn-success">Read the Instructions</a>
 | 
			
		||||
        <a href="{{ url_for('quiz_views.instructions') }}" class="btn btn-success">
 | 
			
		||||
            <i class="bi bi-book-fill button-icon"></i>
 | 
			
		||||
            Read the Instructions
 | 
			
		||||
        </a>
 | 
			
		||||
    </div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -50,6 +50,9 @@
 | 
			
		||||
        </p>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="button-container">
 | 
			
		||||
        <a href="{{ url_for('quiz_views.start') }}" class="btn btn-success">Take the Quiz</a>
 | 
			
		||||
        <a href="{{ url_for('quiz_views.start') }}" class="btn btn-success">
 | 
			
		||||
            <i class="bi bi-pencil-fill button-icon"></i>
 | 
			
		||||
            Take the Exam
 | 
			
		||||
        </a>
 | 
			
		||||
    </div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -9,10 +9,10 @@
 | 
			
		||||
        <span class="surname">{{ entry.name.surname }}</span>, {{ entry.name.first_name }}
 | 
			
		||||
    </h3>
 | 
			
		||||
 | 
			
		||||
    <strong class="results-details">Email Address</strong>: {{ entry.email }}
 | 
			
		||||
    <strong class="results-details">Email Address</strong>: {{ entry.email }} <br />
 | 
			
		||||
 | 
			
		||||
    {% if entry.club %}
 | 
			
		||||
        <strong class="results-details">Club</strong>: {{ entry.club }}
 | 
			
		||||
        <strong class="results-details">Club</strong>: {{ entry.club }} <br />
 | 
			
		||||
    {% endif%}
 | 
			
		||||
 | 
			
		||||
    {% if entry.status == 'late' %}
 | 
			
		||||
@@ -34,7 +34,7 @@
 | 
			
		||||
 | 
			
		||||
            <ul>
 | 
			
		||||
                {% for tag in tag_output %}
 | 
			
		||||
                    <li>{{ tag[0] }}</li>
 | 
			
		||||
                    <li>{{ tag }}</li>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </ul>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,10 @@
 | 
			
		||||
            <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">Start Quiz</button>
 | 
			
		||||
                        <button class="btn btn-md btn-success btn-block" type="submit">
 | 
			
		||||
                            <i class="bi bi-pencil-fill button-icon"></i>
 | 
			
		||||
                            Start Exam
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -104,6 +104,7 @@ def fetch_questions():
 | 
			
		||||
        'end_time': end_time
 | 
			
		||||
    }
 | 
			
		||||
    entry = db.entries.find_one_and_update({'_id': _id}, {'$set': update}, upsert=False, return_document=ReturnDocument.AFTER)
 | 
			
		||||
    db.tests.find_one_and_update({'_id': test['_id']}, {'$push': {'entries': _id}})
 | 
			
		||||
    dataset = test['dataset']
 | 
			
		||||
    dataset_path = os.path.join(app.config['DATA_FILE_DIRECTORY'], dataset)
 | 
			
		||||
    with open(dataset_path, 'r') as data_file:
 | 
			
		||||
@@ -172,6 +173,7 @@ def result():
 | 
			
		||||
    elif entry['results']['grade'] == 'fail':
 | 
			
		||||
        flavour_text_plain = """Unfortunately, you were not successful in passing the theory exam in this attempt. We hope that this does not dissuade you, and that you try again in the future.
 | 
			
		||||
        """
 | 
			
		||||
    if not entry['status'] == 'late':
 | 
			
		||||
        email = Message(
 | 
			
		||||
            subject="SKA Refereeing Theory Exam Results",
 | 
			
		||||
            recipients=[entry['email']],
 | 
			
		||||
@@ -200,7 +202,7 @@ def result():
 | 
			
		||||
            <p><strong>Email Address</strong>: {entry['email']}</p>
 | 
			
		||||
            {f"<p><strong>Club</strong>: {entry['club']}</p>" if entry['club'] else ''}
 | 
			
		||||
            <p><strong>Date of Test</strong>: {entry['submission_time'].strftime('%d %b %Y')}</p>
 | 
			
		||||
        <h1>{score}%</h1>
 | 
			
		||||
            <h1>{score}%</h1>
 | 
			
		||||
            <h2>{entry['results']['grade']}</h2>
 | 
			
		||||
            <p>{flavour_text_plain}</p>
 | 
			
		||||
            <p>Based on your answers, we would also suggest you revise the following topics as you continue refereeing:</p>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user