Skip to content

Commit a2d0a9b

Browse files
Merge branch 'main' into fmd-808-nginx-rate-limiting
2 parents 3df72b3 + 4714de1 commit a2d0a9b

File tree

10 files changed

+308
-51
lines changed

10 files changed

+308
-51
lines changed

home/service/details_csv.py

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from home.service.details import (
2+
DashboardDetailsService,
3+
DatabaseDetailsService,
4+
DatasetDetailsService,
5+
)
6+
7+
8+
class DatasetDetailsCsvFormatter:
9+
def __init__(self, details_service: DatasetDetailsService):
10+
self.details_service = details_service
11+
12+
def data(self):
13+
return [
14+
(
15+
column.name,
16+
column.display_name,
17+
column.type,
18+
column.description,
19+
)
20+
for column in self.details_service.table_metadata.column_details
21+
]
22+
23+
def headers(self):
24+
return [
25+
"name",
26+
"display_name",
27+
"type",
28+
"description",
29+
]
30+
31+
32+
class DatabaseDetailsCsvFormatter:
33+
def __init__(self, details_service: DatabaseDetailsService):
34+
self.details_service = details_service
35+
36+
def data(self):
37+
return [
38+
(
39+
table.entity_ref.urn,
40+
table.entity_ref.display_name,
41+
table.description,
42+
)
43+
for table in self.details_service.entities_in_database
44+
]
45+
46+
def headers(self):
47+
return [
48+
"urn",
49+
"display_name",
50+
"description",
51+
]
52+
53+
54+
class DashboardDetailsCsvFormatter:
55+
def __init__(self, details_service: DashboardDetailsService):
56+
self.details_service = details_service
57+
58+
def data(self):
59+
return [
60+
(chart.entity_ref.urn, chart.entity_ref.display_name, chart.description)
61+
for chart in self.details_service.children
62+
]
63+
64+
def headers(self):
65+
return ["urn", "display_name", "description"]

home/tests.py

-3
This file was deleted.

home/urls.py

+5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
views.metadata_specification_view,
1414
name="metadata_specification",
1515
),
16+
path(
17+
"details/<str:result_type>/<str:urn>.csv",
18+
views.details_view_csv,
19+
name="details_csv",
20+
),
1621
path(
1722
"details/<str:result_type>/<str:urn>",
1823
views.details_view,

home/views.py

+49-47
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import csv
12
from urllib.parse import urlparse
23

34
from data_platform_catalogue.client.exceptions import EntityDoesNotExist
45
from data_platform_catalogue.search_types import DomainOption
56
from django.conf import settings
6-
from django.http import Http404, HttpResponseBadRequest
7+
from django.http import Http404, HttpResponse, HttpResponseBadRequest
78
from django.shortcuts import render
89
from django.utils.translation import gettext as _
910
from django.views.decorators.cache import cache_control
@@ -15,6 +16,11 @@
1516
DatabaseDetailsService,
1617
DatasetDetailsService,
1718
)
19+
from home.service.details_csv import (
20+
DashboardDetailsCsvFormatter,
21+
DatabaseDetailsCsvFormatter,
22+
DatasetDetailsCsvFormatter,
23+
)
1824
from home.service.domain_fetcher import DomainFetcher
1925
from home.service.glossary import GlossaryService
2026
from home.service.metadata_specification import MetadataSpecificationService
@@ -33,60 +39,56 @@ def home_view(request):
3339

3440
@cache_control(max_age=300, private=True)
3541
def details_view(request, result_type, urn):
36-
if result_type == "table":
37-
service = dataset_service(urn)
38-
return render(request, service.template, service.context)
39-
if result_type == "database":
40-
context = database_details(urn)
41-
return render(request, "details_database.html", context)
42-
if result_type == "chart":
43-
context = chart_details(urn)
44-
return render(request, "details_chart.html", context)
45-
if result_type == "dashboard":
46-
context = dashboard_details(urn)
47-
return render(request, "details_dashboard.html", context)
48-
49-
50-
def database_details(urn):
5142
try:
52-
service = DatabaseDetailsService(urn)
53-
except EntityDoesNotExist:
54-
raise Http404("Asset does not exist")
55-
56-
context = service.context
57-
58-
return context
59-
43+
if result_type == "table":
44+
service = DatasetDetailsService(urn)
45+
template = service.template
46+
elif result_type == "database":
47+
service = DatabaseDetailsService(urn)
48+
template = "details_database.html"
49+
elif result_type == "chart":
50+
service = ChartDetailsService(urn)
51+
template = "details_chart.html"
52+
elif result_type == "dashboard":
53+
service = DashboardDetailsService(urn)
54+
template = "details_dashboard.html"
55+
else:
56+
raise Http404("Invalid result type")
57+
58+
return render(request, template, service.context)
6059

