Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Draft] Create a dashboard to show pending requests. #158

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
289 changes: 12 additions & 277 deletions jasmin_services/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
__author__ = "Matt Pryor"
__copyright__ = "Copyright 2015 UK Science and Technology Facilities Council"

from datetime import date
from urllib.parse import urlparse

import django.shortcuts
from django import http
from django.conf import settings
Expand All @@ -20,25 +17,15 @@
from django.db import models
from django.shortcuts import redirect, render
from django.template.loader import render_to_string
from django.urls import Resolver404, path, re_path, resolve, reverse
from django.urls import path, re_path, reverse
from django.utils.safestring import mark_safe

from jasmin_metadata.admin import HasMetadataModelAdmin
from jasmin_metadata.models import Form, Metadatum
from jasmin_metadata.models import Form

from .. import models as service_models
from ..actions import (
remind_pending,
send_expiry_notifications,
synchronise_service_access,
)
from ..forms import (
AdminDecisionForm,
AdminGrantForm,
AdminRequestForm,
AdminRevokeForm,
admin_message_form_factory,
)
from ..actions import remind_pending
from ..forms import AdminDecisionForm, AdminRequestForm, admin_message_form_factory
from ..models import (
Access,
Category,
Expand All @@ -53,6 +40,11 @@

# Load the admin for behaviours which are turned on.
from . import behaviour # unimport:skip
from . import dashboard, filters, grant

# Register admins from submodules.
admin.site.register(Grant, grant.GrantAdmin)
admin.site.register_view("jasmin_services/dashboard/", view=dashboard.AdminDashboardView)


class GroupAdmin(admin.ModelAdmin):
Expand Down Expand Up @@ -420,46 +412,6 @@ def has_module_permission(self, request):
return False


class _ServiceFilter(admin.SimpleListFilter):
title = "Service"
parameter_name = "service_id"

def lookups(self, request, model_admin):
# Fetch the services and the categories at once
services = Service.objects.all().select_related("category")
return tuple((s.pk, str(s)) for s in services)

def queryset(self, request, queryset):
if self.value():
return queryset.filter(access__role__service__pk=self.value())


class _ExpiredListFilter(admin.SimpleListFilter):
title = "Expired"
parameter_name = "expired"

def lookups(self, request, model_admin):
return (("1", "Yes"), ("0", "No"))

def queryset(self, request, queryset):
if self.value() == "1":
return queryset.filter(expires__lt=date.today())
elif self.value() == "0":
return queryset.filter(expires__gte=date.today())


class _ActiveListFilter(admin.SimpleListFilter):
title = "Active"
parameter_name = "active"

def lookups(self, request, model_admin):
return (("1", "Active only"),)

def queryset(self, request, queryset):
if self.value():
return queryset.filter_active()


class GrantInline(admin.TabularInline):
model = Grant
fields = ("active", "revoked", "expired", "expires")
Expand Down Expand Up @@ -493,223 +445,6 @@ class AccessAdmin(admin.ModelAdmin):
)


@admin.register(Grant)
class GrantAdmin(HasMetadataModelAdmin):
list_display = ("access", "active", "revoked", "expired", "expires", "granted_at")
list_filter = (
_ServiceFilter,
"access__role__name",
("access__user", admin.RelatedOnlyFieldListFilter),
_ActiveListFilter,
"revoked",
_ExpiredListFilter,
)
# This is expensive and unnecessary
show_full_result_count = False
search_fields = (
"access__role__service__name",
"access__role__name",
"access__user__username",
"access__user__email",
"access__user__last_name",
)
actions = (
"synchronise_service_access",
"send_expiry_notifications",
"revoke_grants",
)
list_select_related = (
"access__role",
"access__role__service",
"access__role__service__category",
"access__user",
)
# Allow "Save as new" for quick duplication of grants
save_as = True

change_form_template = "admin/jasmin_services/grant/change_form.html"

raw_id_fields = (
"access",
"previous_grant",
)

def get_form(self, request, obj=None, change=None, **kwargs):
kwargs["form"] = AdminGrantForm
return super().get_form(request, obj=obj, change=change, **kwargs)

def get_queryset(self, request):
# Annotate with information about active status
return super().get_queryset(request).annotate_active()

def synchronise_service_access(self, request, queryset):
"""
Admin action that synchronises actual service access with the selected grants.
"""
synchronise_service_access(queryset)

synchronise_service_access.short_description = "Synchronise service access"

def send_expiry_notifications(self, request, queryset):
"""
Admin action that sends expiry notifications, where required, for the selected grants.
"""
send_expiry_notifications(queryset)

send_expiry_notifications.short_description = "Send expiry notifications"

def revoke_grants(self, request, queryset):
"""
Admin action that revokes the selected grants.
"""
selected = queryset.values_list("pk", flat=True)
selected_ids = "_".join(str(pk) for pk in selected)

return redirect(
reverse(
"admin:jasmin_services_bulk_revoke",
kwargs={"ids": selected_ids},
current_app=self.admin_site.name,
)
)

revoke_grants.short_description = "Revoke selected grants"

def active(self, obj):
"""
Returns ``True`` if the given grant is the active grant for the
service/role/user combination.
"""
return obj.active

active.boolean = True

