Skip to content

Commit

Permalink
[Feature] doc templating
Browse files Browse the repository at this point in the history
  • Loading branch information
Sispheor committed Jan 5, 2024
1 parent 59cd5b8 commit 4d8e1c1
Show file tree
Hide file tree
Showing 18 changed files with 189 additions and 69 deletions.
35 changes: 35 additions & 0 deletions docs/manual/service_catalog/docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Docs

Docs section allow administrators to create and link documentation to Squest services or operations.

Documentation are writen with Markdown syntax.

## Linked to services

When linked to one or more service, the documentation is shown in each "instance detail" page that correspond to the type of selected services.

Jinja templating is supported with the `instance` as context.

E.g:
```
You instance is available at {{ instance.spec.dns }}
```

## Linked to operations

When linked to one or more operation, the documentation is shown during the survey of the selected operations.

Like for services, Jinja templating is supported with the `instance` as context.

!!!note

No instance context is injected on "create" operations as the instance doesn't exist yet at this stage

## When filter

When filter can be applied to only show the documentation when some criteria based on the instance are respected.

E.g:
```
instance.user_spec.cluster_hostname == "cluster-test.lab.local"
```
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ nav:
- Service: manual/service_catalog/service.md
- Operation: manual/service_catalog/operation.md
- Survey: manual/service_catalog/survey.md
- Docs: manual/service_catalog/docs.md
- Resource tracking:
- Concept: manual/resource_tracking/concept.md
- Attribute: manual/resource_tracking/attributes.md
Expand Down
18 changes: 0 additions & 18 deletions project-static/squest/js/admin_service_form.js

This file was deleted.

10 changes: 10 additions & 0 deletions service_catalog/models/documentation.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.db.models import CharField, ManyToManyField
from jinja2 import Template
from martor.models import MartorField

from Squest.utils.squest_model import SquestModel
Expand All @@ -25,3 +26,12 @@ class Doc(SquestModel):

def __str__(self):
return self.title

def render(self, instance=None):
if instance is None:
return self.content
template = Template(self.content)
context = {
"instance": instance
}
return template.render(context)
1 change: 1 addition & 0 deletions service_catalog/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
# Doc CRUD
path('doc/', views.DocListView.as_view(), name='doc_list'),
path('doc/<int:pk>/', views.doc_details, name='doc_details'),
path('doc/<int:pk>/instance/<int:instance_id>/', views.doc_details, name='doc_details'),

# Tower server CRUD
path('tower/', views.TowerServerListView.as_view(), name='towerserver_list'),
Expand Down
39 changes: 33 additions & 6 deletions service_catalog/views/doc.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from jinja2 import UndefinedError

from Squest.utils.squest_views import SquestListView
from service_catalog.filters.doc_filter import DocFilter
from service_catalog.models import Doc
from service_catalog.models import Doc, Instance
from service_catalog.tables.doc_tables import DocTable

import logging

logger = logging.getLogger(__name__)


class DocListView(SquestListView):
table_class = DocTable
Expand All @@ -22,16 +28,37 @@ def get_context_data(self, **kwargs):


@login_required
def doc_details(request, pk):
def doc_details(request, pk, instance_id=None):
doc = get_object_or_404(Doc, id=pk)
if not request.user.has_perm('service_catalog.view_doc', doc):
raise PermissionDenied
breadcrumbs = [
{'text': 'Documentations', 'url': reverse('service_catalog:doc_list')},
{'text': doc.title, 'url': ""}
]

rendered_doc = doc.content

if instance_id is not None:
instance = get_object_or_404(Instance, id=instance_id)
if not request.user.has_perm('service_catalog.view_instance', instance):
raise PermissionDenied
try:
rendered_doc = doc.render(instance)
except UndefinedError as e:
logger.warning(f"Error: {e.message}, instance: {instance}, doc: {doc}")
messages.warning(request, f'Failure while templating documentations: {e.message}')
breadcrumbs = [
{'text': 'Instances', 'url': reverse('service_catalog:instance_list')},
{'text': f"{instance}", 'url': instance.get_absolute_url()},
{'text': "Documentation", 'url': ""},
{'text': doc.title, 'url': ""}
]
else:
breadcrumbs = [
{'text': 'Documentations', 'url': reverse('service_catalog:doc_list')},
{'text': doc.title, 'url': ""}
]

