Compare commits

...

89 Commits

Author SHA1 Message Date
dcba456af3 build: bump version 1.0.0 -> 1.1.0 2024-08-23 08:26:37 +00:00
Jon
6d98006a37 Merge pull request #210 from nofusscomputing/v1-1 2024-08-23 17:47:59 +09:30
Jon
4ac0c157bc feat(itim): Dont attempt to apply cluster type config if no type specified.
ref: #247 #71
2024-08-23 17:45:44 +09:30
Jon
e696129f0b feat(itim): Service config rendered as part of cluster config
ref: #247 #125 closes #69
2024-08-23 17:45:44 +09:30
Jon
cf5c512a64 feat(itim): dont force config key, validate when it's required
ref: #247 #69
2024-08-23 17:45:44 +09:30
Jon
32f45f2d5f feat(itim): Services assignable to cluster
ref: #247 #125
2024-08-23 17:45:44 +09:30
Jon
66b8bd5a74 feat(itim): Ability to add configuration to cluster type
ref: #247 closes #71
2024-08-23 17:45:44 +09:30
Jon
bfb20dab0f feat(itim): Ability to add external link to cluster
ref: #244 #71 #6
2024-08-23 17:45:44 +09:30
Jon
6b28569bca fix(settings): return the rendering of external links to models
ref: #244
2024-08-23 17:45:44 +09:30
Jon
79b2c668fa docs(itim): cluster user docs
ref: #244 #71
2024-08-23 17:45:44 +09:30
Jon
5d660694c3 refactor(itim): Add Cluster type to index page
ref: #244 #71
2024-08-23 17:45:44 +09:30
Jon
e70d0392c0 chore(itim): Add Cluster icon to navigation
ref: #244 #71
2024-08-23 17:45:44 +09:30
Jon
caa47a3bb6 test(itim): Cluster Types unit tests
ref: #244 #71
2024-08-23 17:45:44 +09:30
Jon
75203c022a feat(itim): Ability to add and configure Cluster Types
ref: #244 #71
2024-08-23 17:45:44 +09:30
Jon
b65e577017 chore(itim): add placeholder for assigning service to a cluster
ref: #244 #71 #125
2024-08-23 17:45:44 +09:30
Jon
45ef81481f test(itim): Cluster unit tests
ref: #244 #71
2024-08-23 17:45:44 +09:30
Jon
f9dee4465b feat(itim): Add cluster to history save
ref: #244 #71
2024-08-23 17:45:44 +09:30
Jon
8ec1ea2a4c feat(itim): prevent cluster from setting itself as parent
ref: #244 #71
2024-08-23 17:45:44 +09:30
Jon
17df9d1fa3 fix(core): Ensure when saving history json is correctly formatted
ref: #244
2024-08-23 17:45:44 +09:30
Jon
24967ae3a6 fix(itim): Fix name typo in Add Service button
ref: #244
2024-08-23 17:45:44 +09:30
Jon
30bd8aa483 feat(itim): Ability to add and configure cluster
ref: #244 #71
2024-08-23 17:45:44 +09:30
Jon
efce9c0219 chore: update migrations from previous days work
ref: #244 #71 #245
2024-08-23 17:45:44 +09:30
Jon
0020550dde feat(itam): Track if device is virtual
ref: #244 closes #245
2024-08-23 17:45:44 +09:30
Jon
04a9cde47e feat(api): Endpoint to fetch user permissions
ref: #244 closes #164
2024-08-23 17:45:44 +09:30
Jon
e472022c91 feat(development): Add function to filter permissions to those used by centurion
ref: #244 #164
2024-08-23 17:45:44 +09:30
Jon
d778cd0e83 feat(development): Add new template tag choice_ids for string list casting
Ref: #244 closes #243
2024-08-23 17:45:44 +09:30
Jon
1f76da8709 refactor(itam): Knowledge Base now uses details template
#242 closes #231
2024-08-23 17:45:44 +09:30
Jon
7ddc0abce6 test(itam): Correct Device Type Model permissions test to use "change" view
#242 #234
2024-08-23 17:45:44 +09:30
Jon
a2af58ae09 refactor(itam): Device Type now uses details template
#242 closes #234
2024-08-23 17:45:44 +09:30
Jon
8e71bb932e test(itam): Correct Operating System Model permissions test to use "change" view
#242 #229
2024-08-23 17:45:44 +09:30
Jon
8c1f033b1c refactor(itam): Operating System now uses details template
#242 closes #229
2024-08-23 17:45:44 +09:30
Jon
eb4df77614 refactor(itim): Service Port now uses details template
#242 closes #238
2024-08-23 17:45:44 +09:30
Jon
fed6eee951 test(config_management): Correct Device Model permissions test to use "change" view
#242 #235
2024-08-23 17:45:44 +09:30
Jon
6a0b507c3b refactor(itam): Device Model now uses details template
#242 closes #235
2024-08-23 17:45:44 +09:30
Jon
47b2e61987 test(config_management): Correct Config Group permissions test to use "change" view
#242 #230
2024-08-23 17:45:44 +09:30
Jon
28259b329e refactor(config_management): Config Groups now uses details template
#242 closes #230
2024-08-23 17:45:44 +09:30
Jon
4391aa3ea8 test(itam): Correct Software Category permissions test to use "change" view
#242 #236
2024-08-23 17:45:44 +09:30
Jon
4a4c8e94e4 refactor(itam): Software Categories now uses details template
#242 closes #236
2024-08-23 17:45:44 +09:30
Jon
d41cc312bb test(core): Correct manufacturer permissions test to use "change" view
.#242 #232
2024-08-23 17:45:44 +09:30
Jon
12abc741d2 refactor(itam): manufacturer now uses details template
#242 closes #232
2024-08-23 17:45:44 +09:30
Jon
a8262e0a54 test(itam): Correct software permissions test to use "change" view
#240 #233
2024-08-23 17:45:44 +09:30
Jon
2011c212ba refactor(itam): software now uses details template
#240 closes #233
2024-08-23 17:45:44 +09:30
Jon
564871ca3c chore(itam): remove commented code from device form
#240 closes #227
2024-08-23 17:45:44 +09:30
Jon
95bb15238a fix: Ensure tenancy models have Meta.verbose_name_plural attribute
.#240 closes #239
2024-08-23 17:45:44 +09:30
Jon
cafc5ce6e2 test(model): test for checking if Meta sub-class has variable verbose_name_plural
#239 #240
2024-08-23 17:45:44 +09:30
Jon
68c3b64424 fix(base): Use correct url for back button
.#227 #240
2024-08-23 17:45:44 +09:30
Jon
300fe283d6 refactor(itam): device now use details template
#227 #240
2024-08-23 17:45:44 +09:30
Jon
ac6408c3bb feat(development): Render model_name_plural as part of back button
#227 #239
2024-08-23 17:45:44 +09:30
Jon
750e323947 feat(development): add to form field model_name_plural
#227 #239
2024-08-23 17:45:44 +09:30
Jon
4cca9d9904 feat(development): render heading if section included
#227
2024-08-23 17:45:44 +09:30
Jon
955081f155 chore: add Merge/Pull request template
#226
2024-08-23 17:45:44 +09:30
Jon
01e47c889b docs(roadmap): update completed features
#226
2024-08-23 17:45:43 +09:30
Jon
2cd4d387a7 docs(base): detail view template
. #24 #226 closes #22
2024-08-23 17:45:43 +09:30
Jon
ea8c60ccc5 refactor(itim): services now use details template
. #22 #226
2024-08-23 17:45:43 +09:30
Jon
4ecf5236c1 feat(base): create detail view templates
purpose is to aid in the development of a detail form

