Merge branch 'development' into 'master'
chore: release 0.3.0 See merge request nofusscomputing/projects/django_template!14
This commit is contained in:
@ -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:
|
||||
|
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
@ -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
20
.vscode/launch.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -4,4 +4,9 @@
|
||||
"cSpell.enableFiletypes": [
|
||||
"!python"
|
||||
],
|
||||
"python.testing.pytestArgs": [
|
||||
"app"
|
||||
],
|
||||
"python.testing.unittestEnabled": true,
|
||||
"python.testing.pytestEnabled": true,
|
||||
}
|
@ -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
8
README.md
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||

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

|
||||
|
||||

|
||||
|
19
app/access/migrations/0002_alter_team_organization.py
Normal file
19
app/access/migrations/0002_alter_team_organization.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -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:
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
532
app/access/tests/organization/test_organizaiton_permission.py
Normal file
532
app/access/tests/organization/test_organizaiton_permission.py
Normal 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')
|
@ -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
|
10
app/access/tests/organization/test_organization.py
Normal file
10
app/access/tests/organization/test_organization.py
Normal 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
|
||||
|
@ -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
|
43
app/access/tests/team/test_team_core_history.py
Normal file
43
app/access/tests/team/test_team_core_history.py
Normal 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
|
510
app/access/tests/team/test_team_permission.py
Normal file
510
app/access/tests/team/test_team_permission.py
Normal 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})
|
32
app/access/tests/team/test_team_permission_api.py
Normal file
32
app/access/tests/team/test_team_permission_api.py
Normal 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
|
24
app/access/tests/team_user/test_team_user_core_history.py
Normal file
24
app/access/tests/team_user/test_team_user_core_history.py
Normal 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
|
48
app/access/tests/team_user/test_team_user_functions.py
Normal file
48
app/access/tests/team_user/test_team_user_functions.py
Normal 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
|
||||
|
538
app/access/tests/team_user/test_team_user_permission.py
Normal file
538
app/access/tests/team_user/test_team_user_permission.py
Normal 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
|
||||
}
|
||||
)
|
32
app/access/tests/team_user/test_team_user_permission_api.py
Normal file
32
app/access/tests/team_user/test_team_user_permission_api.py
Normal 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
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"),
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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',
|
||||
)
|
||||
|
@ -25,6 +25,7 @@ class DeviceSerializer(serializers.ModelSerializer):
|
||||
fields = '__all__'
|
||||
|
||||
read_only_fields = [
|
||||
'inventorydate',
|
||||
'is_global',
|
||||
'organization',
|
||||
]
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
151
app/api/tests/test_api_inventory.py
Normal file
151
app/api/tests/test_api_inventory.py
Normal 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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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),
|
||||
}
|
||||
)
|
||||
|
0
app/api/views/itam/device/__init__.py
Normal file
0
app/api/views/itam/device/__init__.py
Normal file
280
app/api/views/itam/device/inventory.py
Normal file
280
app/api/views/itam/device/inventory.py
Normal 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"
|
@ -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),
|
||||
}
|
@ -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',
|
||||
]
|
||||
|
@ -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
|
@ -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
|
18
app/app/tests/test_context_processor_base.py
Normal file
18
app/app/tests/test_context_processor_base.py
Normal 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
|
66
app/app/tests/test_settings.py
Normal file
66
app/app/tests/test_settings.py
Normal 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
|
@ -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")),
|
||||
|
||||
]
|
||||
|
0
app/config_management/__init__.py
Normal file
0
app/config_management/__init__.py
Normal file
6
app/config_management/apps.py
Normal file
6
app/config_management/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ConfigManagementConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'config_management'
|
0
app/config_management/migrations/__init__.py
Normal file
0
app/config_management/migrations/__init__.py
Normal file
1
app/config_management/models.py
Normal file
1
app/config_management/models.py
Normal file
@ -0,0 +1 @@
|
||||
from django.db import models
|
9
app/config_management/urls.py
Normal file
9
app/config_management/urls.py
Normal 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'),
|
||||
|
||||
]
|
18
app/config_management/views.py
Normal file
18
app/config_management/views.py
Normal 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
15
app/core/forms/comment.py
Normal 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
17
app/core/http/common.py
Normal 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
|
64
app/core/management/commands/manufacturer.py
Normal file
64
app/core/management/commands/manufacturer.py
Normal 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')
|
0
app/core/middleware/__init__.py
Normal file
0
app/core/middleware/__init__.py
Normal file
21
app/core/middleware/get_request.py
Normal file
21
app/core/middleware/get_request.py
Normal 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
|
41
app/core/migrations/0001_initial.py
Normal file
41
app/core/migrations/0001_initial.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
41
app/core/migrations/0003_alter_notes_note_history.py
Normal file
41
app/core/migrations/0003_alter_notes_note_history.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
20
app/core/migrations/0004_notes_is_null.py
Normal file
20
app/core/migrations/0004_notes_is_null.py
Normal 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'),
|
||||
),
|
||||
]
|
32
app/core/migrations/0005_manufacturer.py
Normal file
32
app/core/migrations/0005_manufacturer.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
21
app/core/migrations/0006_alter_history_user.py
Normal file
21
app/core/migrations/0006_alter_history_user.py
Normal 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),
|
||||
),
|
||||
]
|
0
app/core/migrations/__init__.py
Normal file
0
app/core/migrations/__init__.py
Normal file
162
app/core/mixin/history_save.py
Normal file
162
app/core/mixin/history_save.py
Normal 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()
|
95
app/core/models/history.py
Normal file
95
app/core/models/history.py
Normal 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,
|
||||
)
|
59
app/core/models/manufacturer.py
Normal file
59
app/core/models/manufacturer.py
Normal 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
100
app/core/models/notes.py
Normal 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)
|
60
app/core/templates/history.html.j2
Normal file
60
app/core/templates/history.html.j2
Normal 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 %}
|
35
app/core/templates/note.html.j2
Normal file
35
app/core/templates/note.html.j2
Normal 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"> </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"> </span>
|
||||
</div>
|
||||
|
||||
</div>
|
13
app/core/templatetags/json.py
Normal file
13
app/core/templatetags/json.py
Normal 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)
|
12
app/core/templatetags/markdown.py
Normal file
12
app/core/templatetags/markdown.py
Normal 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'])
|
31
app/core/tests/test_history.py
Normal file
31
app/core/tests/test_history.py
Normal 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
|
21
app/core/tests/test_notes.py
Normal file
21
app/core/tests/test_notes.py
Normal 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
40
app/core/views/history.py
Normal 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)
|
0
app/information/__init__.py
Normal file
0
app/information/__init__.py
Normal file
6
app/information/apps.py
Normal file
6
app/information/apps.py
Normal 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
13
app/information/urls.py
Normal 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"),
|
||||
|
||||
]
|
32
app/information/views/knowledge_base.py
Normal file
32
app/information/views/knowledge_base.py
Normal 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)
|
30
app/information/views/playbooks.py
Normal file
30
app/information/views/playbooks.py
Normal 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)
|
@ -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,
|
||||
)
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
@ -11,6 +11,7 @@ class Update(forms.ModelForm):
|
||||
model = OperatingSystem
|
||||
fields = [
|
||||
"name",
|
||||
'publisher',
|
||||
'slug',
|
||||
'id',
|
||||
'organization',
|
||||
|
@ -10,6 +10,7 @@ class Update(forms.ModelForm):
|
||||
model = Software
|
||||
fields = [
|
||||
"name",
|
||||
'publisher',
|
||||
'slug',
|
||||
'id',
|
||||
'organization',
|
||||
|
@ -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),
|
||||
),
|
||||
]
|
18
app/itam/migrations/0007_device_inventorydate.py
Normal file
18
app/itam/migrations/0007_device_inventorydate.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
39
app/itam/migrations/0009_devicemodel_device_device_model.py
Normal file
39
app/itam/migrations/0009_devicemodel_device_device_model.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
20
app/itam/migrations/0011_software_publisher.py
Normal file
20
app/itam/migrations/0011_software_publisher.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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,
|
||||
)
|
||||
|
35
app/itam/models/device_common.py
Normal file
35
app/itam/models/device_common.py
Normal 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()
|
44
app/itam/models/device_models.py
Normal file
44
app/itam/models/device_models.py
Normal 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
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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> </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> </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>
|
||||
|
@ -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> </td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
|
@ -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> </td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
@ -9,18 +9,22 @@
|
||||
<table class="data">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Created</th>
|
||||
<th>Modified</th>
|
||||
<th>Publisher</th>
|
||||
<th>Organization</th>
|
||||
<th> </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> </td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
|
@ -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> </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> </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> </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
Reference in New Issue
Block a user