Merge branch 'development' into 'master'

chore: release 0.3.0

See merge request nofusscomputing/projects/django_template!14
This commit is contained in:
2024-05-29 01:23:28 +00:00
200 changed files with 11232 additions and 889 deletions

View File

@ -31,6 +31,101 @@ include:
- template/automagic.gitlab-ci.yaml
Docker Container:
extends: .build_docker_container
resource_group: build
needs: []
script:
- update-binfmts --display
- |
echo "[DEBUG] building multiarch/specified arch image";
docker buildx build --platform=$DOCKER_IMAGE_BUILD_TARGET_PLATFORMS . \
--label org.opencontainers.image.created="$(date '+%Y-%m-%d %H:%M:%S%:z')" \
--label org.opencontainers.image.documentation="$CI_PROJECT_URL" \
--label org.opencontainers.image.source="$CI_PROJECT_URL" \
--label org.opencontainers.image.revision="$CI_COMMIT_SHA" \
--push \
--build-arg CI_PROJECT_URL=$CI_PROJECT_URL \
--build-arg CI_COMMIT_SHA=$CI_COMMIT_SHA \
--build-arg CI_COMMIT_TAG=$CI_COMMIT_TAG \
--file $DOCKER_DOCKERFILE \
--tag $DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG;
docker buildx imagetools inspect $DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG;
# during docker multi platform build there are >=3 additional unknown images added to gitlab container registry. cleanup
DOCKER_MULTI_ARCH_IMAGES=$(docker buildx imagetools inspect "$DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG" --format "{{ range .Manifest.Manifests }}{{ if ne (print .Platform) \"&{unknown unknown [] }\" }}$DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG@{{ println .Digest }}{{end}} {{end}}");
docker buildx imagetools create $DOCKER_MULTI_ARCH_IMAGES --tag $DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG;
docker buildx imagetools inspect $DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG;
rules: # rules manually synced from docker/publish.gitlab-ci.yaml removing git tag
# - if: # condition_master_branch_push
# $CI_COMMIT_BRANCH == "master" &&
# $CI_PIPELINE_SOURCE == "push"
# exists:
# - '{dockerfile,dockerfile.j2}'
# when: always
- if: # condition_not_master_or_dev_push
$CI_COMMIT_BRANCH != "master" &&
$CI_COMMIT_BRANCH != "development" &&
$CI_PIPELINE_SOURCE == "push"
exists:
- '{dockerfile,dockerfile.j2}'
changes:
paths:
- '{dockerfile,dockerfile.j2,includes/**/*}'
compare_to: 'development'
when: always
- if: # condition_dev_branch_push
(
$CI_COMMIT_BRANCH == "development"
||
$CI_COMMIT_BRANCH == "master"
)
&&
$CI_PIPELINE_SOURCE == "push"
exists:
- '{dockerfile,dockerfile.j2}'
allow_failure: true
when: on_success
- when: never
Docker.Hub.Branch.Publish:
extends: .publish-docker-hub
needs: [ "Docker Container" ]
resource_group: build
rules: # rules manually synced from docker/publish.gitlab-ci.yaml removing git tag
# - if: # condition_master_branch_push
# $CI_COMMIT_BRANCH == "master" &&
# $CI_PIPELINE_SOURCE == "push"
# exists:
# - '{dockerfile,dockerfile.j2}'
# when: always
- if: # condition_dev_branch_push
$CI_COMMIT_BRANCH == "development" &&
$CI_PIPELINE_SOURCE == "push"
exists:
- '{dockerfile,dockerfile.j2}'
allow_failure: true
when: on_success
- when: never
Website.Submodule.Deploy:
extends: .submodule_update_trigger
variables:

View File

@ -1,6 +1,7 @@
{
"recommendations": [
"ms-python.python",
"ms-python.debugpy",
"njpwerner.autodocstring",
"streetsidesoftware.code-spell-checker-australian-english",
"streetsidesoftware.code-spell-checker",

20
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,20 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Debug: Django",
"type": "debugpy",
"request": "launch",
"args": [
"runserver",
"0.0.0.0:8002"
],
"django": true,
"autoStartBrowser": false,
"program": "${workspaceFolder}/app/manage.py"
}
]
}

View File

@ -4,4 +4,9 @@
"cSpell.enableFiletypes": [
"!python"
],
"python.testing.pytestArgs": [
"app"
],
"python.testing.unittestEnabled": true,
"python.testing.pytestEnabled": true,
}

View File

@ -35,7 +35,46 @@ python3 manage.py makemigrations --noinput
Updates to python modules will need to be captured with SCM. This can be done by running `pip freeze > requirements.txt` from the running virtual environment.
## Running Tests
## Tests
!!! danger "Requirement"
All models **are** to have tests written for them, Including testing between dependent models.
To ensure consistency and reliability of this application, tests are to be written. Each test is to test one item ONLY and no more. Each module is to contain a tests directory of the model being tested with a single file for grouping of what is being tested. for items that depend upon a parent model, the test file is to be within the child-models test directory named with format `test_<model>_<parent app>_<parent model name>`
_example structure for the device model that relies upon access app model organization, core app model history and model notes._
``` text
├── tests
│   ├── device
│   │   ├── test_device_access_organization.py
│   │   ├── test_device_api_permission.py
│   │   ├── test_device_core_history.py
│   │   ├── test_device_core_notes.py
│   │   ├── test_device_permission.py
│   │   └── test_device.py
```
Items to test include but are not limited to:
- CRUD permissions admin site
- CRUD permissions api site
- CRUD permissions main site
- can only access organization object
- can access global object (still to require model CRUD permission)
- parent models
### Running Tests
test can be run by running the following:

8
README.md Normal file
View File

