Skip to content

Commit

Permalink
Improve role_apply view to allow better licence handling in CEDA Serv…
Browse files Browse the repository at this point in the history
…ices Portal. (#128)

* Change role_apply to a class based view.
* Remove licence handling code from jasmin-services.

It moves to the ceda services portal.
  • Loading branch information
amanning9 authored Feb 28, 2024
1 parent d0f71cd commit 36357c6
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 150 deletions.
Empty file removed core
Empty file.
16 changes: 0 additions & 16 deletions jasmin_services/templates/jasmin_services/role_apply.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,6 @@
</div>
</div>
</div>

{% if licence_url %}
<div class="form-group">
<div class="row">
<label class="col-2 text-right control-label" for="id_licence"><b>License</b></label>
<div class="col">
<p class="form-control-static" id="id_licence">
<object data="{{ licence_url }}" type="application/pdf" width="600" height="300">
<a href="{{ licence_url }}">Click here</a>
</object>
</p>
</div>
</div>
</div>
{% endif %}

{% if req or grant %}
<div class="row">
<div class="col-md-9 col-md-offset-3">
Expand Down
4 changes: 2 additions & 2 deletions jasmin_services/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@
),
path(
"<slug:category>/<slug:service>/apply/<slug:role>/",
views.role_apply,
views.RoleApplyView.as_view(),
name="role_apply",
),
path(
"<slug:category>/<slug:service>/apply/<slug:role>/<int:bool_grant>/<int:previous>/",
views.role_apply,
views.RoleApplyView.as_view(),
name="role_apply",
),
path("request/<int:pk>/decide/", views.request_decide, name="request_decide"),
Expand Down
4 changes: 2 additions & 2 deletions jasmin_services/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from .my_services import my_services
from .request_decide import request_decide
from .reverse_dns_check import reverse_dns_check
from .role_apply import role_apply
from .role_apply import RoleApplyView
from .service_details import service_details
from .service_list import service_list
from .service_message import service_message
Expand All @@ -16,7 +16,7 @@
"my_services",
"request_decide",
"reverse_dns_check",
"role_apply",
"RoleApplyView",
"service_details",
"service_list",
"service_message",
Expand Down
297 changes: 167 additions & 130 deletions jasmin_services/views/role_apply.py
Original file line number Diff line number Diff line change
@@ -1,158 +1,195 @@
import logging
from datetime import date

import django.contrib.auth.mixins
import django.core.exceptions
import django.http
import requests as reqs
import django.views.generic
import django.views.generic.edit
import requests
from dateutil.relativedelta import relativedelta
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.db import transaction
from django.shortcuts import render
from django.views.decorators.http import require_http_methods

from .. import models
from ..models import Access, Grant, Group, Request, RequestState, Role
from . import common

_log = logging.getLogger(__name__)


@require_http_methods(["GET", "POST"])
@login_required
@common.with_service
def role_apply(request, service, role, bool_grant=None, previous=None):
class RoleApplyView(
django.contrib.auth.mixins.LoginRequiredMixin, django.views.generic.edit.FormView
):
"""Handle for ``/<category>/<service>/apply/<role>/``.
Responds to GET and POST requests. The user must be authenticated.
Collects the necessary information to raise a request for a role.
"""
# Prevent users who are not allowed to apply for this service from doing so.
user_may_apply = common.user_may_apply(request.user, service)
if not user_may_apply[0]:
return django.http.HttpResponseForbidden(
"You do not have permission to apply for this service."
)

try:
role = Role.objects.get(service=service, name=role)
except Role.DoesNotExist:
messages.error(request, "Role does not exist")
return common.redirect_to_service(service)

previous_grant = None
previous_request = None
# bool_grant = 1 if the new request is being made from a previous grant
if bool_grant == 1:
previous_grant = Grant.objects.get(pk=previous)
previous_request = (
Request.objects.filter_active().filter(previous_grant=previous_grant).first()
@staticmethod
def get_service(category_name, service_name):
"""Get a service from it's category and name."""
try:
return models.Service.objects.get(name=service_name, category__name=category_name)
except models.Service.DoesNotExist as err:
raise django.http.Http404("Service does not exist.") from err

@staticmethod
def get_previous_request_and_grant(bool_grant, previous):
"""Get the previous request or grant from the id supplied."""
error = None
previous_grant = None
previous_request = None
# bool_grant = 1 if the new request is being made from a previous grant
if bool_grant == 1:
previous_grant = Grant.objects.get(pk=previous)
previous_request = (
Request.objects.filter_active().filter(previous_grant=previous_grant).first()
)
# bool_grant = 0 if the new request is being made from a previous request
elif bool_grant == 0:
previous_request = Request.objects.get(pk=previous)
if previous_request.previous_grant:
previous_grant = previous_request.previous_grant

# If the user has a more recent request or grant for this chain they must use that
if (previous_request and hasattr(previous_request, "next_request")) or (
previous_grant and hasattr(previous_grant, "next_grant")
):
error = "Please use the most recent request or grant"

# If the user has an active request for this chain it must be rejected
elif previous_request and previous_request.state != RequestState.REJECTED:
error = "You have already have an active request for the specified grant"

return error, previous_grant, previous_request

def setup(self, request, *args, **kwargs):
"""Set up extra class attributes depending on the service and role we are dealing with.
These are needed throughout and not just in the context.
"""
# pylint: disable=attribute-defined-outside-init
super().setup(request, *args, **kwargs)
self.redirect_to_service = True

# Get the service from the request.
self.service = self.get_service(kwargs["category"], kwargs["service"])
if self.service.disabled:
raise django.http.Http404("Service has been retired.")

# Prevent users who are not allowed to apply for this service from doing so.
user_may_apply = common.user_may_apply(request.user, self.service)
if not user_may_apply[0]:
raise django.core.exceptions.PermissionDenied(
"You do not have permission to apply for this service."
)

# Get the role we are applying for from the request.
try:
self.role = Role.objects.get(service=self.service, name=kwargs["role"])
except Role.DoesNotExist:
messages.error(request, "Role does not exist")
self.redirect_to_service = True
return

# Get the previous request or grant, if it is supplies.
error, self.previous_request, self.previous_grant = self.get_previous_request_and_grant(
kwargs.get("bool_grant", None), kwargs.get("previous", None)
)
# bool_grant = 0 if the new request is being made from a previous request
elif bool_grant == 0:
previous_request = Request.objects.get(pk=previous)
if previous_request.previous_grant:
previous_grant = previous_request.previous_grant

# If the user has a more recent request or grant for this chain they must use that
if (previous_request and hasattr(previous_request, "next_request")) or (
previous_grant and hasattr(previous_grant, "next_grant")
):
messages.info(request, "Please use the most recent request or grant")
return common.redirect_to_service(service)

# If the user has an active request for this chain it must be rejected
if previous_request and previous_request.state != RequestState.REJECTED:
messages.info(request, "You have already have an active request for the specified grant")
return common.redirect_to_service(service)

# ONLY FOR CEDA SERVICES: Get licence url
licence_url = None
if settings.LICENCE_REQUIRED:
groups = [b for b in role.behaviours.all() if isinstance(b, Group)]
if groups:
group = groups[0]
response = reqs.get(
settings.licence_url,
params={"group": group.name},
if error is not None:
messages.info(request, error)
self.redirect_to_service = True
return

# If we have sucessfuly got all the info, we do not need to redirect to the service.
self.redirect_to_service = False

def dispatch(self, request, *args, **kwargs):
"""Check the user may apply for the role before dispatching."""
if self.redirect_to_service:
return common.redirect_to_service(self.service)

return super().dispatch(request, *args, **kwargs)

def get_form_class(self):
"""Use the form class for the role being applied for.
This includes the correct metadata forms for the role.
"""
return self.role.metadata_form_class

def get_initial(self):
"""Add previous requests to inital form data."""
initial = {}
if self.previous_request is not None:
for datum in self.previous_request.metadata.all():
initial[datum.key] = datum.value
return initial

def get_template_names(self):
"""Allow overriding the template at multiple levels."""
return [
f"jasmin_services/{self.service.category.name}/{self.service.name}/{self.role.name}/role_apply.html",
f"jasmin_services/{self.service.category.name}/{self.service.name}/role_apply.html",
f"jasmin_services/{self.service.category.name}/role_apply.html",
"jasmin_services/role_apply.html",
]

def get_context_data(self, **kwargs):
"""Add the role, grant and request to the context."""
context = super().get_context_data(**kwargs)
context |= {
"role": self.role,
"grant": self.previous_grant,
"req": self.previous_request,
}
return context

def form_valid(self, form):
"""Handle the form and create the role request."""
with transaction.atomic():
access, _ = Access.objects.get_or_create(
role=self.role,
user=self.request.user,
)
json_response = response.json()
licence_url = json_response["licence"]

# Otherwise, attempt to do something
form_class = role.metadata_form_class
if request.method == "POST":
form = form_class(data=request.POST)
if form.is_valid():
with transaction.atomic():
access, _ = Access.objects.get_or_create(
role=role,
user=request.user,
# If the role is set to auto accept, grant before saving
if self.role.auto_accept:
req = Request.objects.create(
access=access,
requested_by=self.request.user.username,
state=RequestState.APPROVED,
)
req.resulting_grant = Grant.objects.create(
access=access,
granted_by="automatic",
expires=date.today() + relativedelta(years=1),
)
# If the role is set to auto accept, grant before saving
if role.auto_accept:
req = Request.objects.create(
access=access,
requested_by=request.user.username,
state=RequestState.APPROVED,
)
req.resulting_grant = Grant.objects.create(
access=access,
granted_by="automatic",
expires=date.today() + relativedelta(years=1),
)

if previous_request:
req.previous_request = previous_request
req.save()

if previous_grant:
req.resulting_grant.previous_grant = previous_grant
req.previous_grant = previous_grant
req.resulting_grant.save()

if self.previous_request is not None:
req.previous_request = self.previous_request
req.save()
form.save(req)
req.copy_metadata_to(req.resulting_grant)
else:
req = Request.objects.create(access=access, requested_by=request.user.username)

if previous_request:
previous_request.next_request = req
previous_request.save()
if self.previous_grant is not None:
req.resulting_grant.previous_grant = self.previous_grant
req.previous_grant = self.previous_grant
req.resulting_grant.save()

if previous_grant:
req.previous_grant = previous_grant
req.save()
form.save(req)
req.copy_metadata_to(req.resulting_grant)
else:
req = Request.objects.create(access=access, requested_by=self.request.user.username)

req.save()
form.save(req)
messages.success(request, "Request submitted successfully")
return common.redirect_to_service(service)
else:
messages.error(request, "Error with one or more fields")
else:
# Set the initial data to the metadata attached to the active request
initial = {}
if previous_request:
for datum in previous_request.metadata.all():
initial[datum.key] = datum.value
form = form_class(initial=initial)
templates = [
"jasmin_services/{}/{}/{}/role_apply.html".format(
service.category.name, service.name, role.name
),
"jasmin_services/{}/{}/role_apply.html".format(service.category.name, service.name),
"jasmin_services/{}/role_apply.html".format(service.category.name),
"jasmin_services/role_apply.html",
]
return render(
request,
templates,
{
"role": role,
"grant": previous_grant,
"req": previous_request,
"form": form,
"licence_url": licence_url,
},
)
if self.previous_request is not None:
self.previous_request.next_request = req
self.previous_request.save()

if self.previous_grant is not None:
req.previous_grant = self.previous_grant
req.save()
form.save(req)

messages.success(self.request, "Request submitted successfully")
return common.redirect_to_service(self.service)
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ isort = "^5.10.1"
mypy = "^1.2.0"
types-python-dateutil = "^2.8.19.12"
django-stubs = {extras = ["compatible-mypy"], version = "^4.2.0"}
pylint-django = "^2.5.5"

[tool.poetry.group.test.dependencies]
django-oauth-toolkit = "^1.7.0"
Expand All @@ -62,6 +63,9 @@ django_settings_module = "tests.settings"
[tool.pydocstyle]
ignore = ['D106']

[tool.pylint.main]
load-plugins = 'pylint_django'

[tool.pylint.'MESSAGES CONTROL']
disable = ['too-few-public-methods', 'missing-class-docstring']

Expand Down

0 comments on commit 36357c6

Please sign in to comment.