def expired(self, obj):
"""
Returns ``True`` if the given grant has expired, ``False`` otherwise.
"""
return obj.expired

expired.boolean = True

def get_referring_request(self, request):
"""
Tries to get the request which referred the user to the grant page.
"""
# If the request is not a GET request, don't bother
if request.method != "GET":
return None
referrer = request.META.get("HTTP_REFERER")
if not referrer:
return None
req_change_url_name = "{}_{}_change".format(
Request._meta.app_label, Request._meta.model_name
)
try:
match = resolve(urlparse(referrer).path)
if match.url_name == req_change_url_name:
return Request.objects.get(pk=match.args[0])
except (ValueError, Resolver404, Request.DoesNotExist):
# These are expected errors
return None

def get_changeform_initial_data(self, request):
initial = super().get_changeform_initial_data(request)
initial["granted_by"] = request.user.username
# If there is data from a referring request to populate, do that
referring = self.get_referring_request(request)
if referring:
initial.update(role=referring.access.role, user=referring.access.user)
return initial

def add_view(self, request, form_url="", extra_context=None):
# When adding a grant, add the ID of the referring request to the context if present
if request.method == "GET":
referring = self.get_referring_request(request)
if referring:
extra_context = extra_context or {}
extra_context.update(from_request=referring.pk)
elif "_from_request" in request.POST:
extra_context = extra_context or {}
extra_context.update(from_request=request.POST["_from_request"])
return super().add_view(request, form_url, extra_context)

def get_metadata_form_class(self, request, obj=None):
if not obj:
return None
try:
return obj.access.role.metadata_form_class
except Role.DoesNotExist:
return None

def get_metadata_form_initial_data(self, request, obj):
"""
Gets the initial data for the metadata form. By default, this just
returns the metadata currently attached to the object.
"""
if obj.pk:
return super().get_metadata_form_initial_data(request, obj)
# If the object has not been saved, try to get initial metadata from a
# referring request
if "_from_request" in request.POST:
referring = Request.objects.filter(pk=request.POST["_from_request"]).first()
else:
referring = self.get_referring_request(request)
if referring:
ctype = ContentType.objects.get_for_model(referring)
metadata = Metadatum.objects.filter(content_type=ctype, object_id=referring.pk)
return {d.key: d.value for d in metadata.all()}
return super().get_metadata_form_initial_data(request, obj)

def get_urls(self):
return [
re_path(
r"^bulk_revoke/(?P<ids>[0-9_]+)/$",
self.admin_site.admin_view(self.bulk_revoke),
name="jasmin_services_bulk_revoke",
),
] + super().get_urls()

def bulk_revoke(self, request, ids):
ids = ids.split("_")
if request.method == "POST":
form = AdminRevokeForm(data=request.POST)
if form.is_valid():
user_reason = form.cleaned_data["user_reason"]
internal_reason = form.cleaned_data["internal_reason"]

Grant.objects.filter(pk__in=ids).update(
revoked=True,
user_reason=user_reason,
internal_reason=internal_reason,
)
return redirect(f"{self.admin_site.name}:jasmin_services_grant_changelist")
else:
form = AdminRevokeForm()
context = {
"title": "Bulk Revoke Grants",
"form": form,
"opts": self.model._meta,
"media": self.media + form.media,
}
context.update(self.admin_site.each_context(request))
request.current_app = self.admin_site.name
return render(request, "admin/jasmin_services/grant/bulk_revoke.html", context)


class _StateListFilter(admin.SimpleListFilter):
title = "State"
parameter_name = "state"

def lookups(self, request, model_admin):
return RequestState.choices()

def queryset(self, request, queryset):
value = self.value()
if value in RequestState.all():
return queryset.filter(state=value)


@admin.register(Request)
class RequestAdmin(HasMetadataModelAdmin):
list_display = (
Expand All @@ -722,11 +457,11 @@ class RequestAdmin(HasMetadataModelAdmin):
"requested_at",
)
list_filter = (
_ServiceFilter,
filters.ServiceFilter,
"access__role__name",
("access__user", admin.RelatedOnlyFieldListFilter),
_ActiveListFilter,
_StateListFilter,
filters.ActiveListFilter,
filters.StateListFilter,
)
# This is expensive and unnecessary
show_full_result_count = False
Expand Down
34 changes: 34 additions & 0 deletions jasmin_services/admin/dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import django.db.models
import django.views.generic

from .. import models as js_models


class AdminDashboardView(django.views.generic.base.TemplateView):
template_name = "admin/jasmin_services/dashboard.html"

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)

roles_with_pending_requests = (
js_models.Role.objects.all()
.annotate(
num_pending=django.db.models.Count(
"access",
filter=django.db.models.Q(
access__request__state=js_models.RequestState.PENDING
),
)
)
.filter(num_pending__gt=0)
)
ceda_managed_pending = roles_with_pending_requests # .filter(service__ceda_managed=True) # TODO: Reinstate this check.
longer_than_a_week_pending = roles_with_pending_requests # TODO: write this check.
no_approver_pending = roles_with_pending_requests # TODO: write this check.

extra_context = {
"ceda_managed_pending": ceda_managed_pending,
"longer_than_a_week_pending": longer_than_a_week_pending,
"no_approver_pending": no_approver_pending,
}
return context | extra_context
Loading
Loading