Skip to content
New issue

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

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

Already on GitHub? # to your account

[Feature] doc templating #759

Merged
merged 1 commit into from
Jan 5, 2024
Merged
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
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not available too if you click directly from "Docs" (sidebar), you need to access it trough instance_detail to use jinja templating


## 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
Loading