Skip to content

Commit

Permalink
feat: add review access request form
Browse files Browse the repository at this point in the history
  • Loading branch information
Ian leggett committed Oct 29, 2024
1 parent 6b842a1 commit 921dd2a
Show file tree
Hide file tree
Showing 5 changed files with 338 additions and 0 deletions.
49 changes: 49 additions & 0 deletions dataworkspace/dataworkspace/apps/datasets/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
import waffle

from django import forms
from django.db import models
from django.conf import settings
from django.contrib.auth import get_user_model


from dataworkspace.apps.datasets.constants import AggregationType, DataSetType, TagType
from .models import DataSet, SourceLink, Tag, VisualisationCatalogueItem
from .search import SORT_FIELD_MAP, SearchDatasetsFilters
from dataworkspace.apps.core.forms import ConditionalSupportTypeRadioWidget
from ...forms import (
GOVUKDesignSystemChoiceField,
GOVUKDesignSystemForm,
Expand Down Expand Up @@ -756,3 +759,49 @@ def clean(self):
self.fields["aggregate_field"].widget.custom_context["errors"] = [err]
raise forms.ValidationError({"aggregate_field": err})
return cleaned_data


class ReviewAccessForm(GOVUKDesignSystemForm):

def __init__(self, *args, **kwargs):
self.requester = kwargs.pop("requester")
super(ReviewAccessForm, self).__init__(*args, **kwargs)
first_name = self.requester.first_name
last_name = self.requester.last_name
full_name = f"{first_name} {last_name}"
self.fields["action_type"].choices = [
("grant", f"Grant {full_name} access to this dataset"),
("other", f"Deny {full_name} access to this dataset"),
]

class ActionTypes(models.TextChoices):
GRANT = "grant", "grant"
DENY = "other", "other"

action_type = GOVUKDesignSystemRadioField(
required=True,
label="Actions you can take",
choices=ActionTypes.choices,
widget=ConditionalSupportTypeRadioWidget(heading="h2"),
)

message = GOVUKDesignSystemTextareaField(
required=False,
label="Why are you denying access to this data?",
help_text="Your answer below will be emailed to the requestor",
widget=GOVUKDesignSystemTextareaWidget(
label_is_heading=False,
attrs={"rows": 5},
),
)

def clean(self):
cleaned = super().clean()
action_type = self.cleaned_data.get("action_type", None)
if not action_type:
return
if cleaned["action_type"] == self.ActionTypes.DENY and not cleaned["message"]:
raise forms.ValidationError(
{"message": "Enter the reason(s) why you are denying access to this data"}
)
return cleaned
5 changes: 5 additions & 0 deletions dataworkspace/dataworkspace/apps/datasets/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,4 +312,9 @@
namespace="add_table",
),
),
path(
"<uuid:pk>/review-access/<int:user_id>",
login_required(views.DataSetReviewAccess.as_view()),
name="review_access",
),
]
80 changes: 80 additions & 0 deletions dataworkspace/dataworkspace/apps/datasets/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from dataworkspace import datasets_db
from dataworkspace.apps.accounts.models import UserDataTableView
from dataworkspace.apps.api_v1.core.views import invalidate_superset_user_cached_credentials
from dataworkspace.apps.request_access.models import AccessRequest
from dataworkspace.apps.applications.models import ApplicationInstance
from dataworkspace.apps.core.boto3_client import get_s3_client
from dataworkspace.apps.core.errors import DatasetPermissionDenied, DatasetPreviewDisabledError
Expand All @@ -61,6 +62,7 @@
RelatedVisualisationsSortForm,
UserSearchForm,
VisualisationCatalogueItemEditForm,
ReviewAccessForm,
)
from dataworkspace.apps.datasets.models import (
CustomDatasetQuery,
Expand Down Expand Up @@ -1871,6 +1873,84 @@ def post(self, request, *args, **kwargs):
)


class DataSetReviewAccess(EditBaseView, FormView):
form_class = ReviewAccessForm
template_name = "datasets/manage_permissions/review_access.html"

def form_valid(self, form):
[user] = get_user_model().objects.filter(id=self.kwargs["user_id"])
summary = (
PendingAuthorizedUsers.objects.all()
.filter(created_by_id=self.request.user)
.order_by("-id")
.first()
)
has_granted_access = True if self.request.POST["action_type"] == "grant" else False

if has_granted_access:
permissions = DataSetUserPermission.objects.filter(dataset=self.obj)
users_with_permission = [p.user.id for p in permissions]
users_with_permission.append(user.id)
new_user_summary = PendingAuthorizedUsers.objects.create(
created_by=self.request.user, users=json.dumps(users_with_permission)
)
new_user_summary.save()
authorized_users = set(
get_user_model().objects.filter(
id__in=json.loads(new_user_summary.users if new_user_summary.users else "[]")
)
)
AccessRequest.objects.all().filter(id=user.id).update(data_access_status="confirmed")
process_dataset_authorized_users_change(
set(authorized_users), self.request.user, self.obj, False, False, True
)
messages.success(
self.request,
f"An email has been sent to {user.first_name} {user.last_name} to let them know they now have access.",
)
# TODO: Send email to user
else:
messages.success(
self.request,
f"An email has been sent to {user.first_name} {user.last_name} to let them know their access request was not successful.",
)
# TODO: Send email to user
return HttpResponseRedirect(
reverse(
"datasets:edit_permissions_summary",
args=[self.obj.id, new_user_summary.id if has_granted_access else summary.id],
)
)

def get_form_kwargs(self):
kwargs = super(DataSetReviewAccess, self).get_form_kwargs()
args = self.kwargs
user_id = args["user_id"]
[user] = get_user_model().objects.filter(id=user_id)
kwargs["requester"] = user
return kwargs

def get_context_data(self, **kwargs):
args = self.kwargs
user_id = args["user_id"]
context = super().get_context_data(**kwargs)
context["obj"] = self.obj
context["eligibility_criteria"] = self.obj.eligibility_criteria
[user] = get_user_model().objects.filter(id=user_id)
context["full_name"] = f"{user.first_name} {user.last_name}"
context["email"] = user.email
access_request = AccessRequest.objects.filter(requester=user_id).latest("created_date")
context["is_eligible"] = access_request.eligibility_criteria_met
context["reason_for_access"] = access_request.reason_for_access
context["obj_edit_url"] = (
reverse("datasets:edit_dataset", args=[self.obj.pk])
if isinstance(self.obj, DataSet)
else reverse("datasets:edit_visualisation_catalogue_item", args=[self.obj.pk])
)
context["obj_manage_url"] = reverse("datasets:edit_permissions", args=[self.obj.id])
return context


class DatasetAuthorisedUsersSearchView(UserSearchFormView):
template_name = "datasets/search_authorised_users.html"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
{% extends '_main.html' %}
{% load humanize static datasets_tags core_tags waffle_tags %}

{% block page_title %}{{ obj.name }} - {{ block.super }}{% endblock %}

{% block breadcrumbs %}
<div class="govuk-grid-row">
<div class="govuk-grid-column-three-quarters">
<div class="govuk-breadcrumbs">
<ol class="govuk-breadcrumbs__list">
<li class="govuk-breadcrumbs__list-item">
<a class="govuk-breadcrumbs__link" href="/">Home</a>
</li>
<li class="govuk-breadcrumbs__list-item">
<a class="govuk-breadcrumbs__link" href="{{ obj.get_absolute_url }}">{{ obj.name }}</a>
</li>
<li class="govuk-breadcrumbs__list-item">
<a class="govuk-breadcrumbs__link" href="{{ obj_edit_url }}">Manage this dataset</a>
</li>
<li class="govuk-breadcrumbs__list-item">
<a class="govuk-breadcrumbs__link" href="{{obj_manage_url}}">Manage permissions</a>
</li>
<li class="govuk-breadcrumbs__list-item">
Review
</li>
</ol>
</div>
</div>
</div>
{% endblock %}