context = {
"doc": doc,
"rendered_doc": rendered_doc,
"breadcrumbs": breadcrumbs
}
return render(request,
Expand Down
21 changes: 20 additions & 1 deletion service_catalog/views/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
from service_catalog.tables.request_tables import RequestTable
from service_catalog.tables.support_tables import SupportTable

import logging

logger = logging.getLogger(__name__)


class InstanceListViewGeneric(SquestListView):
table_class = InstanceTable
Expand Down Expand Up @@ -163,6 +167,21 @@ def instance_request_new_operation(request, instance_id, operation_id):
else:
form = OperationRequestForm(request.user, **parameters)
docs = Doc.objects.filter(operations__in=[operation])
# add instance so it can be used in doc templating
rendered_docs = list()
for doc in docs:
rendered_doc = doc.content
try:
rendered_doc = doc.render(instance)
except UndefinedError as e:
logger.warning(f"Error: {e.message}, instance: {instance}, doc: {doc}")
messages.warning(request, f'Failure while templating documentation: {doc.title}. {e.message}')
rendered_docs.append({
"id": doc.id,
"rendered_doc": rendered_doc,
"title": doc.title
})

context = {
'form': form,
'operation': operation,
Expand All @@ -176,7 +195,7 @@ def instance_request_new_operation(request, instance_id, operation_id):
'icon_button': "fas fa-shopping-cart",
'text_button': "Request the operation",
'color_button': "success",
'docs': docs
'docs': rendered_docs
}
return render(request, 'service_catalog/customer/generic_list_with_docs.html', context)

Expand Down
1 change: 0 additions & 1 deletion templates/generics/confirm.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ <h5>Warning</h5>
</div>
</div>
{% load static %}
<script src="{% static 'squest/js/admin_service_form.js' %}"></script>
<script src="{% static 'admin-lte/plugins/daterangepicker/daterangepicker.js' %}"></script>
<script src="{% static 'admin-lte/plugins/moment/moment.min.js' %}"></script>
<script src="{% static 'admin-lte/plugins/tempusdominus-bootstrap-4/js/tempusdominus-bootstrap-4.js' %}"></script>
Expand Down
2 changes: 1 addition & 1 deletion templates/generics/doc_aside.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ <h3 class="card-title">
</div>
<div class="card-body p-0">
<div class="martor-preview{% if request.user.profile.theme == "dark" %} bg-dark{% endif %}">
{{ doc.content|safe_markdown }}
{{ doc.rendered_doc| safe_markdown }}
</div>
</div>
</div>
Expand Down
1 change: 0 additions & 1 deletion templates/generics/generic_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
</div>
</div>
{% load static %}
<script src="{% static 'squest/js/admin_service_form.js' %}"></script>
<script src="{% static 'admin-lte/plugins/daterangepicker/daterangepicker.js' %}"></script>
<script src="{% static 'admin-lte/plugins/moment/moment.min.js' %}"></script>
<script src="{% static 'admin-lte/plugins/tempusdominus-bootstrap-4/js/tempusdominus-bootstrap-4.js' %}"></script>
Expand Down
50 changes: 21 additions & 29 deletions templates/service_catalog/common/documentation/doc-show.html
Original file line number Diff line number Diff line change
@@ -1,45 +1,37 @@
{% extends 'base.html' %}
{% block title %}
Docs
{% endblock %}
Docs #{{ doc.id }}
{% endblock %}
{% load static %}
{% load martortags %}

{% block content %}
<div class="content-wrapper">
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
{% include "generics/breadcrumbs.html" %}
</div>
<div class="col-sm-6">
{% if request.user.is_staff %}
<a class="btn btn-default float-sm-right" href="{% url 'admin:service_catalog_doc_change' doc.id %}">
<i class="fas fa-edit"></i> Edit
</a>
{% endif %}
</div>
</div>
</div>
</div>
<div class="content">
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="martor-preview{% if request.user.profile.theme == "dark" %} bg-dark{% endif %}">
{{ doc.content|safe_markdown }}
</div>
</div>


{% block extra_header_button %}
{% if request.user.is_staff %}
<a class="btn btn-default float-sm-right"
href="{% url 'admin:service_catalog_doc_change' doc.id %}">
<i class="fas fa-edit"></i> Edit
</a>
{% endif %}
{% endblock %}

{% block main %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="martor-preview{% if request.user.profile.theme == "dark" %} bg-dark{% endif %}">
{{ rendered_doc | safe_markdown }}
</div>
</div>
</div>
</div>
{% endblock %}

{% block js %}
<script type="text/javascript" src="{% static 'plugins/js/highlight.min.js' %}"></script>
<script>
$('.martor-preview pre').each(function(i, block){
$('.martor-preview pre').each(function (i, block) {
hljs.highlightBlock(block);
});
</script>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
{% extends 'base.html' %}
{% load martortags %}
{% block header_button %}
{% if html_button_path %}
{% include html_button_path %}
{% endif %}
{% endblock %}

{% block main %}
<div class="content">
<div class="container-fluid">
<div class="row">
<div class="col-6">
Expand All @@ -22,9 +17,7 @@
</div>
</div>
</div>
</div>
{% load static %}
<script src="{% static 'squest/js/admin_service_form.js' %}"></script>
<script src="{% static 'admin-lte/plugins/daterangepicker/daterangepicker.js' %}"></script>
<script src="{% static 'admin-lte/plugins/moment/moment.min.js' %}"></script>
<script src="{% static 'admin-lte/plugins/tempusdominus-bootstrap-4/js/tempusdominus-bootstrap-4.js' %}"></script>
Expand Down
1 change: 0 additions & 1 deletion templates/service_catalog/generic_form_multiple_step.html
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ <h5 class="text-primary">{{ field.field.form_title }}</h5>
</div>
</div>
{% load static %}
<script src="{% static 'squest/js/admin_service_form.js' %}"></script>
<script src="{% static 'admin-lte/plugins/daterangepicker/daterangepicker.js' %}"></script>
<script src="{% static 'admin-lte/plugins/moment/moment.min.js' %}"></script>
<script src="{% static 'admin-lte/plugins/tempusdominus-bootstrap-4/js/tempusdominus-bootstrap-4.js' %}"></script>
Expand Down
2 changes: 1 addition & 1 deletion templates/service_catalog/instance_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ <h3 class="card-title">{{ object.name }}</h3>
<li class="list-group-item border-bottom-0">
<b>Docs</b>
{% for doc in object.docs %}
<a class="float-right" href="{% url 'service_catalog:doc_details' doc.id %}">
<a class="float-right" href="{% url 'service_catalog:doc_details' pk=doc.id instance_id=object.id %}">
{{ doc.title }}
</a> <br>
{% endfor %}
Expand Down
2 changes: 1 addition & 1 deletion templates/service_catalog/mails/request_state_update.html
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@
<ul>
{% for doc in request.instance.service.docs.all %}
<li style="line-height: 19.6px; text-align: left;">
<a rel="noopener" href="{{ current_site }}{% url 'service_catalog:doc_details' doc.id %}"
<a rel="noopener" href="{{ current_site }}{% url 'service_catalog:doc_details' pk=doc.id instance_id=request.instance.id %}"
target="_blank">{{ doc.title }}</a></li>
{% endfor %}
</ul>
Expand Down
17 changes: 17 additions & 0 deletions tests/test_service_catalog/test_models/test_doc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from service_catalog.models import Doc
from tests.setup import SetupInstance


class TestDoc(SetupInstance):

def test_render(self):
# no instance, render return content
new_doc = Doc.objects.create(title="test", content="test")
self.assertEqual(new_doc.render(), "test")

# with an instance with use the templating
self.instance_1_org1.spec["dns"] = "name.domain.local"
self.instance_1_org1.save()
new_doc.content = "test {{ instance.spec.dns }}"
new_doc.save()
self.assertEqual(new_doc.render(self.instance_1_org1), "test name.domain.local")
30 changes: 30 additions & 0 deletions tests/test_service_catalog/test_views/test_common/test_doc_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,33 @@ def test_customer_cannot_list_admin_doc(self):
def test_get_doc_page(self):
response = self.client.get(reverse('service_catalog:doc_details', args=[self.new_doc.id]))
self.assertEqual(200, response.status_code)

def test_get_doc_page_with_instance(self):
self.test_instance.spec["dns"] = "name.domain.local"
self.test_instance.save()
self.new_doc.content = "start {{ instance.spec.dns }} end"
self.new_doc.save()
response = self.client.get(reverse('service_catalog:doc_details',
args=[self.new_doc.id, self.test_instance.id]))
self.assertEqual(200, response.status_code)
self.assertIn(b"start name.domain.local end", response.content)

# selected attribute not present in the instance spec
self.test_instance.spec = {}
self.test_instance.save()
response = self.client.get(reverse('service_catalog:doc_details',
args=[self.new_doc.id, self.test_instance.id]))
self.assertEqual(200, response.status_code)
self.assertIn(b"start end", response.content)

# user has no right on the instance
self.client.force_login(self.standard_user_2)
response = self.client.get(reverse('service_catalog:doc_details',
args=[self.new_doc.id, self.test_instance.id]))
self.assertEqual(403, response.status_code)

# user has right on the instance
self.client.force_login(self.standard_user)
response = self.client.get(reverse('service_catalog:doc_details',
args=[self.new_doc.id, self.test_instance.id]))
self.assertEqual(200, response.status_code)
Loading

0 comments on commit 4d8e1c1

Please sign in to comment.