Skip to content

Commit

Permalink
Feature/filter databases (#268)
Browse files Browse the repository at this point in the history
* Filter database list by user access

* Refactor filtering to a mixin and include

Allow filtering of databases and tables by the users access

* Super-linter

* Disable HTML super linter

* Only catch AttributeError in templatetags

* Disable debug in prod

* Bump chart version
  • Loading branch information
michaeljcollinsuk authored Sep 6, 2024
1 parent eeec73c commit f50e1b0
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 34 deletions.
1 change: 1 addition & 0 deletions .github/workflows/super-linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ jobs:
VALIDATE_CHECKOV: false # TODO failures to remediate at later date
VALIDATE_PYTHON_PYINK: false # we are using Black instead
VALIDATE_HTML_PRETTIER: false # incompatible with django templating language
VALIDATE_HTML: false # issues with django templating language, need to revist
12 changes: 12 additions & 0 deletions ap/database_access/models/access.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ def database_details(self):
def target_database(self):
return self.database_details.get("TargetDatabase", {})

def get_absolute_url(self):
return reverse("database_access:detail", kwargs={"database_name": self.name})

def grant_lakeformation_permissions(self, create_hybrid_opt_in=False):
lake_formation = aws.LakeFormationService()
quicksight_user = lake_formation.arn(
Expand Down Expand Up @@ -152,6 +155,15 @@ def get_absolute_url(self, viewname: str = "database_access:manage_table_access"
def get_absolute_revoke_url(self):
return self.get_absolute_url(viewname="database_access:revoke_table_access")

def get_absolute_table_detail_url(self):
return reverse(
viewname="database_access:table_detail",
kwargs={
"database_name": self.database_access.name,
"table_name": self.name,
},
)

def grant_lakeformation_permissions(self, create_hybrid_opt_in=False):
lake_formation = aws.LakeFormationService()
quicksight_user = lake_formation.arn(
Expand Down
Empty file.
23 changes: 23 additions & 0 deletions ap/database_access/templatetags/database_access_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django import template
from django.urls import reverse

register = template.Library()


@register.simple_tag
def database_detail_url(database):
try:
return database.get_absolute_url()
except AttributeError:
return reverse("database_access:detail", kwargs={"database_name": database["Name"]})


@register.simple_tag
def table_detail_url(table, database_name):
try:
return table.get_absolute_table_detail_url()
except AttributeError:
return reverse(
"database_access:table_detail",
kwargs={"database_name": database_name, "table_name": table["Name"]},
)
56 changes: 49 additions & 7 deletions ap/database_access/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.http import Http404, HttpResponse
from django.urls import reverse
from django.views.generic import CreateView, DeleteView, DetailView, TemplateView, UpdateView
from django.views.generic.base import ContextMixin
from django.views.generic.detail import SingleObjectMixin

import botocore
Expand All @@ -16,24 +17,64 @@
from . import models


class DatabaseListView(OIDCLoginRequiredMixin, TemplateView):
template_name = "database_access/database/list.html"
class FilterAccessMixin(ContextMixin):
context_list_name = "objects"

def get_user_access_objects(self):
raise NotImplementedError()

def get_all_objects(self):
raise NotImplementedError()

def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
context = super().get_context_data(**kwargs)
context["databases"] = aws.GlueService().get_database_list()
filter_by = self.request.GET.get("filter", {})
match filter_by:
case "my-access":
context[self.context_list_name] = self.get_user_access_objects()
case "all":
context[self.context_list_name] = self.get_all_objects()
case _:
context[self.context_list_name] = self.get_all_objects()

return context


class DatabaseDetailView(OIDCLoginRequiredMixin, DetailView):
class DatabaseListView(OIDCLoginRequiredMixin, FilterAccessMixin, TemplateView):
template_name = "database_access/database/list.html"
context_list_name = "databases"

def get_all_objects(self):
return aws.GlueService().get_database_list()

def get_user_access_objects(self):
return models.DatabaseAccess.objects.filter(user=self.request.user)


class DatabaseDetailView(OIDCLoginRequiredMixin, FilterAccessMixin, TemplateView):
template_name = "database_access/database/detail.html"
context_object_name = "database"
context_list_name = "tables"

def get_object(self):
def get_database(self):
database = aws.GlueService().get_database_detail(database_name=self.kwargs["database_name"])
if not database:
raise Http404("Database not found")
return database

def get_all_objects(self):
tables = aws.GlueService().get_table_list(database_name=self.kwargs["database_name"])
if not tables:
raise Http404("Database not found")
return {"name": self.kwargs["database_name"], "tables": tables}
return tables

def get_user_access_objects(self):
database = aws.GlueService().get_database_detail(database_name=self.kwargs["database_name"])
if not database:
raise Http404("Database not found")
return models.TableAccess.objects.filter(
database_access__name=self.kwargs["database_name"],
database_access__user=self.request.user,
)


class TableDetailView(OIDCLoginRequiredMixin, DetailView):
Expand All @@ -46,6 +87,7 @@ def get_object(self):
)
if not table:
raise Http404("Table not found")

return {
"name": self.kwargs["table_name"],
"is_registered_with_lake_formation": table.get("IsRegisteredWithLakeFormation"),
Expand Down
5 changes: 4 additions & 1 deletion ap/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
PROJECT_ROOT = dirname(DJANGO_ROOT)

# Name of the deployment environment (dev/prod)
ENV = os.environ.get("APP_ENVIRONMENT", "dev")
ENV = os.environ.get("APP_ENVIRONMENT", "development")

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
Expand All @@ -42,6 +42,9 @@
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get("DEBUG", True)

if ENV == "production":
DEBUG = False

# Application definition

INSTALLED_APPS = [
Expand Down
4 changes: 2 additions & 2 deletions chart/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ apiVersion: v2
name: analytical-platform-ui
description: Analytical Platform UI
type: application
version: 0.1.7
appVersion: 0.1.7
version: 0.2.0
appVersion: 0.2.0
icon: https://upload.wikimedia.org/wikipedia/en/thumb/4/4a/Ministry_of_Justice_logo_%28United_Kingdom%29.svg/611px-Ministry_of_Justice_logo_%28United_Kingdom%29.svg.png
maintainers:
- name: moj-data-platform-robot
Expand Down
51 changes: 29 additions & 22 deletions templates/database_access/database/detail.html
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
{% extends "base.html" %}
{% load database_access_tags %}
{% block title %}Analytical Platform - Tables for {{ database.name }}{% endblock title %}
{% block content %}

<span class="govuk-caption-xl">Database</span>
<h1 class="govuk-heading-xl">{{ database.name }}</h1>
<div class="govuk-width-container">

{% if database.tables %}
<table class="govuk-table">
<thead class="govuk-table__head">
<tr class="govuk-table__row">
<th scope="col" class="govuk-table__header">Table Name</th>
<th scope="col" class="govuk-table__header">Actions</th>
</tr>
</thead>
<tbody class="govuk-table__body">
{% for table in database.tables %}
<span class="govuk-caption-xl">Database</span>
<h1 class="govuk-heading-xl">{{ database_name }}</h1>

{% include "database_access/database/includes/filter_form.html" %}

{% if tables %}
<table class="govuk-table">
<thead class="govuk-table__head">
<tr class="govuk-table__row">
<td class="govuk-table__cell">{{ table.Name }}</td>
<td class="govuk-table__cell">
<a href="{% url 'database_access:table_detail' database.name table.Name %}" class="govuk-link">View</a>
</td>
<th scope="col" class="govuk-table__header">Table Name</th>
<th scope="col" class="govuk-table__header">Actions</th>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="govuk-body">No tables found.</p>
{% endif %}
</thead>
<tbody class="govuk-table__body">
{% for table in tables %}
<tr class="govuk-table__row">
<td class="govuk-table__cell">{% firstof table.name table.Name %}</td>
<td class="govuk-table__cell">
<a href="{% table_detail_url table=table database_name=database_name %}" class="govuk-link">View</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="govuk-body">No tables found.</p>
{% endif %}

</div>

{% endblock %}
11 changes: 11 additions & 0 deletions templates/database_access/database/includes/filter_form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<div class="govuk-form-group">
<form id="filter-form">
<label class="govuk-label" for="filter">
Filter by
</label>
<select class="govuk-select" id="filter" name="filter" onchange="document.getElementById('filter-form').submit();">
<option value="all" {% if "all" in request.GET.filter %}selected{% endif %}>All</option>
<option value="my-access" {% if "my-access" in request.GET.filter %}selected{% endif %}>My access</option>
</select>
</form>
</div>
7 changes: 5 additions & 2 deletions templates/database_access/database/list.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
{% extends "base.html" %}
{% load database_access_tags %}
{% block title %}Analytical Platform - Databases{% endblock title %}

{% block content %}
<div class="govuk-width-container">
<h1 class="govuk-heading-xl">Databases</h1>

{% include "database_access/database/includes/filter_form.html" %}

{% if databases %}
<table class="govuk-table">
<thead class="govuk-table__head">
Expand All @@ -16,9 +19,9 @@ <h1 class="govuk-heading-xl">Databases</h1>
<tbody class="govuk-table__body">
{% for database in databases %}
<tr class="govuk-table__row">
<td class="govuk-table__cell">{{ database.Name }}</td>
<td class="govuk-table__cell">{% firstof database.name database.Name %}</td>
<td class="govuk-table__cell">
<a href="{% url 'database_access:detail' database_name=database.Name %}" class="govuk-link">View</a>
<a href="{% database_detail_url database=database %}" class="govuk-link">View</a>
</td>
</tr>
{% endfor %}
Expand Down

0 comments on commit f50e1b0

Please sign in to comment.