{% block content %}
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
{% include 'design_system/error_summary.html' with form=form %}
<h1 class="govuk-heading-l">Review {{full_name}}'s access to {{obj.name}}</h1>
<h2 class="govuk-heading-m">Requestor</h2>
<p class="govuk-body govuk-!-margin-0"><strong>{{full_name}}</strong></p>
<p class="govuk-body">{{email}}</p>
<h2 class="govuk-heading-m">Requestor's reason for access</h2>
<p class="govuk-body {% if not eligibility_criteria %}govuk-!-margin-bottom-7{% endif %}">{{reason_for_access}}</p>
{% if eligibility_criteria is not null %}
<h2 class="govuk-heading-m">Have the eligibility requirements been met?</h2>
<details class="govuk-details govuk govuk-!-margin-bottom-4" data-module="govuk-details">
<summary class="govuk-details__summary">
<span class="govuk-details__summary-text">
Eligibility requirements needed to access this data
</span>
</summary>
<div class="govuk-details__text">
{% if eligibility_criteria|length == 1 %}
<p class="govuk-body">{{ eligibility_criteria|first }}</p>
{% else %}
<ul class="govuk-list govuk-list--bullet">
{% for criteria in eligibility_criteria %}
<li>{{ criteria }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
</details>
{% if is_eligible %}
<p class="govuk-body"><strong>The requestor answered that they do meet the eligibility requirements</strong></p>
{% else %}
<p class="govuk-body"><strong>The requestor answered that they do not meet the eligibility requirements</strong></p>
<p class="govuk-body govuk-!-margin-bottom-7">You can still grant them access if they have a good reason for it.</p>
{% endif %}
{% endif %}
<form method="post" novalidate>
{% csrf_token %}
<div class="govuk-radios govuk-radios--conditional" data-module="govuk-radios">
{{form.action_type}}
<div class="govuk-radios__conditional govuk-radios__conditional--hidden" id="conditional-message">
{{ form.message }}
</div>
</div>
<button type="submit" class="govuk-button">
Submit
</button>
</form>
</div>
</div>
{% endblock %}
121 changes: 121 additions & 0 deletions dataworkspace/dataworkspace/tests/datasets/test_views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from datetime import timedelta, date, datetime, timezone
import json
import random
from unittest import TestCase
from urllib.parse import quote_plus
from uuid import uuid4

Expand Down Expand Up @@ -48,6 +49,7 @@
VisualisationUserPermissionFactory,
VisualisationLinkFactory,
)
from dataworkspace.tests.request_access.factories import AccessRequestFactory

from dataworkspace.tests.conftest import get_client, get_user_data
from dataworkspace.apps.applications.models import ApplicationInstance
Expand Down Expand Up @@ -5000,3 +5002,122 @@ def test_master_dataset_detail_page_shows_pipeline_failures(client, metadata_db)
len([x for x in response.context["master_datasets_info"] if x.pipeline_last_run_succeeded])
== 1
)


class TestDataSetReviewAccess:
def setUp(
self, eligibility_criteria=["You need to be eligible"], eligibility_criteria_met=True
):
self.user = factories.UserFactory.create(is_superuser=True)
self.client = Client(**get_http_sso_data(self.user))
self.user_requestor = factories.UserFactory.create(
first_name="Bob",
last_name="Testerten",
email="bob.testerten@contact-email.com",
is_superuser=False,
)
self.dataset = factories.DataSetFactory.create(
published=True,
type=DataSetType.MASTER,
name="Master",
eligibility_criteria=eligibility_criteria,
)
AccessRequestFactory(
id=self.user_requestor.id,
requester_id=self.user_requestor.id,
catalogue_item_id=self.dataset.id,
contact_email=self.user_requestor.email,
reason_for_access="I need it",
eligibility_criteria_met=eligibility_criteria_met,
)

