Skip to content

Commit 118a0de

Browse files
committed
add stats report to list all responses for a specific option
Mostly for hunting down when people have selected a bad option for e.g. District Councils where not all questions responses might be valid. Fixes #217
1 parent 1ee0d0a commit 118a0de

File tree

6 files changed

+379
-2
lines changed

6 files changed

+379
-2
lines changed

ceuk-marking/urls.py

+15
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,21 @@
229229
stats.SessionPropertiesCSVView.as_view(),
230230
name="session_properties",
231231
),
232+
path(
233+
"stats/response_report",
234+
stats.ResponseReportView.as_view(),
235+
name="response_report",
236+
),
237+
path(
238+
"stats/available_questions",
239+
stats.AvailableResponseQuestionsView.as_view(),
240+
name="available_questions_json",
241+
),
242+
path(
243+
"stats/available_options",
244+
stats.AvailableResponseOptionsView.as_view(),
245+
name="available_options_json",
246+
),
232247
# audit screens
233248
path(
234249
"section/audit/<section_title>/authorities/",

crowdsourcer/filters.py

+58-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from django.contrib.auth.models import User
2+
from django.db.models import Q
23

34
import django_filters
45

5-
from crowdsourcer.models import ResponseType, Section
6+
from crowdsourcer.models import Option, Question, Response, ResponseType, Section
67

78

89
def filter_not_empty(queryset, name, value):
@@ -32,3 +33,59 @@ class Meta:
3233
"marker__response_type": ["exact"],
3334
"is_active": ["exact"],
3435
}
36+
37+
38+
class ResponseFilter(django_filters.FilterSet):
39+
AUTHORITY_TYPES = [
40+
("COMB", "Combined Authority"),
41+
("COI", "Isles of Scilly"),
42+
("CTY", "County Council"),
43+
("DIS", "District Council"),
44+
("LBO", "London Borough"),
45+
("UTA", "Unitary (includes Scotland and Wales)"),
46+
("MTD", "Metropolitan District"),
47+
("LGD", "Northern Ireland"),
48+
]
49+
response_type = django_filters.ChoiceFilter(
50+
label="Stage",
51+
choices=ResponseType.choices(),
52+
method="response_type_check",
53+
)
54+
question__section = django_filters.ChoiceFilter(
55+
label="Section",
56+
empty_label=None,
57+
choices=Section.objects.values_list("id", "title"),
58+
)
59+
question = django_filters.ChoiceFilter(
60+
field_name="question",
61+
label="Question",
62+
choices=Question.objects.values_list("id", "number"),
63+
)
64+
option = django_filters.ChoiceFilter(
65+
field_name="option",
66+
label="Answer",
67+
method="option_check",
68+
choices=Option.objects.values_list("id", "description"),
69+
)
70+
71+
authority__type = django_filters.ChoiceFilter(
72+
label="Authority Type",
73+
method="response_type_check",
74+
choices=AUTHORITY_TYPES,
75+
)
76+
77+
def option_check(self, queryset, name, value):
78+
queryset = queryset.filter(Q(option=value) | Q(multi_option=value))
79+
return queryset
80+
81+
def response_type_check(self, queryset, name, value):
82+
if value != "":
83+
queryset = queryset.filter(**{name: value})
84+
return queryset
85+
86+
class Meta:
87+
model = Response
88+
fields = {
89+
"question": ["exact"],
90+
"option": ["exact"],
91+
}

crowdsourcer/static/js/stats.esm.js

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
async function getAvailableQuestions($fs) {
2+
var url = "/stats/available_questions";
3+
4+
var $section = $fs.find('#id_question__section').eq(0);
5+
6+
if ($section.val() == "") {
7+
return { results: [] };
8+
}
9+
10+
var params = {
11+
s: $section.val(),
12+
ms: $fs.find('#session').eq(0).val()
13+
};
14+
15+
16+
url = url + '?' + $.param(params);
17+
18+
const response = await fetch(url, {
19+
method: 'GET',
20+
mode: 'cors',
21+
credentials: 'same-origin',
22+
headers: {
23+
"Content-Type": 'application/x-www-form-urlencoded',
24+
"Accept": 'application/json; charset=utf-8',
25+
},
26+
})
27+
28+
return response.json()
29+
}
30+
31+
async function getAvailableOptions($fs) {
32+
var url = "/stats/available_options";
33+
34+
var $question = $fs.find('#id_question').eq(0);
35+
36+
if ($question.val() == "") {
37+
return { results: [] };
38+
}
39+
40+
var params = {
41+
q: $question.val(),
42+
ms: $fs.find('#session').eq(0).val()
43+
};
44+
45+
46+
url = url + '?' + $.param(params);
47+
48+
const response = await fetch(url, {
49+
method: 'GET',
50+
mode: 'cors',
51+
credentials: 'same-origin',
52+
headers: {
53+
"Content-Type": 'application/x-www-form-urlencoded',
54+
"Accept": 'application/json; charset=utf-8',
55+
},
56+
})
57+
58+
return response.json()
59+
}
60+
$(function(){
61+
$('#id_question__section').on('change', function(e){
62+
var $d = $(this);
63+
var $fs = $d.parents('form');
64+
65+
var $q = $fs.find('#id_question').eq(0);
66+
67+
getAvailableQuestions($fs).then(function(data){
68+
if (data["results"].length > 0) {
69+
$q.empty()
70+
let $blank = $('<option></option>').attr('value', "").text("---------");
71+
$q.append($blank);
72+
$.each(data["results"], function(i, opt) {
73+
let $o = $('<option></option>').attr('value', opt["id"]).text(opt["number_and_part"]);
74+
$q.append($o)
75+
});
76+
}
77+
});
78+
});
79+
80+
$('#id_question').on('change', function(e){
81+
var $d = $(this);
82+
var $fs = $d.parents('form');
83+
84+
var $opt = $fs.find('#id_option').eq(0);
85+
86+
getAvailableOptions($fs).then(function(data){
87+
if (data["results"].length > 0) {
88+
$opt.empty()
89+
let $blank = $('<option></option>').attr('value', "").text("---------");
90+
$opt.append($blank);
91+
$.each(data["results"], function(i, opt) {
92+
let $o = $('<option></option>').attr('value', opt["id"]).text(opt["description"]);
93+
$opt.append($o)
94+
});
95+
}
96+
});
97+
});
98+
});

