feat(settings): New model to allow adding templated links to devices and software

!43 #6
This commit is contained in:
2024-07-17 14:48:15 +09:30
parent 93c4fc2009
commit 29f269050f
19 changed files with 733 additions and 4 deletions

View File

@ -1,9 +1,12 @@
from django.template import Template, Context
from django.utils.html import escape
from django.views import generic
from access.mixin import OrganizationPermission
from core.exceptions import MissingAttribute
from settings.models.external_link import ExternalLink
from settings.models.user_settings import UserSettings
@ -50,6 +53,61 @@ class ChangeView(View, generic.UpdateView):
template_name:str = 'form.html.j2'
# ToDo: on migrating all views to seperate display and change views, external_links will not be required in `ChangView`
def get_context_data(self, **kwargs):
""" Get template context
For items that have the ability to have external links, this function
adds the external link details to the context.
!!! Danger "Requirement"
This function may be overridden with the caveat that this function is still called.
by the overriding function. i.e. `super().get_context_data(skwargs)`
!!! note
The adding of `external_links` within this view is scheduled to be removed.
Returns:
(dict): Context for the template to use inclusive of 'external_links'
"""
context = super().get_context_data(**kwargs)
external_links_query = None
if self.model._meta.model_name == 'device':
external_links_query = ExternalLink.objects.filter(devices=True)
elif self.model._meta.model_name == 'software':
external_links_query = ExternalLink.objects.filter(software=True)
if external_links_query:
external_links: list = []
user_context = Context(context)
for external_link in external_links_query:
user_string = Template(external_link)
external_link_context: dict = {
'name': escape(external_link.name),
'link': escape(user_string.render(user_context)),
}
if external_link.colour:
external_link_context.update({'colour': external_link.colour })
external_links += [ external_link_context ]
context['external_links'] = external_links
return context
class DeleteView(OrganizationPermission, generic.DeleteView):
@ -64,6 +122,60 @@ class DisplayView(OrganizationPermission, generic.DetailView):
template_name:str = 'form.html.j2'
# ToDo: on migrating all views to seperate display and change views, external_links will not be required in `ChangView`
def get_context_data(self, **kwargs):
""" Get template context
For items that have the ability to have external links, this function
adds the external link details to the context.
!!! Danger "Requirement"
This function may be overridden with the caveat that this function is still called.
by the overriding function. i.e. `super().get_context_data(skwargs)`
Returns:
(dict): Context for the template to use inclusive of 'external_links'
"""
context = super().get_context_data(**kwargs)
external_links_query = None
if self.model._meta.model_name == 'device':
external_links_query = ExternalLink.objects.filter(devices=True)
elif self.model._meta.model_name == 'software':
external_links_query = ExternalLink.objects.filter(software=True)
if external_links_query:
external_links: list = []
user_context = Context(context)
for external_link in external_links_query:
user_string = Template(external_link)
external_link_context: dict = {
'name': escape(external_link.name),
'link': escape(user_string.render(user_context)),
}
if external_link.colour:
external_link_context.update({'colour': external_link.colour })
external_links += [ external_link_context ]
context['external_links'] = external_links
return context
class IndexView(View, generic.ListView):

View File

@ -41,6 +41,8 @@ class View(OrganizationPermission, generic.View):
from config_management.models.groups import ConfigGroups
from settings.models.external_link import ExternalLink
if not hasattr(self, 'model'):
match self.kwargs['model_name']:
@ -61,6 +63,10 @@ class View(OrganizationPermission, generic.View):
self.model = DeviceType
case 'externallink':
self.model = ExternalLink
case 'manufacturer':
self.model = Manufacturer

View File

@ -83,7 +83,9 @@
<div id="Details" class="tabcontent">
<h3>
Details
<span style="font-weight: normal; float: right;">{% include 'icons/issue_link.html.j2' with issue=6 %}</span>
{% for external_link in external_links %}
<span style="font-weight: normal; float: right;">{% include 'icons/external_link.html.j2' with external_link=external_link %}</span>
{% endfor %}
</h3>
<div style="align-items:flex-start; align-content: center; display: flexbox; width: 100%">

