Merge branch 'development' into 'master'
chore: release 0.7.0 See merge request nofusscomputing/projects/centurion_erp!35
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,10 +1,11 @@
|
||||
venv/**
|
||||
*/static/**
|
||||
__pycache__
|
||||
**db.sqlite3
|
||||
**.sqlite3
|
||||
**.sqlite
|
||||
**.coverage
|
||||
artifacts/
|
||||
**.tmp.*
|
||||
volumes/
|
||||
build/
|
||||
pages/
|
||||
pages/
|
||||
|
@ -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
|
||||
|
@ -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
17
.vscode/launch.json
vendored
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -13,4 +13,8 @@
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"testing.coverageToolbarEnabled": true,
|
||||
"cSpell.words": [
|
||||
"ITSM"
|
||||
],
|
||||
"cSpell.language": "en-AU",
|
||||
}
|
@ -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
|
||||
|
||||
```
|
||||
|
||||
|
66
README.md
66
README.md
@ -1,21 +1,65 @@
|
||||
<span style="text-align: center;">
|
||||
|
||||

|
||||
# No Fuss Computing - Centurion ERP
|
||||
|
||||
<br>
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
[](https://hub.docker.com/r/nofusscomputing/centurion-erp)
|
||||
|
||||
|
||||
artifacts
|
||||
|
||||
----
|
||||
|
||||
<br>
|
||||
|
||||
  [](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues) [](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
|
||||
  
|
||||
<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**
|
||||
|
||||
  [](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/jobs/artifacts/master/browse/artifacts/coverage/?job=Unit)
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Development Branch**
|
||||
|
||||
  [](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)
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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,9 +55,11 @@ class OrganizationMixin():
|
||||
|
||||
id = obj.get_organization().id
|
||||
|
||||
if obj.is_global:
|
||||
if hasattr(obj, 'is_global'):
|
||||
|
||||
id = 0
|
||||
if obj.is_global:
|
||||
|
||||
id = 0
|
||||
|
||||
|
||||
except AttributeError:
|
||||
@ -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,10 +233,32 @@ 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
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
raise PermissionDenied('You are not part of this organization')
|
||||
|
||||
return super().dispatch(self.request, *args, **kwargs)
|
||||
|
@ -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:
|
||||
|
@ -1,6 +1,5 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}Organizations{% endblock %}
|
||||
{% block content_header_icon %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -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">
|
||||
|
@ -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 }}
|
||||
|
||||
|
@ -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'
|
29
app/access/tests/unit/team/test_team_views.py
Normal file
29
app/access/tests/unit/team/test_team_views.py
Normal 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'
|
||||
|
30
app/access/tests/unit/team_user/test_team_user_views.py
Normal file
30
app/access/tests/unit/team_user/test_team_user_views.py
Normal 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'
|
@ -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
|
||||
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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
323
app/api/tasks.py
Normal 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}')
|
@ -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 """
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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:
|
||||
|
||||
|
@ -0,0 +1,3 @@
|
||||
from .celery import worker as celery_app
|
||||
|
||||
__all__ = ('celery_app',)
|
||||
|
18
app/app/celery.py
Normal file
18
app/app/celery.py
Normal 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}')
|
@ -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,11 +144,45 @@ def nav_items(context) -> list(dict()):
|
||||
|
||||
name = str(pattern.name)
|
||||
|
||||
nav_items = nav_items + [ {
|
||||
'name': name,
|
||||
'url': url,
|
||||
'is_active': is_active
|
||||
} ]
|
||||
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,
|
||||
'is_active': is_active
|
||||
} ]
|
||||
|
||||
if len(nav_items) > 0:
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
63
app/app/tests/abstract/models.py
Normal file
63
app/app/tests/abstract/models.py
Normal 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
|
||||
"""
|
565
app/app/tests/abstract/views.py
Normal file
565
app/app/tests/abstract/views.py
Normal 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 """
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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'])
|
||||
)
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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'),
|
||||
),
|
||||
]
|
@ -48,7 +48,7 @@
|
||||
|
||||
{% csrf_token %}
|
||||
{{ form }}
|
||||
{% include 'icons/issue_link.html.j2' with issue=13 %}<br>
|
||||
<br>
|
||||
<input type="submit" value="Submit">
|
||||
|
||||
<script>
|
||||
|
@ -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'
|
@ -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'
|
@ -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'),
|
||||
|
||||
]
|
||||
|
@ -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
|
||||
form_class.fields["host"].queryset = form_class.fields["host"].queryset.filter(
|
||||
).exclude(
|
||||
id__in=exsting_group_hosts.values_list('host', flat=True)
|
||||
)
|
||||
|
||||
if group.is_global:
|
||||
|
||||
form_class.fields["host"].queryset = Device.objects.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'
|
||||
|
@ -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
5
app/core/exceptions.py
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
class MissingAttribute(Exception):
|
||||
""" An attribute is missing"""
|
||||
|
||||
pass
|
0
app/core/forms/__init__.py
Normal file
0
app/core/forms/__init__.py
Normal 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
100
app/core/forms/common.py
Normal 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)
|
||||
)
|
||||
|
||||
|
20
app/core/forms/manufacturer.py
Normal file
20
app/core/forms/manufacturer.py
Normal 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
|
@ -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'),
|
||||
),
|
||||
]
|
138
app/core/templates/celery_log.html.j2
Normal file
138
app/core/templates/celery_log.html.j2
Normal 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 %}
|
25
app/core/templates/celery_log_index.html.j2
Normal file
25
app/core/templates/celery_log_index.html.j2
Normal 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 %}
|
29
app/core/tests/unit/manufacturer/test_manufacturer_views.py
Normal file
29
app/core/tests/unit/manufacturer/test_manufacturer_views.py
Normal 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'
|
35
app/core/tests/unit/test_history/test_history_views.py
Normal file
35
app/core/tests/unit/test_history/test_history_views.py
Normal 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
|
105
app/core/tests/unit/test_task_result/test_task_result.py
Normal file
105
app/core/tests/unit/test_task_result/test_task_result.py
Normal 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
|
@ -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'
|
0
app/core/views/__init__.py
Normal file
0
app/core/views/__init__.py
Normal file
74
app/core/views/celery_log.py
Normal file
74
app/core/views/celery_log.py
Normal 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
76
app/core/views/common.py
Normal 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)
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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'
|
||||
|
||||
|
23
app/itam/forms/device_model.py
Normal file
23
app/itam/forms/device_model.py
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
22
app/itam/forms/device_type.py
Normal file
22
app/itam/forms/device_type.py
Normal 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
|
@ -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)
|
||||
|
17
app/itam/forms/operating_system_version.py
Normal file
17
app/itam/forms/operating_system_version.py
Normal 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
|
@ -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)
|
||||
|
||||
|
22
app/itam/forms/software_category.py
Normal file
22
app/itam/forms/software_category.py
Normal 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
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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 %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% 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">« first</a>
|
||||
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
|
||||
<a href="?page=1&tab=software">« 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 »</a>
|
||||
<a href="?page={{ page_obj.next_page_number }}&tab=software">next</a>
|
||||
<a href="?page={{ page_obj.paginator.num_pages }}&tab=software">last »</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>
|
||||
|
@ -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>
|
||||
|
@ -44,7 +44,7 @@
|
||||
|
||||
{% csrf_token %}
|
||||
{{ form }}
|
||||
{% include 'icons/issue_link.html.j2' with issue=13 %}<br>
|
||||
<br>
|
||||
<input type="submit" value="Submit">
|
||||
|
||||
<script>
|
||||
|
@ -47,7 +47,7 @@
|
||||
|
||||
{% csrf_token %}
|
||||
{{ form }}
|
||||
{% include 'icons/issue_link.html.j2' with issue=13 %}<br>
|
||||
<br>
|
||||
<input type="submit" value="Submit">
|
||||
|
||||
<script>
|
||||
|
28
app/itam/tests/unit/device/test_device_views.py
Normal file
28
app/itam/tests/unit/device/test_device_views.py
Normal 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'
|
29
app/itam/tests/unit/device_model/test_device_model_views.py
Normal file
29
app/itam/tests/unit/device_model/test_device_model_views.py
Normal 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'
|
@ -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
|
@ -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'
|
32
app/itam/tests/unit/device_type/test_device_type_views.py
Normal file
32
app/itam/tests/unit/device_type/test_device_type_views.py
Normal 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'
|
@ -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'
|
@ -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'
|
29
app/itam/tests/unit/software/test_software_views.py
Normal file
29
app/itam/tests/unit/software/test_software_views.py
Normal 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'
|
@ -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'
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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"),
|
||||
|
||||
]
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
1
app/project-static/icons/nav_arrow_down.svg
Normal file
1
app/project-static/icons/nav_arrow_down.svg
Normal 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 |
1
app/project-static/icons/nav_arrow_right.svg
Normal file
1
app/project-static/icons/nav_arrow_right.svg
Normal 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 |
@ -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
Reference in New Issue
Block a user