61-
def dataset_service(urn):
62-
try:
63-
service = DatasetDetailsService(urn)
6460
except EntityDoesNotExist:
65-
raise Http404("Asset does not exist")
66-
67-
return service
61+
raise Http404(f"{result_type} '{urn}' does not exist")
6862

6963

70-
def chart_details(urn):
71-
try:
72-
service = ChartDetailsService(urn)
73-
except EntityDoesNotExist:
74-
raise Http404("Asset does not exist")
75-
76-
context = service.context
77-
78-
return context
79-
80-
81-
def dashboard_details(urn):
82-
try:
64+
@cache_control(max_age=300, private=True)
65+
def details_view_csv(request, result_type, urn) -> HttpResponse:
66+
if result_type == "table":
67+
service = DatasetDetailsService(urn)
68+
csv_formatter = DatasetDetailsCsvFormatter(service)
69+
elif result_type == "database":
70+
service = DatabaseDetailsService(urn)
71+
csv_formatter = DatabaseDetailsCsvFormatter(service)
72+
elif result_type == "dashboard":
8373
service = DashboardDetailsService(urn)
84-
except EntityDoesNotExist:
85-
raise Http404("Asset does not exist")
74+
csv_formatter = DashboardDetailsCsvFormatter(service)
75+
else:
76+
raise Http404("CSV not available")
77+
78+
# In case there are any quotes in the filename, remove them in order to
79+
# not to break the header.
80+
unsavoury_characters = str.maketrans({'"': ""})
81+
filename = urn.translate(unsavoury_characters) + ".csv"
8682

87-
context = service.context
83+
response = HttpResponse(
84+
content_type="text/csv",
85+
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
86+
)
87+
writer = csv.writer(response)
88+
writer.writerow(csv_formatter.headers())
89+
writer.writerows(csv_formatter.data())
8890

89-
return context
91+
return response
9092

9193

9294
@cache_control(max_age=60, private=True)

lib/datahub-client/data_platform_catalogue/entities.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ class EntitySummary(BaseModel):
265265
)
266266
description: str = Field(description="A description of the entity")
267267
entity_type: str = Field(
268-
description="indicates the tpye of entity that is summarised"
268+
description="indicates the type of entity that is summarised"
269269
)
270270
tags: list[TagRef] = Field(description="Any tags associated with the entity")
271271

templates/details_dashboard.html

+5
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@
3333
{% endfor %}
3434
</tbody>
3535
</table>
36+
<form action="{% url 'home:details_csv' result_type='dashboard' urn=entity.urn %}">
37+
<button type="submit" class="govuk-button govuk-button--secondary" data-module="govuk-button">
38+
{% translate 'Download chart descriptions (CSV format)' %}
39+
</button>
40+
</form>
3641
{% else %}
3742
<h2 class="govuk-heading-m">{% translate "Dashboard content" %}</h2>
3843
<p class="govuk-body">{% translate "This dashboard is missing chart information." %}</p>

templates/details_database.html

+5
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@
3939
{% endfor %}
4040
</tbody>
4141
</table>
42+
<form action="{% url 'home:details_csv' result_type='database' urn=entity.urn %}">
43+
<button type="submit" class="govuk-button govuk-button--secondary" data-module="govuk-button">
44+
{% translate 'Download table descriptions (CSV format)' %}
45+
</button>
46+
</form>
4247
{% else %}
4348
<h2 class="govuk-heading-m">{% translate "Database content" %}</h2>
4449
<p class="govuk-body">{% translate "This database is missing table information." %}</p>

