Skip to content

Commit

Permalink
Issue/30/retire service (#53)
Browse files Browse the repository at this point in the history
* Fix linter errors in model.
* Admin action to retire a service.
* Add logic to remove grants when service is disabled.
* Fix linter.
* Add requirements.txt for deployment.
  • Loading branch information
amanning9 committed May 30, 2022
1 parent 908d896 commit 1dfa842
Show file tree
Hide file tree
Showing 9 changed files with 600 additions and 225 deletions.
73 changes: 60 additions & 13 deletions jasmin_services/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
from datetime import date
from urllib.parse import urlparse

import django.shortcuts
from django import http
from django.conf.urls import url
from django.contrib import admin, messages
from django.contrib.admin.options import IS_POPUP_VAR
from django.contrib.admin.utils import quote
Expand All @@ -19,7 +19,7 @@
from django.db import models
from django.shortcuts import redirect, render
from django.template.loader import render_to_string
from django.urls import Resolver404, resolve, reverse
from django.urls import Resolver404, path, re_path, resolve, reverse
from django.utils.safestring import mark_safe
from jasmin_metadata.admin import HasMetadataModelAdmin
from jasmin_metadata.models import Form, Metadatum
Expand Down Expand Up @@ -65,17 +65,13 @@ class GroupAdmin(admin.ModelAdmin):
fieldsets = (
(
None,
{
"fields": ("name", "description", "member_uids"),
},
{"fields": ("name", "description", "member_uids")},
),
)
superuser_fieldsets = (
(
None,
{
"fields": ("name", "description", "member_uids"),
},
{"fields": ("name", "description", "member_uids")},
),
(
"Derived / Calculated Fields",
Expand Down Expand Up @@ -161,11 +157,19 @@ def name_html(self, obj):
@admin.register(Service)
class ServiceAdmin(admin.ModelAdmin):
inlines = (RoleInline,)
list_display = ("full_name", "summary", "hidden", "position", "details_link")
list_display = (
"full_name",
"summary",
"hidden",
"disabled",
"position",
"details_link",
)
list_editable = ("position",)
list_filter = ("category", "hidden")
list_filter = ("category", "hidden", "disabled")
search_fields = ("category__long_name", "category__name", "name", "summary")
list_select_related = ("category",)
ordering = ("disabled", "category__position", "-id")

def full_name(self, obj):
return str(obj)
Expand Down Expand Up @@ -254,11 +258,16 @@ def save_related(self, request, form, formsets, change):

def get_urls(self):
return [
url(
re_path(
r"^(?P<service>[\w-]+)/message/$",
self.admin_site.admin_view(self.support_message),
name="jasmin_services_support_message",
),
path(
"<service>/retire",
self.admin_site.admin_view(self.retire),
name="jasmin_services_retire",
),
] + super().get_urls()

def support_message(self, request, service):
Expand Down Expand Up @@ -298,6 +307,44 @@ def support_message(self, request, service):
request.current_app = self.admin_site.name
return render(request, "admin/jasmin_services/service/message.html", context)

def retire(self, request, service):
"""
Admin action to retire a service.
Retireing a service hides is from all user accessible interfaces, and
"""
service = Service.objects.get(pk=service)
if (
request.method == "POST"
and request.user.is_superuser
and int(request.POST["service_id"]) == service.id
):
# Disable the service.
service.disabled = True
service.save()

# Find a list of current grants for the service.
current_grants = Grant.objects.filter_active().filter(
access__role__service=service,
revoked=False,
)
# And revoke them en-masse.
current_grants.update(
revoked=True,
user_reason="This service has been retired.",
internal_reason=f"Service was retired by {request.user.username}.",
)
return django.shortcuts.redirect(
f"/admin/jasmin_services/service/{service.id}/change"
)

context = {
"title": f"{service.name}: Retire",
"opts": self.model._meta,
"service": service,
}
return render(request, "admin/jasmin_services/service/retire.html", context)


class BehaviourInline(admin.StackedInline):
class Media:
Expand Down Expand Up @@ -669,7 +716,7 @@ def get_metadata_form_initial_data(self, request, obj):

def get_urls(self):
return [
url(
re_path(
r"^bulk_revoke/(?P<ids>[0-9_]+)/$",
self.admin_site.admin_view(self.bulk_revoke),
name="jasmin_services_bulk_revoke",
Expand Down Expand Up @@ -829,7 +876,7 @@ def get_changeform_initial_data(self, request):

def get_urls(self):
return [
url(
re_path(
r"^(.+)/decide/$",
self.admin_site.admin_view(self.decide_request),
name="jasmin_services_request_decide",
Expand Down
21 changes: 21 additions & 0 deletions jasmin_services/migrations/0020_service_disabled.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 3.2.13 on 2022-04-25 12:43

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("jasmin_services", "0019_revoke_permision"),
]

operations = [
migrations.AddField(
model_name="service",
name="disabled",
field=models.BooleanField(
default=False,
help_text="Whether this service is disabled. Disabled services are hidden, and impossible to apply for.",
),
),
]
55 changes: 21 additions & 34 deletions jasmin_services/models/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
"""
Service-related Django models for the JASMIN services app.
"""
"""Service-related Django models for the JASMIN services app."""

__author__ = "Matt Pryor"
__copyright__ = "Copyright 2015 UK Science and Technology Facilities Council"
Expand All @@ -16,17 +14,15 @@
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import models
from django.urls import reverse_lazy
from django.utils.html import mark_safe
from django.utils.safestring import mark_safe
from django_countries.fields import CountryField
from jasmin_metadata.models import Form

from .behaviours import Behaviour


class Category(models.Model):
"""
Model representing a category of services, i.e. a grouping of related services.
"""
"""Model representing a category of services, a grouping of related services."""

id = models.AutoField(primary_key=True)

Expand Down Expand Up @@ -58,9 +54,7 @@ def __str__(self):


class Service(models.Model):
"""
Model representing a service.
"""
"""Model representing a service."""

id = models.AutoField(primary_key=True)

Expand All @@ -71,7 +65,7 @@ class Meta:

@property
def details_link(self):

"""Html anchor tag linking to the service details page."""
details_url = reverse_lazy(
"jasmin_services:service_details",
kwargs={"category": self.category.name, "service": self.name},
Expand Down Expand Up @@ -137,15 +131,18 @@ def details_link(self):
default=False, help_text="Whether the service is managed by CEDA."
)

# Indicates the service is no longer used.
disabled = models.BooleanField(
default=False,
help_text="Whether this service is disabled. Disabled services are hidden, and impossible to apply for.",
)

def __str__(self):
return "{} : {}".format(self.category, self.name)
return f"{self.category} : {self.name}"


class RoleQuerySet(models.QuerySet):
"""
Custom queryset that allows filtering of the roles by the permissions they
grant.
"""
"""Custom queryset that allows filtering of the roles by the permissions granted."""

def filter_permission(self, permission_name, *objs):
"""
Expand All @@ -172,9 +169,7 @@ def filter_permission(self, permission_name, *objs):


class Role(models.Model):
"""
Model representing a role for a service.
"""
"""Model representing a role for a service."""

id = models.AutoField(primary_key=True)

Expand Down Expand Up @@ -229,16 +224,14 @@ class Meta:
)

def __str__(self):
return "{} : {} : {}".format(
self.service.category.long_name, self.service.name, self.name
)
return f"{self.service.category.long_name} : {self.service.name} : {self.name}"

@property
def metadata_form_class(self):
"""
The form class used for collecting metadata for use when approving
access. The returned form class should extend
``jasmin_metadata.forms.MetadataForm``.
Form class used for collecting metadata for use when approving access.
The returned form class should extend ``jasmin_metadata.forms.MetadataForm``.
By default, the form for each role is completely user configurable.
However, if a service type requires particular information for approval,
Expand All @@ -248,9 +241,7 @@ def metadata_form_class(self):

@property
def approvers(self):
"""
Returns a query for the users who can approve requests for this role.
"""
"""Return a query for the users who can approve requests for this role."""
# The approvers for a role are all the users who have the decide_request
# permission for the role
# It isn't practical to loop through all users and run user.has_perm
Expand Down Expand Up @@ -278,9 +269,7 @@ def approvers(self):
)

def enable(self, user):
"""
Enables this role for the given user.
"""
"""Enable this role for the given user."""
# During an import, disable all behaviours
if getattr(settings, "IS_CEDA_IMPORT", False):
return
Expand All @@ -293,9 +282,7 @@ def enable(self, user):
behaviour.apply(user)

def disable(self, user):
"""
Disables this role for the given user.
"""
"""Disable this role for the given user."""
# During an import, disable all behaviours
if getattr(settings, "IS_CEDA_IMPORT", False):
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@
<li>
<a href="{% url 'admin:jasmin_services_support_message' service=original.pk %}">Send message</a>
</li>
<li>
<a href="{% url 'admin:jasmin_services_retire' service=original.pk %}" class="deletelink">Retire Service</a>
</li>
{{ block.super }}
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{% extends "admin/base_site.html" %}
{% load static admin_urls markdown_deux_tags %}


{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script>
<script type="text/javascript" src="/static/admin/js/jquery.min.js"></script>
<script type="text/javascript" src="/static/admin/js/jquery.init.js"></script>
{% endblock %}
{% block extrastyle %}{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}" />
<link rel="stylesheet" type="text/css" href="{% static "admin/css/aside.css" %}" />
<link rel="stylesheet" type="text/css" href="{% static "admin/css/widgets.css" %}" />
{% endblock %}

{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-form{% endblock %}

{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">Home</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
&rsaquo; <a href="{% url 'admin:jasmin_services_service_change' service.pk %}">{{ service }}</a>
&rsaquo; Retire Service
</div>
{% endblock %}

{% block content %}

<div id="content-main">
<form method="POST" id="retire_service_form">
{% csrf_token %}
<input type="number" value="{{ service.id }}" name="service_id" hidden />
<div class="submit-row">
<p>
Retiring a service expires all existing grants for the service and hides it from all user accessible interfaces.
<strong>It is not possible to reverse this action</strong>.
</p>
<p>
<h2>You are going to retire {{ service }}.</h2>
</p>
<input type="submit" name="confirm" value="I am sure I want to do this" class="default" />
</div>
</form>
</div>

{% endblock %}
Loading

0 comments on commit 1dfa842

Please sign in to comment.