Merge branch 'development' into 'master'

chore: release 0.7.0

See merge request nofusscomputing/projects/centurion_erp!35
This commit is contained in:
2024-07-14 06:11:30 +00:00
201 changed files with 3982 additions and 1001 deletions

3
.gitignore vendored
View File

@ -1,7 +1,8 @@
venv/**
*/static/**
__pycache__
**db.sqlite3
**.sqlite3
**.sqlite
**.coverage
artifacts/
**.tmp.*

View File

@ -2,21 +2,21 @@
variables:
MY_PROJECT_ID: "57560288"
GIT_SYNC_URL: "https://$GITHUB_USERNAME_ROBOT:$GITHUB_TOKEN_ROBOT@github.com/NoFussComputing/django_template.git"
GIT_SYNC_URL: "https://$GITHUB_USERNAME_ROBOT:$GITHUB_TOKEN_ROBOT@github.com/NoFussComputing/centurion_erp.git"
# Docker Build / Publish
DOCKER_IMAGE_BUILD_TARGET_PLATFORMS: "linux/amd64,linux/arm64"
DOCKER_IMAGE_BUILD_NAME: django-template
DOCKER_IMAGE_BUILD_NAME: centurion-erp
DOCKER_IMAGE_BUILD_REGISTRY: $CI_REGISTRY_IMAGE
DOCKER_IMAGE_BUILD_TAG: $CI_COMMIT_SHA
# Docker Publish
DOCKER_IMAGE_PUBLISH_NAME: django-template
DOCKER_IMAGE_PUBLISH_NAME: centurion-erp
DOCKER_IMAGE_PUBLISH_REGISTRY: docker.io/nofusscomputing
DOCKER_IMAGE_PUBLISH_URL: https://hub.docker.com/r/nofusscomputing/$DOCKER_IMAGE_PUBLISH_NAME
# Docs NFC
PAGES_ENVIRONMENT_PATH: projects/django-template/
PAGES_ENVIRONMENT_PATH: projects/centurion_erp/
# RELEASE_ADDITIONAL_ACTIONS_BUMP: ./.gitlab/additional_actions_bump.sh
@ -31,6 +31,9 @@ include:
- template/automagic.gitlab-ci.yaml
Update Git Submodules:
extends: .ansible_playbook_git_submodule
Docker Container:
extends: .build_docker_container

View File

@ -49,7 +49,7 @@ Unit:
- artifacts/
environment:
name: Unit Test Coverage Report
url: https://nofusscomputing.gitlab.io/-/projects/django_template/-/jobs/${CI_JOB_ID}/artifacts/artifacts/coverage/index.html
url: https://nofusscomputing.gitlab.io/-/projects/centurion_erp/-/jobs/${CI_JOB_ID}/artifacts/artifacts/coverage/index.html
UI:

17
.vscode/launch.json vendored
View File

@ -15,6 +15,23 @@
"django": true,
"autoStartBrowser": false,
"program": "${workspaceFolder}/app/manage.py"
},
{
"name": "Debug: Celery",
"type": "python",
"request": "launch",
"module": "celery",
"console": "integratedTerminal",
"args": [
"-A",
"app",
"worker",
"-l",
"INFO",
"-n",
"debug-itsm@%h"
],
"cwd": "${workspaceFolder}/app"
}
]
}

View File

@ -13,4 +13,8 @@
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"testing.coverageToolbarEnabled": true,
"cSpell.words": [
"ITSM"
],
"cSpell.language": "en-AU",
}

View File

@ -15,7 +15,7 @@ pip install -r requirements.txt
```
To setup the django test server run the following
To setup the centurion erp test server run the following
``` bash
@ -50,9 +50,9 @@ See [Documentation](https://nofusscomputing.com/projects/django-template/develop
cd app
docker build . --tag django-app:dev
docker build . --tag centurion-erp:dev
docker run -d --rm -v ${PWD}/db.sqlite3:/app/db.sqlite3 -p 8002:8000 --name app django-app:dev
docker run -d --rm -v ${PWD}/db.sqlite3:/app/db.sqlite3 -p 8002:8000 --name app centurion-erp:dev
```

View File

@ -1,21 +1,65 @@
<span style="text-align: center;">
![GitLab Bugs](https://img.shields.io/gitlab/issues/open/nofusscomputing%2Fprojects%2Fdjango_template?labels=type%3A%3Abug&style=plastic&logo=gitlab&label=Bug%20Fixes%20Required&color=fc6d26)
# No Fuss Computing - Centurion ERP
<br>
![Project Status - Active](https://img.shields.io/badge/Project%20Status-Active-green?logo=gitlab&style=plastic)
![GitLab Issues](https://img.shields.io/gitlab/issues/open/nofusscomputing%2Fprojects%2Fdjango_template?style=plastic&logo=gitlab&label=Issues&color=fc6d26)
![Docker Pulls](https://img.shields.io/docker/pulls/nofusscomputing/django-template?style=plastic&logo=docker&color=0db7ed)
![Gitlab Code Coverage](https://img.shields.io/gitlab/pipeline-coverage/nofusscomputing%2Fprojects%2Fdjango_template?branch=master&style=plastic&logo=gitlab&label=Test%20Coverage)
[![Docker Pulls](https://img.shields.io/docker/pulls/nofusscomputing/centurion-erp?style=plastic&logo=docker&color=0db7ed)](https://hub.docker.com/r/nofusscomputing/centurion-erp)
artifacts
----
<br>
![Gitlab forks count](https://img.shields.io/badge/dynamic/json?label=Forks&query=%24.forks_count&url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F57560288%2F&color=ff782e&logo=gitlab&style=plastic) ![Gitlab stars](https://img.shields.io/badge/dynamic/json?label=Stars&query=%24.star_count&url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F57560288%2F&color=ff782e&logo=gitlab&style=plastic) [![Open Issues](https://img.shields.io/badge/dynamic/json?color=ff782e&logo=gitlab&style=plastic&label=Open%20Issues&query=%24.statistics.counts.opened&url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F57560288%2Fissues_statistics)](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues) [![GitLab Bugs](https://img.shields.io/gitlab/issues/open/nofusscomputing%2Fprojects%2Fcenturion_erp?labels=type%3A%3Abug&style=plastic&logo=gitlab&label=Bug%20Fixes%20Required&color=fc6d26)](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/?sort=created_date&state=opened&label_name%5B%5D=type%3A%3Abug)
dont work to file
https://gitlab.com/nofusscomputing/projects/django_template/-/jobs/artifacts/master/browse/artifacts/coverage/index.html?job=Unit
works to dir
https://gitlab.com/nofusscomputing/projects/django_template/-/jobs/artifacts/master/browse/artifacts/coverage/?job=Unit
![GitHub forks](https://img.shields.io/github/forks/NofussComputing/centurion_erp?logo=github&style=plastic&color=000000&labell=Forks) ![GitHub stars](https://img.shields.io/github/stars/NofussComputing/centurion_erp?color=000000&logo=github&style=plastic) ![Github Watchers](https://img.shields.io/github/watchers/NofussComputing/centurion_erp?color=000000&label=Watchers&logo=github&style=plastic)
<br>
This project is hosted on [gitlab](https://gitlab.com/nofusscomputing/projects/centurion_erp) and has a read-only copy hosted on [Github](https://github.com/NofussComputing/centurion_erp).
----
**Stable Branch**
![Gitlab build status - stable](https://img.shields.io/badge/dynamic/json?color=ff782e&label=Build&query=0.status&url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F57560288%2Fpipelines%3Fref%3Dmaster&logo=gitlab&style=plastic) ![branch release version](https://img.shields.io/badge/dynamic/yaml?color=ff782e&logo=gitlab&style=plastic&label=Release&query=%24.commitizen.version&url=https%3A//gitlab.com/nofusscomputing/projects/centurion_erp%2F-%2Fraw%2Fmaster%2F.cz.yaml) [![Gitlab Code Coverage](https://img.shields.io/gitlab/pipeline-coverage/nofusscomputing%2Fprojects%2Fcenturion_erp?branch=master&style=plastic&logo=gitlab&label=Test%20Coverage)](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/jobs/artifacts/master/browse/artifacts/coverage/?job=Unit)
----
**Development Branch**
![Gitlab build status - development](https://img.shields.io/badge/dynamic/json?color=ff782e&label=Build&query=0.status&url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F57560288%2Fpipelines%3Fref%3Ddevelopment&logo=gitlab&style=plastic) ![branch release version](https://img.shields.io/badge/dynamic/yaml?color=ff782e&logo=gitlab&style=plastic&label=Release&query=%24.commitizen.version&url=https%3A//gitlab.com/nofusscomputing/projects/centurion_erp%2F-%2Fraw%2Fdevelopment%2F.cz.yaml) [![Gitlab Code Coverage](https://img.shields.io/gitlab/pipeline-coverage/nofusscomputing%2Fprojects%2Fcenturion_erp?branch=development&style=plastic&logo=gitlab&label=Test%20Coverage)](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/jobs/artifacts/development/browse/artifacts/coverage/?job=Unit)
----
<br>
</div>
links:
- [Issues](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues)
- [Merge Requests (Pull Requests)](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests)
An ERP with a large emphasis on the IT Service Management (ITSM) and Automation.
## Contributing
All contributions for this project must conducted from [Gitlab](https://gitlab.com/nofusscomputing/projects/centurion_erp).
For further details on contributing please refer to the [contribution guide](CONTRIBUTING.md).
## Other
This repo is release under this [license](LICENSE)

View File

@ -5,8 +5,9 @@ from app import settings
from access.models import Organization
from core.forms.common import CommonModelForm
class OrganizationForm(forms.ModelForm):
class OrganizationForm(CommonModelForm):
class Meta:
model = Organization

View File

@ -6,8 +6,10 @@ from django.forms import inlineformset_factory
from app import settings
from .team_users import TeamUsersForm, TeamUsers
from access.models import Team
from core.forms.common import CommonModelForm
TeamUserFormSet = inlineformset_factory(
model=TeamUsers,
@ -19,7 +21,19 @@ TeamUserFormSet = inlineformset_factory(
]
)
class TeamForm(forms.ModelForm):
class TeamFormAdd(CommonModelForm):
class Meta:
model = Team
fields = [
'name',
]
class TeamForm(CommonModelForm):
class Meta:
model = Team
@ -55,12 +69,15 @@ class TeamForm(forms.ModelForm):
'access',
'config_management',
'core',
'django_celery_results',
'itam',
'settings',
]
exclude_models = [
'appsettings',
'chordcounter',
'groupresult',
'organization'
'settings',
'usersettings',
@ -68,8 +85,11 @@ class TeamForm(forms.ModelForm):
exclude_permissions = [
'add_organization',
'add_taskresult',
'change_organization',
'change_taskresult',
'delete_organization',
'delete_taskresult',
]
self.fields['permissions'].queryset = Permission.objects.filter(

View File

@ -1,12 +1,12 @@
from django import forms
from django.db.models import Q
from app import settings
from access.models import TeamUsers
from core.forms.common import CommonModelForm
class TeamUsersForm(forms.ModelForm):
class TeamUsersForm(CommonModelForm):
class Meta:
model = TeamUsers

View File

@ -0,0 +1,23 @@
# Generated by Django 5.0.6 on 2024-07-11 04:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0005_organization_manager_organization_model_notes'),
]
operations = [
migrations.AlterField(
model_name='organization',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'),
),
migrations.AlterField(
model_name='team',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'),
),
]

View File

@ -4,8 +4,6 @@ from django.contrib.auth.models import Group
from django.core.exceptions import PermissionDenied
from django.utils.functional import cached_property
from .models import Organization, Team
@ -57,6 +55,8 @@ class OrganizationMixin():
id = obj.get_organization().id
if hasattr(obj, 'is_global'):
if obj.is_global:
id = 0
@ -70,6 +70,18 @@ class OrganizationMixin():
id = int(self.request.POST.get("organization", ""))
for field in self.request.POST.dict(): # cater for fields prefixed '<prefix>-<field name>'
a_field = str(field).split('-')
if len(a_field) == 2:
if a_field[1] == 'organization':
id = int(self.request.POST.get(field))
return id
@ -191,8 +203,26 @@ class OrganizationMixin():
is_organization_manager = False
queryset = None
if hasattr(self, 'get_queryset'):
queryset = self.get_queryset()
obj = None
if hasattr(self, 'get_object'):
try:
obj = self.get_object()
except:
pass
if hasattr(self, 'model'):
if self.model._meta.label_lower in organization_manager_models:
@ -203,11 +233,33 @@ class OrganizationMixin():
is_organization_manager = True
if not self.has_organization_permission() and not request.user.is_superuser and not is_organization_manager:
return False
return True
if request.user.is_superuser:
return True
perms = self.get_permission_required()
if self.has_organization_permission():
return True
if self.request.user.has_perms(perms) and len(self.kwargs) == 0 and str(self.request.method).lower() == 'get':
return True
for required_permission in self.permission_required:
if required_permission.replace(
'view_', ''
) == 'access.organization' and len(self.kwargs) == 0:
return True
return False
class OrganizationPermission(AccessMixin, OrganizationMixin):
@ -276,7 +328,33 @@ class OrganizationPermission(AccessMixin, OrganizationMixin):
if len(self.permission_required) > 0:
non_organization_models = [
'TaskResult'
]
if hasattr(self, 'model'):
if hasattr(self.model, '__name__'):
if self.model.__name__ in non_organization_models:
if hasattr(self, 'get_object'):
self.get_object()
perms = self.get_permission_required()
if not self.request.user.has_perms(perms):
return self.handle_no_permission()
return super().dispatch(self.request, *args, **kwargs)
if not self.permission_check(request):
raise PermissionDenied('You are not part of this organization')
return super().dispatch(self.request, *args, **kwargs)

View File

@ -49,6 +49,7 @@ class Organization(SaveHistory):
blank = True,
default = None,
null= True,
verbose_name = 'Notes',
)
slug = AutoSlugField()
@ -91,6 +92,7 @@ class TenancyObject(models.Model):
blank = True,
default = None,
null= True,
verbose_name = 'Notes',
)
def get_organization(self) -> Organization:

View File

@ -1,6 +1,5 @@
{% extends 'base.html.j2' %}
{% block title %}Organizations{% endblock %}
{% block content_header_icon %}{% endblock %}
{% block content %}

View File

@ -57,7 +57,7 @@ form div .helptext {
<div class="detail-view-field">
<label>{{ form.manager.label }}</label>
<span>{{ form.manager.value }}</span>
<span>{{ organization.manager }}</span>
</div>
<div class="detail-view-field">

View File

@ -8,7 +8,6 @@
{{ form.as_div }}
{% include 'icons/issue_link.html.j2' with issue=13 %}<br>
<input style="display:unset;" type="submit" value="Submit">
</form>
@ -18,7 +17,7 @@
<input type="button" value="<< Back" onclick="window.location='{% url 'Access:_organization_view' pk=organization.id %}';">
<input type="button" value="Delete Team"
onclick="window.location='{% url 'Access:_team_delete' organization_id=organization.id pk=team.id %}';">
<input type="button" value="New User"
<input type="button" value="Assign User"
onclick="window.location='{% url 'Access:_team_user_add' organization_id=organization.id pk=team.id %}';">
{{ formset.management_form }}

View File

@ -0,0 +1,21 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import ModelDisplay, ModelIndex
class OrganizationViews(
TestCase,
ModelDisplay,
ModelIndex
):
display_module = 'access.views.organization'
display_view = 'View'
index_module = display_module
index_view = 'IndexView'

View File

@ -0,0 +1,29 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import ModelAdd, ModelDelete, ModelDisplay
class TeamViews(
TestCase,
ModelAdd,
ModelDelete,
ModelDisplay,
):
add_module = 'access.views.team'
add_view = 'Add'
# change_module = add_module
# change_view = 'Change'
delete_module = add_module
delete_view = 'Delete'
display_module = add_module
display_view = 'View'

View File

@ -0,0 +1,30 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import AddView, DeleteView
class TeamUserViews(
TestCase,
AddView,
DeleteView
):
add_module = 'access.views.user'
add_view = 'Add'
# change_module = add_module
# change_view = 'GroupView'
delete_module = add_module
delete_view = 'Delete'
# display_module = add_module
# display_view = 'GroupView'
# index_module = add_module
# index_view = 'GroupIndexView'

View File

@ -1,4 +1,5 @@
from django.contrib.auth import decorators as auth_decorator
from django.db.models import Q
from django.utils.decorators import method_decorator
from django.views import generic
@ -7,9 +8,12 @@ from access.models import *
from access.forms.organization import OrganizationForm
from core.views.common import ChangeView, IndexView
class IndexView(OrganizationPermission, generic.ListView):
class IndexView(IndexView):
model = Organization
permission_required = [
'access.view_organization'
]
@ -17,6 +21,14 @@ class IndexView(OrganizationPermission, generic.ListView):
context_object_name = "organization_list"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Organizations'
return context
def get_queryset(self):
if self.request.user.is_superuser:
@ -25,11 +37,15 @@ class IndexView(OrganizationPermission, generic.ListView):
else:
return Organization.objects.filter(pk__in=self.user_organizations())
return Organization.objects.filter(
Q(pk__in=self.user_organizations())
|
Q(manager=self.request.user.id)
)
class View(OrganizationPermission, generic.UpdateView):
class View(ChangeView):
context_object_name = "organization"
@ -70,6 +86,8 @@ class View(OrganizationPermission, generic.UpdateView):
context['model_pk'] = self.kwargs['pk']
context['model_name'] = self.model._meta.verbose_name.replace(' ', '')
context['content_title'] = 'Organization - ' + context[self.context_object_name].name
return context

View File

@ -2,16 +2,15 @@ from django.contrib.auth import decorators as auth_decorator
from django.contrib.auth.models import Permission
from django.utils.decorators import method_decorator
from django.urls import reverse
from django.views import generic
from access.forms.team import TeamForm
# from access.forms.team_users import TeamUsersForm
from access.forms.team import TeamForm, TeamFormAdd
from access.models import Team, TeamUsers, Organization
from access.mixin import *
from core.views.common import AddView, ChangeView, DeleteView
class View(OrganizationPermission, generic.UpdateView):
class View(ChangeView):
context_object_name = "team"
@ -79,15 +78,19 @@ class View(OrganizationPermission, generic.UpdateView):
class Add(OrganizationPermission, generic.CreateView):
class Add(AddView):
form_class = TeamFormAdd
model = Team
parent_model = Organization
permission_required = [
'access.add_team',
]
template_name = 'form.html.j2'
fields = [
'team_name',
]
def form_valid(self, form):
form.instance.organization = Organization.objects.get(pk=self.kwargs['pk'])
@ -101,8 +104,6 @@ class Add(OrganizationPermission, generic.CreateView):
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'] = 'Add Team'
@ -110,7 +111,7 @@ class Add(OrganizationPermission, generic.CreateView):
class Delete(OrganizationPermission, generic.DeleteView):
class Delete(DeleteView):
model = Team
permission_required = [
'access.delete_team'

View File

@ -1,15 +1,13 @@
from django.contrib.auth import decorators as auth_decorator
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import generic
from access.forms.team_users import TeamUsersForm
from access.mixin import OrganizationPermission
from access.models import Team, TeamUsers
from core.views.common import AddView, DeleteView
class Add(OrganizationPermission, generic.CreateView):
class Add(AddView):
context_object_name = "teamuser"
@ -17,10 +15,9 @@ class Add(OrganizationPermission, generic.CreateView):
model = TeamUsers
parent_model = TeamUsers
parent_model = Team
permission_required = [
'access.view_team',
'access.add_teamusers'
]
@ -52,7 +49,7 @@ class Add(OrganizationPermission, generic.CreateView):
return context
class Delete(OrganizationPermission, generic.DeleteView):
class Delete(DeleteView):
model = TeamUsers
permission_required = [
'access.delete_teamusers'

View File

@ -5,9 +5,10 @@ from api.models.tokens import AuthToken
from app import settings
from core.forms.common import CommonModelForm
class AuthTokenForm(forms.ModelForm):
class AuthTokenForm(CommonModelForm):
prefix = 'user_token'

323
app/api/tasks.py Normal file
View File

@ -0,0 +1,323 @@
import json
import re
from django.utils import timezone
from celery import shared_task, current_task
from celery.utils.log import get_task_logger
from celery import states
from access.models import Organization
from api.serializers.inventory import Inventory
from itam.models.device import Device, DeviceType, DeviceOperatingSystem, DeviceSoftware
from itam.models.operating_system import OperatingSystem, OperatingSystemVersion
from itam.models.software import Software, SoftwareCategory, SoftwareVersion
from settings.models.app_settings import AppSettings
logger = get_task_logger(__name__)
@shared_task(bind=True)
def process_inventory(self, data, organization: int):
device = None
device_operating_system = None
operating_system = None
operating_system_version = None
try:
logger.info('Begin Processing Inventory')
data = json.loads(data)
data = Inventory(data)
organization = Organization.objects.get(id=organization)
if Device.objects.filter(slug=str(data.details.name).lower()).exists():
device = Device.objects.get(slug=str(data.details.name).lower())
# device = self.obj
app_settings = AppSettings.objects.get(owner_organization = None)
device_serial_number = None
device_uuid = None
if data.details.serial_number and str(data.details.serial_number).lower() != 'na':
device_serial_number = str(data.details.serial_number)
if data.details.uuid and str(data.details.uuid).lower() != 'na':
device_uuid = str(data.details.uuid)
if not device: # Create the device
device = Device.objects.create(
name = data.details.name,
device_type = None,
serial_number = device_serial_number,
uuid = device_uuid,
organization = organization,
)
if device:
logger.info(f"Device: {device.name}, Serial: {device.serial_number}, UUID: {device.uuid}")
if not device.uuid and device_uuid:
device.uuid = device_uuid
device.save()
if not device.serial_number and device_serial_number:
device.serial_number = data.details.serial_number
device.save()
if OperatingSystem.objects.filter( slug=data.operating_system.name ).exists():
operating_system = OperatingSystem.objects.get( slug=data.operating_system.name )
else: # Create Operating System
operating_system = OperatingSystem.objects.create(
name = data.operating_system.name,
organization = organization,
is_global = True
)
if OperatingSystemVersion.objects.filter( name=data.operating_system.version_major, operating_system=operating_system ).exists():
operating_system_version = OperatingSystemVersion.objects.get(
organization = organization,
is_global = True,
name = data.operating_system.version_major,
operating_system = operating_system
)
else: # Create Operating System Version
operating_system_version = OperatingSystemVersion.objects.create(
organization = organization,
is_global = True,
name = data.operating_system.version_major,
operating_system = operating_system,
)
if DeviceOperatingSystem.objects.filter( version=data.operating_system.version, device=device, operating_system_version=operating_system_version ).exists():
device_operating_system = DeviceOperatingSystem.objects.get(
device=device,
version = data.operating_system.version,
operating_system_version = operating_system_version,
)
if not device_operating_system.installdate: # Only update install date if empty
device_operating_system.installdate = timezone.now()
device_operating_system.save()
else: # Create Operating System Version
device_operating_system = DeviceOperatingSystem.objects.create(
organization = organization,
device=device,
version = data.operating_system.version,
operating_system_version = operating_system_version,
installdate = timezone.now()
)
if app_settings.software_is_global:
software_organization = app_settings.global_organization
else:
software_organization = device.organization
if app_settings.software_categories_is_global:
software_category_organization = app_settings.global_organization
else:
software_category_organization = device.organization
inventoried_software: list = []
for inventory in list(data.software):
software = None
software_category = None
software_version = None
device_software = None
software_category = SoftwareCategory.objects.filter( name = inventory.category )
if software_category.exists():
software_category = SoftwareCategory.objects.get(
name = inventory.category
)
else: # Create Software Category
software_category = SoftwareCategory.objects.create(
organization = software_category_organization,
is_global = True,
name = inventory.category,
)
if software_category.name == inventory.category:
if Software.objects.filter( name = inventory.name ).exists():
software = Software.objects.get(
name = inventory.name
)
if not software.category:
software.category = software_category
software.save()
else: # Create Software
software = Software.objects.create(
organization = software_organization,
is_global = True,
name = inventory.name,
category = software_category,
)
if software.name == inventory.name:
pattern = r"^(\d+:)?(?P<semver>\d+\.\d+(\.\d+)?)"
semver = re.search(pattern, str(inventory.version), re.DOTALL)
if semver:
semver = semver['semver']
else:
semver = inventory.version
if SoftwareVersion.objects.filter( name = semver, software = software ).exists():
software_version = SoftwareVersion.objects.get(
name = semver,
software = software,
)
else: # Create Software Category
software_version = SoftwareVersion.objects.create(
organization = organization,
is_global = True,
name = semver,
software = software,
)
if software_version.name == semver:
if DeviceSoftware.objects.filter( software = software, device=device ).exists():
device_software = DeviceSoftware.objects.get(
device = device,
software = software
)
logger.debug(f"Select Existing Device Software: {device_software.software.name}")
else: # Create Software
device_software = DeviceSoftware.objects.create(
organization = organization,
is_global = True,
installedversion = software_version,
software = software,
device = device,
action=None
)
logger.debug(f"Create Device Software: {device_software.software.name}")
if device_software: # Update the Inventoried software
inventoried_software += [ device_software.id ]
if not device_software.installed: # Only update install date if blank
device_software.installed = timezone.now()
device_software.save()
logger.debug(f"Update Device Software (installed): {device_software.software.name}")
if device_software.installedversion.name != software_version.name:
device_software.installedversion = software_version
device_software.save()
logger.debug(f"Update Device Software (installedversion): {device_software.software.name}")
for not_installed in DeviceSoftware.objects.filter( device=device ):
if not_installed.id not in inventoried_software:
not_installed.delete()
logger.debug(f"Remove Device Software: {not_installed.software.name}")
if device and operating_system and operating_system_version and device_operating_system:
device.inventorydate = timezone.now()
device.save()
logger.info('Finish Processing Inventory')
return str('finished...')
except Exception as e:
logger.critical('Exception')
raise Exception(e)
return str(f'Exception Occured: {e}')

View File

@ -1,4 +1,5 @@
import datetime
import json
import pytest
import unittest
@ -6,6 +7,7 @@ from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import TestCase, Client
from django.test.utils import override_settings
from unittest.mock import patch
@ -14,6 +16,8 @@ from access.models import Organization, Team, TeamUsers, Permission
from api.views.mixin import OrganizationPermissionAPI
from api.serializers.inventory import Inventory
from api.tasks import process_inventory
from itam.models.device import Device, DeviceOperatingSystem, DeviceSoftware
from itam.models.operating_system import OperatingSystem, OperatingSystemVersion
from itam.models.software import Software, SoftwareCategory, SoftwareVersion
@ -105,11 +109,7 @@ class InventoryAPI(TestCase):
)
# upload the inventory
client = Client()
url = reverse('API:_api_device_inventory')
client.force_login(self.add_user)
self.response = client.post(url, data=self.inventory, content_type='application/json')
process_inventory(json.dumps(self.inventory), organization.id)
self.device = Device.objects.get(name=self.inventory['details']['name'])
@ -147,6 +147,8 @@ class InventoryAPI(TestCase):
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
@patch.object(OrganizationPermissionAPI, 'permission_check')
def test_inventory_function_called_permission_check(self, permission_check):
""" Inventory Upload checks permissions
@ -167,6 +169,8 @@ class InventoryAPI(TestCase):
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
@patch.object(Inventory, '__init__')
def test_inventory_serializer_inventory_called(self, serializer):
""" Inventory Upload checks permissions
@ -187,6 +191,8 @@ class InventoryAPI(TestCase):
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
@patch.object(Inventory.Details, '__init__')
def test_inventory_serializer_inventory_details_called(self, serializer):
""" Inventory Upload uses Inventory serializer
@ -204,6 +210,8 @@ class InventoryAPI(TestCase):
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
@patch.object(Inventory.OperatingSystem, '__init__')
def test_inventory_serializer_inventory_operating_system_called(self, serializer):
""" Inventory Upload uses Inventory serializer
@ -221,6 +229,8 @@ class InventoryAPI(TestCase):
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
@patch.object(Inventory.Software, '__init__')
def test_inventory_serializer_inventory_software_called(self, serializer):
""" Inventory Upload uses Inventory serializer
@ -365,7 +375,8 @@ class InventoryAPI(TestCase):
pass
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
def test_api_inventory_valid_status_ok_existing_device(self):
""" Successful inventory upload returns 200 for existing device"""
@ -378,14 +389,8 @@ class InventoryAPI(TestCase):
assert response.status_code == 200
def test_api_inventory_valid_status_created(self):
""" Successful inventory upload returns 201 """
assert self.response.status_code == 201
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
def test_api_inventory_invalid_status_bad_request(self):
""" Incorrectly formated inventory upload returns 400 """

View File

@ -1,3 +1,4 @@
import celery
import pytest
import unittest
import requests
@ -7,6 +8,9 @@ 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
from django.test.utils import override_settings
from unittest.mock import patch
from access.models import Organization, Team, TeamUsers, Permission
@ -188,6 +192,8 @@ class InventoryPermissionsAPI(TestCase):
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
def test_device_auth_add_user_anon_denied(self):
""" Check correct permission for add
@ -203,6 +209,8 @@ class InventoryPermissionsAPI(TestCase):
assert response.status_code == 401
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
def test_device_auth_add_no_permission_denied(self):
""" Check correct permission for add
@ -219,6 +227,8 @@ class InventoryPermissionsAPI(TestCase):
assert response.status_code == 403
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
def test_device_auth_add_different_organization_denied(self):
""" Check correct permission for add
@ -235,6 +245,8 @@ class InventoryPermissionsAPI(TestCase):
assert response.status_code == 403
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
def test_device_auth_add_permission_view_denied(self):
""" Check correct permission for add
@ -251,6 +263,8 @@ class InventoryPermissionsAPI(TestCase):
assert response.status_code == 403
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
def test_device_auth_add_has_permission(self):
""" Check correct permission for add
@ -264,6 +278,6 @@ class InventoryPermissionsAPI(TestCase):
client.force_login(self.add_user)
response = client.post(url, data=self.inventory, content_type='application/json')
assert response.status_code == 201
assert response.status_code == 200

View File

@ -1,32 +1,25 @@
# from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
import json
import re
from django.core.exceptions import ValidationError, PermissionDenied
from django.http import Http404, JsonResponse
from django.utils import timezone
from drf_spectacular.utils import extend_schema, OpenApiExample, OpenApiTypes, OpenApiResponse, OpenApiParameter
from drf_spectacular.utils import extend_schema, OpenApiResponse
from rest_framework import generics, views
from rest_framework.response import Response
from access.mixin import OrganizationMixin
from access.models import Organization
from api.views.mixin import OrganizationPermissionAPI
from api.serializers.itam.inventory import InventorySerializer
from api.serializers.inventory import Inventory
from core.http.common import Http
from itam.models.device import Device, DeviceType, DeviceOperatingSystem, DeviceSoftware
from itam.models.operating_system import OperatingSystem, OperatingSystemVersion
from itam.models.software import Software, SoftwareCategory, SoftwareVersion
from itam.models.device import Device
from settings.models.app_settings import AppSettings
from settings.models.user_settings import UserSettings
from api.tasks import process_inventory
class InventoryPermissions(OrganizationPermissionAPI):
@ -68,9 +61,7 @@ this setting populated, no device will be created and the endpoint will return H
tags = ['device', 'inventory',],
request = InventorySerializer,
responses = {
200: OpenApiResponse(description='Inventory updated an existing device'),
201: OpenApiResponse(description='Inventory created a new device'),
400: OpenApiResponse(description='Inventory is invalid'),
200: OpenApiResponse(description='Inventory upload successful'),
401: OpenApiResponse(description='User Not logged in'),
403: OpenApiResponse(description='User is missing permission or in different organization'),
500: OpenApiResponse(description='Exception occured. View server logs for the Stack Trace'),
@ -102,234 +93,9 @@ this setting populated, no device will be created and the endpoint will return H
raise Http404
device_operating_system = None
operating_system = None
operating_system_version = None
task = process_inventory.delay(request.body, self.default_organization.id)
app_settings = AppSettings.objects.get(owner_organization = None)
if not device: # Create the device
device = Device.objects.create(
name = data.details.name,
device_type = None,
serial_number = data.details.serial_number,
uuid = data.details.uuid,
organization = self.default_organization,
)
status = Http.Status.CREATED
if OperatingSystem.objects.filter( slug=data.operating_system.name ).exists():
operating_system = OperatingSystem.objects.get( slug=data.operating_system.name )
else: # Create Operating System
operating_system = OperatingSystem.objects.create(
name = data.operating_system.name,
organization = self.default_organization,
is_global = True
)
if OperatingSystemVersion.objects.filter( name=data.operating_system.version_major, operating_system=operating_system ).exists():
operating_system_version = OperatingSystemVersion.objects.get(
organization = self.default_organization,
is_global = True,
name = data.operating_system.version_major,
operating_system = operating_system
)
else: # Create Operating System Version
operating_system_version = OperatingSystemVersion.objects.create(
organization = self.default_organization,
is_global = True,
name = data.operating_system.version_major,
operating_system = operating_system,
)
if DeviceOperatingSystem.objects.filter( version=data.operating_system.version, device=device, operating_system_version=operating_system_version ).exists():
device_operating_system = DeviceOperatingSystem.objects.get(
device=device,
version = data.operating_system.version,
operating_system_version = operating_system_version,
)
if not device_operating_system.installdate: # Only update install date if empty
device_operating_system.installdate = timezone.now()
device_operating_system.save()
else: # Create Operating System Version
device_operating_system = DeviceOperatingSystem.objects.create(
organization = self.default_organization,
device=device,
version = data.operating_system.version,
operating_system_version = operating_system_version,
installdate = timezone.now()
)
if app_settings.software_is_global:
software_organization = app_settings.global_organization
else:
software_organization = device.organization
if app_settings.software_categories_is_global:
software_category_organization = app_settings.global_organization
else:
software_category_organization = device.organization
for inventory in list(data.software):
software = None
software_category = None
software_version = None
device_software = None
if SoftwareCategory.objects.filter( name = inventory.category ).exists():
software_category = SoftwareCategory.objects.get(
name = inventory.category
)
else: # Create Software Category
software_category = SoftwareCategory.objects.create(
organization = software_category_organization,
is_global = True,
name = inventory.category,
)
if Software.objects.filter( name = inventory.name ).exists():
software = Software.objects.get(
name = inventory.name
)
if not software.category:
software.category = software_category
software.save()
else: # Create Software
software = Software.objects.create(
organization = software_organization,
is_global = True,
name = inventory.name,
category = software_category,
)
pattern = r"^(\d+:)?(?P<semver>\d+\.\d+(\.\d+)?)"
semver = re.search(pattern, str(inventory.version), re.DOTALL)
if semver:
semver = semver['semver']
else:
semver = inventory.version
if SoftwareVersion.objects.filter( name = semver, software = software ).exists():
software_version = SoftwareVersion.objects.get(
name = semver,
software = software,
)
else: # Create Software Category
software_version = SoftwareVersion.objects.create(
organization = self.default_organization,
is_global = True,
name = semver,
software = software,
)
if DeviceSoftware.objects.filter( software = software, device=device ).exists():
device_software = DeviceSoftware.objects.get(
device = device,
software = software
)
else: # Create Software
device_software = DeviceSoftware.objects.create(
organization = self.default_organization,
is_global = True,
installedversion = software_version,
software = software,
device = device,
action=None
)
if device_software: # Update the Inventoried software
clear_installed_software = DeviceSoftware.objects.filter(
device = device,
software = software
)
# Clear installed version of all installed software
# any found later with no version to be removed
clear_installed_software.update(installedversion=None)
if not device_software.installed: # Only update install date if blank
device_software.installed = timezone.now()
device_software.save()
device_software.installedversion = software_version
device_software.save()
if device and operating_system and operating_system_version and device_operating_system:
# Remove software no longer installed
DeviceSoftware.objects.filter(
device = device,
software = software,
).delete()
device.inventorydate = timezone.now()
device.save()
if status != Http.Status.CREATED:
status = Http.Status.OK
response_data: dict = {"task_id": f"{task.id}"}
except PermissionDenied as e:

View File

@ -0,0 +1,3 @@
from .celery import worker as celery_app
__all__ = ('celery_app',)

18
app/app/celery.py Normal file
View File

@ -0,0 +1,18 @@
import os
from django.conf import settings
from celery import Celery
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')
worker = Celery('app')
worker.config_from_object(f'django.conf:settings', namespace='CELERY')
worker.autodiscover_tasks()
@worker.task(bind=True, ignore_result=True)
def debug_task(self):
print(f'Request: {self!r}')

View File

@ -5,6 +5,8 @@ from app.urls import urlpatterns
from django.conf import settings
from django.urls import URLPattern, URLResolver
from access.models import Organization
from settings.models.user_settings import UserSettings
@ -88,7 +90,7 @@ def nav_items(context) -> list(dict()):
is_active: {bool} if this link is the active URL
Returns:
_type_: _description_
list: Items user has view access to
"""
dnav = []
@ -142,6 +144,40 @@ def nav_items(context) -> list(dict()):
name = str(pattern.name)
if hasattr(pattern.callback.view_class, 'permission_required'):
permissions_required = pattern.callback.view_class.permission_required
user_has_perm = False
if type(permissions_required) is list:
user_has_perm = context.user.has_perms(permissions_required)
else:
user_has_perm = context.user.has_perm(permissions_required)
if hasattr(pattern.callback.view_class, 'model'):
if pattern.callback.view_class.model is Organization and context.user.is_authenticated:
organizations = Organization.objects.filter(manager = context.user)
if len(organizations) > 0:
user_has_perm = True
if str(nav_group.app_name).lower() == 'settings':
user_has_perm = True
if context.user.is_superuser:
user_has_perm = True
if user_has_perm:
nav_items = nav_items + [ {
'name': name,
'url': url,

View File

@ -24,14 +24,61 @@ SETTINGS_DIR = '/etc/itsm' # Primary Settings Directory
BUILD_REPO = os.getenv('CI_PROJECT_URL')
BUILD_SHA = os.getenv('CI_COMMIT_SHA')
BUILD_VERSION = os.getenv('CI_COMMIT_TAG')
DOCS_ROOT = 'https://nofusscomputing.com/projects/django-template/user/'
DOCS_ROOT = 'https://nofusscomputing.com/projects/centurion_erp/user/'
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
# Celery settings
CELERY_ACCEPT_CONTENT = ['json']
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True # broker_connection_retry_on_startup
CELERY_BROKER_URL = 'amqp://guest:guest@172.16.10.102:30712/itsm'
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#broker-use-ssl
# import ssl
# broker_use_ssl = {
# 'keyfile': '/var/ssl/private/worker-key.pem',
# 'certfile': '/var/ssl/amqp-server-cert.pem',
# 'ca_certs': '/var/ssl/myca.pem',
# 'cert_reqs': ssl.CERT_REQUIRED
# }
CELERY_BROKER_POOL_LIMIT = 3 # broker_pool_limit
CELERY_CACHE_BACKEND = 'django-cache'
CELERY_ENABLE_UTC = True
CELERY_RESULT_BACKEND = 'django-db'
CELERY_RESULT_EXTENDED = True
CELERY_TASK_SERIALIZER = 'json'
CELERY_TIMEZONE = 'UTC'
CELERY_TASK_DEFAULT_EXCHANGE = 'ITSM' # task_default_exchange
CELERY_TASK_DEFAULT_PRIORITY = 10 # 1-10=LOW-HIGH task_default_priority
# CELERY_TASK_DEFAULT_QUEUE = 'background'
CELERY_TASK_TIME_LIMIT = 3600 # task_time_limit
CELERY_TASK_TRACK_STARTED = True # task_track_started
# dont set concurrency for docer as it defaults to CPU count
CELERY_WORKER_CONCURRENCY = 2 # worker_concurrency - Default: Number of CPU cores
CELERY_WORKER_DEDUPLICATE_SUCCESSFUL_TASKS = True # worker_deduplicate_successful_tasks
CELERY_WORKER_MAX_TASKS_PER_CHILD = 1 # worker_max_tasks_per_child
# CELERY_WORKER_MAX_MEMORY_PER_CHILD = 10000 # 10000=10mb worker_max_memory_per_child - Default: No limit. Type: int (kilobytes)
# CELERY_TASK_SEND_SENT_EVENT = True
CELERY_WORKER_SEND_TASK_EVENTS = True # worker_send_task_events
# django setting.
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
'LOCATION': 'my_cache_table',
}
}
#
# Defaults
#
ALLOWED_HOSTS = [ '*' ] # Site host to serve
DEBUG = False # SECURITY WARNING: don't run with debug turned on in production!
SITE_URL = 'http://127.0.0.1' # domain with HTTP method for the sites URL
@ -62,6 +109,7 @@ INSTALLED_APPS = [
'rest_framework',
'rest_framework_json_api',
'social_django',
'django_celery_results',
'core.apps.CoreConfig',
'access.apps.AccessConfig',
'itam.apps.ItamConfig',
@ -169,7 +217,7 @@ STATICFILES_DIRS = [
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
SITE_TITLE = "Site Title"
SITE_TITLE = "Centurion ERP"
API_ENABLED = True

View File

@ -118,6 +118,16 @@ class ModelPermissionsAdd:
add_data: dict = None
@pytest.mark.skip(reason="ToDO: write test")
def test_model_requires_attribute_parent_model(self):
""" Child model requires 'django view' attribute 'parent_model'
When a child-model is added the parent model is required so that the organization can be detrmined.
"""
pass
def test_model_add_user_anon_denied(self):
""" Check correct permission for add

View File

@ -0,0 +1,63 @@
import pytest
import unittest
from app.tests.abstract.views import AddView, ChangeView, DeleteView, DisplayView, IndexView
class ModelAdd(
AddView
):
""" Unit Tests for Model Add """
class ModelChange(
ChangeView
):
""" Unit Tests for Model Change """
class ModelDelete(
DeleteView
):
""" Unit Tests for Model delete """
class ModelDisplay(
DisplayView
):
""" Unit Tests for Model display """
class ModelIndex(
IndexView
):
""" Unit Tests for Model index """
class ModelCommon(
ModelAdd,
ModelChange,
ModelDelete,
ModelDisplay
):
""" Unit Tests for all models """
class PrimaryModel(
ModelCommon,
ModelIndex
):
""" Tests for Primary Models
A Primary model is a model that is deemed a model that has the following views:
- Add
- Change
- Delete
- Display
- Index
"""

View File

@ -0,0 +1,565 @@
import inspect
import pytest
import unittest
class AddView:
""" Testing of Display view """
add_module: str = None
""" Full module path to test """
add_view: str = None
""" View Class name to test """
def test_view_add_attribute_not_exists_fields(self):
""" Attribute does not exists test
Ensure that `fields` attribute is not defined as the expectation is that a form will be used.
"""
module = __import__(self.add_module, fromlist=[self.add_view])
assert hasattr(module, self.add_view)
viewclass = getattr(module, self.add_view)
assert viewclass.fields is None
def test_view_add_attribute_exists_form_class(self):
""" Attribute exists test
Ensure that `form_class` attribute is defined as it's required.
"""
module = __import__(self.add_module, fromlist=[self.add_view])
assert hasattr(module, self.add_view)
viewclass = getattr(module, self.add_view)
assert hasattr(viewclass, 'form_class')
def test_view_add_attribute_type_form_class(self):
""" Attribute Type Test
Ensure that `form_class` attribute is a class.
"""
module = __import__(self.add_module, fromlist=[self.add_view])
assert hasattr(module, self.add_view)
viewclass = getattr(module, self.add_view)
assert inspect.isclass(viewclass.form_class)
def test_view_add_attribute_exists_model(self):
""" Attribute exists test
Ensure that `model` attribute is defined as it's required .
"""
module = __import__(self.add_module, fromlist=[self.add_view])
assert hasattr(module, self.add_view)
viewclass = getattr(module, self.add_view)
assert hasattr(viewclass, 'model')
def test_view_add_attribute_exists_permission_required(self):
""" Attribute exists test
Ensure that `permission_required` attribute is defined as it's required.
"""
module = __import__(self.add_module, fromlist=[self.add_view])
assert hasattr(module, self.add_view)
viewclass = getattr(module, self.add_view)
assert hasattr(viewclass, 'permission_required')
def test_view_add_attribute_type_permission_required(self):
""" Attribute Type Test
Ensure that `permission_required` attribute is a list
"""
module = __import__(self.add_module, fromlist=[self.add_view])
assert hasattr(module, self.add_view)
viewclass = getattr(module, self.add_view)
assert type(viewclass.permission_required) is list
def test_view_add_attribute_exists_template_name(self):
""" Attribute exists test
Ensure that `template_name` attribute is defined as it's required.
"""
module = __import__(self.add_module, fromlist=[self.add_view])
assert hasattr(module, self.add_view)
viewclass = getattr(module, self.add_view)
assert hasattr(viewclass, 'template_name')
def test_view_add_attribute_type_template_name(self):
""" Attribute Type Test
Ensure that `template_name` attribute is a string.
"""
module = __import__(self.add_module, fromlist=[self.add_view])
assert hasattr(module, self.add_view)
viewclass = getattr(module, self.add_view)
assert type(viewclass.template_name) is str
class ChangeView:
""" Testing of Display view """
change_module: str = None
""" Full module path to test """
change_view: str = None
""" Change Class name to test """
def test_view_change_attribute_not_exists_fields(self):
""" Attribute does not exists test
Ensure that `fields` attribute is not defined as the expectation is that a form will be used.
"""
module = __import__(self.change_module, fromlist=[self.change_view])
assert hasattr(module, self.change_view)
viewclass = getattr(module, self.change_view)
assert viewclass.fields is None
def test_view_change_attribute_exists_form_class(self):
""" Attribute exists test
Ensure that `form_class` attribute is defined as it's required.
"""
module = __import__(self.change_module, fromlist=[self.change_view])
assert hasattr(module, self.change_view)
viewclass = getattr(module, self.change_view)
assert hasattr(viewclass, 'form_class')
def test_view_change_attribute_type_form_class(self):
""" Attribute Type Test
Ensure that `form_class` attribute is a string.
"""
module = __import__(self.change_module, fromlist=[self.change_view])
assert hasattr(module, self.change_view)
viewclass = getattr(module, self.change_view)
assert inspect.isclass(viewclass.form_class)
def test_view_change_attribute_exists_model(self):
""" Attribute exists test
Ensure that `model` attribute is defined as it's required .
"""
module = __import__(self.change_module, fromlist=[self.change_view])
assert hasattr(module, self.change_view)
viewclass = getattr(module, self.change_view)
assert hasattr(viewclass, 'model')
def test_view_change_attribute_exists_permission_required(self):
""" Attribute exists test
Ensure that `permission_required` attribute is defined as it's required.
"""
module = __import__(self.change_module, fromlist=[self.change_view])
assert hasattr(module, self.change_view)
viewclass = getattr(module, self.change_view)
assert hasattr(viewclass, 'permission_required')
def test_view_change_attribute_type_permission_required(self):
""" Attribute Type Test
Ensure that `permission_required` attribute is a list
"""
module = __import__(self.change_module, fromlist=[self.change_view])
assert hasattr(module, self.change_view)
viewclass = getattr(module, self.change_view)
assert type(viewclass.permission_required) is list
def test_view_change_attribute_exists_template_name(self):
""" Attribute exists test
Ensure that `template_name` attribute is defined as it's required.
"""
module = __import__(self.change_module, fromlist=[self.change_view])
assert hasattr(module, self.change_view)
viewclass = getattr(module, self.change_view)
assert hasattr(viewclass, 'template_name')
def test_view_change_attribute_type_template_name(self):
""" Attribute Type Test
Ensure that `template_name` attribute is a string.
"""
module = __import__(self.change_module, fromlist=[self.change_view])
assert hasattr(module, self.change_view)
viewclass = getattr(module, self.change_view)
assert type(viewclass.template_name) is str
class DeleteView:
""" Testing of Display view """
delete_module: str = None
""" Full module path to test """
delete_view: str = None
""" Delete Class name to test """
def test_view_delete_attribute_exists_model(self):
""" Attribute exists test
Ensure that `model` attribute is defined as it's required .
"""
module = __import__(self.delete_module, fromlist=[self.delete_view])
assert hasattr(module, self.delete_view)
viewclass = getattr(module, self.delete_view)
assert hasattr(viewclass, 'model')
def test_view_delete_attribute_exists_permission_required(self):
""" Attribute exists test
Ensure that `model` attribute is defined as it's required .
"""
module = __import__(self.delete_module, fromlist=[self.delete_view])
assert hasattr(module, self.delete_view)
viewclass = getattr(module, self.delete_view)
assert hasattr(viewclass, 'permission_required')
def test_view_delete_attribute_type_permission_required(self):
""" Attribute Type Test
Ensure that `permission_required` attribute is a list
"""
module = __import__(self.delete_module, fromlist=[self.delete_view])
assert hasattr(module, self.delete_view)
viewclass = getattr(module, self.delete_view)
assert type(viewclass.permission_required) is list
def test_view_delete_attribute_exists_template_name(self):
""" Attribute exists test
Ensure that `template_name` attribute is defined as it's required.
"""
module = __import__(self.delete_module, fromlist=[self.delete_view])
assert hasattr(module, self.delete_view)
viewclass = getattr(module, self.delete_view)
assert hasattr(viewclass, 'template_name')
def test_view_delete_attribute_type_template_name(self):
""" Attribute Type Test
Ensure that `template_name` attribute is a string.
"""
module = __import__(self.delete_module, fromlist=[self.delete_view])
assert hasattr(module, self.delete_view)
viewclass = getattr(module, self.delete_view)
assert type(viewclass.template_name) is str
class DisplayView:
""" Testing of Display view """
display_module: str = None
""" Full module path to test """
display_view: str = None
""" Change Class name to test """
def test_view_display_attribute_exists_model(self):
""" Attribute exists test
Ensure that `model` attribute is defined as it's required .
"""
module = __import__(self.display_module, fromlist=[self.display_view])
assert hasattr(module, self.display_view)
viewclass = getattr(module, self.display_view)
assert hasattr(viewclass, 'model')
def test_view_display_attribute_exists_permission_required(self):
""" Attribute exists test
Ensure that `permission_required` attribute is defined as it's required.
"""
module = __import__(self.display_module, fromlist=[self.display_view])
assert hasattr(module, self.display_view)
viewclass = getattr(module, self.display_view)
assert hasattr(viewclass, 'permission_required')
def test_view_display_attribute_type_permission_required(self):
""" Attribute Type Test
Ensure that `permission_required` attribute is a list
"""
module = __import__(self.display_module, fromlist=[self.display_view])
assert hasattr(module, self.display_view)
viewclass = getattr(module, self.display_view)
assert type(viewclass.permission_required) is list
def test_view_display_attribute_exists_template_name(self):
""" Attribute exists test
Ensure that `template_name` attribute is defined as it's required.
"""
module = __import__(self.display_module, fromlist=[self.display_view])
assert hasattr(module, self.display_view)
viewclass = getattr(module, self.display_view)
assert hasattr(viewclass, 'template_name')
def test_view_display_attribute_type_template_name(self):
""" Attribute Type Test
Ensure that `template_name` attribute is a string.
"""
module = __import__(self.display_module, fromlist=[self.display_view])
assert hasattr(module, self.display_view)
viewclass = getattr(module, self.display_view)
assert type(viewclass.template_name) is str
class IndexView:
""" Testing of Display view """
index_module: str = None
""" Full module path to test """
index_view: str = None
""" Index Class name to test """
def test_view_index_attribute_exists_model(self):
""" Attribute exists test
Ensure that `model` attribute is defined as it's required .
"""
module = __import__(self.index_module, fromlist=[self.index_view])
assert hasattr(module, self.index_view)
viewclass = getattr(module, self.index_view)
assert hasattr(viewclass, 'model')
def test_view_index_attribute_exists_permission_required(self):
""" Attribute exists test
Ensure that `model` attribute is defined as it's required .
"""
module = __import__(self.index_module, fromlist=[self.index_view])
assert hasattr(module, self.index_view)
viewclass = getattr(module, self.index_view)
assert hasattr(viewclass, 'permission_required')
def test_view_index_attribute_type_permission_required(self):
""" Attribute Type Test
Ensure that `permission_required` attribute is a list
"""
module = __import__(self.index_module, fromlist=[self.index_view])
assert hasattr(module, self.index_view)
viewclass = getattr(module, self.index_view)
assert type(viewclass.permission_required) is list
def test_view_index_attribute_exists_template_name(self):
""" Attribute exists test
Ensure that `template_name` attribute is defined as it's required.
"""
module = __import__(self.index_module, fromlist=[self.index_view])
assert hasattr(module, self.index_view)
viewclass = getattr(module, self.index_view)
assert hasattr(viewclass, 'template_name')
def test_view_index_attribute_type_template_name(self):
""" Attribute Type Test
Ensure that `template_name` attribute is a string.
"""
module = __import__(self.index_module, fromlist=[self.index_view])
assert hasattr(module, self.index_view)
viewclass = getattr(module, self.index_view)
assert type(viewclass.template_name) is str
class AllViews(
AddView,
ChangeView,
DeleteView,
DisplayView,
IndexView
):
""" Abstract test class containing ALL view tests """
add_module: str = None
""" Full module path to test """
add_view: str = None
""" View Class name to test """
change_module: str = None
""" Full module path to test """
change_view: str = None
""" Change Class name to test """
delete_module: str = None
""" Full module path to test """
delete_view: str = None
""" Delete Class name to test """
display_module: str = None
""" Full module path to test """
display_view: str = None
""" Change Class name to test """
index_module: str = None
""" Full module path to test """
index_view: str = None
""" Index Class name to test """

View File

@ -1,11 +1,13 @@
from django import forms
from django.db.models import Q
from config_management.models.groups import ConfigGroupSoftware
from core.forms.common import CommonModelForm
from itam.models.software import Software
class SoftwareAdd(forms.ModelForm):
class SoftwareAdd(CommonModelForm):
class Meta:
model = ConfigGroupSoftware
@ -13,9 +15,3 @@ class SoftwareAdd(forms.ModelForm):
'software',
'action'
]
def __init__(self, *args, **kwargs):
organizations = kwargs.pop('organizations')
super().__init__(*args, **kwargs)
self.fields['software'].queryset = Software.objects.filter(Q(organization_id__in=organizations) | Q(is_global = True))

View File

@ -1,11 +1,13 @@
from django import forms
from django.db.models import Q
from config_management.models.groups import ConfigGroupSoftware
from core.forms.common import CommonModelForm
from itam.models.software import Software, SoftwareVersion
class SoftwareUpdate(forms.ModelForm):
class SoftwareUpdate(CommonModelForm):
class Meta:
model = ConfigGroupSoftware

View File

@ -1,11 +1,13 @@
from django import forms
from django.db.models import Q
from config_management.models.groups import ConfigGroups
from core.forms.common import CommonModelForm
from itam.models.software import Software, SoftwareVersion
class ConfigGroupForm(forms.ModelForm):
class ConfigGroupForm(CommonModelForm):
class Meta:
model = ConfigGroups
@ -13,5 +15,20 @@ class ConfigGroupForm(forms.ModelForm):
'name',
'parent',
'is_global',
'organization',
'model_notes',
'config',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'parent' in kwargs['initial']:
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(
).exclude(
id=int(kwargs['initial']['parent'])
)

View File

@ -1,11 +1,12 @@
from django import forms
from itam.models.device import Device
from config_management.models.groups import ConfigGroups, ConfigGroupHosts
from core.forms.common import CommonModelForm
class ConfigGroupHostsForm(forms.ModelForm):
class ConfigGroupHostsForm(CommonModelForm):
__name__ = 'asdsa'

View File

@ -0,0 +1,28 @@
# Generated by Django 5.0.6 on 2024-07-11 04:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config_management', '0005_configgrouphosts_model_notes_and_more'),
]
operations = [
migrations.AlterField(
model_name='configgrouphosts',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'),
),
migrations.AlterField(
model_name='configgroups',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'),
),
migrations.AlterField(
model_name='configgroupsoftware',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'),
),
]

View File

@ -48,7 +48,7 @@
{% csrf_token %}
{{ form }}
{% include 'icons/issue_link.html.j2' with issue=13 %}<br>
<br>
<input type="submit" value="Submit">
<script>

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 = 'config_management.views.groups.groups'
add_view = 'GroupAdd'
change_module = add_module
change_view = 'GroupView'
delete_module = add_module
delete_view = 'GroupDelete'
display_module = add_module
display_view = 'GroupView'
index_module = add_module
index_view = 'GroupIndexView'

View File

@ -0,0 +1,31 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import AddView, ChangeView, DeleteView
class ConfigGroupsSoftwareViews(
TestCase,
AddView,
ChangeView,
DeleteView
):
add_module = 'config_management.views.groups.software'
add_view = 'GroupSoftwareAdd'
change_module = add_module
change_view = 'GroupSoftwareChange'
delete_module = add_module
delete_view = 'GroupSoftwareDelete'
# display_module = add_module
# display_view = 'GroupView'
# index_module = add_module
# index_view = 'GroupIndexView'

View File

@ -10,14 +10,14 @@ urlpatterns = [
path('group/add', GroupAdd.as_view(), name='_group_add'),
path('group/<int:pk>', GroupView.as_view(), name='_group_view'),
path('group/<int:group_id>/child', GroupAdd.as_view(), name='_group_add_child'),
path('group/<int:pk>/child', GroupAdd.as_view(), name='_group_add_child'),
path('group/<int:pk>/delete', GroupDelete.as_view(), name='_group_delete'),
path("group/<int:pk>/software/add", GroupSoftwareAdd.as_view(), name="_group_software_add"),
path("group/<int:group_id>/software/<int:pk>", GroupSoftwareChange.as_view(), name="_group_software_change"),
path("group/<int:group_id>/software/<int:pk>/delete", GroupSoftwareDelete.as_view(), name="_group_software_delete"),
path('group/<int:group_id>/host', GroupHostAdd.as_view(), name='_group_add_host'),
path('group/<int:pk>/host', GroupHostAdd.as_view(), name='_group_add_host'),
path('group/<int:group_id>/host/<int:pk>/delete', GroupHostDelete.as_view(), name='_group_delete_host'),
]

View File

@ -4,12 +4,10 @@ from django.contrib.auth import decorators as auth_decorator
from django.db.models import Count, Q
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import generic
from access.mixin import OrganizationPermission
from core.forms.comment import AddNoteForm
from core.models.notes import Notes
from core.views.common import AddView, ChangeView, DeleteView, IndexView
from itam.models.device import Device
@ -21,7 +19,7 @@ from config_management.models.groups import ConfigGroups, ConfigGroupHosts, Conf
class GroupIndexView(OrganizationPermission, generic.ListView):
class GroupIndexView(IndexView):
context_object_name = "groups"
@ -29,7 +27,9 @@ class GroupIndexView(OrganizationPermission, generic.ListView):
paginate_by = 10
permission_required = 'config_management.view_configgroups'
permission_required = [
'config_management.view_configgroups'
]
template_name = 'config_management/group_index.html.j2'
@ -57,13 +57,11 @@ class GroupIndexView(OrganizationPermission, generic.ListView):
class GroupAdd(OrganizationPermission, generic.CreateView):
class GroupAdd(AddView):
fields = [
'name',
'parent',
'organization',
]
organization_field = 'organization'
form_class = ConfigGroupForm
model = ConfigGroups
@ -80,11 +78,11 @@ class GroupAdd(OrganizationPermission, generic.CreateView):
'organization': UserSettings.objects.get(user = self.request.user).default_organization
}
if 'group_id' in self.kwargs:
if 'pk' in self.kwargs:
if self.kwargs['group_id']:
if self.kwargs['pk']:
initial.update({'parent': self.kwargs['group_id']})
initial.update({'parent': self.kwargs['pk']})
self.model.parent.field.hidden = True
@ -111,7 +109,7 @@ class GroupAdd(OrganizationPermission, generic.CreateView):
class GroupView(OrganizationPermission, generic.UpdateView):
class GroupView(ChangeView):
context_object_name = "group"
@ -195,7 +193,7 @@ class GroupView(OrganizationPermission, generic.UpdateView):
class GroupDelete(OrganizationPermission, generic.DeleteView):
class GroupDelete(DeleteView):
model = ConfigGroups
@ -220,12 +218,14 @@ class GroupDelete(OrganizationPermission, generic.DeleteView):
class GroupHostAdd(OrganizationPermission, generic.CreateView):
class GroupHostAdd(AddView):
model = ConfigGroupHosts
parent_model = ConfigGroups
permission_required = [
'config_management.add_hosts',
'config_management.add_configgrouphosts',
]
template_name = 'form.html.j2'
@ -235,7 +235,9 @@ class GroupHostAdd(OrganizationPermission, generic.CreateView):
def form_valid(self, form):
form.instance.group_id = self.kwargs['group_id']
form.instance.group_id = self.kwargs['pk']
form.instance.organization = self.parent_model.objects.get(pk=form.instance.group_id).organization
return super().form_valid(form)
@ -252,40 +254,31 @@ class GroupHostAdd(OrganizationPermission, generic.CreateView):
form_class = super().get_form(form_class=None)
group = ConfigGroups.objects.get(pk=self.kwargs['group_id'])
group = ConfigGroups.objects.get(pk=self.kwargs['pk'])
exsting_group_hosts = ConfigGroupHosts.objects.filter(group=group)
form_class.fields["host"].queryset = None
if group.is_global:
form_class.fields["host"].queryset = Device.objects.filter(
form_class.fields["host"].queryset = form_class.fields["host"].queryset.filter(
).exclude(
id__in=exsting_group_hosts.values_list('host', flat=True)
)
if form_class.fields["host"].queryset is None:
form_class.fields["host"].queryset = Device.objects.filter(
organization=group.organization.id,
).exclude(id__in=exsting_group_hosts.values_list('host', flat=True))
return form_class
def get_success_url(self, **kwargs):
return reverse('Config Management:_group_view', args=[self.kwargs['group_id'],])
return reverse('Config Management:_group_view', args=[self.kwargs['pk'],])
class GroupHostDelete(OrganizationPermission, generic.DeleteView):
class GroupHostDelete(DeleteView):
model = ConfigGroupHosts
permission_required = [
'config_management.delete_hosts',
'config_management.delete_configgrouphosts',
]
template_name = 'form.html.j2'

View File

@ -1,7 +1,4 @@
from django.urls import reverse
from django.views import generic
from access.mixin import OrganizationPermission
from itam.models.software import Software
@ -9,9 +6,10 @@ from config_management.forms.group.add_software import SoftwareAdd
from config_management.forms.group.change_software import SoftwareUpdate
from config_management.models.groups import ConfigGroups, ConfigGroupSoftware
from core.views.common import AddView, ChangeView, DeleteView
class GroupSoftwareAdd(OrganizationPermission, generic.CreateView):
class GroupSoftwareAdd(AddView):
form_class = SoftwareAdd
@ -53,13 +51,6 @@ class GroupSoftwareAdd(OrganizationPermission, generic.CreateView):
return super().form_valid(form)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
obj = ConfigGroups.objects.get(pk=self.kwargs['pk'])
kwargs['organizations'] = [ obj.organization.id ]
return kwargs
def get_success_url(self, **kwargs):
return reverse('Config Management:_group_view', args=(self.kwargs['pk'],))
@ -74,7 +65,7 @@ class GroupSoftwareAdd(OrganizationPermission, generic.CreateView):
class GroupSoftwareChange(OrganizationPermission, generic.UpdateView):
class GroupSoftwareChange(ChangeView):
form_class = SoftwareUpdate
@ -113,7 +104,7 @@ class GroupSoftwareChange(OrganizationPermission, generic.UpdateView):
class GroupSoftwareDelete(OrganizationPermission, generic.DeleteView):
class GroupSoftwareDelete(DeleteView):
model = ConfigGroupSoftware

5
app/core/exceptions.py Normal file
View File

@ -0,0 +1,5 @@
class MissingAttribute(Exception):
""" An attribute is missing"""
pass

View File

View File

@ -1,10 +1,10 @@
from django import forms
from app import settings
from core.forms.common import CommonModelForm
from core.models.notes import Notes
class AddNoteForm(forms.ModelForm):
class AddNoteForm(CommonModelForm):
prefix = 'note'

100
app/core/forms/common.py Normal file
View File

@ -0,0 +1,100 @@
from django import forms
from django.db.models import Q
from access.models import Organization, TeamUsers
class CommonModelForm(forms.ModelForm):
""" Abstract Form class for form inclusion
This class exists so that common functions can be conducted against forms as they are loaded.
"""
organization_field: str = 'organization'
""" Organization Field
Name of the field that contains Organizations.
This field will be filtered to those that the user is part of.
"""
def __init__(self, *args, **kwargs):
"""Form initialization.
Initialize the form using the super classes first then continue to initialize the form using logic
contained within this method.
## Tenancy Objects
Fields that contain an attribute called `organization` will have the objects filtered to
the organizations the user is part of. If the object has `is_global=True`, that object will not be
filtered out.
"""
user = kwargs.pop('user', None)
user_organizations: list([str]) = []
user_organizations_id: list(int()) = []
for team_user in TeamUsers.objects.filter(user=user):
if team_user.team.organization.name not in user_organizations:
if not user_organizations:
self.user_organizations = []
user_organizations += [ team_user.team.organization.name ]
user_organizations_id += [ team_user.team.organization.id ]
new_kwargs: dict = {}
for key, value in kwargs.items():
if key != 'user':
new_kwargs.update({key: value})
super().__init__(*args, **new_kwargs)
if len(user_organizations_id) > 0:
for field_name in self.fields:
field = self.fields[field_name]
if hasattr(field, 'queryset'):
if hasattr(field.queryset.model, 'organization'):
if hasattr(field.queryset.model, 'is_global'):
self.fields[field_name].queryset = field.queryset.filter(
Q(organization__in=user_organizations_id)
|
Q(is_global = True)
)
else:
self.fields[field_name].queryset = field.queryset.filter(
Q(organization__in=user_organizations_id)
)
if self.Meta.fields:
if self.organization_field in self.Meta.fields:
self.fields[self.organization_field].queryset = self.fields[self.organization_field].queryset.filter(
Q(name__in=user_organizations)
|
Q(manager=user)
)

View File

@ -0,0 +1,20 @@
from django import forms
from core.models.manufacturer import Manufacturer
class ManufacturerForm(forms.ModelForm):
class Meta:
fields = [
'name',
'slug',
'id',
'organization',
'is_global',
'model_notes',
]
model = Manufacturer

View File

@ -0,0 +1,23 @@
# Generated by Django 5.0.6 on 2024-07-11 04:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0009_manufacturer_model_notes_notes_model_notes'),
]
operations = [
migrations.AlterField(
model_name='manufacturer',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'),
),
migrations.AlterField(
model_name='notes',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'),
),
]

View File

@ -0,0 +1,138 @@
{% extends 'base.html.j2' %}
{% load json %}
{% load markdown %}
{% 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 'Settings:_task_results' %}';"
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 Task Results</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</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>
<div id="Details" class="tabcontent">
<h3>Details </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.task_id.label }}</label>
<span>{{ form.task_id.value }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.task_name.label }}</label>
<span>{{ form.task_name.value }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.status.label }}</label>
<span>{{ form.status.value }}</span>
</div>
<div class="detail-view-field">
<label>Created</label>
<span>{{ task_result.date_created }}</span>
</div>
<div class="detail-view-field">
<label>Finished</label>
<span>{{ task_result.date_done }}</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.task_args.label }}</label>
<div style="display: inline-block; text-align: left;">{{ form.task_args.value }}</div>
</div>
<br />
<div>
<label style="font-weight: bold; width: 100%; border-bottom: 1px solid #ccc; display: block; text-align: inherit;">Result</label>
<div style="display: inline-block; text-align: left;"><pre style="text-align: left; max-width: 300px;">{{ task_result.result | json_pretty }}</pre></div>
</div>
</div>
</div>
<script>
// Get the element with id="defaultOpen" and click on it
document.getElementById("defaultOpen").click();
</script>
</div>
{% endblock %}

View File

@ -0,0 +1,25 @@
{% extends 'base.html.j2' %}
{% block content %}
<input type="button" value="<< Back to settings" onclick="window.location='{% url 'Settings:Settings' %}';">
<table style="max-width: 100%;">
<thead>
<th>ID</th>
<th>Name</th>
<th>Status</th>
<th>Created</th>
<th>Completed</th>
</thead>
{% for entry in task_results %}
<tr class="clicker">
<td><a href="{% url 'Settings:_task_result_view' pk=entry.id %}">{{ entry.task_id }}</a></td>
<td>{{ entry.task_name }}</td>
<td>{{ entry.status }}</td>
<td>{{ entry.date_created }}</td>
<td>{{ entry.date_done }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

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 ManufacturerViews(
TestCase,
PrimaryModel
):
add_module = 'settings.views.manufacturer'
add_view = 'Add'
change_module = add_module
change_view = 'View'
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,35 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import ModelDisplay
class HistoryViews(
TestCase,
ModelDisplay
):
# add_module = 'config_management.views.groups.groups'
# add_view = 'GroupAdd'
# change_module = add_module
# change_view = 'GroupView'
# delete_module = add_module
# delete_view = 'GroupDelete'
display_module = 'core.views.history'
display_view = 'View'
# index_module = add_module
# index_view = 'GroupIndexView'
@pytest.mark.skip(reason="test this models dynamic build of self.model")
def test_view_display_attribute_exists_model(self):
""" As part of display init this view dynamically builds self.model """
pass

View File

@ -0,0 +1,105 @@
import pytest
import unittest
import requests
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
from celery import states
from access.models import Organization, Team, TeamUsers, Permission
from app.tests.abstract.model_permissions import ModelPermissionsView
from django_celery_results.models import TaskResult
class TaskResultPermissions(TestCase, ModelPermissionsView):
model = TaskResult
app_label = 'django_celery_results'
app_namespace = 'Settings'
url_name_view = '_task_result_view'
@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(
task_id='organization',
periodic_task_name='',
task_name = 'deviceone',
status=states.SUCCESS,
content_type='application/json',
content_encoding='utf-8',
)
self.url_view_kwargs = {'pk': self.item.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])
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
)
def test_model_view_different_organizaiton_denied(self): # Test is N/A
pass

View File

@ -0,0 +1,30 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import ModelDisplay, ModelIndex
class TaskResultsViews(
TestCase,
ModelDisplay,
ModelIndex
):
# add_module = 'core.views.celery_log'
# add_view = 'GroupAdd'
# change_module = add_module
# change_view = 'GroupView'
# delete_module = add_module
# delete_view = 'GroupDelete'
display_module = 'core.views.celery_log'
display_view = 'View'
index_module = display_module
index_view = 'Index'

View File

View File

@ -0,0 +1,74 @@
import markdown
from django.views import generic
from access.mixin import OrganizationPermission
from django_celery_results.models import TaskResult
class Index(OrganizationPermission, generic.ListView):
context_object_name = "task_results"
fields = [
"task_id",
'task_name',
'status',
'date_created',
'date_done',
]
model = TaskResult
permission_required = [
'django_celery_results.view_taskresult',
]
template_name = 'celery_log_index.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Background Task Results'
return context
def get_success_url(self, **kwargs):
return reverse('Settings:_device_model_view', args=(self.kwargs['pk'],))
class View(OrganizationPermission, generic.UpdateView):
context_object_name = "task_result"
fields = [
"task_id",
'task_name',
'status',
'task_args',
]
model = TaskResult
permission_required = [
'django_celery_results.view_taskresult',
]
template_name = 'celery_log.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = f"Task {self.object.task_id}"
return context
def post(self, request, *args, **kwargs):
pass

76
app/core/views/common.py Normal file
View File

@ -0,0 +1,76 @@
from django.views import generic
from access.mixin import OrganizationPermission
from core.exceptions import MissingAttribute
class View(OrganizationPermission):
""" Abstract class common to all views
!!! Danger
Don't directly use this class within your view as it's already assigned to the views that require it.
"""
template_name:str = 'form.html.j2'
def get_form_kwargs(self) -> dict:
""" Fetch kwargs for form
Returns:
dict: kwargs used in fetching form
"""
kwargs = super().get_form_kwargs()
if self.form_class:
kwargs.update({'user': self.request.user})
return kwargs
class AddView(View, generic.CreateView):
template_name:str = 'form.html.j2'
class ChangeView(View, generic.UpdateView):
template_name:str = 'form.html.j2'
class DeleteView(OrganizationPermission, generic.DeleteView):
template_name:str = 'form.html.j2'
class DisplayView(OrganizationPermission, generic.DetailView):
""" A View used for displaying arbitrary data """
template_name:str = 'form.html.j2'
class IndexView(View, generic.ListView):
model = None
""" Model the view is for
Leaving this value unset will prevent the item from showing up within the navigation menu
"""
template_name:str = None
def __init__(self, **kwargs):
if not self.model:
raise MissingAttribute('Model is required for view')
super().__init__(**kwargs)

View File

@ -1,6 +1,5 @@
import markdown
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import Q
from django.http import HttpResponseRedirect
from django.shortcuts import redirect, render

View File

@ -1,6 +1,5 @@
import json
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import Q
from django.shortcuts import render
from django.template import Template, Context

View File

@ -1,6 +1,5 @@
import json
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import Q
from django.shortcuts import render
from django.template import Template, Context

View File

@ -2,10 +2,13 @@ from django import forms
from django.db.models import Q
from app import settings
from core.forms.common import CommonModelForm
from itam.models.device import Device
class DeviceForm(forms.ModelForm):
class DeviceForm(CommonModelForm):
prefix = 'device'

View File

@ -3,10 +3,13 @@ from django.db.models import Q
from app import settings
from core.forms.common import CommonModelForm
from itam.models.device import DeviceOperatingSystem
class Update(forms.ModelForm):
class Update(CommonModelForm):
prefix = 'operating_system'

View File

@ -0,0 +1,23 @@
from django.db.models import Q
from core.forms.common import CommonModelForm
from itam.models.device_models import DeviceModel
class DeviceModelForm(CommonModelForm):
class Meta:
fields = [
'name',
'slug',
'manufacturer',
'id',
'organization',
'is_global',
'model_notes',
]
model = DeviceModel

View File

@ -1,11 +1,13 @@
from django import forms
from django.db.models import Q
from core.forms.common import CommonModelForm
from itam.models.device import DeviceSoftware
from itam.models.software import Software
class SoftwareAdd(forms.ModelForm):
class SoftwareAdd(CommonModelForm):
class Meta:
model = DeviceSoftware

View File

@ -1,11 +1,12 @@
from django import forms
from django.db.models import Q
from core.forms.common import CommonModelForm
from itam.models.device import DeviceSoftware
from itam.models.software import Software, SoftwareVersion
class SoftwareUpdate(forms.ModelForm):
class SoftwareUpdate(CommonModelForm):
class Meta:
model = DeviceSoftware

View File

@ -0,0 +1,22 @@
from django.db.models import Q
from core.forms.common import CommonModelForm
from itam.models.device import DeviceType
class DeviceTypeForm(CommonModelForm):
class Meta:
fields = [
'name',
'slug',
'id',
'organization',
'is_global',
'model_notes',
]
model = DeviceType

View File

@ -1,14 +1,18 @@
from app import settings
from django import forms
from django.db.models import Q
from app import settings
from core.forms.common import CommonModelForm
from itam.models.operating_system import OperatingSystem
class Update(forms.ModelForm):
class OperatingSystemFormCommon(CommonModelForm):
class Meta:
model = OperatingSystem
fields = [
"name",
'publisher',
@ -19,6 +23,12 @@ class Update(forms.ModelForm):
'model_notes',
]
model = OperatingSystem
class Update(OperatingSystemFormCommon):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@ -0,0 +1,17 @@
from django.db.models import Q
from core.forms.common import CommonModelForm
from itam.models.operating_system import OperatingSystemVersion
class OperatingSystemVersionForm(CommonModelForm):
class Meta:
fields = [
'name',
]
model = OperatingSystemVersion

View File

@ -1,10 +1,12 @@
from django import forms
from django.db.models import Q
from core.forms.common import CommonModelForm
from itam.models.software import Software
class Update(forms.ModelForm):
class SoftwareForm(CommonModelForm):
class Meta:
model = Software
@ -19,6 +21,11 @@ class Update(forms.ModelForm):
'model_notes',
]
class SoftwareFormUpdate(SoftwareForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@ -0,0 +1,22 @@
from django.db.models import Q
from core.forms.common import CommonModelForm
from itam.models.software import SoftwareCategory
class SoftwareCategoryForm(CommonModelForm):
class Meta:
fields = [
'name',
'slug',
'id',
'organization',
'is_global',
'model_notes',
]
model = SoftwareCategory

View File

@ -0,0 +1,63 @@
# Generated by Django 5.0.6 on 2024-07-11 04:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('itam', '0015_alter_device_device_model_alter_device_device_type_and_more'),
]
operations = [
migrations.AlterField(
model_name='device',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'),
),
migrations.AlterField(
model_name='devicemodel',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'),
),
migrations.AlterField(
model_name='deviceoperatingsystem',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'),
),
migrations.AlterField(
model_name='devicesoftware',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'),
),
migrations.AlterField(
model_name='devicetype',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'),
),
migrations.AlterField(
model_name='operatingsystem',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'),
),
migrations.AlterField(
model_name='operatingsystemversion',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'),
),
migrations.AlterField(
model_name='software',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'),
),
migrations.AlterField(
model_name='softwarecategory',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'),
),
migrations.AlterField(
model_name='softwareversion',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'),
),
]

View File

@ -39,9 +39,9 @@
<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 class="tablinks" onclick="openCity(event, 'Software')">Software</button>
<button class="tablinks" onclick="openCity(event, 'Notes')">Notes</button>
<button class="tablinks" onclick="openCity(event, 'ConfigManagement')">Config Management</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>
@ -96,22 +96,46 @@
<div class="detail-view-field">
<label>{{ form.device_model.label }}</label>
<span>{{ form.device_model.value }}</span>
<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>{{ form.serial_number.value }}</span>
<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>{{ form.uuid.value }}</span>
<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>{{ device.device_type }}</span>
<span>
{% if device.device_type %}
{{ device.device_type }}
{% else %}
&nbsp;
{% endif %}
</span>
</div>
<div class="detail-view-field">
@ -121,7 +145,13 @@
<div class="detail-view-field">
<label>{{ form.lastinventory.label }}</label>
<span>{{ form.lastinventory.value }}</span>
<span>
{% if form.lastinventory.value %}
{{ form.lastinventory.value }}
{% else %}
&nbsp;
{% endif %}
</span>
</div>
</div>
@ -130,8 +160,13 @@
<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;">{{ form.model_notes.value | markdown | safe }}</div>
{% include 'icons/issue_link.html.j2' with issue=13 %}<br>
<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>
@ -147,10 +182,12 @@
<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>
@ -217,8 +254,8 @@
<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>
<a href="?page=1&tab=software">&laquo; first</a>
<a href="?page={{ page_obj.previous_page_number }}&tab=software">previous</a>
{% endif %}
<span class="current">
@ -226,11 +263,18 @@
</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>
<a href="?page={{ page_obj.next_page_number }}&tab=software">next</a>
<a href="?page={{ page_obj.paginator.num_pages }}&tab=software">last &raquo;</a>
{% endif %}
</span>
</div>
{% if tab == 'software' %}
<script>
// Get the element with id="defaultOpen" and click on it
document.getElementById("SoftwareOpen").click();
</script>
{% endif %}
</div>
@ -248,6 +292,12 @@
{% endif %}
</div>
{% if tab == 'notes' %}
<script>
// Get the element with id="defaultOpen" and click on it
document.getElementById("NotesOpen").click();
</script>
{% endif %}
</div>
@ -278,6 +328,13 @@
</tr>
{% endif %}
</table>
{% if tab == 'configmanagement' %}
<script>
// Get the element with id="defaultOpen" and click on it
document.getElementById("ConfigManagementOpen").click();
</script>
{% endif %}
</div>
</form>

View File

@ -88,19 +88,19 @@ span.status_icon {
<span class="status_icon status_icon_ok">
{% if device.status == 'OK' %}
<span class="status_icon status_icon_ok">
{% include 'icons/status_ok.svg' %}
{% include 'icons/inventory_status_ok.svg' %}
</span>
{% elif device.status == 'WARN' %}
<span class="status_icon status_icon_warn">
{% include 'icons/status_ok.svg' %}
{% include 'icons/inventory_status_warning.svg' %}
</span>
{% elif device.status == 'BAD' %}
<span class="status_icon status_icon_bad">
{% include 'icons/status_bad.svg' %}
{% include 'icons/inventory_status_bad.svg' %}
</span>
{% else %}
<span class="status_icon status_icon_ukn">
{% include 'icons/status_unknown.svg' %}
{% include 'icons/inventory_status_unknown.svg' %}
</span>
{% endif %}
</td>

View File

@ -44,7 +44,7 @@
{% csrf_token %}
{{ form }}
{% include 'icons/issue_link.html.j2' with issue=13 %}<br>
<br>
<input type="submit" value="Submit">
<script>

View File

@ -47,7 +47,7 @@
{% csrf_token %}
{{ form }}
{% include 'icons/issue_link.html.j2' with issue=13 %}<br>
<br>
<input type="submit" value="Submit">
<script>

View File

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

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 DeviceModelViews(
TestCase,
PrimaryModel
):
add_module = 'itam.views.device_model'
add_view = 'Add'
change_module = add_module
change_view = 'View'
delete_module = add_module
delete_view = 'Delete'
display_module = add_module
display_view = 'View'
index_module = 'settings.views.device_models'
index_view = 'Index'

View File

@ -0,0 +1,35 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import PrimaryModel
class DeviceOperatingSystemViews(
TestCase,
# PrimaryModel
):
add_module = 'itam.views.device_model'
add_view = 'Add'
change_module = add_module
change_view = 'View'
delete_module = add_module
delete_view = 'Delete'
display_module = add_module
display_view = 'View'
index_module = 'settings.views.device_models'
index_view = 'Index'
@pytest.mark.skip(reason="refactor moddel views to proper CRUD views")
def test_dummy(self):
pass

View File

@ -0,0 +1,28 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import ModelAdd, ModelChange, ModelDisplay
class DeviceSoftwareViews(
TestCase,
ModelAdd,
ModelChange,
ModelDisplay
):
add_module = 'itam.views.device'
add_view = 'SoftwareAdd'
change_module = add_module
change_view = 'SoftwareView'
# delete_module = add_module
# delete_view = 'Delete'
display_module = add_module
display_view = 'SoftwareView'

View File

@ -0,0 +1,32 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import ModelAdd, ModelChange, ModelDelete, ModelDisplay
class DeviceTypeViews(
TestCase,
ModelAdd,
ModelChange,
ModelDelete,
ModelDisplay
):
add_module = 'itam.views.device_type'
add_view = 'Add'
change_module = add_module
change_view = 'View'
delete_module = add_module
delete_view = 'Delete'
display_module = add_module
display_view = 'View'
# index_module = 'settings.views.device_models'
# index_view = 'Index'

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 OperatingSystemViews(
TestCase,
PrimaryModel
):
add_module = 'itam.views.operating_system'
add_view = 'Add'
change_module = add_module
change_view = 'View'
delete_module = add_module
delete_view = 'Delete'
display_module = add_module
display_view = 'View'
index_module = 'settings.views.device_models'
index_view = 'Index'

View File

@ -0,0 +1,32 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import ModelAdd, ModelChange, ModelDelete, ModelDisplay
class OperatingSystemVersionViews(
TestCase,
ModelAdd,
ModelChange,
ModelDelete,
ModelDisplay
):
add_module = 'itam.views.operating_system_version'
add_view = 'Add'
change_module = add_module
change_view = 'View'
delete_module = add_module
delete_view = 'Delete'
display_module = add_module
display_view = 'View'
# index_module = 'settings.views.device_models'
# index_view = 'Index'

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 SoftwareViews(
TestCase,
PrimaryModel
):
add_module = 'itam.views.software'
add_view = 'Add'
change_module = add_module
change_view = 'View'
delete_module = add_module
delete_view = 'Delete'
display_module = add_module
display_view = 'View'
index_module = add_module
index_view = 'IndexView'

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 SoftwareCategoryViews(
TestCase,
PrimaryModel
):
add_module = 'itam.views.software'
add_view = 'Add'
change_module = add_module
change_view = 'View'
delete_module = add_module
delete_view = 'Delete'
display_module = add_module
display_view = 'View'
index_module = 'settings.views.software_categories'
index_view = 'Index'

View File

@ -2,24 +2,23 @@ import json
import markdown
from django.contrib.auth import decorators as auth_decorator
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.paginator import Paginator
from django.db.models import Q
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import generic
from access.mixin import OrganizationPermission
from access.models import Organization
from config_management.models.groups import ConfigGroupHosts
from ..models.device import Device, DeviceSoftware, DeviceOperatingSystem
from ..models.software import Software
from core.forms.comment import AddNoteForm
from core.models.notes import Notes
from core.views.common import AddView, ChangeView, DeleteView, IndexView
from itam.forms.device_softwareadd import SoftwareAdd
from itam.forms.device_softwareupdate import SoftwareUpdate
@ -29,10 +28,18 @@ from itam.forms.device.operating_system import Update as OperatingSystemForm
from settings.models.user_settings import UserSettings
class IndexView(PermissionRequiredMixin, OrganizationPermission, generic.ListView):
class IndexView(IndexView):
model = Device
permission_required = 'itam.view_device'
permission_required = [
'itam.view_device'
]
template_name = 'itam/device_index.html.j2'
context_object_name = "devices"
paginate_by = 10
@ -62,13 +69,12 @@ def _get_form(request, formcls, prefix, **kwargs):
data = request.POST if prefix in request.POST else None
return formcls(data, prefix=prefix, **kwargs)
class View(OrganizationPermission, generic.UpdateView):
class View(ChangeView):
model = Device
permission_required = [
'itam.view_device',
'itam.change_device'
]
template_name = 'itam/device.html.j2'
@ -105,13 +111,21 @@ class View(OrganizationPermission, generic.UpdateView):
context['installed_software'] = len(DeviceSoftware.objects.filter(device=self.kwargs['pk']))
if hasattr(self.request.GET, 'page'):
if 'page' in self.request.GET:
context['page_number'] = int(self.request.GET.get("page"))
else:
context['page_number'] = 1
if 'tab' in self.request.GET:
context['tab'] = str(self.request.GET.get("tab")).lower()
else:
context['tab'] = None
context['page_obj'] = softwares.get_page(context['page_number'])
context['softwares'] = softwares.page(context['page_number']).object_list
@ -181,7 +195,7 @@ class View(OrganizationPermission, generic.UpdateView):
class SoftwareView(OrganizationPermission, generic.UpdateView):
class SoftwareView(ChangeView):
model = DeviceSoftware
permission_required = [
'itam.view_devicesoftware'
@ -218,7 +232,7 @@ class SoftwareView(OrganizationPermission, generic.UpdateView):
class Add(OrganizationPermission, generic.CreateView):
class Add(AddView):
form_class = DeviceForm
@ -253,7 +267,7 @@ class Add(OrganizationPermission, generic.CreateView):
class SoftwareAdd(PermissionRequiredMixin, OrganizationPermission, generic.CreateView):
class SoftwareAdd(AddView):
model = DeviceSoftware
permission_required = [
'itam.add_devicesoftware',
@ -313,7 +327,7 @@ class SoftwareAdd(PermissionRequiredMixin, OrganizationPermission, generic.Creat
class Delete(OrganizationPermission, generic.DeleteView):
class Delete(DeleteView):
model = Device
permission_required = [
'itam.delete_device',
@ -334,7 +348,7 @@ class Delete(OrganizationPermission, generic.DeleteView):
return context
class Change(OrganizationPermission, generic.UpdateView):
class Change(ChangeView):
model = Device
permission_required = [
'itam.change_device',

View File

@ -1,35 +1,31 @@
from django.contrib.auth import decorators as auth_decorator
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import generic
from access.mixin import OrganizationPermission
from itam.forms.device_model import DeviceModelForm
from itam.models.device_models import DeviceModel
from core.views.common import AddView, ChangeView, DeleteView
from settings.models.user_settings import UserSettings
class View(OrganizationPermission, generic.UpdateView):
class View(ChangeView):
form_class = DeviceModelForm
context_object_name = "device_model"
model = DeviceModel
permission_required = [
'itam.view_devicemodel',
'itam.change_devicemodel',
]
template_name = 'form.html.j2'
fields = [
"name",
'slug',
'manufacturer',
'id',
'organization',
'is_global',
'model_notes',
]
context_object_name = "device_model"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -55,18 +51,17 @@ class View(OrganizationPermission, generic.UpdateView):
class Add(OrganizationPermission, generic.CreateView):
class Add(AddView):
form_class = DeviceModelForm
model = DeviceModel
permission_required = [
'itam.add_devicemodel',
]
template_name = 'form.html.j2'
fields = [
'name',
'manufacturer',
'organization',
'is_global'
]
def get_initial(self):
@ -90,7 +85,7 @@ class Add(OrganizationPermission, generic.CreateView):
class Delete(OrganizationPermission, generic.DeleteView):
class Delete(DeleteView):
model = DeviceModel
permission_required = [
'itam.delete_devicemodel',

View File

@ -1,32 +1,28 @@
from django.contrib.auth import decorators as auth_decorator
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import generic
from access.mixin import OrganizationPermission
from core.views.common import AddView, ChangeView, DeleteView, IndexView
from ..models.device import DeviceType
from itam.models.device import DeviceType
from itam.forms.device_type import DeviceTypeForm
from settings.models.user_settings import UserSettings
class View(OrganizationPermission, generic.UpdateView):
class View(ChangeView):
form_class = DeviceTypeForm
model = DeviceType
permission_required = [
'itam.view_devicetype',
'itam.change_devicetype'
]
template_name = 'form.html.j2'
fields = [
"name",
'slug',
'id',
'organization',
'is_global',
'model_notes',
]
template_name = 'form.html.j2'
context_object_name = "device_category"
@ -52,17 +48,17 @@ class View(OrganizationPermission, generic.UpdateView):
class Add(OrganizationPermission, generic.CreateView):
class Add(AddView):
form_class = DeviceTypeForm
model = DeviceType
permission_required = [
'itam.add_devicetype',
]
template_name = 'form.html.j2'
fields = [
'name',
'organization',
'is_global'
]
def get_initial(self):
@ -86,7 +82,7 @@ class Add(OrganizationPermission, generic.CreateView):
class Delete(OrganizationPermission, generic.DeleteView):
class Delete(DeleteView):
model = DeviceType
permission_required = [
'itam.delete_devicetype',

View File

@ -2,23 +2,23 @@ from django.contrib.auth import decorators as auth_decorator
from django.db.models import Q, Count
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import generic
from access.mixin import OrganizationPermission
from core.forms.comment import AddNoteForm
from core.models.notes import Notes
from 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 Update
from itam.forms.operating_system.update import OperatingSystemFormCommon, Update
from settings.models.user_settings import UserSettings
class IndexView(OrganizationPermission, generic.ListView):
class IndexView(IndexView):
model = OperatingSystem
permission_required = 'itam.view_operating_system'
permission_required = [
'itam.view_operatingsystem'
]
template_name = 'itam/operating_system_index.html.j2'
context_object_name = "operating_systems"
paginate_by = 10
@ -44,18 +44,21 @@ class IndexView(OrganizationPermission, generic.ListView):
class View(OrganizationPermission, generic.UpdateView):
class View(ChangeView):
context_object_name = "operating_system"
form_class = Update
model = OperatingSystem
permission_required = [
'itam.view_operatingsystem',
'itam.change_operatingsystem',
]
template_name = 'itam/operating_system.html.j2'
form_class = Update
context_object_name = "operating_system"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -103,18 +106,17 @@ class View(OrganizationPermission, generic.UpdateView):
class Add(OrganizationPermission, generic.CreateView):
class Add(AddView):
form_class = OperatingSystemFormCommon
model = OperatingSystem
permission_required = [
'itam.add_operatingsystem',
]
template_name = 'form.html.j2'
fields = [
'name',
'publisher',
'organization',
'is_global'
]
def get_initial(self):
@ -138,7 +140,7 @@ class Add(OrganizationPermission, generic.CreateView):
class Delete(OrganizationPermission, generic.DeleteView):
class Delete(DeleteView):
model = OperatingSystem

View File

@ -1,25 +1,24 @@
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.urls import reverse
from django.views import generic
from access.mixin import OrganizationPermission
from core.views.common import AddView, ChangeView, DeleteView
from ..models.operating_system import OperatingSystem, OperatingSystemVersion
from itam.forms.operating_system_version import OperatingSystemVersionForm
from itam.models.operating_system import OperatingSystem, OperatingSystemVersion
class View(OrganizationPermission, generic.UpdateView):
class View(ChangeView):
form_class = OperatingSystemVersionForm
model = OperatingSystemVersion
permission_required = [
'itam.view_operating_systemversion'
]
template_name = 'form.html.j2'
fields = [
"name",
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -34,15 +33,17 @@ class View(OrganizationPermission, generic.UpdateView):
class Add(PermissionRequiredMixin, OrganizationPermission, generic.CreateView):
class Add(AddView):
form_class = OperatingSystemVersionForm
model = OperatingSystemVersion
permission_required = [
'access.add_operating_systemversion',
]
template_name = 'form.html.j2'
fields = [
'name'
]
def form_valid(self, form):
operating_system = OperatingSystem.objects.get(pk=self.kwargs['pk'])
@ -67,7 +68,7 @@ class Add(PermissionRequiredMixin, OrganizationPermission, generic.CreateView):
class Delete(PermissionRequiredMixin, OrganizationPermission, generic.DeleteView):
class Delete(DeleteView):
model = OperatingSystemVersion
permission_required = [
'access.delete_operating_system',

View File

@ -1,30 +1,35 @@
from django.contrib.auth import decorators as auth_decorator
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import Count, Q
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import generic
from access.mixin import OrganizationPermission
from core.forms.comment import AddNoteForm
from core.models.notes import Notes
from 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 Update as SoftwareUpdate_Form
from itam.forms.software.update import SoftwareForm, SoftwareFormUpdate
from settings.models.user_settings import UserSettings
class IndexView(PermissionRequiredMixin, OrganizationPermission, generic.ListView):
model = Software
permission_required = 'itam.view_software'
template_name = 'itam/software_index.html.j2'
class IndexView(IndexView):
context_object_name = "softwares"
model = Software
paginate_by = 10
permission_required = [
'itam.view_software'
]
template_name = 'itam/software_index.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -46,19 +51,21 @@ class IndexView(PermissionRequiredMixin, OrganizationPermission, generic.ListVie
class View(ChangeView):
context_object_name = "software"
form_class = SoftwareFormUpdate
class View(OrganizationPermission, generic.UpdateView):
model = Software
permission_required = [
'itam.view_software',
'itam.change_software'
]
template_name = 'itam/software.html.j2'
form_class = SoftwareUpdate_Form
context_object_name = "software"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -128,19 +135,17 @@ class View(OrganizationPermission, generic.UpdateView):
class Add(OrganizationPermission, generic.CreateView):
class Add(AddView):
form_class = SoftwareForm
model = Software
permission_required = [
'itam.add_software',
]
template_name = 'form.html.j2'
fields = [
'name',
'publisher',
'category',
'organization',
'is_global'
]
def get_initial(self):
@ -162,7 +167,7 @@ class Add(OrganizationPermission, generic.CreateView):
return context
class Delete(OrganizationPermission, generic.DeleteView):
class Delete(DeleteView):
model = Software
permission_required = [
'itam.delete_software',

View File

@ -1,34 +1,30 @@
from django.contrib.auth import decorators as auth_decorator
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import generic
from access.mixin import OrganizationPermission
from core.views.common import AddView, ChangeView, DeleteView
from ..models.software import Software, SoftwareCategory
from itam.forms.software_category import SoftwareCategoryForm
from itam.models.software import Software, SoftwareCategory
from settings.models.user_settings import UserSettings
class View(OrganizationPermission, generic.UpdateView):
class View(ChangeView):
context_object_name = "software"
form_class = SoftwareCategoryForm
model = SoftwareCategory
permission_required = [
'itam.view_softwarecategory',
'itam.change_softwarecategory',
]
template_name = 'form.html.j2'
fields = [
"name",
'slug',
'id',
'organization',
'is_global',
'model_notes',
]
context_object_name = "software"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -52,18 +48,17 @@ class View(OrganizationPermission, generic.UpdateView):
class Add(OrganizationPermission, generic.CreateView):
class Add(AddView):
form_class = SoftwareCategoryForm
model = SoftwareCategory
permission_required = [
'itam.add_softwarecategory',
]
template_name = 'form.html.j2'
fields = [
'name',
'organization',
'is_global'
]
template_name = 'form.html.j2'
def get_initial(self):
@ -86,7 +81,7 @@ class Add(OrganizationPermission, generic.CreateView):
class Delete(OrganizationPermission, generic.DeleteView):
class Delete(DeleteView):
model = SoftwareCategory
permission_required = [
'itam.delete_softwarecategory',

View File

@ -1,12 +1,9 @@
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.views import generic
from access.mixin import OrganizationPermission
from core.views.common import AddView, ChangeView
from ..models.software import Software, SoftwareVersion
class View(OrganizationPermission, generic.UpdateView):
class View(ChangeView):
model = SoftwareVersion
permission_required = [
'itam.view_softwareversion'
@ -30,7 +27,7 @@ class View(OrganizationPermission, generic.UpdateView):
class Add(PermissionRequiredMixin, OrganizationPermission, generic.CreateView):
class Add(AddView):
model = SoftwareVersion
permission_required = [
'access.add_softwareversion',

View File

@ -6,7 +6,7 @@ from itam.views import device, device_type, software, software_category, softwar
app_name = "ITIM"
urlpatterns = [
path("clusters", device.IndexView.as_view(), name="Clusters"),
path("services", device.IndexView.as_view(), name="Services"),
# path("clusters", device.IndexView.as_view(), name="Clusters"),
# path("services", device.IndexView.as_view(), name="Services"),
]

View File

@ -188,6 +188,24 @@ footer {
vertical-align: middle;
padding: 0%;
margin: 0%;
color: #aaa;
font-size: 12px;
}
footer span {
display: inline-block;
width: 33%;
padding: 0px 10px 0px 10px;
margin: 0px;
vertical-align: middle;
height: 100%;
/*line-height: 76px;*/
}
footer span svg {
height: 100%;
width: 30px;
fill: #177ee6
}
/* Style The Dropdown Button */
@ -288,13 +306,15 @@ nav button.collapsible {
background-color: inherit;
color: white;
cursor: pointer;
padding: 18px;
padding: 0px 18px 0px 18px;
width: 100%;
border: none;
text-align: left;
outline: none;
font-size: 15px;
margin: 0px;
line-height: 50px;
vertical-align: middle;
}
nav button.active, .collapsible:hover {
@ -302,14 +322,14 @@ nav button.active, .collapsible:hover {
}
nav button.collapsible:after {
content: '\002B';
content: url('/static/icons/nav_arrow_right.svg');
color: white;
float: right;
margin-left: 5px;
}
nav button.active:after {
content: "\2212";
content: url('/static/icons/nav_arrow_down.svg');
font-weight: bold;
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#fff"><path d="M480-333 240-573l51-51 189 189 189-189 51 51-240 240Z"/></svg>

After

Width:  |  Height:  |  Size: 175 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#fff"><path d="M522-480 333-669l51-51 240 240-240 240-51-51 189-189Z"/></svg>

After

Width:  |  Height:  |  Size: 175 B

View File

@ -4,6 +4,6 @@ from .views import ProjectIndex
app_name = "Project Management"
urlpatterns = [
path('', ProjectIndex.as_view(), name='Projects'),
# path('', ProjectIndex.as_view(), name='Projects'),
]

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