Merge branch 'development' into 'master'

chore: release

See merge request nofusscomputing/projects/django_template!7
This commit is contained in:
2024-05-18 18:25:36 +00:00
44 changed files with 1446 additions and 229 deletions

View File

@ -8,7 +8,7 @@ Unit:
- pip install -r requirements.txt
- pip install -r requirements_test.txt
- cd app
- pytest --cov --cov-report term --cov-report xml:../artifacts/coverage.xml --cov-report html:../artifacts/coverage/ --junit-xml=../artifacts/test.junit.xml
- pytest --cov --cov-report term --cov-report xml:../artifacts/coverage.xml --cov-report html:../artifacts/coverage/ --junit-xml=../artifacts/unit.JUnit.xml
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
artifacts:
expire_in: "30 days"
@ -18,7 +18,7 @@ Unit:
coverage_format: cobertura
path: artifacts/coverage.xml
junit:
- artifacts/unit.JUnit.xml
- artifacts/*.JUnit.xml
paths:
- artifacts/
rules:

View File

@ -168,11 +168,13 @@ if API_ENABLED:
],
'DEFAULT_PAGINATION_CLASS':
'rest_framework_json_api.pagination.JsonApiPageNumberPagination',
'DEFAULT_PARSER_CLASSES': (
'rest_framework_json_api.parsers.JSONParser',
'rest_framework.parsers.FormParser',
'rest_framework.parsers.MultiPartParser'
),
# leaving these uncommented, even though are the default renderers
# causes the api to require inputs the fields under an 'attributes' key
# 'DEFAULT_PARSER_CLASSES': (
# 'rest_framework_json_api.parsers.JSONParser',
# 'rest_framework.parsers.FormParser',
# 'rest_framework.parsers.MultiPartParser'
# ),
# leaving these uncommented, even though are the default renderers
# causes the api to output the fields under a 'attributes' key
# 'DEFAULT_RENDERER_CLASSES': (
@ -190,10 +192,11 @@ if API_ENABLED:
'TEST_REQUEST_RENDERER_CLASSES': (
'rest_framework_json_api.renderers.JSONRenderer',
),
'TEST_REQUEST_DEFAULT_FORMAT': 'vnd.api+json'
# 'TEST_REQUEST_DEFAULT_FORMAT': 'vnd.api+json'
'TEST_REQUEST_DEFAULT_FORMAT': 'json'
}
DATETIME_FORMAT = 'j N Y H:i:s'
if os.path.isdir(SETTINGS_DIR):

View File

@ -0,0 +1,19 @@
from django import forms
from django.db.models import Q
from itam.models.device import Device
class DeviceForm(forms.ModelForm):
prefix = 'device'
class Meta:
model = Device
fields = [
'id',
'name',
'serial_number',
'uuid',
'device_type',
]

View File

@ -0,0 +1,18 @@
from django import forms
from django.db.models import Q
from itam.models.device import DeviceOperatingSystem
class Update(forms.ModelForm):
prefix = 'operating_system'
class Meta:
model = DeviceOperatingSystem
fields = [
"id",
"version",
'operating_system_version',
]

View File

@ -0,0 +1,22 @@
from django import forms
from django.db.models import Q
from itam.models.device import DeviceSoftware
from itam.models.software import Software, SoftwareVersion
class SoftwareUpdate(forms.ModelForm):
class Meta:
model = DeviceSoftware
fields = [
'action',
'version',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['version'].queryset = SoftwareVersion.objects.filter(software_id=self.instance.software.id)

View File

@ -0,0 +1,41 @@
from app import settings
from django import forms
from django.db.models import Q
from itam.models.operating_system import OperatingSystem
class Update(forms.ModelForm):
class Meta:
model = OperatingSystem
fields = [
"name",
'slug',
'id',
'organization',
'is_global',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['_created'] = forms.DateTimeField(
label="Created",
input_formats=settings.DATETIME_FORMAT,
initial=kwargs['instance'].created,
disabled=True
)
self.fields['_modified'] = forms.DateTimeField(
label="Modified",
input_formats=settings.DATETIME_FORMAT,
initial=kwargs['instance'].modified,
disabled=True
)
if kwargs['instance'].is_global:
self.fields['is_global'].widget.attrs['disabled'] = True

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2024-05-17 10:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('itam', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='softwareversion',
name='name',
field=models.CharField(max_length=50),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.0.6 on 2024-05-17 10:18
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('itam', '0002_alter_softwareversion_name'),
]
operations = [
migrations.AddField(
model_name='devicesoftware',
name='version',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.softwareversion'),
),
]

View File

@ -0,0 +1,47 @@
# Generated by Django 5.0.6 on 2024-05-18 08:51
import access.fields
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0001_initial'),
('itam', '0003_devicesoftware_version'),
]
operations = [
migrations.CreateModel(
name='OperatingSystem',
fields=[
('is_global', models.BooleanField(default=False)),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('name', models.CharField(max_length=50, unique=True)),
('slug', access.fields.AutoSlugField()),
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='access.organization')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='OperatingSystemVersion',
fields=[
('is_global', models.BooleanField(default=False)),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('name', models.CharField(max_length=50)),
('operating_system', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='itam.operatingsystem')),
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='access.organization')),
],
options={
'abstract': False,
},
),
]

View File

@ -0,0 +1,38 @@
# Generated by Django 5.0.6 on 2024-05-18 15:20
import access.fields
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0001_initial'),
('itam', '0004_operatingsystem_operatingsystemversion'),
]
operations = [
migrations.AlterField(
model_name='operatingsystemversion',
name='name',
field=models.CharField(max_length=50, verbose_name='Major Version'),
),
migrations.CreateModel(
name='DeviceOperatingSystem',
fields=[
('is_global', models.BooleanField(default=False)),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('version', models.CharField(max_length=15, verbose_name='Installed Version')),
('device', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='itam.device')),
('operating_system_version', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='itam.operatingsystemversion', verbose_name='Operating System/Version')),
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='access.organization')),
],
options={
'abstract': False,
},
),
]

View File

@ -2,8 +2,8 @@ from django.db import models
from access.fields import *
from access.models import TenancyObject
from itam.models.software import Software
from itam.models.software import Software, SoftwareVersion
from itam.models.operating_system import OperatingSystemVersion
class DeviceCommonFields(TenancyObject, models.Model):
@ -98,11 +98,16 @@ class Device(DeviceCommonFieldsName):
state = 'absent'
software = {
software_action = {
"name": software.software.slug,
"state": state
}
config['software'] = config['software'] + [ software ]
if software.version:
software_action['version'] = software.version.name
config['software'] = config['software'] + [ software_action ]
return config
@ -140,3 +145,42 @@ class DeviceSoftware(DeviceCommonFields):
choices=Actions,
default=None,
)
version = models.ForeignKey(
SoftwareVersion,
on_delete=models.CASCADE,
default = None,
null = True,
blank= True
)
class DeviceOperatingSystem(DeviceCommonFields):
device = models.ForeignKey(
Device,
on_delete = models.CASCADE,
default = None,
null = False,
blank = False,
)
operating_system_version = models.ForeignKey(
OperatingSystemVersion,
verbose_name = 'Operating System/Version',
on_delete = models.CASCADE,
default = None,
null = False,
blank = False
)
version = models.CharField(
verbose_name = 'Installed Version',
max_length = 15,
null = False,
blank = False,
)

View File

@ -0,0 +1,64 @@
from django.db import models
from access.fields import *
from access.models import TenancyObject
class OperatingSystemCommonFields(TenancyObject, models.Model):
class Meta:
abstract = True
id = models.AutoField(
primary_key=True,
unique=True,
blank=False
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
class OperatingSystemFieldsName(OperatingSystemCommonFields):
class Meta:
abstract = True
name = models.CharField(
blank = False,
max_length = 50,
unique = True,
)
slug = AutoSlugField()
class OperatingSystem(OperatingSystemFieldsName):
def __str__(self):
return self.name
class OperatingSystemVersion(OperatingSystemCommonFields):
operating_system = models.ForeignKey(
OperatingSystem,
on_delete=models.CASCADE,
)
name = models.CharField(
verbose_name = 'Major Version',
blank = False,
max_length = 50,
unique = False,
)
def __str__(self):
return self.operating_system.name + ' ' + self.name

View File

@ -62,5 +62,15 @@ class SoftwareVersion(SoftwareCommonFields):
on_delete=models.CASCADE,
)
name = models.CharField(
blank = False,
max_length = 50,
unique = False,
)
def __str__(self):
return self.name

View File

@ -30,62 +30,83 @@
</script>
<div class="tab">
<button onclick="window.location='{% url 'ITAM:Devices' %}';">
<< Back to Devices</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'OperatingSystem')">Operating System</button>
<button class="tablinks" onclick="openCity(event, 'Software')">Software</button>
<button class="tablinks" onclick="openCity(event, 'ConfigManagement')">Config Management</button>
<!-- <button class="tablinks" onclick="openCity(event, 'Installations')">Installations</button> -->
<button onclick="window.location='{% url 'ITAM:Devices' %}';"
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 Devices</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'OperatingSystem')">Operating System</button>
<button class="tablinks" onclick="openCity(event, 'Software')">Software</button>
<button class="tablinks" onclick="openCity(event, 'ConfigManagement')">Config Management</button>
<!-- <button class="tablinks" onclick="openCity(event, 'Installations')">Installations</button> -->
</div>
<form action="" method="post">
{% csrf_token %}
<!-- Tab content -->
<div id="Details" class="tabcontent">
<h3>Details</h3>
<form method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="Submit">
</form>
<script>
// Get the element with id="defaultOpen" and click on it
document.getElementById("defaultOpen").click();
</script>
<div id="Details" class="tabcontent">
<h3>
Details
<span style="font-weight: normal; float: right;">{% include 'icons/issue_link.html.j2' with issue=6 %}</span>
</h3>
{{ form.as_p }}
<input name="{{form.prefix}}" type="submit" value="Submit">
<script>
// Get the element with id="defaultOpen" and click on it
document.getElementById("defaultOpen").click();
</script>
</div>
<div id="OperatingSystem" class="tabcontent">
<h3>Operating System</h3>
</div>
<div id="Software" class="tabcontent">
<h3>Software</h3>
<input type="button" value="Add Software Action" onclick="window.location='{% url 'ITAM:_device_software_add' device.id %}';">
<table>
<thead>
<th>Name</th>
<th>Action</th>
<th>Installed</th>
<th>&nbsp;</th>
</thead>
{% for software in softwares %}
<tr>
<td><a href="{% url 'ITAM:_software_view' pk=software.id %}">{{ software.software }}</a></td>
<td><a href="{% url 'ITAM:_device_software_view' device_id=device.id pk=software.id %}">{{ software.get_action_display }}</a></td>
<td>&nbsp;</td>
<th>&nbsp;</th>
</tr>
{% endfor %}
</table>
</div>
<div id="ConfigManagement" class="tabcontent">
<h3>Configuration Management</h3>
<div>
<textarea cols="90" rows="30" readonly>{{ config }}</textarea>
</div>
</div>
<div id="OperatingSystem" class="tabcontent">
<h3>Operating System</h3>
{{ operating_system.as_p }}
<input type="submit" name="{{operating_system.prefix}}" value="Submit" />
</div>
<div id="Software" class="tabcontent">
<h3>Software</h3>
<input type="button" value="Add Software Action" onclick="window.location='{% url 'ITAM:_device_software_add' device.id %}';">
<table>
<thead>
<th>Name</th>
<th>Action</th>
<th>Version</th>
<th>Installed</th>
<th>&nbsp;</th>
</thead>
{% if softwares %}
{% for software in softwares %}
<tr>
<td><a href="{% url 'ITAM:_software_view' pk=software.software_id %}">{{ software.software }}</a></td>
<td><a href="{% url 'ITAM:_device_software_view' device_id=device.id pk=software.id %}">{{ software.get_action_display }}</a></td>
<td>
{% if software.version %}
{{ software.version }}
{% else %}
Any
{% endif %}
</td>
<td>
{% include 'icons/issue_link.html.j2' with issue=2 %}
</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
{% else %}
<td colspan="5">Nothing Found {% include 'icons/issue_link.html.j2' with issue=2 %}</td>
{% endif %}
</table>
</div>
<div id="ConfigManagement" class="tabcontent">
<h3>Configuration Management</h3>
<div>
<textarea cols="90" rows="30" readonly>{{ config }}</textarea>
</div>
</div>
</form>
{% endblock %}

View File

@ -28,4 +28,22 @@
{% 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

@ -0,0 +1,131 @@
{% extends 'base.html.j2' %}
{% block title %}{{ operating_system.name }}{% endblock %}
{% block body %}
<script>
function openCity(evt, cityName) {
var i, tabcontent, tablinks;
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(cityName).style.display = "block";
evt.currentTarget.className += " active";
}
</script>
<div class="tab">
<button onclick="window.location='{% url 'ITAM:Operating Systems' %}';" 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 Operating Systems</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
<button class="tablinks" onclick="openCity(event, 'Versions')">Versions</button>
<button class="tablinks" onclick="openCity(event, 'Licences')">Licences</button>
<button class="tablinks" onclick="openCity(event, 'Installations')">Installations</button>
</div>
<div id="Details" class="tabcontent">
<h3>Details</h3>
<form method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="Submit">
</form>
<script>
document.getElementById("defaultOpen").click();
</script>
</div>
<div id="Versions" class="tabcontent">
<h3>Versions</h3>
<input type="button" value="New Operating System Version" onclick="window.location='{% url 'ITAM:_operating_system_version_add' pk=operating_system.id %}';">
<table>
<thead>
<th>Version</th>
<th>Installations</th>
<th>Vulnerable</th>
<th>&nbsp;</th>
</thead>
{% for version in operating_system_versions %}
<tr>
<td><a href="{% url 'ITAM:_operating_system_version_view' operating_system_id=operating_system.id pk=version.id %}">{{ version.name }}</a></td>
<td>{% if version.installs == 0%}-{% else %}{{ version.installs }}{% endif %}</td>
<td>&nbsp;</td>
<td><a href="{% url 'ITAM:_operating_system_version_delete' operating_system_id=operating_system.id pk=version.id %}">DELETE</a></td>
</tr>
{% endfor %}
</table>
</div>
<div id="Licences" class="tabcontent">
<h3>Licences</h3>
{% include 'icons/issue_link.html.j2' with issue=4 %}
<table>
<thead>
<th>Name</th>
<th>Type</th>
<th>Available</th>
<th>&nbsp;</th>
</thead>
<tr>
<td>GPL-3</td>
<td>Open Source</td>
<td>1 / 5</td>
<th>&nbsp;</th>
</tr>
<tr>
<td>MIT</td>
<td>Open Source</td>
<td>Unlimited</td>
<th>&nbsp;</th>
</tr>
<tr>
<td>Windows Device</td>
<td>CAL</td>
<td>11 / 15</td>
<th>&nbsp;</th>
</tr>
</table>
</div>
<div id="Installations" class="tabcontent">
<h3>Installations</h3>
<table>
<thead>
<th>Device</th>
<th>Organization</th>
<th>Version</th>
<th title="Date Software Installed">Installed</th>
<th>&nbsp;</th>
</thead>
{% for install in installs %}
<tr>
<td><a href="{% url 'ITAM:_device_view' pk=install.device_id %}">{{ install.device }}</a></td>
<td>{{ install.organization }}</td>
<td>{{ install.version }}</td>
<td>{% include 'icons/issue_link.html.j2' with issue=2 %}</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
</table>
</div>
{% endblock %}

View File

@ -0,0 +1,47 @@
{% extends 'base.html.j2' %}
{% block title %}Operating Systems{% endblock %}
{% block content_header_icon %}{% endblock %}
{% block body %}
<input type="button" value="New Operating System" onclick="window.location='{% url 'ITAM:_operating_system_add' %}';">
<table class="data">
<tr>
<th>Name</th>
<th>Created</th>
<th>Modified</th>
<th>Organization</th>
<th>&nbsp;</th>
</tr>
{% for operating_system in operating_systems %}
<tr>
<td><a href="{% url 'ITAM:_operating_system_view' pk=operating_system.id %}">{{ operating_system.name }}</a></td>
<td>{{ operating_system.created }}</td>
<td>{{ operating_system.modified }}</td>
<td>{% if operating_system.is_global %}Global{% else %}{{ operating_system.organization }}{% endif %}</td>
<td><a href="{% url 'ITAM:_operating_system_delete' pk=operating_system.id %}">Delete</a></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

@ -30,8 +30,10 @@
</script>
<div class="tab">
<button onclick="window.location='{% url 'ITAM:Software' %}';">
<< Back to Software</button>
<button onclick="window.location='{% url 'ITAM:Software' %}';" 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 Software</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Versions')">Versions</button>
<button class="tablinks" onclick="openCity(event, 'Licences')">Licences</button>
@ -56,7 +58,7 @@
<div id="Versions" class="tabcontent">
<h3>Versions</h3>
Not Yet Implemented
<input type="button" value="New Software Version" onclick="window.location='{% url 'ITAM:_software_version_add' pk=software.id %}';">
<table>
<thead>
<th>Version</th>
@ -64,17 +66,20 @@
<th>Vulnerable</th>
<th>&nbsp;</th>
</thead>
{% for version in software_versions %}
<tr>
<td>1.0.0</td>
<td>5</td>
<td><input type="checkbox" checked disabled></td>
<td><a href="{% url 'ITAM:_software_version_view' software_id=software.id pk=version.id %}">{{ version.name }}</a></td>
<td>{% include 'icons/issue_link.html.j2' with issue=2 %}</td>
<td>{% include 'icons/issue_link.html.j2' with issue=3 %}</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
</table>
</div>
<div id="Licences" class="tabcontent">
<h3>Licences</h3>
Not Yet Implemented
{% include 'icons/issue_link.html.j2' with issue=4 %}
<table>
<thead>
<th>Name</th>
@ -105,24 +110,45 @@
<div id="Installations" class="tabcontent">
<h3>Installations</h3>
Dev Notes: This table will show joined tables installed software and device software action as a single row.
<table>
<table>
<thead>
<th>Device</th>
<th>Organization</th>
<th title="Not Set/Install/Remove">Action</th>
<th>Version</th>
<th title="Date Software Installed">Installed</th>
<th>&nbsp;</th>
</thead>
{% if device_software %}
{% for device in device_software %}
<tr>
<td><a href="{% url 'ITAM:_device_view' pk=device.device.id %}">{{ device.device }}</a></td>
<td>{{ device.organization }}</td>
<td>{{ device.get_action_display }}</td>
<td>Not Implemented</td>
<td>
{% if device.get_action_display == 'Install' %}
{% include 'icons/success_text.html.j2' with icon_text=device.get_action_display %}
{% elif device.get_action_display == 'Remove'%}
{% include 'icons/cross_text.html.j2' with icon_text=device.get_action_display %}
{% else %}
{{ device.get_action_display }}
{% endif %}
</td>
<td>
{% if device.version %}
{{ device.version }}
{% else %}
Any
{% endif %}
</td>
<td>{% include 'icons/issue_link.html.j2' with issue=2 %}</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="6">Nothing Found</td>
</tr>
{% endif %}
</table>
</div>

View File

@ -26,4 +26,23 @@
{% 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

@ -20,3 +20,19 @@ def test_device_software_action(user):
"""Ensure only software that is from the same organization or is global can be added to the device
"""
pass
@pytest.mark.skip(reason="to be written")
def test_device_not_global(user):
"""Devices are not global items.
Ensure that a device can't be set to be global.
"""
pass
@pytest.mark.skip(reason="to be written")
def test_device_operating_system_version_only_one(user):
"""model deviceoperatingsystem must only contain one value per device
"""
pass

View File

@ -0,0 +1,41 @@
# from django.conf import settings
# from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
import requests
# from django.contrib.auth import get_user_model
# from django.core.exceptions import ValidationError
# from access.models import Organization
# class Test_app_structure_auth(unittest.TestCase):
@pytest.mark.skip(reason="to be written")
def test_operating_system_update_is_global_no_change(user):
"""Once operating_system is set to global it can't be changed.
global status can't be changed as non-global items may reference the item.
"""
pass
@pytest.mark.skip(reason="to be written")
def test_operating_system_prevent_delete_if_used(user):
"""Any operating_system in use by a device must not be deleted.
i.e. A global os can't be deleted
"""
pass
@pytest.mark.skip(reason="to be written")
def test_operating_system_version_installs_by_os_count(user):
"""Operating System Versions has a count field that must be accurate
The count is of model OperatingSystemVersion linked to model DeviceOperatingSystem
"""
pass

View File

@ -20,3 +20,12 @@ def test_software_update_is_global_no_change(user):
"""
pass
@pytest.mark.skip(reason="to be written")
def test_software_prevent_delete_if_used(user):
"""Any software in use by a device must not be deleted.
i.e. A device has an action set for the software.
"""
pass

View File

@ -1,7 +1,7 @@
from django.urls import path
from . import views
from .views import device, device_type, software, software_category
from .views import device, device_type, software, software_category, software_version, operating_system, operating_system_version
app_name = "ITAM"
urlpatterns = [
@ -11,15 +11,30 @@ urlpatterns = [
path("device/<int:pk>/", device.View.as_view(), name="_device_view"),
path("device/<int:pk>/software/add", device.SoftwareAdd.as_view(), name="_device_software_add"),
path("device/<int:device_id>/software/<int:pk>", device.SoftwareView.as_view(), name="_device_software_view"),
path("device/<int:pk>/delete", device.Delete.as_view(), name="_device_delete"),
path("device/add/", device.Add.as_view(), name="_device_add"),
path("device_type/add/", device_type.Add.as_view(), name="_device_type_add"),
path("operating_system", operating_system.IndexView.as_view(), name="Operating Systems"),
path("operating_system/<int:pk>", operating_system.View.as_view(), name="_operating_system_view"),
path("operating_system/add", operating_system.Add.as_view(), name="_operating_system_add"),
path("operating_system/delete/<int:pk>", operating_system.Delete.as_view(), name="_operating_system_delete"),
path("operating_system/<int:operating_system_id>/version/<int:pk>", operating_system_version.View.as_view(), name="_operating_system_version_view"),
path("operating_system/<int:pk>/version/add", operating_system_version.Add.as_view(), name="_operating_system_version_add"),
path("operating_system/<int:operating_system_id>/version/<int:pk>/delete", operating_system_version.Delete.as_view(), name="_operating_system_version_delete"),
path("software/", software.IndexView.as_view(), name="Software"),
path("software/<int:pk>/", software.View.as_view(), name="_software_view"),
path("software/<int:pk>/delete", software.Delete.as_view(), name="_software_delete"),
path("software/<int:pk>/version/add", software_version.Add.as_view(), name="_software_version_add"),
path("software/<int:software_id>/version/<int:pk>", software_version.View.as_view(), name="_software_version_view"),
path("software/add/", software.Add.as_view(), name="_software_add"),
path("software_category/add/", software_category.Add.as_view(), name="_software_category_add"),

View File

@ -7,8 +7,12 @@ from django.views import generic
from access.mixin import OrganizationPermission
from access.models import Organization
from ..models.device import Device, DeviceSoftware
from ..models.device import Device, DeviceSoftware, DeviceOperatingSystem
from itam.forms.device_softwareadd import SoftwareAdd
from itam.forms.device_softwareupdate import SoftwareUpdate
from itam.forms.device.device import DeviceForm
from itam.forms.device.operating_system import Update as OperatingSystemForm
class IndexView(PermissionRequiredMixin, OrganizationPermission, generic.ListView):
@ -17,6 +21,8 @@ class IndexView(PermissionRequiredMixin, OrganizationPermission, generic.ListVie
template_name = 'itam/device_index.html.j2'
context_object_name = "devices"
paginate_by = 10
def get_queryset(self):
if self.request.user.is_superuser:
@ -29,38 +35,80 @@ class IndexView(PermissionRequiredMixin, OrganizationPermission, generic.ListVie
def _get_form(request, formcls, prefix, **kwargs):
data = request.POST if prefix in request.POST else None
return formcls(data, prefix=prefix, **kwargs)
class View(OrganizationPermission, generic.UpdateView):
model = Device
permission_required = [
'itam.view_device'
]
template_name = 'itam/device.html.j2'
fields = [
'id',
'name',
'serial_number',
'uuid',
'device_type',
'is_global'
]
form_class = DeviceForm
context_object_name = "device"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
softwares = DeviceSoftware.objects.filter(device=self.kwargs['pk'])
try:
operating_system_version = DeviceOperatingSystem.objects.get(device=self.kwargs['pk'])
except DeviceOperatingSystem.DoesNotExist:
context['content_title'] = self.object.name
operating_system_version = None
if operating_system_version:
context['operating_system'] = OperatingSystemForm(prefix='operating_system', instance=operating_system_version)
else:
context['operating_system'] = OperatingSystemForm(prefix='operating_system')
softwares = DeviceSoftware.objects.filter(device=self.kwargs['pk'])
context['softwares'] = softwares
config = self.object.get_configuration(self.kwargs['pk'])
context['config'] = json.dumps(config, indent=4, sort_keys=True)
context['content_title'] = self.object.name
return context
def post(self, request, *args, **kwargs):
device = Device.objects.get(pk=self.kwargs['pk'])
try:
existing_os = DeviceOperatingSystem.objects.get(device=self.kwargs['pk'])
except DeviceOperatingSystem.DoesNotExist:
existing_os = None
operating_system = OperatingSystemForm(request.POST, prefix='operating_system', instance=existing_os)
if operating_system.is_bound and operating_system.is_valid():
operating_system.instance.organization = device.organization
operating_system.instance.device = device
operating_system.save()
return super().post(request, *args, **kwargs)
def get_success_url(self, **kwargs):
return f"/itam/device/{self.kwargs['pk']}/"
@ -74,13 +122,12 @@ class SoftwareView(OrganizationPermission, generic.UpdateView):
]
template_name = 'form.html.j2'
fields = [
'action',
]
context_object_name = "devicesoftware"
form_class = SoftwareUpdate
def form_valid(self, form):
device = Device.objects.get(pk=self.kwargs['device_id'])
@ -108,9 +155,12 @@ class Add(PermissionRequiredMixin, OrganizationPermission, generic.CreateView):
'uuid',
'device_type',
'organization',
'is_global'
]
def form_valid(self, form):
form.instance.is_global = False
return super().form_valid(form)
def get_success_url(self, **kwargs):

View File

@ -0,0 +1,112 @@
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import Q, Count
from django.urls import reverse
from django.views import generic
from access.mixin import OrganizationPermission
from itam.models.device import DeviceOperatingSystem
from itam.models.operating_system import OperatingSystem, OperatingSystemVersion
from itam.forms.operating_system.update import Update
class IndexView(PermissionRequiredMixin, OrganizationPermission, generic.ListView):
model = OperatingSystem
permission_required = 'itam.view_operating_system'
template_name = 'itam/operating_system_index.html.j2'
context_object_name = "operating_systems"
paginate_by = 10
def get_queryset(self):
if self.request.user.is_superuser:
return OperatingSystem.objects.filter().order_by('name')
else:
return OperatingSystem.objects.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True)).order_by('name')
class View(OrganizationPermission, generic.UpdateView):
model = OperatingSystem
permission_required = [
'itam.view_operating_system'
]
template_name = 'itam/operating_system.html.j2'
form_class = Update
context_object_name = "operating_system"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
operating_system_versions = OperatingSystemVersion.objects.filter(operating_system=self.kwargs['pk']).order_by('name').annotate(installs=Count("deviceoperatingsystem"))
context['operating_system_versions'] = operating_system_versions
installs = DeviceOperatingSystem.objects.filter(operating_system_version__operating_system_id=self.kwargs['pk'])
context['installs'] = installs
context['content_title'] = self.object.name
return context
def get_success_url(self, **kwargs):
return reverse('ITAM:_operating_system_view', args=(self.kwargs['pk'],))
class Add(PermissionRequiredMixin, OrganizationPermission, generic.CreateView):
model = OperatingSystem
permission_required = [
'access.add_operating_system',
]
template_name = 'form.html.j2'
fields = [
'name',
'organization',
'is_global'
]
def get_success_url(self, **kwargs):
return reverse('ITAM:Operating Systems')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Add Operating System'
return context
class Delete(PermissionRequiredMixin, OrganizationPermission, generic.DeleteView):
model = OperatingSystem
permission_required = [
'access.delete_operating_system',
]
template_name = 'form.html.j2'
def get_success_url(self, **kwargs):
return reverse('ITAM:Operating Systems')
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,88 @@
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.urls import reverse
from django.views import generic
from access.mixin import OrganizationPermission
from ..models.operating_system import OperatingSystem, OperatingSystemVersion
class View(OrganizationPermission, generic.UpdateView):
model = OperatingSystemVersion
permission_required = [
'itam.view_operating_systemversion'
]
template_name = 'form.html.j2'
fields = [
"name",
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = self.object.operating_system.name + ' ' + self.object.name
return context
def get_success_url(self, **kwargs):
return reverse('ITAM:_operating_system_view', args=(self.kwargs['operating_system_id'],))
class Add(PermissionRequiredMixin, OrganizationPermission, generic.CreateView):
model = OperatingSystemVersion
permission_required = [
'access.add_operating_systemversion',
]
template_name = 'form.html.j2'
fields = [
'name'
]
def form_valid(self, form):
operating_system = OperatingSystem.objects.get(pk=self.kwargs['pk'])
form.instance.is_global = operating_system.is_global
form.instance.organization_id = operating_system.organization.id
form.instance.operating_system_id = self.kwargs['pk']
return super().form_valid(form)
def get_success_url(self, **kwargs):
return reverse('ITAM:_operating_system_view', args=(self.kwargs['pk'],))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Add Operating System Version'
return context
class Delete(PermissionRequiredMixin, OrganizationPermission, generic.DeleteView):
model = OperatingSystemVersion
permission_required = [
'access.delete_operating_system',
]
template_name = 'form.html.j2'
def get_success_url(self, **kwargs):
return reverse('ITAM:_operating_system_view', args=(self.kwargs['operating_system_id'],))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Delete ' + self.object.name
return context

View File

@ -5,7 +5,7 @@ from django.views import generic
from access.mixin import OrganizationPermission
from itam.models.device import DeviceSoftware
from itam.models.software import Software
from itam.models.software import Software, SoftwareVersion
from itam.forms.software.update import Update as SoftwareUpdate_Form
class IndexView(PermissionRequiredMixin, OrganizationPermission, generic.ListView):
@ -13,6 +13,7 @@ class IndexView(PermissionRequiredMixin, OrganizationPermission, generic.ListVie
permission_required = 'itam.view_software'
template_name = 'itam/software_index.html.j2'
context_object_name = "softwares"
paginate_by = 10
def get_queryset(self):
@ -42,6 +43,10 @@ class View(OrganizationPermission, generic.UpdateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
software_versions = SoftwareVersion.objects.filter(software=self.kwargs['pk'])
context['software_versions'] = software_versions
context['content_title'] = self.object.name
if self.request.user.is_superuser:

View File

@ -0,0 +1,62 @@
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.views import generic
from access.mixin import OrganizationPermission
from ..models.software import Software, SoftwareVersion
class View(OrganizationPermission, generic.UpdateView):
model = SoftwareVersion
permission_required = [
'itam.view_softwareversion'
]
template_name = 'form.html.j2'
fields = [
"name",
]
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 f"/itam/software/{self.kwargs['software_id']}/"
class Add(PermissionRequiredMixin, OrganizationPermission, generic.CreateView):
model = SoftwareVersion
permission_required = [
'access.add_softwareversion',
]
template_name = 'form.html.j2'
fields = [
'name'
]
def form_valid(self, form):
software = Software.objects.get(pk=self.kwargs['pk'])
form.instance.is_global = software.is_global
form.instance.organization_id = software.organization.id
form.instance.software_id = self.kwargs['pk']
return super().form_valid(form)
def get_success_url(self, **kwargs):
return f"/itam/software/{self.kwargs['pk']}/"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Add Software Version'
return context

View File

@ -41,6 +41,81 @@ span#content_header_icon {
color: #177ee6;
}
span.icon-text {
vertical-align: middle;
border-radius: 25px;
font-size: inherit;
border: 1px solid #ccc;
line-height: 30px;
padding-left: 1px;
padding-right: 10px;
height: 30px;
display: inline-block;
}
span.success {
color: #319c3a;
}
span.icon-success {
fill: #319c3a;
height: 30px;
line-height: 30px;
margin: 0px;
margin-bottom: -7px;
padding: 0px;
vertical-align: middle;
display: inline-block;
}
span.cross {
color: #9c3131;
}
span.icon-cross {
fill: #9c3131;
height: 30px;
line-height: 30px;
margin: 0px;
margin-bottom: -7px;
padding: 0px;
vertical-align: middle;
display: inline-block;
}
span.change {
color: #cab706;
}
span.icon-change {
fill: #cab706;
height: 30px;
line-height: 30px;
margin: 0px;
margin-bottom: -7px;
padding: 0px;
vertical-align: middle;
display: inline-block;
}
/* span.issue {
color: #fc6d26;
} */
span.icon-issue {
fill: #fc6d26;
height: 30px;
line-height: 30px;
margin: 0px;
/* margin-bottom: -2px; */
padding: 0px;
vertical-align: middle;
display: inline-block;
}
/* .icon {
display: block;
content: none;

View File

@ -115,6 +115,14 @@ input[type=submit] {
/* background-color: #f1f1f1; */
width: 100%;
text-align: left;
padding: 0px;
margin: 0px
}
.tablinks {
border: 0px;
margin: none;
padding: none;
}
/* Style the buttons that are used to open the tab content */
@ -125,9 +133,12 @@ input[type=submit] {
border: none;
outline: none;
cursor: pointer;
margin: 0px;
padding: 0px;
padding: 14px 16px;
transition: 0.3s;
font-size: inherit;
color: #6a6e73;
}
/* Change background color of buttons on hover */
@ -146,7 +157,8 @@ input[type=submit] {
.tabcontent {
width: 100%;
display: none;
padding: 6px 12px;
/* padding: 6px 12px; */
padding-bottom: 0px;
border: none;
border-top: none;
}

View File

@ -2,4 +2,13 @@
{% block title %}Home{% endblock %}
{% block body%}{% endblock %}
{% block body%}
To Do List:
<ul>
<li>{% include 'icons/issue_link.html.j2' with issue=5 %} - Item History</li>
</ul>
{% endblock %}

View File

@ -0,0 +1,8 @@
<span class="icon-text change">
<span class="icon-change">
<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px">
<path d="M483-337q-29 0-56.5-10T378-378q-20-20-31-46.28T336-480q0-9.74.8-18.99.8-9.25 3.2-18.01 2-10-1.66-18.82-3.65-8.83-12.59-12.5-9.75-3.68-18.37.51-8.63 4.19-11.63 14.24Q292-521 290-507.63q-2 13.37-2 27.63 0 38 14.71 73.42Q317.42-371.17 344-344q28 28 64.5 42t75.5 14l-27 27q-7 7.36-7 17.18t7 16.82q7 7 16.82 7t17.18-7l59.79-59.79Q562-298 562-312.18T551-337l-59-60q-7.36-7-17.18-7T458-397q-7 7-7 16.82t7 17.18l25 26Zm-7-287q29.7 0 57.35 10.5Q561-603 582-582q20 20 31 46.28T624-480q0 9-.8 18.5T620-443q-2 10 1.5 19t12.59 12q9.91 3 18.89-1.32 8.99-4.33 12.11-14.71Q669-441 670.5-453.5 672-466 672-480q0-38-15-73.5T615-616q-28-28-65-41.5T474-670l29-29q7-7.36 7-17.18T503-733q-7-7-16.82-7T469-733l-59.79 59.79Q398-662 398-647.82T409-623l60 60q7.36 7 17.18 7t16.82-7q7-7 7-16.82T503-597l-27-27Zm4.28 528Q401-96 331-126t-122.5-82.5Q156-261 126-330.96t-30-149.5Q96-560 126-629.5q30-69.5 82.5-122T330.96-834q69.96-30 149.5-30t149.04 30q69.5 30 122 82.5T834-629.28q30 69.73 30 149Q864-401 834-331t-82.5 122.5Q699-156 629.28-126q-69.73 30-149 30Z"/>
</svg>
</span>
{{ icon_text }}
</span>

View File

@ -0,0 +1,8 @@
<span class="icon-text cross">
<span class="icon-cross">
<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px">
<path d="m480-429 116 116q11 11 25.5 10.5T647-314q11-11 11-25.5t-11-25.46L531-480.5l116-115.54q11-10.96 11-25.46T647-647q-11-11-25.5-11T596-647L480-531 364-647q-11-11-25-11t-25 11q-11 11-11 25.5t10.91 25.5L429-480 313-364q-11 11-10.5 25t11.5 25q11 11 25.5 11t25.41-10.91L480-429Zm.28 333Q401-96 331-126t-122.5-82.5Q156-261 126-330.96t-30-149.5Q96-560 126-629.5q30-69.5 82.5-122T330.96-834q69.96-30 149.5-30t149.04 30q69.5 30 122 82.5T834-629.28q30 69.73 30 149Q864-401 834-331t-82.5 122.5Q699-156 629.28-126q-69.73 30-149 30Z" />
</svg>
</span>
{{ icon_text }}
</span>

View File

@ -0,0 +1,6 @@
<span class="icon-text issue">
<span class="icon-issue">
<svg xmlns="http://www.w3.org/2000/svg" height="25px" viewBox="0 -960 960 960" width="25px"><path d="M480-144q-60 0-109-32.5T302-264h-74q-15.3 0-25.65-10.29Q192-284.58 192-299.79t10.35-25.71Q212.7-336 228-336h60v-60h-60q-15.3 0-25.65-10.29Q192-416.58 192-431.79t10.35-25.71Q212.7-468 228-468h60v-60h-60q-15.3 0-25.65-10.29Q192-548.58 192-563.79t10.35-25.71Q212.7-600 228-600h74q8-26 25.8-47.09Q345.6-668.18 369-684l-56-56q-11-11-10.5-25.5T314-791q11-11 25-11t25 11l76 75q19.86-5 40.43-5t40.57 5l75-75q11-11 25.67-11 14.66 0 25.33 11 11 11 11 25.5T647-740l-56 56q23 16 40 37t27 47h74q15.3 0 25.65 10.29Q768-579.42 768-564.21t-10.35 25.71Q747.3-528 732-528h-60v60h60q15.3 0 25.65 10.29Q768-447.42 768-432.21t-10.35 25.71Q747.3-396 732-396h-60v60h60q15.3 0 25.65 10.29Q768-315.42 768-300.21t-10.35 25.71Q747.3-264 732-264h-74q-20 55-69 87.5T480-144Zm0-72q48.67 0 83.34-35Q598-286 600-336v-192q2-50-33.5-85t-86-35q-50.5 0-85 35T360-528v192q-1 50 34 85t86 35Zm-36.09-120h71.83q15.26 0 25.76-10.29 10.5-10.29 10.5-25.5t-10.32-25.71Q531.35-408 516.09-408h-71.83q-15.26 0-25.76 10.29-10.5 10.29-10.5 25.5t10.32 25.71q10.33 10.5 25.59 10.5Zm0-120h71.83q15.26 0 25.76-10.29 10.5-10.29 10.5-25.5t-10.32-25.71Q531.35-528 516.09-528h-71.83q-15.26 0-25.76 10.29-10.5 10.29-10.5 25.5t10.32 25.71q10.33 10.5 25.59 10.5ZM480-430Z"/></svg>
</span>
<a href="https://gitlab.com/nofusscomputing/projects/django_template/-/issues/{{ issue }}" target="_blank"> see #{{ issue }}</a>
</span>

View File

@ -0,0 +1,8 @@
<span class="icon-text success">
<span class="icon-success">
<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px">
<path class="tick" d="m424-408-86-86q-11-11-28-11t-28 11q-11 11-11 28t11 28l114 114q12 12 28 12t28-12l226-226q11-11 11-28t-11-28q-11-11-28-11t-28 11L424-408Zm56 328q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Z"></path>
</svg>
</span>
{{ icon_text }}
</span>

View File

@ -0,0 +1,140 @@
---
title: Django Template Devlopment
description: No Fuss Computings NetBox Django Site Template Development
date: 2024-05-17
template: project.html
about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/django_app
---
This page contains different items related to the development of this application.
## Icons
To locate additional icons for use see [material icons](https://fonts.google.com/icons).
Icons with text:
- Success `{% include 'icons/success_text.html.j2' with icon_text='success' %}` _denotes yes, success etc_
- Cross `{% include 'icons/cross_text.html.j2' with icon_text='cross' %}` _denotes no, negative etc_
- Change `{% include 'icons/change_text.html.j2' with icon_text='change' %}` _denotes that change management needs to run_
- Issue `{% include 'icons/issue_link.html.j2' with issue=2 %}` _Used to provide a link to an issue on GitLab. i.e. incomplete feature ticket_
## Adding an Application
1. Install the django application with `pip <app-name>`
1. Update `app.settings.py`
``` python
INSTALLED_APPS = [
'<app name>.apps.<apps.py Class Name>', # Within project directory
'<app name>', # not in project directory
]
```
1. Update `itsm/urls.py`
``` python
urlpatterns = [
path("<url path>/", include("<app name>.urls")),
]
```
!!! tip
No url from the application will be visible without including the `name` parameter when calling the `path` function within the applications `url.py`. i.e. `urlpatterns[].path(name='<Navigation Name>')`. This is by design and when combined with a prefix of `_` provides the option to limit what URL's are displayed within the navigation menu. A name beginning with an underscore `_` will not be displayed in the menu.
Once you have completed the above list, your application will display collapsed within the navigation menu with the name of your application.
## Tenancy Setup
Within your view class include the mixin class `OrganizationPermission`, ensuring that you set the `permission_required` attribute.
### Model Setup
Any item you wish to be multi-tenant, ensure within your model you include the tenancy model abstract class. The class includes a field called `organization` which links directly to the organization model and is used by the tenancy permission check.
``` python title="<your app name>/models.py"
from access.models import TenancyObject
class YourObject(TenancyObject):
...
```
### View Setup
The mixin inlcuded in this template `OrganizationPermission` is designed to work with all django built in views and is what does the multi-tenancy permission checks.
``` python title="<your app name>/views.py"
from django.db.models import Q
from access.mixins import OrganizationPermission
class IndexView(OrganizationPermission, generic.ListView):
model = YourModel
permission_required = 'access.view_organization'
# Use this for static success url
success_url = f"/organization/" + pk_url_kwarg
# Use this to build dynamic success URL
def get_success_url(self, **kwargs):
return f"/organization/{self.kwargs['pk']}/"
def get_queryset(self):
return MyModel.objects.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True))
```
Using a filter `pk__in=self.user_organizations()` for the queryset using the mixins function `user_organizations`, will limit the query set to only items where the user is a member of the organization.
### Templates
The base template includes blocks that are designed to assist in rendering your content. The following blocks are available:
- `title` - The page and title
- `content_header_icon` - Header icon that is middle aligned with the page title, floating right.
- `body` - The html content of the page
``` html title="template.html.j2"
{% extends 'base.html.j2' %}
{% block title %}{% endblock %}
{% block content_header_icon %}<span title="View History" id="content_header_icon">H</span>{% endblock %}
{% block body %}
your content here
{% endblock %}
```

View File

@ -1,56 +1,20 @@
---
title: Django Template
description: No Fuss Computings NetBox Django Site Template
title: Django ITSM
description: No Fuss Computings NetBox Django ITSM
date: 2024-05-06
template: project.html
about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/django_app
---
This Django Project is designed to be a base template for Django applications. It's intent is to contain only the minimal functionality that is/would be common to all Django applications. for instance: base templates, auth and the functions required to make the site navigable. Currently the template style is that of the Red Hat echo system (AWX, Foreman, EDA, Cockpit etc).
This template has built into it multi-tenancy which can easily added to your django application if using this template.
This Django Project is designed to be a tool that forms part of IT Service Management (ITSM). The goal is to provide a system that is not only an IT Information Library (ITIL), but that of which will connect to other ITSM systems, i.e. AWX for automation orchestration. Currently the template style is that of the Red Hat echo system (AWX, Foreman, EDA, Cockpit etc).
## Features
- [API](api.md)
- [Multi-Tenancy](permissions.md)
- [Multi-Tenant](permissions.md)
- Auto-Generated Navigation Menu
- [IT Asset Management (ITAM)](itam/index.md)
## Adding an Application
1. Install the django application with `pip <app-name>`
1. Update `app.settings.py`
``` python
INSTALLED_APPS = [
'<app name>.apps.<apps.py Class Name>', # Within project directory
'<app name>', # not in project directory
]
```
1. Update `itsm/urls.py`
``` python
urlpatterns = [
path("<url path>/", include("<app name>.urls")),
]
```
!!! tip
No url from the application will be visible without including the `name` parameter when calling the `path` function within the applications `url.py`. i.e. `urlpatterns[].path(name='<Navigation Name>')`. This is by design and when combined with a prefix of `_` provides the option to limit what URL's are displayed within the navigation menu. A name beginning with an underscore `_` will not be displayed in the menu.
Once you have completed the above list, your application will display collapsed within the navigation menu with the name of your application.
- [Configuration ready for ansible](itam/device.md#configuration)

View File

@ -6,4 +6,36 @@ template: project.html
about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/django_app
---
This component within ITAM is intended to display information about a device, be it a computer, router or switch etc.
## Features
For each device within your inventory, the following fields/tabs are available to track items:
- Name
- Operating System
- Software
- Configuration
### Operating System
This tab shows the operating system selected as installed on the device. the version `name` is intended to be full [semver](https://semver.org/).
### Software
This tab displays any action against software. For instance, you can select a piece of software from the inventory and have it set to either `Install` or `Remove` and the ansible config will be updated so that you can pull this config to use within a playbook.
Configuration for this tab is shown as a `list` of `dict` under the configuration key `software` and is setup for ease of use for the `ansible.builtin.apt` module.
### Configuration
Although, configuration is generally part of config management. This tab displays in `JSON` format configuration that is ready for use. The intended audience is Ansible users with the fields provided matching established Ansible modules, if they exist.
This configuration can also be obtained from API endpoint `/api/config/<machine-slug>` where `<machine-slug>` would match the Ansible `inventory_hostname`.

View File

@ -6,4 +6,13 @@ template: project.html
about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/django_app
---
IT Asset Management (ITAM) is a crucial area of IT Service Management (ITSM).
## Features
- [Devices](device.md)
- [Operating Systems](operating_system.md)
- [Software](software.md)

View File

@ -0,0 +1,28 @@
---
title: Operating System
description: No Fuss Computings Django Template ITAM Operating System
date: 2024-05-19
template: project.html
about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/django_app
---
This ITAM component tracks Operating systems within your inventory.
## Features
- Name
- Versions
- Installations
### Versions
This tab displays the Operating system version with the intent of the version name to be the `MAJOR` version number of a [semver](https://semver.org/) operating systems version number. For each version installed, the total installations is also displayed.
## Installations
This tab show all devices with the operating system installed.

View File

@ -34,58 +34,3 @@ The workflow is conducted as part of the view and has the following flow:
1. confirms that the team the permission came from is part of the same organization as the object the action is being conducted on.
1. ONLY on success of the above items, grants access.
## Tenancy Setup
Within your view class include the mixin class `OrganizationPermission`, ensuring that you set the `permission_required` attribute.
### Model Setup
Any item you wish to be multi-tenant, ensure within your model you include the tenancy model abstract class. The class includes a field called `organization` which links directly to the organization model and is used by the tenancy permission check.
``` python title="<your app name>/models.py"
from access.models import TenancyObject
class YourObject(TenancyObject):
...
```
### View Setup
The mixin inlcuded in this template `OrganizationPermission` is designed to work with all django built in views and is what does the multi-tenancy permission checks.
``` python title="<your app name>/views.py"
from django.db.models import Q
from access.mixins import OrganizationPermission
class IndexView(OrganizationPermission, generic.ListView):
model = YourModel
permission_required = 'access.view_organization'
# Use this for static success url
success_url = f"/organization/" + pk_url_kwarg
# Use this to build dynamic success URL
def get_success_url(self, **kwargs):
return f"/organization/{self.kwargs['pk']}/"
def get_queryset(self):
return MyModel.objects.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True))
```
Using a filter `pk__in=self.user_organizations()` for the queryset using the mixins function `user_organizations`, will limit the query set to only items where the user is a member of the organization.

View File

@ -1,34 +0,0 @@
---
title: Template
description: No Fuss Computings Django Template Jinja TEmplate
date: 2024-05-14
template: project.html
about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/django_app
---
### Template
The base template includes blocks that are designed to assist in rendering your content. The following blocks are available:
- `title` - The page and title
- `content_header_icon` - Header icon that is middle aligned with the page title, floating right.
- `body` - The html content of the page
``` html title="template.html.j2"
{% extends 'base.html.j2' %}
{% block title %}{% endblock %}
{% block content_header_icon %}<span title="View History" id="content_header_icon">H</span>{% endblock %}
{% block body %}
your content here
{% endblock %}
```

View File

@ -25,16 +25,20 @@ nav:
- projects/django-template/permissions.md
- projects/django-template/template.md
- ITAM:
- projects/django-template/itam/index.md
- projects/django-template/itam/device.md
- projects/django-template/itam/operating_system.md
- projects/django-template/itam/software.md
- Development:
- projects/django-template/development/index.md
- Operations:
- operations/index.md