#22 #24 #226
2024-08-23 17:45:43 +09:30
Jon
eb919f2d5e docs: initial adding of template page
#22
2024-08-23 17:45:43 +09:30
Jon
485dd43b58 chore: add services navigation icon
!43 #69
2024-08-23 17:45:43 +09:30
Jon
fd4da657fb fix(itim): ensure that the service template config is also rendered as part of device config
!43 #69
2024-08-23 17:45:43 +09:30
Jon
acc6879fb1 docs: fluff the port and services
!43 closes #69
2024-08-23 17:45:43 +09:30
Jon
d339fdb645 fix(itim): dont render link if no device
!43 #69
2024-08-23 17:45:43 +09:30
Jon
53a720a802 feat(itam): Render Service Config with device config
!43 #69
2024-08-23 17:45:43 +09:30
Jon
0b04cdcfbf feat(itam): Display deployed services for devices
!43 #69
2024-08-23 17:45:43 +09:30
Jon
b5d2fe70ff feat(itim): Prevent circular service dependencies
!43 #69
2024-08-23 17:45:43 +09:30
Jon
6d6f1c5401 feat(itim): Port number validation to check for valid port numbers
!43 #69
2024-08-23 17:45:43 +09:30
Jon
7b8b8a6394 feat(itim): Prevent Service template from being assigned as dependent service
!43 #69
2024-08-23 17:45:43 +09:30
Jon
2a3373a19b feat(itim): Add service template support
!43 #69
2024-08-23 17:45:43 +09:30
Jon
eb320c4e95 fix(itim): Dont show self within service dependencies
!43 #69
2024-08-23 17:45:43 +09:30
Jon
0b220424bb feat(itim): Ports for service management
!43 #69
2024-08-23 17:45:43 +09:30
Jon
a948ec7bd7 feat(itim): Service Management
!43 #69
2024-08-23 17:45:43 +09:30
Jon
56196f721d fix(assistance): Only return distinct values when limiting KB articles
!43 #10
2024-08-23 17:45:43 +09:30
Jon
3d06112860 docs(assistance): document kb categories for user
!43 closes #10
2024-08-23 17:45:43 +09:30
Jon
05484d9e02 feat(assistance): Filter KB articles to target user
only intended to filter for users whom dont have change perm.

