diff --git a/ceuk-marking/urls.py b/ceuk-marking/urls.py index 319d2bb..b129f36 100644 --- a/ceuk-marking/urls.py +++ b/ceuk-marking/urls.py @@ -88,6 +88,11 @@ rightofreply.AuthorityRORSectionQuestions.as_view(), name="authority_ror", ), + path( + "authorities//ror/download/", + rightofreply.AuthorityRORCSVView.as_view(), + name="authority_ror_download", + ), path( "authority_ror_authorities/", rightofreply.AuthorityRORList.as_view(), diff --git a/crowdsourcer/fixtures/ror_responses.json b/crowdsourcer/fixtures/ror_responses.json index 89f8f25..191a978 100644 --- a/crowdsourcer/fixtures/ror_responses.json +++ b/crowdsourcer/fixtures/ror_responses.json @@ -9,7 +9,7 @@ "option": 181, "response_type": 2, "public_notes": "", - "page_number": "0", + "page_number": "", "evidence": "", "private_notes": "", "agree_with_response": true, @@ -29,9 +29,9 @@ "user": 3, "option": 191, "response_type": 2, - "public_notes": "", - "page_number": "0", - "evidence": "", + "public_notes": "http://example.org/", + "page_number": "20", + "evidence": "We do not agree for reasons", "private_notes": "a council objection", "agree_with_response": false, "revision_type": null, @@ -40,5 +40,45 @@ "last_update": "2023-03-15T17:22:10+0000", "multi_option": [] } +}, +{ + "model": "crowdsourcer.response", + "pk": 101, + "fields": { + "authority": 2, + "question": 272, + "user": 2, + "option": 181, + "response_type": 1, + "public_notes": "a public note", + "page_number": "0", + "evidence": "", + "private_notes": "a private note", + "revision_type": null, + "revision_notes": null, + "created": "2023-03-15T17:22:10+0000", + "last_update": "2023-03-15T17:22:10+0000", + "multi_option": [] + } +}, +{ + "model": "crowdsourcer.response", + "pk": 102, + "fields": { + "authority": 2, + "question": 273, + "user": 2, + "option": 6, + "response_type": 1, + "public_notes": "a public note", + "page_number": "0", + "evidence": "", + "private_notes": "a private note", + "revision_type": null, + "revision_notes": null, + "created": "2023-03-15T17:22:10+0000", + "last_update": "2023-03-15T17:22:10+0000", + "multi_option": [] + } } ] diff --git a/crowdsourcer/forms.py b/crowdsourcer/forms.py index a832822..54f83dd 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/management/commands/import_councils.py b/crowdsourcer/management/commands/import_councils.py index ef4b795..6626828 100644 --- a/crowdsourcer/management/commands/import_councils.py +++ b/crowdsourcer/management/commands/import_councils.py @@ -4,7 +4,13 @@ import pandas as pd -from crowdsourcer.models import Assigned, Marker, PublicAuthority, ResponseType +from crowdsourcer.models import ( + Assigned, + Marker, + MarkingSession, + PublicAuthority, + ResponseType, +) YELLOW = "\033[33m" RED = "\033[31m" @@ -22,13 +28,20 @@ def add_arguments(self, parser): "-q", "--quiet", action="store_true", help="Silence progress bars." ) + parser.add_argument( + "--session", + action="store", + required=True, + help="Marking session to use assignments with", + ) + parser.add_argument( "--add_users", action="store_true", help="add users to database" ) parser.add_argument("--council_list", help="file to import data from") - def handle(self, quiet: bool = False, *args, **options): + def handle(self, quiet: bool = False, session: str = None, *args, **options): if options.get("council_list") is not None: self.council_file = options["council_list"] @@ -43,6 +56,7 @@ def handle(self, quiet: bool = False, *args, **options): ], ) + session = MarkingSession.objects.get(label=session) rt = ResponseType.objects.get(type="Right of Reply") for index, row in df.iterrows(): if pd.isna(row["email"]) or pd.isna(row["gssNumber"]): @@ -63,12 +77,41 @@ def handle(self, quiet: bool = False, *args, **options): continue if Marker.objects.filter(authority=council).exists(): - self.stdout.write(f"user already exists for council: {row['council']}") - continue + m = Marker.objects.get(authority=council) + + if ( + m.user.email == row["email"] + and m.marking_session.filter(pk=session.pk).exists() + ): + self.stdout.write( + f"user already exists for council: {row['council']}" + ) + continue if User.objects.filter(username=row["email"]).exists(): u = User.objects.get(username=row["email"]) - if u.marker.authority is not None and u.marker.authority != council: + if ( + u.marker.authority == council + and not u.marker.marking_session.filter(pk=session.pk).exists() + ): + u.marker.marking_session.set([session]) + self.stdout.write( + f"updating marker to current session: {row['email']} ({council}, {u.marker.authority}" + ) + elif ( + u.marker.authority is None + and not Assigned.objects.filter( + user=u, authority=council, marking_session=session + ).exists() + ): + self.stdout.write( + f"updating marker to council: {row['email']} ({council}, {u.marker.authority}" + ) + if options["add_users"]: + u.marker.authority = council + u.marker.save() + u.marker.marking_session.set([session]) + elif u.marker.authority is not None and u.marker.authority != council: self.stdout.write( f"dual email for councils: {row['email']} ({council}, {u.marker.authority}" ) @@ -76,10 +119,14 @@ def handle(self, quiet: bool = False, *args, **options): for c in [council, u.marker.authority]: if options["add_users"]: a, _ = Assigned.objects.update_or_create( - user=u, authority=c + user=u, + authority=c, + marking_session=session, ) u.marker.authority = None + u.marker.send_welcome_email = True u.marker.save() + u.marker.marking_session.set([session]) continue self.stdout.write(f"user already exists for email: {row['email']}") continue @@ -98,4 +145,8 @@ def handle(self, quiet: bool = False, *args, **options): user=u, authority=council, response_type=rt, + defaults={ + "send_welcome_email": True, + }, ) + m.marking_session.set([session]) diff --git a/crowdsourcer/management/commands/send_welcome_emails.py b/crowdsourcer/management/commands/send_welcome_emails.py index 961046d..89fcdaf 100644 --- a/crowdsourcer/management/commands/send_welcome_emails.py +++ b/crowdsourcer/management/commands/send_welcome_emails.py @@ -22,7 +22,10 @@ def add_arguments(self, parser): parser.add_argument("--send_emails", action="store_true", help="Send emails") parser.add_argument( - "--stage", action="store", help="Only send emails to people in this stage" + "--stage", + required=True, + action="store", + help="Use template for this stage and only send emails to people in this stage", ) parser.add_argument( @@ -42,6 +45,16 @@ def get_config(self, session): return None + def get_templates(self, config, user, stage="First Mark"): + if config.get(stage): + config = config[stage] + + template = config["new_user_template"] + if user.password != "": + template = config["previous_user_template"] + + return (template, config["subject_template"]) + def handle(self, *args, **kwargs): if not kwargs["send_emails"]: self.stdout.write( @@ -94,12 +107,12 @@ def handle(self, *args, **kwargs): if user.email: self.stdout.write(f"Sending email for to this email: {user.email}") if kwargs["send_emails"]: - template = config["new_user_template"] + template, subject_template = self.get_templates( + config, user, kwargs["stage"] + ) if user.password == "": user.set_password(get_random_string(length=20)) user.save() - else: - template = config["previous_user_template"] form = PasswordResetForm({"email": user.email}) assert form.is_valid() @@ -111,7 +124,7 @@ def handle(self, *args, **kwargs): domain_override=config["server_name"], use_https=True, from_email=config["from_email"], - subject_template_name=config["subject_template"], + subject_template_name=subject_template, email_template_name=template, ) marker.send_welcome_email = False 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 0000000..972b52e --- /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 %} +
+
+
+
+
+
+
+

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 Scorecards 2023 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 }} +
+
+
+ {% 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/templates/crowdsourcer/authority_section_list.html b/crowdsourcer/templates/crowdsourcer/authority_section_list.html index 164f6db..b5d5450 100644 --- a/crowdsourcer/templates/crowdsourcer/authority_section_list.html +++ b/crowdsourcer/templates/crowdsourcer/authority_section_list.html @@ -45,5 +45,7 @@

{% endfor %} + + Download {% endif %} {% endblock %} diff --git a/crowdsourcer/tests/data/council_new_joint_users.csv b/crowdsourcer/tests/data/council_new_joint_users.csv new file mode 100644 index 0000000..305c526 --- /dev/null +++ b/crowdsourcer/tests/data/council_new_joint_users.csv @@ -0,0 +1,5 @@ +,firstName,surname,council,councilInternalName,gssNumber,email,official-name +0,Armagh,Staff,"Armagh, Banbridge and Craigavon","Armagh City, Banbridge and Craigavon Borough Council",N09000002,armagh@example.org,"Armagh City, Banbridge and Craigavon Borough Council" +1,Aberdeen,Staff,Aberdeenshire,Aberdeenshire Council,S12000034,aberdeenshire@example.org,Aberdeenshire Council +2,Aberdeen,Staff,Aberdeen City,Aberdeen City Council,S12000033,aberdeenshire@example.org,Aberdeen City Council +3,Adur,Staff,Adur,Adur District Council,E07000223,adur@example.org,Adur District Council diff --git a/crowdsourcer/tests/data/council_new_users.csv b/crowdsourcer/tests/data/council_new_users.csv new file mode 100644 index 0000000..989c09d --- /dev/null +++ b/crowdsourcer/tests/data/council_new_users.csv @@ -0,0 +1,5 @@ +,firstName,surname,council,councilInternalName,gssNumber,email,official-name +0,Armagh,Staff,"Armagh, Banbridge and Craigavon","Armagh City, Banbridge and Craigavon Borough Council",N09000002,armagh@example.org,"Armagh City, Banbridge and Craigavon Borough Council" +1,Aberdeen,Staff,Aberdeenshire,Aberdeenshire Council,S12000034,aberdeen@example.org,Aberdeenshire Council +2,Aberdeen,Staff,Aberdeen City,Aberdeen City Council,S12000033,aberdeen@example.org,Aberdeen City Council +3,Adur,Staff,Adur,Adur District Council,E07000223,new_adur@example.org,Adur District Council diff --git a/crowdsourcer/tests/data/council_split_users.csv b/crowdsourcer/tests/data/council_split_users.csv new file mode 100644 index 0000000..f6ab775 --- /dev/null +++ b/crowdsourcer/tests/data/council_split_users.csv @@ -0,0 +1,5 @@ +,firstName,surname,council,councilInternalName,gssNumber,email,official-name +0,Armagh,Staff,"Armagh, Banbridge and Craigavon","Armagh City, Banbridge and Craigavon Borough Council",N09000002,armagh@example.org,"Armagh City, Banbridge and Craigavon Borough Council" +1,Aberdeen,Staff,Aberdeenshire,Aberdeenshire Council,S12000034,aberdeenshire@example.org,Aberdeenshire Council +2,Aberdeen,Staff,Aberdeen City,Aberdeen City Council,S12000033,aberdeen@example.org,Aberdeen City Council +3,Adur,Staff,Adur,Adur District Council,E07000223,adur@example.org,Adur District Council diff --git a/crowdsourcer/tests/test_commands.py b/crowdsourcer/tests/test_commands.py index bb586a1..2672f99 100644 --- a/crowdsourcer/tests/test_commands.py +++ b/crowdsourcer/tests/test_commands.py @@ -1,6 +1,5 @@ import pathlib from io import StringIO -from unittest import skip from django.contrib.auth.models import User from django.core import mail @@ -103,7 +102,6 @@ def test_unassign(self): ) -@skip("need to fix adding marking session with assigment") class ImportCouncilsTestCase(BaseCommandTestCase): fixtures = [ "authorities.json", @@ -115,7 +113,7 @@ def test_import_councils_no_commit(self): pathlib.Path(__file__).parent.resolve() / "data" / "merged_contacts.csv" ) - self.call_command("import_councils", council_list=data_file) + self.call_command("import_councils", session="Default", council_list=data_file) self.assertEquals(0, Assigned.objects.count()) self.assertEquals(0, Marker.objects.count()) @@ -125,7 +123,12 @@ def test_import_councils(self): pathlib.Path(__file__).parent.resolve() / "data" / "merged_contacts.csv" ) - self.call_command("import_councils", add_users=True, council_list=data_file) + self.call_command( + "import_councils", + session="Default", + add_users=True, + council_list=data_file, + ) self.assertEquals(2, User.objects.count()) self.assertEquals(2, Assigned.objects.count()) @@ -151,6 +154,252 @@ def test_import_councils(self): self.assertFalse(User.objects.filter(username="armagh@example.org").exists()) + def test_import_for_new_session_all_same_users(self): + data_file = ( + pathlib.Path(__file__).parent.resolve() / "data" / "merged_contacts.csv" + ) + + self.call_command( + "import_councils", + session="Default", + add_users=True, + council_list=data_file, + ) + + self.assertEquals(2, User.objects.count()) + self.assertEquals(2, Assigned.objects.count()) + self.assertEquals( + 2, Marker.objects.filter(marking_session__label="Default").count() + ) + + self.call_command( + "import_councils", + session="Second Session", + add_users=True, + council_list=data_file, + ) + + self.assertEquals(2, User.objects.count()) + self.assertEquals(4, Assigned.objects.count()) + self.assertEquals( + 2, Assigned.objects.filter(marking_session__label="Default").count() + ) + self.assertEquals( + 2, Assigned.objects.filter(marking_session__label="Second Session").count() + ) + self.assertEquals( + 0, Marker.objects.filter(marking_session__label="Default").count() + ) + self.assertEquals( + 2, Marker.objects.filter(marking_session__label="Second Session").count() + ) + self.assertTrue( + Marker.objects.filter( + marking_session__label="Second Session", + user__email="aberdeen@example.org", + authority__isnull=True, + ).exists() + ) + self.assertTrue( + Marker.objects.filter( + marking_session__label="Second Session", + user__email="adur@example.org", + authority__name="Adur District Council", + ).exists() + ) + + def test_import_for_new_session_new_users(self): + data_file = ( + pathlib.Path(__file__).parent.resolve() / "data" / "merged_contacts.csv" + ) + + self.call_command( + "import_councils", + session="Default", + add_users=True, + council_list=data_file, + ) + + self.assertEquals(2, User.objects.count()) + self.assertEquals(2, Assigned.objects.count()) + self.assertEquals( + 2, Marker.objects.filter(marking_session__label="Default").count() + ) + data_file = ( + pathlib.Path(__file__).parent.resolve() / "data" / "council_new_users.csv" + ) + + self.call_command( + "import_councils", + session="Second Session", + add_users=True, + council_list=data_file, + ) + + self.assertEquals(3, User.objects.count()) + self.assertEquals(3, Marker.objects.count()) + self.assertEquals(4, Assigned.objects.count()) + self.assertEquals( + 2, Assigned.objects.filter(marking_session__label="Default").count() + ) + for council in ["Aberdeen City Council", "Aberdeenshire Council"]: + self.assertTrue( + Assigned.objects.filter( + user__email="aberdeen@example.org", + marking_session__label="Default", + authority__name=council, + ).exists() + ) + self.assertEquals( + 2, Assigned.objects.filter(marking_session__label="Second Session").count() + ) + for council in ["Aberdeen City Council", "Aberdeenshire Council"]: + self.assertTrue( + Assigned.objects.filter( + user__email="aberdeen@example.org", + marking_session__label="Second Session", + authority__name=council, + ).exists() + ) + self.assertEquals( + 2, Marker.objects.filter(marking_session__label="Second Session").count() + ) + self.assertTrue( + Marker.objects.filter( + marking_session__label="Second Session", + user__email="aberdeen@example.org", + authority__isnull=True, + ).exists() + ) + self.assertTrue( + Marker.objects.filter( + marking_session__label="Second Session", + user__email="new_adur@example.org", + authority__name="Adur District Council", + ).exists() + ) + self.assertTrue( + Marker.objects.filter( + marking_session__label="Default", + user__email="adur@example.org", + authority__name="Adur District Council", + ).exists() + ) + + def test_import_for_new_session_council_split_users(self): + data_file = ( + pathlib.Path(__file__).parent.resolve() / "data" / "merged_contacts.csv" + ) + + self.call_command( + "import_councils", + session="Default", + add_users=True, + council_list=data_file, + ) + + self.assertEquals(2, User.objects.count()) + self.assertEquals(2, Assigned.objects.count()) + self.assertEquals( + 2, Marker.objects.filter(marking_session__label="Default").count() + ) + data_file = ( + pathlib.Path(__file__).parent.resolve() / "data" / "council_split_users.csv" + ) + + self.call_command( + "import_councils", + session="Second Session", + add_users=True, + council_list=data_file, + ) + + self.assertEquals(3, User.objects.count()) + self.assertEquals(3, Marker.objects.count()) + self.assertEquals(2, Assigned.objects.count()) + self.assertEquals( + 2, Assigned.objects.filter(marking_session__label="Default").count() + ) + self.assertEquals( + 0, Assigned.objects.filter(marking_session__label="Second Session").count() + ) + self.assertEquals( + 3, Marker.objects.filter(marking_session__label="Second Session").count() + ) + + def test_import_for_new_session_council_new_joint_users(self): + data_file = ( + pathlib.Path(__file__).parent.resolve() / "data" / "merged_contacts.csv" + ) + + self.call_command( + "import_councils", + session="Default", + add_users=True, + council_list=data_file, + ) + + self.assertEquals(2, User.objects.count()) + self.assertEquals(2, Assigned.objects.count()) + self.assertEquals( + 2, Marker.objects.filter(marking_session__label="Default").count() + ) + data_file = ( + pathlib.Path(__file__).parent.resolve() + / "data" + / "council_new_joint_users.csv" + ) + + self.call_command( + "import_councils", + session="Second Session", + add_users=True, + council_list=data_file, + ) + + self.assertEquals(3, User.objects.count()) + self.assertEquals(3, Marker.objects.count()) + self.assertEquals(4, Assigned.objects.count()) + self.assertEquals( + 2, Assigned.objects.filter(marking_session__label="Default").count() + ) + for council in ["Aberdeen City Council", "Aberdeenshire Council"]: + self.assertTrue( + Assigned.objects.filter( + user__email="aberdeen@example.org", + marking_session__label="Default", + authority__name=council, + ).exists() + ) + self.assertEquals( + 2, Assigned.objects.filter(marking_session__label="Second Session").count() + ) + for council in ["Aberdeen City Council", "Aberdeenshire Council"]: + self.assertTrue( + Assigned.objects.filter( + user__email="aberdeenshire@example.org", + marking_session__label="Second Session", + authority__name=council, + ).exists() + ) + self.assertEquals( + 2, Marker.objects.filter(marking_session__label="Second Session").count() + ) + self.assertTrue( + Marker.objects.filter( + marking_session__label="Second Session", + user__email="aberdeenshire@example.org", + authority__isnull=True, + ).exists() + ) + self.assertTrue( + Marker.objects.filter( + marking_session__label="Second Session", + user__email="adur@example.org", + authority__name="Adur District Council", + ).exists() + ) + class RemoveIdenticalDuplicatesTestCase(BaseCommandTestCase): fixtures = [ @@ -550,7 +799,7 @@ class SendWelcomeEmails(BaseCommandTestCase): def test_required_args(self): self.assertEquals(len(mail.outbox), 0) with self.assertRaisesRegex( - CommandError, r"following arguments are required: --session" + CommandError, r"following arguments are required: --stage, --session" ): self.call_command( "send_welcome_emails", @@ -561,12 +810,14 @@ def test_basic_run(self): self.assertEquals(len(mail.outbox), 0) self.call_command( "send_welcome_emails", + stage="First Mark", session="Default", ) self.assertEquals(len(mail.outbox), 0) self.call_command( "send_welcome_emails", + stage="First Mark", session="Default", send_emails=True, ) @@ -574,6 +825,7 @@ def test_basic_run(self): self.call_command( "send_welcome_emails", + stage="First Mark", session="Default", send_emails=True, ) @@ -588,6 +840,7 @@ def test_only_sends_if_flag_set(self): self.call_command( "send_welcome_emails", + stage="First Mark", session="Default", send_emails=True, ) @@ -599,6 +852,7 @@ def test_only_sends_if_flag_set(self): def test_email_comtent(self): self.call_command( "send_welcome_emails", + stage="First Mark", session="Default", send_emails=True, ) @@ -634,6 +888,7 @@ def test_limit_session(self): self.assertEquals(len(mail.outbox), 0) self.call_command( "send_welcome_emails", + stage="First Mark", send_emails=True, session="Second Session", ) @@ -641,6 +896,7 @@ def test_limit_session(self): self.call_command( "send_welcome_emails", + stage="First Mark", send_emails=True, session="Default", ) @@ -654,6 +910,7 @@ def test_config_loading(self): self.assertEquals(len(mail.outbox), 0) self.call_command( "send_welcome_emails", + stage="First Mark", send_emails=True, session="Second Session", ) @@ -664,6 +921,7 @@ def test_config_loading(self): mail.outbox = [] self.call_command( "send_welcome_emails", + stage="First Mark", send_emails=True, session="Default", ) @@ -671,11 +929,64 @@ def test_config_loading(self): email = mail.outbox[0] self.assertEquals(email.from_email, "Default From ") + @override_settings( + WELCOME_EMAIL={ + "Default": { + "server_name": "example.org", + "from_email": "Default From ", + "subject_template": "registration/initial_password_email_subject.txt", + "new_user_template": "registration/initial_password_email.html", + "previous_user_template": "registration/repeat_password_email.html", + "Right of Reply": { + "subject_template": "registration/council_password_email_subject.txt", + "new_user_template": "registration/council_password_email.html", + "previous_user_template": "registration/council_repeat_password_email.html", + }, + }, + } + ) + def test_template_loading(self): + marker = Marker.objects.get(user__email="new_marker@example.org") + rt = ResponseType.objects.get(type="Right of Reply") + marker.response_type = rt + marker.save() + + self.assertEquals(len(mail.outbox), 0) + self.call_command( + "send_welcome_emails", + stage="First Mark", + send_emails=True, + session="Default", + ) + self.assertEquals(len(mail.outbox), 1) + email = mail.outbox[0] + self.assertEquals( + email.subject, + "Registration link for CEUK Council Climate Scorecards Scoring System", + ) + self.assertRegex(email.body, r"Thanks for volunteering") + + mail.outbox = [] + self.call_command( + "send_welcome_emails", + stage="Right of Reply", + send_emails=True, + session="Default", + ) + self.assertEquals(len(mail.outbox), 1) + email = mail.outbox[0] + self.assertEquals( + email.subject, + "Registration link for CEUK Council Climate Scorecards Scoring System", + ) + self.assertRegex(email.body, r"council’s contact to receive") + @override_settings(WELCOME_EMAIL={}) def test_error_if_no_config(self): self.assertEquals(len(mail.outbox), 0) _, err = self.call_command( "send_welcome_emails", + stage="First Mark", send_emails=True, session="Default", ) diff --git a/crowdsourcer/tests/test_right_of_reply_views.py b/crowdsourcer/tests/test_right_of_reply_views.py index c130066..13ee81b 100644 --- a/crowdsourcer/tests/test_right_of_reply_views.py +++ b/crowdsourcer/tests/test_right_of_reply_views.py @@ -1,7 +1,11 @@ +import io + from django.contrib.auth.models import User from django.test import TestCase from django.urls import reverse +import pandas as pd + from crowdsourcer.models import ( Assigned, MarkingSession, @@ -554,3 +558,40 @@ def test_view_other_session(self): progress = response.context["progress"] self.assertEqual(len(progress.keys()), 2) + + +class TestCSVDownloadView(BaseTestCase): + def test_download(self): + url = reverse("authority_ror_download", args=("Aberdeenshire Council",)) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + content = response.content.decode("utf-8") + # the dtype bit stops pandas doing annoying conversions and ending up + # with page numers as floats etc + df = pd.read_csv(io.StringIO(content), dtype="object") + # avoid nan results + df = df.fillna("") + + self.assertEqual(df.shape[0], 2) + b_and_h_q4 = df.iloc[0] + b_and_h_q5 = df.iloc[1] + + self.assertEqual(b_and_h_q4.question_no, "4") + self.assertEqual( + b_and_h_q4.first_mark_response, + "The council has completed an exercise to measure how much, approximately, it will cost them to retrofit all homes (to EPC C or higher, or equivalent) and there is a target date of 2030.", + ) + self.assertEqual(b_and_h_q4.agree_with_mark, "Yes") + self.assertEqual(b_and_h_q4.council_page_number, "") + self.assertEqual(b_and_h_q4.council_evidence, "") + + self.assertEqual(b_and_h_q5.question_no, "5") + self.assertEqual( + b_and_h_q5.first_mark_response, + "The council convenes or is a member of a local retrofit partnership", + ) + self.assertEqual(b_and_h_q5.council_evidence, "http://example.org/") + self.assertEqual(b_and_h_q5.agree_with_mark, "No") + self.assertEqual(b_and_h_q5.council_page_number, "20") + self.assertEqual(b_and_h_q5.council_notes, "We do not agree for reasons") diff --git a/crowdsourcer/views/base.py b/crowdsourcer/views/base.py index 11b1c28..17ac170 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 f2aff19..66c2379 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 beabb6a..13e981d 100644 --- a/crowdsourcer/views/rightofreply.py +++ b/crowdsourcer/views/rightofreply.py @@ -1,6 +1,9 @@ +import csv import logging +from collections import defaultdict from django.core.exceptions import PermissionDenied +from django.http import HttpResponse from django.shortcuts import redirect from django.urls import reverse from django.views.generic import ListView @@ -143,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() @@ -157,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): @@ -209,3 +222,120 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["ror_user"] = True return context + + +class AuthorityRORCSVView(ListView): + context_object_name = "responses" + + def get_queryset(self): + user = self.request.user + + rt = ResponseType.objects.get(type="Right of Reply") + if user.is_superuser: + authority_name = self.kwargs["name"] + authority = PublicAuthority.objects.get(name=authority_name) + else: + authority = self.request.user.marker.authority + + self.authority = authority + + if authority is not None: + return ( + Response.objects.filter( + question__section__marking_session=self.request.current_session, + response_type=rt, + authority=authority, + ) + .select_related("question", "question__section") + .order_by( + "question__section__title", + "question__number", + "question__number_part", + ) + ) + + return None + + def get_first_mark_responses(self): + rt = ResponseType.objects.get(type="First Mark") + responses = ( + Response.objects.filter( + question__section__marking_session=self.request.current_session, + response_type=rt, + authority=self.authority, + ) + .select_related("question", "question__section") + .order_by( + "question__section__title", + "question__number", + "question__number_part", + ) + ) + + by_section = defaultdict(dict) + + for r in responses: + by_section[r.question.section.title][ + r.question.number_and_part + ] = r.option.description + + return by_section + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + rows = [] + rows.append( + [ + "section", + "question_no", + "question", + "first_mark_response", + "agree_with_mark", + "council_response", + "council_evidence", + "council_page_number", + "council_notes", + ] + ) + + first_mark_responses = self.get_first_mark_responses() + + for response in context["responses"]: + first_mark_response = "" + if first_mark_responses.get( + response.question.section.title + ) and first_mark_responses[response.question.section.title].get( + response.question.number_and_part + ): + first_mark_response = first_mark_responses[ + response.question.section.title + ][response.question.number_and_part] + rows.append( + [ + response.question.section.title, + response.question.number_and_part, + response.question.description, + first_mark_response, + "Yes" if response.agree_with_response else "No", + response.option, + ",".join(response.evidence_links), + response.page_number, + response.evidence, + ] + ) + + context["authority"] = self.authority.name + context["rows"] = rows + + return context + + def render_to_response(self, context, **response_kwargs): + filename = f"{self.request.current_session.label}_{context['authority']}_Right_of_Reply.csv" + response = HttpResponse( + content_type="text/csv", + headers={"Content-Disposition": 'attachment; filename="' + filename + '"'}, + ) + writer = csv.writer(response) + for row in context["rows"]: + writer.writerow(row) + return response diff --git a/templates/registration/council_repeat_password_email.html b/templates/registration/council_repeat_password_email.html new file mode 100644 index 0000000..c17c8ca --- /dev/null +++ b/templates/registration/council_repeat_password_email.html @@ -0,0 +1,21 @@ +{% autoescape off %} +Hi, + +You're receiving this email because you are your council’s contact to receive the Right of Reply for Climate Emergency UK’s Council Climate Scorecards. This email allows you to log into the online data collection system to submit the Right of Reply and respond to your council’s first mark in the Council Climate Scorecards. + +Please go to the following page and choose a new password: +{% block reset_link %} +{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %} +{% endblock %} +Your username is {{ user.get_username }} + +Please Note: Multiple people will be able to login to GRACE as long as they have this username and password. Please only share this with relevant staff within your council. We will be unable to set up multiple accounts for your Council. + +You will also receive a second email containing further information, a guide to the Right of Reply process and a recording of the Right of Reply briefing. This email will be sent to every officer contact for your council on our email list. + +Kind Regards, + +The CEUK team. + +{% endautoescape %} +