templates/details_table.html

+6
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@
4040
{% endfor %}
4141
</tbody>
4242
</table>
43+
44+
<form action="{% url 'home:details_csv' result_type='table' urn=entity.urn %}">
45+
<button type="submit" class="govuk-button govuk-button--secondary" data-module="govuk-button">
46+
{% translate 'Download table schema (CSV format)' %}
47+
</button>
48+
</form>
4349
{% else %}
4450
<h2 class="govuk-heading-m">{% translate "Table schema" %}</h2>
4551
<p class="govuk-body">{% translate "The schema for this table is not available." %}</p>
+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
from unittest.mock import MagicMock
2+
3+
from data_platform_catalogue.entities import Column, EntityRef, EntitySummary
4+
5+
from home.service.details import (
6+
DashboardDetailsService,
7+
DatabaseDetailsService,
8+
DatasetDetailsService,
9+
)
10+
from home.service.details_csv import (
11+
DashboardDetailsCsvFormatter,
12+
DatabaseDetailsCsvFormatter,
13+
DatasetDetailsCsvFormatter,
14+
)
15+
16+
17+
def test_dataset_details_csv_formatter(example_table):
18+
details_service = MagicMock(spec=DatasetDetailsService)
19+
columns = [
20+
Column(
21+
name="foo",
22+
display_name="Foo",
23+
type="string",
24+
description="an example",
25+
nullable=False,
26+
is_primary_key=True,
27+
),
28+
Column(
29+
name="bar",
30+
display_name="Bar",
31+
type="integer",
32+
description="another example",
33+
nullable=True,
34+
is_primary_key=False,
35+
),
36+
]
37+
details_service.table_metadata = example_table
38+
example_table.column_details = columns
39+
csv_formatter = DatasetDetailsCsvFormatter(details_service)
40+
41+
assert csv_formatter.headers() == [
42+
"name",
43+
"display_name",
44+
"type",
45+
"description",
46+
]
47+
assert csv_formatter.data() == [
48+
(
49+
"foo",
50+
"Foo",
51+
"string",
52+
"an example",
53+
),
54+
(
55+
"bar",
56+
"Bar",
57+
"integer",
58+
"another example",
59+
),
60+
]
61+
62+
63+
def test_database_details_csv_formatter(example_database):
64+
tables = [
65+
EntitySummary(
66+
entity_ref=EntityRef(display_name="foo", urn="urn:foo"),
67+
description="an example",
68+
entity_type="Table",
69+
tags=[],
70+
),
71+
EntitySummary(
72+
entity_ref=EntityRef(display_name="bar", urn="urn:bar"),
73+
description="another example",
74+
entity_type="Table",
75+
tags=[],
76+
),
77+
]
78+
79+
details_service = MagicMock(spec=DatabaseDetailsService)
80+
details_service.entities_in_database = tables
81+
82+
csv_formatter = DatabaseDetailsCsvFormatter(details_service)
83+
84+
assert csv_formatter.headers() == ["urn", "display_name", "description"]
85+
assert csv_formatter.data() == [
86+
("urn:foo", "foo", "an example"),
87+
("urn:bar", "bar", "another example"),
88+
]
89+
90+
91+
def test_dashboard_details_csv_formatter(example_dashboard):
92+
charts = [
93+
EntitySummary(
94+
entity_ref=EntityRef(display_name="foo", urn="urn:foo"),
95+
description="an example",
96+
entity_type="Chart",
97+
tags=[],
98+
),
99+
EntitySummary(
100+
entity_ref=EntityRef(display_name="bar", urn="urn:bar"),
101+
description="another example",
102+
entity_type="Chart",
103+
tags=[],
104+
),
105+
]
106+
107+
details_service = MagicMock(spec=DashboardDetailsService)
108+
details_service.children = charts
109+
110+
csv_formatter = DashboardDetailsCsvFormatter(details_service)
111+
112+
assert csv_formatter.headers() == ["urn", "display_name", "description"]
113+
assert csv_formatter.data() == [
114+
("urn:foo", "foo", "an example"),
115+
("urn:bar", "bar", "another example"),
116+
]

0 commit comments

Comments
 (0)