Skip to content

Commit 700efc5

Browse files
authored
Merge branch 'main' into CDD-3090-simplified-BE-chart-watermark
2 parents 7d9a4af + 330c3f7 commit 700efc5

19 files changed

Lines changed: 500 additions & 153 deletions

File tree

README.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,20 +77,25 @@ Once again, you should include this line in the `.env` file at the root level of
7777

7878
See the [Django documentation | SECRET_KEY](https://docs.djangoproject.com/en/4.2/ref/settings/#secret-key) for more information.
7979

80-
4. Set up the virtual environment and install the project dependencies via:
80+
4. Set up for running the non-public version locally.
81+
82+
If you want to run the non-public version (i.e. pass a valid JWT into requests), you'll have to link to a deployed cognito instance by adding env variables as per the [guidance below](#remote-infrastructure)
83+
Once again, you should include these in the `.env` file at the root level of your project structure.
84+
85+
5. Set up the virtual environment and install the project dependencies via:
8186
```bash
8287
uhd venv create
8388
```
8489
This command will create a virtual environment at the `.venv/` folder at the root level of the project.
8590
The version of Python which will be used is dictated by the aforementioned `.python-version` file.
8691
And finally, the entire project dependencies will be installed within the virtual environment.
8792

88-
5. Apply the database migrations and ensure Django collects all required static files.
93+
6. Apply the database migrations and ensure Django collects all required static files.
8994
```bash
9095
uhd server setup-all
9196
```
9297

93-
6. Run a development server:
98+
7. Run a development server:
9499
```bash
95100
uhd server run-local
96101
```
@@ -323,6 +328,13 @@ for the app to collect the necessary static files:
323328
uhd server setup-static-files
324329
```
325330

331+
If using the JWT locally for passing permission sets with API requests from the frontend,
332+
you will need to set up the variables for validating the token via cognito:
333+
334+
- `export COGNITO_AWS_REGION=eu-west-2` - This is unlikely to change
335+
- `export COGNITO_USER_POOL=eu-west-2_a123bc4DE` - Can be found be checking the `User pool ID` value for your environment on the [AWS console] (https://eu-west-2.console.aws.amazon.com/cognito/v2/idp/user-pools?region=eu-west-2)
336+
- `export COGNITO_JWT_AUTH_HEADER=HTTP_X_UHD_AUTH` - This is unlikely to change
337+
326338
---
327339

328340
## Using the API
Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,43 @@
11
{
2-
"id": 79,
3-
"meta": {
4-
"search_description": "",
5-
"type": "landing_page.LandingPage",
6-
"detail_url": "https://http:/api/pages/79/",
7-
"html_url": "https://http://localhost:3000/landing-page/",
8-
"slug": "landing-page",
9-
"show_in_menus": false,
10-
"seo_title": "Home | UKHSA data dashboard",
11-
"first_published_at": "2024-09-20T13:06:42.106160+01:00",
12-
"alias_of": null,
13-
"parent": {
14-
"id": 3,
15-
"meta": {
16-
"type": "home.UKHSARootPage",
17-
"detail_url": "https://http:/api/pages/3/",
18-
"html_url": null
19-
},
20-
"title": "UKHSA Dashboard Root"
21-
}
22-
},
23-
"title": "Landing page",
24-
"sub_title": "showing public health data across England",
25-
"page_description": "",
26-
"body": "",
27-
"related_links": [],
28-
"related_links_layout": "Footer",
29-
"last_published_at": "2024-09-20T14:04:30.332316+01:00",
30-
"last_updated_at": "2024-09-24T16:40:55.228390+01:00",
31-
"seo_change_frequency": 5,
32-
"seo_priority": "0.5"
2+
"id": 79,
3+
"meta": {
4+
"search_description": "",
5+
"type": "landing_page.LandingPage",
6+
"detail_url": "https://http:/api/pages/79/",
7+
"html_url": "https://http://localhost:3000/landing-page/",
8+
"slug": "landing-page",
9+
"show_in_menus": false,
10+
"seo_title": "Home | UKHSA data dashboard",
11+
"first_published_at": "2024-09-20T13:06:42.106160+01:00",
12+
"alias_of": null,
13+
"parent": {
14+
"id": 3,
15+
"meta": {
16+
"type": "home.UKHSARootPage",
17+
"detail_url": "https://http:/api/pages/3/",
18+
"html_url": null
19+
},
20+
"title": "UKHSA Dashboard Root"
21+
}
22+
},
23+
"title": "Landing page",
24+
"sub_title": "showing public health data across England",
25+
"page_description": "",
26+
"health_topic": [
27+
{
28+
"type": "health_topic",
29+
"value": {
30+
"heading": "Health themes",
31+
"page": 83
32+
},
33+
"id": "557c2a3c-3487-4f22-91f4-76905eee178f"
34+
}
35+
],
36+
"body": "",
37+
"related_links": [],
38+
"related_links_layout": "Footer",
39+
"last_published_at": "2024-09-20T14:04:30.332316+01:00",
40+
"last_updated_at": "2024-09-24T16:40:55.228390+01:00",
41+
"seo_change_frequency": 5,
42+
"seo_priority": "0.5"
3343
}

cms/dynamic_content/blocks.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django.core.exceptions import ValidationError
22
from django.db import models
3+
from wagtail import blocks
34
from wagtail.blocks import (
45
CharBlock,
56
ChoiceBlock,
@@ -263,3 +264,26 @@ class SectionFooterLink(StructBlock):
263264

264265
class Meta:
265266
icon = "link"
267+
268+
269+
class HealthTopicSectionLink(blocks.StructBlock):
270+
heading = blocks.TextBlock(help_text=help_texts.HEADING_BLOCK, required=True)
271+
page = PageLinkChooserBlock(
272+
page_type=["topics_list.TopicsListPage"],
273+
required=True,
274+
help_text=help_texts.TOPIC_PAGE_FIELD,
275+
)
276+
277+
class Meta:
278+
icon = "link"
279+
280+
@classmethod
281+
def get_api_representation(cls, value, context=None) -> dict | None:
282+
if value:
283+
page = value.get("page")
284+
285+
return {
286+
"heading": value["heading"],
287+
"page": page.id if page else None,
288+
}
289+
return None
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Generated by Django 5.2.13 on 2026-05-07 09:08
2+
3+
import wagtail.fields
4+
from django.db import migrations
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("home", "0034_alter_landingpage_body_headline_metric_card"),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name="landingpage",
16+
name="health_topic",
17+
field=wagtail.fields.StreamField(
18+
[("health_topic", 2)],
19+
block_lookup={
20+
0: (
21+
"wagtail.blocks.TextBlock",
22+
(),
23+
{
24+
"help_text": "\nThe text you add here will be used as the heading for this section. \n",
25+
"required": True,
26+
},
27+
),
28+
1: (
29+
"cms.dynamic_content.blocks.PageLinkChooserBlock",
30+
(),
31+
{
32+
"help_text": "\nThe related topic page you want to link to. Eg: `COVID-19`\n",
33+
"page_type": ["topics_list.TopicsListPage"],
34+
"required": True,
35+
},
36+
),
37+
2: (
38+
"wagtail.blocks.StructBlock",
39+
[[("heading", 0), ("page", 1)]],
40+
{},
41+
),
42+
},
43+
default=list,
44+
),
45+
),
46+
]

cms/home/models/landing_page.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from modelcluster.fields import ParentalKey
33
from wagtail.admin.panels import FieldPanel, InlinePanel, ObjectList, TabbedInterface
44
from wagtail.api import APIField
5-
from wagtail.fields import RichTextField
5+
from wagtail.fields import RichTextField, StreamField
66
from wagtail.models import Page
77

88
from cms.dashboard.enums import (
@@ -17,8 +17,12 @@
1717
from cms.dynamic_content import help_texts
1818
from cms.dynamic_content.access import ALLOWABLE_BODY_CONTENT_SECTION_LINK
1919
from cms.dynamic_content.announcements import Announcement
20+
from cms.dynamic_content.blocks import HealthTopicSectionLink
2021
from cms.home.managers import LandingPageManager
2122

23+
HEALTH_TOPIC_SECTION_LINK_MIN_NUM: int = 1
24+
HEALTH_TOPIC_SECTION_LINK_MAX_NUM: int = 1
25+
2226

2327
class LandingPage(UKHSAPage):
2428
is_creatable = True
@@ -31,6 +35,13 @@ class LandingPage(UKHSAPage):
3135
help_text=help_texts.PAGE_DESCRIPTION_FIELD,
3236
)
3337
body = ALLOWABLE_BODY_CONTENT_SECTION_LINK
38+
health_topic = StreamField(
39+
block_types=[("health_topic", HealthTopicSectionLink())],
40+
min_num=HEALTH_TOPIC_SECTION_LINK_MIN_NUM,
41+
max_num=HEALTH_TOPIC_SECTION_LINK_MAX_NUM,
42+
default=list,
43+
use_json_field=True,
44+
)
3445

3546
related_links_layout = models.CharField(
3647
verbose_name="Layout",
@@ -49,13 +60,15 @@ class LandingPage(UKHSAPage):
4960
FieldPanel("sub_title"),
5061
FieldPanel("page_description"),
5162
FieldPanel("body"),
63+
FieldPanel("health_topic"),
5264
]
5365

5466
api_fields = UKHSAPage.api_fields + [
5567
APIField("title"),
5668
APIField("sub_title"),
5769
APIField("page_description"),
5870
APIField("body"),
71+
APIField("health_topic"),
5972
APIField("related_links_layout"),
6073
APIField("related_links"),
6174
APIField("search_description"),
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Generated by Django 5.2.13 on 2026-05-07 09:08
2+
3+
import django.db.models.deletion
4+
import wagtail.fields
5+
from django.db import migrations
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
("snippets", "0014_simplemenu"),
12+
]
13+
14+
operations = [
15+
migrations.AlterField(
16+
model_name="simplemenu",
17+
name="body",
18+
field=wagtail.fields.StreamField(
19+
[("link", 2)],
20+
block_lookup={
21+
0: (
22+
"wagtail.blocks.TextBlock",
23+
(),
24+
{
25+
"help_text": "\nThe title to display for this menu item.\nAs a general rule of thumb, the title length should be no longer than 60 characters.\n",
26+
"required": True,
27+
},
28+
),
29+
1: (
30+
"wagtail.blocks.PageChooserBlock",
31+
("wagtailcore.Page",),
32+
{
33+
"on_delete": django.db.models.deletion.CASCADE,
34+
"related_name": "+",
35+
},
36+
),
37+
2: (
38+
"wagtail.blocks.StructBlock",
39+
[[("title", 0), ("page", 1)]],
40+
{},
41+
),
42+
},
43+
help_text="\nLinks to display in the menu.\n",
44+
),
45+
),
46+
]

common/auth/cognito_jwt/backend.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import logging
2+
13
from django.apps import apps as django_apps
24
from django.conf import settings
35
from django.utils.encoding import force_str
@@ -8,17 +10,20 @@
810

911
from .validator import TokenError, TokenValidator
1012

13+
logger = logging.getLogger(__name__)
14+
1115
# 2 objects expected when parsing Auth Header: 'Bearer' + token
1216
VALID_AUTH_HEADER_LENGTH = 2
1317

1418

1519
def get_authorization_header(request):
1620
"""
17-
Return request's 'X-UHD-AUTH:' header, as a bytestring.
21+
Return request's authentication header, as a bytestring.
1822
1923
Hide some test client ickyness where the header can be unicode.
2024
"""
21-
auth = request.META.get("HTTP_X_UHD_AUTH", b"")
25+
auth_header = getattr(settings, "COGNITO_JWT_AUTH_HEADER", "Authorization")
26+
auth = request.META.get(auth_header, b"")
2227
if isinstance(auth, str):
2328
# Work around django test client oddness
2429
auth = auth.encode(HTTP_HEADER_ENCODING)
@@ -50,6 +55,11 @@ def authenticate(self, request):
5055
else:
5156
user_model = self.get_user_model()
5257
user = user_model.objects.get_or_create_for_cognito(jwt_payload)
58+
if not user:
59+
logger.debug(
60+
"Unable to create user from JWT, defaulting to unauthenticated"
61+
)
62+
return None
5363
return (user, jwt_token)
5464

5565
@staticmethod
Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22

33
from django.contrib.auth import get_user_model
4-
from django.contrib.auth.models import BaseUserManager, User
4+
from django.contrib.auth.models import BaseUserManager
55

66
logger = logging.getLogger(__name__)
77

@@ -10,17 +10,28 @@ class CognitoManager(BaseUserManager):
1010

1111
@staticmethod
1212
def get_or_create_for_cognito(jwt_payload):
13-
username = jwt_payload["entraObjectId"]
13+
"""Create an ephemeral user instance for this request.
14+
We don't need to store or retrieve any info, we use what's in the JWT,
15+
so this speeds up the request by removing the need for any DB access
16+
"""
1417
try:
15-
user = get_user_model().objects.get(username=username)
16-
logger.debug("Found existing user %s", user.username)
17-
except User.DoesNotExist:
18-
password = None
19-
user = get_user_model().objects.create_user(
20-
username=username,
21-
password=password,
18+
username = jwt_payload["entraObjectId"]
19+
permission_sets = jwt_payload["permissionSets"]
20+
if not permission_sets:
21+
logger.debug(
22+
"Empty permissionSets in token for user: '%s'",
23+
username,
24+
)
25+
return None
26+
except KeyError:
27+
logger.debug(
28+
"Error getting entraObjectId and/or permissionSets field(s)"
29+
" from jwt payload: '%s'",
30+
jwt_payload,
2231
)
23-
logger.info("Created user %s", user.username)
24-
user.is_active = True
25-
user.save()
32+
return None
33+
34+
user_class = get_user_model()
35+
user = user_class(username=username)
36+
user.permission_sets = permission_sets
2637
return user

config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363

6464
# Cognito configuration
6565
COGNITO_AWS_REGION = os.environ.get("COGNITO_AWS_REGION")
66+
COGNITO_JWT_AUTH_HEADER = os.environ.get("COGNITO_JWT_AUTH_HEADER")
6667
COGNITO_USER_POOL = os.environ.get("COGNITO_USER_POOL")
6768

6869
# Database configuration

0 commit comments

Comments
 (0)