diff --git a/app/core/views/common.py b/app/core/views/common.py index fef95df0..f1baac20 100644 --- a/app/core/views/common.py +++ b/app/core/views/common.py @@ -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): diff --git a/app/core/views/history.py b/app/core/views/history.py index be6d44bc..5b95b5ab 100644 --- a/app/core/views/history.py +++ b/app/core/views/history.py @@ -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 diff --git a/app/itam/templates/itam/device.html.j2 b/app/itam/templates/itam/device.html.j2 index e145329f..15c9beb5 100644 --- a/app/itam/templates/itam/device.html.j2 +++ b/app/itam/templates/itam/device.html.j2 @@ -83,7 +83,9 @@

Details - {% include 'icons/issue_link.html.j2' with issue=6 %} + {% for external_link in external_links %} + {% include 'icons/external_link.html.j2' with external_link=external_link %} + {% endfor %}

diff --git a/app/itam/templates/itam/software.html.j2 b/app/itam/templates/itam/software.html.j2 index 64290aa4..1544c00d 100644 --- a/app/itam/templates/itam/software.html.j2 +++ b/app/itam/templates/itam/software.html.j2 @@ -43,8 +43,12 @@
-

Details

- +

+ Details + {% for external_link in external_links %} + {% include 'icons/external_link.html.j2' with external_link=external_link %} + {% endfor %} +

{% csrf_token %} {{ form }}
diff --git a/app/project-static/base.css b/app/project-static/base.css index 350d6d7e..28723032 100644 --- a/app/project-static/base.css +++ b/app/project-static/base.css @@ -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; diff --git a/app/settings/forms/external_links.py b/app/settings/forms/external_links.py new file mode 100644 index 00000000..51061def --- /dev/null +++ b/app/settings/forms/external_links.py @@ -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 diff --git a/app/settings/migrations/0002_externallink.py b/app/settings/migrations/0002_externallink.py new file mode 100644 index 00000000..0b488acf --- /dev/null +++ b/app/settings/migrations/0002_externallink.py @@ -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, + }, + ), + ] diff --git a/app/settings/models/__init__.py b/app/settings/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/settings/models/external_link.py b/app/settings/models/external_link.py new file mode 100644 index 00000000..33fafcc1 --- /dev/null +++ b/app/settings/models/external_link.py @@ -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) diff --git a/app/settings/templates/icons/external_link.html.j2 b/app/settings/templates/icons/external_link.html.j2 new file mode 100644 index 00000000..7982524d --- /dev/null +++ b/app/settings/templates/icons/external_link.html.j2 @@ -0,0 +1,18 @@ + + + + + {% include 'icons/link.svg' %} + + {{ external_link.name }} + \ No newline at end of file diff --git a/app/settings/templates/icons/link.svg b/app/settings/templates/icons/link.svg new file mode 100644 index 00000000..38ec0372 --- /dev/null +++ b/app/settings/templates/icons/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/settings/templates/settings/external_link.html.j2 b/app/settings/templates/settings/external_link.html.j2 new file mode 100644 index 00000000..e16e75e1 --- /dev/null +++ b/app/settings/templates/settings/external_link.html.j2 @@ -0,0 +1,194 @@ +{% extends 'base.html.j2' %} + +{% load markdown %} + +{% block title %}{{ externallink.name }}{% endblock %} + +{% block content %} + + + +
+ + + +
+ + + {% csrf_token %} + + +
+

+ Details + {% for external_link in external_links %} + {% include 'icons/external_link.html.j2' with external_link=external_link %} + {% endfor %} +

+
+ +
+ +
+ + {{ externallink.organization }} +
+ +
+ + {{ form.name.value }} +
+ +
+ + {{ externallink.template }} +
+ +
+ + + {% if form.colour.value %} + {{ form.colour.value }} + {% else %} +   + {% endif %} + +
+ +
+ + {{ form.devices.value }} +
+ +
+ + {{ externallink.software }} +
+ +
+ + {{ externallink.created }} +
+ +
+ + {{ externallink.modified }} +
+ +
+ +
+
+ + +
+ {% if form.model_notes.value %} + {{ form.model_notes.value | markdown | safe }} + {% else %} +   + {% endif %} +
+
+
+
+ + + + + + + {% if not tab %} + + {% endif %} + +
+ + + + +
+

+ Notes +

+ {{ notes_form }} + +
+ {% if notes %} + {% for note in notes%} + {% include 'note.html.j2' %} + {% endfor %} + {% endif %} +
+ + {% if tab == 'notes' %} + + {% endif %} +
+ + + +{% endblock %} \ No newline at end of file diff --git a/app/settings/templates/settings/external_links.html.j2 b/app/settings/templates/settings/external_links.html.j2 new file mode 100644 index 00000000..28b2bb1c --- /dev/null +++ b/app/settings/templates/settings/external_links.html.j2 @@ -0,0 +1,42 @@ +{% extends 'base.html.j2' %} + + +{% block content_header_icon %}{% endblock %} + +{% block content %} + + + + + + + + + {% for item in list %} + + + + + + {% endfor %} + +
NameOrganization 
{{ item.name }}{% if item.is_global %}Global{% else %}{{ item.organization }}{% endif %} 
+ + +{% endblock %} \ No newline at end of file diff --git a/app/settings/urls.py b/app/settings/urls.py index 3b44b111..4fb0dcad 100644 --- a/app/settings/urls.py +++ b/app/settings/urls.py @@ -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/", external_link.View.as_view(), name="_external_link_view"), + path("external_links//edit", external_link.Change.as_view(), name="_external_link_change"), + path("external_links//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"), diff --git a/app/settings/views/external_link.py b/app/settings/views/external_link.py new file mode 100644 index 00000000..6ebc2b1b --- /dev/null +++ b/app/settings/views/external_link.py @@ -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 + diff --git a/docs/projects/centurion_erp/development/api/models/external_links.md b/docs/projects/centurion_erp/development/api/models/external_links.md new file mode 100644 index 00000000..ee1c10b4 --- /dev/null +++ b/docs/projects/centurion_erp/development/api/models/external_links.md @@ -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 diff --git a/docs/projects/centurion_erp/development/models.md b/docs/projects/centurion_erp/development/models.md index a7d8158c..03a6131a 100644 --- a/docs/projects/centurion_erp/development/models.md +++ b/docs/projects/centurion_erp/development/models.md @@ -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: diff --git a/docs/projects/centurion_erp/user/settings/external_links.md b/docs/projects/centurion_erp/user/settings/external_links.md new file mode 100644 index 00000000..1f20e5c9 --- /dev/null +++ b/docs/projects/centurion_erp/user/settings/external_links.md @@ -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. diff --git a/mkdocs.yml b/mkdocs.yml index 181907af..4e06137a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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: