From b77289eff78a94a3d832b7adc38e269ea4793cb9 Mon Sep 17 00:00:00 2001 From: Struan Donald Date: Tue, 24 Sep 2024 12:49:06 +0200 Subject: [PATCH 1/2] display previous year's response on RoR page, if exists Pulls the code for much of this out into the base view so it can be shared between the first mark and right of reply views. --- crowdsourcer/forms.py | 1 + ...authority_ror_questions_with_previous.html | 166 ++++++++++++++++++ crowdsourcer/views/base.py | 32 ++++ crowdsourcer/views/marking.py | 31 +--- crowdsourcer/views/rightofreply.py | 10 ++ 5 files changed, 211 insertions(+), 29 deletions(-) create mode 100644 crowdsourcer/templates/crowdsourcer/authority_ror_questions_with_previous.html diff --git a/crowdsourcer/forms.py b/crowdsourcer/forms.py index a8328224..54f83ddc 100644 --- a/crowdsourcer/forms.py +++ b/crowdsourcer/forms.py @@ -214,6 +214,7 @@ def __init__(self, *args, **kwargs): self.authority_obj = self.initial.get("authority", None) self.question_obj = self.initial.get("question", None) + self.previous_response = self.initial.get("previous_response", None) self.orig = self.initial.get("original_response", None) def clean(self): diff --git a/crowdsourcer/templates/crowdsourcer/authority_ror_questions_with_previous.html b/crowdsourcer/templates/crowdsourcer/authority_ror_questions_with_previous.html new file mode 100644 index 00000000..7a50673f --- /dev/null +++ b/crowdsourcer/templates/crowdsourcer/authority_ror_questions_with_previous.html @@ -0,0 +1,166 @@ +{% extends 'crowdsourcer/base.html' %} + +{% load django_bootstrap5 %} +{% load neighbourhood_filters %} + +{% block content %} +

+ {% if authority.website %} + {{ authority_name }}: + {% else %} + {{ authority_name }}: + {% endif %} + {{section_title}} +

+ +
+ +
+ + {% if message %} +

+ {{ message }} +

+ {% endif %} + +
+ {% csrf_token %} + {% if form.total_error_count > 0 %} +
+
+ Changes not saved. There were some errors which are highlighted in red below. Your progress will not be saved until you correct these. +
+
+ {% endif %} + {{ form.management_form }} + {% for q_form in form %} +
+
+
+ + {{q_form.question_obj.number_and_part}}. {{ q_form.question_obj.description }} + {% if q_form.question_obj.how_marked == "foi" %}(FOI){% endif %} + +
+ Criteria + {% autoescape off %} + {{q_form.question_obj.criteria|linebreaks }} + {% endautoescape %} +
+
+ Clarifications + {% autoescape off %} + {{q_form.question_obj.clarifications|linebreaks }} + {% endautoescape %} +
+
+
+
+
+

Your 2023 Scorecards Right of Reply response

+

This is what you or someone from your council supplied when responding to the equivalent question in the 2023 Scorecards Right of Reply, in Spring 2023. This is for your information only.

+ +
Agree with response
+
+ {% if q_form.previous_response.agree_with_response %} + Yes + {% elif q_form.previous_response.agree_with_response == False %} + No + {% else %} + (none) + {% endif %} +
+ +
Links to evidence
+
+ {{ q_form.previous_response.evidence|default:"(none)" }} +
+ +
Additional notes
+
+ {{ q_form.previous_response.private_notes|default:"(none)"|linebreaks }} +
+
+
+

2025 Scorecards First Mark assessment

+ +
Marker’s answer
+
+ {% if q_form.orig.multi_option.values %} +

+ {% for option in q_form.orig.multi_option.values %} + {{ option.description }}, + {% empty %} + (none) + {% endfor %} +

+ {% else %} + {{ q_form.orig.option|default:"(none)"|linebreaks }} + {% endif %} +
+ + {% if q_form.question_obj.how_marked == 'foi' %} +
FOI request
+
+ {{ q_form.orig.evidence|urlize }} +
+ {% else %} +
Marker’s evidence of criteria met
+
+ {{ q_form.orig.evidence|default:"(none)"|linebreaks }} +
+ {% endif %} + + {% if q_form.question_obj.how_marked != 'foi' %} +
Links to evidence
+
+ {{ q_form.orig.public_notes|default:"(none)"|urlize|linebreaks }} +
+ +
Page number
+
+ {{ q_form.orig.page_number|default:"(none)" }}
+
+ {% endif %} + +
Marker’s additional notes
+
+ {{ q_form.orig.private_notes|default:"(none)"|linebreaks }} +
+
+
+

Your 2025 Scorecards Right of Reply response

+

Please provide your response here on the 2025 Scorecards First Mark assessment.

+ + {% bootstrap_field q_form.agree_with_response %} + + {% bootstrap_field q_form.evidence %} + + {% bootstrap_field q_form.private_notes %} + + {{ q_form.authority }} + {{ q_form.question }} + {{ q_form.id }} +
+
+
+ {% endfor %} + +
+ +
+
+ +{% endblock %} diff --git a/crowdsourcer/views/base.py b/crowdsourcer/views/base.py index 11b1c285..17ac1708 100644 --- a/crowdsourcer/views/base.py +++ b/crowdsourcer/views/base.py @@ -57,6 +57,38 @@ def check_permissions(self): ): raise PermissionDenied + def has_previous(self): + has_previous = Question.objects.filter( + section__marking_session=self.request.current_session, + section__title=self.kwargs["section_title"], + questiongroup=self.authority.questiongroup, + how_marked__in=self.how_marked_in, + previous_question__isnull=False, + ).exists() + + self.has_previous_questions = has_previous + return has_previous + + def add_previous(self, initial, rt): + question_list = self.questions.values_list("previous_question_id", flat=True) + prev_responses = Response.objects.filter( + authority=self.authority, + question_id__in=question_list, + response_type=rt, + ).select_related("question") + + response_map = {} + for r in prev_responses: + response_map[r.question.id] = r + + for q in self.questions: + data = initial[q.id] + data["previous_response"] = response_map.get(q.previous_question_id) + + initial[q.id] = data + + return initial + def get_initial_obj(self): self.authority = PublicAuthority.objects.get(name=self.kwargs["name"]) self.questions = Question.objects.filter( diff --git a/crowdsourcer/views/marking.py b/crowdsourcer/views/marking.py index f2aff19f..66c23793 100644 --- a/crowdsourcer/views/marking.py +++ b/crowdsourcer/views/marking.py @@ -12,7 +12,6 @@ MarkingSession, PublicAuthority, Question, - Response, ResponseType, SessionProperties, SessionPropertyValues, @@ -249,35 +248,9 @@ def get_initial_obj(self): initial = super().get_initial_obj() - is_previous = Question.objects.filter( - section__marking_session=self.request.current_session, - section__title=self.kwargs["section_title"], - questiongroup=self.authority.questiongroup, - how_marked__in=self.how_marked_in, - previous_question__isnull=False, - ).exists() - - if is_previous: - self.has_previous_questions = True + if self.has_previous(): audit_rt = ResponseType.objects.get(type="Audit") - question_list = self.questions.values_list( - "previous_question_id", flat=True - ) - prev_responses = Response.objects.filter( - authority=self.authority, - question__in=question_list, - response_type=audit_rt, - ).select_related("question") - - response_map = {} - for r in prev_responses: - response_map[r.question.id] = r - - for q in self.questions: - data = initial[q.id] - data["previous_response"] = response_map.get(q.previous_question_id) - - initial[q.id] = data + initial = self.add_previous(initial, audit_rt) return initial diff --git a/crowdsourcer/views/rightofreply.py b/crowdsourcer/views/rightofreply.py index e8b5184f..13e981de 100644 --- a/crowdsourcer/views/rightofreply.py +++ b/crowdsourcer/views/rightofreply.py @@ -146,6 +146,12 @@ class AuthorityRORSectionQuestions(BaseQuestionView): title_start = "Right of Reply - " how_marked_in = ["volunteer", "national_volunteer", "foi"] + def get_template_names(self): + if self.has_previous_questions: + return ["crowdsourcer/authority_ror_questions_with_previous.html"] + else: + return [self.template_name] + def get_initial_obj(self): initial = super().get_initial_obj() @@ -160,6 +166,10 @@ def get_initial_obj(self): initial[r.question.id] = data + if self.has_previous(): + ror_rt = ResponseType.objects.get(type="Right of Reply") + initial = self.add_previous(initial, ror_rt) + return initial def check_permissions(self): From 199bd54299c2860ed4653cc395d20dc40ee1c438 Mon Sep 17 00:00:00 2001 From: Struan Donald Date: Tue, 15 Oct 2024 13:20:04 +0100 Subject: [PATCH 2/2] add a command to set up some minimal example data --- crowdsourcer/fixtures/test_data_questions.csv | 31 +++ crowdsourcer/fixtures/test_data_responses.csv | 13 ++ .../management/commands/create_test_data.py | 218 ++++++++++++++++++ 3 files changed, 262 insertions(+) create mode 100644 crowdsourcer/fixtures/test_data_questions.csv create mode 100644 crowdsourcer/fixtures/test_data_responses.csv create mode 100644 crowdsourcer/management/commands/create_test_data.py diff --git a/crowdsourcer/fixtures/test_data_questions.csv b/crowdsourcer/fixtures/test_data_questions.csv new file mode 100644 index 00000000..b71d729d --- /dev/null +++ b/crowdsourcer/fixtures/test_data_questions.csv @@ -0,0 +1,31 @@ +number,section,question,criteria,clarifications,type,option_1,option_2,option_3,option_4 +1,First Section,Is the council transitioning their vehicle fleet to electric?,"Two-tier criteria + +First tier criteria met if 10% or more of the council's fleet are electric vehicles. + +Second tier criteria met if 50% or more of the council's fleet are electric vehicles","A council's fleet includes council owned or leased vehicles, and may include street cleaners and waste collection vehicles. + +Usually the breakdown of the council fleet by vehicle type is not published, therefore we will just take the stated total fleet number and number of electric vehicles.",multiple_choice,No evidence found,Evidence doesn't meet criteria,10% or over,50% or over +2,First Section,Has the council set up or supported a shared transport scheme that can be used across their whole area?,"Criteria met for each type of scheme where a member of the public can hire a vehicle (e.g. car/scooter/bike/mobility device) within the local authorities area. + +The following schemes will be awarded points: +- Car share scheme of any size in the area. Including: + - Community car clubs. + - Car clubs provided by private companies + - Hiring of council vehicles when not in use +- Bike share scheme +- E-bike or cargo bike share scheme +- E-scooter scheme +- Mobility Devices +- Wheels 2 Work scheme + +Trial schemes that are active at the time of marking will be accepted.",Marked initially using Coordinated Mobility's publicly available data on shared transport schemes.,select_one,No evidence found,Car share,Bike share,E-bike or cargo bike share +1,Second Section,Does the council's corporate plan include a net zero target and make tackling the climate emergency one of its main priorities?,"Criteria met if climate action (alternatively called sustainability or environment) is listed as one of the council's core priorities or equivalent. It must have its own heading or section and a net zero target date must be referenced. + +The net zero target date must be an area-wide target, either the UK Government's national target, the devolved nation's target or the council's area-wide net zero target. + +",,yes_no,,,, +2,Second Section,"Does the council's medium term financial plan include the council's net zero target and make tackling the climate emergency one of its main priorities? +","Criteria met if climate action (alternatively called sustainability or environment) is listed as one of the council's core priorities or equivalent. It must have its own heading or section and a net zero target date must be referenced. + +The net zero target date must be an area-wide target, either the UK Government's national target, the devolved nation's target or the council's area-wide net zero target. ",,yes_no,,,, diff --git a/crowdsourcer/fixtures/test_data_responses.csv b/crowdsourcer/fixtures/test_data_responses.csv new file mode 100644 index 00000000..0d67e63c --- /dev/null +++ b/crowdsourcer/fixtures/test_data_responses.csv @@ -0,0 +1,13 @@ +section,session,number,stage,authority,answer,public_notes,page_number,evidence,private_notes,agree_with_response +First Section,Session One,1,First Mark,Test Council,10% or over,Section One Q1 2023 notes,1,http://example.com,First Section Q1 2023 private notes, +First Section,Session One,2,First Mark,Test Council,Car share,Section One Q2 2023 notes,2,http://example.com,Section One Q2 2023 private notes, +Second Section,Session One,1,First Mark,Test Council,Yes,Section Two Q1 2023 notes,34,http://example.com,Section Two Q1 2023 private notes, +Second Section,Session One,2,First Mark,Test Council,No,Section Two Q2 2023 notes,,http://example.com,Section Two Q2 2023 private notes, +First Section,Session One,1,Right of Reply,Test Council,,,,http://example.com,Section One Q1 2023 council private notes,Yes +First Section,Session One,2,Right of Reply,Test Council,,,,http://example.com,Section One Q1 2023 council private notes,No +Second Section,Session One,1,Right of Reply,Test Council,,,,http://example.com,Section One Q1 2023 council private notes,Yes +Second Section,Session One,2,Right of Reply,Test Council,,,,http://example.com,Section One Q1 2023 council private notes,No +First Section,Session Two,1,First Mark,Test Council,10% or over,Section One Q1 2025 notes,1,http://example.com,Section One Q1 2025 private notes, +First Section,Session Two,2,First Mark,Test Council,Car share,Section One Q2 2025 notes,2,http://example.com,Section One Q2 2025 private notes, +Second Section,Session Two,1,First Mark,Test Council,Yes,Section Two Q1 2025 notes,34,http://example.com,Section Two Q1 2025 private notes, +Second Section,Session Two,2,First Mark,Test Council,No,Section Two Q2 2025 notes,,http://example.com,Section Two Q2 2025 private notes, diff --git a/crowdsourcer/management/commands/create_test_data.py b/crowdsourcer/management/commands/create_test_data.py new file mode 100644 index 00000000..4c8d95b1 --- /dev/null +++ b/crowdsourcer/management/commands/create_test_data.py @@ -0,0 +1,218 @@ +from django.conf import settings +from django.contrib.auth.models import User +from django.core.management.base import BaseCommand + +import pandas as pd + +from crowdsourcer.models import ( + MarkingSession, + Option, + PublicAuthority, + Question, + QuestionGroup, + Response, + ResponseType, + Section, +) + + +class Command(BaseCommand): + help = "set up some helpful data for testing" + + question_list = ( + settings.BASE_DIR / "crowdsourcer" / "fixtures" / "test_data_questions.csv" + ) + responses_list = ( + settings.BASE_DIR / "crowdsourcer" / "fixtures" / "test_data_responses.csv" + ) + + marking_sessions = ["Session One", "Session Two"] + groups = ["Single Tier", "District", "County", "Northern Ireland"] + + sections = [ + "First Section", + "Second Section", + ] + + areas = [ + { + "name": "Test Council", + "type": "CTY", + "gss": "E100001", + }, + { + "name": "Example Council", + "type": "CTY", + "gss": "E100002", + }, + ] + + response_types = ["First Mark", "Right of Reply", "Audit"] + + def add_arguments(self, parser): + parser.add_argument( + "-q", "--quiet", action="store_true", help="Silence progress bars." + ) + + def get_group(self, props): + group = "District" + + print(props["name"], props["type"]) + if props["type"] == "LGD": + group = "Northern Ireland" + elif props["type"] in ["CC", "MTD", "LBO", "UTA"]: + group = "Single Tier" + elif props["type"] in ["CTY"]: + group = "County" + + g = QuestionGroup.objects.get(description=group) + return g + + def add_questions(self): + df = pd.read_csv(self.question_list) + for _, question in df.iterrows(): + sections = Section.objects.filter(title=question["section"]).all() + defaults = { + "description": question["question"], + "criteria": question["criteria"], + "question_type": question["type"], + "clarifications": question["clarifications"], + } + + for section in sections: + q, c = Question.objects.update_or_create( + number=int(question["number"]), + section=section, + defaults=defaults, + ) + + if q.question_type in ["select_one", "tiered", "multiple_choice"]: + o, c = Option.objects.update_or_create( + question=q, + description="None", + defaults={"score": 0, "ordering": 100}, + ) + + for i in range(1, 4): + desc = question[f"option_{i}"] + score = 1 + ordering = i + + o, c = Option.objects.update_or_create( + question=q, + description=desc, + defaults={"score": score, "ordering": ordering}, + ) + elif q.question_type == "yes_no": + for desc in ["Yes", "No"]: + ordering = 1 + score = 1 + if desc == "No": + score = 0 + ordering = 2 + o, c = Option.objects.update_or_create( + question=q, + description=desc, + defaults={"score": score, "ordering": ordering}, + ) + + for group in QuestionGroup.objects.all(): + q.questiongroup.add(group) + + for section in Section.objects.filter(marking_session__label="Session Two"): + prev_section = Section.objects.get( + title=section.title, marking_session__label="Session One" + ) + for question in Question.objects.filter(section=section): + prev_question = Question.objects.get( + number=question.number, section=prev_section + ) + question.previous_question = prev_question + question.save() + + def add_responses(self): + df = pd.read_csv(self.responses_list) + for _, response in df.iterrows(): + question = Question.objects.get( + number=response["number"], + section=Section.objects.get( + title=response["section"], + marking_session__label=response["session"], + ), + ) + + stage = ResponseType.objects.get(type=response["stage"]) + authority = PublicAuthority.objects.get(name=response["authority"]) + defaults = { + "public_notes": response["public_notes"], + "page_number": response["page_number"], + "evidence": response["evidence"], + "private_notes": response["private_notes"], + "user": self.user, + } + + if stage.type != "Right of Reply": + option = Option.objects.get( + question=question, description=response["answer"] + ) + + defaults["option"] = option + else: + defaults["agree_with_response"] = False + if response["agree_with_response"] == "Yes": + defaults["agree_with_response"] = True + + _, r = Response.objects.update_or_create( + question=question, + authority=authority, + response_type=stage, + defaults=defaults, + ) + + def handle(self, quiet: bool = False, *args, **options): + + for group in self.groups: + g, c = QuestionGroup.objects.update_or_create(description=group) + + for r_type in self.response_types: + r, c = ResponseType.objects.update_or_create(type=r_type, priority=1) + + stage = ResponseType.objects.get(type="First Mark") + + for session in self.marking_sessions: + m, c = MarkingSession.objects.update_or_create( + label=session, + defaults={"active": True, "stage": stage, "start_date": "2024-10-01"}, + ) + + for section in self.sections: + s, c = Section.objects.update_or_create( + title=section, marking_session=m + ) + + for group in QuestionGroup.objects.all(): + group.marking_session.add(m) + + m.default = True + m.save() + + for area in self.areas: + defaults = { + "name": area["name"], + "questiongroup": self.get_group(area), + "do_not_mark": False, + "type": area["type"], + } + + a, created = PublicAuthority.objects.update_or_create( + unique_id=area["gss"], + defaults=defaults, + ) + + for session in MarkingSession.objects.all(): + a.marking_session.add(session) + + self.user, _ = User.objects.get_or_create(username="test_data_user@example.com") + + self.add_questions() + self.add_responses()