Merge branch 'feat-2024-06-02' into 'development'

feat: 2024 06 02

Closes #42 and #45

See merge request nofusscomputing/projects/django_template!17
This commit is contained in:
2024-06-03 06:13:17 +00:00
45 changed files with 3304 additions and 289 deletions

View File

@ -1,6 +1,7 @@
from django.conf import settings
from django.db import models
from django.contrib.auth.models import User, Group, Permission
from django.forms import ValidationError
from .fields import *
@ -52,11 +53,19 @@ class TenancyObject(models.Model):
class Meta:
abstract = True
def validatate_organization_exists(self):
if not self:
raise ValidationError('You must provide an organization')
organization = models.ForeignKey(
Organization,
on_delete=models.CASCADE,
blank = False,
null = True,
validators = [validatate_organization_exists],
)
is_global = models.BooleanField(

View File

@ -1,46 +1,234 @@
from django.test import TestCase, Client
import pytest
import unittest
import requests
from django.test import TestCase, Client
from access.models import Organization
from core.models.history import History
from access.models import Organization
# @pytest.mark.skip(reason="to be written")
# def test_history_auth_view():
# """ User requires Permission view_history """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_create():
# """ History row must be added to history table on create """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_update():
# """ History row must be added to history table on updatej """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_delete():
# """ History row must be added to history table on delete """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_create():
# """ History row must be added to history table on create
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_update():
# """ History row must be added to history table on update
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_delete():
# """ History row must be added to history table on delete
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_create():
# """ History row must be added to history table on create
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_update():
# """ History row must be added to history table on update
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_delete():
# """ History row must be added to history table on delete
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
class OrganizationHistory(TestCase):
@pytest.mark.skip(reason="to be written")
def test_history_organization_create():
""" History row must be added to history table on create """
pass
model = Organization
model_name = 'organization'
@pytest.mark.skip(reason="to be written")
def test_history_organization_update():
""" History row must be added to history table on updatej """
pass
@classmethod
def setUpTestData(self):
""" Setup Test """
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.item_create = self.model.objects.create(
name = 'test_item_' + self.model_name,
)
@pytest.mark.skip(reason="to be written")
def test_history_organization_delete():
""" History row must be added to history table on delete """
pass
self.history_create = History.objects.get(
action = History.Actions.ADD[0],
item_pk = self.item_create.pk,
item_class = self.model._meta.model_name,
)
self.item_change = self.item_create
self.item_change.name = 'test_item_' + self.model_name + '_changed'
self.item_change.save()
self.history_change = History.objects.get(
action = History.Actions.UPDATE[0],
item_pk = self.item_change.pk,
item_class = self.model._meta.model_name,
)
@pytest.mark.skip(reason="to be written")
def test_history_organization_team_create():
""" History row must be added to history table on create """
pass
# field type testing to be done as part of model testing
def test_history_entry_item_add_field_action(self):
""" Ensure action is "add" for item creation """
history = self.history_create.__dict__
assert history['action'] == int(History.Actions.ADD[0])
# assert type(history['action']) is int
@pytest.mark.skip(reason="to be written")
def test_history_organization_team_update():
""" History row must be added to history table on updatej """
pass
@pytest.mark.skip(reason="to be written")
def test_history_entry_item_add_field_after(self):
""" Ensure after field contains correct value """
history = self.history_create.__dict__
assert history['after'] == str('{}')
# assert type(history['after']) is str
def test_history_entry_item_add_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_create.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
def test_history_entry_item_add_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_create.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
def test_history_entry_item_add_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_create.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str
################################## Change ##################################
# field type testing to be done as part of model testing
def test_history_entry_item_change_field_action(self):
""" Ensure action is "add" for item creation """
history = self.history_change.__dict__
assert history['action'] == int(History.Actions.UPDATE[0])
# assert type(history['action']) is int
def test_history_entry_item_change_field_after(self):
""" Ensure after field contains correct value """
history = self.history_change.__dict__
assert history['after'] == str('{"name": "test_item_' + self.model_name + '_changed"}')
# assert type(history['after']) is str
@pytest.mark.skip(reason="to be written")
def test_history_entry_item_change_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_change.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
def test_history_entry_item_change_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_change.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
def test_history_entry_item_change_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_change.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str
@pytest.mark.skip(reason="to be written")
def test_history_organization_team_delete():
""" History row must be added to history table on delete """
pass

View File

@ -1,43 +1,243 @@
from django.test import TestCase, Client
import pytest
import unittest
import requests
from django.test import TestCase, Client
from access.models import Organization
from core.models.history import History
from access.models import Team
@pytest.mark.skip(reason="to be written")
def test_history_team_create():
""" History row must be added to history table on create """
pass
# @pytest.mark.skip(reason="to be written")
# def test_history_auth_view():
# """ User requires Permission view_history """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_team_update():
""" History row must be added to history table on updatej """
pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_create():
# """ History row must be added to history table on create """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_team_delete():
""" History row must be added to history table on delete """
pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_update():
# """ History row must be added to history table on updatej """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_team_users_create():
""" History row must be added to history table on create """
pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_delete():
# """ History row must be added to history table on delete """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_team_users_update():
""" History row must be added to history table on updatej """
pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_create():
# """ History row must be added to history table on create
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_update():
# """ History row must be added to history table on update
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_delete():
# """ History row must be added to history table on delete
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_create():
# """ History row must be added to history table on create
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_update():
# """ History row must be added to history table on update
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_delete():
# """ History row must be added to history table on delete
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
class TeamHistory(TestCase):
model = Team
model_name = 'team'
@classmethod
def setUpTestData(self):
""" Setup Test """
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.item_create = self.model.objects.create(
name = 'test_item_' + self.model_name,
organization = self.organization
)
self.history_create = History.objects.get(
action = History.Actions.ADD[0],
item_pk = self.item_create.pk,
item_class = self.model._meta.model_name,
)
self.item_change = self.item_create
self.item_change.name = 'test_item_' + self.model_name + '_changed'
self.item_change.save()
self.history_change = History.objects.get(
action = History.Actions.UPDATE[0],
item_pk = self.item_change.pk,
item_class = self.model._meta.model_name,
)
@pytest.mark.skip(reason="fails, fixme see #46")
# field type testing to be done as part of model testing
def test_history_entry_item_add_field_action(self):
""" Ensure action is "add" for item creation """
history = self.history_create.__dict__
assert history['action'] == int(History.Actions.ADD[0])
# assert type(history['action']) is int
@pytest.mark.skip(reason="to be written")
def test_history_entry_item_add_field_after(self):
""" Ensure after field contains correct value """
history = self.history_create.__dict__
assert history['after'] == str('{}')
# assert type(history['after']) is str
@pytest.mark.skip(reason="fails, fixme see #46")
def test_history_entry_item_add_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_create.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
@pytest.mark.skip(reason="fails, fixme see #46")
def test_history_entry_item_add_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_create.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
@pytest.mark.skip(reason="fails, fixme see #46")
def test_history_entry_item_add_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_create.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str
################################## Change ##################################
@pytest.mark.skip(reason="fails, fixme see #46")
# field type testing to be done as part of model testing
def test_history_entry_item_change_field_action(self):
""" Ensure action is "add" for item creation """
history = self.history_change.__dict__
assert history['action'] == int(History.Actions.UPDATE[0])
# assert type(history['action']) is int
@pytest.mark.skip(reason="fails, fixme see #46")
def test_history_entry_item_change_field_after(self):
""" Ensure after field contains correct value """
history = self.history_change.__dict__
assert history['after'] == str('{"name": "test_item_' + self.model_name + '_changed"}')
# assert type(history['after']) is str
@pytest.mark.skip(reason="to be written")
def test_history_entry_item_change_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_change.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
@pytest.mark.skip(reason="fails, fixme see #46")
def test_history_entry_item_change_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_change.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
@pytest.mark.skip(reason="fails, fixme see #46")
def test_history_entry_item_change_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_change.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str
@pytest.mark.skip(reason="to be written")
def test_history_team_users_delete():
""" History row must be added to history table on delete """
pass

View File

@ -1,24 +1,243 @@
from django.test import TestCase, Client
import pytest
import unittest
import requests
from django.test import TestCase, Client
@pytest.mark.skip(reason="to be written")
def test_history_team_users_create():
""" History row must be added to history table on create """
pass
from access.models import Organization
from core.models.history import History
from access.models import TeamUsers
@pytest.mark.skip(reason="to be written")
def test_history_team_users_update():
""" History row must be added to history table on updatej """
pass
# @pytest.mark.skip(reason="to be written")
# def test_history_auth_view():
# """ User requires Permission view_history """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_create():
# """ History row must be added to history table on create """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_update():
# """ History row must be added to history table on updatej """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_delete():
# """ History row must be added to history table on delete """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_create():
# """ History row must be added to history table on create
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_update():
# """ History row must be added to history table on update
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_delete():
# """ History row must be added to history table on delete
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_create():
# """ History row must be added to history table on create
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_update():
# """ History row must be added to history table on update
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_delete():
# """ History row must be added to history table on delete
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
@pytest.mark.skip(reason="to do")
class TeamUsersHistory(TestCase):
model = TeamUsers
model_name = 'teamusers'
@classmethod
def setUpTestData(self):
""" Setup Test """
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.item_create = self.model.objects.create(
name = 'test_item_' + self.model_name,
organization = self.organization
)
self.history_create = History.objects.get(
action = History.Actions.ADD[0],
item_pk = self.item_create.pk,
item_class = self.model._meta.model_name,
)
self.item_change = self.item_create
self.item_change.name = 'test_item_' + self.model_name + '_changed'
self.item_change.save()
self.history_change = History.objects.get(
action = History.Actions.UPDATE[0],
item_pk = self.item_change.pk,
item_class = self.model._meta.model_name,
)
@pytest.mark.skip(reason="to do")
# field type testing to be done as part of model testing
def test_history_entry_item_add_field_action(self):
""" Ensure action is "add" for item creation """
history = self.history_create.__dict__
assert history['action'] == int(History.Actions.ADD[0])
# assert type(history['action']) is int
@pytest.mark.skip(reason="to be written")
def test_history_entry_item_add_field_after(self):
""" Ensure after field contains correct value """
history = self.history_create.__dict__
assert history['after'] == str('{}')
# assert type(history['after']) is str
@pytest.mark.skip(reason="to do")
def test_history_entry_item_add_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_create.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
@pytest.mark.skip(reason="to do")
def test_history_entry_item_add_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_create.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
@pytest.mark.skip(reason="to do")
def test_history_entry_item_add_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_create.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str
################################## Change ##################################
@pytest.mark.skip(reason="to do")
# field type testing to be done as part of model testing
def test_history_entry_item_change_field_action(self):
""" Ensure action is "add" for item creation """
history = self.history_change.__dict__
assert history['action'] == int(History.Actions.UPDATE[0])
# assert type(history['action']) is int
@pytest.mark.skip(reason="to do")
def test_history_entry_item_change_field_after(self):
""" Ensure after field contains correct value """
history = self.history_change.__dict__
assert history['after'] == str('{"name": "test_item_' + self.model_name + '_changed"}')
# assert type(history['after']) is str
@pytest.mark.skip(reason="to be written")
def test_history_entry_item_change_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_change.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
@pytest.mark.skip(reason="to do")
def test_history_entry_item_change_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_change.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
@pytest.mark.skip(reason="to do")
def test_history_entry_item_change_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_change.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str
@pytest.mark.skip(reason="to be written")
def test_history_team_users_delete():
""" History row must be added to history table on delete """
pass

View File

@ -27,7 +27,6 @@ class DeviceSerializer(serializers.ModelSerializer):
read_only_fields = [
'inventorydate',
'is_global',
'organization',
'slug',
]

View File

@ -1,4 +1,5 @@
from django.urls import path
from rest_framework.routers import DefaultRouter
from rest_framework.urlpatterns import format_suffix_patterns
@ -21,6 +22,9 @@ router.register('software', software.SoftwareViewSet, basename='software')
urlpatterns = [
path("config/<slug:slug>/", itam_config.View.as_view(), name="_api_device_config"),
path("device/inventory/<slug:slug>", inventory.Collect.as_view(), name="_api_device_inventory"),
path("organization/", access.OrganizationList.as_view(), name='_api_orgs'),
path("organization/<int:pk>/", access.OrganizationDetail.as_view(), name='_api_organization'),
@ -29,11 +33,6 @@ urlpatterns = [
path("organization/<int:organization_id>/team/<int:group_ptr_id>/permissions", access.TeamPermissionDetail.as_view(), name='_api_team_permission'),
path("organization/team/", access.TeamList.as_view(), name='_api_teams'),
path("config/<slug:slug>/", itam_config.View.as_view(), name="_api_device_config"),
path("device/inventory/<slug:slug>", inventory.Collect.as_view(), name="_api_device_inventory"),
]
urlpatterns = format_suffix_patterns(urlpatterns)

View File

@ -1,15 +1,20 @@
# from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
from django.db.models import Q
from django.shortcuts import get_object_or_404
from drf_spectacular.utils import extend_schema
from rest_framework import generics, viewsets
from itam.models.device import Device
from access.mixin import OrganizationMixin
from api.serializers.itam.device import DeviceSerializer
from api.views.mixin import OrganizationPermissionAPI
from itam.models.device import Device
class DeviceViewSet(viewsets.ModelViewSet):
class DeviceViewSet(OrganizationMixin, viewsets.ModelViewSet):
permission_classes = [
OrganizationPermissionAPI
@ -19,5 +24,32 @@ class DeviceViewSet(viewsets.ModelViewSet):
serializer_class = DeviceSerializer
@extend_schema( description='Fetch devices that are from the users assigned organization(s)', methods=["GET"])
def list(self, request):
return super().list(request)
@extend_schema( description='Fetch the selected device', methods=["GET"])
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
def get_queryset(self):
if self.request.user.is_superuser:
return self.queryset.filter().order_by('name')
else:
return self.queryset.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True)).order_by('name')
def get_view_name(self):
return "Device"
if self.detail:
return "Device"
return 'Devices'

View File

@ -1,12 +1,13 @@
# from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
from django.db.models import Q
from django.shortcuts import get_object_or_404
from rest_framework import generics, viewsets
from itam.models.software import Software
from rest_framework import generics, viewsets
from api.serializers.itam.software import SoftwareSerializer
from api.views.mixin import OrganizationPermissionAPI
from itam.models.software import Software
class SoftwareViewSet(viewsets.ModelViewSet):
@ -26,7 +27,14 @@ class SoftwareViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return Software.objects.all()
if self.request.user.is_superuser:
return self.queryset.filter().order_by('name')
else:
return self.queryset.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True)).order_by('name')
def get_view_name(self):