def assert_common(self, include_eligibility_requirements=False):
response = self.client.get(
reverse(
"datasets:review_access",
kwargs={"pk": self.dataset.id, "user_id": self.user_requestor.id},
)
)
soup = BeautifulSoup(response.content.decode(response.charset))
header_one = soup.find("h1")
requester_section_header = header_one.find_next_sibling("h2")
requester_section_name = requester_section_header.find_next_sibling("p")
requester_section_email = requester_section_name.find_next_sibling("p")
requester_reason_section_header = soup.find_all("h2")[1]
requester_reason_section_reason = requester_reason_section_header.find_next_sibling("p")

form = soup.find("form")
form_legend = form.find("legend")
label_grant = form.find_all("label")[0]
label_deny = form.find_all("label")[1]
label_why_deny = form.find_all("label")[2]
[input_grant] = form.find("input", {"id": "id_action_type_0"}).get_attribute_list("value")
[input_deny] = form.find("input", {"id": "id_action_type_1"}).get_attribute_list("value")
form.find("textarea", {"id": "id_message"})

assert response.status_code == 200
assert header_one.get_text() == "Review Bob Testerten's access to Master"
assert requester_section_header.get_text() == "Requestor"
assert requester_section_name.get_text() == "Bob Testerten"
assert requester_section_email.get_text() == "bob.testerten@contact-email.com"
assert requester_reason_section_header.get_text() == "Requestor's reason for access"
assert requester_reason_section_reason.get_text() == "I need it"

assert form_legend.find("h2").get_text() == "Actions you can take"
assert label_grant.get_text() == "Grant Bob Testerten access to this dataset"
assert input_grant == "grant"
assert label_deny.get_text() == "Deny Bob Testerten access to this dataset"
assert input_deny == "other"
assert "Why are you denying access to this data?" in label_why_deny.get_text()
if include_eligibility_requirements:
self.assert_eligibility_requirements_details(soup)

return [soup, response]

def assert_eligibility_requirements_details(self, soup):
eligibility_requirements_summary = soup.find("summary")
eligibility_requirements_reason = eligibility_requirements_summary.find_next_sibling(
"div"
).find_next("p")
assert (
"Eligibility requirements needed to access this data"
in eligibility_requirements_summary.get_text()
)
assert eligibility_requirements_reason.get_text() == "You need to be eligible"

@pytest.mark.django_db
def test_user_has_met_eligibility_requirements(self):
self.setUp()
[soup, _] = self.assert_common()
self.assert_eligibility_requirements_details(soup)
requesters_eligibility_requirements_answer = soup.find("details").find_next_sibling("p")
assert (
requesters_eligibility_requirements_answer.get_text()
== "The requestor answered that they do meet the eligibility requirements"
)

@pytest.mark.django_db
def test_user_has_not_met_eligibility_requirements(self):
self.setUp(eligibility_criteria_met=False)
[soup, _] = self.assert_common()
self.assert_eligibility_requirements_details(soup)
requesters_eligibility_requirements_answer = soup.find("details").find_next_sibling("p")
requesters_eligibility_override_message = (
requesters_eligibility_requirements_answer.find_next_sibling("p")
)
assert (
requesters_eligibility_requirements_answer.get_text()
== "The requestor answered that they do not meet the eligibility requirements"
)
assert (
requesters_eligibility_override_message.get_text()
== "You can still grant them access if they have a good reason for it."
)

@pytest.mark.django_db
def test_dataset_does_not_have_eligibility_requirements(self):
self.setUp(eligibility_criteria=None)
[_, response] = self.assert_common()
assert "Have the eligibility requirements been met?" not in response.content.decode(
response.charset
)

0 comments on commit 921dd2a

Please sign in to comment.