@ -0,0 +1,8 @@
![GitLab Bugs](https://img.shields.io/gitlab/issues/open/nofusscomputing%2Fprojects%2Fdjango_template?labels=type%3A%3Abug&style=plastic&logo=gitlab&label=Bug%20Fixes%20Required&color=fc6d26)
![GitLab Issues](https://img.shields.io/gitlab/issues/open/nofusscomputing%2Fprojects%2Fdjango_template?style=plastic&logo=gitlab&label=Issues&color=fc6d26)
![Docker Pulls](https://img.shields.io/docker/pulls/nofusscomputing/django-template?style=plastic&logo=docker&color=0db7ed)

View File

@ -0,0 +1,19 @@
# Generated by Django 5.0.6 on 2024-05-23 10:37
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='team',
name='organization',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization'),
),
]

View File

@ -1,5 +1,5 @@
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.auth.mixins import AccessMixin, PermissionRequiredMixin
from django.contrib.auth.models import Group
from django.core.exceptions import PermissionDenied
from django.utils.functional import cached_property
@ -16,18 +16,28 @@ class OrganizationMixin():
def object_organization(self) -> int:
if 'access.models.Organization' in str(type(self.get_object())):
id = None
id = self.get_object().id
try:
else:
self.get_queryset()
id = self.get_object().organization.id
self.get_object()
id = self.get_object().get_organization().id
if self.get_object().is_global:
id = 0
except AttributeError:
if self.request.method == 'POST':
if self.request.POST.get("organization", ""):
id = int(self.request.POST.get("organization", ""))
return id
@ -124,7 +134,7 @@ class OrganizationMixin():
assembled_permission = str(permission["content_type__app_label"]) + '.' + str(permission["codename"])
if assembled_permission in self.get_permission_required()[0] and (team['organization_id'] == self.object_organization() or self.object_organization() == 0):
if assembled_permission in self.get_permission_required() and (team['organization_id'] == self.object_organization() or self.object_organization() == 0):
return True
@ -132,13 +142,16 @@ class OrganizationMixin():
class OrganizationPermission(OrganizationMixin):
class OrganizationPermission(AccessMixin, OrganizationMixin):
"""checking organization membership"""
def dispatch(self, request, *args, **kwargs):
self.request = request
if not request.user.is_authenticated:
return self.handle_no_permission()
if hasattr(self, 'get_object'):
if not self.has_permission() and not request.user.is_superuser:

View File

@ -1,11 +1,14 @@
from django.conf import settings
from django.db import models
from django.contrib.auth.models import Group, Permission
from django.contrib.auth.models import User, Group, Permission
from .fields import *
from core.middleware.get_request import get_request
from core.mixin.history_save import SaveHistory
class Organization(models.Model):
class Organization(SaveHistory):
class Meta:
verbose_name_plural = "Organizations"
@ -40,6 +43,10 @@ class Organization(models.Model):
modified = AutoLastModifiedField()
def get_organization(self):
return self
class TenancyObject(models.Model):
class Meta:
@ -48,6 +55,8 @@ class TenancyObject(models.Model):
organization = models.ForeignKey(
Organization,
on_delete=models.CASCADE,
blank = False,
null = True,
)
is_global = models.BooleanField(
@ -55,8 +64,11 @@ class TenancyObject(models.Model):
blank = False
)
def get_organization(self) -> Organization:
return self.organization
class Team(Group, TenancyObject):
class Team(Group, TenancyObject, SaveHistory):
class Meta:
# proxy = True
verbose_name_plural = "Teams"
@ -67,7 +79,6 @@ class Team(Group, TenancyObject):
def save(self, *args, **kwargs):
self.name = self.organization.name.lower().replace(' ', '_') + '_' + self.team_name.lower().replace(' ', '_')
super().save(*args, **kwargs)
@ -86,7 +97,7 @@ class Team(Group, TenancyObject):
modified = AutoLastModifiedField()
class TeamUsers(models.Model):
class TeamUsers(SaveHistory):
class Meta:
# proxy = True
@ -118,3 +129,40 @@ class TeamUsers(models.Model):
created = AutoCreatedField()
modified = AutoLastModifiedField()
def delete(self, using=None, keep_parents=False):
""" Delete Team
Overrides, post-action
As teams are an extension of Groups, remove the user to the team.
"""
super().delete(using=using, keep_parents=keep_parents)
group = Group.objects.get(pk=self.team.id)
user = User.objects.get(pk=self.user_id)
user.groups.remove(group)
def get_organization(self) -> Organization:
return self.team.organization
def save(self, *args, **kwargs):
""" Save Team
Overrides, post-action
As teams are an extension of groups, add the user to the matching group.
"""
super().save(*args, **kwargs)
group = Group.objects.get(pk=self.team.id)
user = User.objects.get(pk=self.user_id)
user.groups.add(group)

View File

@ -10,6 +10,7 @@
<fieldset><label>Created</label><input type="text" value="{{ organization.created }}" readonly /></fieldset>
<fieldset><label>Modified</label><input type="text" value="{{ organization.modified }}" readonly /></fieldset>
</section>
{% include 'icons/issue_link.html.j2' with issue=13 %}<br>
<input type="button" value="<< Back" onclick="window.location='{% url 'Access:Organizations' %}';">
<input type="button" value="New Team" onclick="window.location='{% url 'Access:_team_add' organization.id %}';">
@ -25,7 +26,7 @@
</thead>
{% for field in teams %}
<tr>
<td><a href="{% url 'Access:_team' organization_id=organization.id pk=field.id %}">{{ field.team_name }}</a></td>
<td><a href="{% url 'Access:_team_view' organization_id=organization.id pk=field.id %}">{{ field.team_name }}</a></td>
<td>{{ field.created }}</td>
<td>{{ field.modified }}</td>
</tr>

View File

@ -22,13 +22,14 @@
</fieldset>
</section>
</div>
{% include 'icons/issue_link.html.j2' with issue=13 %}<br>
<input style="display:unset;" type="submit" value="Submit">
</form>
<hr />
<input type="button" value="<< Back" onclick="window.location='{% url 'Access:_organization' pk=organization.id %}';">
<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"

View File

@ -0,0 +1,532 @@
# from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
import requests
from access.models import Organization, Team, TeamUsers, Permission
class OrganizationPermissions(TestCase):
model = Organization
model_name = 'organization'
app_label = 'access'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
. create an organization that is different to item
2. Create a device
3. create teams with each permission: view, add, change, delete
4. create a user per team
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
different_organization = Organization.objects.create(name='test_different_organization')
# self.item = self.model.objects.create(
# organization=organization,
# name = 'deviceone'
# )
self.item = organization
view_permissions = Permission.objects.get(
codename = 'view_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = organization,
)
view_team.permissions.set([view_permissions])
add_permissions = Permission.objects.get(
codename = 'add_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
add_team = Team.objects.create(
team_name = 'add_team',
organization = organization,
)
add_team.permissions.set([add_permissions])
change_permissions = Permission.objects.get(
codename = 'change_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
change_team = Team.objects.create(
team_name = 'change_team',
organization = organization,
)
change_team.permissions.set([change_permissions])
delete_permissions = Permission.objects.get(
codename = 'delete_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
delete_team = Team.objects.create(
team_name = 'delete_team',
organization = organization,
)
delete_team.permissions.set([delete_permissions])
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
self.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
self.add_user = User.objects.create_user(username="test_user_add", password="password")
teamuser = TeamUsers.objects.create(
team = add_team,
user = self.add_user
)
self.change_user = User.objects.create_user(username="test_user_change", password="password")
teamuser = TeamUsers.objects.create(
team = change_team,
user = self.change_user
)
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
teamuser = TeamUsers.objects.create(
team = delete_team,
user = self.delete_user
)
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
different_organization_team = Team.objects.create(
team_name = 'different_organization_team',
organization = different_organization,
)
different_organization_team.permissions.set([
view_permissions,
add_permissions,
change_permissions,
delete_permissions,
])
TeamUsers.objects.create(
team = different_organization_team,
user = self.different_organization_user
)
def test_organization_auth_view_user_anon_denied(self):
""" Check correct permission for view
Attempt to view as anon user
"""
client = Client()
url = reverse('Access:_organization_view', kwargs={'pk': self.item.id})
response = client.get(url)
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_organization_auth_view_no_permission_denied(self):
""" Check correct permission for view
Attempt to view with user missing permission
"""
client = Client()
url = reverse('Access:_organization_view', kwargs={'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.get(url)
assert response.status_code == 403
def test_organization_auth_view_different_organizaiton_denied(self):
""" Check correct permission for view
Attempt to view with user from different organization
"""
client = Client()
url = reverse('Access:_organization_view', kwargs={'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.get(url)
assert response.status_code == 403
def test_organization_auth_view_has_permission(self):
""" Check correct permission for view
Attempt to view as user with view permission
"""
client = Client()
url = reverse('Access:_organization_view', kwargs={'pk': self.item.id})
client.force_login(self.view_user)
response = client.get(url)
assert response.status_code == 200
@pytest.mark.skip(reason="No Add view exists")
def test_organization_auth_add_user_anon_denied(self):
""" Check correct permission for add
Attempt to add as anon user
"""
client = Client()
url = reverse('Access:_organization_add')
response = client.put(url, data={'device': 'device'})
assert (
response.status_code == 302
or
response.status_code == 403
)
@pytest.mark.skip(reason="No Add view exists")
def test_organization_auth_add_no_permission_denied(self):
""" Check correct permission for add
Attempt to add as user with no permissions
"""
client = Client()
url = reverse('Access:_organization_add')
client.force_login(self.no_permissions_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
@pytest.mark.skip(reason="No Add view exists")
def test_organization_auth_add_different_organization_denied(self):
""" Check correct permission for add
attempt to add as user from different organization
"""
client = Client()
url = reverse('Access:_organization_add')
client.force_login(self.different_organization_user)
response = client.post(url, data={'name': 'device', 'organization': self.organization.id})
assert response.status_code == 403
@pytest.mark.skip(reason="No Add view exists")
def test_organization_auth_add_permission_view_denied(self):
""" Check correct permission for add
Attempt to add a user with view permission
"""
client = Client()
url = reverse('Access:_organization_add')
client.force_login(self.view_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
@pytest.mark.skip(reason="No Add view exists")
def test_organization_auth_add_has_permission(self):
""" Check correct permission for add
Attempt to add as user with no permission
"""
client = Client()
url = reverse('Access:_organization_add')
client.force_login(self.add_user)
response = client.post(url, data={'device': 'device', 'organization': self.organization.id})
assert response.status_code == 200
def test_organization_auth_change_user_anon_denied(self):
""" Check correct permission for change
Attempt to change as anon
"""
client = Client()
url = reverse('Access:_organization_view', kwargs={'pk': self.item.id})
response = client.patch(url, data={'device': 'device'})
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_organization_auth_change_no_permission_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user without permissions
"""
client = Client()
url = reverse('Access:_organization_view', kwargs={'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
def test_organization_auth_change_different_organization_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user from different organization
"""
client = Client()
url = reverse('Access:_organization_view', kwargs={'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
def test_organization_auth_change_permission_view_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with view permission
"""
client = Client()
url = reverse('Access:_organization_view', kwargs={'pk': self.item.id})
client.force_login(self.view_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
def test_organization_auth_change_permission_add_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with add permission
"""
client = Client()
url = reverse('Access:_organization_view', kwargs={'pk': self.item.id})
client.force_login(self.add_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
def test_organization_auth_change_has_permission(self):
""" Check correct permission for change
Make change with user who has change permission
"""
client = Client()
url = reverse('Access:_organization_view', kwargs={'pk': self.item.id})
client.force_login(self.change_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 200
@pytest.mark.skip(reason="No Delete view exists")
def test_organization_auth_delete_user_anon_denied(self):
""" Check correct permission for delete
Attempt to delete item as anon user
"""
client = Client()
url = reverse('Access:_organization_delete', kwargs={'pk': self.item.id})
response = client.delete(url, data={'device': 'device'})
assert (
response.status_code == 302
or
response.status_code == 403
)
@pytest.mark.skip(reason="No Delete view exists")
def test_organization_auth_delete_no_permission_denied(self):
""" Check correct permission for delete
Attempt to delete as user with no permissons
"""
client = Client()
url = reverse('Access:_organization_delete', kwargs={'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
@pytest.mark.skip(reason="No Delete view exists")
def test_organization_auth_delete_different_organization_denied(self):
""" Check correct permission for delete
Attempt to delete as user from different organization
"""
client = Client()
url = reverse('Access:_organization_delete', kwargs={'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
@pytest.mark.skip(reason="No Delete view exists")
def test_organization_auth_delete_permission_view_denied(self):
""" Check correct permission for delete
Attempt to delete as user with veiw permission only
"""
client = Client()
url = reverse('Access:_organization_delete', kwargs={'pk': self.item.id})
client.force_login(self.view_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
@pytest.mark.skip(reason="No Delete view exists")
def test_organization_auth_delete_permission_add_denied(self):
""" Check correct permission for delete
Attempt to delete as user with add permission only
"""
client = Client()
url = reverse('Access:_organization_delete', kwargs={'pk': self.item.id})
client.force_login(self.add_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
@pytest.mark.skip(reason="No Delete view exists")
def test_organization_auth_delete_permission_change_denied(self):
""" Check correct permission for delete
Attempt to delete as user with change permission only
"""
client = Client()
url = reverse('Access:_organization_delete', kwargs={'pk': self.item.id})
client.force_login(self.change_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
@pytest.mark.skip(reason="No Delete view exists")
def test_organization_auth_delete_has_permission(self):
""" Check correct permission for delete
Delete item as user with delete permission
"""
client = Client()
url = reverse('Access:_organization_delete', kwargs={'pk': self.item.id})
client.force_login(self.delete_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 302 and response.url == reverse('Access:Devices')

View File

@ -0,0 +1,32 @@
# from django.conf import settings
# from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
import requests
@pytest.mark.skip(reason="to be written")
def test_organization_auth_view_api(user):
""" Check correct permission for view """
pass
@pytest.mark.skip(reason="to be written")
def test_organization_auth_add_api(user):
""" Check correct permission for add """
pass
@pytest.mark.skip(reason="to be written")
def test_organization_auth_change_api(user):
""" Check correct permission for change """
pass
@pytest.mark.skip(reason="to be written")
def test_organization_auth_delete_api(user):
""" Check correct permission for delete """
pass

View File

@ -0,0 +1,10 @@
from django.test import TestCase
import pytest
import requests
import unittest
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from access.models import Organization, Team

View File

@ -0,0 +1,46 @@
from django.test import TestCase, Client
import pytest
import unittest
import requests
@pytest.mark.skip(reason="to be written")
def test_history_organization_create():
""" History row must be added to history table on create """
pass
@pytest.mark.skip(reason="to be written")
def test_history_organization_update():
""" History row must be added to history table on updatej """
pass
@pytest.mark.skip(reason="to be written")
def test_history_organization_delete():
""" History row must be added to history table on delete """
pass
@pytest.mark.skip(reason="to be written")
def test_history_organization_team_create():
""" History row must be added to history table on create """
pass
@pytest.mark.skip(reason="to be written")
def test_history_organization_team_update():
""" History row must be added to history table on updatej """
pass
@pytest.mark.skip(reason="to be written")
def test_history_organization_team_delete():
""" History row must be added to history table on delete """
pass

View File

@ -0,0 +1,43 @@
from django.test import TestCase, Client
import pytest
import unittest
import requests
@pytest.mark.skip(reason="to be written")
def test_history_team_create():
""" History row must be added to history table on create """
pass
@pytest.mark.skip(reason="to be written")
def test_history_team_update():
""" History row must be added to history table on updatej """
pass
@pytest.mark.skip(reason="to be written")
def test_history_team_delete():
""" History row must be added to history table on delete """
pass
@pytest.mark.skip(reason="to be written")
def test_history_team_users_create():
""" History row must be added to history table on create """
pass
@pytest.mark.skip(reason="to be written")
def test_history_team_users_update():
""" History row must be added to history table on updatej """
pass
@pytest.mark.skip(reason="to be written")
def test_history_team_users_delete():
""" History row must be added to history table on delete """
pass

View File

@ -0,0 +1,510 @@
# from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
import requests
from access.models import Organization, Team, TeamUsers, Permission
class TeamPermissions(TestCase):
model = Team
model_name = 'team'
app_label = 'access'
@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 team
3. create teams with each permission: view, add, change, delete
4. create a user per team
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
different_organization = Organization.objects.create(name='test_different_organization')
self.item = self.model.objects.create(
organization=organization,
name = 'teamone'
)
view_permissions = Permission.objects.get(
codename = 'view_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = organization,
)
view_team.permissions.set([view_permissions])
add_permissions = Permission.objects.get(
codename = 'add_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
add_team = Team.objects.create(
team_name = 'add_team',
organization = organization,
)
add_team.permissions.set([add_permissions])
change_permissions = Permission.objects.get(
codename = 'change_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
change_team = Team.objects.create(
team_name = 'change_team',
organization = organization,
)
change_team.permissions.set([change_permissions])
delete_permissions = Permission.objects.get(
codename = 'delete_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
delete_team = Team.objects.create(
team_name = 'delete_team',
organization = organization,
)
delete_team.permissions.set([delete_permissions])
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
self.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
self.add_user = User.objects.create_user(username="test_user_add", password="password")
teamuser = TeamUsers.objects.create(
team = add_team,
user = self.add_user
)
self.change_user = User.objects.create_user(username="test_user_change", password="password")
teamuser = TeamUsers.objects.create(
team = change_team,
user = self.change_user
)
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
teamuser = TeamUsers.objects.create(
team = delete_team,
user = self.delete_user
)
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
different_organization_team = Team.objects.create(
team_name = 'different_organization_team',
organization = different_organization,
)
different_organization_team.permissions.set([
view_permissions,
add_permissions,
change_permissions,
delete_permissions,
])
TeamUsers.objects.create(
team = different_organization_team,
user = self.different_organization_user
)
def test_team_auth_view_user_anon_denied(self):
""" Check correct permission for view
Attempt to view as anon user
"""
client = Client()
url = reverse('Access:_team_view', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
response = client.get(url)
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_team_auth_view_no_permission_denied(self):
""" Check correct permission for view
Attempt to view with user missing permission
"""
client = Client()
url = reverse('Access:_team_view', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.get(url)
assert response.status_code == 403
def test_team_auth_view_different_organizaiton_denied(self):
""" Check correct permission for view
Attempt to view with user from different organization
"""
client = Client()
url = reverse('Access:_team_view', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.get(url)
assert response.status_code == 403
def test_team_auth_view_has_permission(self):
""" Check correct permission for view
Attempt to view as user with view permission
"""
client = Client()
url = reverse('Access:_team_view', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.view_user)
response = client.get(url)
assert response.status_code == 200
def test_team_auth_add_user_anon_denied(self):
""" Check correct permission for add
Attempt to add as anon user
"""
client = Client()
url = reverse('Access:_team_add', kwargs={'pk': self.organization.id})
response = client.put(url, data={'team': 'team'})
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_team_auth_add_no_permission_denied(self):
""" Check correct permission for add
Attempt to add as user with no permissions
"""
client = Client()
url = reverse('Access:_team_add', kwargs={'pk': self.organization.id})
client.force_login(self.no_permissions_user)
response = client.post(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_add_different_organization_denied(self):
""" Check correct permission for add
attempt to add as user from different organization
"""
client = Client()
url = reverse('Access:_team_add', kwargs={'pk': self.organization.id})
client.force_login(self.different_organization_user)
response = client.post(url, data={'name': 'team', 'organization': self.organization.id})
assert response.status_code == 403
def test_team_auth_add_permission_view_denied(self):
""" Check correct permission for add
Attempt to add a user with view permission
"""
client = Client()
url = reverse('Access:_team_add', kwargs={'pk': self.organization.id})
client.force_login(self.view_user)
response = client.post(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_add_has_permission(self):
""" Check correct permission for add
Attempt to add as user with no permission
"""
client = Client()
url = reverse('Access:_team_add', kwargs={'pk': self.organization.id})
client.force_login(self.add_user)
response = client.post(url, data={'team': 'team', 'organization': self.organization.id})
assert response.status_code == 200
def test_team_auth_change_user_anon_denied(self):
""" Check correct permission for change
Attempt to change as anon
"""
client = Client()
url = reverse('Access:_team_view', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
response = client.patch(url, data={'team': 'team'})
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_team_auth_change_no_permission_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user without permissions
"""
client = Client()
url = reverse('Access:_team_view', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.post(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_change_different_organization_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user from different organization
"""
client = Client()
url = reverse('Access:_team_view', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.post(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_change_permission_view_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with view permission
"""
client = Client()
url = reverse('Access:_team_view', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.view_user)
response = client.post(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_change_permission_add_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with add permission
"""
client = Client()
url = reverse('Access:_team_view', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.add_user)
response = client.post(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_change_has_permission(self):
""" Check correct permission for change
Make change with user who has change permission
"""
client = Client()
url = reverse('Access:_team_view', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.change_user)
response = client.post(url, data={'team': 'team'})
assert response.status_code == 200
def test_team_auth_delete_user_anon_denied(self):
""" Check correct permission for delete
Attempt to delete item as anon user
"""
client = Client()
url = reverse('Access:_team_delete', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
response = client.delete(url, data={'team': 'team'})
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_team_auth_delete_no_permission_denied(self):
""" Check correct permission for delete
Attempt to delete as user with no permissons
"""
client = Client()
url = reverse('Access:_team_delete', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.delete(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_delete_different_organization_denied(self):
""" Check correct permission for delete
Attempt to delete as user from different organization
"""
client = Client()
url = reverse('Access:_team_delete', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.delete(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_delete_permission_view_denied(self):
""" Check correct permission for delete
Attempt to delete as user with veiw permission only
"""
client = Client()
url = reverse('Access:_team_delete', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.view_user)
response = client.delete(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_delete_permission_add_denied(self):
""" Check correct permission for delete
Attempt to delete as user with add permission only
"""
client = Client()
url = reverse('Access:_team_delete', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.add_user)
response = client.delete(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_delete_permission_change_denied(self):
""" Check correct permission for delete
Attempt to delete as user with change permission only
"""
client = Client()
url = reverse('Access:_team_delete', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.change_user)
response = client.delete(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_delete_has_permission(self):
""" Check correct permission for delete
Delete item as user with delete permission
"""
client = Client()
url = reverse('Access:_team_delete', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.delete_user)
response = client.delete(url, data={'team': 'team'})
assert response.status_code == 302 and response.url == reverse('Access:_organization_view', kwargs={'pk': self.organization.id})

View File

@ -0,0 +1,32 @@
# from django.conf import settings
# from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
import requests
@pytest.mark.skip(reason="to be written")
def test_team_auth_view_api(user):
""" Check correct permission for view """
pass
@pytest.mark.skip(reason="to be written")
def test_team_auth_add_api(user):
""" Check correct permission for add """
pass
@pytest.mark.skip(reason="to be written")
def test_team_auth_change_api(user):
""" Check correct permission for change """
pass
@pytest.mark.skip(reason="to be written")
def test_team_auth_delete_api(user):
""" Check correct permission for delete """
pass

View File

@ -0,0 +1,24 @@
from django.test import TestCase, Client
import pytest
import unittest
import requests
@pytest.mark.skip(reason="to be written")
def test_history_team_users_create():
""" History row must be added to history table on create """
pass
@pytest.mark.skip(reason="to be written")
def test_history_team_users_update():
""" History row must be added to history table on updatej """
pass
@pytest.mark.skip(reason="to be written")
def test_history_team_users_delete():
""" History row must be added to history table on delete """
pass

View File

@ -0,0 +1,48 @@
from django.test import TestCase
import pytest
import requests
import unittest
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from access.models import Organization, Team
# @pytest.fixture
# def organization() -> Organization:
# return Organization.objects.create(
# name='Test org',
# )
# @pytest.fixture
# def team() -> Team:
# return Team.objects.create(
# name='Team one',
# organization = Organization.objects.create(
# name='Test org',
# ),
# )
@pytest.mark.skip(reason="to be written")
def test_authorization_user_permission_add_team_manager(user):
"""Ensure user can be added when user is team manager
user requires permissions team view and user add
"""
pass
@pytest.mark.skip(reason="to be written")
def test_authorization_user_permission_delete_team_manager(user):
"""Ensure user can be deleted when user is team manager
user requires permissions team view and user delete
"""
pass
# is_superuser to be able to view, add, change, delete for all objects

View File

@ -0,0 +1,538 @@
# from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
import requests
from access.models import Organization, Team, TeamUsers, Permission
class TeamUserPermissions(TestCase):
model = TeamUsers
model_name = 'teamusers'
app_label = 'access'
@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.test_team = Team.objects.create(
team_name = 'test_team',
organization = organization,
)
self.team_user = User.objects.create_user(username="test_self.team_user", password="password")
self.item = self.model.objects.create(
team = self.test_team,
user = self.team_user
)
view_permissions = Permission.objects.get(
codename = 'view_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = organization,
)
view_team.permissions.set([view_permissions])
add_permissions = Permission.objects.get(
codename = 'add_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
add_team = Team.objects.create(
team_name = 'add_team',
organization = organization,
)
add_team.permissions.set([add_permissions])
change_permissions = Permission.objects.get(
codename = 'change_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
change_team = Team.objects.create(
team_name = 'change_team',
organization = organization,
)
change_team.permissions.set([change_permissions])
delete_permissions = Permission.objects.get(
codename = 'delete_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
delete_team = Team.objects.create(
team_name = 'delete_team',
organization = organization,
)
delete_team.permissions.set([delete_permissions])
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
self.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
self.add_user = User.objects.create_user(username="test_user_add", password="password")
teamuser = TeamUsers.objects.create(
team = add_team,
user = self.add_user
)
self.change_user = User.objects.create_user(username="test_user_change", password="password")
teamuser = TeamUsers.objects.create(
team = change_team,
user = self.change_user
)
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
teamuser = TeamUsers.objects.create(
team = delete_team,
user = self.delete_user
)
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
different_organization_team = Team.objects.create(
team_name = 'different_organization_team',
organization = different_organization,
)
different_organization_team.permissions.set([
view_permissions,
add_permissions,
change_permissions,
delete_permissions,
])
TeamUsers.objects.create(
team = different_organization_team,
user = self.different_organization_user
)
@pytest.mark.skip(reason="feature does not exist")
def test_team_user_auth_view_user_anon_denied(self):
""" Check correct permission for view
Attempt to view as anon user
"""
client = Client()
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
response = client.get(url)
assert response.status_code == 302 and response.url.startswith('/account/login')
@pytest.mark.skip(reason="feature does not exist")
def test_team_user_auth_view_no_permission_denied(self):
""" Check correct permission for view
Attempt to view with user missing permission
"""
client = Client()
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.get(url)
assert response.status_code == 403
@pytest.mark.skip(reason="feature does not exist")
def test_team_user_auth_view_different_organizaiton_denied(self):
""" Check correct permission for view
Attempt to view with user from different organization
"""
client = Client()
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.get(url)
assert response.status_code == 403
@pytest.mark.skip(reason="feature does not exist")
def test_team_user_auth_view_has_permission(self):
""" Check correct permission for view
Attempt to view as user with view permission
"""
client = Client()
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
client.force_login(self.view_user)
response = client.get(url)
assert response.status_code == 200
def test_team_user_auth_add_user_anon_denied(self):
""" Check correct permission for add
Attempt to add as anon user
"""
client = Client()
url = reverse('Access:_team_user_add', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
response = client.put(url, data={'device': 'device'})
assert response.status_code == 302 and response.url.startswith('/account/login')
# @pytest.mark.skip(reason="ToDO: figure out why fails")
def test_team_user_auth_add_no_permission_denied(self):
""" Check correct permission for add
Attempt to add as user with no permissions
"""
client = Client()
url = reverse('Access:_team_user_add', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
# @pytest.mark.skip(reason="ToDO: figure out why fails")
def test_team_user_auth_add_different_organization_denied(self):
""" Check correct permission for add
attempt to add as user from different organization
"""
client = Client()
url = reverse('Access:_team_user_add', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.post(url, data={'name': 'device', 'organization': self.organization.id})
assert response.status_code == 403
def test_team_user_auth_add_permission_view_denied(self):
""" Check correct permission for add
Attempt to add a user with view permission
"""
client = Client()
url = reverse('Access:_team_user_add', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.view_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
def test_team_user_auth_add_has_permission(self):
""" Check correct permission for add
Attempt to add as user with no permission
"""
client = Client()
url = reverse('Access:_team_user_add', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.add_user)
response = client.post(url, data={'device': 'device', 'organization': self.organization.id})
assert response.status_code == 200
@pytest.mark.skip(reason="feature does not exist")
def test_team_user_auth_change_user_anon_denied(self):
""" Check correct permission for change
Attempt to change as anon
"""
client = Client()
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
response = client.patch(url, data={'device': 'device'})
assert response.status_code == 302 and response.url.startswith('/account/login')
@pytest.mark.skip(reason="feature does not exist")
def test_team_user_auth_change_no_permission_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user without permissions
"""
client = Client()
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
@pytest.mark.skip(reason="feature does not exist")
def test_team_user_auth_change_different_organization_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user from different organization
"""
client = Client()
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
@pytest.mark.skip(reason="feature does not exist")
def test_team_user_auth_change_permission_view_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with view permission
"""
client = Client()
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
client.force_login(self.view_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
@pytest.mark.skip(reason="feature does not exist")
def test_team_user_auth_change_permission_add_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with add permission
"""
client = Client()
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
client.force_login(self.add_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
@pytest.mark.skip(reason="feature does not exist")
def test_team_user_auth_change_has_permission(self):
""" Check correct permission for change
Make change with user who has change permission
"""
client = Client()
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
client.force_login(self.change_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 200
def test_team_user_auth_delete_user_anon_denied(self):
""" Check correct permission for delete
Attempt to delete item as anon user
"""
client = Client()
url = reverse('Access:_team_user_delete', kwargs={'organization_id': self.organization.id, 'team_id': self.item.team.id, 'pk': self.item.id})
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_team_user_auth_delete_no_permission_denied(self):
""" Check correct permission for delete
Attempt to delete as user with no permissons
"""
client = Client()
url = reverse('Access:_team_user_delete', kwargs={'organization_id': self.organization.id, 'team_id': self.item.team.id, 'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
def test_team_user_auth_delete_different_organization_denied(self):
""" Check correct permission for delete
Attempt to delete as user from different organization
"""
client = Client()
url = reverse('Access:_team_user_delete', kwargs={'organization_id': self.organization.id, 'team_id': self.item.team.id, 'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
def test_team_user_auth_delete_permission_view_denied(self):
""" Check correct permission for delete
Attempt to delete as user with veiw permission only
"""
client = Client()
url = reverse('Access:_team_user_delete', kwargs={'organization_id': self.organization.id, 'team_id': self.item.team.id, 'pk': self.item.id})
client.force_login(self.view_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
def test_team_user_auth_delete_permission_add_denied(self):
""" Check correct permission for delete
Attempt to delete as user with add permission only
"""
client = Client()
url = reverse('Access:_team_user_delete', kwargs={'organization_id': self.organization.id, 'team_id': self.item.team.id, 'pk': self.item.id})
client.force_login(self.add_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
def test_team_user_auth_delete_permission_change_denied(self):
""" Check correct permission for delete
Attempt to delete as user with change permission only
"""
client = Client()
url = reverse('Access:_team_user_delete', kwargs={'organization_id': self.organization.id, 'team_id': self.item.team.id, 'pk': self.item.id})
client.force_login(self.change_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
def test_team_user_auth_delete_has_permission(self):
""" Check correct permission for delete
Delete item as user with delete permission
"""
client = Client()
url = reverse('Access:_team_user_delete',
kwargs={
'organization_id': self.organization.id,
'team_id': self.test_team.id,
'pk': self.item.id
}
)
client.force_login(self.delete_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 302 and response.url == reverse('Access:_team_view',
kwargs={
'organization_id': self.organization.id,
'pk': self.test_team.id
}
)

View File

@ -0,0 +1,32 @@
# from django.conf import settings
# from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
import requests
@pytest.mark.skip(reason="to be written")
def test_team_user_auth_view_api(user):
""" Check correct permission for view """
pass
@pytest.mark.skip(reason="to be written")
def test_team_user_auth_add_api(user):
""" Check correct permission for add """
pass
@pytest.mark.skip(reason="to be written")
def test_team_user_auth_change_api(user):
""" Check correct permission for change """
pass
@pytest.mark.skip(reason="to be written")
def test_team_user_auth_delete_api(user):
""" Check correct permission for delete """
pass

View File

@ -1,122 +0,0 @@
from django.conf import settings
from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
import requests
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from access.models import Organization
# class Test_app_structure_auth(unittest.TestCase):
User = get_user_model()
@pytest.fixture
def user() -> User:
return User.objects.create_user(username="testuser", password="testpassword")
@pytest.fixture
def organization() -> Organization:
return Organization.objects.create(
name='Test org',
)
@pytest.mark.django_db
def test_require_login_organizations():
"""Some docstring defining what the test is checking."""
client = Client()
url = reverse('Access:Organizations')
response = client.get(url)
assert response.status_code == 302
@pytest.mark.skip(reason="to be re-written for orgmixin")
@pytest.mark.django_db
def test_require_login_organization_pk(organization):
"""Ensure login is required to view an organization"""
client = Client()
url = reverse('Access:_organization', kwargs={'organization_id': 1})
response = client.get(url)
assert response.status_code == 302
@pytest.mark.django_db
def test_login_view_organizations_no_permission(user):
"""Some docstring defining what the test is checking."""
client = Client()
url = reverse('Access:Organizations')
client.force_login(user)
response = client.get(url)
assert response.status_code == 403
@pytest.mark.skip(reason="to be written")
def test_organizations_permission_change(user):
"""ensure user with permission can change organization
Args:
user (_type_): _description_
"""
pass
@pytest.mark.skip(reason="to be written")
def test_organizations_permission_delete_denied(user):
"""ensure non-admin user cant delete organization
Args:
user (_type_): _description_
"""
pass
@pytest.mark.skip(reason="to be written")
def test_team_permission_add_in_org(user):
"""ensure user with add permission to an organization can add team
Args:
user (_type_): _description_
"""
pass
@pytest.mark.skip(reason="to be written")
def test_team_permission_add_not_in_org(user):
"""ensure user with add permission to an organization can add team
Args:
user (_type_): _description_
"""
pass
@pytest.mark.skip(reason="to be written")
def test_team_permission_change(user):
"""ensure user can change a team
Args:
user (_type_): _description_
"""
pass
@pytest.mark.skip(reason="to be written")
def test_team_permission_delete(user):
"""ensure user can delete a team
Args:
user (_type_): _description_
"""
pass

View File

@ -1,205 +0,0 @@
from django.test import TestCase
import pytest
import requests
import unittest
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from access.models import Organization, Team
# @pytest.fixture
# def organization() -> Organization:
# return Organization.objects.create(
# name='Test org',
# )
# @pytest.fixture
# def team() -> Team:
# return Team.objects.create(
# name='Team one',
# organization = Organization.objects.create(
# name='Test org',
# ),
# )
######################################################################
# SoF for loop for tests
# for test in ['organization','team', 'users']
#
# permissions for each item as per the action plus view of the parent item
######################################################################
@pytest.mark.skip(reason="to be written")
def test_authorization_organization_view(user):
"""User of organization can view
user requires permissions organization view
"""
pass
@pytest.mark.skip(reason="to be written")
def test_authorization_organization_no_view(user):
"""User not part of organization cant view
user requires permissions organization view
"""
pass
###################################################################
@pytest.mark.skip(reason="to be written")
def test_authorization_team_permission_view(user):
""" Ensure team can be viewed when user has correct permissions
user requires permissions organization view and team view
"""
pass
@pytest.mark.skip(reason="to be written")
def test_authorization_team_permission_no_view(user):
""" Ensure team can't be viewed when user is missing permissions
user requires permissions organization view and team view
"""
pass
@pytest.mark.skip(reason="to be written")
def test_authorization_team_permission_add(user):
"""Ensure team can be added when user has correct permissions
user requires permissions organization view and team add
"""
pass
@pytest.mark.skip(reason="to be written")
def test_authorization_team_permission_no_view(user):
"""Ensure team can't be added when user is missing permissions
user requires permissions organization view and team add
"""
pass
@pytest.mark.skip(reason="to be written")
def test_authorization_team_permission_change(user):
"""Ensure team can be changed when user has correct permissions
user requires permissions organization view and team change
"""
pass
@pytest.mark.skip(reason="to be written")
def test_authorization_team_permission_no_change(user):
"""Ensure team can't be change when user is missing permissions
user requires permissions organization view and team change
"""
pass
@pytest.mark.skip(reason="to be written")
def test_authorization_team_permission_delete(user):
"""Ensure team can be deleted when user has correct permissions
user requires permissions organization view and team delete
"""
pass
@pytest.mark.skip(reason="to be written")
def test_authorization_team_permission_no_delete(user):
"""Ensure team can't be deleted when user is missing permissions
user requires permissions organization view and team delete
"""
pass
###################################################################
@pytest.mark.skip(reason="to be written")
def test_authorization_user_permission_add(user):
"""Ensure user can be added when user has correct permissions
user requires permissions team view and user add
"""
pass
@pytest.mark.skip(reason="to be written")
def test_authorization_user_permission_no_add(user):
"""Ensure user can't be added when user is missing permissions
user requires permissions team view and user add
"""
pass
@pytest.mark.skip(reason="to be written")
def test_authorization_user_permission_add_team_manager(user):
"""Ensure user can be added when user is team manager
user requires permissions team view and user add
"""
pass
@pytest.mark.skip(reason="to be written")
def test_authorization_user_permission_change(user):
"""Ensure user can be changed when user has correct permissions
user requires permissions team view and user change
"""
pass
@pytest.mark.skip(reason="to be written")
def test_authorization_user_permission_no_change(user):
"""Ensure user can't be change when user is missing permissions
user requires permissions team view and user change
"""
pass
@pytest.mark.skip(reason="to be written")
def test_authorization_user_permission_delete(user):
"""Ensure user can be deleted when user has correct permissions
user requires permissions team view and user delete
"""
pass
@pytest.mark.skip(reason="to be written")
def test_authorization_user_permission_no_delete(user):
"""Ensure user can't be deleted when user is missing permissions
user requires permissions team view and user delete
"""
pass
@pytest.mark.skip(reason="to be written")
def test_authorization_user_permission_delete_team_manager(user):
"""Ensure user can be deleted when user is team manager
user requires permissions team view and user delete
"""
pass
######################################################################
# EoF for loop for tests
# for test in ['organization','team']
######################################################################
# is_superuser to be able to view, add, change, delete for all objects

View File

@ -6,11 +6,10 @@ from .views import team, organization, user
app_name = "Access"
urlpatterns = [
path("", organization.IndexView.as_view(), name="Organizations"),
path("<int:pk>/", organization.View.as_view(), name="_organization"),
path("<int:pk>/edit", organization.Change.as_view(), name="_organization_change"),
path("<int:organization_id>/team/<int:pk>/", team.View.as_view(), name="_team"),
path("<int:pk>/", organization.View.as_view(), name="_organization_view"),
# path("<int:pk>/edit", organization.Change.as_view(), name="_organization_change"),
path("<int:organization_id>/team/<int:pk>/", team.View.as_view(), name="_team_view"),
path("<int:pk>/team/add", team.Add.as_view(), name="_team_add"),
path("<int:organization_id>/team/<int:pk>/edit", team.Change.as_view(), name="_team_change"),
path("<int:organization_id>/team/<int:pk>/delete", team.Delete.as_view(), name="_team_delete"),
path("<int:organization_id>/team/<int:pk>/user/add", user.Add.as_view(), name="_team_user_add"),
path("<int:organization_id>/team/<int:team_id>/user/<int:pk>/delete", user.Delete.as_view(), name="_team_user_delete"),

View File

@ -1,4 +1,5 @@
from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
from django.contrib.auth import decorators as auth_decorator
from django.utils.decorators import method_decorator
from django.views import generic
from access.mixin import *
@ -6,8 +7,10 @@ from access.models import *
class IndexView(PermissionRequiredMixin, OrganizationPermission, generic.ListView):
permission_required = 'access.view_organization'
class IndexView(OrganizationPermission, generic.ListView):
permission_required = [
'access.view_organization'
]
template_name = 'access/index.html.j2'
context_object_name = "organization_list"
@ -24,9 +27,12 @@ class IndexView(PermissionRequiredMixin, OrganizationPermission, generic.ListVie
class View(LoginRequiredMixin, OrganizationPermission, generic.UpdateView):
class View(OrganizationPermission, generic.UpdateView):
model = Organization
permission_required = 'access.view_organization'
permission_required = [
'access.view_organization',
'access.change_organization',
]
template_name = "access/organization.html.j2"
fields = ["name", 'id']
@ -46,16 +52,25 @@ class View(LoginRequiredMixin, OrganizationPermission, generic.UpdateView):
context['teams'] = Team.objects.filter(organization=self.kwargs['pk'])
context['model_pk'] = self.kwargs['pk']
context['model_name'] = self.model._meta.verbose_name.replace(' ', '')
return context
@method_decorator(auth_decorator.permission_required("access.change_organization", raise_exception=True))
def post(self, request, *args, **kwargs):
class Change(LoginRequiredMixin, OrganizationPermission, generic.DetailView):
return super().post(request, *args, **kwargs)
class Change(OrganizationPermission, generic.DetailView):
pass
class Delete(LoginRequiredMixin, OrganizationPermission, generic.DetailView):
class Delete(OrganizationPermission, generic.DetailView):
pass

View File

@ -1,5 +1,7 @@
from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
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.models import Team, TeamUsers, Organization
@ -10,7 +12,7 @@ from access.mixin import *
class View(OrganizationPermission, generic.UpdateView):
model = Team
permission_required = [
'access.add_team',
'access.view_team',
'access.change_team',
]
template_name = 'access/team.html.j2'
@ -37,14 +39,23 @@ class View(OrganizationPermission, generic.UpdateView):
context['teamusers'] = teamusers
context['permissions'] = Permission.objects.filter()
context['model_pk'] = self.kwargs['pk']
context['model_name'] = self.model._meta.verbose_name.replace(' ', '')
return context
def get_success_url(self, **kwargs):
return f"/organization/{self.kwargs['organization_id']}/team/{self.kwargs['pk']}/"
return reverse('Access:_team_view', args=(self.kwargs['organization_id'], self.kwargs['pk'],))
@method_decorator(auth_decorator.permission_required("access.change_team", raise_exception=True))
def post(self, request, *args, **kwargs):
return super().post(request, *args, **kwargs)
class Add(PermissionRequiredMixin, OrganizationPermission, generic.CreateView):
class Add(OrganizationPermission, generic.CreateView):
model = Team
permission_required = [
'access.add_team',
@ -66,36 +77,16 @@ class Add(PermissionRequiredMixin, 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'
return context
class Change(PermissionRequiredMixin, OrganizationPermission, generic.UpdateView):
model = Team
permission_required = [
'access.change_team',
]
template_name = 'form.html.j2'
fields = [
'team_name',
'permissions',
'organization'
]
def get_success_url(self, **kwargs):
return f"/organization/{self.kwargs['pk']}/"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Edit Team'
return context
class Delete(PermissionRequiredMixin, OrganizationPermission, generic.DeleteView):
class Delete(OrganizationPermission, generic.DeleteView):
model = Team
permission_required = [
'access.delete_team'
@ -115,6 +106,9 @@ class Delete(PermissionRequiredMixin, OrganizationPermission, generic.DeleteView
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['model_pk'] = self.kwargs['pk']
context['model_name'] = self.model._meta.verbose_name.replace(' ', '')
context['content_title'] = 'Delete Team'
return context

View File

@ -1,5 +1,6 @@
from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
from django.contrib.auth.models import User, Group
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
@ -7,7 +8,7 @@ from access.models import Team, TeamUsers
class Add(PermissionRequiredMixin, OrganizationPermission, generic.CreateView):
class Add(OrganizationPermission, generic.CreateView):
model = TeamUsers
permission_required = [
'access.view_team',
@ -24,15 +25,17 @@ class Add(PermissionRequiredMixin, OrganizationPermission, generic.CreateView):
team = Team.objects.get(pk=self.kwargs['pk'])
form.instance.team = team
group = Group.objects.get(pk=team.group_ptr_id)
user = User.objects.get(pk=self.request.POST['user'][0])
user.groups.add(group)
return super().form_valid(form)
def get_success_url(self, **kwargs):
return f"/organization/{self.kwargs['organization_id']}/team/{self.kwargs['pk']}"
return reverse('Access:_team_view',
kwargs={
'organization_id': self.kwargs['organization_id'],
'pk': self.kwargs['pk']
}
)
def get_context_data(self, **kwargs):
@ -43,31 +46,22 @@ class Add(PermissionRequiredMixin, OrganizationPermission, generic.CreateView):
return context
class Delete(PermissionRequiredMixin, OrganizationPermission, generic.DeleteView):
class Delete(OrganizationPermission, generic.DeleteView):
model = TeamUsers
permission_required = [
'access.view_team',
'access.delete_teamusers'
]
template_name = 'form.html.j2'
def form_valid(self, form):
team = Team.objects.get(pk=self.kwargs['team_id'])
teamuser = TeamUsers.objects.get(pk=self.kwargs['pk'])
group = Group.objects.get(pk=team.group_ptr_id)
user = User.objects.get(pk=teamuser.user_id)
user.groups.remove(group)
return super().form_valid(form)
def get_success_url(self, **kwargs):
return f"/organization/{self.kwargs['organization_id']}/team/{self.kwargs['team_id']}"
return reverse('Access:_team_view',
kwargs={
'organization_id': self.kwargs['organization_id'],
'pk': self.kwargs['team_id']
}
)
def get_context_data(self, **kwargs):

View File

@ -1,15 +1,48 @@
from rest_framework import serializers
from rest_framework import serializers, request
from rest_framework.reverse import reverse
from access.models import Organization, Team
from django.contrib.auth.models import Permission
class TeamSerializer(serializers.ModelSerializer):
class TeamSerializerBase(serializers.ModelSerializer):
view_name="_api_team"
url = serializers.SerializerMethodField('get_url')
class Meta:
model = Team
fields = (
"group_ptr_id",
"id",
"name",
'organization',
'url',
)
def get_url(self, obj):
request = self.context.get('request')
return request.build_absolute_uri(reverse(self.view_name, args=[obj.organization.id,obj.pk]))
class TeamSerializer(TeamSerializerBase):
class Meta:
model = Team
depth = 1
fields = (
"id",
"name",
'organization',
'permissions',
'url',
)
@ -20,11 +53,16 @@ class OrganizationSerializer(serializers.ModelSerializer):
view_name="_api_organization", format="html"
)
teams = TeamSerializerBase(source='team_set', many=True, read_only=False)
view_name="_api_organization"
class Meta:
model = Organization
fields = (
"id",
"name",
'teams',
'url',
)

View File

@ -25,6 +25,7 @@ class DeviceSerializer(serializers.ModelSerializer):
fields = '__all__'
read_only_fields = [
'inventorydate',
'is_global',
'organization',
]

View File

@ -22,188 +22,6 @@ def test_api_access_home(user):
pass
@pytest.mark.skip(reason="to be written")
def test_api_access_model_view_organization(user):
"""Ensure api model access
test_api_access_model_view_organization = test_api_access_model_<action>_<model name>
Test to ensure that action can only occur when authenticated and against the model
"""
pass
@pytest.mark.skip(reason="to be written")
def test_api_access_model_view_team(user):
"""Ensure api model access
test_api_access_model_view_organization = test_api_access_model_<action>_<model name>
Test to ensure that action can only occur when authenticated and against the model
"""
pass
@pytest.mark.skip(reason="to be written")
def test_api_access_model_add_organization(user):
"""Ensure api model access
test_api_access_model_view_organization = test_api_access_model_<action>_<model name>
Test to ensure that action can only occur when authenticated and against the model
"""
pass
@pytest.mark.skip(reason="to be written")
def test_api_access_model_add_team(user):
"""Ensure api model access
test_api_access_model_view_organization = test_api_access_model_<action>_<model name>
Test to ensure that action can only occur when authenticated and against the model
"""
pass
@pytest.mark.skip(reason="to be written")
def test_api_access_model_change_organization(user):
"""Ensure api model access
test_api_access_model_view_organization = test_api_access_model_<action>_<model name>
Test to ensure that action can only occur when authenticated and against the model
"""
pass
@pytest.mark.skip(reason="to be written")
def test_api_access_model_change_team(user):
"""Ensure api model access
test_api_access_model_view_organization = test_api_access_model_<action>_<model name>
Test to ensure that action can only occur when authenticated and against the model
"""
pass
@pytest.mark.skip(reason="to be written")
def test_api_access_model_delete_organization(user):
"""Ensure api model access
test_api_access_model_view_organization = test_api_access_model_<action>_<model name>
Test to ensure that action can only occur when authenticated and against the model
"""
pass
@pytest.mark.skip(reason="to be written")
def test_api_access_model_delete_team(user):
"""Ensure api model access
test_api_access_model_view_organization = test_api_access_model_<action>_<model name>
Test to ensure that action can only occur when authenticated and against the model
"""
pass
@pytest.mark.skip(reason="to be written")
def test_api_access_model_view_device(user):
"""Ensure api model access
test_api_access_model_view_organization = test_api_access_model_<action>_<model name>
Test to ensure that action can only occur when authenticated and against the model
"""
pass
@pytest.mark.skip(reason="to be written")
def test_api_access_model_add_device(user):
"""Ensure api model access
test_api_access_model_view_organization = test_api_access_model_<action>_<model name>
Test to ensure that action can only occur when authenticated and against the model
"""
pass
@pytest.mark.skip(reason="to be written")
def test_api_access_model_change_device(user):
"""Ensure api model access
test_api_access_model_view_organization = test_api_access_model_<action>_<model name>
Test to ensure that action can only occur when authenticated and against the model
"""
pass
@pytest.mark.skip(reason="to be written")
def test_api_access_model_delete_device(user):
"""Ensure api model access
test_api_access_model_view_organization = test_api_access_model_<action>_<model name>
Test to ensure that action can only occur when authenticated and against the model
"""
pass
@pytest.mark.skip(reason="to be written")
def test_api_access_model_view_software(user):
"""Ensure api model access
test_api_access_model_view_organization = test_api_access_model_<action>_<model name>
Test to ensure that action can only occur when authenticated and against the model
"""
pass
@pytest.mark.skip(reason="to be written")
def test_api_access_model_add_software(user):
"""Ensure api model access
test_api_access_model_view_organization = test_api_access_model_<action>_<model name>
Test to ensure that action can only occur when authenticated and against the model
"""
pass
@pytest.mark.skip(reason="to be written")
def test_api_access_model_change_software(user):
"""Ensure api model access
test_api_access_model_view_organization = test_api_access_model_<action>_<model name>
Test to ensure that action can only occur when authenticated and against the model
"""
pass
@pytest.mark.skip(reason="to be written")
def test_api_access_model_delete_software(user):
"""Ensure api model access
test_api_access_model_view_organization = test_api_access_model_<action>_<model name>
Test to ensure that action can only occur when authenticated and against the model
"""
pass

View File

@ -0,0 +1,151 @@
from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
@pytest.mark.skip(reason="to be written")
def test_api_inventory_device_added():
""" Device is created """
pass
@pytest.mark.skip(reason="to be written")
def test_api_inventory_operating_system_added():
""" Operating System is created """
pass
@pytest.mark.skip(reason="to be written")
def test_api_inventory_operating_system_version_added():
""" Operating System version is created """
pass
@pytest.mark.skip(reason="to be written")
def test_api_inventory_device_has_operating_system_added():
""" Operating System version linked to device """
pass
@pytest.mark.skip(reason="to be written")
def test_api_inventory_device_operating_system_version_is_semver():
""" Operating System version is full semver
Operating system versions name is the major version number of semver.
The device version is to be full semver
"""
pass
@pytest.mark.skip(reason="to be written")
def test_api_inventory_software_no_version_cleaned():
""" Check softare cleaned up
As part of the inventory upload the software versions of software found on the device is set to null
and before the processing is completed, the version=null software is supposed to be cleaned up.
"""
pass
@pytest.mark.skip(reason="to be written")
def test_api_inventory_software_category_added():
""" Software category exists """
pass
@pytest.mark.skip(reason="to be written")
def test_api_inventory_software_added():
""" Test software exists """
pass
@pytest.mark.skip(reason="to be written")
def test_api_inventory_software_category_linked_to_software():
""" Software category linked to software """
pass
@pytest.mark.skip(reason="to be written")
def test_api_inventory_software_version_added():
""" Test software version exists """
pass
@pytest.mark.skip(reason="to be written")
def test_api_inventory_software_version_returns_semver():
""" Software Version from inventory returns semver if within version string """
pass
@pytest.mark.skip(reason="to be written")
def test_api_inventory_software_version_returns_original_version():
""" Software Version from inventory returns inventoried version if no semver found """
pass
@pytest.mark.skip(reason="to be written")
def test_api_inventory_software_version_linked_to_software():
""" Test software version linked to software it belongs too """
pass
@pytest.mark.skip(reason="to be written")
def test_api_inventory_device_has_software_version():
""" Inventoried software is linked to device and it's the corret one"""
pass
@pytest.mark.skip(reason="to be written")
def test_api_inventory_device_software_has_installed_date():
""" Inventoried software version has install date """
pass
@pytest.mark.skip(reason="to be written")
def test_api_inventory_device_software_blank_installed_date_is_updated():
""" A blank installed date of software is updated if the software was already attached to the device """
pass
@pytest.mark.skip(reason="to be written")
def test_api_inventory_valid_status_created():
""" Successful inventory upload returns 201 """
pass
@pytest.mark.skip(reason="to be written")
def test_api_inventory_invalid_status_bad_request():
""" Incorrectly formated inventory upload returns 400 """
pass
@pytest.mark.skip(reason="to be written")
def test_api_inventory_exeception_status_sever_error():
""" if the method throws an exception 500 must be returned.
idea to test: add a random key to the report that is not documented
and perform some action against it that will cause a python exception.
"""
pass

View File

@ -3,7 +3,9 @@ from rest_framework.urlpatterns import format_suffix_patterns
from .views import access, index
from .views.itam import device as itam_device, software as itam_software, config as itam_config
from .views.itam import software as itam_software, config as itam_config
from .views.itam.device import detail as itam_device
from .views.itam.device import inventory
urlpatterns = [
path("", index.IndexView.as_view(), name='_api_home'),
@ -21,6 +23,8 @@ urlpatterns = [
path("software/", itam_software.List.as_view(), name="_api_softwares"),
path("software/<int:pk>/", itam_software.Detail.as_view(), name="_api_software_view"),
path("device/inventory/<slug:slug>", inventory.Collect.as_view(), name="_api_device_inventory"),
]
urlpatterns = format_suffix_patterns(urlpatterns)

View File

@ -21,6 +21,7 @@ class OrganizationList(generics.ListCreateAPIView):
class OrganizationDetail(generics.RetrieveUpdateDestroyAPIView):
permission_required = 'access.view_organization'
queryset = Organization.objects.all()
lookup_field = 'pk'
serializer_class = OrganizationSerializer

View File

@ -27,9 +27,9 @@ class IndexView(routers.APIRootView):
def get(self, request, *args, **kwargs):
return Response(
{
"organizations": reverse("_api_orgs", request=request),
"teams": reverse("_api_teams", request=request),
# "teams": reverse("_api_teams", request=request),
"devices": reverse("_api_devices", request=request),
"organizations": reverse("_api_orgs", request=request),
"software": reverse("_api_softwares", request=request),
}
)

View File

View File

@ -0,0 +1,280 @@
# from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
import json
import re
from django.http import JsonResponse
from django.utils import timezone
from rest_framework import generics, views
from rest_framework.response import Response
from access.models import Organization
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 settings.models.app_settings import AppSettings
from settings.models.user_settings import UserSettings
class Collect(views.APIView):
def post(self, request, *args, **kwargs):
data = json.loads(request.body)
status = Http.Status.BAD_REQUEST
device = None
device_operating_system = None
operating_system = None
operating_system_version = None
try:
default_organization = UserSettings.objects.get(user=request.user).default_organization
app_settings = AppSettings.objects.get(owner_organization = None)
if Device.objects.filter(name=data['details']['name']).exists():
device = Device.objects.get(name=data['details']['name'])
else: # 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 = default_organization,
)
status = Http.Status.CREATED
if OperatingSystem.objects.filter( slug=data['os']['name'] ).exists():
operating_system = OperatingSystem.objects.get( slug=data['os']['name'] )
else: # Create Operating System
operating_system = OperatingSystem.objects.create(
name = data['os']['name'],
organization = default_organization,
is_global = True
)
if OperatingSystemVersion.objects.filter( name=data['os']['version_major'], operating_system=operating_system ).exists():
operating_system_version = OperatingSystemVersion.objects.get(
organization = default_organization,
is_global = True,
name = data['os']['version_major'],
operating_system = operating_system
)
else: # Create Operating System Version
operating_system_version = OperatingSystemVersion.objects.create(
organization = default_organization,
is_global = True,
name = data['os']['version_major'],
operating_system = operating_system,
)
if DeviceOperatingSystem.objects.filter( version=data['os']['version'], device=device, operating_system_version=operating_system_version ).exists():
device_operating_system = DeviceOperatingSystem.objects.get(
device=device,
version = data['os']['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 = default_organization,
device=device,
version = data['os']['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 = 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 = 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()
status = Http.Status.OK
except Exception as e:
print(f'An error occured{e}')
status = Http.Status.SERVER_ERROR
return Response(data='OK',status=status)
def get_view_name(self):
return "Inventory"

View File

@ -1,13 +1,49 @@
import re
from .urls import urlpatterns
from app.urls import urlpatterns
from django.conf import settings
from django.urls import URLPattern, URLResolver
from settings.models.user_settings import UserSettings
def build_details(context) -> dict:
return {
'project_url': settings.BUILD_REPO,
'sha': settings.BUILD_SHA,
'version': settings.BUILD_VERSION,
}
def request(request):
return request.get_full_path()
def user_settings(context) -> int:
""" Provides the settings ID for the current user.
If user settings object doesn't exist, it's probably a new user. So create their settings row.
Returns:
int: model usersettings Primary Key
"""
if context.user.is_authenticated:
settings = UserSettings.objects.filter(user=context.user)
if not settings.exists():
UserSettings.objects.create(user=context.user)
settings = UserSettings.objects.filter(user=context.user)
return settings[0].pk
return None
def nav_items(context) -> list(dict()):
""" Fetch All Project URLs
@ -61,7 +97,7 @@ def nav_items(context) -> list(dict()):
url = '/' + str(nav_group.pattern) + str(pattern.pattern)
if str(context.path) == url:
if str(context.path).startswith(url):
is_active = True
@ -94,7 +130,10 @@ def nav_items(context) -> list(dict()):
return dnav
def navigation(context):
def common(context):
return {
'nav_items': nav_items(context)
'build_details': build_details(context),
'nav_items': nav_items(context),
'user_settings': user_settings(context),
}

View File

@ -9,6 +9,7 @@ https://docs.djangoproject.com/en/5.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""
import os
from pathlib import Path
@ -18,6 +19,11 @@ from split_settings.tools import optional, include
BASE_DIR = Path(__file__).resolve().parent.parent
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')
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
@ -46,6 +52,7 @@ INSTALLED_APPS = [
'core.apps.CoreConfig',
'access.apps.AccessConfig',
'itam.apps.ItamConfig',
'settings.apps.SettingsConfig',
]
MIDDLEWARE = [
@ -56,6 +63,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'core.middleware.get_request.RequestMiddleware',
]
@ -74,7 +82,7 @@ TEMPLATES = [
'django.contrib.messages.context_processors.messages',
'social_django.context_processors.backends',
'social_django.context_processors.login_redirect',
'app.context_processors.navigation',
'app.context_processors.base.common',
],
},
},
@ -217,3 +225,9 @@ if DEBUG:
"127.0.0.1",
]
# Apps Under Development
INSTALLED_APPS += [
'information.apps.InformationConfig',
'config_management.apps.ConfigManagementConfig',
'project_management.apps.ProjectManagementConfig',
]

View File

@ -1,41 +0,0 @@
from app import settings
import pytest
import unittest
class Test_aa_settings_default(unittest.TestCase):
# @pytest.mark.django_db
# def test_setting_api_disabled_default(self):
# """ As the API is only partially developed, it must be disabled.
# This test can be removed when the API has been fully developed and functioning as it should.
# """
# assert not settings.API_ENABLED
@pytest.mark.django_db
def test_setting_login_required_default(self):
""" By default login should be required
"""
assert settings.LOGIN_REQUIRED
@pytest.mark.django_db
def test_setting_use_tz_default(self):
""" Ensure that 'USE_TZ = True' is within settings
"""
assert settings.USE_TZ
@pytest.mark.django_db
def test_setting_debug_off(self):
""" Ensure that debug is off within settings by default
Debug is only required during development with this setting must always remain off within the committed code.
"""
assert not settings.DEBUG

View File

@ -1,38 +0,0 @@
from django.conf import settings
from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
from access.models import Organization
@pytest.mark.django_db
def test_setting_login_required():
"""Some docstring defining what the test is checking."""
client = Client()
url = reverse('home')
# client.force_login(user)
# default_settings = settings
settings.LOGIN_REQUIRED = True
response = client.get(url)
assert response.status_code == 302
# settings = default_settings
@pytest.mark.django_db
def test_setting_login_required_not():
"""Some docstring defining what the test is checking."""
client = Client()
url = reverse('home')
settings.LOGIN_REQUIRED = False
response = client.get(url)
assert response.status_code == 200

View File

@ -0,0 +1,18 @@
from django.test import TestCase, Client
import pytest
import unittest
import requests
@pytest.mark.skip(reason="to be written")
def test_context_processor_base_user_settings_if_authenticated_only():
""" Context Processor base to only provide `user_settings` for an authenticated user """
pass
@pytest.mark.skip(reason="to be written")
def test_context_processor_base_user_settings_is_logged_in_user():
""" Context Processor base to only provide `user_settings` for the current logged in user """
pass

View File

@ -0,0 +1,66 @@
from django.conf import settings as django_settings
from django.shortcuts import reverse
from django.test import TestCase, Client
from app import settings
import pytest
import unittest
class SettingsDefault(TestCase):
""" Test Settings file default values """
def test_setting_default_login_required(self):
""" By default login should be required
"""
assert settings.LOGIN_REQUIRED
def test_setting_default_use_tz(self):
""" Ensure that 'USE_TZ = True' is within settings
"""
assert settings.USE_TZ
def test_setting_default_debug_off(self):
""" Ensure that debug is off within settings by default
Debug is only required during development with this setting must always remain off within the committed code.
"""
assert not settings.DEBUG
class SettingsValues(TestCase):
""" Test Each setting that offers different functionality """
def test_setting_value_login_required(self):
"""Some docstring defining what the test is checking."""
client = Client()
url = reverse('home')
django_settings.LOGIN_REQUIRED = True
response = client.get(url)
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_setting_value_login_required_not(self):
"""Some docstring defining what the test is checking."""
client = Client()
url = reverse('home')
django_settings.LOGIN_REQUIRED = False
response = client.get(url)
assert response.status_code == 200

View File

@ -17,19 +17,29 @@ Including another URLconf
from django.conf import settings
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import include, path
from django.views.static import serve
from django.urls import include, path, re_path
from .views import home
from core.views import history
from settings.views import user_settings
from .views import HomeView
urlpatterns = [
path('', HomeView.as_view(), name='home'),
path('', home.HomeView.as_view(), name='home'),
path('admin/', admin.site.urls, name='_administration'),
path('account/password_change/', auth_views.PasswordChangeView.as_view(template_name="password_change.html.j2"),
name="change_password"),
path('account/password_change/', auth_views.PasswordChangeView.as_view(template_name="password_change.html.j2"), name="change_password"),
path('account/settings/<int:pk>', user_settings.View.as_view(), name="_settings_user"),
path("account/", include("django.contrib.auth.urls")),
path("organization/", include("access.urls")),
path("itam/", include("itam.urls")),
path("history/<str:model_name>/<int:model_pk>", history.View.as_view(), name='_history'),
re_path(r'^static/(?P<path>.*)$', serve,{'document_root': settings.STATIC_ROOT})
]
if settings.API_ENABLED:
@ -43,4 +53,15 @@ if settings.DEBUG:
urlpatterns += [
path("__debug__/", include("debug_toolbar.urls"), name='_debug'),
# Apps Under Development
path("information/", include("information.urls")),
path("config_management/", include("config_management.urls")),
path("project_management/", include("project_management.urls")),
]
# must be after above
urlpatterns += [
path("settings/", include("settings.urls")),
]

View File

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ConfigManagementConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'config_management'

View File

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

View File

@ -0,0 +1,9 @@
from django.urls import path
from .views import ConfigIndex
app_name = "Config Management"
urlpatterns = [
path('', ConfigIndex.as_view(), name='Config Management'),
]

View File

@ -0,0 +1,18 @@
from django.shortcuts import render
from django.views import generic
class ConfigIndex(generic.View):
permission_required = 'itam.view_device'
template_name = 'form.html.j2'
def get(self, request):
context = {}
context['content_title'] = 'Config Management'
return render(request, self.template_name, context)

15
app/core/forms/comment.py Normal file
View File

@ -0,0 +1,15 @@
from django import forms
from app import settings
from core.models.notes import Notes
class AddNoteForm(forms.ModelForm):
prefix = 'note'
class Meta:
model = Notes
fields = [
'note'
]

17
app/core/http/common.py Normal file
View File

@ -0,0 +1,17 @@
from enum import IntEnum
class Http():
"""Common HTTP Related objects"""
class Status(IntEnum):
"""HTTP server status codes."""
OK = 200
CREATED = 201
BAD_REQUEST = 400
SERVER_ERROR = 500

View File

@ -0,0 +1,64 @@
from django.core.management.base import BaseCommand
from django.db.models import Q
from django.utils import timezone
from core.models.manufacturer import Manufacturer
from settings.models.app_settings import AppSettings
class Command(BaseCommand):
help = 'Manage Common item Manufacturer for the entire application.'
def add_arguments(self, parser):
parser.add_argument('-g', '--global', action='store_true', help='Sets all manufacturer to be global (manufacturers will be migrated to global organization if set)')
parser.add_argument('-m', '--migrate', action='store_true', help='Migrate existing global manufacturers to global organization')
def handle(self, *args, **kwargs):
if kwargs['global']:
softwares = Manufacturer.objects.filter(is_global = False)
self.stdout.write('Running global')
self.stdout.write(f'found manufacturer {str(len(softwares))} to set as global')
for software in softwares:
software.clean()
software.save()
self.stdout.write(f"Setting {software} as global")
self.stdout.write('Global finished')
if kwargs['migrate']:
app_settings = AppSettings.objects.get(owner_organization=None)
self.stdout.write('Running Migrate')
self.stdout.write(f'Global organization: {app_settings.global_organization}')
softwares = Manufacturer.objects.filter(
~Q(organization = app_settings.global_organization)
|
Q(is_global = False)
&
Q(organization=app_settings.global_organization),
)
self.stdout.write(f'found manufacturer {str(len(softwares))} to migrate')
for software in softwares:
software.clean()
software.save()
self.stdout.write(f"Migrating manufacturer {software} to organization {app_settings.global_organization.name}")
self.stdout.write('Migrate finished')

View File

View File

@ -0,0 +1,21 @@
import threading
request_local = threading.local()
def get_request():
return getattr(request_local, 'request', None)
class RequestMiddleware():
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request_local.request = request
return self.get_response(request)
def process_exception(self, request, exception):
request_local.request = None
def process_template_response(self, request, response):
request_local.request = None
return response

View File

@ -0,0 +1,41 @@
# Generated by Django 5.0.6 on 2024-05-20 15:44
import access.fields
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('access', '0001_initial'),
('itam', '0007_device_inventorydate'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Notes',
fields=[
('is_global', models.BooleanField(default=False)),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('serial_number', models.CharField(blank=True, default=None, max_length=50, null=True, verbose_name='Serial Number')),
('note', models.TextField(default=None, null=True, verbose_name='Note')),
('device', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='itam.device')),
('operatingsystem', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='itam.operatingsystem')),
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='access.organization')),
('software', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='itam.software')),
('usercreated', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='usercreated', to=settings.AUTH_USER_MODEL, verbose_name='Added By')),
('usermodified', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='usermodified', to=settings.AUTH_USER_MODEL, verbose_name='Edited By')),
],
options={
'ordering': ['-created'],
},
),
]

View File

@ -0,0 +1,34 @@
# Generated by Django 5.0.6 on 2024-05-20 16:06
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
('itam', '0007_device_inventorydate'),
]
operations = [
migrations.RemoveField(
model_name='notes',
name='serial_number',
),
migrations.AlterField(
model_name='notes',
name='device',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.device'),
),
migrations.AlterField(
model_name='notes',
name='operatingsystem',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.operatingsystem'),
),
migrations.AlterField(
model_name='notes',
name='software',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.software'),
),
]

View File

@ -0,0 +1,41 @@
# Generated by Django 5.0.6 on 2024-05-23 03:59
import access.fields
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0002_remove_notes_serial_number_alter_notes_device_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='notes',
name='note',
field=models.TextField(blank=True, default=None, null=True, verbose_name='Note'),
),
migrations.CreateModel(
name='History',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('before', models.TextField(blank=True, default=None, help_text='JSON Object before Change', null=True)),
('after', models.TextField(blank=True, default=None, help_text='JSON Object After Change', null=True)),
('action', models.IntegerField(choices=[('1', 'Create'), ('2', 'Update'), ('3', 'Delete')], default=None, null=True)),
('item_pk', models.IntegerField(default=None, null=True)),
('item_class', models.CharField(default=None, max_length=50, null=True)),
('item_parent_pk', models.IntegerField(default=None, null=True)),
('item_parent_class', models.CharField(default=None, max_length=50, null=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created'],
},
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 5.0.6 on 2024-05-23 10:37
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0002_alter_team_organization'),
('core', '0003_alter_notes_note_history'),
]
operations = [
migrations.AlterField(
model_name='notes',
name='organization',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization'),
),
]

View File

@ -0,0 +1,32 @@
# Generated by Django 5.0.6 on 2024-05-23 10:58
import access.fields
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0002_alter_team_organization'),
('core', '0004_notes_is_null'),
]
operations = [
migrations.CreateModel(
name='Manufacturer',
fields=[
('is_global', models.BooleanField(default=False)),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('name', models.CharField(max_length=50, unique=True)),
('slug', access.fields.AutoSlugField()),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization')),
],
options={
'ordering': ['name'],
},
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 5.0.6 on 2024-05-25 05:21
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0005_manufacturer'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='history',
name='user',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL),
),
]

View File

View File

@ -0,0 +1,162 @@
import json
from django.db import models
from core.middleware.get_request import get_request
from core.models.history import History
class SaveHistory(models.Model):
class Meta:
abstract = True
@property
def fields(self):
return [ f.name for f in self._meta.fields + self._meta.many_to_many ]
def save(self, *args, **kwargs):
""" OverRides save for keeping model history.
Not a Full-Override as this is just to add to existing.
Before to fetch from DB to ensure the changed value is the actual changed value and the after
is the data that was saved to the DB.
"""
remove_keys = [
'_state',
'created',
'modified'
]
before = {}
try:
before = self.__class__.objects.get(pk=self.pk).__dict__.copy()
except Exception:
pass
clean = {}
for entry in before:
if type(before[entry]) == type(int()):
value = int(before[entry])
elif type(before[entry]) == type(bool()):
value = bool(before[entry])
else:
value = str(before[entry])
if entry not in remove_keys:
clean[entry] = value
before_json = json.dumps(clean)
# Process the save
super().save(*args, **kwargs)
after = self.__dict__.copy()
clean = {}
for entry in after:
if type(after[entry]) == type(int()):
value = int(after[entry])
elif type(after[entry]) == type(bool()):
value = bool(after[entry])
else:
value = str(after[entry])
if entry not in remove_keys and str(before) != '{}':
if after[entry] != before[entry]:
clean[entry] = value
elif entry not in remove_keys:
clean[entry] = value
after = json.dumps(clean)
item_parent_pk = None
item_parent_class = None
if self._meta.model_name == 'deviceoperatingsystem':
item_parent_pk = self.device.pk
item_parent_class = self.device._meta.model_name
if self._meta.model_name == 'devicesoftware':
item_parent_pk = self.device.pk
item_parent_class = self.device._meta.model_name
if self._meta.model_name == 'operatingsystemversion':
item_parent_pk = self.operating_system_id
item_parent_class = self.operating_system._meta.model_name
if self._meta.model_name == 'softwareversion':
item_parent_pk = self.software.pk
item_parent_class = self.software._meta.model_name
if self._meta.model_name == 'team':
item_parent_pk = self.organization.pk
item_parent_class = self.organization._meta.model_name
if self._meta.model_name == 'teamusers':
item_parent_pk = self.team.pk
item_parent_class = self.team._meta.model_name
if not before:
action = History.Actions.ADD
elif before != after:
action = History.Actions.UPDATE
elif not after:
action = History.Actions.DELETE
current_user = None
if get_request() is not None:
current_user = get_request().user
if current_user.is_anonymous:
current_user = None
if before != after and after != '{}':
entry = History.objects.create(
before = before_json,
after = after,
user = current_user,
action = action,
item_pk = self.pk,
item_class = self._meta.model_name,
item_parent_pk = item_parent_pk,
item_parent_class = item_parent_class,
)
entry.save()

View File

@ -0,0 +1,95 @@
from django.contrib.auth.models import User
from django.db import models
from access.fields import *
class HistoryCommonFields(models.Model):
class Meta:
abstract = True
id = models.AutoField(
primary_key=True,
unique=True,
blank=False
)
created = AutoCreatedField()
class History(HistoryCommonFields):
class Meta:
ordering = [
'-created'
]
class Actions(models.TextChoices):
ADD = '1', 'Create'
UPDATE = '2', 'Update'
DELETE = '3', 'Delete'
before = models.TextField(
help_text = 'JSON Object before Change',
blank = True,
default = None,
null = True
)
after = models.TextField(
help_text = 'JSON Object After Change',
blank = True,
default = None,
null = True
)
action = models.IntegerField(
choices=Actions,
default=None,
null=True,
blank = False,
)
user = models.ForeignKey(
User,
on_delete=models.DO_NOTHING,
null = True,
blank= False,
)
item_pk = models.IntegerField(
default=None,
null = True,
blank = False,
)
item_class = models.CharField(
blank = False,
default=None,
null = True,
max_length = 50,
unique = False,
)
item_parent_pk = models.IntegerField(
default=None,
null = True,
blank = False,
)
item_parent_class = models.CharField(
blank = False,
default=None,
null = True,
max_length = 50,
unique = False,
)

View File

@ -0,0 +1,59 @@
from django.contrib.auth.models import User
from django.db import models
from access.fields import *
from access.models import TenancyObject
from core.mixin.history_save import SaveHistory
from settings.models.app_settings import AppSettings
class ManufacturerCommonFields(models.Model):
class Meta:
abstract = True
id = models.AutoField(
primary_key=True,
unique=True,
blank=False
)
created = AutoCreatedField()
modified = AutoCreatedField()
class Manufacturer(TenancyObject, ManufacturerCommonFields, SaveHistory):
class Meta:
ordering = [
'name'
]
name = models.CharField(
blank = False,
max_length = 50,
unique = True,
)
slug = AutoSlugField()
def clean(self):
app_settings = AppSettings.objects.get(owner_organization=None)
if app_settings.manufacturer_is_global:
self.organization = app_settings.global_organization
self.is_global = app_settings.manufacturer_is_global
def __str__(self):
return self.name

100
app/core/models/notes.py Normal file
View File

@ -0,0 +1,100 @@
from django.contrib.auth.models import User
from django.db import models
from access.fields import *
from access.models import TenancyObject
from itam.models.device import Device
from itam.models.software import Software
from itam.models.operating_system import OperatingSystem
class NotesCommonFields(TenancyObject, models.Model):
class Meta:
abstract = True
id = models.AutoField(
primary_key=True,
unique=True,
blank=False
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
class Notes(NotesCommonFields):
""" Notes that can be left against a model
Currently supported models are:
- Device
- Operating System
- Software
"""
class Meta:
ordering = [
'-created'
]
note = models.TextField(
verbose_name = 'Note',
blank = True,
default = None,
null = True
)
usercreated = models.ForeignKey(
User,
verbose_name = 'Added By',
related_name = 'usercreated',
on_delete=models.SET_DEFAULT,
default = None,
null = True,
blank= True
)
usermodified = models.ForeignKey(
User,
verbose_name = 'Edited By',
related_name = 'usermodified',
on_delete=models.SET_DEFAULT,
default = None,
null = True,
blank= True
)
device = models.ForeignKey(
Device,
on_delete=models.CASCADE,
default = None,
null = True,
blank= True
)
software = models.ForeignKey(
Software,
on_delete=models.CASCADE,
default = None,
null = True,
blank= True
)
operatingsystem = models.ForeignKey(
OperatingSystem,
on_delete=models.CASCADE,
default = None,
null = True,
blank= True
)
def __str__(self):
return 'Note ' + str(self.id)

View File

@ -0,0 +1,60 @@
{% extends 'base.html.j2' %}
{% load json %}
{% block body %}
<script>
$('.clicker').click(function(){
$(this).nextUntil('.clicker').slideToggle('normal');
});
</script>
<style>
.hidden {
/*display: none;*/
}
.down {
display: unset;
}
</style>
<table style="max-width: 100%;">
<thead>
<th style="width: 25%;">Created</th>
<th style="width: 25%;">Action</th>
<th style="width: 25%;">Item</th>
<th style="width: 25%;">User</th>
</thead>
{% for entry in history %}
<tr class="clicker">
<td>{{ entry.created }}</td>
<td>
{% if entry.action == 1 %}
Create
{% elif entry.action == 2 %}
Update
{% elif entry.action == 3 %}
Delete
{% else %}
fuck knows
{% endif %}
</td>
<td>
{{ entry.item_class}}
</td>
<td>{{ entry.user }}</td>
<tr class="hidden">
<th colspan="2">Before</th>
<th colspan="2">Changed</th>
</tr>
<tr class="hidden">
<td colspan="2"><pre style="text-align: left; max-width: 300px;">{{ entry.before | json_pretty }}</pre></td>
<td colspan="2"><pre style="text-align: left; max-width: 300px;">{{ entry.after | json_pretty }}</pre></td>
</tr>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@ -0,0 +1,35 @@
{% load markdown %}
<div class="comment">
<div class="comment-header">
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 -960 960 960" width="16px">
<path
d="M480-400q-17 0-28.5-11.5T440-440q0-17 11.5-28.5T480-480q17 0 28.5 11.5T520-440q0 17-11.5 28.5T480-400Zm-160 0q-17 0-28.5-11.5T280-440q0-17 11.5-28.5T320-480q17 0 28.5 11.5T360-440q0 17-11.5 28.5T320-400Zm320 0q-17 0-28.5-11.5T600-440q0-17 11.5-28.5T640-480q17 0 28.5 11.5T680-440q0 17-11.5 28.5T640-400ZM480-240q-17 0-28.5-11.5T440-280q0-17 11.5-28.5T480-320q17 0 28.5 11.5T520-280q0 17-11.5 28.5T480-240Zm-160 0q-17 0-28.5-11.5T280-280q0-17 11.5-28.5T320-320q17 0 28.5 11.5T360-280q0 17-11.5 28.5T320-240Zm320 0q-17 0-28.5-11.5T600-280q0-17 11.5-28.5T640-320q17 0 28.5 11.5T680-280q0 17-11.5 28.5T640-240ZM200-80q-33 0-56.5-23.5T120-160v-560q0-33 23.5-56.5T200-800h40v-80h80v80h320v-80h80v80h40q33 0 56.5 23.5T840-720v560q0 33-23.5 56.5T760-80H200Zm0-80h560v-400H200v400Z" />
</svg>
<span>{{ note.created }}</span>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 -960 960 960" width="16px" style="">
<path
d="M480-480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47ZM160-160v-112q0-34 17.5-62.5T224-378q62-31 126-46.5T480-440q66 0 130 15.5T736-378q29 15 46.5 43.5T800-272v112H160Z" />
</svg>
<span style="">{{ note.usercreated }}</span>
<span style="display: inline; margin-right: auto">&nbsp;</span>
</div>
<div class="comment-body">{{ note.note | markdown | safe }}</div>
<div class="comment-footer">
<span>edited by: </span>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 -960 960 960" width="16px">
<path
d="M480-400q-17 0-28.5-11.5T440-440q0-17 11.5-28.5T480-480q17 0 28.5 11.5T520-440q0 17-11.5 28.5T480-400Zm-160 0q-17 0-28.5-11.5T280-440q0-17 11.5-28.5T320-480q17 0 28.5 11.5T360-440q0 17-11.5 28.5T320-400Zm320 0q-17 0-28.5-11.5T600-440q0-17 11.5-28.5T640-480q17 0 28.5 11.5T680-440q0 17-11.5 28.5T640-400ZM480-240q-17 0-28.5-11.5T440-280q0-17 11.5-28.5T480-320q17 0 28.5 11.5T520-280q0 17-11.5 28.5T480-240Zm-160 0q-17 0-28.5-11.5T280-280q0-17 11.5-28.5T320-320q17 0 28.5 11.5T360-280q0 17-11.5 28.5T320-240Zm320 0q-17 0-28.5-11.5T600-280q0-17 11.5-28.5T640-320q17 0 28.5 11.5T680-280q0 17-11.5 28.5T640-240ZM200-80q-33 0-56.5-23.5T120-160v-560q0-33 23.5-56.5T200-800h40v-80h80v80h320v-80h80v80h40q33 0 56.5 23.5T840-720v560q0 33-23.5 56.5T760-80H200Zm0-80h560v-400H200v400Z" />
</svg>
<span>date </span>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 -960 960 960" width="16px" style="">
<path
d="M480-480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47ZM160-160v-112q0-34 17.5-62.5T224-378q62-31 126-46.5T480-440q66 0 130 15.5T736-378q29 15 46.5 43.5T800-272v112H160Z" />
</svg>
<span>username</span>
<span style="display: inline; margin-right: auto">&nbsp;</span>
</div>
</div>

View File

@ -0,0 +1,13 @@
from django import template
from django.template.defaultfilters import stringfilter
import json
register = template.Library()
@register.filter()
@stringfilter
def json_pretty(value):
return json.dumps(json.loads(value), indent=4, sort_keys=True)

View File

@ -0,0 +1,12 @@
from django import template
from django.template.defaultfilters import stringfilter
import markdown as md
register = template.Library()
@register.filter()
@stringfilter
def markdown(value):
return md.markdown(value, extensions=['markdown.extensions.fenced_code'])

View File

@ -0,0 +1,31 @@
from django.test import TestCase, Client
import pytest
import unittest
import requests
@pytest.mark.skip(reason="to be written")
def test_history_auth_view_super_admin():
""" Super Admin can view history without requiring permission """
pass
@pytest.mark.skip(reason="to be written")
def test_history_no_entry_without_item():
""" A history entry cant be created without an item
fields required `item_pk` and `item_class`
"""
pass
@pytest.mark.skip(reason="to be written")
def test_history_no_entry_without_parent_item():
""" A history entry cant be created without a parent item
fields required `parent_item_pk` and `parent_item_class
"""
pass

View File

@ -0,0 +1,21 @@
from django.test import TestCase, Client
import pytest
import unittest
import requests
@pytest.mark.skip(reason="to be written")
def test_note_new_correct_usercreated():
""" The user who added the note must be added to the note """
pass
@pytest.mark.skip(reason="to be written")
def test_note_new_correct_usermodified():
""" The user who edited the note must be added to the note """
pass

40
app/core/views/history.py Normal file
View File

@ -0,0 +1,40 @@
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
from django.views import generic
from access.mixin import OrganizationPermission
from core.models.history import History
from itam.models.device import Device, DeviceSoftware, DeviceOperatingSystem
from itam.models.software import Software
class View(OrganizationPermission, generic.View):
permission_required = [
'itam.view_history'
]
template_name = 'history.html.j2'
def get(self, request, model_name, model_pk):
if not request.user.is_authenticated and settings.LOGIN_REQUIRED:
return redirect(f"{settings.LOGIN_URL}?next={request.path}")
context = {}
context['history'] = History.objects.filter(
Q(item_pk = model_pk, item_class = model_name)
|
Q(item_parent_pk = model_pk, item_parent_class = model_name)
)
context['content_title'] = 'History'
return render(request, self.template_name, context)

View File

6
app/information/apps.py Normal file
View File

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

13
app/information/urls.py Normal file
View File

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

View File

@ -0,0 +1,32 @@
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
from django.views import generic
from access.mixin import OrganizationPermission
class Index(generic.View):
# permission_required = [
# 'itil.view_knowledge_base'
# ]
template_name = 'form.html.j2'
def get(self, request):
context = {}
user_string = Template("{% include 'icons/issue_link.html.j2' with issue=10 %}")
user_context = Context(context)
context['form'] = user_string.render(user_context)
context['content_title'] = 'Knowledge Base'
return render(request, self.template_name, context)

View File

@ -0,0 +1,30 @@
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
from django.views import generic
from access.mixin import OrganizationPermission
class Index(generic.View):
# permission_required = [
# 'itil.view_playbook'
# ]
template_name = 'form.html.j2'
def get(self, request):
context = {}
user_string = Template("{% include 'icons/issue_link.html.j2' with issue=11 %}")
user_context = Context(context)
context['form'] = user_string.render(user_context)
context['content_title'] = 'Playbooks'
return render(request, self.template_name, context)

View File

@ -1,6 +1,7 @@
from django import forms
from django.db.models import Q
from app import settings
from itam.models.device import Device
@ -13,7 +14,21 @@ class DeviceForm(forms.ModelForm):
fields = [
'id',
'name',
'device_model',
'serial_number',
'uuid',
'device_type',
'organization'
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['_lastinventory'] = forms.DateTimeField(
label="Last Inventory Date",
input_formats=settings.DATETIME_FORMAT,
initial=kwargs['instance'].inventorydate,
disabled=True,
required=False,
)

View File

@ -1,6 +1,8 @@
from django import forms
from django.db.models import Q
from app import settings
from itam.models.device import DeviceOperatingSystem
@ -16,3 +18,17 @@ class Update(forms.ModelForm):
'operating_system_version',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'instance' in kwargs.keys():
if kwargs['instance'] is not None:
self.fields['_created'] = forms.DateTimeField(
label="Install Date",
input_formats=settings.DATETIME_FORMAT,
initial=kwargs['instance'].installdate,
disabled=True
)

View File

@ -11,6 +11,7 @@ class Update(forms.ModelForm):
model = OperatingSystem
fields = [
"name",
'publisher',
'slug',
'id',
'organization',

View File

@ -10,6 +10,7 @@ class Update(forms.ModelForm):
model = Software
fields = [
"name",
'publisher',
'slug',
'id',
'organization',

View File

@ -0,0 +1,38 @@
# Generated by Django 5.0.6 on 2024-05-20 06:36
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('itam', '0005_alter_operatingsystemversion_name_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='devicesoftware',
options={'ordering': ['-action', 'software']},
),
migrations.AddField(
model_name='deviceoperatingsystem',
name='installdate',
field=models.DateTimeField(blank=True, default=None, null=True, verbose_name='Install Date'),
),
migrations.AddField(
model_name='devicesoftware',
name='installed',
field=models.DateTimeField(blank=True, null=True, verbose_name='Install Date'),
),
migrations.AddField(
model_name='devicesoftware',
name='installedversion',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='installedversion', to='itam.softwareversion'),
),
migrations.AlterField(
model_name='devicesoftware',
name='action',
field=models.CharField(blank=True, choices=[('1', 'Install'), ('0', 'Remove')], default=None, max_length=1, null=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2024-05-20 09:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('itam', '0006_alter_devicesoftware_options_and_more'),
]
operations = [
migrations.AddField(
model_name='device',
name='inventorydate',
field=models.DateTimeField(blank=True, null=True, verbose_name='Last Inventory Date'),
),
]

View File

@ -0,0 +1,60 @@
# Generated by Django 5.0.6 on 2024-05-23 10:37
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0002_alter_team_organization'),
('itam', '0007_device_inventorydate'),
]
operations = [
migrations.AlterField(
model_name='device',
name='organization',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization'),
),
migrations.AlterField(
model_name='deviceoperatingsystem',
name='organization',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization'),
),
migrations.AlterField(
model_name='devicesoftware',
name='organization',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization'),
),
migrations.AlterField(
model_name='devicetype',
name='organization',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization'),
),
migrations.AlterField(
model_name='operatingsystem',
name='organization',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization'),
),
migrations.AlterField(
model_name='operatingsystemversion',
name='organization',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization'),
),
migrations.AlterField(
model_name='software',
name='organization',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization'),
),
migrations.AlterField(
model_name='softwarecategory',
name='organization',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization'),
),
migrations.AlterField(
model_name='softwareversion',
name='organization',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization'),
),
]

View File

@ -0,0 +1,39 @@
# Generated by Django 5.0.6 on 2024-05-23 12:05
import access.fields
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0002_alter_team_organization'),
('core', '0005_manufacturer'),
('itam', '0008_alter_device_organization_and_more'),
]
operations = [
migrations.CreateModel(
name='DeviceModel',
fields=[
('is_global', models.BooleanField(default=False)),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('name', models.CharField(max_length=50, unique=True)),
('slug', access.fields.AutoSlugField()),
('manufacturer', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.manufacturer')),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization')),
],
options={
'ordering': ['manufacturer', 'name'],
},
),
migrations.AddField(
model_name='device',
name='device_model',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.devicemodel'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 5.0.6 on 2024-05-23 12:48
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0005_manufacturer'),
('itam', '0009_devicemodel_device_device_model'),
]
operations = [
migrations.AddField(
model_name='operatingsystem',
name='publisher',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.manufacturer'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 5.0.6 on 2024-05-23 12:49
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0005_manufacturer'),
('itam', '0010_operatingsystem_publisher_software_publisher'),
]
operations = [
migrations.AddField(
model_name='software',
name='publisher',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.manufacturer'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.0.6 on 2024-05-28 06:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('itam', '0011_software_publisher'),
]
operations = [
migrations.AlterField(
model_name='device',
name='serial_number',
field=models.CharField(blank=True, default=None, max_length=50, null=True, unique=True, verbose_name='Serial Number'),
),
migrations.AlterField(
model_name='device',
name='uuid',
field=models.CharField(blank=True, default=None, max_length=50, null=True, unique=True, verbose_name='UUID'),
),
]

View File

@ -2,58 +2,45 @@ from django.db import models
from access.fields import *
from access.models import TenancyObject
from core.mixin.history_save import SaveHistory
from itam.models.device_common import DeviceCommonFields, DeviceCommonFieldsName
from itam.models.device_models import DeviceModel
from itam.models.software import Software, SoftwareVersion
from itam.models.operating_system import OperatingSystemVersion
class DeviceCommonFields(TenancyObject, models.Model):
class Meta:
abstract = True
id = models.AutoField(
primary_key=True,
unique=True,
blank=False
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
class DeviceCommonFieldsName(DeviceCommonFields):
class Meta:
abstract = True
name = models.CharField(
blank = False,
max_length = 50,
unique = True,
)
slug = AutoSlugField()
from settings.models.app_settings import AppSettings
class DeviceType(DeviceCommonFieldsName):
def clean(self):
app_settings = AppSettings.objects.get(owner_organization=None)
if app_settings.device_type_is_global:
self.organization = app_settings.global_organization
self.is_global = app_settings.device_type_is_global
def __str__(self):
return self.name
class Device(DeviceCommonFieldsName):
class Device(DeviceCommonFieldsName, SaveHistory):
serial_number = models.CharField(
verbose_name = 'Serial Number',
max_length = 50,
default = None,
null = True,
blank = True
blank = True,
unique = True,
)
@ -62,10 +49,18 @@ class Device(DeviceCommonFieldsName):
max_length = 50,
default = None,
null = True,
blank = True
blank = True,
unique = True,
)
device_model = models.ForeignKey(
DeviceModel,
on_delete=models.CASCADE,
default = None,
null = True,
blank= True
)
device_type = models.ForeignKey(
DeviceType,
@ -76,10 +71,19 @@ class Device(DeviceCommonFieldsName):
)
inventorydate = models.DateTimeField(
verbose_name = 'Last Inventory Date',
null = True,
blank = True
)
def __str__(self):
return self.name
def get_configuration(self, id):
softwares = DeviceSoftware.objects.filter(device=id)
@ -89,33 +93,40 @@ class Device(DeviceCommonFieldsName):
}
for software in softwares:
if software.action:
if int(software.action) == 1:
if int(software.action) == 1:
state = 'present'
state = 'present'
elif int(software.action) == 0:
elif int(software.action) == 0:
state = 'absent'
state = 'absent'
software_action = {
"name": software.software.slug,
"state": state
}
software_action = {
"name": software.software.slug,
"state": state
}
if software.version:
software_action['version'] = software.version.name
if software.version:
software_action['version'] = software.version.name
config['software'] = config['software'] + [ software_action ]
config['software'] = config['software'] + [ software_action ]
return config
class DeviceSoftware(DeviceCommonFields):
class DeviceSoftware(DeviceCommonFields, SaveHistory):
""" A way for the device owner to configure software to install/remove """
class Meta:
ordering = [
'-action',
'software'
]
class Actions(models.TextChoices):
INSTALL = '1', 'Install'
@ -128,7 +139,6 @@ class DeviceSoftware(DeviceCommonFields):
default = None,
null = False,
blank= False
)
software = models.ForeignKey(
@ -137,13 +147,14 @@ class DeviceSoftware(DeviceCommonFields):
default = None,
null = False,
blank= False
)
action = models.CharField(
max_length=1,
choices=Actions,
default=None,
null=True,
blank = True,
)
version = models.ForeignKey(
@ -152,12 +163,27 @@ class DeviceSoftware(DeviceCommonFields):
default = None,
null = True,
blank= True
)
installedversion = models.ForeignKey(
SoftwareVersion,
related_name = 'installedversion',
on_delete=models.CASCADE,
default = None,
null = True,
blank= True
)
installed = models.DateTimeField(
verbose_name = 'Install Date',
null = True,
blank = True
)
class DeviceOperatingSystem(DeviceCommonFields):
class DeviceOperatingSystem(DeviceCommonFields, SaveHistory):
device = models.ForeignKey(
Device,
@ -184,3 +210,10 @@ class DeviceOperatingSystem(DeviceCommonFields):
null = False,
blank = False,
)
installdate = models.DateTimeField(
verbose_name = 'Install Date',
null = True,
blank = True,
default = None,
)

View File

@ -0,0 +1,35 @@
from django.db import models
from access.fields import *
from access.models import TenancyObject
class DeviceCommonFields(TenancyObject, models.Model):
class Meta:
abstract = True
id = models.AutoField(
primary_key=True,
unique=True,
blank=False
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
class DeviceCommonFieldsName(DeviceCommonFields):
class Meta:
abstract = True
name = models.CharField(
blank = False,
max_length = 50,
unique = True,
)
slug = AutoSlugField()

View File

@ -0,0 +1,44 @@
from django.contrib.auth.models import User
from django.db import models
from itam.models.device_common import DeviceCommonFieldsName
from access.models import TenancyObject
from core.mixin.history_save import SaveHistory
from core.models.manufacturer import Manufacturer
from settings.models.app_settings import AppSettings
class DeviceModel(DeviceCommonFieldsName, SaveHistory):
class Meta:
ordering = [
'manufacturer',
'name',
]
manufacturer = models.ForeignKey(
Manufacturer,
on_delete=models.CASCADE,
default = None,
null = True,
blank= True
)
def clean(self):
app_settings = AppSettings.objects.get(owner_organization=None)
if app_settings.device_model_is_global:
self.organization = app_settings.global_organization
self.is_global = app_settings.device_model_is_global
def __str__(self):
return self.manufacturer.name + ' ' + self.name

View File

@ -3,6 +3,9 @@ from django.db import models
from access.fields import *
from access.models import TenancyObject
from core.mixin.history_save import SaveHistory
from core.models.manufacturer import Manufacturer
class OperatingSystemCommonFields(TenancyObject, models.Model):
@ -37,14 +40,22 @@ class OperatingSystemFieldsName(OperatingSystemCommonFields):
class OperatingSystem(OperatingSystemFieldsName):
class OperatingSystem(OperatingSystemFieldsName, SaveHistory):
publisher = models.ForeignKey(
Manufacturer,
on_delete=models.CASCADE,
default = None,
null = True,
blank= True
)
def __str__(self):
return self.name
class OperatingSystemVersion(OperatingSystemCommonFields):
class OperatingSystemVersion(OperatingSystemCommonFields, SaveHistory):
operating_system = models.ForeignKey(
OperatingSystem,

View File

@ -3,6 +3,10 @@ from django.db import models
from access.fields import *
from access.models import TenancyObject
from core.mixin.history_save import SaveHistory
from core.models.manufacturer import Manufacturer
from settings.models.app_settings import AppSettings
class SoftwareCommonFields(TenancyObject, models.Model):
@ -30,7 +34,18 @@ class SoftwareCommonFields(TenancyObject, models.Model):
class SoftwareCategory(SoftwareCommonFields):
class SoftwareCategory(SoftwareCommonFields, SaveHistory):
def clean(self):
app_settings = AppSettings.objects.get(owner_organization=None)
if app_settings.software_categories_is_global:
self.organization = app_settings.global_organization
self.is_global = app_settings.software_categories_is_global
def __str__(self):
@ -38,7 +53,15 @@ class SoftwareCategory(SoftwareCommonFields):
class Software(SoftwareCommonFields):
class Software(SoftwareCommonFields, SaveHistory):
publisher = models.ForeignKey(
Manufacturer,
on_delete=models.CASCADE,
default = None,
null = True,
blank= True
)
category = models.ForeignKey(
SoftwareCategory,
@ -46,16 +69,27 @@ class Software(SoftwareCommonFields):
default = None,
null = True,
blank= True
)
def clean(self):
app_settings = AppSettings.objects.get(owner_organization=None)
if app_settings.software_is_global:
self.organization = app_settings.global_organization
self.is_global = app_settings.software_is_global
def __str__(self):
return self.name
class SoftwareVersion(SoftwareCommonFields):
class SoftwareVersion(SoftwareCommonFields, SaveHistory):
software = models.ForeignKey(
Software,

View File

@ -39,6 +39,7 @@
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'OperatingSystem')">Operating System</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 class="tablinks" onclick="openCity(event, 'Installations')">Installations</button> -->
</div>
@ -52,6 +53,7 @@
<span style="font-weight: normal; float: right;">{% include 'icons/issue_link.html.j2' with issue=6 %}</span>
</h3>
{{ form.as_p }}
{% include 'icons/issue_link.html.j2' with issue=13 %}<br>
<input name="{{form.prefix}}" type="submit" value="Submit">
<script>
// Get the element with id="defaultOpen" and click on it
@ -68,13 +70,16 @@
<div id="Software" class="tabcontent">
<h3>Software</h3>
<hr>
Installed Software: {{ installed_software }}
<input type="button" value="Add Software Action" onclick="window.location='{% url 'ITAM:_device_software_add' device.id %}';">
<table>
<thead>
<th>Name</th>
<th>Category</th>
<th>Action</th>
<th>Version</th>
<th>Desired Version</th>
<th>Installed Version</th>
<th>Installed</th>
<th>&nbsp;</th>
</thead>
@ -82,26 +87,66 @@
{% for software in softwares %}
<tr>
<td><a href="{% url 'ITAM:_software_view' pk=software.software_id %}">{{ software.software }}</a></td>
<td><a href="{% url 'ITAM:_device_software_view' device_id=device.id pk=software.id %}">{{ software.get_action_display }}</a></td>
<td>{{ software.software.category }}</td>
<td>
{% if software.version %}
{{ software.version }}
{% url 'ITAM:_device_software_view' device_id=device.id pk=software.id as icon_link %}
{% if software.get_action_display == 'Install' %}
{% include 'icons/success_text.html.j2' with icon_text=software.get_action_display icon_link=icon_link %}
{% elif software.get_action_display == 'Remove'%}
{% include 'icons/cross_text.html.j2' with icon_text=software.get_action_display icon_link=icon_link %}
{% else %}
Any
{% include 'icons/add_link.html.j2' with icon_text='Add' icon_link=icon_link %}
{% endif %}
</td>
<td>
{% include 'icons/issue_link.html.j2' with issue=2 %}
{% if software.version %}
{{ software.version }}
{% else %}
-
{% endif %}
</td>
<td>
{% if software.installedversion %}
{{ software.installedversion }}
{% else %}
-
{% endif %}
</td>
<td>
{% if software.installed %}
{{ software.installed }}
{% else %}
-
{% endif %}
</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
{% else %}
<td colspan="5">Nothing Found {% include 'icons/issue_link.html.j2' with issue=2 %}</td>
<td colspan="5">Nothing Found</td>
{% endif %}
</table>
</div>
<div id="Notes" class="tabcontent">
<h3>
Notes
</h3>
{{ notes_form }}
<input type="submit" name="{{notes_form.prefix}}" value="Submit" />
<div class="comments">
{% if notes %}
{% for note in notes%}
{% include 'note.html.j2' %}
{% endfor %}
{% endif %}
</div>
</div>
<div id="ConfigManagement" class="tabcontent">
<h3>Configuration Management</h3>
<div>

View File

@ -6,7 +6,6 @@
{% block body %}
<input type="button" value="New Device" onclick="window.location='{% url 'ITAM:_device_add' %}';">
<input type="button" value="New Device Type" onclick="window.location='{% url 'ITAM:_device_type_add' %}';">
<table class="data">
<tr>
<th>Name</th>
@ -19,11 +18,29 @@
{% for device in devices %}
<tr>
<td><a href="{% url 'ITAM:_device_view' pk=device.id %}">{{ device.name }}</a></td>
<td>{{ device.device_type }}</td>
<td>manufacturer</td>
<td>model</td>
<td>
{% if device.device_type %}
{{ device.device_type }}
{% else %}
-
{% endif %}
</td>
<td>
{% if device.device_model.manufacturer %}
<a href="{% url 'Settings:_manufacturer_view' pk=device.device_model.manufacturer.id %}">{{ device.device_model.manufacturer }}</a>
{% else %}
-
{% endif %}
</td>
<td>
{% if device.device_model.name %}
<a href="{% url 'Settings:_device_model_view' pk=device.device_model.id %}">{{ device.device_model.name }}</a>
{% else %}
-
{% endif %}
</td>
<td>{% if software.is_global %}Global{% else %}{{ device.organization }}{% endif %}</td>
<td><a href="{% url 'ITAM:_device_delete' pk=device.id %}">Delete</a></td>
<td>&nbsp;</td>
</tr>
{% endfor %}

View File

@ -31,19 +31,21 @@
<svg xmlns="http://www.w3.org/2000/svg" height="25px" viewBox="0 -960 960 960" width="25px" style="vertical-align: middle; margin: 0px; padding: 0px border: none; " fill="#6a6e73">
<path d="m313-480 155 156q11 11 11.5 27.5T468-268q-11 11-28 11t-28-11L228-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T468-692q11 11 11 28t-11 28L313-480Zm264 0 155 156q11 11 11.5 27.5T732-268q-11 11-28 11t-28-11L492-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T732-692q11 11 11 28t-11 28L577-480Z"/>
</svg> Back to Operating Systems</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
<button class="tablinks" onclick="openCity(event, 'Versions')">Versions</button>
<button class="tablinks" onclick="openCity(event, 'Licences')">Licences</button>
<button class="tablinks" onclick="openCity(event, 'Installations')">Installations</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
<button class="tablinks" onclick="openCity(event, 'Versions')">Versions</button>
<button class="tablinks" onclick="openCity(event, 'Licences')">Licences</button>
<button class="tablinks" onclick="openCity(event, 'Notes')">Notes</button>
<button class="tablinks" onclick="openCity(event, 'Installations')">Installations</button>
</div>
<form method="post">
<div id="Details" class="tabcontent">
<h3>Details</h3>
<form method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="Submit">
</form>
{% csrf_token %}
{{ form }}
{% include 'icons/issue_link.html.j2' with issue=13 %}<br>
<input type="submit" value="Submit">
<script>
document.getElementById("defaultOpen").click();
@ -106,6 +108,24 @@
</div>
<div id="Notes" class="tabcontent">
<h3>
Notes
</h3>
{{ notes_form }}
<input type="submit" name="{{notes_form.prefix}}" value="Submit" />
<div class="comments">
{% if notes %}
{% for note in notes%}
{% include 'note.html.j2' %}
{% endfor %}
{% endif %}
</div>
</div>
<div id="Installations" class="tabcontent">
<h3>Installations</h3>
<table>
@ -121,11 +141,18 @@
<td><a href="{% url 'ITAM:_device_view' pk=install.device_id %}">{{ install.device }}</a></td>
<td>{{ install.organization }}</td>
<td>{{ install.version }}</td>
<td>{% include 'icons/issue_link.html.j2' with issue=2 %}</td>
<td>
{% if install.installdate %}
{{ install.installdate }}
{% else %}
-
{% endif %}
</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
</table>
</div>
</form>
{% endblock %}

View File

@ -9,18 +9,22 @@
<table class="data">
<tr>
<th>Name</th>
<th>Created</th>
<th>Modified</th>
<th>Publisher</th>
<th>Organization</th>
<th>&nbsp;</th>
</tr>
{% for operating_system in operating_systems %}
<tr>
<td><a href="{% url 'ITAM:_operating_system_view' pk=operating_system.id %}">{{ operating_system.name }}</a></td>
<td>{{ operating_system.created }}</td>
<td>{{ operating_system.modified }}</td>
<td>
{% if operating_system.publisher %}
<a href="{% url 'Settings:_manufacturer_view' pk=operating_system.publisher.id %}">{{ operating_system.publisher }}</a>
{% else %}
-
{% endif %}
</td>
<td>{% if operating_system.is_global %}Global{% else %}{{ operating_system.organization }}{% endif %}</td>
<td><a href="{% url 'ITAM:_operating_system_delete' pk=operating_system.id %}">Delete</a></td>
<td>&nbsp;</td>
</tr>
{% endfor %}

View File

@ -37,17 +37,18 @@
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Versions')">Versions</button>
<button class="tablinks" onclick="openCity(event, 'Licences')">Licences</button>
<button class="tablinks" onclick="openCity(event, 'Notes')">Notes</button>
<button class="tablinks" onclick="openCity(event, 'Installations')">Installations</button>
</div>
<!-- Tab content -->
<form method="post">
<div id="Details" class="tabcontent">
<h3>Details</h3>
<form method="post">
{% csrf_token %}
{{ form }}
{% include 'icons/issue_link.html.j2' with issue=13 %}<br>
<input type="submit" value="Submit">
</form>
<script>
// Get the element with id="defaultOpen" and click on it
@ -69,7 +70,7 @@
{% for version in software_versions %}
<tr>
<td><a href="{% url 'ITAM:_software_version_view' software_id=software.id pk=version.id %}">{{ version.name }}</a></td>
<td>{% include 'icons/issue_link.html.j2' with issue=2 %}</td>
<td>{{ version.installs }}</td>
<td>{% include 'icons/issue_link.html.j2' with issue=3 %}</td>
<td>&nbsp;</td>
</tr>
@ -108,6 +109,24 @@
</table>
</div>
<div id="Notes" class="tabcontent">
<h3>
Notes
</h3>
{{ notes_form }}
<input type="submit" name="{{notes_form.prefix}}" value="Submit" />
<div class="comments">
{% if notes %}
{% for note in notes%}
{% include 'note.html.j2' %}
{% endfor %}
{% endif %}
</div>
</div>
<div id="Installations" class="tabcontent">
<h3>Installations</h3>
<table>
@ -115,8 +134,8 @@
<th>Device</th>
<th>Organization</th>
<th title="Not Set/Install/Remove">Action</th>
<th>Version</th>
<th title="Date Software Installed">Installed</th>
<th>Installed Version</th>
<th title="Date Software Installed">Install Date</th>
<th>&nbsp;</th>
</thead>
{% if device_software %}
@ -130,17 +149,23 @@
{% elif device.get_action_display == 'Remove'%}
{% include 'icons/cross_text.html.j2' with icon_text=device.get_action_display %}
{% else %}
{{ device.get_action_display }}
-
{% endif %}
</td>
<td>
{% if device.version %}
{{ device.version }}
{% if device.installedversion %}
{{ device.installedversion }}
{% else %}
Any
-
{% endif %}
</td>
<td>{% include 'icons/issue_link.html.j2' with issue=2 %}</td>
<td>
{% if device.installed %}
{{ device.installed }}
{% else %}
-
{% endif %}
</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
@ -151,6 +176,6 @@
{% endif %}
</table>
</div>
</form>
{% endblock %}

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