View File

@ -1,4 +1,6 @@
from django.forms import ValidationError
from rest_framework.permissions import DjangoObjectPermissions
from access.mixin import OrganizationMixin
@ -44,6 +46,9 @@ class OrganizationPermissionAPI(DjangoObjectPermissions, OrganizationMixin):
if 'organization' in request.data:
if not request.data['organization']:
raise ValidationError('you must provide an organization')
object_organization = int(request.data['organization'])
elif method == 'patch':

View File

@ -73,6 +73,7 @@ def nav_items(context) -> list(dict()):
ignored_apps = [
'admin',
'djdt', # Debug application
'api',
]
nav_items = []
@ -87,7 +88,7 @@ def nav_items(context) -> list(dict()):
isinstance(nav_group, URLResolver)
):
if nav_group.app_name is not None and nav_group.app_name not in ignored_apps:
if nav_group.app_name is not None and str(nav_group.app_name).lower() not in ignored_apps:
group_name = str(nav_group.app_name)

View File

@ -53,6 +53,9 @@ INSTALLED_APPS = [
'access.apps.AccessConfig',
'itam.apps.ItamConfig',
'settings.apps.SettingsConfig',
'drf_spectacular',
'drf_spectacular_sidecar',
'config_management.apps.ConfigManagementConfig',
]
MIDDLEWARE = [
@ -201,7 +204,19 @@ if API_ENABLED:
# 'rest_framework_json_api.renderers.JSONRenderer',
# ),
# 'TEST_REQUEST_DEFAULT_FORMAT': 'vnd.api+json'
'TEST_REQUEST_DEFAULT_FORMAT': 'json'
'TEST_REQUEST_DEFAULT_FORMAT': 'json',
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
SPECTACULAR_SETTINGS = {
'TITLE': 'Your Project API',
'DESCRIPTION': 'Your project description',
'VERSION': '1.0.0',
'SERVE_INCLUDE_SCHEMA': False,
'SWAGGER_UI_DIST': 'SIDECAR',
'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
'REDOC_DIST': 'SIDECAR',
}
DATETIME_FORMAT = 'j N Y H:i:s'
@ -228,6 +243,5 @@ if DEBUG:
# Apps Under Development
INSTALLED_APPS += [
'information.apps.InformationConfig',
'config_management.apps.ConfigManagementConfig',
'project_management.apps.ProjectManagementConfig',
]

View File

@ -20,6 +20,8 @@ from django.contrib.auth import views as auth_views
from django.views.static import serve
from django.urls import include, path, re_path
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
from .views import home
from core.views import history
@ -38,6 +40,8 @@ urlpatterns = [
path("organization/", include("access.urls")),
path("itam/", include("itam.urls")),
path("config_management/", include("config_management.urls")),
path("history/<str:model_name>/<int:model_pk>", history.View.as_view(), name='_history'),
re_path(r'^static/(?P<path>.*)$', serve,{'document_root': settings.STATIC_ROOT})
]
@ -46,6 +50,8 @@ if settings.API_ENABLED:
urlpatterns += [
path("api/", include("api.urls")),
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/swagger/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
]
if settings.DEBUG:
@ -55,7 +61,6 @@ if settings.DEBUG:
path("__debug__/", include("debug_toolbar.urls"), name='_debug'),
# Apps Under Development
path("information/", include("information.urls")),
path("config_management/", include("config_management.urls")),
path("project_management/", include("project_management.urls")),
]

View File

@ -0,0 +1,20 @@
from django import forms
from itam.models.device import Device
from config_management.models.groups import ConfigGroups, ConfigGroupHosts
class ConfigGroupHostsForm(forms.ModelForm):
__name__ = 'asdsa'
class Meta:
fields = [
'host'
]
model = ConfigGroupHosts
prefix = 'config_group_hosts'

View File

@ -0,0 +1,34 @@
# Generated by Django 5.0.6 on 2024-06-02 14:48
import access.fields
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('access', '0002_alter_team_organization'),
]
operations = [
migrations.CreateModel(
name='ConfigGroups',
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)),
('config', models.JSONField(blank=True, default=None, null=True)),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization')),
('parent', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='config_management.configgroups')),
],
options={
'verbose_name': 'Config Groups',
},
),
]

View File

@ -0,0 +1,43 @@
# Generated by Django 5.0.6 on 2024-06-02 20:51
import access.fields
import config_management.models.groups
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0002_alter_team_organization'),
('config_management', '0001_initial'),
('itam', '0012_alter_device_serial_number_alter_device_uuid'),
]
operations = [
migrations.AlterModelOptions(
name='configgroups',
options={},
),
migrations.AlterField(
model_name='configgroups',
name='config',
field=models.JSONField(blank=True, default=None, null=True, validators=[config_management.models.groups.ConfigGroups.validate_config_keys]),
),
migrations.CreateModel(
name='ConfigGroupHosts',
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)),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='config_management.configgroups')),
('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='itam.device', validators=[config_management.models.groups.ConfigGroupHosts.validate_host_no_parent_group])),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization')),
],
options={
'abstract': False,
},
),
]

View File

@ -1 +0,0 @@
from django.db import models

View File

@ -0,0 +1,154 @@
import json
from django.db import models
from django.forms import ValidationError
from access.fields import *
from access.models import TenancyObject
from core.mixin.history_save import SaveHistory
from itam.models.device import Device
class GroupsCommonFields(TenancyObject, models.Model):
class Meta:
abstract = True
id = models.AutoField(
primary_key=True,
unique=True,
blank=False
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
class ConfigGroups(GroupsCommonFields, SaveHistory):
reserved_config_keys: list = [
'software'
]
def validate_config_keys(self):
value: dict = self
for invalid_key in ConfigGroups.reserved_config_keys:
if invalid_key in value.keys():
raise ValidationError(f'json key "{invalid_key}" is a reserved configuration key')
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
default = None,
null = True,
blank= True
)
name = models.CharField(
blank = False,
max_length = 50,
unique = False,
)
config = models.JSONField(
blank = True,
default = None,
null = True,
validators=[validate_config_keys]
)
def count_children(self) -> int:
""" Count all child groups recursively
Returns:
int: Total count of ALL child-groups
"""
count = 0
children = ConfigGroups.objects.filter(parent=self.pk)
for child in children.all():
count += 1
count += child.count_children()
return count
def render_config(self) -> str:
config: dict = dict()
if self.parent:
config.update(json.loads(ConfigGroups.objects.get(id=self.parent.id).render_config()))
if self.config:
config.update(self.config)
return json.dumps(config)
def save(self, *args, **kwargs):
self.is_global = False
if self.parent:
self.organization = ConfigGroups.objects.get(id=self.parent.id).organization
super().save(*args, **kwargs)
def __str__(self):
return self.name
class ConfigGroupHosts(GroupsCommonFields, SaveHistory):
def validate_host_no_parent_group(self):
""" Ensure that the host is not within any parent group
Raises:
ValidationError: host exists within group chain
"""
if False:
raise ValidationError(f'host {self} is already a member of this chain as it;s a member of group ""')
host = models.ForeignKey(
Device,
on_delete=models.CASCADE,
null = False,
blank= False,
validators = [ validate_host_no_parent_group ]
)
group = models.ForeignKey(
ConfigGroups,
on_delete=models.CASCADE,
null = False,
blank= False
)

View File

@ -0,0 +1,150 @@
{% extends 'base.html.j2' %}
{% 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='{% if group.parent %}{% url 'Config Management:_group_view' pk=group.parent.id %}{% else %}{% url 'Config Management:Groups' %}{% endif %}';"
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 {% if group.parent %}Parent{% else %}Groups{% endif %}</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Children')">Child Groups</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Hosts')">Hosts</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Software')">Software</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Configuration')">Configuration</button>
<button class="tablinks" onclick="openCity(event, 'Notes')">Notes</button>
</div>
<form method="post">
<div id="Details" class="tabcontent">
<h3>Details</h3>
{% csrf_token %}
{{ form }}
{% include 'icons/issue_link.html.j2' with issue=13 %}<br>
<input type="submit" value="Submit">
<script>
document.getElementById("defaultOpen").click();
</script>
</div>
<div id="Children" class="tabcontent">
<h3>Child Groups</h3>
<input type="button" value="Add Child Group" onclick="window.location='{% url 'Config Management:_group_add_child' group.id %}';">
<table class="data">
<tr>
<th>Name</th>
<th>Sub-Groups</th>
<th>&nbsp;</th>
</tr>
{% if child_groups %}
{% for group in child_groups %}
<tr>
<td><a href="{% url 'Config Management:_group_view' pk=group.id %}">{{ group.name }}</a></td>
<td>{{ group.count_children }}</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="4">Nothing Found</td>
</tr>
{% endif %}
</table>
</div>
<div id="Hosts" class="tabcontent">
<h3>
Hosts
</h3>
<input type="button" value="Add Host" onclick="window.location='{% url 'Config Management:_group_add_host' group.id %}';">
<table class="data">
<tr>
<th>Name</th>
<th>Organization</th>
<th>&nbsp;</th>
</tr>
{% if config_group_hosts %}
{% for host in config_group_hosts %}
<tr>
<td><a href="{% url 'ITAM:_device_view' pk=host.host.id %}">{{ host.host }}</a></td>
<td>{{ host.host.organization }}</td>
<td><a href="{% url 'Config Management:_group_delete_host' group_id=group.id pk=host.id %}">Delete</a></td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="3">Nothing Found</td>
</tr>
{% endif %}
</table>
</div>
<div id="Software" class="tabcontent">
<h3>
Software
</h3>
</div>
<div id="Configuration" class="tabcontent">
<h3>Configuration</h3>
<div>
<textarea cols="90" rows="30" readonly>{{ config }}</textarea>
</div>
</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>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,47 @@
{% extends 'base.html.j2' %}
{% block body %}
<input type="button" value="New Group" onclick="window.location='{% url 'Config Management:_group_add' %}';">
<table class="data">
<tr>
<th>Name</th>
<th>Organization</th>
<th>Sub-Groups</th>
<th>&nbsp;</th>
</tr>
{% if groups %}
{% for group in groups %}
<tr>
<td><a href="{% url 'Config Management:_group_view' pk=group.id %}">{{ group.name }}</a></td>
<td>{{ group.organization }}</td>
<td>{{ group.count_children }}</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="4">Nothing Found</td>
</tr>
{% endif %}
</table>
<br>
<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,235 @@
import pytest
import unittest
import requests
from django.test import TestCase, Client
from access.models import Organization
from core.models.history import History
from config_management.models.groups import ConfigGroups
# @pytest.mark.skip(reason="to be written")
# def test_history_auth_view():
# """ User requires Permission view_history """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_create():
# """ History row must be added to history table on create """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_update():
# """ History row must be added to history table on updatej """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_delete():
# """ History row must be added to history table on delete """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_create():
# """ History row must be added to history table on create
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_update():
# """ History row must be added to history table on update
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_delete():
# """ History row must be added to history table on delete
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_create():
# """ History row must be added to history table on create
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_update():
# """ History row must be added to history table on update
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_delete():
# """ History row must be added to history table on delete
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
class ConfigGroupsHistory(TestCase):
model = ConfigGroups
model_name = 'configgroups'
@classmethod
def setUpTestData(self):
""" Setup Test """
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.item_create = self.model.objects.create(
name = 'test_item_' + self.model_name,
organization = self.organization
)
self.history_create = History.objects.get(
action = History.Actions.ADD[0],
item_pk = self.item_create.pk,
item_class = self.model._meta.model_name,
)
self.item_change = self.item_create
self.item_change.name = 'test_item_' + self.model_name + '_changed'
self.item_change.save()
self.history_change = History.objects.get(
action = History.Actions.UPDATE[0],
item_pk = self.item_change.pk,
item_class = self.model._meta.model_name,
)
# field type testing to be done as part of model testing
def test_device_history_entry_item_add_field_action(self):
""" Ensure action is "add" for item creation """
history = self.history_create.__dict__
assert history['action'] == int(History.Actions.ADD[0])
# assert type(history['action']) is int
@pytest.mark.skip(reason="to be written")
def test_device_history_entry_item_add_field_after(self):
""" Ensure after field contains correct value """
history = self.history_create.__dict__
assert history['after'] == str('{}')
# assert type(history['after']) is str
def test_device_history_entry_item_add_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_create.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
def test_device_history_entry_item_add_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_create.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
def test_device_history_entry_item_add_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_create.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str
################################## Change ##################################
# field type testing to be done as part of model testing
def test_device_history_entry_item_change_field_action(self):
""" Ensure action is "add" for item creation """
history = self.history_change.__dict__
assert history['action'] == int(History.Actions.UPDATE[0])
# assert type(history['action']) is int
def test_device_history_entry_item_change_field_after(self):
""" Ensure after field contains correct value """
history = self.history_change.__dict__
assert history['after'] == str('{"name": "test_item_' + self.model_name + '_changed"}')
# assert type(history['after']) is str
@pytest.mark.skip(reason="to be written")
def test_device_history_entry_item_change_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_change.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
def test_device_history_entry_item_change_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_change.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
def test_device_history_entry_item_change_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_change.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str