View File

@ -43,8 +43,12 @@
<form method="post">
<div id="Details" class="tabcontent">
<h3>Details</h3>
<h3>
Details
{% for external_link in external_links %}
<span style="font-weight: normal; float: right;">{% include 'icons/external_link.html.j2' with external_link=external_link %}</span>
{% endfor %}
</h3>
{% csrf_token %}
{{ form }}
<br>

View File

@ -52,6 +52,7 @@ span.icon-text {
padding-right: 10px;
height: 30px;
display: inline-block;
margin-left: 5px;
}
span.icon-text a {
@ -142,6 +143,16 @@ span.icon-issue {
display: inline-block;
}
span.icon-external-link {
height: 30px;
line-height: 30px;
margin: 0px;
padding: 0px;
vertical-align: middle;
display: inline-block;
width: 25px;
}
/* .icon {
display: block;
content: none;

View File

@ -0,0 +1,21 @@
from django import forms
from django.db.models import Q
from django.contrib.auth.models import User
from access.models import Organization, TeamUsers
from core.forms.common import CommonModelForm
from settings.models.external_link import ExternalLink
class ExternalLinksForm(CommonModelForm):
prefix = 'external_links'
class Meta:
fields = '__all__'
model = ExternalLink

View File

@ -0,0 +1,37 @@
# Generated by Django 5.0.7 on 2024-07-17 05:02
import access.fields
import access.models
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0001_initial'),
('settings', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='ExternalLink',
fields=[
('is_global', models.BooleanField(default=False)),
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('name', models.CharField(help_text='Name to display on link button', max_length=30, unique=True, verbose_name='Button Name')),
('template', models.CharField(help_text='External Link template', max_length=180, verbose_name='Link Template')),
('colour', models.CharField(blank=True, default=None, help_text='Colour to render the link button. Use HTML colour code', max_length=80, null=True, verbose_name='Button Colour')),
('devices', models.BooleanField(default=False, help_text='Render link for devices', verbose_name='Devices')),
('software', models.BooleanField(default=False, help_text='Render link for software', verbose_name='Software')),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
],
options={
'abstract': False,
},
),
]

View File

View File

@ -0,0 +1,66 @@
from django.template import Template
from access.fields import *
from access.models import TenancyObject
class ExternalLink(TenancyObject):
id = models.AutoField(
primary_key=True,
unique=True,
blank=False
)
name = models.CharField(
blank = False,
max_length = 30,
unique = True,
help_text = 'Name to display on link button',
verbose_name = 'Button Name',
)
slug = None
template = models.CharField(
blank = False,
max_length = 180,
unique = False,
help_text = 'External Link template',
verbose_name = 'Link Template',
)
colour = models.CharField(
blank = True,
null = True,
default = None,
max_length = 80,
unique = False,
help_text = 'Colour to render the link button. Use HTML colour code',
verbose_name = 'Button Colour',
)
devices = models.BooleanField(
default = False,
blank = False,
help_text = 'Render link for devices',
verbose_name = 'Devices',
)
software = models.BooleanField(
default = False,
blank = False,
help_text = 'Render link for software',
verbose_name = 'Software',
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
def __str__(self):
""" Return the Template to render """
return str(self.template)

View File

@ -0,0 +1,18 @@
<style>
.inner-text {
background-color: #fff;
border-top-right-radius: 15px;
border-bottom-right-radius: 15px;
border-right: 15px;
margin-right: -5px;
padding: 1px 5px 1px 5px;
}
</style>
<span class="icon-text external-link" style="background-color: {% if external_link.colour %}{{ external_link.colour }}{% else %}#177ee6{% endif %};">
<span class="icon-external-link" style="margin-left: 5px;">
{% include 'icons/link.svg' %}
</span>
<a class="inner-text" href="{{ external_link.link }}" target="_blank"> {{ external_link.name }}</a>
</span>

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10.59,13.41C11,13.8 11,14.44 10.59,14.83C10.2,15.22 9.56,15.22 9.17,14.83C7.22,12.88 7.22,9.71 9.17,7.76V7.76L12.71,4.22C14.66,2.27 17.83,2.27 19.78,4.22C21.73,6.17 21.73,9.34 19.78,11.29L18.29,12.78C18.3,11.96 18.17,11.14 17.89,10.36L18.36,9.88C19.54,8.71 19.54,6.81 18.36,5.64C17.19,4.46 15.29,4.46 14.12,5.64L10.59,9.17C9.41,10.34 9.41,12.24 10.59,13.41M13.41,9.17C13.8,8.78 14.44,8.78 14.83,9.17C16.78,11.12 16.78,14.29 14.83,16.24V16.24L11.29,19.78C9.34,21.73 6.17,21.73 4.22,19.78C2.27,17.83 2.27,14.66 4.22,12.71L5.71,11.22C5.7,12.04 5.83,12.86 6.11,13.65L5.64,14.12C4.46,15.29 4.46,17.19 5.64,18.36C6.81,19.54 8.71,19.54 9.88,18.36L13.41,14.83C14.59,13.66 14.59,11.76 13.41,10.59C13,10.2 13,9.56 13.41,9.17Z" /></svg>

After

Width:  |  Height:  |  Size: 795 B

View File

@ -0,0 +1,194 @@
{% extends 'base.html.j2' %}
{% load markdown %}
{% block title %}{{ externallink.name }}{% endblock %}
{% block content %}
<script>
function openCity(evt, cityName) {
// Declare all variables
var i, tabcontent, tablinks;
// Get all elements with class="tabcontent" and hide them
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
// Get all elements with class="tablinks" and remove the class "active"
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
// Show the current tab, and add an "active" class to the button that opened the tab
document.getElementById(cityName).style.display = "block";
evt.currentTarget.className += " active";
}
</script>
<div class="tab">
<button onclick="window.location='{% url 'Settings:External Links' %}';"
style="vertical-align: middle; padding: auto; margin: 0px">
<svg xmlns="http://www.w3.org/2000/svg" height="25px" viewBox="0 -960 960 960" width="25px"
style="vertical-align: middle; margin: 0px; padding: 0px border: none; " fill="#6a6e73">
<path d="m313-480 155 156q11 11 11.5 27.5T468-268q-11 11-28 11t-28-11L228-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T468-692q11 11 11 28t-11 28L313-480Zm264 0 155 156q11 11 11.5 27.5T732-268q-11 11-28 11t-28-11L492-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T732-692q11 11 11 28t-11 28L577-480Z" />
</svg> Back to External Links</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
<button id="NotesOpen" class="tablinks" onclick="openCity(event, 'Notes')">Notes</button>
</div>
<style>
.detail-view-field {
display:unset;
height: 30px;
line-height: 30px;
padding: 0px 20px 40px 20px;
}
.detail-view-field label {
display: inline-block;
font-weight: bold;
width: 200px;
margin: 10px;
/*padding: 10px;*/
height: 30px;
line-height: 30px;
}
.detail-view-field span {
display: inline-block;
width: 340px;
margin: 10px;
border-bottom: 1px solid #ccc;
height: 30px;
line-height: 30px;
}
</style>
<form action="" method="post">
{% csrf_token %}
<div id="Details" class="tabcontent">
<h3>
Details
{% for external_link in external_links %}
<span style="font-weight: normal; float: right;">{% include 'icons/external_link.html.j2' with external_link=external_link %}</span>
{% endfor %}
</h3>
<div style="align-items:flex-start; align-content: center; display: flexbox; width: 100%">
<div style="display: inline; width: 40%; margin: 30px;">
<div class="detail-view-field">
<label>{{ form.organization.label }}</label>
<span>{{ externallink.organization }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.name.label }}</label>
<span>{{ form.name.value }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.template.label }}</label>
<span>{{ externallink.template }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.colour.label }}</label>
<span>
{% if form.colour.value %}
{{ form.colour.value }}
{% else %}
&nbsp;
{% endif %}
</span>
</div>
<div class="detail-view-field">
<label>{{ form.devices.label }}</label>
<span> {{ form.devices.value }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.software.label }}</label>
<span>{{ externallink.software }}</span>
</div>
<div class="detail-view-field">
<label>Created</label>
<span>{{ externallink.created }}</span>
</div>
<div class="detail-view-field">
<label>Modified</label>
<span>{{ externallink.modified }}</span>
</div>
</div>
<div style="display: inline; width: 40%; margin: 30px; text-align: left;">
<div>
<label style="font-weight: bold; width: 100%; border-bottom: 1px solid #ccc; display: block; text-align: inherit;">{{ form.model_notes.label }}</label>
<div style="display: inline-block; text-align: left;">
{% if form.model_notes.value %}
{{ form.model_notes.value | markdown | safe }}
{% else %}
&nbsp;
{% endif %}
</div>
</div>
</div>
</div>
<input type="button" value="Edit" onclick="window.location='{% url 'Settings:_external_link_change' externallink.id %}';">
{% if not tab %}
<script>
// Get the element with id="defaultOpen" and click on it
document.getElementById("defaultOpen").click();
</script>
{% endif %}
</div>
<div id="Notes" class="tabcontent">
<h3>
Notes
</h3>
{{ notes_form }}
<input type="submit" name="{{notes_form.prefix}}" value="Submit" />
<div class="comments">
{% if notes %}
{% for note in notes%}
{% include 'note.html.j2' %}
{% endfor %}
{% endif %}
</div>
{% if tab == 'notes' %}
<script>
// Get the element with id="defaultOpen" and click on it
document.getElementById("NotesOpen").click();
</script>
{% endif %}
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,42 @@
{% extends 'base.html.j2' %}
{% block content_header_icon %}{% endblock %}
{% block content %}
<input type="button" value="New External Link" onclick="window.location='{% url 'Settings:_external_link_add' %}';">
<table class="data">
<tr>
<th>Name</th>
<th>Organization</th>
<th>&nbsp;</th>
</tr>
{% for item in list %}
<tr>
<td><a href="{% url 'Settings:_external_link_view' pk=item.id %}">{{ item.name }}</a></td>
<td>{% if item.is_global %}Global{% else %}{{ item.organization }}{% endif %}</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
</table>
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page=1">&laquo; first</a>
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
<a href="?page={{ page_obj.paginator.num_pages }}">last &raquo;</a>
{% endif %}
</span>
</div>
{% endblock %}

View File

@ -2,7 +2,7 @@ from django.urls import path
from core.views import celery_log
from .views import app_settings, home, device_models, device_types, manufacturer, software_categories
from settings.views import app_settings, home, device_models, device_types, external_link, manufacturer, software_categories
from itam.views import device_type, device_model, software_category
@ -11,6 +11,13 @@ urlpatterns = [
path("", home.View.as_view(), name="Settings"),
path("external_links", external_link.Index.as_view(), name="External Links"),
path("external_links/add", external_link.Add.as_view(), name="_external_link_add"),
path("external_links/<int:pk>", external_link.View.as_view(), name="_external_link_view"),
path("external_links/<int:pk>/edit", external_link.Change.as_view(), name="_external_link_change"),
path("external_links/<int:pk>/delete", external_link.Delete.as_view(), name="_external_link_delete"),
path('application', app_settings.View.as_view(), name="_settings_application"),
path("task_results", celery_log.Index.as_view(), name="_task_results"),

View File

@ -0,0 +1,164 @@
from django.contrib.auth import decorators as auth_decorator
from django.db.models import Q
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import generic
from access.mixin import OrganizationPermission
from core.views.common import AddView, ChangeView, DeleteView, DisplayView, IndexView
from settings.forms.external_links import ExternalLinksForm
from settings.models.external_link import ExternalLink
class Index(IndexView):
context_object_name = "list"
model = ExternalLink
paginate_by = 10
permission_required = [
'settings.view_externallink'
]
template_name = 'settings/external_links.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['model_docs_path'] = self.model._meta.app_label + '/external_links/'
context['content_title'] = 'External Links'
return context
class View(ChangeView):
context_object_name = "externallink"
form_class = ExternalLinksForm
model = ExternalLink
permission_required = [
'settings.view_externallink',
]
template_name = 'settings/external_link.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['model_pk'] = self.kwargs['pk']
context['model_name'] = self.model._meta.verbose_name.replace(' ', '')
context['model_delete_url'] = reverse('Settings:_external_link_delete', args=(self.kwargs['pk'],))
context['content_title'] = self.object.name
return context
def get_success_url(self, **kwargs):
return reverse('Settings:_external_link_view', args={self.kwargs['pk']})
@method_decorator(auth_decorator.permission_required("settings.change_externallink", raise_exception=True))
def post(self, request, *args, **kwargs):
return super().post(request, *args, **kwargs)
class Change(ChangeView):
context_object_name = "externallink"
form_class = ExternalLinksForm
model = ExternalLink
permission_required = [
'settings.change_externallink',
]
template_name = 'form.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = self.object.name
return context
def get_success_url(self, **kwargs):
return reverse('Settings:_external_link_view', args={self.kwargs['pk']})
@method_decorator(auth_decorator.permission_required("settings.change_externallink", raise_exception=True))
def post(self, request, *args, **kwargs):
return super().post(request, *args, **kwargs)
class Add(AddView):
form_class = ExternalLinksForm
model = ExternalLink
permission_required = [
'settings.add_externallink',
]
template_name = 'form.html.j2'
def get_success_url(self, **kwargs):
return reverse(viewname = 'Settings:External Links')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Add External Link'
return context
class Delete(DeleteView):
model = ExternalLink
permission_required = [
'settings.delete_externallink',
]
template_name = 'form.html.j2'
def get_success_url(self, **kwargs):
return reverse('Settings:External Links')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Delete ' + self.object.name
return context

View File

@ -0,0 +1,17 @@
---
title: External Links
description: No Fuss Computings Centurion ERP External Links model documentation.
date: 2024-07-15
template: project.html
about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp
---
This model enables the end user to define external links to be rendered alongside other models display pages. The values are added to the page context in the [Change View](../common_views.md#display-view).
## External Links
::: app.settings.models.external_link.ExternalLink
options:
inherited_members: true
heading_level: 3

View File

@ -34,6 +34,11 @@ All models must meet the following requirements:
- No `queryset` is to return data that the user has not got access to. _see [queryset()](./api/models/tenancy_object.md#tenancy-object-manager)_
## History
Currently the adding of history to a model is a manual process. edit the file located at `core.views.history` and within `View.get_object` add the model to the `switch` statement.
## Tests
The following Unit test cases exists for models:

View File

@ -0,0 +1,18 @@
---
title: External Links
description: External Links user documentation for Centurion ERP by No Fuss Computing
date: 2024-07-17
template: project.html
about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp
---
External Links allow an end user to specify by means of a jinja template a link that when displayed upon an items display page will add a button with a hyperlink to the url provided. External links can be assigned to: devices and software. This includes both at the same time.
## Create a link
- Software context is under key `software`
- Device context is under key `device`
To add a templated link within the `Link Template` field enter your url, with the variable within jinja braces. for example to add a link that will expand with the devices id, specify `{{ device.id }}`. i.e. `https://domainname.tld/{{ device.id }}`. If the link is for software use key `software`. Available fields under context key all of those that are available at the time the page is rendered.

View File

@ -82,6 +82,8 @@ nav:
- projects/centurion_erp/development/api/models/core_history_save.md
- projects/centurion_erp/development/api/models/external_links.md
- projects/centurion_erp/development/api/models/itam_device.md
- projects/centurion_erp/development/api/models/access_organization_permission_checking.md
@ -194,6 +196,8 @@ nav:
- projects/centurion_erp/user/settings/app_settings.md
- projects/centurion_erp/user/settings/external_links.md
- Operations: