Skip to content

Commit 1fd0ff1

Browse files
Merge pull request #56 from ministryofjustice/implement-django-forms
Implement django forms
2 parents 5a18c6a + 8f7b4e7 commit 1fd0ff1

16 files changed

+342
-188
lines changed

.vscode/launch.json

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"name": "Python Debugger: Django",
9+
"type": "debugpy",
10+
"request": "launch",
11+
"program": "${workspaceFolder}/manage.py",
12+
"args": ["runserver"],
13+
"django": true
14+
}
15+
]
16+
}

home/forms/__init__.py

Whitespace-only changes.

home/forms/search.py

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from copy import deepcopy
2+
from django import forms
3+
from urllib.parse import urlencode
4+
5+
# from home.helper import get_domain_list
6+
7+
8+
def get_domain_choices():
9+
"""Make API call to obtain domain choices"""
10+
# TODO: pull in the domains from the catalogue client
11+
# facets = client.search_facets()
12+
# domain_list = facets.options("domains")
13+
return [
14+
("urn:li:domain:HMCTS", "HMCTS"),
15+
("urn:li:domain:HMPPS", "HMPPS"),
16+
("urn:li:domain:OPG", "OPG"),
17+
("urn:li:domain:HQ", "HQ"),
18+
]
19+
20+
21+
def get_sort_choices():
22+
return [
23+
("relevance", "Relevance"),
24+
("ascending", "Ascending"),
25+
("descending", "Descending"),
26+
]
27+
28+
29+
class SearchForm(forms.Form):
30+
"""Django form to represent data product search page inputs"""
31+
32+
query = forms.CharField(
33+
max_length=100,
34+
strip=False,
35+
required=False,
36+
widget=forms.TextInput(attrs={"class": "govuk-input search-input"}),
37+
)
38+
domains = forms.MultipleChoiceField(
39+
choices=get_domain_choices,
40+
required=False,
41+
widget=forms.CheckboxSelectMultiple(
42+
attrs={"class": "govuk-checkboxes__input", "form": "searchform"}
43+
),
44+
)
45+
sort = forms.ChoiceField(
46+
choices=get_sort_choices,
47+
widget=forms.RadioSelect(
48+
attrs={
49+
"class": "govuk-radios__input",
50+
"form": "searchform",
51+
"onchange": "document.getElementById('searchform').submit();",
52+
}
53+
),
54+
required=False,
55+
)
56+
clear_filter = forms.BooleanField(initial=False, required=False)
57+
clear_label = forms.BooleanField(initial=False, required=False)
58+
59+
def __init__(self, *args, **kwargs):
60+
super().__init__(*args, **kwargs)
61+
self.initial["sort"] = "relevance"
62+
63+
def clean_query(self):
64+
"""Example clean method to apply custom validation to input fields"""
65+
return str(self.cleaned_data["query"]).capitalize()
66+
67+
def encode_without_filter(self, filter_to_remove):
68+
"""Preformat hrefs to drop individual filters"""
69+
# Deepcopy the cleaned data dict to avoid modifying it inplace
70+
query_params = deepcopy(self.cleaned_data)
71+
72+
query_params["domains"].remove(filter_to_remove)
73+
if len(query_params["domains"]) == 0:
74+
query_params.pop("domains")
75+
76+
return f"?{urlencode(query_params, doseq=True)}"

home/service/__init__.py

Whitespace-only changes.

home/service/base.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from data_platform_catalogue.client import BaseCatalogueClient
2+
from data_platform_catalogue.client.datahub import DataHubCatalogueClient
3+
from django.conf import settings
4+
5+
6+
class GenericService:
7+
@staticmethod
8+
def _get_catalogue_client() -> BaseCatalogueClient:
9+
return DataHubCatalogueClient(
10+
jwt_token=settings.CATALOGUE_TOKEN, api_url=settings.CATALOGUE_URL
11+
)

home/service/details.py

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from data_platform_catalogue.search_types import MultiSelectFilter, ResultType
2+
from django.core.exceptions import ObjectDoesNotExist
3+
4+
from .base import GenericService
5+
6+
7+
class DetailsService(GenericService):
8+
def __init__(self, urn: str):
9+
self.urn = urn
10+
self.client = self._get_catalogue_client()
11+
12+
filter_value = [MultiSelectFilter("urn", [urn])]
13+
search_results = self.client.search(
14+
query="", page=None, filters=filter_value)
15+
16+
if not search_results.page_results:
17+
raise ObjectDoesNotExist(urn)
18+
19+
self.result = search_results.page_results[0]
20+
self.context = self._get_context()
21+
22+
def _get_context(self):
23+
context = {
24+
"result": self.result,
25+
"result_type": (
26+
"Data product"
27+
if self.result.result_type == ResultType.DATA_PRODUCT
28+
else "Table"
29+
),
30+
"page_title": f"{self.result.name} - Data catalogue",
31+
}
32+
33+
return context