!43 #10
2024-08-23 17:45:43 +09:30
Jon
b73807a140 feat(assistance): Add date picker to date fields for KB articles
!43 #10
2024-08-23 17:45:43 +09:30
Jon
215c5e464c feat(assistance): Dont display expired articles for "view" users
!43 #10
2024-08-23 17:45:43 +09:30
Jon
cf2dce320c docs(assistance): document kb for user
!43 #10
2024-08-23 17:45:43 +09:30
Jon
32cdcc38b5 feat(base): add code highlighting to markdown
!43 #10
2024-08-23 17:45:43 +09:30
Jon
4b3ea06f70 feat(assistance): Categorised Knowledge base articles
!43 #10
2024-08-23 17:45:43 +09:30
Jon
2e7a6a42b4 docs(assistance): added pages for knowledgebase
!43 #10
2024-08-23 17:45:43 +09:30
Jon
be0ec86c48 chore(base): rename information -> assistance
!43 #10
2024-08-23 17:45:43 +09:30
Jon
8c493e8fa3 feat(itim): Add menu entry
!43 #69 #71
2024-08-23 17:45:43 +09:30
Jon
9668e811c5 feat(itam): Ability to add device configuration
!43 fixes #44
2024-08-23 17:45:43 +09:30
Jon
28ce99f46a test(external_link): add tests
!43 fixes #6
2024-08-23 17:45:43 +09:30
Jon
9b4dbc58f3 feat(settings): New model to allow adding templated links to devices and software
!43 #6
2024-08-23 17:45:43 +09:30
Jon
f295f15034 docs: move settings pages into sub-directory
!43 #6
2024-08-23 17:45:43 +09:30
4c41994068 build: bump version 1.0.0-b14 -> 1.0.0 2024-08-23 08:03:31 +00:00
Jon
6f7b3ffad6 chore: add github pr template
ref: #213
2024-08-20 14:21:00 +09:30
Jon
cc97128e25 ci: add mkdocs workflow
ref: #213
2024-08-20 12:28:43 +09:30
Jon
b6ba3d38dc docs: migrate project links to github
ref: #213
2024-08-20 12:08:59 +09:30
Jon
18b788844a ci: update gitlab-ci to current head
ref: #209
2024-08-19 16:53:31 +09:30
201 changed files with 10977 additions and 1020 deletions

View File

@ -17,5 +17,5 @@ commitizen:
prerelease_offset: 1
tag_format: $version
update_changelog_on_bump: false
version: 1.0.0-b14
version: 1.1.0
version_scheme: semver

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

@ -16,6 +16,17 @@ env:
jobs:
mkdocs:
name: 'MKDocs'
permissions:
pull-requests: write
contents: write
statuses: write
checks: write
actions: write
uses: nofusscomputing/action_mkdocs/.github/workflows/reusable_mkdocs.yaml@development
docker:
name: 'Docker'
uses: nofusscomputing/action_docker/.github/workflows/docker.yaml@development

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

@ -1,3 +1,85 @@
## 1.1.0 (2024-08-23)
### feat
- **itim**: Dont attempt to apply cluster type config if no type specified.
- **itim**: Service config rendered as part of cluster config
- **itim**: dont force config key, validate when it's required
- **itim**: Services assignable to cluster
- **itim**: Ability to add configuration to cluster type
- **itim**: Ability to add external link to cluster
- **itim**: Ability to add and configure Cluster Types
- **itim**: Add cluster to history save
- **itim**: prevent cluster from setting itself as parent
- **itim**: Ability to add and configure cluster
- **itam**: Track if device is virtual
- **api**: Endpoint to fetch user permissions
- **development**: Add function to filter permissions to those used by centurion
- **development**: Add new template tag `choice_ids` for string list casting
- **development**: Render `model_name_plural` as part of back button
- **development**: add to form field `model_name_plural`
- **development**: render heading if section included
- **base**: create detail view templates
- **itam**: Render Service Config with device config
- **itam**: Display deployed services for devices
- **itim**: Prevent circular service dependencies
- **itim**: Port number validation to check for valid port numbers
- **itim**: Prevent Service template from being assigned as dependent service
- **itim**: Add service template support
- **itim**: Ports for service management
- **itim**: Service Management
- **assistance**: Filter KB articles to target user
- **assistance**: Add date picker to date fields for KB articles
- **assistance**: Dont display expired articles for "view" users
- **base**: add code highlighting to markdown
- **assistance**: Categorised Knowledge base articles
- **itim**: Add menu entry
- **itam**: Ability to add device configuration
- **settings**: New model to allow adding templated links to devices and software
### Fixes
- **settings**: return the rendering of external links to models
- **core**: Ensure when saving history json is correctly formatted
- **itim**: Fix name typo in Add Service button
- Ensure tenancy models have `Meta.verbose_name_plural` attribute
- **base**: Use correct url for back button
- **itim**: ensure that the service template config is also rendered as part of device config
- **itim**: dont render link if no device
- **itim**: Dont show self within service dependencies
- **assistance**: Only return distinct values when limiting KB articles
### Refactoring
- **itim**: Add Cluster type to index page
- **itam**: Knowledge Base now uses details template
- **itam**: Device Type now uses details template
- **itam**: Operating System now uses details template
- **itim**: Service Port now uses details template
- **itam**: Device Model now uses details template
- **config_management**: Config Groups now uses details template
- **itam**: Software Categories now uses details template
- **itam**: manufacturer now uses details template
- **itam**: software now uses details template
- **itam**: device now use details template
- **itim**: services now use details template
### Tests
- **itim**: Cluster Types unit tests
- **itim**: Cluster unit tests
- **itam**: Correct Device Type Model permissions test to use "change" view
- **itam**: Correct Operating System Model permissions test to use "change" view
- **config_management**: Correct Device Model permissions test to use "change" view
- **config_management**: Correct Config Group permissions test to use "change" view
- **itam**: Correct Software Category permissions test to use "change" view
- **core**: Correct manufacturer permissions test to use "change" view
- **itam**: Correct software permissions test to use "change" view
- **model**: test for checking if Meta sub-class has variable verbose_name_plural
- **external_link**: add tests
## 1.0.0 (2024-08-23)
## 1.0.0-b14 (2024-08-12)
### Fixes

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')

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