View File

@ -0,0 +1,511 @@
# from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
import requests
from access.models import Organization, Team, TeamUsers, Permission
from config_management.models.groups import ConfigGroups
class ConfigGroupPermissions(TestCase):
model = ConfigGroups
model_name = 'configgroups'
app_label = 'config_management'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
. create an organization that is different to item
2. Create a device
3. create teams with each permission: view, add, change, delete
4. create a user per team
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
different_organization = Organization.objects.create(name='test_different_organization')
self.item = self.model.objects.create(
organization=organization,
name = 'deviceone'
)
view_permissions = Permission.objects.get(
codename = 'view_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = organization,
)
view_team.permissions.set([view_permissions])
add_permissions = Permission.objects.get(
codename = 'add_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
add_team = Team.objects.create(
team_name = 'add_team',
organization = organization,
)
add_team.permissions.set([add_permissions])
change_permissions = Permission.objects.get(
codename = 'change_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
change_team = Team.objects.create(
team_name = 'change_team',
organization = organization,
)
change_team.permissions.set([change_permissions])
delete_permissions = Permission.objects.get(
codename = 'delete_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
delete_team = Team.objects.create(
team_name = 'delete_team',
organization = organization,
)
delete_team.permissions.set([delete_permissions])
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
self.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
self.add_user = User.objects.create_user(username="test_user_add", password="password")
teamuser = TeamUsers.objects.create(
team = add_team,
user = self.add_user
)
self.change_user = User.objects.create_user(username="test_user_change", password="password")
teamuser = TeamUsers.objects.create(
team = change_team,
user = self.change_user
)
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
teamuser = TeamUsers.objects.create(
team = delete_team,
user = self.delete_user
)
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
different_organization_team = Team.objects.create(
team_name = 'different_organization_team',
organization = different_organization,
)
different_organization_team.permissions.set([
view_permissions,
add_permissions,
change_permissions,
delete_permissions,
])
TeamUsers.objects.create(
team = different_organization_team,
user = self.different_organization_user
)
def test_config_groups_auth_view_user_anon_denied(self):
""" Check correct permission for view
Attempt to view as anon user
"""
client = Client()
url = reverse('Config Management:_group_view', kwargs={'pk': self.item.id})
response = client.get(url)
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_config_groups_auth_view_no_permission_denied(self):
""" Check correct permission for view
Attempt to view with user missing permission
"""
client = Client()
url = reverse('Config Management:_group_view', kwargs={'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.get(url)
assert response.status_code == 403
def test_config_groups_auth_view_different_organizaiton_denied(self):
""" Check correct permission for view
Attempt to view with user from different organization
"""
client = Client()
url = reverse('Config Management:_group_view', kwargs={'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.get(url)
assert response.status_code == 403
def test_config_groups_auth_view_has_permission(self):
""" Check correct permission for view
Attempt to view as user with view permission
"""
client = Client()
url = reverse('Config Management:_group_view', kwargs={'pk': self.item.id})
client.force_login(self.view_user)
response = client.get(url)
assert response.status_code == 200
def test_config_groups_auth_add_user_anon_denied(self):
""" Check correct permission for add
Attempt to add as anon user
"""
client = Client()
url = reverse('Config Management:_group_add')
response = client.put(url, data={'device': 'device'})
assert response.status_code == 302 and response.url.startswith('/account/login')
# @pytest.mark.skip(reason="ToDO: figure out why fails")
def test_config_groups_auth_add_no_permission_denied(self):
""" Check correct permission for add
Attempt to add as user with no permissions
"""
client = Client()
url = reverse('Config Management:_group_add')
client.force_login(self.no_permissions_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
# @pytest.mark.skip(reason="ToDO: figure out why fails")
def test_config_groups_auth_add_different_organization_denied(self):
""" Check correct permission for add
attempt to add as user from different organization
"""
client = Client()
url = reverse('Config Management:_group_add')
client.force_login(self.different_organization_user)
response = client.post(url, data={'name': 'device', 'organization': self.organization.id})
assert response.status_code == 403
def test_config_groups_auth_add_permission_view_denied(self):
""" Check correct permission for add
Attempt to add a user with view permission
"""
client = Client()
url = reverse('Config Management:_group_add')
client.force_login(self.view_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
def test_config_groups_auth_add_has_permission(self):
""" Check correct permission for add
Attempt to add as user with no permission
"""
client = Client()
url = reverse('Config Management:_group_add')
client.force_login(self.add_user)
response = client.post(url, data={'device': 'device', 'organization': self.organization.id})
assert response.status_code == 200
def test_config_groups_auth_change_user_anon_denied(self):
""" Check correct permission for change
Attempt to change as anon
"""
client = Client()
url = reverse('Config Management:_group_view', kwargs={'pk': self.item.id})
response = client.patch(url, data={'device': 'device'})
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_config_groups_auth_change_no_permission_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user without permissions
"""
client = Client()
url = reverse('Config Management:_group_view', kwargs={'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
def test_config_groups_auth_change_different_organization_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user from different organization
"""
client = Client()
url = reverse('Config Management:_group_view', kwargs={'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
def test_config_groups_auth_change_permission_view_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with view permission
"""
client = Client()
url = reverse('Config Management:_group_view', kwargs={'pk': self.item.id})
client.force_login(self.view_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
def test_config_groups_auth_change_permission_add_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with add permission
"""
client = Client()
url = reverse('Config Management:_group_view', kwargs={'pk': self.item.id})
client.force_login(self.add_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
def test_config_groups_auth_change_has_permission(self):
""" Check correct permission for change
Make change with user who has change permission
"""
client = Client()
url = reverse('Config Management:_group_view', kwargs={'pk': self.item.id})
client.force_login(self.change_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 200
def test_config_groups_auth_delete_user_anon_denied(self):
""" Check correct permission for delete
Attempt to delete item as anon user
"""
client = Client()
url = reverse('Config Management:_group_delete', kwargs={'pk': self.item.id})
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_config_groups_auth_delete_no_permission_denied(self):
""" Check correct permission for delete
Attempt to delete as user with no permissons
"""
client = Client()
url = reverse('Config Management:_group_delete', kwargs={'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
def test_config_groups_auth_delete_different_organization_denied(self):
""" Check correct permission for delete
Attempt to delete as user from different organization
"""
client = Client()
url = reverse('Config Management:_group_delete', kwargs={'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
def test_config_groups_auth_delete_permission_view_denied(self):
""" Check correct permission for delete
Attempt to delete as user with veiw permission only
"""
client = Client()
url = reverse('Config Management:_group_delete', kwargs={'pk': self.item.id})
client.force_login(self.view_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
def test_config_groups_auth_delete_permission_add_denied(self):
""" Check correct permission for delete
Attempt to delete as user with add permission only
"""
client = Client()
url = reverse('Config Management:_group_delete', kwargs={'pk': self.item.id})
client.force_login(self.add_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
def test_config_groups_auth_delete_permission_change_denied(self):
""" Check correct permission for delete
Attempt to delete as user with change permission only
"""
client = Client()
url = reverse('Config Management:_group_delete', kwargs={'pk': self.item.id})
client.force_login(self.change_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
def test_config_groups_auth_delete_has_permission(self):
""" Check correct permission for delete
Delete item as user with delete permission
"""
client = Client()
url = reverse('Config Management:_group_delete', kwargs={'pk': self.item.id})
client.force_login(self.delete_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 302 and response.url == reverse('Config Management:Groups')

View File

@ -1,9 +1,17 @@
from django.urls import path
from .views import ConfigIndex
from config_management.views.groups import GroupIndexView, GroupAdd, GroupDelete, GroupView, GroupHostAdd, GroupHostDelete
app_name = "Config Management"
urlpatterns = [
path('', ConfigIndex.as_view(), name='Config Management'),
path('group', GroupIndexView.as_view(), name='Groups'),
path('group/add', GroupAdd.as_view(), name='_group_add'),
path('group/<int:pk>', GroupView.as_view(), name='_group_view'),
path('group/<int:group_id>/child', GroupAdd.as_view(), name='_group_add_child'),
path('group/<int:pk>/delete', GroupDelete.as_view(), name='_group_delete'),
path('group/<int:group_id>/host', GroupHostAdd.as_view(), name='_group_add_host'),
path('group/<int:group_id>/host/<int:pk>/delete', GroupHostDelete.as_view(), name='_group_delete_host'),
]

View File

@ -1,18 +0,0 @@
from django.shortcuts import render
from django.views import generic
class ConfigIndex(generic.View):
permission_required = 'itam.view_device'
template_name = 'form.html.j2'
def get(self, request):
context = {}
context['content_title'] = 'Config Management'
return render(request, self.template_name, context)

View File

@ -0,0 +1,289 @@
import json
from django.contrib.auth import decorators as auth_decorator
from django.db.models import Count, 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.forms.comment import AddNoteForm
from core.models.notes import Notes
from itam.models.device import Device
from settings.models.user_settings import UserSettings
from config_management.forms.group_hosts import ConfigGroupHostsForm
from config_management.models.groups import ConfigGroups, ConfigGroupHosts
class GroupIndexView(OrganizationPermission, generic.ListView):
context_object_name = "groups"
model = ConfigGroups
paginate_by = 10
permission_required = 'config_management.view_configgroups'
template_name = 'config_management/group_index.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Config Groups'
return context
def get_queryset(self):
if self.request.user.is_superuser:
return self.model.objects.filter(parent=None).order_by('name')
else:
return self.model.objects.filter(Q(parent=None, organization__in=self.user_organizations()) | Q(parent=None, is_global = True)).order_by('name')
class GroupAdd(OrganizationPermission, generic.CreateView):
fields = [
'name',
'parent',
'organization',
]
model = ConfigGroups
permission_required = [
'config_management.add_configgroups',
]
template_name = 'form.html.j2'
def get_initial(self):
initial: dict = {
'organization': UserSettings.objects.get(user = self.request.user).default_organization
}
if 'group_id' in self.kwargs:
if self.kwargs['group_id']:
initial.update({'parent': self.kwargs['group_id']})
self.model.parent.field.hidden = True
return initial
def get_success_url(self, **kwargs):
if self.kwargs['group_id']:
return reverse('Config Management:_group_view', args=(self.kwargs['group_id'],))
return reverse('Config Management:Groups')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'New Group'
return context
class GroupView(OrganizationPermission, generic.UpdateView):
context_object_name = "group"
model = ConfigGroups
permission_required = [
'config_management.view_configgroups',
'config_management.change_configgroups',
]
template_name = 'config_management/group.html.j2'
fields = [
'name',
'parent',
'config',
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['child_groups'] = ConfigGroups.objects.filter(parent=self.kwargs['pk'])
context['config'] = json.dumps(json.loads(self.object.render_config()), indent=4, sort_keys=True)
context['config_group_hosts'] = ConfigGroupHosts.objects.filter(group_id = self.kwargs['pk']).order_by('-host')
context['notes_form'] = AddNoteForm(prefix='note')
context['notes'] = Notes.objects.filter(config_group=self.kwargs['pk'])
context['model_pk'] = self.kwargs['pk']
context['model_name'] = self.model._meta.verbose_name.replace(' ', '')
context['model_delete_url'] = reverse('Config Management:_group_delete', args=(self.kwargs['pk'],))
context['content_title'] = self.object.name
# if self.request.user.is_superuser:
# context['device_software'] = DeviceSoftware.objects.filter(
# software=self.kwargs['pk']
# ).order_by(
# 'device',
# 'organization'
# )
# elif not self.request.user.is_superuser:
# context['device_software'] = DeviceSoftware.objects.filter(
# Q(device__in=self.user_organizations(),
# software=self.kwargs['pk'])
# ).order_by(
# 'device',
# 'organization'
# )
return context
@method_decorator(auth_decorator.permission_required("config_management.change_configgroups", raise_exception=True))
def post(self, request, *args, **kwargs):
item = ConfigGroups.objects.get(pk=self.kwargs['pk'])
notes = AddNoteForm(request.POST, prefix='note')
if notes.is_bound and notes.is_valid() and notes.instance.note != '':
notes.instance.organization = item.organization
notes.instance.config_group = item
notes.instance.usercreated = request.user
notes.save()
return super().post(request, *args, **kwargs)
def get_success_url(self, **kwargs):
return reverse('Config Management:_group_view', args=(self.kwargs['pk'],))
class GroupDelete(OrganizationPermission, generic.DeleteView):
model = ConfigGroups
permission_required = [
'config_management.delete_configgroups',
]
template_name = 'form.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Delete ' + self.object.name
return context
def get_success_url(self, **kwargs):
return reverse('Config Management:Groups')
class GroupHostAdd(OrganizationPermission, generic.CreateView):
model = ConfigGroupHosts
permission_required = [
'config_management.add_hosts',
]
template_name = 'form.html.j2'
form_class = ConfigGroupHostsForm
def form_valid(self, form):
form.instance.group_id = self.kwargs['group_id']
return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Add Host to Group'
return context
def get_form(self, form_class=None):
form_class = super().get_form(form_class=None)
group = ConfigGroups.objects.get(pk=self.kwargs['group_id'])
exsting_group_hosts = ConfigGroupHosts.objects.filter(group=group)
form_class.fields["host"].queryset = Device.objects.filter(
organization=group.organization.id,
).exclude(id__in=exsting_group_hosts.values_list('host', flat=True))
return form_class
def get_success_url(self, **kwargs):
return reverse('Config Management:_group_view', args=[self.kwargs['group_id'],])
class GroupHostDelete(OrganizationPermission, generic.DeleteView):
model = ConfigGroupHosts
permission_required = [
'config_management.delete_hosts',
]
template_name = 'form.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Delete ' + self.object.host.name
return context
def get_success_url(self, **kwargs):
return reverse('Config Management:_group_view', args=[self.kwargs['group_id'],])

View File

@ -0,0 +1,20 @@
# Generated by Django 5.0.6 on 2024-06-02 14:48
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config_management', '0001_initial'),
('core', '0006_alter_history_user'),
]
operations = [
migrations.AddField(
model_name='notes',
name='config_group',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='config_management.configgroups'),
),
]

View File

@ -125,6 +125,11 @@ class SaveHistory(models.Model):
item_parent_pk = self.team.pk
item_parent_class = self.team._meta.model_name
if self._meta.model_name == 'configgrouphosts':
item_parent_pk = self.group.id
item_parent_class = self.group._meta.model_name
if not before:

View File

@ -4,6 +4,8 @@ from django.db import models
from access.fields import *
from access.models import TenancyObject
from config_management.models.groups import ConfigGroups
from itam.models.device import Device
from itam.models.software import Software
from itam.models.operating_system import OperatingSystem
@ -70,6 +72,14 @@ class Notes(NotesCommonFields):
blank= True
)
config_group = models.ForeignKey(
ConfigGroups,
on_delete=models.CASCADE,
default = None,
null = True,
blank= True
)
device = models.ForeignKey(
Device,
on_delete=models.CASCADE,

View File

@ -1,3 +1,5 @@
import json
from django.db import models
from access.fields import *
@ -115,6 +117,22 @@ class Device(DeviceCommonFieldsName, SaveHistory):
config['software'] = config['software'] + [ software_action ]
config: dict = config
from config_management.models.groups import ConfigGroupHosts
if self.id:
config_groups = ConfigGroupHosts.objects.filter(host=self.id).order_by('group')
for group in config_groups:
rendered_config = group.group.render_config()
if rendered_config:
config.update(json.loads(group.group.render_config()))
return config

View File

@ -95,7 +95,6 @@
{% elif software.get_action_display == 'Remove'%}
{% include 'icons/cross_text.html.j2' with icon_text=software.get_action_display icon_link=icon_link %}
{% else %}
{% include 'icons/add_link.html.j2' with icon_text='Add' icon_link=icon_link %}
{% endif %}
</td>
@ -152,6 +151,29 @@
<div>
<textarea cols="90" rows="30" readonly>{{ config }}</textarea>
</div>
<br />
<hr />
<table class="data">
<tr>
<th>Group</th>
<th>Added</th>
<th>&nbsp;</th>
</tr>
{% if config_groups %}
{% for group in config_groups %}
<tr>
<td><a href="{% url 'Config Management:_group_view' pk=group.id %}">{{ group.group }}</a></td>
<td>{{ group.created }}</td>
<td><a href="{% url 'Config Management:_group_delete_host' group_id=group.group.id pk=group.id %}">Delete</a></td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="3">Nothing Found</td>
</tr>
{% endif %}
</table>
</div>
</form>
{% endblock %}

View File

@ -1,87 +1,235 @@
from django.test import TestCase, Client
import pytest
import unittest
import requests
from django.test import TestCase, Client
from access.models import Organization
from core.models.history import History
from itam.models.device import Device
@pytest.mark.skip(reason="to be written")
def test_history_auth_view():
""" User requires Permission view_history """
pass
# @pytest.mark.skip(reason="to be written")
# def test_history_auth_view():
# """ User requires Permission view_history """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_device_create():
""" History row must be added to history table on create """
pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_create():
# """ History row must be added to history table on create """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_device_update():
""" History row must be added to history table on updatej """
pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_update():
# """ History row must be added to history table on updatej """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_device_delete():
""" History row must be added to history table on delete """
pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_delete():
# """ History row must be added to history table on delete """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_device_operating_system_create():
""" History row must be added to history table on create
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_create():
# """ History row must be added to history table on create
Must also have populated parent_item_pk and parent_item_class columns
"""
pass
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_device_operating_system_update():
""" History row must be added to history table on update
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_update():
# """ History row must be added to history table on update
Must also have populated parent_item_pk and parent_item_class columns
"""
pass
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_device_operating_system_delete():
""" History row must be added to history table on delete
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_delete():
# """ History row must be added to history table on delete
Must also have populated parent_item_pk and parent_item_class columns
"""
pass
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_device_software_create():
""" History row must be added to history table on create
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_create():
# """ History row must be added to history table on create
Must also have populated parent_item_pk and parent_item_class columns
"""
pass
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_device_software_update():
""" History row must be added to history table on update
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_update():
# """ History row must be added to history table on update
Must also have populated parent_item_pk and parent_item_class columns
"""
pass
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_device_software_delete():
""" History row must be added to history table on delete
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_delete():
# """ History row must be added to history table on delete
Must also have populated parent_item_pk and parent_item_class columns
"""
pass
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
class DeviceHistory(TestCase):
model = Device
model_name = 'device'
@classmethod
def setUpTestData(self):
""" Setup Test """
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.item_create = self.model.objects.create(
name = 'test_item_' + self.model_name,
organization = self.organization
)
self.history_create = History.objects.get(
action = History.Actions.ADD[0],
item_pk = self.item_create.pk,
item_class = self.model._meta.model_name,
)
self.item_change = self.item_create
self.item_change.name = 'test_item_' + self.model_name + '_changed'
self.item_change.save()
self.history_change = History.objects.get(
action = History.Actions.UPDATE[0],
item_pk = self.item_change.pk,
item_class = self.model._meta.model_name,
)
# field type testing to be done as part of model testing
def test_device_history_entry_item_add_field_action(self):
""" Ensure action is "add" for item creation """
history = self.history_create.__dict__
assert history['action'] == int(History.Actions.ADD[0])
# assert type(history['action']) is int
@pytest.mark.skip(reason="to be written")
def test_device_history_entry_item_add_field_after(self):
""" Ensure after field contains correct value """
history = self.history_create.__dict__
assert history['after'] == str('{}')
# assert type(history['after']) is str
def test_device_history_entry_item_add_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_create.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
def test_device_history_entry_item_add_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_create.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
def test_device_history_entry_item_add_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_create.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str
################################## Change ##################################
# field type testing to be done as part of model testing
def test_device_history_entry_item_change_field_action(self):
""" Ensure action is "add" for item creation """
history = self.history_change.__dict__
assert history['action'] == int(History.Actions.UPDATE[0])
# assert type(history['action']) is int
def test_device_history_entry_item_change_field_after(self):
""" Ensure after field contains correct value """
history = self.history_change.__dict__
assert history['after'] == str('{"name": "test_item_' + self.model_name + '_changed"}')
# assert type(history['after']) is str
@pytest.mark.skip(reason="to be written")
def test_device_history_entry_item_change_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_change.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
def test_device_history_entry_item_change_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_change.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
def test_device_history_entry_item_change_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_change.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str

View File

@ -1,87 +1,235 @@
from django.test import TestCase, Client
import pytest
import unittest
import requests
from django.test import TestCase, Client
from access.models import Organization
from core.models.history import History
from itam.models.device_models import DeviceModel
@pytest.mark.skip(reason="to be written")
def test_history_auth_view():
""" User requires Permission view_history """
pass
# @pytest.mark.skip(reason="to be written")
# def test_history_auth_view():
# """ User requires Permission view_history """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_device_model_create():
""" History row must be added to history table on create """
pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_create():
# """ History row must be added to history table on create """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_device_model_update():
""" History row must be added to history table on updatej """
pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_update():
# """ History row must be added to history table on updatej """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_device_model_delete():
""" History row must be added to history table on delete """
pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_delete():
# """ History row must be added to history table on delete """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_device_model_operating_system_create():
""" History row must be added to history table on create
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_create():
# """ History row must be added to history table on create
Must also have populated parent_item_pk and parent_item_class columns
"""
pass
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_device_model_operating_system_update():
""" History row must be added to history table on update
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_update():
# """ History row must be added to history table on update
Must also have populated parent_item_pk and parent_item_class columns
"""
pass
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_device_model_operating_system_delete():
""" History row must be added to history table on delete
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_delete():
# """ History row must be added to history table on delete
Must also have populated parent_item_pk and parent_item_class columns
"""
pass
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_device_model_software_create():
""" History row must be added to history table on create
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_create():
# """ History row must be added to history table on create
Must also have populated parent_item_pk and parent_item_class columns
"""
pass
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_device_model_software_update():
""" History row must be added to history table on update
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_update():
# """ History row must be added to history table on update
Must also have populated parent_item_pk and parent_item_class columns
"""
pass
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_device_model_software_delete():
""" History row must be added to history table on delete
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_delete():
# """ History row must be added to history table on delete
Must also have populated parent_item_pk and parent_item_class columns
"""
pass
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
class DeviceModelHistory(TestCase):
model = DeviceModel
model_name = 'devicemodel'
@classmethod
def setUpTestData(self):
""" Setup Test """
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.item_create = self.model.objects.create(
name = 'test_item_' + self.model_name,
organization = self.organization
)
self.history_create = History.objects.get(
action = History.Actions.ADD[0],
item_pk = self.item_create.pk,
item_class = self.model._meta.model_name,
)
self.item_change = self.item_create
self.item_change.name = 'test_item_' + self.model_name + '_changed'
self.item_change.save()
self.history_change = History.objects.get(
action = History.Actions.UPDATE[0],
item_pk = self.item_change.pk,
item_class = self.model._meta.model_name,
)
# field type testing to be done as part of model testing
def test_history_entry_item_add_field_action(self):
""" Ensure action is "add" for item creation """
history = self.history_create.__dict__
assert history['action'] == int(History.Actions.ADD[0])
# assert type(history['action']) is int
@pytest.mark.skip(reason="to be written")
def test_history_entry_item_add_field_after(self):
""" Ensure after field contains correct value """
history = self.history_create.__dict__
assert history['after'] == str('{}')
# assert type(history['after']) is str
def test_history_entry_item_add_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_create.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
def test_history_entry_item_add_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_create.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
def test_history_entry_item_add_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_create.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str
################################## Change ##################################
# field type testing to be done as part of model testing
def test_history_entry_item_change_field_action(self):
""" Ensure action is "add" for item creation """
history = self.history_change.__dict__
assert history['action'] == int(History.Actions.UPDATE[0])
# assert type(history['action']) is int
def test_history_entry_item_change_field_after(self):
""" Ensure after field contains correct value """
history = self.history_change.__dict__
assert history['after'] == str('{"name": "test_item_' + self.model_name + '_changed"}')
# assert type(history['after']) is str
@pytest.mark.skip(reason="to be written")
def test_history_entry_item_change_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_change.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
def test_history_entry_item_change_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_change.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
def test_history_entry_item_change_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_change.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str

View File

@ -1,52 +1,235 @@
from django.test import TestCase, Client
import pytest
import unittest
import requests
from django.test import TestCase, Client
from access.models import Organization
from core.models.history import History
from itam.models.operating_system import OperatingSystem
@pytest.mark.skip(reason="to be written")
def test_history_operating_system_create():
""" History row must be added to history table on create """
pass
# @pytest.mark.skip(reason="to be written")
# def test_history_auth_view():
# """ User requires Permission view_history """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_operating_system_update():
""" History row must be added to history table on updatej """
pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_create():
# """ History row must be added to history table on create """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_operating_system_delete():
""" History row must be added to history table on delete """
pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_update():
# """ History row must be added to history table on updatej """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_operating_system_version_create():
""" History row must be added to history table on create
# @pytest.mark.skip(reason="to be written")
# def test_history_device_delete():
# """ History row must be added to history table on delete """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_create():
# """ History row must be added to history table on create
Must also have populated parent_item_pk and parent_item_class columns
"""
pass
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_operating_system_version_update():
""" History row must be added to history table on update
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_update():
# """ History row must be added to history table on update
Must also have populated parent_item_pk and parent_item_class columns
"""
pass
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_operating_system_version_delete():
""" History row must be added to history table on delete
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_delete():
# """ History row must be added to history table on delete
Must also have populated parent_item_pk and parent_item_class columns
"""
pass
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_create():
# """ History row must be added to history table on create
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_update():
# """ History row must be added to history table on update
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_delete():
# """ History row must be added to history table on delete
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
class OperatingSystemHistory(TestCase):
model = OperatingSystem
model_name = 'operatingsystem'
@classmethod
def setUpTestData(self):
""" Setup Test """
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.item_create = self.model.objects.create(
name = 'test_item_' + self.model_name,
organization = self.organization
)
self.history_create = History.objects.get(
action = History.Actions.ADD[0],
item_pk = self.item_create.pk,
item_class = self.model._meta.model_name,
)
self.item_change = self.item_create
self.item_change.name = 'test_item_' + self.model_name + '_changed'
self.item_change.save()
self.history_change = History.objects.get(
action = History.Actions.UPDATE[0],
item_pk = self.item_change.pk,
item_class = self.model._meta.model_name,
)
# field type testing to be done as part of model testing
def test_history_entry_item_add_field_action(self):
""" Ensure action is "add" for item creation """
history = self.history_create.__dict__
assert history['action'] == int(History.Actions.ADD[0])
# assert type(history['action']) is int
@pytest.mark.skip(reason="to be written")
def test_history_entry_item_add_field_after(self):
""" Ensure after field contains correct value """
history = self.history_create.__dict__
assert history['after'] == str('{}')
# assert type(history['after']) is str
def test_history_entry_item_add_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_create.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
def test_history_entry_item_add_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_create.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
def test_history_entry_item_add_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_create.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str
################################## Change ##################################
# field type testing to be done as part of model testing
def test_history_entry_item_change_field_action(self):
""" Ensure action is "add" for item creation """
history = self.history_change.__dict__
assert history['action'] == int(History.Actions.UPDATE[0])
# assert type(history['action']) is int
def test_history_entry_item_change_field_after(self):
""" Ensure after field contains correct value """
history = self.history_change.__dict__
assert history['after'] == str('{"name": "test_item_' + self.model_name + '_changed"}')
# assert type(history['after']) is str
@pytest.mark.skip(reason="to be written")
def test_history_entry_item_change_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_change.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
def test_history_entry_item_change_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_change.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
def test_history_entry_item_change_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_change.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str

View File

@ -1,52 +1,235 @@
from django.test import TestCase, Client
import pytest
import unittest
import requests
from django.test import TestCase, Client
from access.models import Organization
from core.models.history import History
from itam.models.software import Software
@pytest.mark.skip(reason="to be written")
def test_history_software_create():
""" History row must be added to history table on create """
pass
# @pytest.mark.skip(reason="to be written")
# def test_history_auth_view():
# """ User requires Permission view_history """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_software_update():
""" History row must be added to history table on updatej """
pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_create():
# """ History row must be added to history table on create """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_software_delete():
""" History row must be added to history table on delete """
pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_update():
# """ History row must be added to history table on updatej """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_software_version_create():
""" History row must be added to history table on create
# @pytest.mark.skip(reason="to be written")
# def test_history_device_delete():
# """ History row must be added to history table on delete """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_create():
# """ History row must be added to history table on create
Must also have populated parent_item_pk and parent_item_class columns
"""
pass
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_software_version_update():
""" History row must be added to history table on update
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_update():
# """ History row must be added to history table on update
Must also have populated parent_item_pk and parent_item_class columns
"""
pass
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
@pytest.mark.skip(reason="to be written")
def test_history_software_version_delete():
""" History row must be added to history table on delete
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_delete():
# """ History row must be added to history table on delete
Must also have populated parent_item_pk and parent_item_class columns
"""
pass
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_create():
# """ History row must be added to history table on create
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_update():
# """ History row must be added to history table on update
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_delete():
# """ History row must be added to history table on delete
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
class SoftwareHistory(TestCase):
model = Software
model_name = 'software'
@classmethod
def setUpTestData(self):
""" Setup Test """
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.item_create = self.model.objects.create(
name = 'test_item_' + self.model_name,
organization = self.organization
)
self.history_create = History.objects.get(
action = History.Actions.ADD[0],
item_pk = self.item_create.pk,
item_class = self.model._meta.model_name,
)
self.item_change = self.item_create
self.item_change.name = 'test_item_' + self.model_name + '_changed'
self.item_change.save()
self.history_change = History.objects.get(
action = History.Actions.UPDATE[0],
item_pk = self.item_change.pk,
item_class = self.model._meta.model_name,
)
# field type testing to be done as part of model testing
def test_history_entry_item_add_field_action(self):
""" Ensure action is "add" for item creation """
history = self.history_create.__dict__
assert history['action'] == int(History.Actions.ADD[0])
# assert type(history['action']) is int
@pytest.mark.skip(reason="to be written")
def test_history_entry_item_add_field_after(self):
""" Ensure after field contains correct value """
history = self.history_create.__dict__
assert history['after'] == str('{}')
# assert type(history['after']) is str
def test_history_entry_item_add_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_create.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
def test_history_entry_item_add_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_create.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
def test_history_entry_item_add_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_create.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str
################################## Change ##################################
# field type testing to be done as part of model testing
def test_history_entry_item_change_field_action(self):
""" Ensure action is "add" for item creation """
history = self.history_change.__dict__
assert history['action'] == int(History.Actions.UPDATE[0])
# assert type(history['action']) is int
def test_history_entry_item_change_field_after(self):
""" Ensure after field contains correct value """
history = self.history_change.__dict__
assert history['after'] == str('{"name": "test_item_' + self.model_name + '_changed"}')
# assert type(history['after']) is str
@pytest.mark.skip(reason="to be written")
def test_history_entry_item_change_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_change.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
def test_history_entry_item_change_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_change.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
def test_history_entry_item_change_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_change.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str

View File

@ -12,6 +12,8 @@ from django.views import generic
from access.mixin import OrganizationPermission
from access.models import Organization
from config_management.models.groups import ConfigGroupHosts
from ..models.device import Device, DeviceSoftware, DeviceOperatingSystem
from ..models.software import Software
@ -101,6 +103,8 @@ class View(OrganizationPermission, generic.UpdateView):
config = self.object.get_configuration(self.kwargs['pk'])
context['config'] = json.dumps(config, indent=4, sort_keys=True)
context['config_groups'] = ConfigGroupHosts.objects.filter(host = self.object.id)
context['model_pk'] = self.kwargs['pk']
context['model_name'] = self.model._meta.verbose_name.replace(' ', '')

View File

@ -19,6 +19,8 @@ curl -X GET http://127.0.0.1:8000/api/ -H 'Authorization: Token <token>'
- Inventory Report Collection
- Swagger UI
## Inventory Reports

View File

@ -0,0 +1,33 @@
---
title: Config Management
description: No Fuss Computings Django ITSM Config Management Module
date: 2024-06-03
template: project.html
about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/django_app
---
Config Management is an ITSM process that deals with the management and storing of device/host configuration. This module aims to bridge the gap between manual entry of config data via JSON/YAML to entry via a UI. For items that are yet to be integrated into the UI, if at all possible, that config is still manually entered as JSON. The rendered configuration is intended to be consumed by Ansible. For all intents and purposes, consider this module to be the equivalent of Ansible's host groups.
## Features
This module contains the following features:
- Config Groups
- Assign host to multiple groups
- **Planned** Assign software action to group _See [issue #43](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/43)_
- History
## Config Groups
Considerable thought was placed into as wide a scope as possible, how the host config groups would function. This includes how the end product (the config) would be rendered. To aid in conveying how the config is rendered, consider the following image, which is a basic tree from a single root at the top, with three branches.
![config-merging](../images/config-groups-merging.png)
A host can be assigned to multiple groups as long as the host is not part of the same branch. This image has had each node coloured to denote different groups of the same branch. Note: the red node is a common node for the three branches. for example a host can be placed in each of the three coloured branches. the root node however, if the host is placed in this group then the host can not be placed in any other node. this is because the red node is the root for all three coloured branches.
When it comes time to merge the configuration, if a parent group has the same config as it's childs config. The childs config will take precedence. For a host that is placed in all three branches (orange, green and blue), based of of the group name, sorted alphanumerically, the last group that has conflicting config will be the one that is used. A groups config will always be rendered with it's parents config included all the way up the branch to the root node.

View File

@ -8,6 +8,7 @@ about: https://gitlab.com/nofusscomputing/infrastructure/configuration-managemen
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).
@ -116,7 +117,7 @@ Using a filter `pk__in=self.user_organizations()` for the queryset using the mix
### Templates
The base template includes blocks that are designed to assist in rendering your content. The following blocks are available:
The base template includes blocks that are designed to assist in rendering your content. The following blocks are available:
- `title` - The page and title

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -0,0 +1,76 @@
<mxfile host="app.diagrams.net" modified="2024-06-03T01:49:35.015Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" etag="jYMoo90cuGXn_K-Y5V_J" version="24.2.1" type="device">
<diagram name="Page-1" id="sbUyBCaYiuDQ_FkowPAe">
<mxGraphModel dx="1434" dy="766" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="3ROtmLI4iqX4xD-AvKj1-1" value="" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#FF3333;" vertex="1" parent="1">
<mxGeometry x="545" y="40" width="80" height="80" as="geometry" />
</mxCell>
<mxCell id="3ROtmLI4iqX4xD-AvKj1-2" value="" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#FF9933;" vertex="1" parent="1">
<mxGeometry x="345" y="214" width="80" height="80" as="geometry" />
</mxCell>
<mxCell id="3ROtmLI4iqX4xD-AvKj1-6" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="3ROtmLI4iqX4xD-AvKj1-3" target="3ROtmLI4iqX4xD-AvKj1-2">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="3ROtmLI4iqX4xD-AvKj1-3" value="" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#FF9933;" vertex="1" parent="1">
<mxGeometry x="345" y="374" width="80" height="80" as="geometry" />
</mxCell>
<mxCell id="3ROtmLI4iqX4xD-AvKj1-7" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="3ROtmLI4iqX4xD-AvKj1-4" target="3ROtmLI4iqX4xD-AvKj1-3">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="3ROtmLI4iqX4xD-AvKj1-4" value="" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#FF9933;" vertex="1" parent="1">
<mxGeometry x="345" y="534" width="80" height="80" as="geometry" />
</mxCell>
<mxCell id="3ROtmLI4iqX4xD-AvKj1-8" value="" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#99FF33;" vertex="1" parent="1">
<mxGeometry x="545" y="214" width="80" height="80" as="geometry" />
</mxCell>
<mxCell id="3ROtmLI4iqX4xD-AvKj1-9" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="3ROtmLI4iqX4xD-AvKj1-10" target="3ROtmLI4iqX4xD-AvKj1-8">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="3ROtmLI4iqX4xD-AvKj1-10" value="" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#99FF33;" vertex="1" parent="1">
<mxGeometry x="545" y="374" width="80" height="80" as="geometry" />
</mxCell>
<mxCell id="3ROtmLI4iqX4xD-AvKj1-11" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="3ROtmLI4iqX4xD-AvKj1-12" target="3ROtmLI4iqX4xD-AvKj1-10">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="3ROtmLI4iqX4xD-AvKj1-12" value="" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#99FF33;" vertex="1" parent="1">
<mxGeometry x="545" y="534" width="80" height="80" as="geometry" />
</mxCell>
<mxCell id="3ROtmLI4iqX4xD-AvKj1-13" value="" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#3399FF;" vertex="1" parent="1">
<mxGeometry x="745" y="214" width="80" height="80" as="geometry" />
</mxCell>
<mxCell id="3ROtmLI4iqX4xD-AvKj1-14" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="3ROtmLI4iqX4xD-AvKj1-15" target="3ROtmLI4iqX4xD-AvKj1-13">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="3ROtmLI4iqX4xD-AvKj1-15" value="" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#3399FF;" vertex="1" parent="1">
<mxGeometry x="745" y="374" width="80" height="80" as="geometry" />
</mxCell>
<mxCell id="3ROtmLI4iqX4xD-AvKj1-16" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="3ROtmLI4iqX4xD-AvKj1-17" target="3ROtmLI4iqX4xD-AvKj1-15">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="3ROtmLI4iqX4xD-AvKj1-17" value="" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#3399FF;" vertex="1" parent="1">
<mxGeometry x="745" y="534" width="80" height="80" as="geometry" />
</mxCell>
<mxCell id="3ROtmLI4iqX4xD-AvKj1-20" value="" style="endArrow=classic;html=1;rounded=0;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="390" y="210" as="sourcePoint" />
<mxPoint x="540" y="110" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="3ROtmLI4iqX4xD-AvKj1-21" value="" style="endArrow=classic;html=1;rounded=0;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="780" y="210" as="sourcePoint" />
<mxPoint x="630" y="110" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="3ROtmLI4iqX4xD-AvKj1-22" value="" style="endArrow=classic;html=1;rounded=0;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="585" y="210" as="sourcePoint" />
<mxPoint x="585" y="130" as="targetPoint" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@ -17,13 +17,13 @@ This application contains the following module:
- [Application wide settings](settings.md)
- [Multi-Tenant](permissions.md)
- [Configuration Management](config_management/index.md)
- History
- [IT Asset Management (ITAM)](itam/index.md)
- [Configuration ready for ansible](itam/device.md#configuration)
- History
- [Multi-Tenant](permissions.md)
Specific features for a module can be found on the module's documentation un the features heading

View File

@ -34,6 +34,7 @@ This tab display the details of the device.
To add a new model navigate to `settings -> ITAM -> Device Models`
### 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/).
@ -49,17 +50,17 @@ This tab shows the operating system selected as installed on the device. the ver
This tab displays both software actions and installed software. Software install details are added/updated by uploading an [inventory report](../api.md#inventory-reports).
You can specify a software action for any piece of software within the ITAM database. You can do this by pressing the `dd software action` button or if the software is installed clicking on the `+ Add` button on the row of the software to add the action to. An action can be set to either `Install` or `Remove` and you can also select a software version from the database if you choose to do so. Software actions are added to config management and can be pulled from the API for use within an Ansible playbook.
You can specify a software action for any piece of software within the ITAM database. You can do this by pressing the `Add Software Action` button or if the software is installed clicking on the `+ Add` button on the row of the software to add the action to. An action can be set to either `Install` or `Remove` and you can also select a software version from the database if you choose to do so. Software actions are added to config management and can be pulled from the API for use within an Ansible playbook.
Display of both installed software and software actions is within a single row, if it's for the same software. Any software that you add an action to, will be displayed at the top of the list of software tab.
!!! info
If you add a software action for software that is already installed using the `add software action` button, an additional row will not be added as the applications logic is smart enough to check if the software is already installed.
If you add a software action for software that is already installed using the `Add Software Action` button, an additional row will not be added as the applications logic is smart enough to check if the software is already installed.
### 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 tab displays in `JSON` format configuration that is ready for use. Config from the [Config Management](../config_management/index.md) module is also included and rendered as part of this config. 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

@ -40,7 +40,7 @@ This tab displays the details of the software, in particular:
If a super admin sets [application setting](../settings.md#global-software) `software is global`, when any software is created, regardless of what organization you set. The software will be created in the "global" organization.
# Versions
## Versions
This tab displays the different software versions and how many of each version are installed on devices within your inventory.

View File

@ -15,10 +15,11 @@ The overall permissions system of django has not been modified with it remaining
A User who is added to an team as a "Manager" can modify the team members or if they have permission `access.change_team` which also allows the changing of team permissions. Modification of an organization can be done by the django administrator (super user) or any user with permission `access._change_organization`.
Items can be set as `Global`, meaning that all users who have the correct permission regardless of organization will be able to take action against the object.
Items can be set as `Global`, meaning that all users who have the correct permission regardless of organization will be able to take action against the object.
Permissions that can be modified for a team have been limited to application permissions only unless adjust the permissions from the django admin site.
## Multi-Tenancy workflow
The workflow is conducted as part of the view and has the following flow:

View File

@ -23,6 +23,8 @@ nav:
- projects/django-template/api.md
- projects/django-template/config_management/index.md
- projects/django-template/permissions.md
- Core:

View File

@ -9,6 +9,13 @@ djangorestframework-jsonapi==7.0.0
pyyaml==6.0.1
django-filter==24.2
# OpenAPI Schema
uritemplate==4.1.1
coreapi==2.3.3
drf-spectacular==0.27.2
drf-spectacular[sidecar]==0.27.2
django_split_settings==1.3.1
markdown==3.6