home/service/search.py

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from typing import Any
2+
3+
from data_platform_catalogue.search_types import MultiSelectFilter, SortOption
4+
from django.core.paginator import Paginator
5+
6+
from home.forms.search import SearchForm
7+
8+
from .base import GenericService
9+
10+
11+
class SearchService(GenericService):
12+
def __init__(self, form: SearchForm, page: str, items_per_page: int = 20):
13+
self.form = form
14+
self.page = page
15+
self.client = self._get_catalogue_client()
16+
self.results = self._get_search_results(page, items_per_page)
17+
self.paginator = self._get_paginator(items_per_page)
18+
self.context = self._get_context()
19+
20+
def _get_search_results(self, page: str, items_per_page: int):
21+
if self.form.is_bound:
22+
form_data = self.form.cleaned_data
23+
else:
24+
form_data = {}
25+
query = form_data.get("query", "")
26+
sort = form_data.get("sort", "relevance")
27+
domains = form_data.get("domains", [])
28+
filter_value = [MultiSelectFilter(
29+
"domains", domains)] if domains else []
30+
page_for_search = str(int(page) - 1)
31+
if sort == "ascending":
32+
sort_option = SortOption(field="name", ascending=True)
33+
elif sort == "descending":
34+
sort_option = SortOption(field="name", ascending=False)
35+
else:
36+
sort_option = None
37+
38+
results = self.client.search(
39+
query=query,
40+
page=page_for_search,
41+
filters=filter_value,
42+
sort=sort_option,
43+
count=items_per_page,
44+
)
45+
46+
return results
47+
48+
def _get_paginator(self, items_per_page: int) -> Paginator:
49+
pages_list = list(range(self.results.total_results))
50+
51+
return Paginator(pages_list, items_per_page)
52+
53+
def _get_context(self) -> dict[str, Any]:
54+
55+
if self.form["query"].value:
56+
page_title = f'Search for "{self.form["query"].value}" - Data catalogue'
57+
else:
58+
page_title = "Search - Data catalogue"
59+
60+
if self.form.is_bound:
61+
label_clear_href = {
62+
filter.split(":")[-1]: self.form.encode_without_filter(filter)
63+
for filter in self.form.cleaned_data.get("domains")
64+
}
65+
else:
66+
label_clear_href = None
67+
68+
context = {
69+
"form": self.form,
70+
"results": self.results.page_results,
71+
"page_title": page_title,
72+
"page_obj": self.paginator.get_page(self.page),
73+
"page_range": self.paginator.get_elided_page_range(
74+
self.page, on_each_side=2, on_ends=1
75+
),
76+
"paginator": self.paginator,
77+
"total_results": self.results.total_results,
78+
"label_clear_href": label_clear_href,
79+
}
80+
81+
return context

home/services.py

-9
This file was deleted.

home/templatetags/future.py

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from django import template
2+
from django.utils.itercompat import is_iterable
3+
4+
register = template.Library()
5+
6+
7+
# Backport from unreleased Django
8+
# This can be removed after Django 5.1 is released
9+
@register.simple_tag(takes_context=True)
10+
def query_string(context, query_dict=None, **kwargs):
11+
"""
12+
Add, remove, and change parameters of a ``QueryDict`` and return the result
13+
as a query string. If the ``query_dict`` argument is not provided, default
14+
to ``request.GET``.
15+
For example::
16+
{% query_string foo=3 %}
17+
To remove a key::
18+
{% query_string foo=None %}
19+
To use with pagination::
20+
{% query_string page=page_obj.next_page_number %}
21+
A custom ``QueryDict`` can also be used::
22+
{% query_string my_query_dict foo=3 %}
23+
"""
24+
if query_dict is None:
25+
query_dict = context.request.GET
26+
query_dict = query_dict.copy()
27+
for key, value in kwargs.items():
28+
if value is None:
29+
if key in query_dict:
30+
del query_dict[key]
31+
elif is_iterable(value) and not isinstance(value, str):
32+
query_dict.setlist(key, value)
33+
else:
34+
query_dict[key] = value
35+
if not query_dict:
36+
return ""
37+
query_string = query_dict.urlencode()
38+
return f"?{query_string}"

0 commit comments

Comments
 (0)