feat(config_management): add configuration groups

!17 #42
This commit is contained in:
2024-06-03 00:21:15 +09:30
parent 8061b7c8e2
commit fdeae217fa
13 changed files with 519 additions and 7 deletions

View File

@ -54,7 +54,8 @@ INSTALLED_APPS = [
'itam.apps.ItamConfig', 'itam.apps.ItamConfig',
'settings.apps.SettingsConfig', 'settings.apps.SettingsConfig',
'drf_spectacular', 'drf_spectacular',
'drf_spectacular_sidecar' 'drf_spectacular_sidecar',
'config_management.apps.ConfigManagementConfig',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -242,6 +243,5 @@ if DEBUG:
# Apps Under Development # Apps Under Development
INSTALLED_APPS += [ INSTALLED_APPS += [
'information.apps.InformationConfig', 'information.apps.InformationConfig',
'config_management.apps.ConfigManagementConfig',
'project_management.apps.ProjectManagementConfig', 'project_management.apps.ProjectManagementConfig',
] ]

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

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

View File

@ -0,0 +1,88 @@
from django.db import models
from access.fields import *
from access.models import TenancyObject
from core.mixin.history_save import SaveHistory
class GroupsCommonFields(TenancyObject, models.Model):
class Meta:
abstract = True
id = models.AutoField(
primary_key=True,
unique=True,
blank=False
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
def __str__(self):
return self.name
class ConfigGroups(GroupsCommonFields, SaveHistory):
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,
)
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 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)

View File

@ -0,0 +1,107 @@
{% 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:_group_index' %}{% 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, 'Software')">Software</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>
<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="Software" class="tabcontent">
<h3>
Software
</h3>
</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,9 @@
{% extends 'base.html.j2' %}
{% block body %}
what will go here?
<ul>
<li><a href="{% url 'Config Management:_group_index' %}">Groups</a></li>
</ul>
{% endblock %}

View File

@ -1,9 +1,15 @@
from django.urls import path from django.urls import path
from .views import ConfigIndex from config_management.views.index import ConfigIndex
from config_management.views.groups import GroupIndexView, GroupAdd, GroupDelete, GroupView
app_name = "Config Management" app_name = "Config Management"
urlpatterns = [ urlpatterns = [
path('', ConfigIndex.as_view(), name='Config Management'), path('', ConfigIndex.as_view(), name='Config Management'),
path('group', GroupIndexView.as_view(), name='_group_index'),
path('group/add', GroupAdd.as_view(), name='_group_add'),
path('group/<int:pk>', GroupView.as_view(), name='_group_view'),
path('group/<int:pk>/delete', GroupDelete.as_view(), name='_group_delete'),
] ]

View File

@ -0,0 +1,192 @@
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 settings.models.user_settings import UserSettings
from config_management.models.groups import ConfigGroups
class GroupIndexView(OrganizationPermission, generic.ListView):
context_object_name = "groups"
model = ConfigGroups
paginate_by = 10
permission_required = 'config_management.view_groups'
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_groups',
]
template_name = 'form.html.j2'
def get_initial(self):
return {
'organization': UserSettings.objects.get(user = self.request.user).default_organization
}
def get_success_url(self, **kwargs):
return reverse('Config Management:_group_index')
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_groups',
]
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['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("itam.change_software", 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_groups',
]
template_name = 'form.html.j2'
def get_success_url(self, **kwargs):
return reverse('Config Management:_group_index')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Delete ' + self.object.name
return context

View File

@ -6,7 +6,7 @@ class ConfigIndex(generic.View):
permission_required = 'itam.view_device' permission_required = 'itam.view_device'
template_name = 'form.html.j2' template_name = 'config_management/index.html.j2'
def get(self, request): def get(self, request):

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

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

View File

@ -14,7 +14,7 @@ uritemplate==4.1.1
coreapi==2.3.3 coreapi==2.3.3
drf-spectacular==0.27.2 drf-spectacular==0.27.2
drf-spectacular-sidecar==2024.6.1 drf-spectacular[sidecar]==0.27.2
django_split_settings==1.3.1 django_split_settings==1.3.1