crowdsourcer/templates/crowdsourcer/stats.html

+3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ <h1 class="mb-4">{{ page_title }}</h1>
5757
<a class="list-group-item list-group-item-action d-flex align-items-center justify-content-between" href="{% session_url 'select_council_for_history' %}">
5858
Question response history
5959
</a>
60+
<a class="list-group-item list-group-item-action d-flex align-items-center justify-content-between" href="{% session_url 'response_report' %}">
61+
Question response report
62+
</a>
6063
<a class="list-group-item list-group-item-action d-flex align-items-center justify-content-between" href="{% session_url 'session_properties' %}">
6164
<span class="me-3">Session Properties</span>
6265
{% include 'crowdsourcer/includes/csv-badge.html' %}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
{% extends 'crowdsourcer/base.html' %}
2+
3+
{% load crowdsourcer_tags %}
4+
{% load django_bootstrap5 %}
5+
{% load static %}
6+
7+
{% block script %}
8+
<script type="module" src="{% static 'js/stats.esm.js' %}"></script>
9+
{% endblock %}
10+
11+
{% block content %}
12+
{% if show_login %}
13+
<h1 class="mb-4">Sign in</h1>
14+
<a href="{% url 'login' %}">Sign in</a>
15+
{% else %}
16+
<div class="d-md-flex align-items-center mb-4">
17+
<h1 class="mb-md-0 me-md-auto">Responses</h1>
18+
</div>
19+
20+
<form method="GET" id="response_filter" class="bg-light p-3 rounded-sm mb-3">
21+
<input type="hidden" id="session" value="{{ marking_session.id }}">
22+
<div class="row align-items-end flex-wrap mb-n3 mb-md-n4">
23+
<div class="col" style="min-width: 10rem">
24+
{% bootstrap_field filter.form.question__section %}
25+
</div>
26+
<div class="col" style="min-width: 10rem">
27+
{% bootstrap_field filter.form.question %}
28+
</div>
29+
<div class="col" style="min-width: 10rem">
30+
{% bootstrap_field filter.form.option %}
31+
</div>
32+
<div class="col" style="min-width: 10rem">
33+
{% bootstrap_field filter.form.response_type %}
34+
</div>
35+
<div class="col" style="min-width: 10rem">
36+
{% bootstrap_field filter.form.authority__type %}
37+
</div>
38+
<div class="col" style="min-width: 10rem">
39+
<button type="submit" class="btn btn-primary btn-block mb-3 mb-md-4">Filter list</button>
40+
</div>
41+
</div>
42+
</form>
43+
44+
<table class="table">
45+
<thead>
46+
<tr>
47+
<th>Question</th>
48+
<th>Authority</th>
49+
<th>Response</th>
50+
</tr>
51+
</thead>
52+
<tbody>
53+
{% if params_required %}
54+
<tr><td colspan="3">Please select all options above</td></tr>
55+
{% else %}
56+
{% for response in responses %}
57+
<tr>
58+
<td>
59+
<a href="{% session_url url_pattern response.authority.name response.question.section.title %}">{{ response.question.number_and_part }}</a>
60+
</td>
61+
<td>
62+
{{ response.authority.name }}
63+
</td>
64+
<td>
65+
{% if response.multi_option.values %}
66+
<p>
67+
{% for option in response.multi_option.values %}
68+
{{ option.description }},
69+
{% empty %}
70+
(none)
71+
{% endfor %}
72+
</p>
73+
{% else %}
74+
{{ response.option|default:"(none)"|linebreaks }}
75+
{% endif %}
76+
</td>
77+
</tr>
78+
{% endfor %}
79+
{% endif %}
80+
</tbody>
81+
</table>
82+
{% endif %}
83+
{% endblock %}

0 commit comments

Comments
 (0)