Merge pull request #255 from nofusscomputing/development

This commit is contained in:
Jon
2024-08-23 17:56:17 +09:30
committed by GitHub
193 changed files with 10746 additions and 1015 deletions

39
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,39 @@
### :books: Summary
<!-- your summary here emojis ref: https://github.com/yodamad/gitlab-emoji -->
### :link: Links / References
<!--
using a list as any links to other references or links as required. if relevant, describe the link/reference
Include any issues or related merge requests. Note: dependent MR's also to be added to "Merge request dependencies"
-->
### :construction_worker: Tasks
- [ ] Add your tasks here if required (delete)
<!-- dont remove tasks below strike through including the checkbox by enclosing in double tidle '~~' -->
- [ ] :firecracker: Contains breaking-change Any Breaking change(s)?
_Breaking Change must also be notated in the commit that introduces it and in [Conventional Commit Format](https://www.conventionalcommits.org/en/v1.0.0/)._
- [ ] :notebook: Release notes updated
- [ ] :blue_book: Documentation written
_All features to be documented within the correct section(s). Administration, Development and/or User_
- [ ] :checkered_flag: Milestone assigned
- [ ] :test_tube: [Unit Test(s) Written](https://nofusscomputing.com/projects/centurion_erp/development/testing/)
_ensure test coverage delta is not less than zero_
- [ ] :page_facing_up: Roadmap updated

View File

@ -35,3 +35,5 @@
- [ ] [Unit Test(s) Written](https://nofusscomputing.com/projects/centurion_erp/development/testing/)
_ensure test coverage delta is not less than zero_
- [ ] :page_facing_up: Roadmap updated

View File

@ -30,6 +30,9 @@ python3 manage.py createsuperuser
# If model changes
python3 manage.py makemigrations --noinput
# To update code highlight run
pygmentize -S default -f html -a .codehilite > project-static/code.css
```
Updates to python modules will need to be captured with SCM. This can be done by running `pip freeze > requirements.txt` from the running virtual environment.

View File

@ -1,13 +1,13 @@
from django import forms
from django.contrib.auth.models import Permission
from django.db.models import Q
from django.forms import inlineformset_factory
from app import settings
from .team_users import TeamUsersForm, TeamUsers
from access.models import Team
from access.functions import permissions
from app import settings
from core.forms.common import CommonModelForm
@ -66,37 +66,4 @@ class TeamForm(CommonModelForm):
self.fields['permissions'].widget.attrs = {'style': "height: 200px;"}
apps = [
'access',
'config_management',
'core',
'django_celery_results',
'itam',
'settings',
]
exclude_models = [
'appsettings',
'chordcounter',
'groupresult',
'organization'
'settings',
'usersettings',
]
exclude_permissions = [
'add_organization',
'add_taskresult',
'change_organization',
'change_taskresult',
'delete_organization',
'delete_taskresult',
]
self.fields['permissions'].queryset = Permission.objects.filter(
content_type__app_label__in=apps,
).exclude(
content_type__model__in=exclude_models
).exclude(
codename__in = exclude_permissions
)
self.fields['permissions'].queryset = permissions.permission_queryset()

View File

@ -0,0 +1,45 @@
from django.contrib.auth.models import Permission
def permission_queryset():
"""Filter Permissions to those used within the application
Returns:
list: Filtered queryset that only contains the used permissions
"""
apps = [
'access',
'assistance',
'config_management',
'core',
'django_celery_results',
'itam',
'itim',
'settings',
]
exclude_models = [
'appsettings',
'chordcounter',
'groupresult',
'organization'
'settings',
'usersettings',
]
exclude_permissions = [
'add_organization',
'add_taskresult',
'change_organization',
'change_taskresult',
'delete_organization',
'delete_taskresult',
]
return Permission.objects.filter(
content_type__app_label__in=apps,
).exclude(
content_type__model__in=exclude_models
).exclude(
codename__in = exclude_permissions
)

View File

@ -15,9 +15,6 @@ class Organization(SaveHistory):
verbose_name_plural = "Organizations"
ordering = ['name']
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if self.slug == '_':
@ -62,6 +59,9 @@ class Organization(SaveHistory):
def get_organization(self):
return self
def __str__(self):
return self.name
class TenancyManager(models.Manager):
@ -196,9 +196,6 @@ class Team(Group, TenancyObject):
verbose_name_plural = "Teams"
ordering = ['team_name']
def __str__(self):
return self.name
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
@ -241,6 +238,10 @@ class Team(Group, TenancyObject):
return [permission_list, self.permissions.all()]
def __str__(self):
return self.team_name
class TeamUsers(SaveHistory):
@ -318,3 +319,6 @@ class TeamUsers(SaveHistory):
return self.team
def __str__(self):
return self.user.username

View File

@ -5,6 +5,9 @@ from rest_framework.urlpatterns import format_suffix_patterns
from .views import access, config, index
from api.views.settings import permissions
from api.views.settings import index as settings
from .views.itam import software, config as itam_config
from .views.itam.device import DeviceViewSet
from .views.itam import inventory
@ -36,6 +39,9 @@ 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("settings", settings.View.as_view(), name='_settings'),
path("settings/permissions", permissions.View.as_view(), name='_settings_permissions'),
]
urlpatterns = format_suffix_patterns(urlpatterns)

View File

@ -1,15 +1,20 @@
# from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
from django.contrib.auth.models import User
from django.utils.safestring import mark_safe
from rest_framework import generics, permissions, routers, viewsets
from rest_framework.decorators import api_view
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.reverse import reverse
class Index(viewsets.ViewSet):
# permission_required = 'access.view_organization'
permission_classes = [
IsAuthenticated,
]
def get_view_name(self):
return "API Index"
@ -29,6 +34,7 @@ class Index(viewsets.ViewSet):
"devices": reverse("API:device-list", request=request),
"config_groups": reverse("API:_api_config_groups", request=request),
"organizations": reverse("API:_api_orgs", request=request),
"settings": reverse('API:_settings', request=request),
"software": reverse("API:software-list", request=request),
}
)

View File

@ -0,0 +1,47 @@
from django.contrib.auth.models import Permission
from drf_spectacular.utils import extend_schema, OpenApiResponse
from rest_framework import views
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.reverse import reverse
from core.http.common import Http
class View(views.APIView):
permission_classes = [
IsAuthenticated,
]
@extend_schema(
summary = "Settings Index Page",
description = """This endpoint provides the available settings as available via the API.
""",
methods=["GET"],
parameters = None,
tags = ['settings',],
responses = {
200: OpenApiResponse(description='Inventory upload successful'),
401: OpenApiResponse(description='User Not logged in'),
500: OpenApiResponse(description='Exception occured. View server logs for the Stack Trace'),
}
)
def get(self, request, *args, **kwargs):
status = Http.Status.OK
response_data: dict = {
"permissions": reverse('API:_settings_permissions', request=request)
}
return Response(data=response_data,status=status)
def get_view_name(self):
return "Settings"

View File

@ -0,0 +1,67 @@
from drf_spectacular.utils import extend_schema, OpenApiResponse
from rest_framework import views
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from access.functions import permissions
from core.http.common import Http
class View(views.APIView):
permission_classes = [
IsAuthenticated,
]
@extend_schema(
summary = "Fetch available permissions",
description = """This endpoint provides a list of permissions that are available within
Centurion ERP. The format of each permission is `<app name>.<permission>_<model>`.
This endpoint is available to **all** authenticated users.
""",
methods=["GET"],
parameters = None,
tags = ['settings',],
responses = {
200: OpenApiResponse(description='Inventory upload successful'),
401: OpenApiResponse(description='User Not logged in'),
500: OpenApiResponse(description='Exception occured. View server logs for the Stack Trace'),
}
)
def get(self, request, *args, **kwargs):
status = Http.Status.OK
response_data: list = []
try:
for permission in permissions.permission_queryset():
response_data += [ str(f"{permission.content_type.app_label}.{permission.codename}") ]
except PermissionDenied as e:
status = Http.Status.FORBIDDEN
response_data = ''
except Exception as e:
print(f'An error occured{e}')
status = Http.Status.SERVER_ERROR
response_data = 'Unknown Server Error occured'
return Response(data=response_data,status=status)
def get_view_name(self):
return "Permissions"

View File

@ -113,6 +113,8 @@ INSTALLED_APPS = [
'core.apps.CoreConfig',
'access.apps.AccessConfig',
'itam.apps.ItamConfig',
'itim.apps.ItimConfig',
'assistance.apps.AssistanceConfig',
'settings.apps.SettingsConfig',
'drf_spectacular',
'drf_spectacular_sidecar',
@ -357,7 +359,6 @@ if DEBUG:
# Apps Under Development
INSTALLED_APPS += [
'information.apps.InformationConfig',
'project_management.apps.ProjectManagementConfig',
]

View File

@ -1,6 +1,8 @@
import importlib
import pytest
import unittest
from access.models import TenancyObject
from access.tests.abstract.tenancy_object import TenancyObject as TenancyObjectTestCases
@ -40,6 +42,40 @@ class TenancyModel(
""" Model to test """
def test_field_exists_verbose_name_plural(self):
"""Test for existance of field in `<model>.Meta`
Field is required for `templates/detail.html.js`
Attribute `verbose_name_plural` must be defined in `Meta` class.
"""
assert 'verbose_name_plural' in self.model._meta.original_attrs
def test_field_not_empty_verbose_name_plural(self):
"""Test field `<model>.Meta` is not empty
Field is required for `templates/detail.html.js`
Attribute `verbose_name_plural` must be defined in `Meta` class.
"""
assert self.model._meta.original_attrs['verbose_name_plural'] is not None
def test_field_type_verbose_name_plural(self):
"""Test field `<model>.Meta` is not empty
Field is required for `templates/detail.html.js`
Attribute `verbose_name_plural` must be of type str.
"""
assert type(self.model._meta.original_attrs['verbose_name_plural']) is str
class ModelAdd(
AddView

View File

@ -563,3 +563,33 @@ class AllViews(
index_view: str = None
""" Index Class name to test """
@pytest.mark.skip(reason='write test')
def test_view_index_attribute_missing_permission_required(self):
""" Attribute missing Test
Ensure that `permission_required` attribute is not defined within the view.
this can be done by mocking the inherited class with the `permission_required` attribute
set to a value that if it changed would be considered defined in the created view.
## Why?
This attribute can be dynamically added based of of the view name along with attributes
`model._meta.model_name` and `str(__class__.__name__).lower()`.
Additional test:
- ensure that the attribute does get automagically created.
- ensure that the classes name is one of add, change, delete, display or index.
"""
@pytest.mark.skip(reason='write test')
def test_view_index_attribute_missing_template_name(self):
""" Attribute missing Test
Ensure that `template_name` attribute is not defined within the view if the value
is `form.html.j2`
this valuse is already defined in the base form
"""

View File

@ -42,7 +42,9 @@ urlpatterns = [
path("account/", include("django.contrib.auth.urls")),
path("organization/", include("access.urls")),
path("assistance/", include("assistance.urls")),
path("itam/", include("itam.urls")),
path("itim/", include("itim.urls")),
path("config_management/", include("config_management.urls")),
path("history/<str:model_name>/<int:model_pk>", history.View.as_view(), name='_history'),
@ -72,9 +74,6 @@ if settings.DEBUG:
urlpatterns += [
path("__debug__/", include("debug_toolbar.urls"), name='_debug'),
# Apps Under Development
path("itim/", include("itim.urls")),
path("information/", include("information.urls")),
path("project_management/", include("project_management.urls")),
]

View File

@ -1,6 +1,6 @@
from django.apps import AppConfig
class InformationConfig(AppConfig):
class AssistanceConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'information'
name = 'assistance'

View File

@ -0,0 +1,147 @@
from django import forms
from django.urls import reverse
from django.forms import ValidationError
from app import settings
from assistance.models.knowledge_base import KnowledgeBase
from core.forms.common import CommonModelForm
class KnowledgeBaseForm(CommonModelForm):
__name__ = 'asdsa'
class Meta:
fields = '__all__'
model = KnowledgeBase
prefix = 'knowledgebase'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['expiry_date'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local', 'format': "%Y-%m-%dT%H:%M"})
self.fields['expiry_date'].input_formats = settings.DATETIME_FORMAT
self.fields['expiry_date'].format="%Y-%m-%dT%H:%M"
self.fields['release_date'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local', 'format': "%Y-%m-%dT%H:%M"})
self.fields['release_date'].input_formats = settings.DATETIME_FORMAT
self.fields['release_date'].format="%Y-%m-%dT%H:%M"
def clean(self):
cleaned_data = super().clean()
responsible_user = cleaned_data.get("responsible_user")
responsible_teams = cleaned_data.get("responsible_teams")
if not responsible_user and not responsible_teams:
raise ValidationError('A Responsible User or Team must be assigned.')
target_team = cleaned_data.get("target_team")
target_user = cleaned_data.get("target_user")
if not target_team and not target_user:
raise ValidationError('A Target Team or Target User must be assigned.')
if target_team and target_user:
raise ValidationError('Both a Target Team or Target User Cant be assigned at the same time. Use one or the other')
return cleaned_data
class DetailForm(KnowledgeBaseForm):
tabs: dict = {
"details": {
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'title',
'category',
'responsible_user',
'organization',
'is_global',
'c_created',
'c_modified',
],
"right": [
'release_date',
'expiry_date',
'target_user',
'target_team',
]
},
{
"layout": "single",
"name": "Summary",
"fields": [
'summary',
],
"markdown": [
'summary',
]
},
{
"layout": "single",
"name": "Content",
"fields": [
'content',
],
"markdown": [
'content',
]
}
]
},
"notes": {
"name": "Notes",
"slug": "notes",
"sections": []
}
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['c_created'] = forms.DateTimeField(
label = 'Created',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.created,
)
self.fields['c_modified'] = forms.DateTimeField(
label = 'Modified',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.modified,
)
self.tabs['details'].update({
"edit_url": reverse('Assistance:_knowledge_base_change', args=(self.instance.pk,))
})
self.url_index_view = reverse('Assistance:Knowledge Base')

View File

@ -0,0 +1,36 @@
from django.forms import ValidationError
from assistance.models.knowledge_base import KnowledgeBaseCategory
from core.forms.common import CommonModelForm
class KnowledgeBaseCategoryForm(CommonModelForm):
__name__ = 'asdsa'
class Meta:
fields = '__all__'
model = KnowledgeBaseCategory
prefix = 'knowledgebase_category'
def clean(self):
cleaned_data = super().clean()
target_team = cleaned_data.get("target_team")
target_user = cleaned_data.get("target_user")
if target_team and target_user:
raise ValidationError('Both a Target Team or Target User Cant be assigned at the same time. Use one or the other or None')
return cleaned_data

View File

@ -0,0 +1,68 @@
# Generated by Django 5.0.7 on 2024-07-20 14:37
import access.fields
import access.models
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('access', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='KnowledgeBaseCategory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_global', models.BooleanField(default=False)),
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
('name', models.CharField(help_text='Name/Title of the Category', max_length=50, verbose_name='Title')),
('slug', access.fields.AutoSlugField()),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
('parent_category', models.ForeignKey(blank=True, default=None, help_text='Category this category belongs to', null=True, on_delete=django.db.models.deletion.SET_NULL, to='assistance.knowledgebasecategory', verbose_name='Parent Category')),
('target_team', models.ManyToManyField(blank=True, default=None, help_text='Team(s) to grant access to the article', to='access.team', verbose_name='Target Team(s)')),
('target_user', models.ForeignKey(blank=True, default=None, help_text='User(s) to grant access to the article', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Target Users(s)')),
],
options={
'verbose_name': 'Category',
'verbose_name_plural': 'Categorys',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='KnowledgeBase',
fields=[
('is_global', models.BooleanField(default=False)),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('title', models.CharField(help_text='Title of the article', max_length=50, verbose_name='Title')),
('summary', models.TextField(blank=True, default=None, help_text='Short Summary of the article', null=True, verbose_name='Summary')),
('content', models.TextField(blank=True, default=None, help_text='Content of the article. Markdown is supported', null=True, verbose_name='Article Content')),
('release_date', models.DateTimeField(blank=True, default=None, help_text='Date the article will be published', null=True, verbose_name='Publish Date')),
('expiry_date', models.DateTimeField(blank=True, default=None, help_text='Date the article will be removed from published articles', null=True, verbose_name='End Date')),
('public', models.BooleanField(default=False, help_text='Is this article to be made available publically', verbose_name='Public Article')),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
('responsible_teams', models.ManyToManyField(blank=True, default=None, help_text='Team(s) whom is considered the articles owner.', related_name='responsible_teams', to='access.team', verbose_name='Responsible Team(s)')),
('responsible_user', models.ForeignKey(default=None, help_text='User(s) whom is considered the articles owner.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='responsible_user', to=settings.AUTH_USER_MODEL, verbose_name='Responsible User')),
('target_team', models.ManyToManyField(blank=True, default=None, help_text='Team(s) to grant access to the article', to='access.team', verbose_name='Target Team(s)')),
('target_user', models.ForeignKey(blank=True, default=None, help_text='User(s) to grant access to the article', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Target Users(s)')),
('category', models.ForeignKey(default=None, help_text='Article Category', max_length=50, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assistance.knowledgebasecategory', verbose_name='Category')),
],
options={
'verbose_name': 'Article',
'verbose_name_plural': 'Articles',
'ordering': ['title'],
},
),
]

View File

View File

@ -0,0 +1,219 @@
from django.contrib.auth.models import User
from django.db import models
from django.forms import ValidationError
from access.fields import *
from access.models import Team, TenancyObject
class KnowledgeBaseCategory(TenancyObject):
class Meta:
ordering = [
'name',
]
verbose_name = "Category"
verbose_name_plural = "Categorys"
parent_category = models.ForeignKey(
'self',
blank = True,
default = None,
help_text = 'Category this category belongs to',
null = True,
on_delete = models.SET_NULL,
verbose_name = 'Parent Category',
)
name = models.CharField(
blank = False,
help_text = 'Name/Title of the Category',
max_length = 50,
unique = False,
verbose_name = 'Title',
)
slug = AutoSlugField()
target_team = models.ManyToManyField(
Team,
blank = True,
default = None,
help_text = 'Team(s) to grant access to the article',
verbose_name = 'Target Team(s)',
)
target_user = models.ForeignKey(
User,
blank = True,
default = None,
help_text = 'User(s) to grant access to the article',
null = True,
on_delete = models.SET_NULL,
verbose_name = 'Target Users(s)',
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
def __str__(self):
return self.name
class KnowledgeBase(TenancyObject):
class Meta:
ordering = [
'title',
]
verbose_name = "Article"
verbose_name_plural = "Articles"
model_notes = None
id = models.AutoField(
primary_key=True,
unique=True,
blank=False
)
title = models.CharField(
blank = False,
help_text = 'Title of the article',
max_length = 50,
unique = False,
verbose_name = 'Title',
)
summary = models.TextField(
blank = True,
default = None,
help_text = 'Short Summary of the article',
null = True,
verbose_name = 'Summary',
)
content = models.TextField(
blank = True,
default = None,
help_text = 'Content of the article. Markdown is supported',
null = True,
verbose_name = 'Article Content',
)
category = models.ForeignKey(
KnowledgeBaseCategory,
blank = False,
default = None,
help_text = 'Article Category',
max_length = 50,
null = True,
on_delete = models.SET_NULL,
unique = False,
verbose_name = 'Category',
)
release_date = models.DateTimeField(
blank = True,
default = None,
help_text = 'Date the article will be published',
null = True,
verbose_name = 'Publish Date',
)
expiry_date = models.DateTimeField(
blank = True,
default = None,
help_text = 'Date the article will be removed from published articles',
null = True,
verbose_name = 'End Date',
)
target_team = models.ManyToManyField(
Team,
blank = True,
default = None,
help_text = 'Team(s) to grant access to the article',
verbose_name = 'Target Team(s)',
)
target_user = models.ForeignKey(
User,
blank = True,
default = None,
help_text = 'User(s) to grant access to the article',
null = True,
on_delete = models.SET_NULL,
verbose_name = 'Target Users(s)',
)
responsible_user = models.ForeignKey(
User,
blank = False,
default = None,
help_text = 'User(s) whom is considered the articles owner.',
null = True,
on_delete = models.SET_NULL,
related_name = 'responsible_user',
verbose_name = 'Responsible User',
)
responsible_teams = models.ManyToManyField(
Team,
blank = True,
default = None,
help_text = 'Team(s) whom is considered the articles owner.',
related_name = 'responsible_teams',
verbose_name = 'Responsible Team(s)',
)
public = models.BooleanField(
blank = False,
default = False,
help_text = 'Is this article to be made available publically',
verbose_name = 'Public Article',
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
def __str__(self):
return self.title

View File

@ -0,0 +1,40 @@
{% extends 'detail.html.j2' %}
{% load json %}
{% load markdown %}
{% block tabs %}
<form action="" method="post">
{% csrf_token %}
<div id="details" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.details %}
</div>
{% if perms.assistance.change_knowledgebase %}
<div id="notes" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.notes %}
{{ 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>
{% endif %}
</form>
{% endblock %}

View File

@ -0,0 +1,213 @@
{% extends 'base.html.j2' %}
{% load markdown %}
{% block content %}
<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>
<style>
.detail-view-field {
display:unset;
height: 30px;
line-height: 30px;
padding: 0px 20px 40px 20px;
}
.detail-view-field label {
display: inline-block;
font-weight: bold;
width: 200px;
margin: 10px;
/*padding: 10px;*/
height: 30px;
line-height: 30px;
}
.detail-view-field span {
display: inline-block;
width: 340px;
margin: 10px;
/*padding: 10px;*/
border-bottom: 1px solid #ccc;
height: 30px;
line-height: 30px;
}
pre {
word-wrap: break-word;
white-space: pre-wrap;
}
</style>
<div class="tab">
<button
onclick="window.location='{% url 'Settings:KB Categories' %}';"
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 Articles</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
<button class="tablinks" onclick="openCity(event, 'Articles')">Articles</button>
{% if perms.assistance.change_knowledgebase %}
<button class="tablinks" onclick="openCity(event, 'Notes')">Notes</button>
{% endif %}
</div>
<form method="post">
<div id="Details" class="tabcontent">
<h3>Details</h3>
{% csrf_token %}
<div style="align-items:flex-start; align-content: center; display: flexbox; width: 100%">
<div style="display: inline; width: 40%; margin: 30px;">
<div class="detail-view-field">
<label>{{ form.name.label }}</label>
<span>{{ form.name.value }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.parent_category.label }}</label>
<span>
{% if item.parent_category %}
{{ item.parent_category }}
{% else %}
&nbsp;
{% endif %}
</span>
</div>
<div class="detail-view-field">
<label>Created</label>
<span>{{ item.created }}</span>
</div>
<div class="detail-view-field">
<label>Modified</label>
<span>{{ item.modified }}</span>
</div>
</div>
<div style="display: inline; width: 40%; margin: 30px; text-align: left;">
<div class="detail-view-field">
<label>{{ form.organization.label }}</label>
<span>
{% if form.organization.value %}
{{ item.organization }}
{% else %}
&nbsp;
{% endif %}
</span>
</div>
<div class="detail-view-field">
<label>{{ form.target_user.label }}</label>
<span>
{% if form.target_user.value %}
{{ form.target_user.value }}
{% else %}
&nbsp;
{% endif %}
</span>
</div>
<div class="detail-view-field">
<label>{{ form.target_team.label }}</label>
<span>
{% if form.target_team.value %}
{{ form.target_team.value }}
{% else %}
&nbsp;
{% endif %}
</span>
</div>
</div>
</div>
<input type="button" value="Edit" onclick="window.location='{% url 'Settings:_knowledge_base_category_change' item.id %}';">
<br>
<script>
document.getElementById("defaultOpen").click();
</script>
</div>
<div id="Articles" class="tabcontent">
<h3>
Articles
</h3>
<table>
<tr>
<th>Title</th>
<th>Organization</th>
</tr>
{% for article in articles %}
<tr>
<td><a href="{% url 'Assistance:_knowledge_base_view' article.id %}">{{ article.title }}</a></td>
<td>{{ article.organization }}</td>
</tr>
{% endfor %}
</table>
</div>
{% if perms.assistance.change_knowledgebase %}
<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>
{% endif %}
</form>
{% endblock %}

View File

@ -0,0 +1,47 @@
{% extends 'base.html.j2' %}
{% block content %}
<input type="button" value="New Article" onclick="window.location='{% url 'Settings:_knowledge_base_category_add' %}';">
<table class="data">
<tr>
<th>Title</th>
<th>Parent</th>
<th>Organization</th>
<th>&nbsp;</th>
</tr>
{% if items %}
{% for item in items %}
<tr>
<td><a href="{% url 'Settings:_knowledge_base_category_view' pk=item.id %}">{{ item.name }}</a></td>
<td>{{ item.parent_category }}</td>
<td>{{ item.organization }}</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,47 @@
{% extends 'base.html.j2' %}
{% block content %}
<input type="button" value="New Article" onclick="window.location='{% url 'Assistance:_knowledge_base_add' %}';">
<table class="data">
<tr>
<th>Title</th>
<th>Category</th>
<th>Organization</th>
<th>&nbsp;</th>
</tr>
{% if items %}
{% for item in items %}
<tr>
<td><a href="{% url 'Assistance:_knowledge_base_view' pk=item.id %}">{{ item.title }}</a></td>
<td><a href="{% url 'Settings:_knowledge_base_category_view' pk=item.category.id %}">{{ item.category }}</a></td>
<td>{{ item.organization }}</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,44 @@
import pytest
import unittest
from django.test import TestCase
from access.models import Organization
from app.tests.abstract.models import TenancyModel
from assistance.models.knowledge_base import KnowledgeBase
@pytest.mark.django_db
class KnowledgeBaseModel(
TestCase,
TenancyModel
):
model = KnowledgeBase
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
2. Create an item
"""
self.organization = Organization.objects.create(name='test_org')
self.item = self.model.objects.create(
organization = self.organization,
title = 'one',
content = 'dict({"key": "one", "existing": "dont_over_write"})'
)
self.second_item = self.model.objects.create(
organization = self.organization,
title = 'one_two',
content = 'dict({"key": "two"})',
)

View File

@ -0,0 +1,78 @@
import pytest
import unittest
import requests
from django.test import TestCase, Client
from access.models import Organization
from core.models.history import History
from core.tests.abstract.history_entry import HistoryEntry
from core.tests.abstract.history_entry_parent_model import HistoryEntryParentItem
from assistance.models.knowledge_base import KnowledgeBase
class KnowledgeBaseHistory(TestCase, HistoryEntry, HistoryEntryParentItem):
model = KnowledgeBase
@classmethod
def setUpTestData(self):
""" Setup Test """
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.item_parent = self.model.objects.create(
title = 'test_item_parent_' + self.model._meta.model_name,
organization = self.organization
)
self.item_create = self.model.objects.create(
title = 'test_item_' + self.model._meta.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.title = 'test_item_' + self.model._meta.model_name + '_changed'
self.item_change.save()
self.field_after_expected_value = '{"title": "' + self.item_change.title + '"}'
self.history_change = History.objects.get(
action = History.Actions.UPDATE[0],
item_pk = self.item_change.pk,
item_class = self.model._meta.model_name,
)
self.item_delete = self.model.objects.create(
title = 'test_item_delete_' + self.model._meta.model_name,
organization = self.organization,
)
self.deleted_pk = self.item_delete.pk
self.item_delete.delete()
self.history_delete = History.objects.filter(
item_pk = self.deleted_pk,
item_class = self.model._meta.model_name,
)
self.history_delete_children = History.objects.filter(
item_parent_pk = self.deleted_pk,
item_parent_class = self.item_parent._meta.model_name,
)

View File

@ -0,0 +1,95 @@
# 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 assistance.models.knowledge_base import KnowledgeBase
from core.tests.abstract.history_permissions import HistoryPermissions
class KnowledgeBaseHistoryPermissions(TestCase, HistoryPermissions):
item_model = KnowledgeBase
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
2. create an organization that is different to item
3. Create a device
4. Add history device history entry as item
5. create a user
6. create user in different organization (with the required permission)
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
different_organization = Organization.objects.create(name='test_different_organization')
self.item = self.item_model.objects.create(
organization=organization,
title = 'deviceone'
)
self.history = self.model.objects.get(
item_pk = self.item.id,
item_class = self.item._meta.model_name,
action = self.model.Actions.ADD,
)
view_permissions = Permission.objects.get(
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = organization,
)
view_team.permissions.set([view_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.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,
])
TeamUsers.objects.create(
team = different_organization_team,
user = self.different_organization_user
)

View File

@ -0,0 +1,189 @@
# 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 app.tests.abstract.model_permissions import ModelPermissions
from assistance.models.knowledge_base import KnowledgeBase
class KnowledgeBasePermissions(TestCase, ModelPermissions):
model = KnowledgeBase
app_namespace = 'Assistance'
url_name_view = '_knowledge_base_view'
url_name_add = '_knowledge_base_add'
url_name_change = '_knowledge_base_change'
url_name_delete = '_knowledge_base_delete'
url_delete_response = reverse('Assistance:Knowledge Base')
@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,
title = 'deviceone'
)
self.url_view_kwargs = {'pk': self.item.id}
# self.url_add_kwargs = {'pk': self.item.id}
self.add_data = {'device': 'device', 'organization': self.organization.id}
self.url_change_kwargs = {'pk': self.item.id}
self.change_data = {'device': 'device', 'organization': self.organization.id}
self.url_delete_kwargs = {'pk': self.item.id}
self.delete_data = {'device': 'device', 'organization': self.organization.id}
view_permissions = Permission.objects.get(
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.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._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.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._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.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._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.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
)

View File

@ -0,0 +1,29 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import PrimaryModel
class KnowledgeBaseViews(
TestCase,
PrimaryModel
):
add_module = 'assistance.views.knowledge_base'
add_view = 'Add'
change_module = add_module
change_view = 'Change'
delete_module = add_module
delete_view = 'Delete'
display_module = add_module
display_view = 'View'
index_module = add_module
index_view = 'Index'

View File

@ -0,0 +1,42 @@
import pytest
import unittest
from django.test import TestCase
from access.models import Organization
from app.tests.abstract.models import TenancyModel
from assistance.models.knowledge_base import KnowledgeBaseCategory
@pytest.mark.django_db
class KnowledgeBaseModel(
TestCase,
TenancyModel
):
model = KnowledgeBaseCategory
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
2. Create an item
"""
self.organization = Organization.objects.create(name='test_org')
self.item = self.model.objects.create(
organization = self.organization,
name = 'one',
)
self.second_item = self.model.objects.create(
organization = self.organization,
name = 'one_two',
)

View File

@ -0,0 +1,75 @@
import pytest
import unittest
import requests
from django.test import TestCase, Client
from access.models import Organization
from core.models.history import History
from core.tests.abstract.history_entry import HistoryEntry
from core.tests.abstract.history_entry_parent_model import HistoryEntryParentItem
from assistance.models.knowledge_base import KnowledgeBaseCategory
class KnowledgeBaseHistory(TestCase, HistoryEntry, HistoryEntryParentItem):
model = KnowledgeBaseCategory
@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._meta.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._meta.model_name + '_changed'
self.item_change.save()
self.field_after_expected_value = '{"name": "' + self.item_change.name + '"}'
self.history_change = History.objects.get(
action = History.Actions.UPDATE[0],
item_pk = self.item_change.pk,
item_class = self.model._meta.model_name,
)
self.item_delete = self.model.objects.create(
name = 'test_item_delete_' + self.model._meta.model_name,
organization = self.organization,
)
self.deleted_pk = self.item_delete.pk
self.item_delete.delete()
self.history_delete = History.objects.filter(
item_pk = self.deleted_pk,
item_class = self.model._meta.model_name,
)
def test_history_entry_children_delete(self):
""" Model has no child items """
pass

View File

@ -0,0 +1,95 @@
# 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 assistance.models.knowledge_base import KnowledgeBaseCategory
from core.tests.abstract.history_permissions import HistoryPermissions
class KnowledgeBaseHistoryPermissions(TestCase, HistoryPermissions):
item_model = KnowledgeBaseCategory
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
2. create an organization that is different to item
3. Create a device
4. Add history device history entry as item
5. create a user
6. create user in different organization (with the required permission)
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
different_organization = Organization.objects.create(name='test_different_organization')
self.item = self.item_model.objects.create(
organization=organization,
name = 'deviceone'
)
self.history = self.model.objects.get(
item_pk = self.item.id,
item_class = self.item._meta.model_name,
action = self.model.Actions.ADD,
)
view_permissions = Permission.objects.get(
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = organization,
)
view_team.permissions.set([view_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.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,
])
TeamUsers.objects.create(
team = different_organization_team,
user = self.different_organization_user
)

View File

@ -0,0 +1,189 @@
# 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 app.tests.abstract.model_permissions import ModelPermissions
from assistance.models.knowledge_base import KnowledgeBaseCategory
class KnowledgeBasePermissions(TestCase, ModelPermissions):
model = KnowledgeBaseCategory
app_namespace = 'Settings'
url_name_view = '_knowledge_base_category_view'
url_name_add = '_knowledge_base_category_add'
url_name_change = '_knowledge_base_category_change'
url_name_delete = '_knowledge_base_category_delete'
url_delete_response = reverse('Settings:KB Categories')
@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'
)
self.url_view_kwargs = {'pk': self.item.id}
# self.url_add_kwargs = {'pk': self.item.id}
self.add_data = {'device': 'device', 'organization': self.organization.id}
self.url_change_kwargs = {'pk': self.item.id}
self.change_data = {'device': 'device', 'organization': self.organization.id}
self.url_delete_kwargs = {'pk': self.item.id}
self.delete_data = {'device': 'device', 'organization': self.organization.id}
view_permissions = Permission.objects.get(
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.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._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.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._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.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._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.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
)

View File

@ -0,0 +1,29 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import PrimaryModel
class ConfigManagementViews(
TestCase,
PrimaryModel
):
add_module = 'assistance.views.knowledge_base_category'
add_view = 'Add'
change_module = add_module
change_view = 'Change'
delete_module = add_module
delete_view = 'Delete'
display_module = add_module
display_view = 'View'
index_module = add_module
index_view = 'Index'

15
app/assistance/urls.py Normal file
View File

@ -0,0 +1,15 @@
from django.urls import path
from assistance.views import knowledge_base
app_name = "Assistance"
urlpatterns = [
path("information", knowledge_base.Index.as_view(), name="Knowledge Base"),
path("information/add", knowledge_base.Add.as_view(), name="_knowledge_base_add"),
path("information/<int:pk>/edit", knowledge_base.Change.as_view(), name="_knowledge_base_change"),
path("information/<int:pk>/delete", knowledge_base.Delete.as_view(), name="_knowledge_base_delete"),
path("information/<int:pk>", knowledge_base.View.as_view(), name="_knowledge_base_view"),
]

View File

View File

@ -0,0 +1,215 @@
from datetime import datetime
from django.contrib.auth import decorators as auth_decorator
from django.db.models import Q
from django.urls import reverse
from django.utils.decorators import method_decorator
from access.models import TeamUsers
from assistance.forms.knowledge_base import DetailForm, KnowledgeBaseForm
from assistance.models.knowledge_base import KnowledgeBase
from core.forms.comment import AddNoteForm
from core.models.notes import Notes
from core.views.common import AddView, ChangeView, DeleteView, DisplayView, IndexView
from settings.models.user_settings import UserSettings
class Index(IndexView):
context_object_name = "items"
model = KnowledgeBase
paginate_by = 10
permission_required = [
'assistance.view_knowledgebase'
]
template_name = 'assistance/kb_index.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if not self.request.user.has_perm('assistance.change_knowledgebase') and not self.request.user.is_superuser:
user_teams = []
for team_user in TeamUsers.objects.filter(user=self.request.user):
if team_user.team.id not in user_teams:
user_teams += [ team_user.team.id ]
context['items'] = self.get_queryset().filter(
Q(expiry_date__lte=datetime.now())
|
Q(expiry_date=None)
).filter(
Q(target_team__in=user_teams)
|
Q(target_user=self.request.user.id)
).distinct()
context['model_docs_path'] = self.model._meta.app_label + '/knowledge_base/'
context['content_title'] = 'Knowledge Base Articles'
return context
class Add(AddView):
form_class = KnowledgeBaseForm
model = KnowledgeBase
permission_required = [
'assistance.add_knowledgebase',
]
def get_initial(self):
initial: dict = {
'organization': UserSettings.objects.get(user = self.request.user).default_organization
}
if 'pk' in self.kwargs:
if self.kwargs['pk']:
initial.update({'parent': self.kwargs['pk']})
self.model.parent.field.hidden = True
return initial
def get_success_url(self, **kwargs):
return reverse('Assistance:Knowledge Base')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'New Group'
return context
class Change(ChangeView):
context_object_name = "group"
form_class = KnowledgeBaseForm
model = KnowledgeBase
permission_required = [
'assistance.change_knowledgebase',
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = self.object.title
return context
def get_success_url(self, **kwargs):
return reverse('Assistance:_knowledge_base_view', args=(self.kwargs['pk'],))
class View(ChangeView):
context_object_name = "kb"
form_class = DetailForm
model = KnowledgeBase
permission_required = [
'assistance.view_knowledgebase',
]
template_name = 'assistance/kb_article.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
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.model_name
context['model_delete_url'] = reverse('Assistance:_knowledge_base_delete', args=(self.kwargs['pk'],))
context['content_title'] = self.object.title
return context
# @method_decorator(auth_decorator.permission_required("assistance.change_knowledgebase", raise_exception=True))
def post(self, request, *args, **kwargs):
item = KnowledgeBase.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.save()
# dont allow saving any post data outside notes.
# todo: figure out what needs to be returned
# return super().post(request, *args, **kwargs)
def get_success_url(self, **kwargs):
return reverse('Assistance:_knowledge_base_view', args=(self.kwargs['pk'],))
class Delete(DeleteView):
model = KnowledgeBase
permission_required = [
'assistance.delete_knowledgebase',
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Delete ' + self.object.title
return context
def get_success_url(self, **kwargs):
return reverse('Assistance:Knowledge Base')

View File

@ -0,0 +1,191 @@
from django.contrib.auth import decorators as auth_decorator
from django.urls import reverse
from django.utils.decorators import method_decorator
from assistance.forms.knowledge_base_category import KnowledgeBaseCategoryForm
from assistance.models.knowledge_base import KnowledgeBase, KnowledgeBaseCategory
from core.forms.comment import AddNoteForm
from core.models.notes import Notes
from core.views.common import AddView, ChangeView, DeleteView, DisplayView, IndexView
from settings.models.user_settings import UserSettings
class Index(IndexView):
context_object_name = "items"
model = KnowledgeBaseCategory
paginate_by = 10
permission_required = [
'assistance.view_knowledgebasecategory'
]
template_name = 'assistance/kb_category_index.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['model_docs_path'] = self.model._meta.app_label + '/knowledge_base/'
context['content_title'] = 'Knowledge Base Categories'
return context
class Add(AddView):
form_class = KnowledgeBaseCategoryForm
model = KnowledgeBaseCategory
permission_required = [
'assistance.add_knowledgebasecategory',
]
def get_initial(self):
initial: dict = {
'organization': UserSettings.objects.get(user = self.request.user).default_organization
}
if 'pk' in self.kwargs:
if self.kwargs['pk']:
initial.update({'parent': self.kwargs['pk']})
self.model.parent.field.hidden = True
return initial
def get_success_url(self, **kwargs):
return reverse('Settings:KB Categories')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'New Group'
return context
class Change(ChangeView):
context_object_name = "group"
form_class = KnowledgeBaseCategoryForm
model = KnowledgeBaseCategory
permission_required = [
'assistance.change_knowledgebasecategory',
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = self.object.name
return context
def get_success_url(self, **kwargs):
return reverse('Settings:_knowledge_base_category_view', args=(self.kwargs['pk'],))
class View(ChangeView):
context_object_name = "item"
form_class = KnowledgeBaseCategoryForm
model = KnowledgeBaseCategory
permission_required = [
'assistance.view_knowledgebasecategory',
]
template_name = 'assistance/kb_category.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['articles'] = KnowledgeBase.objects.filter(category=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.model_name
context['model_delete_url'] = reverse('Settings:_knowledge_base_category_delete', args=(self.kwargs['pk'],))
context['content_title'] = self.object.name
return context
@method_decorator(auth_decorator.permission_required("assistance.change_knowledgebasecategory", raise_exception=True))
def post(self, request, *args, **kwargs):
item = KnowledgeBase.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.save()
# dont allow saving any post data outside notes.
# todo: figure out what needs to be returned
# return super().post(request, *args, **kwargs)
def get_success_url(self, **kwargs):
return reverse('Settings:_knowledge_base_category_view', args=(self.kwargs['pk'],))
class Delete(DeleteView):
model = KnowledgeBaseCategory
permission_required = [
'assistance.delete_knowledgebasecategory',
]
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('Settings:KB Categories')

View File

@ -1,4 +1,7 @@
from django.db.models import Q
from django import forms
from django.urls import reverse
from app import settings
from config_management.models.groups import ConfigGroups
@ -32,3 +35,89 @@ class ConfigGroupForm(CommonModelForm):
).exclude(
id=int(kwargs['instance'].id)
)
class DetailForm(ConfigGroupForm):
tabs: dict = {
"details": {
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'name',
'parent',
'is_global',
'organization',
'c_created',
'c_modified',
],
"right": [
'model_notes',
]
},
{
"layout": "single",
"fields": [
'config',
]
}
]
},
"child_groups": {
"name": "Child Groups",
"slug": "child_groups",
"sections": []
},
"hosts": {
"name": "Hosts",
"slug": "hosts",
"sections": []
},
"software": {
"name": "Software",
"slug": "software",
"sections": []
},
"configuration": {
"name": "Configuration",
"slug": "configuration",
"sections": []
},
"notes": {
"name": "Notes",
"slug": "notes",
"sections": []
}
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['c_created'] = forms.DateTimeField(
label = 'Created',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.created,
)
self.fields['c_modified'] = forms.DateTimeField(
label = 'Modified',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.modified,
)
self.tabs['details'].update({
"edit_url": reverse('Config Management:_group_change', args=(self.instance.pk,))
})
self.url_index_view = reverse('Config Management:Groups')

View File

@ -0,0 +1,21 @@
# Generated by Django 5.0.7 on 2024-08-17 08:05
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('config_management', '0002_configgrouphosts_configgroupsoftware'),
]
operations = [
migrations.AlterModelOptions(
name='configgroups',
options={'verbose_name_plural': 'Config Groups'},
),
migrations.AlterModelOptions(
name='configgroupsoftware',
options={'ordering': ['-action', 'software'], 'verbose_name_plural': 'Config Group Softwares'},
),
]

View File

@ -35,6 +35,12 @@ class GroupsCommonFields(TenancyObject, models.Model):
class ConfigGroups(GroupsCommonFields, SaveHistory):
class Meta:
verbose_name_plural = 'Config Groups'
reserved_config_keys: list = [
'software'
]
@ -264,6 +270,8 @@ class ConfigGroupSoftware(GroupsCommonFields, SaveHistory):
'software'
]
verbose_name_plural = 'Config Group Softwares'
config_group = models.ForeignKey(
ConfigGroups,

View File

@ -1,47 +1,180 @@
{% extends 'base.html.j2' %}
{% extends 'detail.html.j2' %}
{% block content %}
{% load json %}
{% load markdown %}
<script>
function openCity(evt, cityName) {
var i, tabcontent, tablinks;
{% block tabs %}
<form action="" method="post">
{% csrf_token %}
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
<div id="details" class="content-tab">
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>
{% include 'content/section.html.j2' with tab=form.tabs.details %}
</div>
<div id="child_groups" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.child_groups %}
<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="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.hosts %}
<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="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.software %}
<input type="button" value="Add Software Action" onclick="window.location='{% url 'Config Management:_group_software_add' model_pk %}';">
<table>
<thead>
<th>Name</th>
<th>Category</th>
<th>Action</th>
<th>Desired Version</th>
<th>&nbsp;</th>
</thead>
{% if softwares %}
{% for software in softwares %}
<tr>
<td><a href="{% url 'ITAM:_software_view' pk=software.software_id %}">{{ software.software }}</a></td>
<td>{{ software.software.category }}</td>
<td>
{% url 'Config Management:_group_software_change' group_id=group.id pk=software.id as icon_link %}
{% if software.get_action_display == 'Install' %}
{% include 'icons/success_text.html.j2' with icon_text=software.get_action_display icon_link=icon_link %}
{% elif software.get_action_display == 'Remove'%}
{% include 'icons/cross_text.html.j2' with icon_text=software.get_action_display %}
{% else %}
{% include 'icons/add_link.html.j2' with icon_text='Add' %}
{% endif %}
</td>
<td>
{% if software.version %}
{{ software.version }}
{% else %}
-
{% endif %}
</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
{% else %}
<td colspan="5">Nothing Found</td>
{% endif %}
</table>
</div>
<div id="configuration" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.configuration %}
<div>
<textarea cols="90" rows="30" readonly>{{ config }}</textarea>
</div>
</div>
<div id="notes" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.notes %}
{{ 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 %}
{% block contents %}
<form method="post">
<div id="Details" class="tabcontent">
<h3>Details</h3>
@ -60,28 +193,6 @@
<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>
@ -90,28 +201,6 @@
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>
@ -120,52 +209,11 @@
Software
</h3>
<input type="button" value="Add Software Action" onclick="window.location='{% url 'Config Management:_group_software_add' model_pk %}';">
<table>
<thead>
<th>Name</th>
<th>Category</th>
<th>Action</th>
<th>Desired Version</th>
<th>&nbsp;</th>
</thead>
{% if softwares %}
{% for software in softwares %}
<tr>
<td><a href="{% url 'ITAM:_software_view' pk=software.software_id %}">{{ software.software }}</a></td>
<td>{{ software.software.category }}</td>
<td>
{% url 'Config Management:_group_software_change' group_id=group.id pk=software.id as icon_link %}
{% if software.get_action_display == 'Install' %}
{% include 'icons/success_text.html.j2' with icon_text=software.get_action_display icon_link=icon_link %}
{% elif software.get_action_display == 'Remove'%}
{% include 'icons/cross_text.html.j2' with icon_text=software.get_action_display %}
{% else %}
{% include 'icons/add_link.html.j2' with icon_text='Add' %}
{% endif %}
</td>
<td>
{% if software.version %}
{{ software.version }}
{% else %}
-
{% endif %}
</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
{% else %}
<td colspan="5">Nothing Found</td>
{% endif %}
</table>
</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">

View File

@ -27,7 +27,7 @@ class ConfigGroupPermissions(TestCase, ModelPermissions):
url_name_add = '_group_add'
url_name_change = '_group_view'
url_name_change = '_group_change'
url_name_delete = '_group_delete'

View File

@ -1,6 +1,6 @@
from django.urls import path
from config_management.views.groups.groups import GroupIndexView, GroupAdd, GroupDelete, GroupView, GroupHostAdd, GroupHostDelete
from config_management.views.groups.groups import GroupIndexView, GroupAdd, GroupChange, GroupDelete, GroupView, GroupHostAdd, GroupHostDelete
from config_management.views.groups.software import GroupSoftwareAdd, GroupSoftwareChange, GroupSoftwareDelete
app_name = "Config Management"
@ -9,6 +9,7 @@ urlpatterns = [
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:pk>/edit', GroupChange.as_view(), name='_group_change'),
path('group/<int:pk>/child', GroupAdd.as_view(), name='_group_add_child'),
path('group/<int:pk>/delete', GroupDelete.as_view(), name='_group_delete'),

View File

@ -13,7 +13,7 @@ from itam.models.device import Device
from settings.models.user_settings import UserSettings
from config_management.forms.group_hosts import ConfigGroupHostsForm
from config_management.forms.group.group import ConfigGroupForm
from config_management.forms.group.group import ConfigGroupForm, DetailForm
from config_management.models.groups import ConfigGroups, ConfigGroupHosts, ConfigGroupSoftware
@ -102,7 +102,7 @@ class GroupAdd(AddView):
class GroupView(ChangeView):
class GroupChange(ChangeView):
context_object_name = "group"
@ -111,10 +111,39 @@ class GroupView(ChangeView):
model = ConfigGroups
permission_required = [
'config_management.view_configgroups',
'config_management.change_configgroups',
]
template_name = 'form.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = self.object.name
return context
def get_success_url(self, **kwargs):
return reverse('Config Management:_group_view', args=(self.kwargs['pk'],))
class GroupView(ChangeView):
context_object_name = "group"
form_class = DetailForm
model = ConfigGroups
permission_required = [
'config_management.view_configgroups',
]
template_name = 'config_management/group.html.j2'

View File

@ -98,3 +98,7 @@ class CommonModelForm(forms.ModelForm):
|
Q(manager=user)
)
if hasattr(self, 'instance'):
self.model_name_plural = self.instance._meta.verbose_name_plural

View File

@ -1,4 +1,7 @@
from django import forms
from django.urls import reverse
from app import settings
from core.forms.common import CommonModelForm
from core.models.manufacturer import Manufacturer
@ -24,3 +27,64 @@ class ManufacturerForm(
]
model = Manufacturer
class DetailForm(ManufacturerForm):
tabs: dict = {
"details": {
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'name',
'slug',
'organization',
'is_global',
'c_created',
'c_modified',
],
"right": [
'model_notes',
]
}
]
},
# "notes": {
# "name": "Notes",
# "slug": "notes",
# "sections": []
# }
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['c_created'] = forms.DateTimeField(
label = 'Created',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.created,
)
self.fields['c_modified'] = forms.DateTimeField(
label = 'Modified',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.modified,
)
self.tabs['details'].update({
"edit_url": reverse('Settings:_manufacturer_change', args=(self.instance.pk,))
})
self.url_index_view = reverse('Settings:_manufacturers')

View File

@ -0,0 +1,21 @@
# Generated by Django 5.0.7 on 2024-08-17 08:05
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0002_notes'),
]
operations = [
migrations.AlterModelOptions(
name='manufacturer',
options={'ordering': ['name'], 'verbose_name_plural': 'Manufacturers'},
),
migrations.AlterModelOptions(
name='notes',
options={'ordering': ['-created'], 'verbose_name_plural': 'Notes'},
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 5.0.7 on 2024-08-17 08:05
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0003_alter_manufacturer_options_alter_notes_options'),
('itim', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='notes',
name='service',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itim.service'),
),
]

View File

@ -41,6 +41,18 @@ class SaveHistory(models.Model):
value = bool(before[entry])
elif (
"{" in str(after[entry])
and
"}" in str(after[entry])
) or (
"[" in str(after[entry])
and
"]" in str(after[entry])
):
value = str(after[entry]).replace("'", '\"')
else:
value = str(before[entry])
@ -62,6 +74,18 @@ class SaveHistory(models.Model):
value = bool(after[entry])
elif (
"{" in str(after[entry])
and
"}" in str(after[entry])
) or (
"[" in str(after[entry])
and
"]" in str(after[entry])
):
value = str(after[entry]).replace("'", '\"')
else:
value = str(after[entry])

View File

@ -34,6 +34,9 @@ class Manufacturer(TenancyObject, ManufacturerCommonFields, SaveHistory):
'name'
]
verbose_name_plural = 'Manufacturers'
name = models.CharField(
blank = False,
max_length = 50,

View File

@ -10,6 +10,9 @@ from itam.models.device import Device
from itam.models.software import Software
from itam.models.operating_system import OperatingSystem
from itim.models.services import Service
class NotesCommonFields(TenancyObject, models.Model):
@ -43,6 +46,9 @@ class Notes(NotesCommonFields):
'-created'
]
verbose_name_plural = 'Notes'
note = models.TextField(
verbose_name = 'Note',
@ -88,6 +94,14 @@ class Notes(NotesCommonFields):
blank= True
)
service = models.ForeignKey(
Service,
on_delete=models.CASCADE,
default = None,
null = True,
blank= True
)
software = models.ForeignKey(
Software,
on_delete=models.CASCADE,

View File

@ -0,0 +1,34 @@
{% extends 'detail.html.j2' %}
{% load json %}
{% load markdown %}
{% block tabs %}
<form action="" method="post">
{% csrf_token %}
<div id="details" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.details %}
</div>
<div id="notes" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.notes %}
{{ 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,64 @@
from django import template
from django.template.defaultfilters import stringfilter
register = template.Library()
@register.filter()
@stringfilter
def choice_ids(value):
"""Convert choice field value to list
Provide from `{{ field.field.choices }}` the `field.value` and have it converted to a loop
Args:
value (string): for field that has `field.field.choices`, provide `field.value`
Returns:
list: `field.value` casted to a useable list
"""
if value == 'None':
return ''
alist: list = []
if '[' in value:
value = str(value).replace('[', '').replace(']', '')
if ',' in value:
for item in value.split(','):
try:
alist += [ int(item) ]
except:
alist += [ str(item) ]
else:
try:
alist += [ int(item) ]
except:
alist += [ str(item) ]
else:
try:
alist += [ int(value) ]
except:
alist += [ str(value) ]
return alist

View File

@ -14,4 +14,4 @@ def json_pretty(value):
return str('{}')
return json.dumps(json.loads(value), indent=4, sort_keys=True)
return json.dumps(json.loads(value.replace("'", '"')), indent=4, sort_keys=True)

View File

@ -9,4 +9,4 @@ register = template.Library()
@register.filter()
@stringfilter
def markdown(value):
return md.markdown(value, extensions=['markdown.extensions.fenced_code'])
return md.markdown(value, extensions=['markdown.extensions.fenced_code', 'codehilite'])

View File

@ -12,7 +12,12 @@ from itam.models.device import Device
class HistoryPermissions:
"""Test cases for accessing History """
"""Test cases for accessing History
For this test to function properly you must add the history items model to
`app.core.views.history.View.get_object()`. specifically an entry to the switch in the middle
of the function.
"""
item: object

View File

@ -26,7 +26,7 @@ class ManufacturerPermissions(TestCase, ModelPermissions):
url_name_add = '_manufacturer_add'
url_name_change = '_manufacturer_view'
url_name_change = '_manufacturer_change'
url_name_delete = '_manufacturer_delete'

View File

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

View File

@ -41,10 +41,24 @@ class View(OrganizationPermission, generic.View):
from config_management.models.groups import ConfigGroups
from settings.models.external_link import ExternalLink
if not hasattr(self, 'model'):
match self.kwargs['model_name']:
case 'cluster':
from itim.models.clusters import Cluster
self.model = Cluster
case 'clustertype':
from itim.models.clusters import ClusterType
self.model = ClusterType
case 'configgroups':
self.model = ConfigGroups
@ -61,6 +75,22 @@ class View(OrganizationPermission, generic.View):
self.model = DeviceType
case 'externallink':
self.model = ExternalLink
case 'knowledgebase':
from assistance.models.knowledge_base import KnowledgeBase
self.model = KnowledgeBase
case 'knowledgebasecategory':
from assistance.models.knowledge_base import KnowledgeBaseCategory
self.model = KnowledgeBaseCategory
case 'manufacturer':
self.model = Manufacturer
@ -81,10 +111,22 @@ class View(OrganizationPermission, generic.View):
self.model = Organization
case 'port':
from itim.models.services import Port
self.model = Port
case 'team':
self.model = Team
case 'service':
from itim.models.services import Service
self.model = Service
case _:
raise Exception('Unable to determine history items model')

View File

@ -1,13 +0,0 @@
from django.urls import path
from . import views
from .views import knowledge_base, playbooks
app_name = "Information"
urlpatterns = [
path("kb/", knowledge_base.Index.as_view(), name="Knowledge Base"),
path("playbook/", playbooks.Index.as_view(), name="Playbooks"),
]

View File

@ -1,31 +0,0 @@
import json
from django.db.models import Q
from django.shortcuts import render
from django.template import Template, Context
from django.views import generic
from access.mixin import OrganizationPermission
class Index(generic.View):
# permission_required = [
# 'itil.view_knowledge_base'
# ]
template_name = 'form.html.j2'
def get(self, request):
context = {}
user_string = Template("{% include 'icons/issue_link.html.j2' with issue=10 %}")
user_context = Context(context)
context['form'] = user_string.render(user_context)
context['content_title'] = 'Knowledge Base'
return render(request, self.template_name, context)

View File

@ -1,29 +0,0 @@
import json
from django.db.models import Q
from django.shortcuts import render
from django.template import Template, Context
from django.views import generic
from access.mixin import OrganizationPermission
class Index(generic.View):
# permission_required = [
# 'itil.view_playbook'
# ]
template_name = 'form.html.j2'
def get(self, request):
context = {}
user_string = Template("{% include 'icons/issue_link.html.j2' with issue=11 %}")
user_context = Context(context)
context['form'] = user_string.render(user_context)
context['content_title'] = 'Playbooks'
return render(request, self.template_name, context)

View File

@ -1,5 +1,5 @@
from django import forms
from django.db.models import Q
from django.urls import reverse
from app import settings
@ -22,15 +22,78 @@ class DeviceForm(CommonModelForm):
'uuid',
'device_type',
'organization',
'is_virtual',
'model_notes',
'config',
]
class DetailForm(DeviceForm):
tabs: dict = {
"details": {
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'name',
'device_model',
'serial_number',
'uuid',
'device_type',
'organization',
'c_created',
'c_modified',
'lastinventory',
],
"right": [
'model_notes',
'is_virtual',
]
}
]
},
"software": {
"name": "Software",
"slug": "software",
"sections": []
},
"notes": {
"name": "Notes",
"slug": "notes",
"sections": []
},
"config_management": {
"name": "Config Management",
"slug": "config_management",
"sections": []
}
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if hasattr(kwargs['instance'], 'inventorydate'):
self.fields['lastinventory'] = forms.DateTimeField(
self.fields['c_created'] = forms.DateTimeField(
label = 'Created',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.created,
)
self.fields['c_modified'] = forms.DateTimeField(
label = 'Modified',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.modified,
)
self.fields['lastinventory'] = forms.DateTimeField(
label="Last Inventory Date",
input_formats=settings.DATETIME_FORMAT,
initial=kwargs['instance'].inventorydate,
@ -38,5 +101,8 @@ class DeviceForm(CommonModelForm):
required=False,
)
# for key in self.fields.keys():
# self.fields[key].widget.attrs['disabled'] = True
self.tabs['details'].update({
"edit_url": reverse('ITAM:_device_change', args=(self.instance.pk,))
})
self.url_index_view = reverse('ITAM:Devices')

View File

@ -1,4 +1,7 @@
from django.db.models import Q
from django import forms
from django.urls import reverse
from app import settings
from core.forms.common import CommonModelForm
@ -27,3 +30,62 @@ class DeviceModelForm(
]
model = DeviceModel
class DetailForm(DeviceModelForm):
tabs: dict = {
"details": {
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'name',
'slug',
'manufacturer',
'organization',
'is_global',
'c_created',
'c_modified',
],
"right": [
'model_notes',
]
}
]
},
# "notes": {
# "name": "Notes",
# "slug": "notes",
# "sections": []
# },
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['c_created'] = forms.DateTimeField(
label = 'Created',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.created,
)
self.fields['c_modified'] = forms.DateTimeField(
label = 'Modified',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.modified,
)
self.tabs['details'].update({
"edit_url": reverse('Settings:_device_model_change', args=(self.instance.pk,))
})
self.url_index_view = reverse('Settings:_device_models')

View File

@ -1,4 +1,7 @@
from django.db.models import Q
from django import forms
from django.urls import reverse
from app import settings
from core.forms.common import CommonModelForm
@ -26,3 +29,61 @@ class DeviceTypeForm(
]
model = DeviceType
class DetailForm(DeviceTypeForm):
tabs: dict = {
"details": {
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'name',
'slug',
'organization',
'is_global',
'c_created',
'c_modified',
],
"right": [
'model_notes',
]
}
]
},
# "notes": {
# "name": "Notes",
# "slug": "notes",
# "sections": []
# }
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['c_created'] = forms.DateTimeField(
label = 'Created',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.created,
)
self.fields['c_modified'] = forms.DateTimeField(
label = 'Modified',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.modified,
)
self.tabs['details'].update({
"edit_url": reverse('Settings:_device_type_change', args=(self.instance.pk,))
})
self.url_index_view = reverse('Settings:_device_types')

View File

@ -1,5 +1,5 @@
from django import forms
from django.db.models import Q
from django.urls import reverse
from app import settings
@ -9,7 +9,7 @@ from itam.models.operating_system import OperatingSystem
class OperatingSystemFormCommon(CommonModelForm):
class OperatingSystemForm(CommonModelForm):
class Meta:
@ -27,27 +27,99 @@ class OperatingSystemFormCommon(CommonModelForm):
class Update(OperatingSystemFormCommon):
# class Update(OperatingSystemFormCommon):
# def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs)
# self.fields['_created'] = forms.DateTimeField(
# label="Created",
# input_formats=settings.DATETIME_FORMAT,
# initial=kwargs['instance'].created,
# disabled=True
# )
# self.fields['_modified'] = forms.DateTimeField(
# label="Modified",
# input_formats=settings.DATETIME_FORMAT,
# initial=kwargs['instance'].modified,
# disabled=True
# )
# if kwargs['instance'].is_global:
# self.fields['is_global'].widget.attrs['disabled'] = True
class DetailForm(OperatingSystemForm):
tabs: dict = {
"details": {
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'name',
'publisher',
'serial_number',
'organization',
'c_created',
'c_modified',
],
"right": [
'model_notes',
]
}
]
},
"versions": {
"name": "Versions",
"slug": "versions",
"sections": []
},
"licences": {
"name": "Licences",
"slug": "licences",
"sections": []
},
"installations": {
"name": "Installations",
"slug": "installations",
"sections": []
},
"notes": {
"name": "Notes",
"slug": "notes",
"sections": []
}
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['_created'] = forms.DateTimeField(
label="Created",
self.fields['c_created'] = forms.DateTimeField(
label = 'Created',
input_formats=settings.DATETIME_FORMAT,
initial=kwargs['instance'].created,
disabled=True
disabled = True,
initial = self.instance.created,
)
self.fields['_modified'] = forms.DateTimeField(
label="Modified",
self.fields['c_modified'] = forms.DateTimeField(
label = 'Modified',
input_formats=settings.DATETIME_FORMAT,
initial=kwargs['instance'].modified,
disabled=True
disabled = True,
initial = self.instance.modified,
)
self.tabs['details'].update({
"edit_url": reverse('ITAM:_operating_system_change', args=(self.instance.pk,))
})
if kwargs['instance'].is_global:
self.fields['is_global'].widget.attrs['disabled'] = True
self.url_index_view = reverse('ITAM:Operating Systems')

View File

@ -1,4 +1,8 @@
from django import forms
from django.db.models import Q
from django.urls import reverse
from app import settings
from core.forms.common import CommonModelForm
@ -11,22 +15,108 @@ class SoftwareForm(CommonModelForm):
class Meta:
model = Software
fields = [
"name",
'name',
'publisher',
'slug',
'id',
'organization',
'is_global',
'category',
'model_notes',
]
class SoftwareChange(SoftwareForm):
class SoftwareFormUpdate(SoftwareForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.instance.is_global:
self.fields['is_global'] = forms.BooleanField(
label = 'Is Global',
initial = self.instance.is_global
)
self.fields['organization'] = forms.CharField(
label = 'Organization',
initial = self.instance.organization
)
class DetailForm(SoftwareForm):
tabs: dict = {
"details": {
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'name',
'publisher',
'slug',
'organization',
'is_global',
'category',
'c_created',
'c_modified',
],
"right": [
'model_notes',
]
}
]
},
"versions": {
"name": "Versions",
"slug": "versions",
"sections": []
},
"licences": {
"name": "Licences",
"slug": "licences",
"sections": []
},
"notes": {
"name": "Notes",
"slug": "notes",
"sections": []
},
"installations": {
"name": "Installations",
"slug": "installations",
"sections": []
}
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['is_global'].widget.attrs['disabled'] = True
self.fields[ 'organization' ] = forms.CharField(
label = 'Organization',
initial = self.instance.organization
)
self.fields['c_created'] = forms.DateTimeField(
label = 'Created',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.created,
)
self.fields['c_modified'] = forms.DateTimeField(
label = 'Modified',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.modified,
)
self.tabs['details'].update({
"edit_url": reverse('ITAM:_software_change', args=(self.instance.pk,))
})
self.url_index_view = reverse('ITAM:Software')

View File

@ -1,4 +1,7 @@
from django.db.models import Q
from django import forms
from django.urls import reverse
from app import settings
from core.forms.common import CommonModelForm
@ -25,3 +28,61 @@ class SoftwareCategoryForm(
]
model = SoftwareCategory
class DetailForm(SoftwareCategoryForm):
tabs: dict = {
"details": {
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'name',
'slug',
'organization',
'is_global',
'c_created',
'c_modified',
],
"right": [
'model_notes',
]
}
]
},
# "notes": {
# "name": "Notes",
# "slug": "notes",
# "sections": []
# },
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['c_created'] = forms.DateTimeField(
label = 'Created',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.created,
)
self.fields['c_modified'] = forms.DateTimeField(
label = 'Modified',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.modified,
)
self.tabs['details'].update({
"edit_url": reverse('Settings:_software_category_change', args=(self.instance.pk,))
})
self.url_index_view = reverse('Settings:_software_categories')

View File

@ -0,0 +1,19 @@
# Generated by Django 5.0.7 on 2024-07-17 07:17
import itam.models.device
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('itam', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='device',
name='config',
field=models.JSONField(blank=True, default=None, help_text='Configuration for this device', null=True, validators=[itam.models.device.Device.validate_config_keys_not_reserved], verbose_name='Host Configuration'),
),
]

View File

@ -0,0 +1,69 @@
# Generated by Django 5.0.7 on 2024-08-17 08:05
import itam.models.device
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('itam', '0002_device_config'),
]
operations = [
migrations.AlterModelOptions(
name='device',
options={'verbose_name_plural': 'Devices'},
),
migrations.AlterModelOptions(
name='devicemodel',
options={'ordering': ['manufacturer', 'name'], 'verbose_name_plural': 'Device Models'},
),
migrations.AlterModelOptions(
name='deviceoperatingsystem',
options={'verbose_name_plural': 'Device Operating Systems'},
),
migrations.AlterModelOptions(
name='devicesoftware',
options={'ordering': ['-action', 'software'], 'verbose_name_plural': 'Device Softwares'},
),
migrations.AlterModelOptions(
name='devicetype',
options={'verbose_name_plural': 'Device Types'},
),
migrations.AlterModelOptions(
name='operatingsystem',
options={'verbose_name_plural': 'Operating Systems'},
),
migrations.AlterModelOptions(
name='operatingsystemversion',
options={'verbose_name_plural': 'Operating System Versions'},
),
migrations.AlterModelOptions(
name='software',
options={'verbose_name_plural': 'Softwares'},
),
migrations.AlterModelOptions(
name='softwarecategory',
options={'verbose_name_plural': 'Software Categories'},
),
migrations.AlterModelOptions(
name='softwareversion',
options={'verbose_name_plural': 'Software Versions'},
),
migrations.AddField(
model_name='device',
name='is_virtual',
field=models.BooleanField(blank=True, default=False, help_text='Is this device a virtual machine', verbose_name='Is Virtual'),
),
migrations.AlterField(
model_name='device',
name='name',
field=models.CharField(max_length=50, unique=True, validators=[itam.models.device.Device.validate_hostname_format]),
),
migrations.AlterField(
model_name='device',
name='uuid',
field=models.CharField(blank=True, default=None, help_text='System GUID/UUID.', max_length=50, null=True, unique=True, validators=[itam.models.device.Device.validate_uuid_format], verbose_name='UUID'),
),
]

View File

@ -25,6 +25,11 @@ from settings.models.app_settings import AppSettings
class DeviceType(DeviceCommonFieldsName, SaveHistory):
class Meta:
verbose_name_plural = 'Device Types'
def clean(self):
app_settings = AppSettings.objects.get(owner_organization=None)
@ -44,6 +49,25 @@ class DeviceType(DeviceCommonFieldsName, SaveHistory):
class Device(DeviceCommonFieldsName, SaveHistory):
class Meta:
verbose_name_plural = 'Devices'
reserved_config_keys: list = [
'software'
]
def validate_config_keys_not_reserved(self):
value: dict = self
for invalid_key in Device.reserved_config_keys:
if invalid_key in value.keys():
raise ValidationError(f'json key "{invalid_key}" is a reserved configuration key')
def validate_uuid_format(self):
pattern = r'[0-9|a-f]{8}\-[0-9|a-f]{4}\-[0-9|a-f]{4}\-[0-9|a-f]{4}\-[0-9|a-f]{12}'
@ -113,12 +137,30 @@ class Device(DeviceCommonFieldsName, SaveHistory):
)
config = models.JSONField(
blank = True,
default = None,
null = True,
validators=[ validate_config_keys_not_reserved ],
verbose_name = 'Host Configuration',
help_text = 'Configuration for this device'
)
inventorydate = models.DateTimeField(
verbose_name = 'Last Inventory Date',
null = True,
blank = True,
)
is_virtual = models.BooleanField(
blank = True,
default = False,
help_text = 'Is this device a virtual machine',
null = False,
verbose_name = 'Is Virtual',
)
def save(
self, force_insert=False, force_update=False, using=None, update_fields=None
):
@ -254,6 +296,25 @@ class Device(DeviceCommonFieldsName, SaveHistory):
config['software'] = merge_software(group_software, host_software)
if self.config:
config.update(self.config)
from itim.models.services import Service
services = Service.objects.filter(
device = self.pk
)
for service in services:
if service.config_variables:
service_config:dict = {
service.config_key_variable: service.config_variables
}
config.update(service_config)
return config
@ -266,6 +327,9 @@ class DeviceSoftware(DeviceCommonFields, SaveHistory):
'software'
]
verbose_name_plural = 'Device Softwares'
class Actions(models.TextChoices):
INSTALL = '1', 'Install'
@ -341,6 +405,12 @@ class DeviceSoftware(DeviceCommonFields, SaveHistory):
class DeviceOperatingSystem(DeviceCommonFields, SaveHistory):
class Meta:
verbose_name_plural = 'Device Operating Systems'
device = models.ForeignKey(
Device,
on_delete = models.CASCADE,

View File

@ -20,6 +20,9 @@ class DeviceModel(DeviceCommonFieldsName, SaveHistory):
'name',
]
verbose_name_plural = 'Device Models'
manufacturer = models.ForeignKey(
Manufacturer,
on_delete=models.CASCADE,

View File

@ -42,6 +42,12 @@ class OperatingSystemFieldsName(OperatingSystemCommonFields):
class OperatingSystem(OperatingSystemFieldsName, SaveHistory):
class Meta:
verbose_name_plural = 'Operating Systems'
publisher = models.ForeignKey(
Manufacturer,
on_delete=models.CASCADE,
@ -57,6 +63,12 @@ class OperatingSystem(OperatingSystemFieldsName, SaveHistory):
class OperatingSystemVersion(OperatingSystemCommonFields, SaveHistory):
class Meta:
verbose_name_plural = 'Operating System Versions'
operating_system = models.ForeignKey(
OperatingSystem,
on_delete=models.CASCADE,

View File

@ -37,6 +37,12 @@ class SoftwareCommonFields(TenancyObject, models.Model):
class SoftwareCategory(SoftwareCommonFields, SaveHistory):
class Meta:
verbose_name_plural = 'Software Categories'
def clean(self):
app_settings = AppSettings.objects.get(owner_organization=None)
@ -55,6 +61,12 @@ class SoftwareCategory(SoftwareCommonFields, SaveHistory):
class Software(SoftwareCommonFields, SaveHistory):
class Meta:
verbose_name_plural = 'Softwares'
publisher = models.ForeignKey(
Manufacturer,
on_delete=models.CASCADE,
@ -91,6 +103,12 @@ class Software(SoftwareCommonFields, SaveHistory):
class SoftwareVersion(SoftwareCommonFields, SaveHistory):
class Meta:
verbose_name_plural = 'Software Versions'
software = models.ForeignKey(
Software,
on_delete=models.CASCADE,

View File

@ -1,199 +1,64 @@
{% extends 'base.html.j2' %}
{% extends 'detail.html.j2' %}
{% load json %}
{% load markdown %}
{% block title %}{{ device.name }}{% endblock %}
{% block content %}
<script>
function openCity(evt, cityName) {
// Declare all variables
var i, tabcontent, tablinks;
// Get all elements with class="tabcontent" and hide them
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
// Get all elements with class="tablinks" and remove the class "active"
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
// Show the current tab, and add an "active" class to the button that opened the tab
document.getElementById(cityName).style.display = "block";
evt.currentTarget.className += " active";
}
</script>
<div class="tab">
<button onclick="window.location='{% url 'ITAM:Devices' %}';"
style="vertical-align: middle; padding: auto; margin: 0px">
<svg xmlns="http://www.w3.org/2000/svg" height="25px" viewBox="0 -960 960 960" width="25px"
style="vertical-align: middle; margin: 0px; padding: 0px border: none; " fill="#6a6e73">
<path d="m313-480 155 156q11 11 11.5 27.5T468-268q-11 11-28 11t-28-11L228-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T468-692q11 11 11 28t-11 28L313-480Zm264 0 155 156q11 11 11.5 27.5T732-268q-11 11-28 11t-28-11L492-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T732-692q11 11 11 28t-11 28L577-480Z" />
</svg> Back to Devices</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
<button id="SoftwareOpen" class="tablinks" onclick="openCity(event, 'Software')">Software</button>
<button id="NotesOpen" class="tablinks" onclick="openCity(event, 'Notes')">Notes</button>
<button id="ConfigManagementOpen" class="tablinks" onclick="openCity(event, 'ConfigManagement')">Config Management</button>
<!-- <button class="tablinks" onclick="openCity(event, 'Installations')">Installations</button> -->
</div>
<style>
.detail-view-field {
display:unset;
height: 30px;
line-height: 30px;
padding: 0px 20px 40px 20px;
}
.detail-view-field label {
display: inline-block;
font-weight: bold;
width: 200px;
margin: 10px;
/*padding: 10px;*/
height: 30px;
line-height: 30px;
}
.detail-view-field span {
display: inline-block;
width: 340px;
margin: 10px;
/*padding: 10px;*/
border-bottom: 1px solid #ccc;
height: 30px;
line-height: 30px;
}
</style>
{% block tabs %}
<form action="" method="post">
{% csrf_token %}
<div id="details" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.details %}
<div id="Details" class="tabcontent">
<h3>
Details
<span style="font-weight: normal; float: right;">{% include 'icons/issue_link.html.j2' with issue=6 %}</span>
</h3>
<div style="align-items:flex-start; align-content: center; display: flexbox; width: 100%">
<div style="display: inline; width: 40%; margin: 30px;">
<div class="detail-view-field">
<label>{{ form.name.label }}</label>
<span>{{ form.name.value }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.device_model.label }}</label>
<span>
{% if device.device_model %}
{{ device.device_model }}
{% else %}
&nbsp;
{% endif %}
</span>
</div>
<div class="detail-view-field">
<label>{{ form.serial_number.label }}</label>
<span>
{% if form.serial_number.value %}
{{ form.serial_number.value }}
{% else %}
&nbsp;
{% endif %}
</span>
</div>
<div class="detail-view-field">
<label>{{ form.uuid.label }}</label>
<span>
{% if form.uuid.value %}
{{ form.uuid.value }}
{% else %}
&nbsp;
{% endif %}
</span>
</div>
<div class="detail-view-field">
<label>{{ form.device_type.label }}</label>
<span>
{% if device.device_type %}
{{ device.device_type }}
{% else %}
&nbsp;
{% endif %}
</span>
</div>
<div class="detail-view-field">
<label>{{ form.organization.label }}</label>
<span>{{ device.organization }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.lastinventory.label }}</label>
<span>
{% if form.lastinventory.value %}
{{ form.lastinventory.value }}
{% else %}
&nbsp;
{% endif %}
</span>
</div>
</div>
<div style="display: inline; width: 40%; margin: 30px; text-align: left;">
<div>
<label style="font-weight: bold; width: 100%; border-bottom: 1px solid #ccc; display: block; text-align: inherit;">{{ form.model_notes.label }}</label>
<div style="display: inline-block; text-align: left;">
{% if form.model_notes.value %}
{{ form.model_notes.value | markdown | safe }}
{% else %}
&nbsp;
{% endif %}
</div>
</div>
</div>
</div>
<input type="button" value="Edit" onclick="window.location='{% url 'ITAM:_device_change' device.id %}';">
<hr />
<div style="display: block; width: 100%;">
<h3>Operating System</h3>
<br>
{{ operating_system.as_p }}
<input type="submit" name="{{operating_system.prefix}}" value="Submit" />
<input type="submit" name="{{ operating_system.prefix }}" value="Submit" />
</div>
{% if not tab %}
<script>
// Get the element with id="defaultOpen" and click on it
document.getElementById("defaultOpen").click();
</script>
{% endif %}
</div>
<div style="display: block; width: 100%;">
<h3>Dependent Services</h3>
<table>
<tr>
<th>Name</th>
<th>Ports</th>
</tr>
{% if services %}
{% for service in services %}
<tr>
<td><a href="{% url 'ITIM:_service_view' service.pk %}">{{ service }}</a></td>
<td>{% for port in service.port.all %}{{ port }} ({{ port.description}}), {% endfor %}</td>
</tr>
{% endfor%}
{% else %}
<tr>
<td colspan="2"> Nothing Found</td>
</tr>
{% endif %}
</table>
</div>
<div id="Software" class="tabcontent">
<h3>Software</h3>
<div style="display: block; width: 100%;">
<h3>Device Config</h3>
<br>
<textarea cols="90" rows="30" readonly>{{ device.config }}</textarea>
</div>
</div>
<div id="software" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.software %}
<hr>
Installed Software: {{ installed_software }}
<input type="button" value="Add Software Action" onclick="window.location='{% url 'ITAM:_device_software_add' device.id %}';">
@ -269,19 +134,14 @@
</span>
</div>
{% if tab == 'software' %}
<script>
// Get the element with id="defaultOpen" and click on it
document.getElementById("SoftwareOpen").click();
</script>
{% endif %}
</div>
</div>
<div id="Notes" class="tabcontent">
<h3>
Notes
</h3>
<div id="notes" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.notes %}
{{ notes_form }}
<input type="submit" name="{{notes_form.prefix}}" value="Submit" />
<div class="comments">
@ -292,17 +152,14 @@
{% endif %}
</div>
{% if tab == 'notes' %}
<script>
// Get the element with id="defaultOpen" and click on it
document.getElementById("NotesOpen").click();
</script>
{% endif %}
</div>
</div>
<div id="ConfigManagement" class="tabcontent">
<h3>Configuration Management</h3>
<div id="config_management" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.config_management %}
<div>
<textarea cols="90" rows="30" readonly>{{ config }}</textarea>
</div>
@ -329,13 +186,6 @@
{% endif %}
</table>
{% if tab == 'configmanagement' %}
<script>
// Get the element with id="defaultOpen" and click on it
document.getElementById("ConfigManagementOpen").click();
</script>
{% endif %}
</div>
</div>
</form>
{% endblock %}
{% endblock %}

View File

@ -0,0 +1,34 @@
{% extends 'detail.html.j2' %}
{% load json %}
{% load markdown %}
{% block tabs %}
<form action="" method="post">
{% csrf_token %}
<div id="details" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.details %}
</div>
<div id="notes" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.notes %}
{{ 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,36 @@
{% extends 'detail.html.j2' %}
{% load json %}
{% load markdown %}
{% block tabs %}
<form action="" method="post">
{% csrf_token %}
<div id="details" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.details %}
</div>
<div id="notes" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.notes %}
{{ 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

@ -1,118 +1,116 @@
{% extends 'base.html.j2' %}
{% extends 'detail.html.j2' %}
{% block title %}{{ operating_system.name }}{% endblock %}
{% load json %}
{% load markdown %}
{% block content %}
<script>
function openCity(evt, cityName) {
var i, tabcontent, tablinks;
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(cityName).style.display = "block";
evt.currentTarget.className += " active";
}
</script>
<div class="tab">
<button onclick="window.location='{% url 'ITAM:Operating Systems' %}';" style="vertical-align: middle; padding: auto; margin: 0px">
<svg xmlns="http://www.w3.org/2000/svg" height="25px" viewBox="0 -960 960 960" width="25px" style="vertical-align: middle; margin: 0px; padding: 0px border: none; " fill="#6a6e73">
<path d="m313-480 155 156q11 11 11.5 27.5T468-268q-11 11-28 11t-28-11L228-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T468-692q11 11 11 28t-11 28L313-480Zm264 0 155 156q11 11 11.5 27.5T732-268q-11 11-28 11t-28-11L492-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T732-692q11 11 11 28t-11 28L577-480Z"/>
</svg> Back to Operating Systems</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
<button class="tablinks" onclick="openCity(event, 'Versions')">Versions</button>
<button class="tablinks" onclick="openCity(event, 'Licences')">Licences</button>
<button class="tablinks" onclick="openCity(event, 'Notes')">Notes</button>
<button class="tablinks" onclick="openCity(event, 'Installations')">Installations</button>
</div>
<form method="post">
<div id="Details" class="tabcontent">
<h3>Details</h3>
{% block tabs %}
<form action="" method="post">
{% csrf_token %}
{{ form }}
<br>
<input type="submit" value="Submit">
<script>
document.getElementById("defaultOpen").click();
</script>
<div id="details" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.details %}
</div>
<div id="Versions" class="tabcontent">
<h3>Versions</h3>
<input type="button" value="New Operating System Version" onclick="window.location='{% url 'ITAM:_operating_system_version_add' pk=operating_system.id %}';">
<table>
<thead>
<th>Version</th>
<th>Installations</th>
<th>Vulnerable</th>
<th>&nbsp;</th>
</thead>
{% for version in operating_system_versions %}
<tr>
<td><a href="{% url 'ITAM:_operating_system_version_view' operating_system_id=operating_system.id pk=version.id %}">{{ version.name }}</a></td>
<td>{% if version.installs == 0%}-{% else %}{{ version.installs }}{% endif %}</td>
<td>&nbsp;</td>
<td><a href="{% url 'ITAM:_operating_system_version_delete' operating_system_id=operating_system.id pk=version.id %}">DELETE</a></td>
</tr>
{% endfor %}
</table>
<div id="versions" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.versions %}
<table>
<thead>
<th>Version</th>
<th>Installations</th>
<th>Vulnerable</th>
<th>&nbsp;</th>
</thead>
{% for version in operating_system_versions %}
<tr>
<td><a href="{% url 'ITAM:_operating_system_version_view' operating_system_id=operating_system.id pk=version.id %}">{{ version.name }}</a></td>
<td>{% if version.installs == 0%}-{% else %}{{ version.installs }}{% endif %}</td>
<td>&nbsp;</td>
<td><a href="{% url 'ITAM:_operating_system_version_delete' operating_system_id=operating_system.id pk=version.id %}">DELETE</a></td>
</tr>
{% endfor %}
</table>
</div>
<div id="Licences" class="tabcontent">
<h3>Licences</h3>
{% include 'icons/issue_link.html.j2' with issue=4 %}
<table>
<thead>
<th>Name</th>
<th>Type</th>
<th>Available</th>
<th>&nbsp;</th>
</thead>
<tr>
<td>GPL-3</td>
<td>Open Source</td>
<td>1 / 5</td>
<th>&nbsp;</th>
</tr>
<tr>
<td>MIT</td>
<td>Open Source</td>
<td>Unlimited</td>
<th>&nbsp;</th>
</tr>
<tr>
<td>Windows Device</td>
<td>CAL</td>
<td>11 / 15</td>
<th>&nbsp;</th>
</tr>
</table>
<div id="licences" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.licences %}
{% include 'icons/issue_link.html.j2' with issue=4 %}
<table>
<thead>
<th>Name</th>
<th>Type</th>
<th>Available</th>
<th>&nbsp;</th>
</thead>
<tr>
<td>GPL-3</td>
<td>Open Source</td>
<td>1 / 5</td>
<th>&nbsp;</th>
</tr>
<tr>
<td>MIT</td>
<td>Open Source</td>
<td>Unlimited</td>
<th>&nbsp;</th>
</tr>
<tr>
<td>Windows Device</td>
<td>CAL</td>
<td>11 / 15</td>
<th>&nbsp;</th>
</tr>
</table>
</div>
<div id="Notes" class="tabcontent">
<h3>
Notes
</h3>
<div id="installations" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.installations %}
<table>
<thead>
<th>Device</th>
<th>Organization</th>
<th>Version</th>
<th title="Date Software Installed">Installed</th>
<th>&nbsp;</th>
</thead>
{% for install in installs %}
<tr>
<td><a href="{% url 'ITAM:_device_view' pk=install.device_id %}">{{ install.device }}</a></td>
<td>{{ install.organization }}</td>
<td>{{ install.version }}</td>
<td>
{% if install.installdate %}
{{ install.installdate }}
{% else %}
-
{% endif %}
</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
</table>
</div>
<div id="notes" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.notes %}
{{ notes_form }}
<input type="submit" name="{{notes_form.prefix}}" value="Submit" />
<div class="comments">
@ -123,36 +121,8 @@
{% endif %}
</div>
</div>
<div id="Installations" class="tabcontent">
<h3>Installations</h3>
<table>
<thead>
<th>Device</th>
<th>Organization</th>
<th>Version</th>
<th title="Date Software Installed">Installed</th>
<th>&nbsp;</th>
</thead>
{% for install in installs %}
<tr>
<td><a href="{% url 'ITAM:_device_view' pk=install.device_id %}">{{ install.device }}</a></td>
<td>{{ install.organization }}</td>
<td>{{ install.version }}</td>
<td>
{% if install.installdate %}
{{ install.installdate }}
{% else %}
-
{% endif %}
</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
</table>
</div>
</form>
{% endblock %}
</form>
{% endblock %}

View File

@ -1,119 +1,85 @@
{% extends 'base.html.j2' %}
{% extends 'detail.html.j2' %}
{% block title %}{{ software.name }}{% endblock %}
{% load json %}
{% load markdown %}
{% block content %}
<script>
{% block tabs %}
<form action="" method="post">
{% csrf_token %}
<div id="details" class="content-tab">
function openCity(evt, cityName) {
// Declare all variables
var i, tabcontent, tablinks;
// Get all elements with class="tabcontent" and hide them
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
// Get all elements with class="tablinks" and remove the class "active"
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
// Show the current tab, and add an "active" class to the button that opened the tab
document.getElementById(cityName).style.display = "block";
evt.currentTarget.className += " active";
}
</script>
<div class="tab">
<button onclick="window.location='{% url 'ITAM:Software' %}';" style="vertical-align: middle; padding: auto; margin: 0px">
<svg xmlns="http://www.w3.org/2000/svg" height="25px" viewBox="0 -960 960 960" width="25px" style="vertical-align: middle; margin: 0px; padding: 0px border: none; " fill="#6a6e73">
<path d="m313-480 155 156q11 11 11.5 27.5T468-268q-11 11-28 11t-28-11L228-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T468-692q11 11 11 28t-11 28L313-480Zm264 0 155 156q11 11 11.5 27.5T732-268q-11 11-28 11t-28-11L492-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T732-692q11 11 11 28t-11 28L577-480Z"/>
</svg> Back to Software</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Versions')">Versions</button>
<button class="tablinks" onclick="openCity(event, 'Licences')">Licences</button>
<button class="tablinks" onclick="openCity(event, 'Notes')">Notes</button>
<button class="tablinks" onclick="openCity(event, 'Installations')">Installations</button>
</div>
<form method="post">
<div id="Details" class="tabcontent">
<h3>Details</h3>
{% csrf_token %}
{{ form }}
<br>
<input type="submit" value="Submit">
<script>
// Get the element with id="defaultOpen" and click on it
document.getElementById("defaultOpen").click();
</script>
{% include 'content/section.html.j2' with tab=form.tabs.details %}
</div>
<div id="Versions" class="tabcontent">
<h3>Versions</h3>
<input type="button" value="New Software Version" onclick="window.location='{% url 'ITAM:_software_version_add' pk=software.id %}';">
<table>
<thead>
<th>Version</th>
<th>Installations</th>
<th>Vulnerable</th>
<th>&nbsp;</th>
</thead>
{% for version in software_versions %}
<tr>
<td><a href="{% url 'ITAM:_software_version_view' software_id=software.id pk=version.id %}">{{ version.name }}</a></td>
<td>{{ version.installs }}</td>
<td>{% include 'icons/issue_link.html.j2' with issue=3 %}</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
</table>
</div>
<div id="Licences" class="tabcontent">
<h3>Licences</h3>
{% include 'icons/issue_link.html.j2' with issue=4 %}
<table>
<thead>
<th>Name</th>
<th>Type</th>
<th>Available</th>
<th>&nbsp;</th>
</thead>
<tr>
<td>GPL-3</td>
<td>Open Source</td>
<td>1 / 5</td>
<th>&nbsp;</th>
</tr>
<tr>
<td>MIT</td>
<td>Open Source</td>
<td>Unlimited</td>
<th>&nbsp;</th>
</tr>
<tr>
<td>Windows Device</td>
<td>CAL</td>
<td>11 / 15</td>
<th>&nbsp;</th>
</tr>
</table>
<div id="versions" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.versions %}
<input type="button" value="New Software Version" onclick="window.location='{% url 'ITAM:_software_version_add' pk=software.id %}';">
<table>
<thead>
<th>Version</th>
<th>Installations</th>
<th>Vulnerable</th>
<th>&nbsp;</th>
</thead>
{% for version in software_versions %}
<tr>
<td><a href="{% url 'ITAM:_software_version_view' software_id=software.id pk=version.id %}">{{ version.name }}</a></td>
<td>{{ version.installs }}</td>
<td>{% include 'icons/issue_link.html.j2' with issue=3 %}</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
</table>
</div>
<div id="Notes" class="tabcontent">
<h3>
Notes
</h3>
<div id="licences" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.licences %}
{% include 'icons/issue_link.html.j2' with issue=4 %}
<table>
<thead>
<th>Name</th>
<th>Type</th>
<th>Available</th>
<th>&nbsp;</th>
</thead>
<tr>
<td>GPL-3</td>
<td>Open Source</td>
<td>1 / 5</td>
<th>&nbsp;</th>
</tr>
<tr>
<td>MIT</td>
<td>Open Source</td>
<td>Unlimited</td>
<th>&nbsp;</th>
</tr>
<tr>
<td>Windows Device</td>
<td>CAL</td>
<td>11 / 15</td>
<th>&nbsp;</th>
</tr>
</table>
</div>
<div id="notes" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.notes %}
{{ notes_form }}
<input type="submit" name="{{notes_form.prefix}}" value="Submit" />
<div class="comments">
@ -124,58 +90,61 @@
{% endif %}
</div>
</div>
<div id="Installations" class="tabcontent">
<h3>Installations</h3>
<table>
<thead>
<th>Device</th>
<th>Organization</th>
<th title="Not Set/Install/Remove">Action</th>
<th>Installed Version</th>
<th title="Date Software Installed">Install Date</th>
<th>&nbsp;</th>
</thead>
{% if device_software %}
{% for device in device_software %}
<tr>
<td><a href="{% url 'ITAM:_device_view' pk=device.device.id %}">{{ device.device }}</a></td>
<td>{{ device.organization }}</td>
<td>
{% if device.get_action_display == 'Install' %}
{% include 'icons/success_text.html.j2' with icon_text=device.get_action_display %}
{% elif device.get_action_display == 'Remove'%}
{% include 'icons/cross_text.html.j2' with icon_text=device.get_action_display %}
{% else %}
-
{% endif %}
</td>
<td>
{% if device.installedversion %}
{{ device.installedversion }}
{% else %}
-
{% endif %}
</td>
<td>
{% if device.installed %}
{{ device.installed }}
{% else %}
-
{% endif %}
</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="6">Nothing Found</td>
</tr>
{% endif %}
</table>
</div>
</form>
{% endblock %}
<div id="installations" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.installations %}
<table>
<thead>
<th>Device</th>
<th>Organization</th>
<th title="Not Set/Install/Remove">Action</th>
<th>Installed Version</th>
<th title="Date Software Installed">Install Date</th>
<th>&nbsp;</th>
</thead>
{% if device_software %}
{% for device in device_software %}
<tr>
<td><a href="{% url 'ITAM:_device_view' pk=device.device.id %}">{{ device.device }}</a></td>
<td>{{ device.organization }}</td>
<td>
{% if device.get_action_display == 'Install' %}
{% include 'icons/success_text.html.j2' with icon_text=device.get_action_display %}
{% elif device.get_action_display == 'Remove'%}
{% include 'icons/cross_text.html.j2' with icon_text=device.get_action_display %}
{% else %}
-
{% endif %}
</td>
<td>
{% if device.installedversion %}
{{ device.installedversion }}
{% else %}
-
{% endif %}
</td>
<td>
{% if device.installed %}
{{ device.installed }}
{% else %}
-
{% endif %}
</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="6">Nothing Found</td>
</tr>
{% endif %}
</table>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,150 @@
{% extends 'detail.html.j2' %}
{% load json %}
{% load markdown %}
{% block tabs %}
<form action="" method="post">
{% csrf_token %}
<div id="details" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.details %}
</div>
<div id="versions" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.versions %}
<input type="button" value="New Software Version" onclick="window.location='{% url 'ITAM:_software_version_add' pk=software.id %}';">
<table>
<thead>
<th>Version</th>
<th>Installations</th>
<th>Vulnerable</th>
<th>&nbsp;</th>
</thead>
{% for version in software_versions %}
<tr>
<td><a href="{% url 'ITAM:_software_version_view' software_id=software.id pk=version.id %}">{{ version.name }}</a></td>
<td>{{ version.installs }}</td>
<td>{% include 'icons/issue_link.html.j2' with issue=3 %}</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
</table>
</div>
<div id="licences" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.licences %}
{% include 'icons/issue_link.html.j2' with issue=4 %}
<table>
<thead>
<th>Name</th>
<th>Type</th>
<th>Available</th>
<th>&nbsp;</th>
</thead>
<tr>
<td>GPL-3</td>
<td>Open Source</td>
<td>1 / 5</td>
<th>&nbsp;</th>
</tr>
<tr>
<td>MIT</td>
<td>Open Source</td>
<td>Unlimited</td>
<th>&nbsp;</th>
</tr>
<tr>
<td>Windows Device</td>
<td>CAL</td>
<td>11 / 15</td>
<th>&nbsp;</th>
</tr>
</table>
</div>
<div id="notes" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.notes %}
{{ 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>
<div id="installations" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.installations %}
<table>
<thead>
<th>Device</th>
<th>Organization</th>
<th title="Not Set/Install/Remove">Action</th>
<th>Installed Version</th>
<th title="Date Software Installed">Install Date</th>
<th>&nbsp;</th>
</thead>
{% if device_software %}
{% for device in device_software %}
<tr>
<td><a href="{% url 'ITAM:_device_view' pk=device.device.id %}">{{ device.device }}</a></td>
<td>{{ device.organization }}</td>
<td>
{% if device.get_action_display == 'Install' %}
{% include 'icons/success_text.html.j2' with icon_text=device.get_action_display %}
{% elif device.get_action_display == 'Remove'%}
{% include 'icons/cross_text.html.j2' with icon_text=device.get_action_display %}
{% else %}
-
{% endif %}
</td>
<td>
{% if device.installedversion %}
{{ device.installedversion }}
{% else %}
-
{% endif %}
</td>
<td>
{% if device.installed %}
{{ device.installed }}
{% else %}
-
{% endif %}
</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="6">Nothing Found</td>
</tr>
{% endif %}
</table>
</div>
</form>
{% endblock %}

View File

@ -30,7 +30,7 @@ class DeviceModelPermissions(TestCase, ModelPermissions):
url_name_add = '_device_model_add'
url_name_change = '_device_model_view'
url_name_change = '_device_model_change'
url_name_delete = '_device_model_delete'

View File

@ -26,7 +26,7 @@ class DeviceTypePermissions(TestCase, ModelPermissions):
url_name_add = '_device_type_add'
url_name_change = '_device_type_view'
url_name_change = '_device_type_change'
url_name_delete = '_device_type_delete'

View File

@ -27,7 +27,7 @@ class OperatingSystemPermissions(TestCase, ModelPermissions):
url_name_add = '_operating_system_add'
url_name_change = '_operating_system_view'
url_name_change = '_operating_system_change'
url_name_delete = '_operating_system_delete'

View File

@ -26,7 +26,7 @@ class SoftwarePermissions(TestCase, ModelPermissions):
url_name_add = '_software_add'
url_name_change = '_software_view'
url_name_change = '_software_change'
url_name_delete = '_software_delete'

View File

@ -29,7 +29,7 @@ class SoftwareCategoryPermissions(TestCase, ModelPermissions):
url_name_add = '_software_category_add'
url_name_change = '_software_category_view'
url_name_change = '_software_category_change'
url_name_delete = '_software_category_delete'

View File

@ -18,6 +18,7 @@ urlpatterns = [
path("operating_system", operating_system.IndexView.as_view(), name="Operating Systems"),
path("operating_system/<int:pk>", operating_system.View.as_view(), name="_operating_system_view"),
path("operating_system/<int:pk>/edit", operating_system.Change.as_view(), name="_operating_system_change"),
path("operating_system/add", operating_system.Add.as_view(), name="_operating_system_add"),
path("operating_system/delete/<int:pk>", operating_system.Delete.as_view(), name="_operating_system_delete"),
@ -30,6 +31,7 @@ urlpatterns = [
path("software/", software.IndexView.as_view(), name="Software"),
path("software/<int:pk>/", software.View.as_view(), name="_software_view"),
path("software/<int:pk>/change", software.Change.as_view(), name="_software_change"),
path("software/<int:pk>/delete", software.Delete.as_view(), name="_software_delete"),
path("software/<int:pk>/version/add", software_version.Add.as_view(), name="_software_version_add"),
path("software/<int:software_id>/version/<int:pk>", software_version.View.as_view(), name="_software_version_view"),

View File

@ -21,10 +21,11 @@ from core.views.common import AddView, ChangeView, DeleteView, IndexView
from itam.forms.device_softwareadd import SoftwareAdd
from itam.forms.device_softwareupdate import SoftwareUpdate
from itam.forms.device.device import DeviceForm
from itam.forms.device.device import DetailForm, DeviceForm
from itam.forms.device.operating_system import Update as OperatingSystemForm
from itim.models.services import Service
from settings.models.user_settings import UserSettings
@ -78,7 +79,7 @@ class View(ChangeView):
template_name = 'itam/device.html.j2'
form_class = DeviceForm
form_class = DetailForm
context_object_name = "device"
@ -104,6 +105,8 @@ class View(ChangeView):
context['operating_system'] = OperatingSystemForm(prefix='operating_system')
context['services'] = Service.objects.filter(device=self.kwargs['pk'])
softwares = DeviceSoftware.objects.filter(device=self.kwargs['pk'])
softwares = Paginator(softwares, 10)

View File

@ -2,7 +2,7 @@ from django.contrib.auth import decorators as auth_decorator
from django.urls import reverse
from django.utils.decorators import method_decorator
from itam.forms.device_model import DeviceModelForm
from itam.forms.device_model import DetailForm, DeviceModelForm
from itam.models.device_models import DeviceModel
from core.views.common import AddView, ChangeView, DeleteView
@ -11,7 +11,7 @@ from settings.models.user_settings import UserSettings
class View(ChangeView):
class Change(ChangeView):
form_class = DeviceModelForm
@ -20,13 +20,40 @@ class View(ChangeView):
model = DeviceModel
permission_required = [
'itam.view_devicemodel',
'itam.change_devicemodel',
]
template_name = 'form.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = self.object.name
return context
def get_success_url(self, **kwargs):
return reverse('Settings:_device_model_view', args=(self.kwargs['pk'],))
class View(ChangeView):
form_class = DetailForm
context_object_name = "device_model"
model = DeviceModel
permission_required = [
'itam.view_devicemodel',
]
template_name = 'itam/device_model.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)

View File

@ -5,48 +5,11 @@ from django.utils.decorators import method_decorator
from core.views.common import AddView, ChangeView, DeleteView, IndexView
from itam.models.device import DeviceType
from itam.forms.device_type import DeviceTypeForm
from itam.forms.device_type import DetailForm, DeviceTypeForm
class View(ChangeView):
form_class = DeviceTypeForm
model = DeviceType
permission_required = [
'itam.view_devicetype',
'itam.change_devicetype'
]
template_name = 'form.html.j2'
context_object_name = "device_category"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['model_delete_url'] = reverse('Settings:_device_type_delete', args=(self.kwargs['pk'],))
context['content_title'] = self.object.name
return context
def get_success_url(self, **kwargs):
return reverse('Settings:_device_type_view', args=(self.kwargs['pk'],))
@method_decorator(auth_decorator.permission_required("itam.change_devicetype", raise_exception=True))
def post(self, request, *args, **kwargs):
return super().post(request, *args, **kwargs)
class Add(AddView):
form_class = DeviceTypeForm
@ -73,6 +36,36 @@ class Add(AddView):
return context
class Change(ChangeView):
form_class = DeviceTypeForm
model = DeviceType
permission_required = [
'itam.change_devicetype'
]
template_name = 'form.html.j2'
context_object_name = "device_category"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['model_delete_url'] = reverse('Settings:_device_type_delete', args=(self.kwargs['pk'],))
context['content_title'] = self.object.name
return context
def get_success_url(self, **kwargs):
return reverse('Settings:_device_type_view', args=(self.kwargs['pk'],))
class Delete(DeleteView):
model = DeviceType
@ -98,3 +91,39 @@ class Delete(DeleteView):
context['content_title'] = 'Delete ' + self.object.name
return context
class View(ChangeView):
form_class = DetailForm
model = DeviceType
permission_required = [
'itam.view_devicetype',
]
template_name = 'itam/device_type.html.j2'
context_object_name = "device_category"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['model_delete_url'] = reverse('Settings:_device_type_delete', args=(self.kwargs['pk'],))
context['content_title'] = self.object.name
return context
def get_success_url(self, **kwargs):
return reverse('Settings:_device_type_view', args=(self.kwargs['pk'],))
@method_decorator(auth_decorator.permission_required("itam.change_devicetype", raise_exception=True))
def post(self, request, *args, **kwargs):
return super().post(request, *args, **kwargs)

View File

@ -9,11 +9,120 @@ from core.views.common import AddView, ChangeView, DeleteView, IndexView
from itam.models.device import DeviceOperatingSystem
from itam.models.operating_system import OperatingSystem, OperatingSystemVersion
from itam.forms.operating_system.update import OperatingSystemFormCommon, Update
from itam.forms.operating_system.update import DetailForm, OperatingSystemForm
from settings.models.user_settings import UserSettings
class Add(AddView):
form_class = OperatingSystemForm
model = OperatingSystem
permission_required = [
'itam.add_operatingsystem',
]
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('ITAM:Operating Systems')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Add Operating System'
return context
class Change(ChangeView):
context_object_name = "operating_system"
form_class = OperatingSystemForm
model = OperatingSystem
permission_required = [
'itam.change_operatingsystem',
]
template_name = 'form.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = self.object.name
return context
@method_decorator(auth_decorator.permission_required("itam.change_operatingsystem", raise_exception=True))
def post(self, request, *args, **kwargs):
operatingsystem = OperatingSystem.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 = operatingsystem.organization
notes.instance.operatingsystem = operatingsystem
notes.instance.usercreated = request.user
notes.save()
return super().post(request, *args, **kwargs)
def get_success_url(self, **kwargs):
return reverse('ITAM:_operating_system_view', args=(self.kwargs['pk'],))
class Delete(DeleteView):
model = OperatingSystem
permission_required = [
'itam.delete_operatingsystem',
]
template_name = 'form.html.j2'
def get_success_url(self, **kwargs):
return reverse('ITAM:Operating Systems')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['model_pk'] = self.kwargs['pk']
context['model_name'] = self.model._meta.verbose_name.replace(' ', '')
context['content_title'] = 'Delete ' + self.object.name
return context
class IndexView(IndexView):
model = OperatingSystem
permission_required = [
@ -48,13 +157,12 @@ class View(ChangeView):
context_object_name = "operating_system"
form_class = Update
form_class = DetailForm
model = OperatingSystem
permission_required = [
'itam.view_operatingsystem',
'itam.change_operatingsystem',
]
template_name = 'itam/operating_system.html.j2'
@ -96,7 +204,7 @@ class View(ChangeView):
return context
@method_decorator(auth_decorator.permission_required("itam.change_operatingsystem", raise_exception=True))
# @method_decorator(auth_decorator.permission_required("itam.change_operatingsystem", raise_exception=True))
def post(self, request, *args, **kwargs):
operatingsystem = OperatingSystem.objects.get(pk=self.kwargs['pk'])
@ -117,65 +225,3 @@ class View(ChangeView):
def get_success_url(self, **kwargs):
return reverse('ITAM:_operating_system_view', args=(self.kwargs['pk'],))
class Add(AddView):
form_class = OperatingSystemFormCommon
model = OperatingSystem
permission_required = [
'itam.add_operatingsystem',
]
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('ITAM:Operating Systems')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Add Operating System'
return context
class Delete(DeleteView):
model = OperatingSystem
permission_required = [
'itam.delete_operatingsystem',
]
template_name = 'form.html.j2'
def get_success_url(self, **kwargs):
return reverse('ITAM:Operating Systems')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['model_pk'] = self.kwargs['pk']
context['model_name'] = self.model._meta.verbose_name.replace(' ', '')
context['content_title'] = 'Delete ' + self.object.name
return context

View File

@ -9,7 +9,7 @@ from core.views.common import AddView, ChangeView, DeleteView, IndexView
from itam.models.device import DeviceSoftware
from itam.models.software import Software, SoftwareVersion
from itam.forms.software.update import SoftwareForm, SoftwareFormUpdate
from itam.forms.software.update import DetailForm, SoftwareForm, SoftwareChange
from settings.models.user_settings import UserSettings
@ -51,11 +51,39 @@ class IndexView(IndexView):
class Change(ChangeView):
model = Software
permission_required = [
'itam.change_software',
]
template_name = 'form.html.j2'
form_class = SoftwareChange
def get_success_url(self, **kwargs):
return reverse('ITAM:_software_view', args=(self.kwargs['pk'],))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Edit ' + self.object.name
return context
class View(ChangeView):
context_object_name = "software"
form_class = SoftwareFormUpdate
form_class = DetailForm
model = Software

View File

@ -4,13 +4,13 @@ from django.utils.decorators import method_decorator
from core.views.common import AddView, ChangeView, DeleteView
from itam.forms.software_category import SoftwareCategoryForm
from itam.forms.software_category import DetailForm, SoftwareCategoryForm
from itam.models.software import Software, SoftwareCategory
from settings.models.user_settings import UserSettings
class View(ChangeView):
class Change(ChangeView):
context_object_name = "software"
@ -19,7 +19,6 @@ class View(ChangeView):
model = SoftwareCategory
permission_required = [
'itam.view_softwarecategory',
'itam.change_softwarecategory',
]
@ -48,6 +47,38 @@ class View(ChangeView):
class View(ChangeView):
context_object_name = "software"
form_class = DetailForm
model = SoftwareCategory
permission_required = [
'itam.view_softwarecategory',
]
template_name = 'itam/software_categories.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['model_delete_url'] = reverse('Settings:_software_category_delete', args=(self.kwargs['pk'],))
context['content_title'] = self.object.name
return context
def get_success_url(self, **kwargs):
return reverse('Settings:_software_category_view', args=(self.kwargs['pk'],))
class Add(AddView):
form_class = SoftwareCategoryForm

View File

@ -0,0 +1,94 @@
from django import forms
from django.forms import ValidationError
from django.urls import reverse
from itim.models.clusters import ClusterType
from app import settings
from core.forms.common import CommonModelForm
class ClusterTypeForm(CommonModelForm):
class Meta:
fields = '__all__'
model = ClusterType
prefix = 'cluster_type'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class DetailForm(ClusterTypeForm):
tabs: dict = {
"details": {
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'name',
'organization',
'c_created',
'c_modified'
],
"right": [
'model_notes',
]
},
{
"layout": "single",
"name": "Configuration",
"fields": [
'config'
],
"json": [
'config',
]
}
]
},
"notes": {
"name": "Notes",
"slug": "notes",
"sections": []
}
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['c_created'] = forms.DateTimeField(
label = 'Created',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.created,
)
self.fields['c_modified'] = forms.DateTimeField(
label = 'Modified',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.modified,
)
self.tabs['details'].update({
"edit_url": reverse('Settings:_cluster_type_change', args=(self.instance.pk,))
})
self.url_index_view = reverse('Settings:_cluster_types')

152
app/itim/forms/clusters.py Normal file
View File

@ -0,0 +1,152 @@
from django import forms
from django.forms import ValidationError
from django.urls import reverse
from itim.models.clusters import Cluster
from app import settings
from core.forms.common import CommonModelForm
class ClusterForm(CommonModelForm):
class Meta:
fields = '__all__'
model = Cluster
prefix = 'cluster'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['parent_cluster'].queryset = self.fields['parent_cluster'].queryset.exclude(
id=self.instance.pk
)
self.fields['devices'].queryset = self.fields['devices'].queryset.exclude(
is_virtual=False
)
def clean(self):
cleaned_data = super().clean()
pk = self.instance.id
parent_cluster = cleaned_data.get("parent_cluster")
if pk:
if parent_cluster == pk:
raise ValidationError("Cluster can't have itself as its parent cluster")
return cleaned_data
class DetailForm(ClusterForm):
tabs: dict = {
"details": {
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'parent_cluster',
'cluster_type',
'name',
'organization',
'c_created',
'c_modified'
],
"right": [
'model_notes',
'resources',
]
},
]
},
"rendered_config": {
"name": "Rendered Config",
"slug": "rendered_config",
"sections": [
{
"layout": "single",
"fields": [
'rendered_config',
],
"json": [
'rendered_config'
]
}
]
},
"notes": {
"name": "Notes",
"slug": "notes",
"sections": []
}
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# self.fields['config_variables'] = forms.fields.JSONField(
# widget = forms.Textarea(
# attrs = {
# "cols": "80",
# "rows": "100"
# }
# ),
# label = 'Rendered Configuration',
# initial = self.instance.config_variables,
# )
self.fields['c_created'] = forms.DateTimeField(
label = 'Created',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.created,
)
self.fields['c_modified'] = forms.DateTimeField(
label = 'Modified',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.modified,
)
self.fields['resources'] = forms.CharField(
label = 'Available Resources',
disabled = True,
initial = 'xx/yy CPU, xx/yy RAM, xx/yy Storage',
)
self.fields['rendered_config'] = forms.fields.JSONField(
label = 'Available Resources',
disabled = True,
initial = self.instance.rendered_config,
)
self.tabs['details'].update({
"edit_url": reverse('ITIM:_cluster_change', args=(self.instance.pk,))
})
self.url_index_view = reverse('ITIM:Clusters')

88
app/itim/forms/ports.py Normal file
View File

@ -0,0 +1,88 @@
from django import forms
from django.urls import reverse
from app import settings
from itim.models.services import Port
from core.forms.common import CommonModelForm
from settings.models.user_settings import UserSettings
class PortForm(CommonModelForm):
class Meta:
fields = '__all__'
model = Port
prefix = 'port'
class DetailForm(PortForm):
tabs: dict = {
"details": {
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'number',
'description',
'protocol',
'organization',
'c_created',
'c_modified',
'lastinventory',
],
"right": [
'model_notes',
]
}
]
},
"services": {
"name": "Services",
"slug": "services",
"sections": []
},
"notes": {
"name": "Notes",
"slug": "notes",
"sections": []
},
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['c_created'] = forms.DateTimeField(
label = 'Created',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.created,
)
self.fields['c_modified'] = forms.DateTimeField(
label = 'Modified',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.modified,
)
self.tabs['details'].update({
"edit_url": reverse('Settings:_port_change', args=(self.instance.pk,))
})
self.url_index_view = reverse('Settings:_ports')

170
app/itim/forms/services.py Normal file
View File

@ -0,0 +1,170 @@
from django import forms
from django.forms import ValidationError
from django.urls import reverse
from itim.models.services import Service
from app import settings
from core.forms.common import CommonModelForm
class ServiceForm(CommonModelForm):
class Meta:
fields = '__all__'
model = Service
prefix = 'service'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['dependent_service'].queryset = self.fields['dependent_service'].queryset.exclude(
id=self.instance.pk
).exclude(
is_template=True
)
self.fields['template'].queryset = self.fields['template'].queryset.exclude(
id=self.instance.pk
)
def clean(self):
cleaned_data = super().clean()
pk = self.instance.id
dependent_service = cleaned_data.get("dependent_service")
device = cleaned_data.get("device")
cluster = cleaned_data.get("cluster")
config_key_variable = cleaned_data.get("config_key_variable")
is_template = cleaned_data.get("is_template")
template = cleaned_data.get("template")
port = cleaned_data.get("port")
if not is_template and not template:
if not device and not cluster:
raise ValidationError('A Service must be assigned to either a "Cluster" or a "Device".')
if device and cluster:
raise ValidationError('A Service must only be assigned to either a "Cluster" or a "Device". Not both.')
if not port:
raise ValidationError('Port(s) must be assigned to a service.')
if not is_template and not config_key_variable:
raise ValidationError('Configuration Key must be specified')
if dependent_service:
for dependency in dependent_service:
query = Service.objects.filter(
dependent_service = pk,
id = dependency.id,
)
if query.exists():
raise ValidationError('A dependent service already depends upon this service. Circular dependencies are not allowed.')
return cleaned_data
class DetailForm(ServiceForm):
tabs: dict = {
"details": {
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'name',
'config_key_variable',
'template',
'organization',
'c_created',
'c_modified'
],
"right": [
'model_notes',
]
}
]
},
"rendered_config": {
"name": "Rendered Config",
"slug": "rendered_config",
"sections": [
{
"layout": "single",
"fields": [
'config_variables',
],
"json": [
'config_variables'
]
}
]
}
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['config_variables'] = forms.fields.JSONField(
widget = forms.Textarea(
attrs = {
"cols": "80",
"rows": "100"
}
),
label = 'Rendered Configuration',
initial = self.instance.config_variables,
)
self.fields['c_created'] = forms.DateTimeField(
label = 'Created',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.created,
)
self.fields['c_modified'] = forms.DateTimeField(
label = 'Modified',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.modified,
)
self.tabs['details'].update({
"edit_url": reverse('ITIM:_service_change', args=(self.instance.pk,))
})
self.url_index_view = reverse('ITIM:Services')

View File

@ -0,0 +1,104 @@
# Generated by Django 5.0.7 on 2024-08-17 08:05
import access.fields
import access.models
import django.db.models.deletion
import django.utils.timezone
import itim.models.services
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('access', '0001_initial'),
('itam', '0003_alter_device_options_alter_devicemodel_options_and_more'),
]
operations = [
migrations.CreateModel(
name='ClusterType',
fields=[
('is_global', models.BooleanField(default=False)),
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('name', models.CharField(help_text='Name of the Cluster Type', max_length=50, verbose_name='Name')),
('slug', access.fields.AutoSlugField()),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
],
options={
'verbose_name': 'Cluster Type',
'verbose_name_plural': 'Cluster Types',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Cluster',
fields=[
('is_global', models.BooleanField(default=False)),
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('name', models.CharField(help_text='Name of the Cluster', max_length=50, verbose_name='Name')),
('slug', access.fields.AutoSlugField()),
('config', models.JSONField(blank=True, default=None, help_text='Cluster Configuration', null=True, verbose_name='Configuration')),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('devices', models.ManyToManyField(blank=True, default=None, help_text='Devices that are deployed upon the cluster.', related_name='cluster_device', to='itam.device', verbose_name='Devices')),
('nodes', models.ManyToManyField(blank=True, default=None, help_text='Hosts for resource consumption that the cluster is deployed upon', related_name='cluster_node', to='itam.device', verbose_name='Nodes')),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
('parent_cluster', models.ForeignKey(blank=True, default=None, help_text='Parent Cluster for this cluster', null=True, on_delete=django.db.models.deletion.CASCADE, to='itim.cluster', verbose_name='Parent Cluster')),
('cluster_type', models.ForeignKey(blank=True, default=None, help_text='Type of Cluster', null=True, on_delete=django.db.models.deletion.CASCADE, to='itim.clustertype', verbose_name='Cluster Type')),
],
options={
'verbose_name': 'Cluster',
'verbose_name_plural': 'Clusters',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Port',
fields=[
('is_global', models.BooleanField(default=False)),
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('number', models.IntegerField(help_text='The port number', validators=[itim.models.services.Port.validation_port_number], verbose_name='Port Number')),
('description', models.CharField(blank=True, default=None, help_text='Short description of port', max_length=80, null=True, verbose_name='Description')),
('protocol', models.CharField(choices=[('TCP', 'TCP'), ('UDP', 'UDP')], default='TCP', help_text='Layer 4 Network Protocol', max_length=3, verbose_name='Protocol')),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
],
options={
'verbose_name': 'Protocol',
'verbose_name_plural': 'Protocols',
'ordering': ['number', 'protocol'],
},
),
migrations.CreateModel(
name='Service',
fields=[
('is_global', models.BooleanField(default=False)),
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('is_template', models.BooleanField(default=False, help_text='Is this service to be used as a template', verbose_name='Template')),
('name', models.CharField(help_text='Name of the Service', max_length=50, verbose_name='Name')),
('config', models.JSONField(blank=True, default=None, help_text='Cluster Configuration', null=True, verbose_name='Configuration')),
('config_key_variable', models.CharField(help_text='Key name to use when merging with cluster/device config.', max_length=50, null=True, validators=[itim.models.services.Service.validate_config_key_variable], verbose_name='Configuration Key')),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('cluster', models.ForeignKey(blank=True, default=None, help_text='Cluster the service is assigned to', null=True, on_delete=django.db.models.deletion.CASCADE, to='itim.cluster', verbose_name='Cluster')),
('dependent_service', models.ManyToManyField(blank=True, default=None, help_text='Services that this service depends upon', related_name='dependentservice', to='itim.service', verbose_name='Dependent Services')),
('device', models.ForeignKey(blank=True, default=None, help_text='Device the service is assigned to', null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.device', verbose_name='Device')),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
('port', models.ManyToManyField(blank=True, help_text='Port the service is available on', to='itim.port', verbose_name='Port')),
('template', models.ForeignKey(blank=True, default=None, help_text='Template this service uses', null=True, on_delete=django.db.models.deletion.CASCADE, to='itim.service', verbose_name='Template Name')),
],
options={
'verbose_name': 'Service',
'verbose_name_plural': 'Services',
'ordering': ['name'],
},
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 5.0.7 on 2024-08-18 03:57
import access.fields
import django.utils.timezone
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('itim', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='clustertype',
name='created',
field=access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False),
),
migrations.AddField(
model_name='clustertype',
name='modified',
field=access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False),
),
]

Some files were not shown because too many files have changed in this diff Show More