From c39da862e092c6b1399b449db4982ca29d704651 Mon Sep 17 00:00:00 2001 From: Struan Donald Date: Wed, 14 Aug 2024 10:25:16 +0100 Subject: [PATCH 01/31] remove create team option from search results page --- neighbourhood/templates/neighbourhood/search_results.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/neighbourhood/templates/neighbourhood/search_results.html b/neighbourhood/templates/neighbourhood/search_results.html index 86d5855..838ed3d 100644 --- a/neighbourhood/templates/neighbourhood/search_results.html +++ b/neighbourhood/templates/neighbourhood/search_results.html @@ -59,6 +59,7 @@

There aren’t any teams in your neighbourhood yet

@@ -68,6 +69,7 @@

Start a team for your neighbourhood

+ {% endcomment %}
From 5725553ac6e392d3cbc65f7fb5ed6d0600d33681 Mon Sep 17 00:00:00 2001 From: Struan Donald Date: Wed, 14 Aug 2024 10:34:11 +0100 Subject: [PATCH 02/31] formatting fixes to appease linting --- neighbourhood_warmth/urls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/neighbourhood_warmth/urls.py b/neighbourhood_warmth/urls.py index d60819f..290a391 100644 --- a/neighbourhood_warmth/urls.py +++ b/neighbourhood_warmth/urls.py @@ -13,6 +13,7 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ + from django.conf import settings from django.conf.urls.static import static from django.contrib import admin From 1d281cdf9959d4d0fd16a25576354ca68ae6b9d5 Mon Sep 17 00:00:00 2001 From: Struan Donald Date: Wed, 14 Aug 2024 11:27:34 +0100 Subject: [PATCH 03/31] add Challenge model Used to list the available challenges --- neighbourhood/migrations/0019_challenges.py | 33 +++++++++++++++++++++ neighbourhood/models.py | 14 +++++++++ 2 files changed, 47 insertions(+) create mode 100644 neighbourhood/migrations/0019_challenges.py diff --git a/neighbourhood/migrations/0019_challenges.py b/neighbourhood/migrations/0019_challenges.py new file mode 100644 index 0000000..2c3ad6b --- /dev/null +++ b/neighbourhood/migrations/0019_challenges.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.15 on 2024-08-14 10:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("neighbourhood", "0018_add_last_updated_and_created"), + ] + + operations = [ + migrations.CreateModel( + name="Challenge", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=300)), + ("description", models.TextField()), + ("template", models.CharField(max_length=300)), + ("is_active", models.BooleanField(default=True)), + ("order", models.IntegerField()), + ("created", models.DateTimeField(auto_now_add=True)), + ("last_updated", models.DateTimeField(auto_now=True)), + ], + ), + ] diff --git a/neighbourhood/models.py b/neighbourhood/models.py index 3153865..b560f1d 100644 --- a/neighbourhood/models.py +++ b/neighbourhood/models.py @@ -136,6 +136,20 @@ def __str__(self): return "{} ({} {})".format(self.name, self.area_type, self.mapit_id) +class Challenge(models.Model): + name = models.CharField(max_length=300) + description = models.TextField() + template = models.CharField(max_length=300) + is_active = models.BooleanField(default=True) + order = models.IntegerField() + + created = models.DateTimeField(auto_now_add=True) + last_updated = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.name + + class Team(models.Model): name = models.CharField(max_length=100) base_pc = models.CharField(max_length=10) From a2804f5304b34538ac2f57adeb2fb7059ed9e848 Mon Sep 17 00:00:00 2001 From: Struan Donald Date: Wed, 14 Aug 2024 11:29:54 +0100 Subject: [PATCH 04/31] add challenge field to Team model used to track the current challenge --- .../migrations/0020_team_challenge.py | 23 +++++++++++++++++++ neighbourhood/models.py | 4 ++++ 2 files changed, 27 insertions(+) create mode 100644 neighbourhood/migrations/0020_team_challenge.py diff --git a/neighbourhood/migrations/0020_team_challenge.py b/neighbourhood/migrations/0020_team_challenge.py new file mode 100644 index 0000000..b7809bf --- /dev/null +++ b/neighbourhood/migrations/0020_team_challenge.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.15 on 2024-08-14 10:29 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("neighbourhood", "0019_challenges"), + ] + + operations = [ + migrations.AddField( + model_name="team", + name="challenge", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="neighbourhood.challenge", + ), + ), + ] diff --git a/neighbourhood/models.py b/neighbourhood/models.py index b560f1d..e586d0f 100644 --- a/neighbourhood/models.py +++ b/neighbourhood/models.py @@ -171,6 +171,10 @@ class Team(models.Model): confirmed = models.BooleanField(default=False) status = models.CharField(max_length=300, blank=True, null=True) + challenge = models.ForeignKey( + Challenge, on_delete=models.SET_NULL, blank=True, null=True + ) + created = models.DateTimeField(auto_now_add=True) last_updated = models.DateTimeField(auto_now=True) From b416a6194b8817e287178de5995456aa5dd054a5 Mon Sep 17 00:00:00 2001 From: Struan Donald Date: Wed, 14 Aug 2024 12:19:36 +0100 Subject: [PATCH 05/31] add Challenge model to admin --- neighbourhood/admin.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/neighbourhood/admin.py b/neighbourhood/admin.py index c365573..81eb415 100644 --- a/neighbourhood/admin.py +++ b/neighbourhood/admin.py @@ -7,7 +7,7 @@ from django.urls import reverse from django.utils.html import format_html, mark_safe -from .models import Team, Token, User +from .models import Challenge, Team, Token, User @admin.register(Token) @@ -46,6 +46,11 @@ def confirmed_members_count(self, obj): return obj.confirmed_members.count() +@admin.register(Challenge) +class ChallengeAdmin(admin.ModelAdmin): + list_display = ("name", "description", "order", "is_active") + + class UserCreationForm(forms.ModelForm): password1 = forms.CharField(label="Password", widget=forms.PasswordInput) password2 = forms.CharField( From 7eeceb7f42e1f18c7dc67df98b2200028c38c354 Mon Sep 17 00:00:00 2001 From: Struan Donald Date: Wed, 14 Aug 2024 12:20:39 +0100 Subject: [PATCH 06/31] if a team has an challange then use that on team page Falls back to a hardcoded challenge if it's not there for now --- .../neighbourhood/challenges/_default.html | 5 ++ .../challenges/_recruit_members.html | 49 +++++++++++++++++++ .../templates/neighbourhood/team_private.html | 4 ++ 3 files changed, 58 insertions(+) create mode 100644 neighbourhood/templates/neighbourhood/challenges/_default.html create mode 100644 neighbourhood/templates/neighbourhood/challenges/_recruit_members.html diff --git a/neighbourhood/templates/neighbourhood/challenges/_default.html b/neighbourhood/templates/neighbourhood/challenges/_default.html new file mode 100644 index 0000000..1f7903c --- /dev/null +++ b/neighbourhood/templates/neighbourhood/challenges/_default.html @@ -0,0 +1,5 @@ +
+

{{ challenge.name }}

+ +

{{ challenge.description }}

+
diff --git a/neighbourhood/templates/neighbourhood/challenges/_recruit_members.html b/neighbourhood/templates/neighbourhood/challenges/_recruit_members.html new file mode 100644 index 0000000..b8363af --- /dev/null +++ b/neighbourhood/templates/neighbourhood/challenges/_recruit_members.html @@ -0,0 +1,49 @@ +
+ +

Recruit at least one more team member from your neighbourhood

+ +

It takes two to tango! Remember, team members should live in the same type of home. But don’t worry if you’re not sure when inviting someone as we can check that when they sign up.

+ +

Use the share buttons below to inspire more community-based action on home energy!

+ +
    +
  • + + {% include 'neighbourhood/icons/facebook.html' with classes="me-2" %} + Facebook + +
  • +
  • + + {% include 'neighbourhood/icons/whatsapp.html' with classes="me-2" %} + WhatsApp + +
  • +
  • + + {% include 'neighbourhood/icons/gmail.html' with classes="me-2" %} + Gmail + +
  • +
  • + + {% include 'neighbourhood/icons/link.html' with classes="me-2" %} + Copy invite link + +
  • +
+ +

Or print out, display, and share our exclusive designer posters and flyers:

+ +

+ Download goodies +

+ +

TIPS: Not sure who to invite?

+ +
    +
  • Try sharing in your neighbourhood WhatsApp or Nextdoor group
  • +
  • Or maybe with your friends from the gym
  • +
  • You could even ask your local councillor for help
  • +
+
diff --git a/neighbourhood/templates/neighbourhood/team_private.html b/neighbourhood/templates/neighbourhood/team_private.html index 8720e29..0ac86a2 100644 --- a/neighbourhood/templates/neighbourhood/team_private.html +++ b/neighbourhood/templates/neighbourhood/team_private.html @@ -96,6 +96,9 @@

Team progress

Current challenge

+ {% if team.challenge %} + {% include team.challenge.template with challenge=team.challenge %} + {% else %}

Recruit at least one more team member from your neighbourhood

@@ -145,6 +148,7 @@

TIPS: Not sure who to invite?

  • You could even ask your local councillor for help
  • + {% endif %} {% if is_team_admin %}
    From 55767308b24ec9e7383da760041ad8ea6ed76917 Mon Sep 17 00:00:00 2001 From: Struan Donald Date: Wed, 14 Aug 2024 12:22:00 +0100 Subject: [PATCH 07/31] if there are challenges in the db use those for progress Falls back to hardcoded challenges if none available --- neighbourhood/models.py | 26 +++++++++++++++++++ .../templates/neighbourhood/team_private.html | 14 ++++++++++ 2 files changed, 40 insertions(+) diff --git a/neighbourhood/models.py b/neighbourhood/models.py index e586d0f..bd40bbd 100644 --- a/neighbourhood/models.py +++ b/neighbourhood/models.py @@ -201,6 +201,32 @@ def vicinity(self): def admins(self): return User.objects.filter(team=self, membership__is_admin=True) + def available_challenges(self): + # if there's no challenge set then use the default ones + if self.challenge is None: + return [] + + challenges = Challenge.objects.filter(is_active=True).order_by("order") + + current_place = 0 + if self.challenge: + current_place = self.challenge.order + + challenge_details = [] + for challenge in challenges: + details = { + "challenge": challenge, + } + + if challenge.order < current_place: + details["done"] = True + elif challenge.order == current_place: + details["active"] = True + + challenge_details.append(details) + + return challenge_details + @classmethod def find_nearest_teams(self, latitude=None, longitude=None, distance=5): if latitude is None or longitude is None: diff --git a/neighbourhood/templates/neighbourhood/team_private.html b/neighbourhood/templates/neighbourhood/team_private.html index 0ac86a2..10a2d0b 100644 --- a/neighbourhood/templates/neighbourhood/team_private.html +++ b/neighbourhood/templates/neighbourhood/team_private.html @@ -33,6 +33,19 @@

    {{ team.name }}

    Team progress

      + {% if team.available_challenges %} + {% for details in team.available_challenges %} +
    • + {% if details.done %} + {% include 'neighbourhood/icons/fa-check-solid.html' with classes='me-2' %} + {% else %} + + {% endif %} + {{ details.challenge.name }} +
    • + + {% endfor %} + {% else %}
    • {% include 'neighbourhood/icons/fa-check-solid.html' with classes='me-2' %} Create team @@ -89,6 +102,7 @@

      Team progress

      Discuss next steps
    • + {% endif %}
    From 589238eeb7014da243c3c0bd2169446b3921107f Mon Sep 17 00:00:00 2001 From: Struan Donald Date: Wed, 14 Aug 2024 13:49:11 +0100 Subject: [PATCH 08/31] add has_rich_description field to Challenge to enable descriptions with HTML so you can use a default template for simplish challenges --- .../0021_challenge_rich_description.py | 19 +++++++++++++++++++ neighbourhood/models.py | 4 ++++ 2 files changed, 23 insertions(+) create mode 100644 neighbourhood/migrations/0021_challenge_rich_description.py diff --git a/neighbourhood/migrations/0021_challenge_rich_description.py b/neighbourhood/migrations/0021_challenge_rich_description.py new file mode 100644 index 0000000..28265ea --- /dev/null +++ b/neighbourhood/migrations/0021_challenge_rich_description.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.15 on 2024-08-14 11:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("neighbourhood", "0020_team_challenge"), + ] + + operations = [ + migrations.AddField( + model_name="challenge", + name="has_rich_description", + field=models.BooleanField( + default=False, help_text="True if description is raw html" + ), + ), + ] diff --git a/neighbourhood/models.py b/neighbourhood/models.py index bd40bbd..1ca7113 100644 --- a/neighbourhood/models.py +++ b/neighbourhood/models.py @@ -143,6 +143,10 @@ class Challenge(models.Model): is_active = models.BooleanField(default=True) order = models.IntegerField() + has_rich_description = models.BooleanField( + default=False, help_text="True if description is raw html" + ) + created = models.DateTimeField(auto_now_add=True) last_updated = models.DateTimeField(auto_now=True) From 9d0b29bd9234364c2fd8edeb34da42f359379974 Mon Sep 17 00:00:00 2001 From: Struan Donald Date: Wed, 14 Aug 2024 13:50:24 +0100 Subject: [PATCH 09/31] update default challenge template to use rich descriptions --- neighbourhood/fixtures/memberships.json | 23 ++++++++++ neighbourhood/fixtures/teams.json | 28 ++++++++++++ .../neighbourhood/challenges/_default.html | 8 +++- neighbourhood/tests/test_views.py | 43 ++++++++++++++++++- 4 files changed, 99 insertions(+), 3 deletions(-) diff --git a/neighbourhood/fixtures/memberships.json b/neighbourhood/fixtures/memberships.json index c49c858..16d5b1c 100644 --- a/neighbourhood/fixtures/memberships.json +++ b/neighbourhood/fixtures/memberships.json @@ -29,6 +29,16 @@ "email_confirmed": true } }, + { + "model": "neighbourhood.user", + "pk": 4, + "fields": { + "email": "scottishparliament-admin@example.org", + "full_name": "Scottish Parliament Admin", + "is_active": true, + "email_confirmed": true + } + }, { "model": "neighbourhood.membership", "pk": 1, @@ -65,5 +75,18 @@ "last_updated": "2023-01-01T14:23:00Z", "date_joined": "2023-01-01" } + }, + { + "model": "neighbourhood.membership", + "pk": 4, + "fields": { + "team_id": 1, + "user_id": 4, + "confirmed": true, + "is_admin": true, + "created": "2023-01-01T14:23:00Z", + "last_updated": "2023-01-01T14:23:00Z", + "date_joined": "2023-01-01" + } } ] diff --git a/neighbourhood/fixtures/teams.json b/neighbourhood/fixtures/teams.json index 8b353a9..7ce0da9 100644 --- a/neighbourhood/fixtures/teams.json +++ b/neighbourhood/fixtures/teams.json @@ -1,4 +1,31 @@ [ + { + "model": "neighbourhood.challenge", + "pk": 1, + "fields": { + "name": "Recruit first team member", + "description": "Recruit your first team member", + "template": "neighbourhood/challenges/_default.html", + "created": "2023-06-22T14:23:00Z", + "last_updated": "2023-06-22T14:23:00Z", + "is_active": true, + "order": 1 + } + }, + { + "model": "neighbourhood.challenge", + "pk": 2, + "fields": { + "name": "Recruit second team member", + "description": "

    Recruit your second team member

    ", + "has_rich_description": true, + "template": "neighbourhood/challenges/_default.html", + "created": "2023-06-22T14:23:00Z", + "last_updated": "2023-06-22T14:23:00Z", + "is_active": true, + "order": 2 + } + }, { "model": "neighbourhood.team", "pk": 1, @@ -24,6 +51,7 @@ "areas": [ 1 ], "created": "2023-06-22T14:23:00Z", "last_updated": "2023-06-22T14:23:00Z", + "challenge_id": 1, "confirmed": true } }, diff --git a/neighbourhood/templates/neighbourhood/challenges/_default.html b/neighbourhood/templates/neighbourhood/challenges/_default.html index 1f7903c..579cffa 100644 --- a/neighbourhood/templates/neighbourhood/challenges/_default.html +++ b/neighbourhood/templates/neighbourhood/challenges/_default.html @@ -1,5 +1,11 @@

    {{ challenge.name }}

    -

    {{ challenge.description }}

    + {% if challenge.has_rich_description %} + {% autoescape off %} + {{ challenge.description }} + {% endautoescape %} + {% else %} + {{ challenge.description|linebreaks }} + {% endif %}
    diff --git a/neighbourhood/tests/test_views.py b/neighbourhood/tests/test_views.py index 6f162f3..9db3420 100644 --- a/neighbourhood/tests/test_views.py +++ b/neighbourhood/tests/test_views.py @@ -6,7 +6,7 @@ from django.shortcuts import reverse from django.test import TestCase -from neighbourhood.models import Team, User +from neighbourhood.models import Challenge, Team, User class CorePageTest(TestCase): @@ -118,7 +118,7 @@ def test_create_team(self, mapit_get): class TeamPagesTest(TestCase): - fixtures = ["teams.json"] + fixtures = ["teams.json", "memberships.json"] def test_existing_team(self): team = Team.objects.get(slug="holyrood-palace") @@ -137,6 +137,45 @@ def test_non_confirmed_team(self): response = self.client.get(reverse("team", args=("holyrood-palace-2",))) self.assertEqual(response.status_code, 404) + def test_team_with_challenge_set(self): + self.client.force_login(User.objects.get(email="holyrood-admin@example.org")) + response = self.client.get(reverse("team", args=("holyrood-palace",))) + + self.assertContains(response, "Recruit your first team member") + self.assertContains(response, "Recruit second team member") + self.assertNotContains(response, "Find second member") + + def test_team_with_rich_challenge_set(self): + challenge = Challenge.objects.get(name="Recruit second team member") + team = Team.objects.get(slug="holyrood-palace") + team.challenge = challenge + team.save() + + self.client.force_login(User.objects.get(email="holyrood-admin@example.org")) + response = self.client.get(reverse("team", args=("holyrood-palace",))) + + self.assertContains(response, "

    Recruit your second team member") + + def test_team_with_non_default_template_challenge_set(self): + challenge = Challenge.objects.get(name="Recruit first team member") + challenge.template = "neighbourhood/challenges/_recruit_members.html" + challenge.save() + + self.client.force_login(User.objects.get(email="holyrood-admin@example.org")) + response = self.client.get(reverse("team", args=("holyrood-palace",))) + + self.assertContains(response, "Use the share buttons below") + + def test_team_with_no_challenge_set(self): + self.client.force_login( + User.objects.get(email="scottishparliament-admin@example.org") + ) + + response = self.client.get(reverse("team", args=("scottish-parliament",))) + + self.assertNotContains(response, "Recruit second team member") + self.assertContains(response, "Find second member") + class TeamManagementPagesTest(TestCase): fixtures = ["teams.json", "memberships.json"] From 33029c506bee85d9c40c8e06a52903eda45d8bdb Mon Sep 17 00:00:00 2001 From: Struan Donald Date: Wed, 14 Aug 2024 14:47:56 +0100 Subject: [PATCH 10/31] handle missing challenge template falls back to configurable default template if the template is missing --- neighbourhood/models.py | 13 +++++++++++++ .../templates/neighbourhood/team_private.html | 2 +- neighbourhood/tests/test_views.py | 10 ++++++++++ neighbourhood_warmth/settings.py | 2 ++ 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/neighbourhood/models.py b/neighbourhood/models.py index 1ca7113..9990f48 100644 --- a/neighbourhood/models.py +++ b/neighbourhood/models.py @@ -1,6 +1,7 @@ from hashlib import sha256 from random import randrange +from django import template from django.conf import settings from django.contrib.auth.models import ( AbstractBaseUser, @@ -11,6 +12,7 @@ from django.contrib.gis.db import models from django.contrib.gis.db.models.functions import Distance from django.contrib.gis.geos import Point +from django.core.mail import mail_admins from django.db.models.signals import pre_save from django.dispatch import receiver from django.utils import timezone @@ -150,6 +152,17 @@ class Challenge(models.Model): created = models.DateTimeField(auto_now_add=True) last_updated = models.DateTimeField(auto_now=True) + def get_template_safe(self): + try: + template.loader.get_template(self.template) + return self.template + except template.TemplateDoesNotExist: + mail_admins( + "Bad challenge template", + f"Challenge {self.name} template not found: {self.template}", + ) + return settings.DEFAULT_CHALLENGE_TEMPLATE + def __str__(self): return self.name diff --git a/neighbourhood/templates/neighbourhood/team_private.html b/neighbourhood/templates/neighbourhood/team_private.html index 10a2d0b..8e630f6 100644 --- a/neighbourhood/templates/neighbourhood/team_private.html +++ b/neighbourhood/templates/neighbourhood/team_private.html @@ -111,7 +111,7 @@

    Team progress

    Current challenge

    {% if team.challenge %} - {% include team.challenge.template with challenge=team.challenge %} + {% include team.challenge.get_template_safe with challenge=team.challenge %} {% else %}
    diff --git a/neighbourhood/tests/test_views.py b/neighbourhood/tests/test_views.py index 9db3420..a0932f7 100644 --- a/neighbourhood/tests/test_views.py +++ b/neighbourhood/tests/test_views.py @@ -166,6 +166,16 @@ def test_team_with_non_default_template_challenge_set(self): self.assertContains(response, "Use the share buttons below") + def test_team_with_bad_template_challenge_set(self): + challenge = Challenge.objects.get(name="Recruit first team member") + challenge.template = "neighbourhood/challenges/_missing.html" + challenge.save() + + self.client.force_login(User.objects.get(email="holyrood-admin@example.org")) + response = self.client.get(reverse("team", args=("holyrood-palace",))) + + self.assertContains(response, "Recruit your first team member") + def test_team_with_no_challenge_set(self): self.client.force_login( User.objects.get(email="scottishparliament-admin@example.org") diff --git a/neighbourhood_warmth/settings.py b/neighbourhood_warmth/settings.py index 5dbc310..c352aa1 100644 --- a/neighbourhood_warmth/settings.py +++ b/neighbourhood_warmth/settings.py @@ -26,6 +26,7 @@ ALLOWED_HOSTS=(list, []), HIDE_DEBUG_TOOLBAR=(bool, False), LOG_LEVEL=(str, "WARNING"), + DEFAULT_CHALLENGE_TEMPLATE=(str, "neighbourhood/challenges/_default.html"), ) environ.Env.read_env(BASE_DIR / ".env") @@ -36,6 +37,7 @@ HIDE_DEBUG_TOOLBAR = env("HIDE_DEBUG_TOOLBAR") MAPIT_URL = env("MAPIT_URL") MAPIT_API_KEY = env("MAPIT_API_KEY") +DEFAULT_CHALLENGE_TEMPLATE = env("DEFAULT_CHALLENGE_TEMPLATE") # make sure CSRF checking still works behind load balancers SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") From daa1ded8b6f7f14fabc029f08d4c4434b8e82f73 Mon Sep 17 00:00:00 2001 From: Struan Donald Date: Wed, 14 Aug 2024 15:11:07 +0100 Subject: [PATCH 11/31] make challenge admin list page a bit more useable --- neighbourhood/admin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/neighbourhood/admin.py b/neighbourhood/admin.py index 81eb415..e9cf444 100644 --- a/neighbourhood/admin.py +++ b/neighbourhood/admin.py @@ -49,6 +49,9 @@ def confirmed_members_count(self, obj): @admin.register(Challenge) class ChallengeAdmin(admin.ModelAdmin): list_display = ("name", "description", "order", "is_active") + list_editable = ("order", "is_active") + list_filter = ["is_active", "has_rich_description"] + ordering = ["order"] class UserCreationForm(forms.ModelForm): From f83c8700baa9fe2b866b12a41ccdce0eb0790ba5 Mon Sep 17 00:00:00 2001 From: Struan Donald Date: Wed, 14 Aug 2024 16:11:00 +0100 Subject: [PATCH 12/31] add is_public and short_desc fields to Challenge model Used for public display of challenge progress --- neighbourhood/admin.py | 6 ++--- .../0022_public_challenge_fields.py | 22 +++++++++++++++++++ neighbourhood/models.py | 2 ++ 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 neighbourhood/migrations/0022_public_challenge_fields.py diff --git a/neighbourhood/admin.py b/neighbourhood/admin.py index e9cf444..32497c4 100644 --- a/neighbourhood/admin.py +++ b/neighbourhood/admin.py @@ -48,9 +48,9 @@ def confirmed_members_count(self, obj): @admin.register(Challenge) class ChallengeAdmin(admin.ModelAdmin): - list_display = ("name", "description", "order", "is_active") - list_editable = ("order", "is_active") - list_filter = ["is_active", "has_rich_description"] + list_display = ("name", "description", "order", "is_active", "is_public") + list_editable = ("order", "is_active", "is_public") + list_filter = ["is_active", "is_public", "has_rich_description"] ordering = ["order"] diff --git a/neighbourhood/migrations/0022_public_challenge_fields.py b/neighbourhood/migrations/0022_public_challenge_fields.py new file mode 100644 index 0000000..7aed624 --- /dev/null +++ b/neighbourhood/migrations/0022_public_challenge_fields.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.15 on 2024-08-14 14:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("neighbourhood", "0021_challenge_rich_description"), + ] + + operations = [ + migrations.AddField( + model_name="challenge", + name="is_public", + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name="challenge", + name="short_description", + field=models.CharField(blank=True, max_length=1000, null=True), + ), + ] diff --git a/neighbourhood/models.py b/neighbourhood/models.py index 9990f48..fb523b1 100644 --- a/neighbourhood/models.py +++ b/neighbourhood/models.py @@ -140,9 +140,11 @@ def __str__(self): class Challenge(models.Model): name = models.CharField(max_length=300) + short_description = models.CharField(max_length=1000, null=True, blank=True) description = models.TextField() template = models.CharField(max_length=300) is_active = models.BooleanField(default=True) + is_public = models.BooleanField(default=True) order = models.IntegerField() has_rich_description = models.BooleanField( From d3c4795076772c77400648e3a708781ea91e4d04 Mon Sep 17 00:00:00 2001 From: Struan Donald Date: Wed, 14 Aug 2024 16:12:29 +0100 Subject: [PATCH 13/31] use team challenges from database on public page if available --- neighbourhood/models.py | 4 +++- .../templates/neighbourhood/team_public.html | 16 +++++++++++++++- neighbourhood/views.py | 13 +++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/neighbourhood/models.py b/neighbourhood/models.py index fb523b1..5774512 100644 --- a/neighbourhood/models.py +++ b/neighbourhood/models.py @@ -220,12 +220,14 @@ def vicinity(self): def admins(self): return User.objects.filter(team=self, membership__is_admin=True) - def available_challenges(self): + def available_challenges(self, public_only=False): # if there's no challenge set then use the default ones if self.challenge is None: return [] challenges = Challenge.objects.filter(is_active=True).order_by("order") + if public_only: + challenges = challenges.filter(is_public=True) current_place = 0 if self.challenge: diff --git a/neighbourhood/templates/neighbourhood/team_public.html b/neighbourhood/templates/neighbourhood/team_public.html index 0a6ead7..882ae8e 100644 --- a/neighbourhood/templates/neighbourhood/team_public.html +++ b/neighbourhood/templates/neighbourhood/team_public.html @@ -42,6 +42,20 @@

    {{ team.name }}

    This team’s progress so far

    + {% if challenges %} +
    +
    +
    +
    + {% for challenge in challenges %} +
    +

    {{ challenge.challenge.name }}

    + {% if challenge.challenge.short_description %} +

    {{ challenge.challenge.short_description }}

    + {% endif %} +
    + {% endfor %} + {% else %}
    {% if 'looking' in team.status %}
    @@ -70,7 +84,7 @@

    This team’s progress so far

    Complete retrofit assessments and discuss next steps

    - + {% endif %}
    diff --git a/neighbourhood/views.py b/neighbourhood/views.py index 82f3bea..13d29c3 100644 --- a/neighbourhood/views.py +++ b/neighbourhood/views.py @@ -80,6 +80,19 @@ def get_context_data(self, **kwargs): context["applicant_count"] = Membership.objects.filter( team=team, confirmed=False, rejected=False ).count() + else: + if team.challenge: + challenges = team.available_challenges(public_only=True) + context["challenge_count"] = len(challenges) + position = next( + i + for i, c in enumerate(challenges) + if c["challenge"].name == team.challenge.name + ) + position += 1 + progress = int((position / context["challenge_count"]) * 100) + context["progress"] = progress + context["challenges"] = challenges return context From 85ab14d269abea7e53a54bef9394c8056a5f0b97 Mon Sep 17 00:00:00 2001 From: Struan Donald Date: Wed, 14 Aug 2024 16:44:05 +0100 Subject: [PATCH 14/31] make ability to create teams a config option --- neighbourhood/fixtures/teams.json | 26 ++++++++++++++ .../neighbourhood/search_results.html | 4 +-- neighbourhood/tests/test_models.py | 4 +-- neighbourhood/tests/test_views.py | 34 ++++++++++++++++++- neighbourhood/views.py | 10 ++++++ neighbourhood_warmth/settings.py | 2 ++ 6 files changed, 75 insertions(+), 5 deletions(-) diff --git a/neighbourhood/fixtures/teams.json b/neighbourhood/fixtures/teams.json index 7ce0da9..8156c00 100644 --- a/neighbourhood/fixtures/teams.json +++ b/neighbourhood/fixtures/teams.json @@ -83,6 +83,20 @@ "confirmed": false } }, + { + "model": "neighbourhood.team", + "pk": 5, + "fields": { + "name": "southwark 1", + "base_pc": "se17 3he", + "centroid": "SRID=4326;POINT (-0.096961 51.484853)", + "slug": "southwark 1", + "areas": [ 2 ], + "created": "2023-06-22T14:23:00Z", + "last_updated": "2023-06-22T14:23:00Z", + "confirmed": true + } + }, { "model": "neighbourhood.area", "pk": 1, @@ -94,5 +108,17 @@ "last_updated": "2023-06-22T14:23:00Z", "mapit_id": 2651 } + }, + { + "model": "neighbourhood.area", + "pk": 2, + "fields": { + "code": "E09000028", + "name": "Southwark Borough Council", + "area_type": "LBO", + "created": "2023-06-22T14:23:00Z", + "last_updated": "2023-06-22T14:23:00Z", + "mapit_id": 2491 + } } ] diff --git a/neighbourhood/templates/neighbourhood/search_results.html b/neighbourhood/templates/neighbourhood/search_results.html index 838ed3d..bb1b03c 100644 --- a/neighbourhood/templates/neighbourhood/search_results.html +++ b/neighbourhood/templates/neighbourhood/search_results.html @@ -59,7 +59,7 @@

    There aren’t any teams in your neighbourhood yet

    @@ -69,7 +69,7 @@

    Start a team for your neighbourhood

    - {% endcomment %} + {% endif %}
    diff --git a/neighbourhood/tests/test_models.py b/neighbourhood/tests/test_models.py index ee87cf2..0902955 100644 --- a/neighbourhood/tests/test_models.py +++ b/neighbourhood/tests/test_models.py @@ -7,8 +7,8 @@ class TeamTest(TestCase): fixtures = ["teams.json"] def test_group_count(self): - self.assertEqual(4, Team.objects.count()) - self.assertEqual(3, Team.objects.filter(confirmed=True).count()) + self.assertEqual(5, Team.objects.count()) + self.assertEqual(4, Team.objects.filter(confirmed=True).count()) def test_find_nearest(self): nearest = Team.find_nearest_teams( diff --git a/neighbourhood/tests/test_views.py b/neighbourhood/tests/test_views.py index a0932f7..6208111 100644 --- a/neighbourhood/tests/test_views.py +++ b/neighbourhood/tests/test_views.py @@ -4,7 +4,7 @@ from django.core import mail from django.shortcuts import reverse -from django.test import TestCase +from django.test import TestCase, override_settings from neighbourhood.models import Challenge, Team, User @@ -20,6 +20,7 @@ def test_about_page(self): class CreateTeamTest(TestCase): + @override_settings(CAN_CREATE_TEAMS=True) @patch("neighbourhood.utils.get_mapit_data") def test_create_team(self, mapit_get): mapit_get.return_value = { @@ -67,6 +68,11 @@ def test_create_team(self, mapit_get): } url = reverse("create_team") + + with self.settings(CAN_CREATE_TEAMS=False): + response = self.client.get(f"{url}?pc=SP1 1SP") + self.assertRedirects(response, "/") + response = self.client.get(f"{url}?pc=SP1 1SP") response = self.client.post( @@ -117,6 +123,32 @@ def test_create_team(self, mapit_get): self.assertEqual(response.url, team_url) +class TeamSearchTest(TestCase): + fixtures = ["teams.json"] + + @patch("neighbourhood.utils.get_mapit_data") + def test_search_for_area(self, mapit_get): + mapit_get.return_value = { + "postcode": "SP1 1SP", + "lon": -3.174588946918464, + "lat": 55.95206388207891, + } + + url = reverse("search_results") + response = self.client.get(url, {"pc": "SP1 1SP"}) + self.assertEqual(200, response.status_code) + + self.assertFalse(response.context["can_create_teams"]) + self.assertEquals(len(response.context["teams"]), 3) + + team_names = [t.name for t in response.context["teams"]] + self.assertFalse("southwark 1" in team_names) + + with self.settings(CAN_CREATE_TEAMS=True): + response = self.client.get(url, {"pc": "SP1 1SP"}) + self.assertTrue(response.context["can_create_teams"]) + + class TeamPagesTest(TestCase): fixtures = ["teams.json", "memberships.json"] diff --git a/neighbourhood/views.py b/neighbourhood/views.py index 13d29c3..7c09e5c 100644 --- a/neighbourhood/views.py +++ b/neighbourhood/views.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.contrib.auth import get_user_model, login from django.contrib.gis.geos import Point from django.contrib.sites.shortcuts import get_current_site @@ -52,6 +53,7 @@ def get_context_data(self, **kwargs): ) context["teams"] = nearest + context["can_create_teams"] = settings.CAN_CREATE_TEAMS return context @@ -102,6 +104,14 @@ class CreateTeamView(TitleMixin, CreateView): form_class = NewTeamForm template_name = "neighbourhood/create_team.html" + def dispatch(self, request, *args, **kwargs): + # redirect to home page if creating teams is disabled + if not settings.CAN_CREATE_TEAMS: + url = reverse("home") + return HttpResponseRedirect(url) + + return super().dispatch(request, args, kwargs) + def get_initial(self): return {"base_pc": self.request.GET.get("pc", "")} diff --git a/neighbourhood_warmth/settings.py b/neighbourhood_warmth/settings.py index c352aa1..5c209c4 100644 --- a/neighbourhood_warmth/settings.py +++ b/neighbourhood_warmth/settings.py @@ -27,6 +27,7 @@ HIDE_DEBUG_TOOLBAR=(bool, False), LOG_LEVEL=(str, "WARNING"), DEFAULT_CHALLENGE_TEMPLATE=(str, "neighbourhood/challenges/_default.html"), + CAN_CREATE_TEAMS=(bool, False), ) environ.Env.read_env(BASE_DIR / ".env") @@ -38,6 +39,7 @@ MAPIT_URL = env("MAPIT_URL") MAPIT_API_KEY = env("MAPIT_API_KEY") DEFAULT_CHALLENGE_TEMPLATE = env("DEFAULT_CHALLENGE_TEMPLATE") +CAN_CREATE_TEAMS = env("CAN_CREATE_TEAMS") # make sure CSRF checking still works behind load balancers SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") From 3d2ec176c4ffe56bbc6e654e4c7ea1d34f6c1b0a Mon Sep 17 00:00:00 2001 From: Zarino Zappia Date: Fri, 20 Sep 2024 15:35:52 +0100 Subject: [PATCH 15/31] Replace boringavatars image API with Django template tag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Boring Avatars (quite fairly) charges for their img src API now. So I’ve ported their JavaScript code to Django-flavoured Python, so we can render boring avatar SVGs directly into our pages. --- neighbourhood/models.py | 6 - .../neighbourhood/includes/generic_promo.html | 6 +- .../neighbourhood/includes/header.html | 4 +- .../neighbourhood/includes/search_result.html | 4 +- .../templates/neighbourhood/team_private.html | 3 +- .../templates/neighbourhood/team_public.html | 4 +- neighbourhood/templatetags/__init__.py | 0 neighbourhood/templatetags/boring_avatars.py | 196 ++++++++++++++++++ 8 files changed, 210 insertions(+), 13 deletions(-) create mode 100644 neighbourhood/templatetags/__init__.py create mode 100644 neighbourhood/templatetags/boring_avatars.py diff --git a/neighbourhood/models.py b/neighbourhood/models.py index 5774512..b795104 100644 --- a/neighbourhood/models.py +++ b/neighbourhood/models.py @@ -1,4 +1,3 @@ -from hashlib import sha256 from random import randrange from django import template @@ -77,11 +76,6 @@ class User(AbstractBaseUser, PermissionsMixin): objects = MyUserManager() - @property - def avatar_url(self): - colours = ["fcbf49", "eae2b7", "198754", "d62828", "ccc7ab"] - return f"https://source.boringavatars.com/beam/120/{sha256(self.email.encode('utf-8')).hexdigest()}?square&colors={','.join(colours)}" - class Area(models.Model): AREA_TYPES = { diff --git a/neighbourhood/templates/neighbourhood/includes/generic_promo.html b/neighbourhood/templates/neighbourhood/includes/generic_promo.html index 18ed6ff..7617308 100644 --- a/neighbourhood/templates/neighbourhood/includes/generic_promo.html +++ b/neighbourhood/templates/neighbourhood/includes/generic_promo.html @@ -1,3 +1,5 @@ +{% load boring_avatars %} +

    Home energy can be complicated, especially if you’re trying to figure it out alone

    @@ -8,12 +10,12 @@

    Home energy can be complicated, especially if you’re tryin {% if testimonials %}

    “We signed up to thermal imaging scans, and now I understand much more about how my home works.”

    -
    + {% boring_avatar name="ljoybcjg" width="100" height="100" class="rounded-circle bg-gray-500 ms-3" alt="" role="presentation" style="flex: 0 0 auto; width: 4rem; height: 4rem;" %}

    “It was great to get impartial, fact-based advice on the improvements we could make to our homes, so we could compare next steps, together.”

    -
    + {% boring_avatar name="afgsdjsf" width="100" height="100" class="rounded-circle bg-gray-500 me-3" alt="" role="presentation" style="flex: 0 0 auto; width: 4rem; height: 4rem;" %}
    {% endif %}

    diff --git a/neighbourhood/templates/neighbourhood/includes/header.html b/neighbourhood/templates/neighbourhood/includes/header.html index e21cb44..1a2507f 100644 --- a/neighbourhood/templates/neighbourhood/includes/header.html +++ b/neighbourhood/templates/neighbourhood/includes/header.html @@ -1,3 +1,5 @@ +{% load boring_avatars %} +
    @@ -22,7 +24,7 @@ {% if request.user.is_authenticated %}