Compare commits
285 Commits
0.5.0
...
4cb37f8347
Author | SHA1 | Date | |
---|---|---|---|
4cb37f8347 | |||
a2010b9517 | |||
c95736ce14 | |||
b46c61954c | |||
afe4266600 | |||
0c8d1c8da1 | |||
eac998b5cc | |||
5914782252 | |||
73d875c4ac | |||
8f439f0675 | |||
0f102c6aaf | |||
4852c6caeb | |||
3fffba2eba | |||
a1293984ea | |||
4876db50c1 | |||
425cc066af | |||
1086f517fa | |||
2fdbf87ddd | |||
86228836c7 | |||
a6e6c948a5 | |||
dcdfa8feb7 | |||
8388d2e695 | |||
29f269050f | |||
93c4fc2009 | |||
9d5464b5a9 | |||
7848397ae2 | |||
f298ce94bf | |||
3cace8943e | |||
aa40d68c88 | |||
c0f186db89 | |||
6b35e7808c | |||
467f6fca6b | |||
f86b2d5216 | |||
e29d8e1ec1 | |||
0fc5f41391 | |||
4b29448d84 | |||
e9fe4896df | |||
b9d32a2c16 | |||
d6bd99c5de | |||
7de5ab12bf | |||
3fe09fb8f9 | |||
eb6b03f731 | |||
40e3078a58 | |||
4ba79c6ae9 | |||
b5c31d81d3 | |||
c3b585d416 | |||
84d21f4af8 | |||
262e431834 | |||
cde2562048 | |||
67d853cf25 | |||
3ba6bb5b4b | |||
84d4f48c63 | |||
5e8bebbeb1 | |||
b66a8644a0 | |||
f0ae185fc5 | |||
43b7e413a6 | |||
04dc00d79d | |||
8e6fd58107 | |||
bfe9a95038 | |||
33687791ec | |||
57bc972b0f | |||
f437eeccb8 | |||
4e11ad67d0 | |||
40ba645a35 | |||
27e73e21d1 | |||
a6c0785de0 | |||
83328be22e | |||
c6ed5c8279 | |||
a4dc7f479a | |||
71726035dc | |||
c624a3617c | |||
cf00ab6234 | |||
e8684c5206 | |||
bb388a1969 | |||
d99f2d3c6f | |||
81a72773cb | |||
5fa88a5209 | |||
366579c12b | |||
fed0c5c3e5 | |||
c496d10c1a | |||
3993cc96a5 | |||
a4b37b34a9 | |||
2f55024f0b | |||
213644a51a | |||
281d839801 | |||
4fd157a785 | |||
968b3a0f92 | |||
f5ba608ed1 | |||
289668bb7f | |||
9e28722dba | |||
9b673f4a07 | |||
3a9e4b29b3 | |||
8d59462561 | |||
098e41e6a1 | |||
fc3f0b39e2 | |||
de53948cea | |||
823ebc0eb5 | |||
41414438d1 | |||
5704560beb | |||
8a48902b64 | |||
61fe059513 | |||
94576cc733 | |||
3a32c62119 | |||
9ea4fe1adc | |||
0798a672c2 | |||
f4e68529ba | |||
92a411baec | |||
034857d088 | |||
e5ce86a9bb | |||
5188b3d52e | |||
61b9435d1f | |||
8244676530 | |||
ec1e7cca85 | |||
72ab9253d7 | |||
4f89255c4f | |||
8d6d1d0d56 | |||
2d0c3a660a | |||
974a208869 | |||
7f225784c2 | |||
a3be95013c | |||
adefbf3960 | |||
9a1ca7a104 | |||
e84e80cd8f | |||
ebc266010a | |||
519277e18b | |||
a5a5874211 | |||
fa2b90ee7b | |||
5c74360842 | |||
8457f15eca | |||
5bc5a4b065 | |||
40350d166e | |||
9a94ba31e4 | |||
55197e7dcc | |||
a67bc70503 | |||
60538e1cec | |||
416e029c23 | |||
fe64c11927 | |||
1f8244ae40 | |||
9871cf248b | |||
a1759ecaaf | |||
30e0342f52 | |||
5a201ef548 | |||
7b26fac73d | |||
7c62309a31 | |||
621cbd2d71 | |||
d8e89bee10 | |||
4ee62aa399 | |||
f1201e8933 | |||
9acc4fdfcb | |||
6776612b66 | |||
af3e770760 | |||
fbe7e63cc9 | |||
aec460306b | |||
46c4419350 | |||
1f35893db9 | |||
935e10dc24 | |||
a4617c28f8 | |||
d4aaea4dbb | |||
a7168834ba | |||
329049e81d | |||
e25ec12cb0 | |||
5ae487cd3e | |||
4c42f77692 | |||
3aab7b57e8 | |||
931c9864db | |||
65bf994619 | |||
367c4bebb6 | |||
8c1be67974 | |||
789c035a03 | |||
1cf15f7339 | |||
77ff580f19 | |||
423ff11d4c | |||
9e4b5185b1 | |||
020441c41a | |||
d0a3b7b49d | |||
960fa5485d | |||
26db463044 | |||
1193f1d86d | |||
9bece0a811 | |||
f29ec63f46 | |||
e48278e6e9 | |||
c41c7ed1f0 | |||
c9190e9a7d | |||
0294f5ed65 | |||
ae4fdcfc58 | |||
a395f30bd4 | |||
c057ffdc9c | |||
6837c38303 | |||
ee8920a464 | |||
ccfdf005f7 | |||
0276f9454b | |||
45cc34284a | |||
7329a65ae7 | |||
9a529a64e2 | |||
f2640df0d3 | |||
7d172fb4af | |||
f848d01b34 | |||
44f20b28be | |||
2e22a484a0 | |||
a62a36ba82 | |||
c00cf16bc8 | |||
7784dfede9 | |||
03d350e302 | |||
9b79c9d7ff | |||
1d5c86f13b | |||
9e336d368d | |||
937e935949 | |||
860eaa6749 | |||
aab94431a9 | |||
7cfede45b8 | |||
65de93715d | |||
fea7ea3119 | |||
524a70ba18 | |||
29c4b4a0ca | |||
f5ae01b08d | |||
ee3dd68cfe | |||
25efa31493 | |||
4a6ce35332 | |||
332810ffd6 | |||
f0bbd22cf4 | |||
6bf681530d | |||
9dd2f6a341 | |||
23c640a460 | |||
3c6092f776 | |||
cb66b9303a | |||
2d80f02634 | |||
abe1ce6948 | |||
fb907283b0 | |||
86ed7318ec | |||
a2a8e12046 | |||
c1a8ee65f2 | |||
6a14f78bf7 | |||
90a01911da | |||
de3ed3a881 | |||
656807e410 | |||
f64be2ea33 | |||
ef9c596ec7 | |||
f22e886d92 | |||
a2c67541ec | |||
5f4231ab04 | |||
b0405c8fd0 | |||
b42bb3a30e | |||
27eb54cc37 | |||
a8e2c687b1 | |||
7aeba34787 | |||
090c4a5425 | |||
87a1f2aa20 | |||
70135eaa91 | |||
f47b97e2a0 | |||
67f20ecb66 | |||
3bceb66600 | |||
fe34b8274d | |||
a235aa7ec3 | |||
f69f883439 | |||
7b4ed7b135 | |||
b801c9a49e | |||
583e1767a1 | |||
241ba47c80 | |||
c2d673ca1b | |||
05c46df0a9 | |||
53284d456f | |||
6cfcf1580c | |||
4d3a238583 | |||
47d6a3beff | |||
111791438a | |||
ce2c6f3b13 | |||
e655f22fac | |||
66b8d9362d | |||
37d277e149 | |||
f686691232 | |||
802f2c410d | |||
be559d3d9d | |||
d6cfef3a0b | |||
4fdb3df06e | |||
7eb0651b89 | |||
6d3984f6e1 | |||
58b134ae30 | |||
50384044c8 | |||
2cda4228ce | |||
67585b9f89 | |||
4e42856027 | |||
58051f297f | |||
0a9a5b20fa | |||
a0874356fd | |||
5d8f5e3a51 |
18
.cz.yaml
18
.cz.yaml
@ -1,7 +1,21 @@
|
||||
---
|
||||
commitizen:
|
||||
name: cz_conventional_commits
|
||||
customize:
|
||||
change_type_map:
|
||||
feature: Features
|
||||
fix: Fixes
|
||||
refactor: Refactoring
|
||||
test: Tests
|
||||
change_type_order:
|
||||
- BREAKING CHANGE
|
||||
- feat
|
||||
- fix
|
||||
- test
|
||||
- refactor
|
||||
commit_parser: ^(?P<change_type>feat|fix|test|refactor|perf|BREAKING CHANGE)(?:\((?P<scope>[^()\r\n]*)\)|\()?(?P<breaking>!)?:\s(?P<message>.*)?
|
||||
name: cz_customize
|
||||
prerelease_offset: 1
|
||||
tag_format: $version
|
||||
update_changelog_on_bump: false
|
||||
version: 0.5.0
|
||||
version: 1.0.0-b14
|
||||
version_scheme: semver
|
||||
|
31
.github/workflows/bump.yaml
vendored
Normal file
31
.github/workflows/bump.yaml
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
---
|
||||
|
||||
name: 'Bump'
|
||||
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
CZ_PRE_RELEASE:
|
||||
default: none
|
||||
required: false
|
||||
description: Create Pre-Release {alpha,beta,rc,none}
|
||||
CZ_INCREMENT:
|
||||
default: none
|
||||
required: false
|
||||
description: Type of bump to conduct {MAJOR,MINOR,PATCH,none}
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
bump:
|
||||
name: 'Bump'
|
||||
uses: nofusscomputing/action_bump/.github/workflows/bump.yaml@development
|
||||
with:
|
||||
CZ_PRE_RELEASE: ${{ inputs.CZ_PRE_RELEASE }}
|
||||
CZ_INCREMENT: ${{ inputs.CZ_INCREMENT }}
|
||||
secrets:
|
||||
WORKFLOW_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
109
.github/workflows/ci.yaml
vendored
Normal file
109
.github/workflows/ci.yaml
vendored
Normal file
@ -0,0 +1,109 @@
|
||||
---
|
||||
|
||||
name: 'CI'
|
||||
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
env:
|
||||
GIT_SYNC_URL: "https://${{ secrets.GITLAB_USERNAME_ROBOT }}:${{ secrets.GITLAB_TOKEN_ROBOT }}@gitlab.com/nofusscomputing/projects/centurion_erp.git"
|
||||
|
||||
jobs:
|
||||
|
||||
|
||||
docker:
|
||||
name: 'Docker'
|
||||
uses: nofusscomputing/action_docker/.github/workflows/docker.yaml@development
|
||||
with:
|
||||
DOCKER_BUILD_IMAGE_NAME: "nofusscomputing/centurion-erp"
|
||||
DOCKER_PUBLISH_REGISTRY: "docker.io"
|
||||
DOCKER_PUBLISH_IMAGE_NAME: "nofusscomputing/centurion-erp"
|
||||
secrets:
|
||||
DOCKER_PUBLISH_USERNAME: ${{ secrets.NFC_DOCKERHUB_USERNAME }}
|
||||
DOCKER_PUBLISH_PASSWORD: ${{ secrets.NFC_DOCKERHUB_TOKEN }}
|
||||
|
||||
|
||||
python:
|
||||
name: 'Python'
|
||||
uses: nofusscomputing/action_python/.github/workflows/python.yaml@development
|
||||
secrets:
|
||||
WORKFLOW_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
||||
|
||||
|
||||
gitlab-mirror:
|
||||
if: ${{ github.repository == 'nofusscomputing/centurion_erp' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
|
||||
- name: Checks
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "0${{ env.GIT_SYNC_URL }}" == "0" ]; then
|
||||
|
||||
echo "[ERROR] you must define variable GIT_SYNC_URL for mirroring this repository.";
|
||||
|
||||
exit 1;
|
||||
|
||||
fi
|
||||
|
||||
|
||||
- name: clone
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
git clone --mirror https://github.com/${{ github.repository }} repo;
|
||||
|
||||
ls -la repo/
|
||||
|
||||
|
||||
- name: add remote
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
cd repo;
|
||||
|
||||
echo "**************************************** - git remote -v";
|
||||
|
||||
git remote -v;
|
||||
|
||||
echo "****************************************";
|
||||
|
||||
git remote add destination $GIT_SYNC_URL;
|
||||
|
||||
|
||||
- name: push branches
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
cd repo;
|
||||
|
||||
echo "**************************************** - git branch";
|
||||
|
||||
git branch;
|
||||
|
||||
echo "****************************************";
|
||||
|
||||
# git push destination --all --force;
|
||||
|
||||
git push destination --mirror || true;
|
||||
|
||||
|
||||
# - name: push tags
|
||||
# shell: bash
|
||||
# run: |
|
||||
|
||||
# cd repo;
|
||||
|
||||
# echo "**************************************** - git tag";
|
||||
|
||||
# git tag;
|
||||
|
||||
# echo "****************************************";
|
||||
|
||||
# git push destination --tags --force;
|
14
.github/workflows/pull-requests.yaml
vendored
Normal file
14
.github/workflows/pull-requests.yaml
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
---
|
||||
|
||||
name: Pull Requests
|
||||
|
||||
|
||||
on:
|
||||
pull_request: {}
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
pull-request:
|
||||
name: pull-request
|
||||
uses: nofusscomputing/action_pull_requests/.github/workflows/pull-requests.yaml@development
|
37
.github/workflows/triage.yaml
vendored
Normal file
37
.github/workflows/triage.yaml
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
|
||||
---
|
||||
|
||||
name: Triage
|
||||
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- transferred
|
||||
- milestoned
|
||||
- demilestoned
|
||||
- closed
|
||||
- assigned
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- assigned
|
||||
- reopened
|
||||
- closed
|
||||
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
|
||||
project:
|
||||
name: Project
|
||||
uses: nofusscomputing/action_project/.github/workflows/project.yaml@development
|
||||
with:
|
||||
PROJECT_URL: https://github.com/orgs/nofusscomputing/projects/3
|
||||
secrets:
|
||||
WORKFLOW_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,10 +1,11 @@
|
||||
venv/**
|
||||
*/static/**
|
||||
__pycache__
|
||||
**db.sqlite3
|
||||
**.sqlite3
|
||||
**.sqlite
|
||||
**.coverage
|
||||
artifacts/
|
||||
**.tmp.*
|
||||
volumes/
|
||||
build/
|
||||
pages/
|
||||
pages/
|
||||
|
382
.gitlab-ci.yml
382
.gitlab-ci.yml
@ -2,143 +2,293 @@
|
||||
|
||||
variables:
|
||||
MY_PROJECT_ID: "57560288"
|
||||
GIT_SYNC_URL: "https://$GITHUB_USERNAME_ROBOT:$GITHUB_TOKEN_ROBOT@github.com/NoFussComputing/django_template.git"
|
||||
# GIT_SYNC_URL: "https://$GITHUB_USERNAME_ROBOT:$GITHUB_TOKEN_ROBOT@github.com/NoFussComputing/centurion_erp.git"
|
||||
|
||||
# Docker Build / Publish
|
||||
DOCKER_IMAGE_BUILD_TARGET_PLATFORMS: "linux/amd64,linux/arm64"
|
||||
DOCKER_IMAGE_BUILD_NAME: django-template
|
||||
DOCKER_IMAGE_BUILD_REGISTRY: $CI_REGISTRY_IMAGE
|
||||
DOCKER_IMAGE_BUILD_TAG: $CI_COMMIT_SHA
|
||||
# # Docker Build / Publish
|
||||
# DOCKER_IMAGE_BUILD_TARGET_PLATFORMS: "linux/amd64,linux/arm64"
|
||||
# DOCKER_IMAGE_BUILD_NAME: centurion-erp
|
||||
# DOCKER_IMAGE_BUILD_REGISTRY: $CI_REGISTRY_IMAGE
|
||||
# DOCKER_IMAGE_BUILD_TAG: $CI_COMMIT_SHA
|
||||
|
||||
# Docker Publish
|
||||
DOCKER_IMAGE_PUBLISH_NAME: django-template
|
||||
DOCKER_IMAGE_PUBLISH_REGISTRY: docker.io/nofusscomputing
|
||||
DOCKER_IMAGE_PUBLISH_URL: https://hub.docker.com/r/nofusscomputing/$DOCKER_IMAGE_PUBLISH_NAME
|
||||
# # Docker Publish
|
||||
# DOCKER_IMAGE_PUBLISH_NAME: centurion-erp
|
||||
# DOCKER_IMAGE_PUBLISH_REGISTRY: docker.io/nofusscomputing
|
||||
# DOCKER_IMAGE_PUBLISH_URL: https://hub.docker.com/r/nofusscomputing/$DOCKER_IMAGE_PUBLISH_NAME
|
||||
|
||||
# # Extra release commands
|
||||
# MY_COMMAND: ./.gitlab/additional_actions_bump.sh
|
||||
|
||||
# Docs NFC
|
||||
PAGES_ENVIRONMENT_PATH: projects/django-template/
|
||||
PAGES_ENVIRONMENT_PATH: projects/centurion_erp/
|
||||
|
||||
# RELEASE_ADDITIONAL_ACTIONS_BUMP: ./.gitlab/additional_actions_bump.sh
|
||||
|
||||
|
||||
include:
|
||||
- local: .gitlab/pytest.gitlab-ci.yml
|
||||
# - local: .gitlab/pytest.gitlab-ci.yml
|
||||
# - local: .gitlab/unit-test.gitlab-ci.yml
|
||||
- project: nofusscomputing/projects/gitlab-ci
|
||||
ref: development
|
||||
file:
|
||||
- .gitlab-ci_common.yaml
|
||||
- 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
|
||||
# - template/automagic.gitlab-ci.yaml
|
||||
- automation/.gitlab-ci-ansible.yaml
|
||||
- template/mkdocs-documentation.gitlab-ci.yaml
|
||||
- lint/ansible.gitlab-ci.yaml
|
||||
|
||||
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:
|
||||
$CI_COMMIT_AUTHOR =='nfc_bot <helpdesk@nofusscomputing.com>'
|
||||
&&
|
||||
$CI_COMMIT_BRANCH == "development"
|
||||
when: never
|
||||
|
||||
- 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: $CI_COMMIT_TAG
|
||||
exists:
|
||||
- '{dockerfile,dockerfile.j2}'
|
||||
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
|
||||
# Update Git Submodules:
|
||||
# extends: .ansible_playbook_git_submodule
|
||||
|
||||
# - if: # condition_master_branch_push
|
||||
# $CI_COMMIT_BRANCH == "master" &&
|
||||
# $CI_PIPELINE_SOURCE == "push"
|
||||
# exists:
|
||||
# - '{dockerfile,dockerfile.j2}'
|
||||
# when: always
|
||||
|
||||
- if: $CI_COMMIT_TAG
|
||||
exists:
|
||||
- '{dockerfile,dockerfile.j2}'
|
||||
when: always
|
||||
# Docker Container:
|
||||
# extends: .build_docker_container
|
||||
# resource_group: build
|
||||
# needs: []
|
||||
# script:
|
||||
# - update-binfmts --display
|
||||
# - |
|
||||
|
||||
- if: # condition_dev_branch_push
|
||||
$CI_COMMIT_BRANCH == "development" &&
|
||||
$CI_PIPELINE_SOURCE == "push"
|
||||
exists:
|
||||
- '{dockerfile,dockerfile.j2}'
|
||||
allow_failure: true
|
||||
when: on_success
|
||||
# echo "[DEBUG] building multiarch/specified arch image";
|
||||
|
||||
- when: never
|
||||
# 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:
|
||||
# $CI_COMMIT_AUTHOR =='nfc_bot <helpdesk@nofusscomputing.com>'
|
||||
# &&
|
||||
# $CI_COMMIT_BRANCH == "development"
|
||||
# when: never
|
||||
|
||||
# - 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: $CI_COMMIT_TAG
|
||||
# exists:
|
||||
# - '{dockerfile,dockerfile.j2}'
|
||||
# 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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# .gitlab_release:
|
||||
# stage: release
|
||||
# image: registry.gitlab.com/gitlab-org/release-cli:latest
|
||||
# before_script:
|
||||
# - if [ "0$JOB_ROOT_DIR" == "0" ]; then ROOT_DIR=gitlab-ci; else ROOT_DIR=$JOB_ROOT_DIR ; fi
|
||||
# - echo "[DEBUG] ROOT_DIR[$ROOT_DIR]"
|
||||
# - mkdir -p "$CI_PROJECT_DIR/artifacts/$CI_JOB_STAGE/$CI_JOB_NAME"
|
||||
# - mkdir -p "$CI_PROJECT_DIR/artifacts/$CI_JOB_STAGE/tests"
|
||||
# - apk update
|
||||
# - apk add git curl
|
||||
# - apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python
|
||||
# - python -m ensurepip && ln -sf pip3 /usr/bin/pip
|
||||
# - pip install --upgrade pip
|
||||
# - pip install -r $ROOT_DIR/gitlab_release/requirements.txt
|
||||
# # - pip install $ROOT_DIR/gitlab_release/python-module/cz_nfc/.
|
||||
# - pip install commitizen --force
|
||||
# - 'CLONE_URL="https://gitlab-ci-token:$GIT_COMMIT_TOKEN@gitlab.com/$CI_PROJECT_PATH.git"'
|
||||
# - echo "[DEBUG] CLONE_URL[$CLONE_URL]"
|
||||
# - git clone -b development $CLONE_URL repo
|
||||
# - cd repo
|
||||
# - git branch
|
||||
# - git config --global user.email "helpdesk@nofusscomputing.com"
|
||||
# - git config --global user.name "nfc_bot"
|
||||
# - git push --set-upstream origin development
|
||||
# - RELEASE_VERSION_CURRENT=$(cz version --project)
|
||||
# script:
|
||||
# - if [ "$CI_COMMIT_BRANCH" == "development" ] ; then RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout --prerelease beta); else RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout); fi
|
||||
# - RELEASE_VERSION_NEW=$(cz version --project)
|
||||
# - RELEASE_TAG=$RELEASE_VERSION_NEW
|
||||
# - echo "[DEBUG] RELEASE_VERSION_CURRENT[$RELEASE_VERSION_CURRENT]"
|
||||
# - echo "[DEBUG] RELEASE_CHANGELOG[$RELEASE_CHANGELOG]"
|
||||
# - echo "[DEBUG] RELEASE_VERSION_NEW[$RELEASE_VERSION_NEW]"
|
||||
# - echo "[DEBUG] RELEASE_TAG[$RELEASE_TAG]"
|
||||
# - RELEASE_TAG_SHA1=$(git log -n1 --format=format:"%H")
|
||||
# - echo "[DEBUG] RELEASE_TAG_SHA1[$RELEASE_TAG_SHA1]"
|
||||
|
||||
# - |
|
||||
# if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then
|
||||
|
||||
# echo "[DEBUG] not running extra actions, no new version";
|
||||
|
||||
# else
|
||||
|
||||
# echo "[DEBUG] Creating new Version Label";
|
||||
|
||||
# echo "----------------------------";
|
||||
|
||||
# echo ${MY_COMMAND};
|
||||
|
||||
# echo "----------------------------";
|
||||
|
||||
# cat ${MY_COMMAND};
|
||||
|
||||
# echo "----------------------------";
|
||||
|
||||
# ${MY_COMMAND};
|
||||
|
||||
# echo "----------------------------";
|
||||
# fi
|
||||
|
||||
# - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No tag to delete, version was not bumped"; else git tag -d $RELEASE_TAG; fi
|
||||
|
||||
# - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No push will be conducted, version was not bumped"; else git push; fi
|
||||
# - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No release will be created, version was not bumped"; else release-cli create --name "Release $RELEASE_TAG" --tag-name "$RELEASE_TAG" --tag-message "$RELEASE_CHANGELOG" --ref "$RELEASE_TAG_SHA1" --description "$RELEASE_CHANGELOG"; fi
|
||||
# - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git checkout master; fi
|
||||
# - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git push --set-upstream origin master; fi
|
||||
# - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git merge --no-ff development; fi
|
||||
# - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git push origin master; fi
|
||||
# after_script:
|
||||
# - rm -Rf repo
|
||||
# rules:
|
||||
# - if: '$JOB_STOP_GITLAB_RELEASE'
|
||||
# when: never
|
||||
|
||||
# - if: "$CI_COMMIT_AUTHOR =='nfc_bot <helpdesk@nofusscomputing.com>'"
|
||||
# when: never
|
||||
|
||||
# - if: # condition_master_branch_push
|
||||
# $CI_COMMIT_BRANCH == "master" &&
|
||||
# $CI_PIPELINE_SOURCE == "push"
|
||||
# allow_failure: false
|
||||
# when: on_success
|
||||
|
||||
# - if: # condition_dev_branch_push
|
||||
# $CI_COMMIT_BRANCH == "development" &&
|
||||
# $CI_PIPELINE_SOURCE == "push"
|
||||
# when: manual
|
||||
# allow_failure: true
|
||||
|
||||
# # for testing
|
||||
# # - if: '$CI_COMMIT_BRANCH != "master"'
|
||||
# # when: always
|
||||
# # allow_failure: true
|
||||
# - when: never
|
||||
|
||||
# #
|
||||
# # Release
|
||||
# #
|
||||
# Gitlab Release:
|
||||
# extends:
|
||||
# - .gitlab_release
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 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:
|
||||
# $CI_COMMIT_AUTHOR =='nfc_bot <helpdesk@nofusscomputing.com>'
|
||||
# &&
|
||||
# $CI_COMMIT_BRANCH == "development"
|
||||
# when: never
|
||||
|
||||
# - if: $CI_COMMIT_TAG
|
||||
# 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
|
||||
|
||||
|
||||
# Github (Push --mirror):
|
||||
# extends:
|
||||
# - .git_push_mirror
|
||||
# needs: []
|
||||
# rules:
|
||||
# - if: '$JOB_STOP_GIT_PUSH_MIRROR'
|
||||
# when: never
|
||||
|
||||
# - if: $GIT_SYNC_URL == null
|
||||
# when: never
|
||||
|
||||
# - if: # condition_master_or_dev_push
|
||||
# $CI_COMMIT_BRANCH
|
||||
# &&
|
||||
# $CI_PIPELINE_SOURCE == "push"
|
||||
# when: always
|
||||
|
||||
# - when: never
|
||||
|
||||
|
||||
Website.Submodule.Deploy:
|
||||
|
7
.gitlab/additional_actions_bump.sh
Executable file
7
.gitlab/additional_actions_bump.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Create Version label wtihn repo
|
||||
curl \
|
||||
--data "name=v${RELEASE_TAG}&color=#eee600&description=Version%20that%20is%20affected" \
|
||||
--header "PRIVATE-TOKEN: $GIT_COMMIT_TOKEN" \
|
||||
"https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/labels"
|
@ -20,16 +20,18 @@
|
||||
|
||||
<!-- dont remove tasks below strike through including the checkbox by enclosing in double tidle '~~' -->
|
||||
|
||||
- [ ] ~"breaking-change" Any Breaking change(s)
|
||||
- [ ] Contains ~"breaking-change" Any Breaking change(s)?
|
||||
|
||||
_Breaking Change must also be notated in the commit that introduces it and in [Conventional Commit Format](https://www.conventionalcommits.org/en/v1.0.0/)._
|
||||
|
||||
- [ ] Release notes updated
|
||||
|
||||
- [ ] ~Documentation Documentation written
|
||||
|
||||
_All features to be documented within the correct section(s). Administration, Development and/or User_
|
||||
|
||||
- [ ] Milestone assigned
|
||||
|
||||
- [ ] [Unit Test(s) Written](https://nofusscomputing.com/projects/django-template/development/testing/)
|
||||
- [ ] [Unit Test(s) Written](https://nofusscomputing.com/projects/centurion_erp/development/testing/)
|
||||
|
||||
_ensure test coverage delta is not less than zero_
|
||||
|
@ -1,29 +1,21 @@
|
||||
|
||||
|
||||
Unit:
|
||||
.pytest:
|
||||
stage: test
|
||||
image: python:3.11-alpine3.19
|
||||
needs: []
|
||||
script:
|
||||
before_script:
|
||||
- pip install -r requirements.txt
|
||||
- pip install -r requirements_test.txt
|
||||
- cd app
|
||||
- pytest --cov --cov-report term --cov-report xml:../artifacts/coverage.xml --cov-report html:../artifacts/coverage/ --junit-xml=../artifacts/unit.JUnit.xml
|
||||
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
|
||||
artifacts:
|
||||
expire_in: "30 days"
|
||||
when: always
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: artifacts/coverage.xml
|
||||
junit:
|
||||
- artifacts/*.JUnit.xml
|
||||
paths:
|
||||
- artifacts/
|
||||
environment:
|
||||
name: PyTest Coverage Report
|
||||
url: https://nofusscomputing.gitlab.io/-/projects/django_template/-/jobs/${CI_JOB_ID}/artifacts/artifacts/coverage/index.html
|
||||
rules:
|
||||
|
||||
- if: # Occur on merge
|
||||
@ -38,3 +30,52 @@ Unit:
|
||||
|
||||
- when: never
|
||||
|
||||
|
||||
Unit:
|
||||
extends: .pytest
|
||||
script:
|
||||
- pytest --cov --cov-report term --cov-report xml:../artifacts/coverage.xml --cov-report html:../artifacts/coverage/ --junit-xml=../artifacts/unit.JUnit.xml **/tests/unit
|
||||
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
|
||||
artifacts:
|
||||
expire_in: "30 days"
|
||||
when: always
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: artifacts/coverage.xml
|
||||
junit:
|
||||
- artifacts/*.JUnit.xml
|
||||
paths:
|
||||
- artifacts/
|
||||
environment:
|
||||
name: Unit Test Coverage Report
|
||||
url: https://nofusscomputing.gitlab.io/-/projects/centurion_erp/-/jobs/${CI_JOB_ID}/artifacts/artifacts/coverage/index.html
|
||||
|
||||
|
||||
UI:
|
||||
extends: .pytest
|
||||
script:
|
||||
- apk update
|
||||
- apk add chromium-chromedriver
|
||||
- pytest --junit-xml=../artifacts/ui.JUnit.xml **/tests/ui
|
||||
artifacts:
|
||||
expire_in: "30 days"
|
||||
when: always
|
||||
reports:
|
||||
junit:
|
||||
- artifacts/*.JUnit.xml
|
||||
paths:
|
||||
- artifacts/
|
||||
rules:
|
||||
- if: # Occur on merge
|
||||
$CI_COMMIT_BRANCH
|
||||
&&
|
||||
(
|
||||
$CI_PIPELINE_SOURCE == "push"
|
||||
||
|
||||
$CI_PIPELINE_SOURCE == "web"
|
||||
)
|
||||
allow_failure: true
|
||||
when: always
|
||||
|
||||
- when: never
|
||||
|
17
.vscode/launch.json
vendored
17
.vscode/launch.json
vendored
@ -15,6 +15,23 @@
|
||||
"django": true,
|
||||
"autoStartBrowser": false,
|
||||
"program": "${workspaceFolder}/app/manage.py"
|
||||
},
|
||||
{
|
||||
"name": "Debug: Celery",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"module": "celery",
|
||||
"console": "integratedTerminal",
|
||||
"args": [
|
||||
"-A",
|
||||
"app",
|
||||
"worker",
|
||||
"-l",
|
||||
"INFO",
|
||||
"-n",
|
||||
"debug-itsm@%h"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/app"
|
||||
}
|
||||
]
|
||||
}
|
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -13,4 +13,8 @@
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"testing.coverageToolbarEnabled": true,
|
||||
"cSpell.words": [
|
||||
"ITSM"
|
||||
],
|
||||
"cSpell.language": "en-AU",
|
||||
}
|
998
CHANGELOG.md
998
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -15,7 +15,7 @@ pip install -r requirements.txt
|
||||
|
||||
```
|
||||
|
||||
To setup the django test server run the following
|
||||
To setup the centurion erp test server run the following
|
||||
|
||||
``` bash
|
||||
|
||||
@ -30,6 +30,9 @@ python3 manage.py createsuperuser
|
||||
# If model changes
|
||||
python3 manage.py makemigrations --noinput
|
||||
|
||||
# To update code highlight run
|
||||
pygmentize -S default -f html -a .codehilite > project-static/code.css
|
||||
|
||||
```
|
||||
|
||||
Updates to python modules will need to be captured with SCM. This can be done by running `pip freeze > requirements.txt` from the running virtual environment.
|
||||
@ -50,9 +53,9 @@ See [Documentation](https://nofusscomputing.com/projects/django-template/develop
|
||||
|
||||
cd app
|
||||
|
||||
docker build . --tag django-app:dev
|
||||
docker build . --tag centurion-erp:dev
|
||||
|
||||
docker run -d --rm -v ${PWD}/db.sqlite3:/app/db.sqlite3 -p 8002:8000 --name app django-app:dev
|
||||
docker run -d --rm -v ${PWD}/db.sqlite3:/app/db.sqlite3 -p 8002:8000 --name app centurion-erp:dev
|
||||
|
||||
```
|
||||
|
||||
|
75
README.md
75
README.md
@ -1,21 +1,74 @@
|
||||
<span style="text-align: center;">
|
||||
|
||||

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

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

|
||||
|
||||

|
||||
|
||||

|
||||
[](https://hub.docker.com/r/nofusscomputing/centurion-erp) [](https://artifacthub.io/packages/container/centurion-erp/centurion-erp)
|
||||
|
||||
|
||||
artifacts
|
||||
|
||||
----
|
||||
|
||||
<br>
|
||||
|
||||
  
|
||||
|
||||
|
||||
dont work to file
|
||||
https://gitlab.com/nofusscomputing/projects/django_template/-/jobs/artifacts/master/browse/artifacts/coverage/index.html?job=Unit
|
||||
|
||||
works to dir
|
||||
https://gitlab.com/nofusscomputing/projects/django_template/-/jobs/artifacts/master/browse/artifacts/coverage/?job=Unit
|
||||
 
|
||||
|
||||
<br>
|
||||
|
||||
 
|
||||
|
||||
|
||||
This project is hosted on [Github](https://github.com/NofussComputing/centurion_erp) and has a read-only copy hosted on [gitlab](https://gitlab.com/nofusscomputing/projects/centurion_erp).
|
||||
|
||||
----
|
||||
|
||||
**Stable Branch**
|
||||
|
||||
  
|
||||

|
||||
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Development Branch**
|
||||
|
||||
|
||||
|
||||
  
|
||||

|
||||
|
||||
|
||||
----
|
||||
<br>
|
||||
|
||||
</div>
|
||||
|
||||
links:
|
||||
|
||||
- [Issues](https://github.com/nofusscomputing/centurion_erp/issues)
|
||||
|
||||
- [Merge Requests (Pull Requests)](https://github.com/nofusscomputing/centurion_erp/pulls)
|
||||
|
||||
|
||||
An ERP with a large emphasis on the IT Service Management (ITSM) and Automation.
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
All contributions for this project must conducted from [GitHub](https://github.com/nofusscomputing/centurion_erp).
|
||||
|
||||
For further details on contributing please refer to the [contribution guide](CONTRIBUTING.md).
|
||||
|
||||
|
||||
## Other
|
||||
|
||||
This repo is release under this [license](LICENSE)
|
||||
|
7
Release-Notes.md
Normal file
7
Release-Notes.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Version 1.0.0
|
||||
|
||||
Initial Release of Centurion ERP.
|
||||
|
||||
## Breaking changes
|
||||
|
||||
- Nil
|
17
app/.coveragerc
Normal file
17
app/.coveragerc
Normal file
@ -0,0 +1,17 @@
|
||||
[run]
|
||||
source = .
|
||||
omit =
|
||||
*migrations/*
|
||||
*tests/*/*
|
||||
|
||||
[report]
|
||||
omit =
|
||||
*/tests/*/*
|
||||
*/migrations/*
|
||||
*apps.py
|
||||
*manage.py
|
||||
*__init__.py
|
||||
*asgi*
|
||||
*wsgi*
|
||||
*admin.py
|
||||
*urls.py
|
@ -5,8 +5,9 @@ from app import settings
|
||||
|
||||
from access.models import Organization
|
||||
|
||||
from core.forms.common import CommonModelForm
|
||||
|
||||
class OrganizationForm(forms.ModelForm):
|
||||
class OrganizationForm(CommonModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Organization
|
||||
|
@ -6,8 +6,10 @@ from django.forms import inlineformset_factory
|
||||
from app import settings
|
||||
|
||||
from .team_users import TeamUsersForm, TeamUsers
|
||||
|
||||
from access.models import Team
|
||||
|
||||
from core.forms.common import CommonModelForm
|
||||
|
||||
TeamUserFormSet = inlineformset_factory(
|
||||
model=TeamUsers,
|
||||
@ -19,12 +21,25 @@ TeamUserFormSet = inlineformset_factory(
|
||||
]
|
||||
)
|
||||
|
||||
class TeamForm(forms.ModelForm):
|
||||
|
||||
|
||||
class TeamFormAdd(CommonModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = [
|
||||
'name',
|
||||
'team_name',
|
||||
'model_notes',
|
||||
]
|
||||
|
||||
|
||||
|
||||
class TeamForm(CommonModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = [
|
||||
'team_name',
|
||||
'permissions',
|
||||
'model_notes',
|
||||
]
|
||||
@ -53,14 +68,18 @@ class TeamForm(forms.ModelForm):
|
||||
|
||||
apps = [
|
||||
'access',
|
||||
'assistance',
|
||||
'config_management',
|
||||
'core',
|
||||
'django_celery_results',
|
||||
'itam',
|
||||
'settings',
|
||||
]
|
||||
|
||||
exclude_models = [
|
||||
'appsettings',
|
||||
'chordcounter',
|
||||
'groupresult',
|
||||
'organization'
|
||||
'settings',
|
||||
'usersettings',
|
||||
@ -68,8 +87,11 @@ class TeamForm(forms.ModelForm):
|
||||
|
||||
exclude_permissions = [
|
||||
'add_organization',
|
||||
'add_taskresult',
|
||||
'change_organization',
|
||||
'change_taskresult',
|
||||
'delete_organization',
|
||||
'delete_taskresult',
|
||||
]
|
||||
|
||||
self.fields['permissions'].queryset = Permission.objects.filter(
|
||||
|
@ -1,12 +1,12 @@
|
||||
from django import forms
|
||||
from django.db.models import Q
|
||||
|
||||
from app import settings
|
||||
|
||||
from access.models import TeamUsers
|
||||
|
||||
from core.forms.common import CommonModelForm
|
||||
|
||||
class TeamUsersForm(forms.ModelForm):
|
||||
class TeamUsersForm(CommonModelForm):
|
||||
|
||||
class Meta:
|
||||
model = TeamUsers
|
||||
|
@ -1,6 +1,7 @@
|
||||
# Generated by Django 5.0.4 on 2024-05-13 16:08
|
||||
# Generated by Django 5.0.7 on 2024-07-12 03:54
|
||||
|
||||
import access.fields
|
||||
import access.models
|
||||
import django.contrib.auth.models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
@ -23,9 +24,11 @@ class Migration(migrations.Migration):
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
|
||||
('slug', access.fields.AutoSlugField()),
|
||||
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
|
||||
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
|
||||
('manager', models.ForeignKey(help_text='Organization Manager', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Organizations',
|
||||
@ -37,10 +40,11 @@ class Migration(migrations.Migration):
|
||||
fields=[
|
||||
('group_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='auth.group')),
|
||||
('is_global', models.BooleanField(default=False)),
|
||||
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
|
||||
('team_name', models.CharField(default='', max_length=50, verbose_name='Name')),
|
||||
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
|
||||
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
|
||||
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='access.organization')),
|
||||
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Teams',
|
||||
|
@ -1,19 +0,0 @@
|
||||
# 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,20 +0,0 @@
|
||||
# Generated by Django 5.0.6 on 2024-06-05 09:16
|
||||
|
||||
import access.models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('access', '0002_alter_team_organization'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='team',
|
||||
name='organization',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]),
|
||||
),
|
||||
]
|
@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.0.6 on 2024-06-11 20:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('access', '0003_alter_team_organization'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='team',
|
||||
name='model_notes',
|
||||
field=models.TextField(blank=True, default=None, null=True),
|
||||
),
|
||||
]
|
@ -1,26 +0,0 @@
|
||||
# Generated by Django 5.0.6 on 2024-06-17 10:03
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('access', '0004_team_model_notes'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='manager',
|
||||
field=models.ForeignKey(help_text='Organization Manager', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='model_notes',
|
||||
field=models.TextField(blank=True, default=None, null=True),
|
||||
),
|
||||
]
|
@ -4,8 +4,6 @@ from django.contrib.auth.models import Group
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
|
||||
from .models import Organization, Team
|
||||
|
||||
|
||||
@ -57,9 +55,11 @@ class OrganizationMixin():
|
||||
|
||||
id = obj.get_organization().id
|
||||
|
||||
if obj.is_global:
|
||||
if hasattr(obj, 'is_global'):
|
||||
|
||||
id = 0
|
||||
if obj.is_global:
|
||||
|
||||
id = 0
|
||||
|
||||
|
||||
except AttributeError:
|
||||
@ -70,6 +70,20 @@ class OrganizationMixin():
|
||||
|
||||
id = int(self.request.POST.get("organization", ""))
|
||||
|
||||
for field in self.request.POST.dict(): # cater for fields prefixed '<prefix>-<field name>'
|
||||
|
||||
a_field = str(field).split('-')
|
||||
|
||||
if len(a_field) == 2:
|
||||
|
||||
if a_field[1] == 'organization':
|
||||
|
||||
id = int(self.request.POST.get(field))
|
||||
|
||||
except:
|
||||
|
||||
pass
|
||||
|
||||
|
||||
return id
|
||||
|
||||
@ -191,8 +205,26 @@ class OrganizationMixin():
|
||||
|
||||
is_organization_manager = False
|
||||
|
||||
queryset = None
|
||||
|
||||
if hasattr(self, 'get_queryset'):
|
||||
|
||||
queryset = self.get_queryset()
|
||||
|
||||
obj = None
|
||||
|
||||
if hasattr(self, 'get_object'):
|
||||
|
||||
|
||||
try:
|
||||
|
||||
obj = self.get_object()
|
||||
|
||||
except:
|
||||
|
||||
pass
|
||||
|
||||
|
||||
if hasattr(self, 'model'):
|
||||
|
||||
if self.model._meta.label_lower in organization_manager_models:
|
||||
@ -203,10 +235,32 @@ class OrganizationMixin():
|
||||
|
||||
is_organization_manager = True
|
||||
|
||||
if not self.has_organization_permission() and not request.user.is_superuser and not is_organization_manager:
|
||||
return False
|
||||
return True
|
||||
|
||||
return True
|
||||
|
||||
if request.user.is_superuser:
|
||||
|
||||
return True
|
||||
|
||||
perms = self.get_permission_required()
|
||||
|
||||
if self.has_organization_permission():
|
||||
|
||||
return True
|
||||
|
||||
if self.request.user.has_perms(perms) and len(self.kwargs) == 0 and str(self.request.method).lower() == 'get':
|
||||
|
||||
return True
|
||||
|
||||
for required_permission in self.permission_required:
|
||||
|
||||
if required_permission.replace(
|
||||
'view_', ''
|
||||
) == 'access.organization' and len(self.kwargs) == 0:
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@ -276,7 +330,33 @@ class OrganizationPermission(AccessMixin, OrganizationMixin):
|
||||
|
||||
if len(self.permission_required) > 0:
|
||||
|
||||
non_organization_models = [
|
||||
'TaskResult'
|
||||
]
|
||||
|
||||
if hasattr(self, 'model'):
|
||||
|
||||
|
||||
if hasattr(self.model, '__name__'):
|
||||
|
||||
if self.model.__name__ in non_organization_models:
|
||||
|
||||
if hasattr(self, 'get_object'):
|
||||
|
||||
self.get_object()
|
||||
|
||||
perms = self.get_permission_required()
|
||||
|
||||
|
||||
if not self.request.user.has_perms(perms):
|
||||
|
||||
return self.handle_no_permission()
|
||||
|
||||
return super().dispatch(self.request, *args, **kwargs)
|
||||
|
||||
|
||||
if not self.permission_check(request):
|
||||
raise PermissionDenied('You are not part of this organization')
|
||||
|
||||
raise PermissionDenied('You are not part of this organization')
|
||||
|
||||
return super().dispatch(self.request, *args, **kwargs)
|
||||
|
@ -15,9 +15,6 @@ class Organization(SaveHistory):
|
||||
verbose_name_plural = "Organizations"
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
if self.slug == '_':
|
||||
@ -49,6 +46,7 @@ class Organization(SaveHistory):
|
||||
blank = True,
|
||||
default = None,
|
||||
null= True,
|
||||
verbose_name = 'Notes',
|
||||
)
|
||||
|
||||
slug = AutoSlugField()
|
||||
@ -61,14 +59,107 @@ class Organization(SaveHistory):
|
||||
def get_organization(self):
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class TenancyObject(models.Model):
|
||||
|
||||
|
||||
class TenancyManager(models.Manager):
|
||||
"""Multi-Tennant Object Manager
|
||||
|
||||
This manager specifically caters for the multi-tenancy features of Centurion ERP.
|
||||
"""
|
||||
|
||||
|
||||
def get_queryset(self):
|
||||
""" Fetch the data
|
||||
|
||||
This function filters the data fetched from the database to that which is from the organizations
|
||||
the user is a part of.
|
||||
|
||||
!!! danger "Requirement"
|
||||
This method may be overridden however must still be called from the overriding function. i.e. `super().get_queryset()`
|
||||
|
||||
## Workflow
|
||||
|
||||
This functions workflow is as follows:
|
||||
|
||||
- Fetch the user from the request
|
||||
|
||||
- Check if the user is authenticated
|
||||
|
||||
- Iterate over the users teams
|
||||
|
||||
- Store unique organizations from users teams
|
||||
|
||||
- return results
|
||||
|
||||
Returns:
|
||||
(queryset): **super user**: return unfiltered data.
|
||||
(queryset): **not super user**: return data from the stored unique organizations.
|
||||
"""
|
||||
|
||||
request = get_request()
|
||||
|
||||
user_organizations: list(str()) = []
|
||||
|
||||
|
||||
if request:
|
||||
|
||||
user = request.user._wrapped if hasattr(request.user,'_wrapped') else request.user
|
||||
|
||||
|
||||
if user.is_authenticated:
|
||||
|
||||
for team_user in TeamUsers.objects.filter(user=user):
|
||||
|
||||
|
||||
if team_user.team.organization.name not in user_organizations:
|
||||
|
||||
|
||||
if not user_organizations:
|
||||
|
||||
self.user_organizations = []
|
||||
|
||||
user_organizations += [ team_user.team.organization.id ]
|
||||
|
||||
|
||||
if len(user_organizations) > 0 and not user.is_superuser:
|
||||
|
||||
return super().get_queryset().filter(
|
||||
models.Q(organization__in=user_organizations)
|
||||
|
|
||||
models.Q(is_global = True)
|
||||
)
|
||||
|
||||
return super().get_queryset()
|
||||
|
||||
|
||||
|
||||
class TenancyObject(SaveHistory):
|
||||
""" Tenancy Model Abstrct class.
|
||||
|
||||
This class is for inclusion wihtin **every** model within Centurion ERP.
|
||||
Provides the required fields, functions and methods for multi tennant objects.
|
||||
Unless otherwise stated, **no** object within this class may be overridden.
|
||||
|
||||
Raises:
|
||||
ValidationError: User failed to supply organization
|
||||
"""
|
||||
|
||||
objects = TenancyManager()
|
||||
""" Multi-Tenanant Objects """
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
def validatate_organization_exists(self):
|
||||
"""Ensure that the user did provide an organization
|
||||
|
||||
Raises:
|
||||
ValidationError: User failed to supply organization.
|
||||
"""
|
||||
|
||||
if not self:
|
||||
raise ValidationError('You must provide an organization')
|
||||
@ -91,21 +182,20 @@ class TenancyObject(models.Model):
|
||||
blank = True,
|
||||
default = None,
|
||||
null= True,
|
||||
verbose_name = 'Notes',
|
||||
)
|
||||
|
||||
def get_organization(self) -> Organization:
|
||||
return self.organization
|
||||
|
||||
|
||||
class Team(Group, TenancyObject, SaveHistory):
|
||||
|
||||
class Team(Group, TenancyObject):
|
||||
class Meta:
|
||||
# proxy = True
|
||||
verbose_name_plural = "Teams"
|
||||
ordering = ['team_name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||
|
||||
@ -148,6 +238,10 @@ class Team(Group, TenancyObject, SaveHistory):
|
||||
return [permission_list, self.permissions.all()]
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.team_name
|
||||
|
||||
|
||||
|
||||
class TeamUsers(SaveHistory):
|
||||
|
||||
@ -225,3 +319,6 @@ class TeamUsers(SaveHistory):
|
||||
|
||||
return self.team
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}Organizations{% endblock %}
|
||||
{% block content_header_icon %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -57,7 +57,7 @@ form div .helptext {
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.manager.label }}</label>
|
||||
<span>{{ form.manager.value }}</span>
|
||||
<span>{{ organization.manager }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
|
@ -8,7 +8,6 @@
|
||||
|
||||
{{ form.as_div }}
|
||||
|
||||
{% include 'icons/issue_link.html.j2' with issue=13 %}<br>
|
||||
<input style="display:unset;" type="submit" value="Submit">
|
||||
</form>
|
||||
|
||||
@ -18,7 +17,7 @@
|
||||
<input type="button" value="<< Back" onclick="window.location='{% url 'Access:_organization_view' pk=organization.id %}';">
|
||||
<input type="button" value="Delete Team"
|
||||
onclick="window.location='{% url 'Access:_team_delete' organization_id=organization.id pk=team.id %}';">
|
||||
<input type="button" value="New User"
|
||||
<input type="button" value="Assign User"
|
||||
onclick="window.location='{% url 'Access:_team_user_add' organization_id=organization.id pk=team.id %}';">
|
||||
{{ formset.management_form }}
|
||||
|
||||
|
88
app/access/tests/abstract/tenancy_object.py
Normal file
88
app/access/tests/abstract/tenancy_object.py
Normal file
@ -0,0 +1,88 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from access.models import TenancyManager
|
||||
|
||||
|
||||
|
||||
class TenancyObject:
|
||||
""" Tests for checking TenancyObject """
|
||||
|
||||
model = None
|
||||
""" Model to be tested """
|
||||
|
||||
|
||||
def test_has_attr_get_organization(self):
|
||||
""" TenancyObject attribute check
|
||||
|
||||
TenancyObject has function get_organization
|
||||
"""
|
||||
|
||||
assert hasattr(self.model, 'get_organization')
|
||||
|
||||
|
||||
def test_has_attr_is_global(self):
|
||||
""" TenancyObject attribute check
|
||||
|
||||
TenancyObject has field is_global
|
||||
"""
|
||||
|
||||
assert hasattr(self.model, 'is_global')
|
||||
|
||||
|
||||
|
||||
def test_has_attr_model_notes(self):
|
||||
""" TenancyObject attribute check
|
||||
|
||||
TenancyObject has field model_notes
|
||||
"""
|
||||
|
||||
assert hasattr(self.model, 'model_notes')
|
||||
|
||||
|
||||
|
||||
def test_has_attr_organization(self):
|
||||
""" TenancyObject attribute check
|
||||
|
||||
TenancyObject has field organization
|
||||
"""
|
||||
|
||||
assert hasattr(self.model, 'organization')
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_create_no_organization_fails(self):
|
||||
""" Devices must be assigned an organization
|
||||
|
||||
Must not be able to create an item without an organization
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_edit_no_organization_fails(self):
|
||||
""" Devices must be assigned an organization
|
||||
|
||||
Must not be able to edit an item without an organization
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def test_has_attr_organization(self):
|
||||
""" TenancyObject attribute check
|
||||
|
||||
TenancyObject has function objects
|
||||
"""
|
||||
|
||||
assert hasattr(self.model, 'objects')
|
||||
|
||||
|
||||
def test_attribute_is_type_objects(self):
|
||||
""" Attribute Check
|
||||
|
||||
attribute `objects` must be set to `access.models.TenancyManager()`
|
||||
"""
|
||||
|
||||
assert type(self.model.objects) is TenancyManager
|
@ -1,15 +0,0 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from django.test import TestCase, Client
|
||||
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from api.tests.abstract.api_permissions import APIPermissions
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
class TeamUsersPermissionsAPI(TestCase, APIPermissions):
|
||||
|
||||
model = TeamUsers
|
@ -1,24 +0,0 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
|
||||
class TenancyObject(TestCase):
|
||||
|
||||
# @classmethod
|
||||
# def setUpTestData(self):
|
||||
# """ Setup Test """
|
||||
|
||||
# pass
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_function_save_attributes(self):
|
||||
""" Ensure save Attributes function match django default
|
||||
|
||||
the save method is overridden. the function attributes must match default django method
|
||||
"""
|
||||
pass
|
371
app/access/tests/unit/organization/test_organizaiton_api.py
Normal file
371
app/access/tests/unit/organization/test_organizaiton_api.py
Normal file
@ -0,0 +1,371 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
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 Client, TestCase
|
||||
|
||||
from rest_framework.relations import Hyperlink
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
|
||||
|
||||
class OrganizationAPI(TestCase):
|
||||
|
||||
model = Organization
|
||||
|
||||
app_namespace = 'API'
|
||||
|
||||
url_name = '_api_organization'
|
||||
|
||||
@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 = organization
|
||||
|
||||
self.url_view_kwargs = {'pk': self.item.id}
|
||||
|
||||
self.url_kwargs = {'pk': self.item.id}
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
view_team = Team.objects.create(
|
||||
team_name = 'view_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
view_team.permissions.set([view_permissions])
|
||||
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = view_team,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.view_user)
|
||||
response = client.get(url)
|
||||
|
||||
self.api_data = response.data
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_name(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
name field must exist
|
||||
"""
|
||||
|
||||
assert 'name' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_name(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
name field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['name']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_teams(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams field must exist
|
||||
"""
|
||||
|
||||
assert 'teams' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_teams(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams field must be list
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams']) is list
|
||||
|
||||
|
||||
def test_api_field_exists_url(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
url field must exist
|
||||
"""
|
||||
|
||||
assert 'url' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_url(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
url field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['url']) is Hyperlink
|
||||
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_teams_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data['teams'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_teams_team_name(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.team_name field must exist
|
||||
"""
|
||||
|
||||
assert 'team_name' in self.api_data['teams'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_team_name(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.team_name field must be string
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['team_name']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions field must exist
|
||||
"""
|
||||
|
||||
assert 'permissions' in self.api_data['teams'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions field must be list
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions']) is list
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions_url(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions_url field must exist
|
||||
"""
|
||||
|
||||
assert 'permissions_url' in self.api_data['teams'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions_url(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions_url field must be url
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions_url']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_teams_url(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.url field must exist
|
||||
"""
|
||||
|
||||
assert 'url' in self.api_data['teams'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_url(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.url field must be url
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['url']) is str
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions.id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data['teams'][0]['permissions'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions.id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions'][0]['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions_name(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions.name field must exist
|
||||
"""
|
||||
|
||||
assert 'name' in self.api_data['teams'][0]['permissions'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions_name(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions.name field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions'][0]['name']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions_codename(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions.codename field must exist
|
||||
"""
|
||||
|
||||
assert 'codename' in self.api_data['teams'][0]['permissions'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions_codename(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions.codename field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions'][0]['codename']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions_content_type(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions.content_type field must exist
|
||||
"""
|
||||
|
||||
assert 'content_type' in self.api_data['teams'][0]['permissions'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions_content_type(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions.content_type field must be dict
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions'][0]['content_type']) is dict
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions_content_type_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions.content_type.id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data['teams'][0]['permissions'][0]['content_type']
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions_content_type_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions.content_type.id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions'][0]['content_type']['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions_content_type_app_label(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions.content_type.app_label field must exist
|
||||
"""
|
||||
|
||||
assert 'app_label' in self.api_data['teams'][0]['permissions'][0]['content_type']
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions_content_type_app_label(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions.content_type.app_label field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions'][0]['content_type']['app_label']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions_content_type_model(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions.content_type.model field must exist
|
||||
"""
|
||||
|
||||
assert 'model' in self.api_data['teams'][0]['permissions'][0]['content_type']
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions_content_type_model(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions.content_type.model field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions'][0]['content_type']['model']) is str
|
@ -0,0 +1,239 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
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 Client, TestCase
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from api.tests.abstract.api_permissions import APIPermissionChange, APIPermissionView
|
||||
|
||||
|
||||
|
||||
class OrganizationPermissionsAPI(TestCase, APIPermissionChange, APIPermissionView):
|
||||
|
||||
model = Organization
|
||||
|
||||
model_name = 'organization'
|
||||
app_label = 'access'
|
||||
|
||||
app_namespace = 'API'
|
||||
|
||||
url_name = '_api_organization'
|
||||
|
||||
url_list = '_api_orgs'
|
||||
|
||||
change_data = {'name': 'device'}
|
||||
|
||||
# delete_data = {'device': 'device'}
|
||||
|
||||
@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 = organization
|
||||
|
||||
self.url_view_kwargs = {'pk': self.item.id}
|
||||
|
||||
self.url_kwargs = {'pk': self.item.id}
|
||||
|
||||
# self.add_data = {'name': 'device', 'organization': self.organization.id}
|
||||
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
view_team = Team.objects.create(
|
||||
team_name = 'view_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
view_team.permissions.set([view_permissions])
|
||||
|
||||
|
||||
|
||||
add_permissions = Permission.objects.get(
|
||||
codename = 'add_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
add_team = Team.objects.create(
|
||||
team_name = 'add_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
add_team.permissions.set([add_permissions])
|
||||
|
||||
|
||||
|
||||
change_permissions = Permission.objects.get(
|
||||
codename = 'change_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
change_team = Team.objects.create(
|
||||
team_name = 'change_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
change_team.permissions.set([change_permissions])
|
||||
|
||||
|
||||
|
||||
delete_permissions = Permission.objects.get(
|
||||
codename = 'delete_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
delete_team = Team.objects.create(
|
||||
team_name = 'delete_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
delete_team.permissions.set([delete_permissions])
|
||||
|
||||
|
||||
self.super_user = User.objects.create_user(username="super_user", password="password", is_superuser=True)
|
||||
|
||||
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_add_is_prohibited_anon_user(self):
|
||||
""" Ensure Organization cant be created
|
||||
|
||||
Attempt to create organization as anon user
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_list)
|
||||
|
||||
|
||||
# client.force_login(self.add_user)
|
||||
response = client.post(url, data={'name': 'should not create'}, content_type='application/json')
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_add_is_prohibited_diff_org_user(self):
|
||||
""" Ensure Organization cant be created
|
||||
|
||||
Attempt to create organization as user with different org permissions.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_list)
|
||||
|
||||
|
||||
client.force_login(self.different_organization_user)
|
||||
response = client.post(url, data={'name': 'should not create'}, content_type='application/json')
|
||||
|
||||
assert response.status_code == 405
|
||||
|
||||
|
||||
def test_add_is_prohibited_super_user(self):
|
||||
""" Ensure Organization cant be created
|
||||
|
||||
Attempt to create organization as user who is super user
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_list)
|
||||
|
||||
|
||||
client.force_login(self.super_user)
|
||||
response = client.post(url, data={'name': 'should not create'}, content_type='application/json')
|
||||
|
||||
assert response.status_code == 405
|
||||
|
||||
|
||||
def test_add_is_prohibited_user_same_org(self):
|
||||
""" Ensure Organization cant be created
|
||||
|
||||
Attempt to create organization as user with permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_list)
|
||||
|
||||
|
||||
client.force_login(self.add_user)
|
||||
response = client.post(url, data={'name': 'should not create'}, content_type='application/json')
|
||||
|
||||
assert response.status_code == 405
|
@ -0,0 +1,21 @@
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from app.tests.abstract.models import ModelDisplay, ModelIndex
|
||||
|
||||
|
||||
|
||||
class OrganizationViews(
|
||||
TestCase,
|
||||
ModelDisplay,
|
||||
ModelIndex
|
||||
):
|
||||
|
||||
display_module = 'access.views.organization'
|
||||
display_view = 'View'
|
||||
|
||||
index_module = display_module
|
||||
index_view = 'IndexView'
|
@ -5,9 +5,14 @@ from django.test import TestCase, Client
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from app.tests.abstract.models import TenancyModel
|
||||
|
||||
|
||||
class TeamModel(TestCase):
|
||||
|
||||
class TeamModel(
|
||||
TestCase,
|
||||
TenancyModel
|
||||
):
|
||||
|
||||
model = Team
|
||||
|
||||
@ -53,4 +58,13 @@ class TeamModel(TestCase):
|
||||
|
||||
the save method is overridden. the function attributes must match default django method
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="uses Django group manager")
|
||||
def test_attribute_is_type_objects(self):
|
||||
pass
|
||||
|
||||
@pytest.mark.skip(reason="uses Django group manager")
|
||||
def test_model_class_tenancy_manager_function_get_queryset_called(self):
|
||||
pass
|
313
app/access/tests/unit/team/test_team_api.py
Normal file
313
app/access/tests/unit/team/test_team_api.py
Normal file
@ -0,0 +1,313 @@
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
|
||||
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 Client, TestCase
|
||||
|
||||
from rest_framework.relations import Hyperlink
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
# from api.tests.abstract.api_permissions import APIPermissions
|
||||
|
||||
|
||||
|
||||
class TeamAPI(TestCase):
|
||||
|
||||
model = Team
|
||||
|
||||
app_namespace = 'API'
|
||||
|
||||
url_name = '_api_team'
|
||||
|
||||
# url_list = '_api_organization_teams'
|
||||
|
||||
# change_data = {'name': 'device'}
|
||||
|
||||
# delete_data = {'device': 'device'}
|
||||
|
||||
@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,
|
||||
team_name = 'teamone',
|
||||
model_notes = 'random note'
|
||||
)
|
||||
|
||||
|
||||
self.url_kwargs = {'organization_id': self.organization.id}
|
||||
|
||||
self.url_view_kwargs = {'organization_id': self.organization.id, 'group_ptr_id': self.item.id}
|
||||
|
||||
self.add_data = {'team_name': 'team_post'}
|
||||
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
# view_team = Team.objects.create(
|
||||
# team_name = 'view_team',
|
||||
# organization = organization,
|
||||
# )
|
||||
|
||||
self.item.permissions.set([view_permissions])
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = self.item,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.view_user)
|
||||
response = client.get(url)
|
||||
|
||||
self.api_data = response.data
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_team_name(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
team_name field must exist
|
||||
"""
|
||||
|
||||
assert 'team_name' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_name(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
team_name field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['team_name']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_model_notes(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
model_notes field must exist
|
||||
"""
|
||||
|
||||
assert 'model_notes' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_model_notes(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
model_notes field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['model_notes']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_url(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
url field must exist
|
||||
"""
|
||||
|
||||
assert 'url' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_url(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
url field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['url']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_permissions(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
permissions field must exist
|
||||
"""
|
||||
|
||||
assert 'permissions' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_permissions(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
url field must be list
|
||||
"""
|
||||
|
||||
assert type(self.api_data['permissions']) is list
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_permissions_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
permissions.id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data['permissions'][0]
|
||||
|
||||
|
||||
def test_api_field_type_permissions_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
permissions.id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['permissions'][0]['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_permissions_name(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
permissions.name field must exist
|
||||
"""
|
||||
|
||||
assert 'name' in self.api_data['permissions'][0]
|
||||
|
||||
|
||||
def test_api_field_type_permissions_name(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
permissions.name field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['permissions'][0]['name']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_permissions_codename(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
permissions.codename field must exist
|
||||
"""
|
||||
|
||||
assert 'codename' in self.api_data['permissions'][0]
|
||||
|
||||
|
||||
def test_api_field_type_permissions_codename(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
permissions.codename field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['permissions'][0]['codename']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_permissions_content_type(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
permissions.content_type field must exist
|
||||
"""
|
||||
|
||||
assert 'content_type' in self.api_data['permissions'][0]
|
||||
|
||||
|
||||
def test_api_field_type_permissions_content_type(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
permissions.content_type field must be dict
|
||||
"""
|
||||
|
||||
assert type(self.api_data['permissions'][0]['content_type']) is dict
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_permissions_content_type_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
permissions.content_type.id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data['permissions'][0]['content_type']
|
||||
|
||||
|
||||
def test_api_field_type_permissions_content_type_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
permissions.content_type.id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['permissions'][0]['content_type']['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_permissions_content_type_app_label(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
permissions.content_type.app_label field must exist
|
||||
"""
|
||||
|
||||
assert 'app_label' in self.api_data['permissions'][0]['content_type']
|
||||
|
||||
|
||||
def test_api_field_type_permissions_content_type_app_label(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
permissions.content_type.app_label field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['permissions'][0]['content_type']['app_label']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_permissions_content_type_model(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
permissions.content_type.model field must exist
|
||||
"""
|
||||
|
||||
assert 'model' in self.api_data['permissions'][0]['content_type']
|
||||
|
||||
|
||||
def test_api_field_type_permissions_content_type_model(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
permissions.content_type.model field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['permissions'][0]['content_type']['model']) is str
|
29
app/access/tests/unit/team/test_team_views.py
Normal file
29
app/access/tests/unit/team/test_team_views.py
Normal file
@ -0,0 +1,29 @@
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from app.tests.abstract.models import ModelAdd, ModelDelete, ModelDisplay
|
||||
|
||||
|
||||
|
||||
class TeamViews(
|
||||
TestCase,
|
||||
ModelAdd,
|
||||
ModelDelete,
|
||||
ModelDisplay,
|
||||
):
|
||||
|
||||
add_module = 'access.views.team'
|
||||
add_view = 'Add'
|
||||
|
||||
# change_module = add_module
|
||||
# change_view = 'Change'
|
||||
|
||||
delete_module = add_module
|
||||
delete_view = 'Delete'
|
||||
|
||||
display_module = add_module
|
||||
display_view = 'View'
|
||||
|
30
app/access/tests/unit/team_user/test_team_user_views.py
Normal file
30
app/access/tests/unit/team_user/test_team_user_views.py
Normal file
@ -0,0 +1,30 @@
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from app.tests.abstract.models import AddView, DeleteView
|
||||
|
||||
|
||||
|
||||
class TeamUserViews(
|
||||
TestCase,
|
||||
AddView,
|
||||
DeleteView
|
||||
):
|
||||
|
||||
add_module = 'access.views.user'
|
||||
add_view = 'Add'
|
||||
|
||||
# change_module = add_module
|
||||
# change_view = 'GroupView'
|
||||
|
||||
delete_module = add_module
|
||||
delete_view = 'Delete'
|
||||
|
||||
# display_module = add_module
|
||||
# display_view = 'GroupView'
|
||||
|
||||
# index_module = add_module
|
||||
# index_view = 'GroupIndexView'
|
93
app/access/tests/unit/tenancy_object/test_tenancy_object.py
Normal file
93
app/access/tests/unit/tenancy_object/test_tenancy_object.py
Normal file
@ -0,0 +1,93 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from access.models import TenancyObject, TenancyManager
|
||||
|
||||
from core.mixin.history_save import SaveHistory
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
|
||||
class TenancyManagerTests(TestCase):
|
||||
|
||||
item = TenancyManager
|
||||
|
||||
|
||||
def test_has_attribute_get_queryset(self):
|
||||
""" Field organization exists """
|
||||
|
||||
assert hasattr(self.item, 'get_queryset')
|
||||
|
||||
|
||||
def test_is_function_get_queryset(self):
|
||||
""" Attribute 'get_organization' is a function """
|
||||
|
||||
assert callable(self.item.get_queryset)
|
||||
|
||||
|
||||
|
||||
class TenancyObjectTests(TestCase):
|
||||
|
||||
item = TenancyObject
|
||||
|
||||
|
||||
def test_class_inherits_save_history(self):
|
||||
""" Confirm class inheritence
|
||||
|
||||
TenancyObject must inherit SaveHistory
|
||||
"""
|
||||
|
||||
assert issubclass(TenancyObject, SaveHistory)
|
||||
|
||||
|
||||
def test_has_attribute_organization(self):
|
||||
""" Field organization exists """
|
||||
|
||||
assert hasattr(self.item, 'organization')
|
||||
|
||||
|
||||
def test_has_attribute_is_global(self):
|
||||
""" Field organization exists """
|
||||
|
||||
assert hasattr(self.item, 'is_global')
|
||||
|
||||
|
||||
def test_has_attribute_model_notes(self):
|
||||
""" Field organization exists """
|
||||
|
||||
assert hasattr(self.item, 'model_notes')
|
||||
|
||||
|
||||
def test_has_attribute_get_organization(self):
|
||||
""" Function 'get_organization' Exists """
|
||||
|
||||
assert hasattr(self.item, 'get_organization')
|
||||
|
||||
|
||||
def test_is_function_get_organization(self):
|
||||
""" Attribute 'get_organization' is a function """
|
||||
|
||||
assert callable(self.item.get_organization)
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="figure out how to test abstract class")
|
||||
def test_has_attribute_objects(self):
|
||||
""" Attribute Check
|
||||
|
||||
attribute `objects` must be set to `access.models.TenancyManager()`
|
||||
"""
|
||||
|
||||
assert 'objects' in self.item
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="figure out how to test abstract class")
|
||||
def test_attribute_not_none_objects(self):
|
||||
""" Attribute Check
|
||||
|
||||
attribute `objects` must be set to `access.models.TenancyManager()`
|
||||
"""
|
||||
|
||||
assert self.item.objects is not None
|
@ -1,4 +1,5 @@
|
||||
from django.contrib.auth import decorators as auth_decorator
|
||||
from django.db.models import Q
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import generic
|
||||
|
||||
@ -7,9 +8,12 @@ from access.models import *
|
||||
|
||||
from access.forms.organization import OrganizationForm
|
||||
|
||||
from core.views.common import ChangeView, IndexView
|
||||
|
||||
|
||||
class IndexView(OrganizationPermission, generic.ListView):
|
||||
class IndexView(IndexView):
|
||||
|
||||
model = Organization
|
||||
permission_required = [
|
||||
'access.view_organization'
|
||||
]
|
||||
@ -17,6 +21,14 @@ class IndexView(OrganizationPermission, generic.ListView):
|
||||
context_object_name = "organization_list"
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['content_title'] = 'Organizations'
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
if self.request.user.is_superuser:
|
||||
@ -25,11 +37,15 @@ class IndexView(OrganizationPermission, generic.ListView):
|
||||
|
||||
else:
|
||||
|
||||
return Organization.objects.filter(pk__in=self.user_organizations())
|
||||
return Organization.objects.filter(
|
||||
Q(pk__in=self.user_organizations())
|
||||
|
|
||||
Q(manager=self.request.user.id)
|
||||
)
|
||||
|
||||
|
||||
|
||||
class View(OrganizationPermission, generic.UpdateView):
|
||||
class View(ChangeView):
|
||||
|
||||
context_object_name = "organization"
|
||||
|
||||
@ -70,6 +86,8 @@ class View(OrganizationPermission, generic.UpdateView):
|
||||
context['model_pk'] = self.kwargs['pk']
|
||||
context['model_name'] = self.model._meta.verbose_name.replace(' ', '')
|
||||
|
||||
context['content_title'] = 'Organization - ' + context[self.context_object_name].name
|
||||
|
||||
return context
|
||||
|
||||
|
||||
|
@ -2,16 +2,15 @@ from django.contrib.auth import decorators as auth_decorator
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.urls import reverse
|
||||
from django.views import generic
|
||||
|
||||
from access.forms.team import TeamForm
|
||||
# from access.forms.team_users import TeamUsersForm
|
||||
from access.forms.team import TeamForm, TeamFormAdd
|
||||
from access.models import Team, TeamUsers, Organization
|
||||
from access.mixin import *
|
||||
|
||||
from core.views.common import AddView, ChangeView, DeleteView
|
||||
|
||||
|
||||
class View(OrganizationPermission, generic.UpdateView):
|
||||
class View(ChangeView):
|
||||
|
||||
context_object_name = "team"
|
||||
|
||||
@ -79,15 +78,19 @@ class View(OrganizationPermission, generic.UpdateView):
|
||||
|
||||
|
||||
|
||||
class Add(OrganizationPermission, generic.CreateView):
|
||||
class Add(AddView):
|
||||
|
||||
form_class = TeamFormAdd
|
||||
|
||||
model = Team
|
||||
|
||||
parent_model = Organization
|
||||
|
||||
permission_required = [
|
||||
'access.add_team',
|
||||
]
|
||||
|
||||
template_name = 'form.html.j2'
|
||||
fields = [
|
||||
'team_name',
|
||||
]
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.organization = Organization.objects.get(pk=self.kwargs['pk'])
|
||||
@ -101,8 +104,6 @@ class Add(OrganizationPermission, generic.CreateView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['model_pk'] = self.kwargs['pk']
|
||||
context['model_name'] = self.model._meta.verbose_name.replace(' ', '')
|
||||
|
||||
context['content_title'] = 'Add Team'
|
||||
|
||||
@ -110,7 +111,7 @@ class Add(OrganizationPermission, generic.CreateView):
|
||||
|
||||
|
||||
|
||||
class Delete(OrganizationPermission, generic.DeleteView):
|
||||
class Delete(DeleteView):
|
||||
model = Team
|
||||
permission_required = [
|
||||
'access.delete_team'
|
||||
|
@ -1,15 +1,13 @@
|
||||
from django.contrib.auth import decorators as auth_decorator
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import generic
|
||||
|
||||
from access.forms.team_users import TeamUsersForm
|
||||
from access.mixin import OrganizationPermission
|
||||
from access.models import Team, TeamUsers
|
||||
|
||||
from core.views.common import AddView, DeleteView
|
||||
|
||||
|
||||
class Add(OrganizationPermission, generic.CreateView):
|
||||
class Add(AddView):
|
||||
|
||||
context_object_name = "teamuser"
|
||||
|
||||
@ -17,10 +15,9 @@ class Add(OrganizationPermission, generic.CreateView):
|
||||
|
||||
model = TeamUsers
|
||||
|
||||
parent_model = TeamUsers
|
||||
parent_model = Team
|
||||
|
||||
permission_required = [
|
||||
'access.view_team',
|
||||
'access.add_teamusers'
|
||||
]
|
||||
|
||||
@ -52,7 +49,7 @@ class Add(OrganizationPermission, generic.CreateView):
|
||||
return context
|
||||
|
||||
|
||||
class Delete(OrganizationPermission, generic.DeleteView):
|
||||
class Delete(DeleteView):
|
||||
model = TeamUsers
|
||||
permission_required = [
|
||||
'access.delete_teamusers'
|
||||
|
79
app/api/auth.py
Normal file
79
app/api/auth.py
Normal file
@ -0,0 +1,79 @@
|
||||
import datetime
|
||||
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.authentication import BaseAuthentication, get_authorization_header
|
||||
|
||||
from api.models.tokens import AuthToken
|
||||
|
||||
|
||||
|
||||
class TokenAuthentication(BaseAuthentication):
|
||||
""" API Token Authentication
|
||||
|
||||
Provides the ability to use the API by using a token to authenticate.
|
||||
"""
|
||||
|
||||
def authenticate_header(self, request):
|
||||
return 'Token'
|
||||
|
||||
|
||||
def authenticate(self, request):
|
||||
""" Authentication the API session using the supplied token
|
||||
|
||||
Args:
|
||||
request (object): API Request Object
|
||||
|
||||
Raises:
|
||||
exceptions.AuthenticationFailed: 'Token header invalid' - Authorization Header Value is not in format `Token <auth-token>`
|
||||
exceptions.AuthenticationFailed: 'Token header invalid. Possibly incorrectly formatted' - Authentication header value has >1 space
|
||||
exceptions.AuthenticationFailed: 'Invalid token header. Token string should not contain invalid characters.' - Authorization header contains non-unicode chars
|
||||
|
||||
Returns:
|
||||
None (None): User not authenticated
|
||||
tuple(user,token): User authenticated
|
||||
"""
|
||||
|
||||
auth = get_authorization_header(request).split()
|
||||
|
||||
if not auth:
|
||||
return None
|
||||
|
||||
if len(auth) == 1:
|
||||
|
||||
raise exceptions.AuthenticationFailed('Token header invalid')
|
||||
|
||||
elif len(auth) > 2:
|
||||
|
||||
raise exceptions.AuthenticationFailed('Token header invalid. Possibly incorrectly formatted')
|
||||
|
||||
|
||||
elif len(auth) == 2:
|
||||
|
||||
try:
|
||||
|
||||
decoded_token: str = auth[1].decode("utf-8")
|
||||
|
||||
for token in AuthToken.objects.filter():
|
||||
|
||||
provided_token: str = token.token_hash(decoded_token)
|
||||
|
||||
if token.token == provided_token:
|
||||
|
||||
if datetime.datetime.strptime(str(token.expires),'%Y-%m-%d %H:%M:%S%z') > datetime.datetime.now(datetime.timezone.utc):
|
||||
|
||||
user = token.user
|
||||
|
||||
return (user, provided_token)
|
||||
|
||||
else:
|
||||
|
||||
expired_token = AuthToken.objects.get(id=token.id)
|
||||
|
||||
expired_token.delete()
|
||||
|
||||
except UnicodeError:
|
||||
|
||||
raise exceptions.AuthenticationFailed('Invalid token header. Token string should not contain invalid characters.')
|
||||
|
||||
|
||||
return None
|
49
app/api/forms/user_token.py
Normal file
49
app/api/forms/user_token.py
Normal file
@ -0,0 +1,49 @@
|
||||
import datetime
|
||||
from django import forms
|
||||
|
||||
from api.models.tokens import AuthToken
|
||||
|
||||
from app import settings
|
||||
|
||||
from core.forms.common import CommonModelForm
|
||||
|
||||
|
||||
class AuthTokenForm(CommonModelForm):
|
||||
|
||||
prefix = 'user_token'
|
||||
|
||||
class Meta:
|
||||
|
||||
fields = [
|
||||
'note',
|
||||
'expires',
|
||||
]
|
||||
|
||||
model = AuthToken
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['expires'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local', 'format': "%Y-%m-%dT%H:%M"})
|
||||
self.fields['expires'].input_formats = settings.DATETIME_FORMAT
|
||||
self.fields['expires'].format="%Y-%m-%dT%H:%M"
|
||||
self.fields['expires'].initial= datetime.datetime.now() + datetime.timedelta(days=90)
|
||||
|
||||
if self.prefix + '-gen_token' not in self.data:
|
||||
|
||||
generated_token = self.instance.generate()
|
||||
|
||||
else:
|
||||
|
||||
generated_token = self.data[self.prefix + '-gen_token']
|
||||
|
||||
self.fields['gen_token'] = forms.CharField(
|
||||
label="Generated Token",
|
||||
initial=generated_token,
|
||||
empty_value= generated_token,
|
||||
required=False,
|
||||
help_text = 'Ensure you save this token somewhere as you will never be able to obtain it again',
|
||||
)
|
||||
|
||||
self.fields['gen_token'].widget.attrs['readonly'] = True
|
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.0.6 on 2024-05-24 23:50
|
||||
# Generated by Django 5.0.7 on 2024-07-12 03:54
|
||||
|
||||
import access.fields
|
||||
import django.db.models.deletion
|
||||
@ -9,24 +9,23 @@ from django.db import migrations, models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('access', '0002_alter_team_organization'),
|
||||
('settings', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserSettings',
|
||||
name='AuthToken',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
|
||||
('note', models.CharField(blank=True, default=None, max_length=50, null=True)),
|
||||
('token', models.CharField(db_index=True, max_length=64, unique=True, verbose_name='Auth Token')),
|
||||
('expires', models.DateTimeField(verbose_name='Expiry Date')),
|
||||
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
|
||||
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
|
||||
('default_organization', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='access.organization')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
0
app/api/migrations/__init__.py
Normal file
0
app/api/migrations/__init__.py
Normal file
0
app/api/models/__init__.py
Normal file
0
app/api/models/__init__.py
Normal file
109
app/api/models/tokens.py
Normal file
109
app/api/models/tokens.py
Normal file
@ -0,0 +1,109 @@
|
||||
import hashlib
|
||||
import random
|
||||
import string
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.db.models import Field
|
||||
from django.forms import ValidationError
|
||||
|
||||
from access.fields import *
|
||||
from access.models import TenancyObject
|
||||
|
||||
|
||||
|
||||
class AuthToken(models.Model):
|
||||
|
||||
|
||||
def validate_note_no_token(self, note, token):
|
||||
""" Ensure plaintext token cant be saved to notes field.
|
||||
|
||||
called from app.settings.views.user_settings.TokenAdd.form_valid()
|
||||
|
||||
Args:
|
||||
note (Field): _Note field_
|
||||
token (Field): _Token field_
|
||||
|
||||
Raises:
|
||||
ValidationError: _Validation failed_
|
||||
"""
|
||||
|
||||
validation: bool = True
|
||||
|
||||
|
||||
if str(note) == str(token):
|
||||
|
||||
validation = False
|
||||
|
||||
|
||||
if str(token)[:9] in str(note): # Allow user to use up to 8 chars so they can reference it.
|
||||
|
||||
validation = False
|
||||
|
||||
if not validation:
|
||||
|
||||
raise ValidationError('Token can not be placed in the notes field.')
|
||||
|
||||
|
||||
|
||||
id = models.AutoField(
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
blank=False
|
||||
)
|
||||
|
||||
note = models.CharField(
|
||||
blank = True,
|
||||
max_length = 50,
|
||||
default = None,
|
||||
null= True,
|
||||
)
|
||||
|
||||
token = models.CharField(
|
||||
verbose_name = 'Auth Token',
|
||||
db_index=True,
|
||||
max_length = 64,
|
||||
null = False,
|
||||
blank = False,
|
||||
unique = True,
|
||||
)
|
||||
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
expires = models.DateTimeField(
|
||||
verbose_name = 'Expiry Date',
|
||||
null = False,
|
||||
blank = False
|
||||
)
|
||||
|
||||
|
||||
created = AutoCreatedField()
|
||||
|
||||
modified = AutoLastModifiedField()
|
||||
|
||||
|
||||
def generate(self) -> str:
|
||||
|
||||
return str(hashlib.sha256(str(self.randomword()).encode('utf-8')).hexdigest())
|
||||
|
||||
|
||||
def token_hash(self, token:str) -> str:
|
||||
|
||||
salt = settings.SECRET_KEY
|
||||
|
||||
return str(hashlib.sha256(str(token + salt).encode('utf-8')).hexdigest())
|
||||
|
||||
|
||||
def randomword(self) -> str:
|
||||
|
||||
return ''.join(random.choice(string.ascii_letters) for i in range(120))
|
||||
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return self.token
|
0
app/api/serializers/__init__.py
Normal file
0
app/api/serializers/__init__.py
Normal file
@ -14,9 +14,9 @@ class TeamSerializerBase(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = (
|
||||
"id",
|
||||
"team_name",
|
||||
'organization',
|
||||
'team_name',
|
||||
'model_notes',
|
||||
'permissions',
|
||||
'url',
|
||||
)
|
||||
|
||||
@ -29,9 +29,18 @@ class TeamSerializerBase(serializers.ModelSerializer):
|
||||
|
||||
|
||||
|
||||
class TeamPermissionSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Permission
|
||||
depth = 1
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class TeamSerializer(TeamSerializerBase):
|
||||
|
||||
permissions = serializers.SerializerMethodField('get_url')
|
||||
permissions_url = serializers.SerializerMethodField('get_url')
|
||||
|
||||
def get_url(self, obj):
|
||||
|
||||
@ -63,16 +72,19 @@ class TeamSerializer(TeamSerializerBase):
|
||||
|
||||
class Meta:
|
||||
model = Team
|
||||
depth = 1
|
||||
depth = 2
|
||||
fields = (
|
||||
"id",
|
||||
"team_name",
|
||||
'organization',
|
||||
'model_notes',
|
||||
'permissions',
|
||||
'permissions_url',
|
||||
'url',
|
||||
)
|
||||
read_only_fields = [
|
||||
'permissions',
|
||||
'id',
|
||||
'organization',
|
||||
'permissions_url',
|
||||
'url'
|
||||
]
|
||||
|
||||
@ -111,7 +123,7 @@ class OrganizationSerializer(serializers.ModelSerializer):
|
||||
|
||||
return request.build_absolute_uri(reverse('API:_api_organization_teams', args=[obj.id]))
|
||||
|
||||
teams = TeamSerializerBase(source='team_set', many=True, read_only=False)
|
||||
teams = TeamSerializer(source='team_set', many=True, read_only=False)
|
||||
|
||||
view_name="API:_api_organization"
|
||||
|
||||
|
86
app/api/serializers/config.py
Normal file
86
app/api/serializers/config.py
Normal file
@ -0,0 +1,86 @@
|
||||
from rest_framework import serializers
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from config_management.models.groups import ConfigGroups
|
||||
|
||||
|
||||
|
||||
class ParentGroupSerializer(serializers.ModelSerializer):
|
||||
|
||||
url = serializers.SerializerMethodField('get_url')
|
||||
|
||||
|
||||
class Meta:
|
||||
model = ConfigGroups
|
||||
fields = [
|
||||
'id',
|
||||
'name',
|
||||
'url',
|
||||
]
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
def get_url(self, obj):
|
||||
|
||||
request = self.context.get('request')
|
||||
|
||||
return request.build_absolute_uri(reverse("API:_api_config_group", args=[obj.pk]))
|
||||
|
||||
|
||||
|
||||
class ConfigGroupsSerializerBase(serializers.ModelSerializer):
|
||||
|
||||
parent = ParentGroupSerializer(read_only=True)
|
||||
url = serializers.SerializerMethodField('get_url')
|
||||
|
||||
|
||||
class Meta:
|
||||
model = ConfigGroups
|
||||
fields = [
|
||||
'id',
|
||||
'parent',
|
||||
'name',
|
||||
'config',
|
||||
'url',
|
||||
]
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'config',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
def get_url(self, obj):
|
||||
|
||||
request = self.context.get('request')
|
||||
|
||||
return request.build_absolute_uri(reverse("API:_api_config_group", args=[obj.pk]))
|
||||
|
||||
|
||||
|
||||
class ConfigGroupsSerializer(ConfigGroupsSerializerBase):
|
||||
|
||||
|
||||
class Meta:
|
||||
model = ConfigGroups
|
||||
depth = 1
|
||||
fields = [
|
||||
'id',
|
||||
'parent',
|
||||
'name',
|
||||
'config',
|
||||
'url',
|
||||
]
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'parent',
|
||||
'name',
|
||||
'config',
|
||||
'url',
|
||||
]
|
||||
|
168
app/api/serializers/inventory.py
Normal file
168
app/api/serializers/inventory.py
Normal file
@ -0,0 +1,168 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.html import escape
|
||||
|
||||
class Inventory:
|
||||
""" Inventory Object
|
||||
|
||||
Pass in an Inventory dict that a device has provided and sanitize ready for use.
|
||||
|
||||
Raises:
|
||||
ValidationError: Malformed inventory data.
|
||||
"""
|
||||
|
||||
|
||||
class Details:
|
||||
|
||||
_name: str
|
||||
|
||||
_serial_number: str
|
||||
|
||||
_uuid: str
|
||||
|
||||
|
||||
def __init__(self, details: dict):
|
||||
|
||||
self._name = escape(details['name'])
|
||||
|
||||
self._serial_number = escape(details['serial_number'])
|
||||
|
||||
self._uuid = escape(details['uuid'])
|
||||
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
||||
return str(self._name)
|
||||
|
||||
|
||||
@property
|
||||
def serial_number(self) -> str:
|
||||
|
||||
return str(self._serial_number)
|
||||
|
||||
|
||||
@property
|
||||
def uuid(self) -> str:
|
||||
|
||||
return str(self._uuid)
|
||||
|
||||
|
||||
|
||||
class OperatingSystem:
|
||||
|
||||
_name: str
|
||||
|
||||
_version_major: str
|
||||
|
||||
_version: str
|
||||
|
||||
|
||||
def __init__(self, operating_system: dict):
|
||||
|
||||
self._name = escape(operating_system['name'])
|
||||
|
||||
self._version_major = escape(operating_system['version_major'])
|
||||
|
||||
self._version = escape(operating_system['version'])
|
||||
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
||||
return str(self._name)
|
||||
|
||||
|
||||
@property
|
||||
def version_major(self) -> str:
|
||||
|
||||
return str(self._version_major)
|
||||
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
|
||||
return str(self._version)
|
||||
|
||||
|
||||
|
||||
class Software:
|
||||
|
||||
_name: str
|
||||
|
||||
_category: str
|
||||
|
||||
_version: str
|
||||
|
||||
|
||||
def __init__(self, software: dict):
|
||||
|
||||
self._name = escape(software['name'])
|
||||
|
||||
self._category = escape(software['category'])
|
||||
|
||||
self._version = escape(software['version'])
|
||||
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
||||
return str(self._name)
|
||||
|
||||
|
||||
@property
|
||||
def category(self) -> str:
|
||||
|
||||
return str(self._category)
|
||||
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
|
||||
return str(self._version)
|
||||
|
||||
|
||||
|
||||
_details: Details = None
|
||||
|
||||
_operating_system: OperatingSystem = None
|
||||
|
||||
_software: list[Software] = []
|
||||
|
||||
|
||||
def __init__(self, inventory: dict):
|
||||
|
||||
if (
|
||||
type(inventory['details']) is dict and
|
||||
type(inventory['os']) is dict and
|
||||
type(inventory['software']) is list
|
||||
):
|
||||
|
||||
self._details = self.Details(inventory['details'])
|
||||
|
||||
self._operating_system = self.OperatingSystem(inventory['os'])
|
||||
|
||||
for software in inventory['software']:
|
||||
|
||||
self._software += [ self.Software(software) ]
|
||||
|
||||
else:
|
||||
|
||||
raise ValidationError('Inventory File is invalid')
|
||||
|
||||
|
||||
@property
|
||||
def details(self) -> Details:
|
||||
|
||||
return self._details
|
||||
|
||||
|
||||
@property
|
||||
def operating_system(self) -> OperatingSystem:
|
||||
|
||||
return self._operating_system
|
||||
|
||||
|
||||
@property
|
||||
def software(self) -> list[Software]:
|
||||
|
||||
return list(self._software)
|
@ -1,9 +1,38 @@
|
||||
from django.urls import reverse
|
||||
|
||||
from itam.models.device import Device
|
||||
from rest_framework import serializers
|
||||
|
||||
from api.serializers.config import ParentGroupSerializer
|
||||
|
||||
from config_management.models.groups import ConfigGroupHosts
|
||||
|
||||
from itam.models.device import Device
|
||||
|
||||
|
||||
|
||||
class DeviceConfigGroupsSerializer(serializers.ModelSerializer):
|
||||
|
||||
name = serializers.CharField(source='group.name', read_only=True)
|
||||
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="API:_api_config_group", format="html"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = ConfigGroupHosts
|
||||
|
||||
fields = [
|
||||
'id',
|
||||
'name',
|
||||
'url',
|
||||
|
||||
]
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
class DeviceSerializer(serializers.ModelSerializer):
|
||||
@ -13,7 +42,9 @@ class DeviceSerializer(serializers.ModelSerializer):
|
||||
)
|
||||
|
||||
config = serializers.SerializerMethodField('get_device_config')
|
||||
|
||||
|
||||
groups = DeviceConfigGroupsSerializer(source='configgrouphosts_set', many=True, read_only=True)
|
||||
|
||||
def get_device_config(self, device):
|
||||
|
||||
request = self.context.get('request')
|
||||
@ -22,11 +53,29 @@ class DeviceSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = '__all__'
|
||||
|
||||
read_only_fields = [
|
||||
'inventorydate',
|
||||
depth = 1
|
||||
fields = [
|
||||
'id',
|
||||
'is_global',
|
||||
'slug',
|
||||
'name',
|
||||
'config',
|
||||
'serial_number',
|
||||
'uuid',
|
||||
'inventorydate',
|
||||
'created',
|
||||
'modified',
|
||||
'groups',
|
||||
'organization',
|
||||
'url',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'config',
|
||||
'inventorydate',
|
||||
'created',
|
||||
'modified',
|
||||
'groups',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
446
app/api/tasks.py
Normal file
446
app/api/tasks.py
Normal file
@ -0,0 +1,446 @@
|
||||
import json
|
||||
import re
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from celery import shared_task, current_task
|
||||
from celery.utils.log import get_task_logger
|
||||
from celery import states
|
||||
|
||||
from access.models import Organization
|
||||
|
||||
from api.serializers.inventory import Inventory
|
||||
|
||||
from itam.models.device import Device, DeviceType, DeviceOperatingSystem, DeviceSoftware
|
||||
from itam.models.operating_system import OperatingSystem, OperatingSystemVersion
|
||||
from itam.models.software import Software, SoftwareCategory, SoftwareVersion
|
||||
|
||||
from settings.models.app_settings import AppSettings
|
||||
|
||||
|
||||
logger = get_task_logger(__name__)
|
||||
|
||||
@shared_task(bind=True)
|
||||
def process_inventory(self, data, organization: int):
|
||||
|
||||
device = None
|
||||
device_operating_system = None
|
||||
operating_system = None
|
||||
operating_system_version = None
|
||||
|
||||
try:
|
||||
|
||||
logger.info('Begin Processing Inventory')
|
||||
|
||||
data = json.loads(data)
|
||||
data = Inventory(data)
|
||||
|
||||
organization = Organization.objects.get(id=organization)
|
||||
|
||||
app_settings = AppSettings.objects.get(owner_organization = None)
|
||||
|
||||
device_serial_number = None
|
||||
device_uuid = None
|
||||
|
||||
if data.details.serial_number and str(data.details.serial_number).lower() != 'na':
|
||||
|
||||
device_serial_number = str(data.details.serial_number)
|
||||
|
||||
if data.details.uuid and str(data.details.uuid).lower() != 'na':
|
||||
|
||||
device_uuid = str(data.details.uuid)
|
||||
|
||||
|
||||
if device_serial_number: # Search for device by serial number.
|
||||
|
||||
device = Device.objects.filter(
|
||||
serial_number__iexact=device_serial_number
|
||||
)
|
||||
|
||||
if device.exists():
|
||||
|
||||
device = Device.objects.get(
|
||||
serial_number__iexact=device_serial_number
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
device = None
|
||||
|
||||
|
||||
if device_uuid and not device: # Search for device by UUID.
|
||||
|
||||
device = Device.objects.filter(
|
||||
uuid__iexact=device_uuid
|
||||
)
|
||||
|
||||
if device.exists():
|
||||
|
||||
device = Device.objects.get(
|
||||
uuid__iexact=device_uuid
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
device = None
|
||||
|
||||
|
||||
if not device: # Search for device by Name.
|
||||
|
||||
device = Device.objects.filter(
|
||||
name__iexact=str(data.details.name).lower()
|
||||
)
|
||||
|
||||
if device.exists():
|
||||
|
||||
device = Device.objects.get(
|
||||
name__iexact=str(data.details.name).lower()
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
device = None
|
||||
|
||||
|
||||
|
||||
|
||||
if not device: # Create the device
|
||||
|
||||
device = Device.objects.create(
|
||||
name = data.details.name,
|
||||
device_type = None,
|
||||
serial_number = device_serial_number,
|
||||
uuid = device_uuid,
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
|
||||
if device:
|
||||
|
||||
logger.info(f"Device: {device.name}, Serial: {device.serial_number}, UUID: {device.uuid}")
|
||||
|
||||
device_edited = False
|
||||
|
||||
|
||||
if not device.uuid and device_uuid:
|
||||
|
||||
device.uuid = device_uuid
|
||||
|
||||
device_edited = True
|
||||
|
||||
|
||||
if not device.serial_number and device_serial_number:
|
||||
|
||||
device.serial_number = data.details.serial_number
|
||||
|
||||
device_edited = True
|
||||
|
||||
|
||||
if str(device.name).lower() != str(data.details.name).lower(): # Update device Name
|
||||
|
||||
device.name = data.details.name
|
||||
|
||||
device_edited = True
|
||||
|
||||
|
||||
if device_edited:
|
||||
|
||||
device.save()
|
||||
|
||||
|
||||
operating_system = OperatingSystem.objects.filter(
|
||||
name=data.operating_system.name,
|
||||
is_global = True
|
||||
)
|
||||
|
||||
if operating_system.exists():
|
||||
|
||||
operating_system = OperatingSystem.objects.get(
|
||||
name=data.operating_system.name,
|
||||
is_global = True
|
||||
)
|
||||
|
||||
|
||||
else:
|
||||
|
||||
operating_system = None
|
||||
|
||||
|
||||
|
||||
if not operating_system:
|
||||
|
||||
operating_system = OperatingSystem.objects.filter(
|
||||
name=data.operating_system.name,
|
||||
organization = organization
|
||||
)
|
||||
|
||||
|
||||
if operating_system.exists():
|
||||
|
||||
operating_system = OperatingSystem.objects.get(
|
||||
name=data.operating_system.name,
|
||||
organization = organization
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
operating_system = None
|
||||
|
||||
|
||||
if not operating_system:
|
||||
|
||||
operating_system = OperatingSystem.objects.create(
|
||||
name = data.operating_system.name,
|
||||
organization = organization,
|
||||
is_global = True
|
||||
)
|
||||
|
||||
|
||||
operating_system_version = OperatingSystemVersion.objects.filter(
|
||||
name=data.operating_system.version_major,
|
||||
operating_system=operating_system
|
||||
)
|
||||
|
||||
if operating_system_version.exists():
|
||||
|
||||
operating_system_version = OperatingSystemVersion.objects.get(
|
||||
name=data.operating_system.version_major,
|
||||
operating_system=operating_system
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
operating_system_version = None
|
||||
|
||||
|
||||
if not operating_system_version:
|
||||
|
||||
operating_system_version = OperatingSystemVersion.objects.create(
|
||||
organization = organization,
|
||||
is_global = True,
|
||||
name = data.operating_system.version_major,
|
||||
operating_system = operating_system,
|
||||
)
|
||||
|
||||
device_operating_system = DeviceOperatingSystem.objects.filter(
|
||||
device=device,
|
||||
)
|
||||
|
||||
if device_operating_system.exists():
|
||||
|
||||
device_operating_system = DeviceOperatingSystem.objects.get(
|
||||
device=device,
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
device_operating_system = None
|
||||
|
||||
|
||||
if not device_operating_system:
|
||||
|
||||
device_operating_system = DeviceOperatingSystem.objects.create(
|
||||
organization = organization,
|
||||
device=device,
|
||||
version = data.operating_system.version,
|
||||
operating_system_version = operating_system_version,
|
||||
installdate = timezone.now()
|
||||
)
|
||||
|
||||
if not device_operating_system.installdate: # Only update install date if empty
|
||||
|
||||
device_operating_system.installdate = timezone.now()
|
||||
|
||||
device_operating_system.save()
|
||||
|
||||
|
||||
if device_operating_system.operating_system_version != operating_system_version:
|
||||
|
||||
device_operating_system.operating_system_version = operating_system_version
|
||||
|
||||
device_operating_system.save()
|
||||
|
||||
|
||||
if device_operating_system.version != data.operating_system.version:
|
||||
|
||||
device_operating_system.version = data.operating_system.version
|
||||
|
||||
device_operating_system.save()
|
||||
|
||||
|
||||
if app_settings.software_is_global:
|
||||
|
||||
software_organization = app_settings.global_organization
|
||||
|
||||
else:
|
||||
|
||||
software_organization = device.organization
|
||||
|
||||
|
||||
if app_settings.software_categories_is_global:
|
||||
|
||||
software_category_organization = app_settings.global_organization
|
||||
|
||||
else:
|
||||
|
||||
software_category_organization = device.organization
|
||||
|
||||
inventoried_software: list = []
|
||||
|
||||
for inventory in list(data.software):
|
||||
|
||||
software = None
|
||||
software_category = None
|
||||
software_version = None
|
||||
|
||||
device_software = None
|
||||
|
||||
software_category = SoftwareCategory.objects.filter( name = inventory.category )
|
||||
|
||||
|
||||
if software_category.exists():
|
||||
|
||||
software_category = SoftwareCategory.objects.get(
|
||||
name = inventory.category
|
||||
)
|
||||
|
||||
else: # Create Software Category
|
||||
|
||||
software_category = SoftwareCategory.objects.create(
|
||||
organization = software_category_organization,
|
||||
is_global = True,
|
||||
name = inventory.category,
|
||||
)
|
||||
|
||||
|
||||
if software_category.name == inventory.category:
|
||||
|
||||
if Software.objects.filter( name = inventory.name ).exists():
|
||||
|
||||
software = Software.objects.get(
|
||||
name = inventory.name
|
||||
)
|
||||
|
||||
if not software.category:
|
||||
|
||||
software.category = software_category
|
||||
software.save()
|
||||
|
||||
else: # Create Software
|
||||
|
||||
software = Software.objects.create(
|
||||
organization = software_organization,
|
||||
is_global = True,
|
||||
name = inventory.name,
|
||||
category = software_category,
|
||||
)
|
||||
|
||||
|
||||
if software.name == inventory.name:
|
||||
|
||||
pattern = r"^(\d+:)?(?P<semver>\d+\.\d+(\.\d+)?)"
|
||||
|
||||
semver = re.search(pattern, str(inventory.version), re.DOTALL)
|
||||
|
||||
|
||||
if semver:
|
||||
|
||||
semver = semver['semver']
|
||||
|
||||
else:
|
||||
semver = inventory.version
|
||||
|
||||
|
||||
if SoftwareVersion.objects.filter( name = semver, software = software ).exists():
|
||||
|
||||
software_version = SoftwareVersion.objects.get(
|
||||
name = semver,
|
||||
software = software,
|
||||
)
|
||||
|
||||
else: # Create Software Category
|
||||
|
||||
software_version = SoftwareVersion.objects.create(
|
||||
organization = organization,
|
||||
is_global = True,
|
||||
name = semver,
|
||||
software = software,
|
||||
)
|
||||
|
||||
|
||||
if software_version.name == semver:
|
||||
|
||||
if DeviceSoftware.objects.filter( software = software, device=device ).exists():
|
||||
|
||||
device_software = DeviceSoftware.objects.get(
|
||||
device = device,
|
||||
software = software
|
||||
)
|
||||
|
||||
logger.debug(f"Select Existing Device Software: {device_software.software.name}")
|
||||
|
||||
else: # Create Software
|
||||
|
||||
device_software = DeviceSoftware.objects.create(
|
||||
organization = organization,
|
||||
is_global = True,
|
||||
installedversion = software_version,
|
||||
software = software,
|
||||
device = device,
|
||||
action=None
|
||||
)
|
||||
|
||||
|
||||
logger.debug(f"Create Device Software: {device_software.software.name}")
|
||||
|
||||
|
||||
if device_software: # Update the Inventoried software
|
||||
|
||||
inventoried_software += [ device_software.id ]
|
||||
|
||||
|
||||
if not device_software.installed: # Only update install date if blank
|
||||
|
||||
device_software.installed = timezone.now()
|
||||
|
||||
device_software.save()
|
||||
|
||||
logger.debug(f"Update Device Software (installed): {device_software.software.name}")
|
||||
|
||||
|
||||
if device_software.installedversion.name != software_version.name:
|
||||
|
||||
device_software.installedversion = software_version
|
||||
|
||||
device_software.save()
|
||||
|
||||
logger.debug(f"Update Device Software (installedversion): {device_software.software.name}")
|
||||
|
||||
for not_installed in DeviceSoftware.objects.filter( device=device ):
|
||||
|
||||
if not_installed.id not in inventoried_software:
|
||||
|
||||
not_installed.delete()
|
||||
|
||||
logger.debug(f"Remove Device Software: {not_installed.software.name}")
|
||||
|
||||
|
||||
if device and operating_system and operating_system_version and device_operating_system:
|
||||
|
||||
|
||||
device.inventorydate = timezone.now()
|
||||
|
||||
device.save()
|
||||
|
||||
|
||||
logger.info('Finish Processing Inventory')
|
||||
|
||||
return str('finished...')
|
||||
|
||||
except Exception as e:
|
||||
|
||||
logger.critical('Exception')
|
||||
|
||||
raise Exception(e)
|
||||
|
||||
return str(f'Exception Occured: {e}')
|
@ -1,251 +0,0 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from api.views.mixin import OrganizationPermissionAPI
|
||||
|
||||
from itam.models.device import Device
|
||||
|
||||
from settings.models.user_settings import UserSettings
|
||||
|
||||
|
||||
|
||||
class InventoryAPI(TestCase):
|
||||
|
||||
model = Device
|
||||
|
||||
model_name = 'device'
|
||||
app_label = 'itam'
|
||||
|
||||
inventory = {
|
||||
"details": {
|
||||
"name": "device_name",
|
||||
"serial_number": "a serial number",
|
||||
"uuid": "string"
|
||||
},
|
||||
"os": {
|
||||
"name": "os_name",
|
||||
"version_major": "12",
|
||||
"version": "12.1"
|
||||
},
|
||||
"software": [
|
||||
{
|
||||
"name": "software_name",
|
||||
"category": "category_name",
|
||||
"version": "1.2.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
@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
|
||||
|
||||
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])
|
||||
|
||||
self.add_user = User.objects.create_user(username="test_user_add", password="password")
|
||||
|
||||
add_user_settings = UserSettings.objects.get(user=self.add_user)
|
||||
|
||||
add_user_settings.default_organization = organization
|
||||
|
||||
add_user_settings.save()
|
||||
|
||||
|
||||
|
||||
@patch.object(OrganizationPermissionAPI, 'permission_check')
|
||||
def test_inventory_function_called_permission_check(self, permission_check):
|
||||
""" Inventory Upload checks permissions
|
||||
|
||||
Function 'permission_check' is the function that checks permissions
|
||||
|
||||
As the non-established way of authentication an API permission is being done
|
||||
confimation that the permissions are still checked is required.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('API:_api_device_inventory')
|
||||
|
||||
client.force_login(self.add_user)
|
||||
response = client.post(url, data=self.inventory, content_type='application/json')
|
||||
|
||||
assert permission_check.called
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_device_added(self):
|
||||
""" Device is created """
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_operating_system_added(self):
|
||||
""" Operating System is created """
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_operating_system_version_added(self):
|
||||
""" Operating System version is created """
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_device_has_operating_system_added(self):
|
||||
""" Operating System version linked to device """
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_device_operating_system_version_is_semver(self):
|
||||
""" 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(self):
|
||||
""" 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(self):
|
||||
""" Software category exists """
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_software_added(self):
|
||||
""" Test software exists """
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_software_category_linked_to_software(self):
|
||||
""" Software category linked to software """
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_software_version_added(self):
|
||||
""" Test software version exists """
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_software_version_returns_semver(self):
|
||||
""" 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(self):
|
||||
""" 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(self):
|
||||
""" 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(self):
|
||||
""" 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(self):
|
||||
""" 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(self):
|
||||
""" 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(self):
|
||||
""" Successful inventory upload returns 201 """
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_invalid_status_bad_request(self):
|
||||
""" Incorrectly formated inventory upload returns 400 """
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_exeception_status_sever_error(self):
|
||||
""" 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
|
||||
|
0
app/api/tests/unit/__init__.py
Normal file
0
app/api/tests/unit/__init__.py
Normal file
989
app/api/tests/unit/inventory/test_api_inventory.py
Normal file
989
app/api/tests/unit/inventory/test_api_inventory.py
Normal file
@ -0,0 +1,989 @@
|
||||
import datetime
|
||||
import json
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from api.views.mixin import OrganizationPermissionAPI
|
||||
from api.serializers.inventory import Inventory
|
||||
|
||||
from api.tasks import process_inventory
|
||||
|
||||
from itam.models.device import Device, DeviceOperatingSystem, DeviceSoftware
|
||||
from itam.models.operating_system import OperatingSystem, OperatingSystemVersion
|
||||
from itam.models.software import Software, SoftwareCategory, SoftwareVersion
|
||||
|
||||
from settings.models.user_settings import UserSettings
|
||||
|
||||
|
||||
|
||||
class InventoryAPI(TestCase):
|
||||
|
||||
model = Device
|
||||
|
||||
model_name = 'device'
|
||||
app_label = 'itam'
|
||||
|
||||
inventory = {
|
||||
"details": {
|
||||
"name": "device_name",
|
||||
"serial_number": "a serial number",
|
||||
"uuid": "string"
|
||||
},
|
||||
"os": {
|
||||
"name": "os_name",
|
||||
"version_major": "12",
|
||||
"version": "12.1"
|
||||
},
|
||||
"software": [
|
||||
{
|
||||
"name": "software_name",
|
||||
"category": "category_name",
|
||||
"version": "1.2.3"
|
||||
},
|
||||
{
|
||||
"name": "software_name_not_semver",
|
||||
"category": "category_name",
|
||||
"version": "2024.4"
|
||||
},
|
||||
{
|
||||
"name": "software_name_semver_contained",
|
||||
"category": "category_name",
|
||||
"version": "1.2.3-rc1"
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user
|
||||
2. Create a team for user with correct permissions
|
||||
3. add user to the teeam
|
||||
4. upload the inventory
|
||||
5. conduct queries for tests
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
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])
|
||||
|
||||
self.add_user = User.objects.create_user(username="test_user_add", password="password")
|
||||
|
||||
add_user_settings = UserSettings.objects.get(user=self.add_user)
|
||||
|
||||
add_user_settings.default_organization = organization
|
||||
|
||||
add_user_settings.save()
|
||||
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = add_team,
|
||||
user = self.add_user
|
||||
)
|
||||
|
||||
# upload the inventory
|
||||
process_inventory(json.dumps(self.inventory), organization.id)
|
||||
|
||||
|
||||
self.device = Device.objects.get(name=self.inventory['details']['name'])
|
||||
|
||||
self.operating_system = OperatingSystem.objects.get(name=self.inventory['os']['name'])
|
||||
|
||||
self.operating_system_version = OperatingSystemVersion.objects.get(name=self.inventory['os']['version_major'])
|
||||
|
||||
self.device_operating_system = DeviceOperatingSystem.objects.get(version=self.inventory['os']['version'])
|
||||
|
||||
self.software = Software.objects.get(name=self.inventory['software'][0]['name'])
|
||||
|
||||
self.software_category = SoftwareCategory.objects.get(name=self.inventory['software'][0]['category'])
|
||||
|
||||
self.software_version = SoftwareVersion.objects.get(
|
||||
name = self.inventory['software'][0]['version'],
|
||||
software = self.software,
|
||||
)
|
||||
|
||||
self.software_not_semver = Software.objects.get(name=self.inventory['software'][1]['name'])
|
||||
|
||||
self.software_version_not_semver = SoftwareVersion.objects.get(
|
||||
name = self.inventory['software'][1]['version'],
|
||||
software = self.software_not_semver
|
||||
)
|
||||
|
||||
self.software_is_semver = Software.objects.get(name=self.inventory['software'][2]['name'])
|
||||
|
||||
self.software_version_is_semver = SoftwareVersion.objects.get(
|
||||
software = self.software_is_semver
|
||||
)
|
||||
|
||||
self.device_software = DeviceSoftware.objects.get(device=self.device,software=self.software)
|
||||
|
||||
|
||||
|
||||
|
||||
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPOGATES=True)
|
||||
@patch.object(OrganizationPermissionAPI, 'permission_check')
|
||||
def test_inventory_function_called_permission_check(self, permission_check):
|
||||
""" Inventory Upload checks permissions
|
||||
|
||||
Function 'permission_check' is the function that checks permissions
|
||||
|
||||
As the non-established way of authentication an API permission is being done
|
||||
confimation that the permissions are still checked is required.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('API:_api_device_inventory')
|
||||
|
||||
client.force_login(self.add_user)
|
||||
response = client.post(url, data=self.inventory, content_type='application/json')
|
||||
|
||||
assert permission_check.called
|
||||
|
||||
|
||||
|
||||
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPOGATES=True)
|
||||
@patch.object(Inventory, '__init__')
|
||||
def test_inventory_serializer_inventory_called(self, serializer):
|
||||
""" Inventory Upload checks permissions
|
||||
|
||||
Function 'permission_check' is the function that checks permissions
|
||||
|
||||
As the non-established way of authentication an API permission is being done
|
||||
confimation that the permissions are still checked is required.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('API:_api_device_inventory')
|
||||
|
||||
client.force_login(self.add_user)
|
||||
response = client.post(url, data=self.inventory, content_type='application/json')
|
||||
|
||||
assert serializer.called
|
||||
|
||||
|
||||
|
||||
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPOGATES=True)
|
||||
@patch.object(Inventory.Details, '__init__')
|
||||
def test_inventory_serializer_inventory_details_called(self, serializer):
|
||||
""" Inventory Upload uses Inventory serializer
|
||||
|
||||
Details Serializer is called for inventory details dict.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('API:_api_device_inventory')
|
||||
|
||||
client.force_login(self.add_user)
|
||||
response = client.post(url, data=self.inventory, content_type='application/json')
|
||||
|
||||
assert serializer.called
|
||||
|
||||
|
||||
|
||||
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPOGATES=True)
|
||||
@patch.object(Inventory.OperatingSystem, '__init__')
|
||||
def test_inventory_serializer_inventory_operating_system_called(self, serializer):
|
||||
""" Inventory Upload uses Inventory serializer
|
||||
|
||||
Operating System Serializer is called for inventory Operating system dict.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('API:_api_device_inventory')
|
||||
|
||||
client.force_login(self.add_user)
|
||||
response = client.post(url, data=self.inventory, content_type='application/json')
|
||||
|
||||
assert serializer.called
|
||||
|
||||
|
||||
|
||||
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPOGATES=True)
|
||||
@patch.object(Inventory.Software, '__init__')
|
||||
def test_inventory_serializer_inventory_software_called(self, serializer):
|
||||
""" Inventory Upload uses Inventory serializer
|
||||
|
||||
Software Serializer is called for inventory software list.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('API:_api_device_inventory')
|
||||
|
||||
client.force_login(self.add_user)
|
||||
response = client.post(url, data=self.inventory, content_type='application/json')
|
||||
|
||||
assert serializer.called
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_added(self):
|
||||
""" Device is created """
|
||||
|
||||
assert self.device.name == self.inventory['details']['name']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_uuid_match(self):
|
||||
""" Device uuid match """
|
||||
|
||||
assert self.device.uuid == self.inventory['details']['uuid']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_serial_number_match(self):
|
||||
""" Device SN match """
|
||||
|
||||
assert self.device.serial_number == self.inventory['details']['serial_number']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_operating_system_added(self):
|
||||
""" Operating System is created """
|
||||
|
||||
assert self.operating_system.name == self.inventory['os']['name']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_operating_system_version_added(self):
|
||||
""" Operating System version is created """
|
||||
|
||||
assert self.operating_system_version.name == self.inventory['os']['version_major']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_has_operating_system_added(self):
|
||||
""" Operating System version linked to device """
|
||||
|
||||
assert self.device_operating_system.version == self.inventory['os']['version']
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_device_operating_system_version_is_semver(self):
|
||||
""" 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(self):
|
||||
""" 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
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_category_added(self):
|
||||
""" Software category exists """
|
||||
|
||||
assert self.software_category.name == self.inventory['software'][0]['category']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_added(self):
|
||||
""" Test software exists """
|
||||
|
||||
assert self.software.name == self.inventory['software'][0]['name']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_category_linked_to_software(self):
|
||||
""" Software category linked to software """
|
||||
|
||||
assert self.software.category == self.software_category
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_added(self):
|
||||
""" Test software version exists """
|
||||
|
||||
assert self.software_version.name == self.inventory['software'][0]['version']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_returns_semver(self):
|
||||
""" Software Version from inventory returns semver if within version string """
|
||||
|
||||
assert self.software_version_is_semver.name == str(self.inventory['software'][2]['version']).split('-')[0]
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_returns_original_version(self):
|
||||
""" Software Version from inventory returns inventoried version if no semver found """
|
||||
|
||||
assert self.software_version_not_semver.name == self.inventory['software'][1]['version']
|
||||
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_linked_to_software(self):
|
||||
""" Test software version linked to software it belongs too """
|
||||
|
||||
assert self.software_version.software == self.software
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_has_software_version(self):
|
||||
""" Inventoried software is linked to device and it's the corret one"""
|
||||
|
||||
assert self.software_version.name == self.inventory['software'][0]['version']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_software_has_installed_date(self):
|
||||
""" Inventoried software version has install date """
|
||||
|
||||
assert self.device_software.installed is not None
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_software_installed_date_type(self):
|
||||
""" Inventoried software version has install date """
|
||||
|
||||
assert type(self.device_software.installed) is datetime.datetime
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_device_software_blank_installed_date_is_updated(self):
|
||||
""" A blank installed date of software is updated if the software was already attached to the device """
|
||||
pass
|
||||
|
||||
|
||||
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPOGATES=True)
|
||||
def test_api_inventory_valid_status_ok_existing_device(self):
|
||||
""" Successful inventory upload returns 200 for existing device"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('API:_api_device_inventory')
|
||||
|
||||
client.force_login(self.add_user)
|
||||
response = client.post(url, data=self.inventory, content_type='application/json')
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPOGATES=True)
|
||||
def test_api_inventory_invalid_status_bad_request(self):
|
||||
""" Incorrectly formated inventory upload returns 400 """
|
||||
|
||||
client = Client()
|
||||
url = reverse('API:_api_device_inventory')
|
||||
|
||||
mod_inventory = self.inventory.copy()
|
||||
|
||||
mod_inventory.update({
|
||||
'details': {
|
||||
'name': 'test_api_inventory_invalid_status_bad_request'
|
||||
},
|
||||
'software': {
|
||||
'not_within_a': 'list'
|
||||
}
|
||||
})
|
||||
|
||||
client.force_login(self.add_user)
|
||||
response = client.post(url, data=mod_inventory, content_type='application/json')
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_exeception_status_sever_error(self):
|
||||
""" 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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class InventoryAPIDifferentNameSerialNumberMatch(TestCase):
|
||||
""" Test inventory upload with different name
|
||||
|
||||
should match by serial number
|
||||
"""
|
||||
|
||||
model = Device
|
||||
|
||||
model_name = 'device'
|
||||
app_label = 'itam'
|
||||
|
||||
inventory = {
|
||||
"details": {
|
||||
"name": "device_name",
|
||||
"serial_number": "serial_number_123",
|
||||
"uuid": "string"
|
||||
},
|
||||
"os": {
|
||||
"name": "os_name",
|
||||
"version_major": "12",
|
||||
"version": "12.1"
|
||||
},
|
||||
"software": [
|
||||
{
|
||||
"name": "software_name",
|
||||
"category": "category_name",
|
||||
"version": "1.2.3"
|
||||
},
|
||||
{
|
||||
"name": "software_name_not_semver",
|
||||
"category": "category_name",
|
||||
"version": "2024.4"
|
||||
},
|
||||
{
|
||||
"name": "software_name_semver_contained",
|
||||
"category": "category_name",
|
||||
"version": "1.2.3-rc1"
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user
|
||||
2. Create a team for user with correct permissions
|
||||
3. add user to the teeam
|
||||
4. upload the inventory
|
||||
5. conduct queries for tests
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
Device.objects.create(
|
||||
name='random device name',
|
||||
serial_number='serial_number_123'
|
||||
)
|
||||
|
||||
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])
|
||||
|
||||
self.add_user = User.objects.create_user(username="test_user_add", password="password")
|
||||
|
||||
add_user_settings = UserSettings.objects.get(user=self.add_user)
|
||||
|
||||
add_user_settings.default_organization = organization
|
||||
|
||||
add_user_settings.save()
|
||||
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = add_team,
|
||||
user = self.add_user
|
||||
)
|
||||
|
||||
# upload the inventory
|
||||
process_inventory(json.dumps(self.inventory), organization.id)
|
||||
|
||||
|
||||
self.device = Device.objects.get(name=self.inventory['details']['name'])
|
||||
|
||||
self.operating_system = OperatingSystem.objects.get(name=self.inventory['os']['name'])
|
||||
|
||||
self.operating_system_version = OperatingSystemVersion.objects.get(name=self.inventory['os']['version_major'])
|
||||
|
||||
self.device_operating_system = DeviceOperatingSystem.objects.get(version=self.inventory['os']['version'])
|
||||
|
||||
self.software = Software.objects.get(name=self.inventory['software'][0]['name'])
|
||||
|
||||
self.software_category = SoftwareCategory.objects.get(name=self.inventory['software'][0]['category'])
|
||||
|
||||
self.software_version = SoftwareVersion.objects.get(
|
||||
name = self.inventory['software'][0]['version'],
|
||||
software = self.software,
|
||||
)
|
||||
|
||||
self.software_not_semver = Software.objects.get(name=self.inventory['software'][1]['name'])
|
||||
|
||||
self.software_version_not_semver = SoftwareVersion.objects.get(
|
||||
name = self.inventory['software'][1]['version'],
|
||||
software = self.software_not_semver
|
||||
)
|
||||
|
||||
self.software_is_semver = Software.objects.get(name=self.inventory['software'][2]['name'])
|
||||
|
||||
self.software_version_is_semver = SoftwareVersion.objects.get(
|
||||
software = self.software_is_semver
|
||||
)
|
||||
|
||||
self.device_software = DeviceSoftware.objects.get(device=self.device,software=self.software)
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_added(self):
|
||||
""" Device is created """
|
||||
|
||||
assert self.device.name == self.inventory['details']['name']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_uuid_match(self):
|
||||
""" Device uuid match """
|
||||
|
||||
assert self.device.uuid == self.inventory['details']['uuid']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_serial_number_match(self):
|
||||
""" Device SN match """
|
||||
|
||||
assert self.device.serial_number == self.inventory['details']['serial_number']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_operating_system_added(self):
|
||||
""" Operating System is created """
|
||||
|
||||
assert self.operating_system.name == self.inventory['os']['name']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_operating_system_version_added(self):
|
||||
""" Operating System version is created """
|
||||
|
||||
assert self.operating_system_version.name == self.inventory['os']['version_major']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_has_operating_system_added(self):
|
||||
""" Operating System version linked to device """
|
||||
|
||||
assert self.device_operating_system.version == self.inventory['os']['version']
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_device_operating_system_version_is_semver(self):
|
||||
""" 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(self):
|
||||
""" 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
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_category_added(self):
|
||||
""" Software category exists """
|
||||
|
||||
assert self.software_category.name == self.inventory['software'][0]['category']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_added(self):
|
||||
""" Test software exists """
|
||||
|
||||
assert self.software.name == self.inventory['software'][0]['name']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_category_linked_to_software(self):
|
||||
""" Software category linked to software """
|
||||
|
||||
assert self.software.category == self.software_category
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_added(self):
|
||||
""" Test software version exists """
|
||||
|
||||
assert self.software_version.name == self.inventory['software'][0]['version']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_returns_semver(self):
|
||||
""" Software Version from inventory returns semver if within version string """
|
||||
|
||||
assert self.software_version_is_semver.name == str(self.inventory['software'][2]['version']).split('-')[0]
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_returns_original_version(self):
|
||||
""" Software Version from inventory returns inventoried version if no semver found """
|
||||
|
||||
assert self.software_version_not_semver.name == self.inventory['software'][1]['version']
|
||||
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_linked_to_software(self):
|
||||
""" Test software version linked to software it belongs too """
|
||||
|
||||
assert self.software_version.software == self.software
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_has_software_version(self):
|
||||
""" Inventoried software is linked to device and it's the corret one"""
|
||||
|
||||
assert self.software_version.name == self.inventory['software'][0]['version']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_software_has_installed_date(self):
|
||||
""" Inventoried software version has install date """
|
||||
|
||||
assert self.device_software.installed is not None
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_software_installed_date_type(self):
|
||||
""" Inventoried software version has install date """
|
||||
|
||||
assert type(self.device_software.installed) is datetime.datetime
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_device_software_blank_installed_date_is_updated(self):
|
||||
""" A blank installed date of software is updated if the software was already attached to the device """
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class InventoryAPIDifferentNameUUIDMatch(TestCase):
|
||||
""" Test inventory upload with different name
|
||||
|
||||
should match by uuid
|
||||
"""
|
||||
|
||||
model = Device
|
||||
|
||||
model_name = 'device'
|
||||
app_label = 'itam'
|
||||
|
||||
inventory = {
|
||||
"details": {
|
||||
"name": "device_name",
|
||||
"serial_number": "serial_number_123",
|
||||
"uuid": "123-456-789"
|
||||
},
|
||||
"os": {
|
||||
"name": "os_name",
|
||||
"version_major": "12",
|
||||
"version": "12.1"
|
||||
},
|
||||
"software": [
|
||||
{
|
||||
"name": "software_name",
|
||||
"category": "category_name",
|
||||
"version": "1.2.3"
|
||||
},
|
||||
{
|
||||
"name": "software_name_not_semver",
|
||||
"category": "category_name",
|
||||
"version": "2024.4"
|
||||
},
|
||||
{
|
||||
"name": "software_name_semver_contained",
|
||||
"category": "category_name",
|
||||
"version": "1.2.3-rc1"
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user
|
||||
2. Create a team for user with correct permissions
|
||||
3. add user to the teeam
|
||||
4. upload the inventory
|
||||
5. conduct queries for tests
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
Device.objects.create(
|
||||
name='random device name',
|
||||
uuid='123-456-789'
|
||||
)
|
||||
|
||||
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])
|
||||
|
||||
self.add_user = User.objects.create_user(username="test_user_add", password="password")
|
||||
|
||||
add_user_settings = UserSettings.objects.get(user=self.add_user)
|
||||
|
||||
add_user_settings.default_organization = organization
|
||||
|
||||
add_user_settings.save()
|
||||
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = add_team,
|
||||
user = self.add_user
|
||||
)
|
||||
|
||||
# upload the inventory
|
||||
process_inventory(json.dumps(self.inventory), organization.id)
|
||||
|
||||
|
||||
self.device = Device.objects.get(name=self.inventory['details']['name'])
|
||||
|
||||
self.operating_system = OperatingSystem.objects.get(name=self.inventory['os']['name'])
|
||||
|
||||
self.operating_system_version = OperatingSystemVersion.objects.get(name=self.inventory['os']['version_major'])
|
||||
|
||||
self.device_operating_system = DeviceOperatingSystem.objects.get(version=self.inventory['os']['version'])
|
||||
|
||||
self.software = Software.objects.get(name=self.inventory['software'][0]['name'])
|
||||
|
||||
self.software_category = SoftwareCategory.objects.get(name=self.inventory['software'][0]['category'])
|
||||
|
||||
self.software_version = SoftwareVersion.objects.get(
|
||||
name = self.inventory['software'][0]['version'],
|
||||
software = self.software,
|
||||
)
|
||||
|
||||
self.software_not_semver = Software.objects.get(name=self.inventory['software'][1]['name'])
|
||||
|
||||
self.software_version_not_semver = SoftwareVersion.objects.get(
|
||||
name = self.inventory['software'][1]['version'],
|
||||
software = self.software_not_semver
|
||||
)
|
||||
|
||||
self.software_is_semver = Software.objects.get(name=self.inventory['software'][2]['name'])
|
||||
|
||||
self.software_version_is_semver = SoftwareVersion.objects.get(
|
||||
software = self.software_is_semver
|
||||
)
|
||||
|
||||
self.device_software = DeviceSoftware.objects.get(device=self.device,software=self.software)
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_added(self):
|
||||
""" Device is created """
|
||||
|
||||
assert self.device.name == self.inventory['details']['name']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_uuid_match(self):
|
||||
""" Device uuid match """
|
||||
|
||||
assert self.device.uuid == self.inventory['details']['uuid']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_serial_number_match(self):
|
||||
""" Device SN match """
|
||||
|
||||
assert self.device.serial_number == self.inventory['details']['serial_number']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_operating_system_added(self):
|
||||
""" Operating System is created """
|
||||
|
||||
assert self.operating_system.name == self.inventory['os']['name']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_operating_system_version_added(self):
|
||||
""" Operating System version is created """
|
||||
|
||||
assert self.operating_system_version.name == self.inventory['os']['version_major']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_has_operating_system_added(self):
|
||||
""" Operating System version linked to device """
|
||||
|
||||
assert self.device_operating_system.version == self.inventory['os']['version']
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_device_operating_system_version_is_semver(self):
|
||||
""" 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(self):
|
||||
""" 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
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_category_added(self):
|
||||
""" Software category exists """
|
||||
|
||||
assert self.software_category.name == self.inventory['software'][0]['category']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_added(self):
|
||||
""" Test software exists """
|
||||
|
||||
assert self.software.name == self.inventory['software'][0]['name']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_category_linked_to_software(self):
|
||||
""" Software category linked to software """
|
||||
|
||||
assert self.software.category == self.software_category
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_added(self):
|
||||
""" Test software version exists """
|
||||
|
||||
assert self.software_version.name == self.inventory['software'][0]['version']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_returns_semver(self):
|
||||
""" Software Version from inventory returns semver if within version string """
|
||||
|
||||
assert self.software_version_is_semver.name == str(self.inventory['software'][2]['version']).split('-')[0]
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_returns_original_version(self):
|
||||
""" Software Version from inventory returns inventoried version if no semver found """
|
||||
|
||||
assert self.software_version_not_semver.name == self.inventory['software'][1]['version']
|
||||
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_linked_to_software(self):
|
||||
""" Test software version linked to software it belongs too """
|
||||
|
||||
assert self.software_version.software == self.software
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_has_software_version(self):
|
||||
""" Inventoried software is linked to device and it's the corret one"""
|
||||
|
||||
assert self.software_version.name == self.inventory['software'][0]['version']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_software_has_installed_date(self):
|
||||
""" Inventoried software version has install date """
|
||||
|
||||
assert self.device_software.installed is not None
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_software_installed_date_type(self):
|
||||
""" Inventoried software version has install date """
|
||||
|
||||
assert type(self.device_software.installed) is datetime.datetime
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_device_software_blank_installed_date_is_updated(self):
|
||||
""" A blank installed date of software is updated if the software was already attached to the device """
|
||||
pass
|
||||
|
@ -1,3 +1,4 @@
|
||||
import celery
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
@ -7,6 +8,9 @@ from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
@ -188,6 +192,8 @@ class InventoryPermissionsAPI(TestCase):
|
||||
|
||||
|
||||
|
||||
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPOGATES=True)
|
||||
def test_device_auth_add_user_anon_denied(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
@ -203,6 +209,8 @@ class InventoryPermissionsAPI(TestCase):
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPOGATES=True)
|
||||
def test_device_auth_add_no_permission_denied(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
@ -219,6 +227,8 @@ class InventoryPermissionsAPI(TestCase):
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPOGATES=True)
|
||||
def test_device_auth_add_different_organization_denied(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
@ -235,6 +245,8 @@ class InventoryPermissionsAPI(TestCase):
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPOGATES=True)
|
||||
def test_device_auth_add_permission_view_denied(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
@ -251,6 +263,8 @@ class InventoryPermissionsAPI(TestCase):
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPOGATES=True)
|
||||
def test_device_auth_add_has_permission(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
@ -264,6 +278,6 @@ class InventoryPermissionsAPI(TestCase):
|
||||
client.force_login(self.add_user)
|
||||
response = client.post(url, data=self.inventory, content_type='application/json')
|
||||
|
||||
assert response.status_code == 201
|
||||
assert response.status_code == 200
|
||||
|
||||
|
326
app/api/tests/unit/token/test_token.py
Normal file
326
app/api/tests/unit/token/test_token.py
Normal file
@ -0,0 +1,326 @@
|
||||
import hashlib
|
||||
import json
|
||||
import pytest
|
||||
import requests
|
||||
import unittest
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from api.models.tokens import AuthToken
|
||||
|
||||
from settings.models.user_settings import UserSettings
|
||||
|
||||
class APIAuthToken(TestCase):
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user
|
||||
3. create user
|
||||
4. create user settings
|
||||
5. create API key (valid)
|
||||
6. generate an API key that does not exist
|
||||
5. create API key (expired)
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
self.add_user = User.objects.create_user(username="test_user_add", password="password")
|
||||
|
||||
add_user_settings = UserSettings.objects.get(user=self.add_user)
|
||||
|
||||
add_user_settings.default_organization = organization
|
||||
|
||||
add_user_settings.save()
|
||||
|
||||
expires = datetime.utcnow() + timedelta(days = 10)
|
||||
|
||||
expires = expires.strftime('%Y-%m-%d %H:%M:%S%z')
|
||||
|
||||
token = AuthToken.objects.create(
|
||||
user = self.add_user,
|
||||
expires=expires
|
||||
)
|
||||
|
||||
self.api_token_valid = token.generate()
|
||||
self.hashed_token = token.token_hash(self.api_token_valid)
|
||||
token.token = self.hashed_token
|
||||
|
||||
token.save()
|
||||
|
||||
self.api_token_does_not_exist = hashlib.sha256(str('a random string').encode('utf-8')).hexdigest()
|
||||
|
||||
|
||||
expires = datetime.utcnow() + timedelta(days = -10)
|
||||
|
||||
expires = expires.strftime('%Y-%m-%d %H:%M:%S%z')
|
||||
|
||||
|
||||
self.api_token_expired = token.generate()
|
||||
|
||||
self.hashed_token_expired = token.token_hash(self.api_token_expired)
|
||||
|
||||
token = AuthToken.objects.create(
|
||||
user = self.add_user,
|
||||
expires=expires,
|
||||
token = self.hashed_token_expired
|
||||
)
|
||||
|
||||
|
||||
|
||||
def test_token_create_own(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
User can only create token for self.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
client.force_login(self.add_user)
|
||||
url = reverse('_user_auth_token_add', kwargs={'user_id': self.add_user.id})
|
||||
|
||||
|
||||
response = client.post(url, kwargs={'user_id': self.add_user.id})
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
|
||||
def test_token_create_other_user(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
User can not create token for another user.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
client.force_login(self.add_user)
|
||||
url = reverse('_user_auth_token_add', kwargs={'user_id': 999})
|
||||
|
||||
|
||||
response = client.post(url, kwargs={'user_id': 999})
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
|
||||
def test_token_delete_own(self):
|
||||
""" Check correct permission for delete
|
||||
|
||||
User can only delete token for self.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
client.force_login(self.add_user)
|
||||
url = reverse('_user_auth_token_delete', kwargs={'user_id': self.add_user.id, 'pk': 1})
|
||||
|
||||
|
||||
response = client.post(url, kwargs={'user_id': self.add_user.id, 'pk': 1})
|
||||
|
||||
assert response.status_code == 302 and response.url == '/account/settings/1'
|
||||
|
||||
|
||||
|
||||
def test_token_delete_other_user(self):
|
||||
""" Check correct permission for delete
|
||||
|
||||
User can not delete another users token.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
client.force_login(self.add_user)
|
||||
url = reverse('_user_auth_token_delete', kwargs={'user_id': 999, 'pk': 1})
|
||||
|
||||
|
||||
response = client.post(url, data={'id': 1}, kwargs={'user_id': 999, 'pk': 1})
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
|
||||
def test_auth_invalid_token(self):
|
||||
""" Check token authentication
|
||||
|
||||
Invalid token does not allow login
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('home') + 'api/'
|
||||
|
||||
|
||||
response = client.get(
|
||||
url,
|
||||
content_type='application/json',
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Token ' + self.api_token_does_not_exist,
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
|
||||
def test_auth_no_token(self):
|
||||
""" Check token authentication
|
||||
|
||||
providing no token does not allow login
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('home') + 'api/'
|
||||
|
||||
|
||||
response = client.get(
|
||||
url,
|
||||
content_type='application/json',
|
||||
headers = {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
|
||||
def test_auth_expired_token(self):
|
||||
""" Check token authentication
|
||||
|
||||
expired token does not allow login
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('home') + 'api/'
|
||||
|
||||
|
||||
response = client.get(
|
||||
url,
|
||||
content_type='application/json',
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Token ' + self.api_token_expired,
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
|
||||
def test_auth_valid_token(self):
|
||||
""" Check token authentication
|
||||
|
||||
Valid token allows login
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('home') + 'api/'
|
||||
|
||||
|
||||
response = client.get(
|
||||
url,
|
||||
content_type='application/json',
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Token ' + self.api_token_valid,
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
|
||||
def test_feat_expired_token_is_removed(self):
|
||||
""" token feature confirmation
|
||||
|
||||
expired token is deleted
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('home') + 'api/'
|
||||
|
||||
|
||||
response = client.get(
|
||||
url,
|
||||
content_type='application/json',
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Token ' + self.api_token_expired,
|
||||
}
|
||||
)
|
||||
|
||||
db_query = AuthToken.objects.filter(
|
||||
token = self.hashed_token_expired
|
||||
)
|
||||
|
||||
assert not db_query.exists()
|
||||
|
||||
|
||||
|
||||
def test_token_not_saved_to_db(self):
|
||||
""" confirm generated token not saved to the database """
|
||||
|
||||
db_query = AuthToken.objects.filter(
|
||||
token = self.api_token_valid
|
||||
)
|
||||
|
||||
assert not db_query.exists()
|
||||
|
||||
|
||||
|
||||
def test_header_format_invalid_token(self):
|
||||
""" token header format check
|
||||
|
||||
header missing 'Token' prefix reports invalid
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('home') + 'api/'
|
||||
|
||||
|
||||
response = client.get(
|
||||
url,
|
||||
content_type='application/json',
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': '' + self.api_token_valid,
|
||||
}
|
||||
)
|
||||
|
||||
content: dict = json.loads(response.content.decode('utf-8'))
|
||||
|
||||
assert response.status_code == 401 and content['detail'] == 'Token header invalid'
|
||||
|
||||
|
||||
|
||||
def test_header_format_invalid_token_spaces(self):
|
||||
""" token header format check
|
||||
|
||||
auth header with extra spaces reports invalid
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('home') + 'api/'
|
||||
|
||||
|
||||
response = client.get(
|
||||
url,
|
||||
content_type='application/json',
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Token A space ' + self.api_token_valid,
|
||||
}
|
||||
)
|
||||
|
||||
content: dict = json.loads(response.content.decode('utf-8'))
|
||||
|
||||
assert response.status_code == 401 and content['detail'] == 'Token header invalid. Possibly incorrectly formatted'
|
||||
|
@ -3,7 +3,7 @@ from django.urls import path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from rest_framework.urlpatterns import format_suffix_patterns
|
||||
|
||||
from .views import access, index
|
||||
from .views import access, config, index
|
||||
|
||||
from .views.itam import software, config as itam_config
|
||||
from .views.itam.device import DeviceViewSet
|
||||
@ -24,6 +24,9 @@ router.register('software', software.SoftwareViewSet, basename='software')
|
||||
urlpatterns = [
|
||||
path("config/<slug:slug>/", itam_config.View.as_view(), name="_api_device_config"),
|
||||
|
||||
path("configuration/", config.ConfigGroupsList.as_view(), name='_api_config_groups'),
|
||||
path("configuration/<int:pk>", config.ConfigGroupsDetail.as_view(), name='_api_config_group'),
|
||||
|
||||
path("device/inventory", inventory.Collect.as_view(), name="_api_device_inventory"),
|
||||
|
||||
path("organization/", access.OrganizationList.as_view(), name='_api_orgs'),
|
||||
|
@ -1,5 +1,7 @@
|
||||
from django.contrib.auth.models import Permission
|
||||
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse
|
||||
|
||||
from rest_framework import generics, routers, serializers, views
|
||||
from rest_framework.permissions import DjangoObjectPermissions
|
||||
from rest_framework.response import Response
|
||||
@ -7,12 +9,17 @@ from rest_framework.response import Response
|
||||
from access.mixin import OrganizationMixin
|
||||
from access.models import Organization, Team
|
||||
|
||||
from api.serializers.access import OrganizationSerializer, OrganizationListSerializer, TeamSerializer
|
||||
from api.serializers.access import OrganizationSerializer, OrganizationListSerializer, TeamSerializer, TeamPermissionSerializer
|
||||
from api.views.mixin import OrganizationPermissionAPI
|
||||
|
||||
|
||||
|
||||
class OrganizationList(generics.ListCreateAPIView):
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary = "Fetch Organizations",
|
||||
description="Returns a list of organizations."
|
||||
),
|
||||
)
|
||||
class OrganizationList(generics.ListAPIView):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
@ -28,7 +35,18 @@ class OrganizationList(generics.ListCreateAPIView):
|
||||
|
||||
|
||||
|
||||
class OrganizationDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary = "Get An Organization",
|
||||
),
|
||||
patch=extend_schema(
|
||||
summary = "Update an organization",
|
||||
),
|
||||
put=extend_schema(
|
||||
summary = "Update an organization",
|
||||
),
|
||||
)
|
||||
class OrganizationDetail(generics.RetrieveUpdateAPIView):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
@ -44,6 +62,20 @@ class OrganizationDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
post=extend_schema(
|
||||
summary = "Create a Team",
|
||||
description = """Create a team within the defined organization.""",
|
||||
tags = ['team',],
|
||||
request = TeamSerializer,
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
|
||||
401: OpenApiResponse(description='User Not logged in'),
|
||||
403: OpenApiResponse(description='User is missing permission or in different organization'),
|
||||
}
|
||||
),
|
||||
create=extend_schema(exclude=True),
|
||||
)
|
||||
class TeamList(generics.ListCreateAPIView):
|
||||
|
||||
permission_classes = [
|
||||
@ -66,6 +98,45 @@ class TeamList(generics.ListCreateAPIView):
|
||||
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary = "Fetch a Team",
|
||||
description = """Fetch a team within the defined organization.
|
||||
""",
|
||||
methods=["GET"],
|
||||
tags = ['team',],
|
||||
request = TeamSerializer,
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
|
||||
401: OpenApiResponse(description='User Not logged in'),
|
||||
403: OpenApiResponse(description='User is missing permission or in different organization'),
|
||||
}
|
||||
),
|
||||
patch=extend_schema(
|
||||
summary = "Update a Team",
|
||||
description = """Update a team within the defined organization.
|
||||
""",
|
||||
methods=["Patch"],
|
||||
tags = ['team',],
|
||||
request = TeamSerializer,
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
|
||||
401: OpenApiResponse(description='User Not logged in'),
|
||||
403: OpenApiResponse(description='User is missing permission or in different organization'),
|
||||
}
|
||||
),
|
||||
put = extend_schema(
|
||||
summary = "Amend a team",
|
||||
tags = ['team',],
|
||||
),
|
||||
delete=extend_schema(
|
||||
summary = "Delete a Team",
|
||||
tags = ['team',],
|
||||
),
|
||||
post = extend_schema(
|
||||
exclude = True,
|
||||
)
|
||||
)
|
||||
class TeamDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
|
||||
permission_classes = [
|
||||
@ -79,12 +150,66 @@ class TeamDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
|
||||
|
||||
|
||||
class TeamPermissionDetail(routers.APIRootView):
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary = "Fetch a teams permissions",
|
||||
tags = ['team',],
|
||||
),
|
||||
post=extend_schema(
|
||||
summary = "Replace team Permissions",
|
||||
description = """Replace the teams permissions with the permissions supplied.
|
||||
|
||||
# temp disabled until permission checker updated
|
||||
# permission_classes = [
|
||||
# OrganizationPermissionAPI
|
||||
# ]
|
||||
Teams Permissions will be replaced with the permissions supplied. **ALL** existing permissions will be
|
||||
removed.
|
||||
|
||||
permissions are required to be in format `<module name>_<permission>_<table name>`
|
||||
""",
|
||||
|
||||
methods=["POST"],
|
||||
tags = ['team',],
|
||||
request = TeamPermissionSerializer,
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
|
||||
401: OpenApiResponse(description='User Not logged in'),
|
||||
403: OpenApiResponse(description='User is missing permission or in different organization'),
|
||||
}
|
||||
),
|
||||
delete=extend_schema(
|
||||
summary = "Delete permissions",
|
||||
tags = ['team',],
|
||||
),
|
||||
patch = extend_schema(
|
||||
summary = "Amend team Permissions",
|
||||
description = """Amend the teams permissions with the permissions supplied.
|
||||
|
||||
Teams permissions will include the existing permissions along with the ones supplied.
|
||||
permissions are required to be in format `<module name>_<permission>_<table name>`
|
||||
""",
|
||||
|
||||
methods=["PATCH"],
|
||||
parameters = None,
|
||||
tags = ['team',],
|
||||
request = TeamPermissionSerializer,
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
|
||||
401: OpenApiResponse(description='User Not logged in'),
|
||||
403: OpenApiResponse(description='User is missing permission or in different organization'),
|
||||
}
|
||||
),
|
||||
put = extend_schema(
|
||||
summary = "Amend team Permissions",
|
||||
tags = ['team',],
|
||||
)
|
||||
)
|
||||
class TeamPermissionDetail(views.APIView):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
]
|
||||
|
||||
queryset = Team.objects.all()
|
||||
|
||||
serializer_class = TeamPermissionSerializer
|
||||
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
54
app/api/views/config.py
Normal file
54
app/api/views/config.py
Normal file
@ -0,0 +1,54 @@
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
|
||||
from rest_framework import generics
|
||||
|
||||
from api.serializers.config import ConfigGroupsSerializer
|
||||
from api.views.mixin import OrganizationPermissionAPI
|
||||
|
||||
from config_management.models.groups import ConfigGroups
|
||||
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary = "Fetch Config groups",
|
||||
description="Returns a list of Config Groups."
|
||||
),
|
||||
)
|
||||
class ConfigGroupsList(generics.ListAPIView):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
]
|
||||
|
||||
queryset = ConfigGroups.objects.all()
|
||||
lookup_field = 'pk'
|
||||
serializer_class = ConfigGroupsSerializer
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
return "Config Groups"
|
||||
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary = "Get A Config Group",
|
||||
# responses = {}
|
||||
),
|
||||
)
|
||||
class ConfigGroupsDetail(generics.RetrieveAPIView):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
]
|
||||
|
||||
queryset = ConfigGroups.objects.all()
|
||||
lookup_field = 'pk'
|
||||
serializer_class = ConfigGroupsSerializer
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
return "Config Group"
|
||||
|
||||
|
@ -27,6 +27,7 @@ class Index(viewsets.ViewSet):
|
||||
{
|
||||
# "teams": reverse("_api_teams", request=request),
|
||||
"devices": reverse("API:device-list", request=request),
|
||||
"config_groups": reverse("API:_api_config_groups", request=request),
|
||||
"organizations": reverse("API:_api_orgs", request=request),
|
||||
"software": reverse("API:software-list", request=request),
|
||||
}
|
||||
|
@ -1,30 +1,25 @@
|
||||
# from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
|
||||
import json
|
||||
import re
|
||||
|
||||
from django.http import Http404, JsonResponse
|
||||
from django.utils import timezone
|
||||
from django.core.exceptions import ValidationError, PermissionDenied
|
||||
|
||||
from drf_spectacular.utils import extend_schema, OpenApiExample, OpenApiTypes, OpenApiResponse, OpenApiParameter
|
||||
from drf_spectacular.utils import extend_schema, OpenApiResponse
|
||||
|
||||
from rest_framework import generics, views
|
||||
from rest_framework.response import Response
|
||||
|
||||
from access.mixin import OrganizationMixin
|
||||
from access.models import Organization
|
||||
|
||||
from api.views.mixin import OrganizationPermissionAPI
|
||||
from api.serializers.itam.inventory import InventorySerializer
|
||||
from api.serializers.inventory import Inventory
|
||||
|
||||
from core.http.common import Http
|
||||
|
||||
from itam.models.device import Device, DeviceType, DeviceOperatingSystem, DeviceSoftware
|
||||
from itam.models.operating_system import OperatingSystem, OperatingSystemVersion
|
||||
from itam.models.software import Software, SoftwareCategory, SoftwareVersion
|
||||
from itam.models.device import Device
|
||||
|
||||
from settings.models.app_settings import AppSettings
|
||||
from settings.models.user_settings import UserSettings
|
||||
|
||||
from api.tasks import process_inventory
|
||||
|
||||
|
||||
|
||||
class InventoryPermissions(OrganizationPermissionAPI):
|
||||
@ -33,7 +28,7 @@ class InventoryPermissions(OrganizationPermissionAPI):
|
||||
|
||||
data = view.request.data
|
||||
|
||||
self.obj = Device.objects.get(slug=str(data['details']['name']).lower())
|
||||
self.obj = Device.objects.get(slug=str(data.details.name).lower())
|
||||
|
||||
return super().permission_check(request, view, obj=None)
|
||||
|
||||
@ -66,9 +61,7 @@ this setting populated, no device will be created and the endpoint will return H
|
||||
tags = ['device', 'inventory',],
|
||||
request = InventorySerializer,
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Inventory updated an existing device'),
|
||||
201: OpenApiResponse(description='Inventory created a new device'),
|
||||
400: OpenApiResponse(description='Inventory is invalid'),
|
||||
200: OpenApiResponse(description='Inventory upload successful'),
|
||||
401: OpenApiResponse(description='User Not logged in'),
|
||||
403: OpenApiResponse(description='User is missing permission or in different organization'),
|
||||
500: OpenApiResponse(description='Exception occured. View server logs for the Stack Trace'),
|
||||
@ -76,267 +69,53 @@ this setting populated, no device will be created and the endpoint will return H
|
||||
)
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
|
||||
data = json.loads(request.body)
|
||||
|
||||
device = None
|
||||
|
||||
self.default_organization = UserSettings.objects.get(user=request.user).default_organization
|
||||
|
||||
if Device.objects.filter(slug=str(data['details']['name']).lower()).exists():
|
||||
|
||||
self.obj = Device.objects.get(slug=str(data['details']['name']).lower())
|
||||
|
||||
device = self.obj
|
||||
|
||||
|
||||
if not self.permission_check(request=request, view=self, obj=device):
|
||||
|
||||
raise Http404
|
||||
|
||||
|
||||
|
||||
status = Http.Status.BAD_REQUEST
|
||||
|
||||
device_operating_system = None
|
||||
operating_system = None
|
||||
operating_system_version = None
|
||||
status = Http.Status.OK
|
||||
response_data = 'OK'
|
||||
|
||||
try:
|
||||
|
||||
app_settings = AppSettings.objects.get(owner_organization = None)
|
||||
data = json.loads(request.body)
|
||||
data = Inventory(data)
|
||||
|
||||
if not device: # Create the device
|
||||
device = None
|
||||
|
||||
device = Device.objects.create(
|
||||
name = data['details']['name'],
|
||||
device_type = None,
|
||||
serial_number = data['details']['serial_number'],
|
||||
uuid = data['details']['uuid'],
|
||||
organization = self.default_organization,
|
||||
)
|
||||
|
||||
status = Http.Status.CREATED
|
||||
self.default_organization = UserSettings.objects.get(user=request.user).default_organization
|
||||
|
||||
if Device.objects.filter(slug=str(data.details.name).lower()).exists():
|
||||
|
||||
if OperatingSystem.objects.filter( slug=data['os']['name'] ).exists():
|
||||
self.obj = Device.objects.get(slug=str(data.details.name).lower())
|
||||
|
||||
operating_system = OperatingSystem.objects.get( slug=data['os']['name'] )
|
||||
device = self.obj
|
||||
|
||||
else: # Create Operating System
|
||||
|
||||
operating_system = OperatingSystem.objects.create(
|
||||
name = data['os']['name'],
|
||||
organization = self.default_organization,
|
||||
is_global = True
|
||||
)
|
||||
if not self.permission_check(request=request, view=self, obj=device):
|
||||
|
||||
raise Http404
|
||||
|
||||
if OperatingSystemVersion.objects.filter( name=data['os']['version_major'], operating_system=operating_system ).exists():
|
||||
task = process_inventory.delay(request.body, self.default_organization.id)
|
||||
|
||||
operating_system_version = OperatingSystemVersion.objects.get(
|
||||
organization = self.default_organization,
|
||||
is_global = True,
|
||||
name = data['os']['version_major'],
|
||||
operating_system = operating_system
|
||||
)
|
||||
response_data: dict = {"task_id": f"{task.id}"}
|
||||
|
||||
else: # Create Operating System Version
|
||||
except PermissionDenied as e:
|
||||
|
||||
operating_system_version = OperatingSystemVersion.objects.create(
|
||||
organization = self.default_organization,
|
||||
is_global = True,
|
||||
name = data['os']['version_major'],
|
||||
operating_system = operating_system,
|
||||
)
|
||||
status = Http.Status.FORBIDDEN
|
||||
response_data = ''
|
||||
|
||||
except ValidationError as e:
|
||||
|
||||
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 = self.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 = self.default_organization,
|
||||
is_global = True,
|
||||
name = semver,
|
||||
software = software,
|
||||
)
|
||||
|
||||
|
||||
if DeviceSoftware.objects.filter( software = software, device=device ).exists():
|
||||
|
||||
device_software = DeviceSoftware.objects.get(
|
||||
device = device,
|
||||
software = software
|
||||
)
|
||||
|
||||
else: # Create Software
|
||||
|
||||
device_software = DeviceSoftware.objects.create(
|
||||
organization = self.default_organization,
|
||||
is_global = True,
|
||||
installedversion = software_version,
|
||||
software = software,
|
||||
device = device,
|
||||
action=None
|
||||
)
|
||||
|
||||
|
||||
if device_software: # Update the Inventoried software
|
||||
|
||||
clear_installed_software = DeviceSoftware.objects.filter(
|
||||
device = device,
|
||||
software = software
|
||||
)
|
||||
|
||||
# Clear installed version of all installed software
|
||||
# any found later with no version to be removed
|
||||
clear_installed_software.update(installedversion=None)
|
||||
|
||||
|
||||
if not device_software.installed: # Only update install date if blank
|
||||
|
||||
device_software.installed = timezone.now()
|
||||
|
||||
device_software.save()
|
||||
|
||||
device_software.installedversion = software_version
|
||||
|
||||
device_software.save()
|
||||
|
||||
|
||||
if device and operating_system and operating_system_version and device_operating_system:
|
||||
|
||||
# Remove software no longer installed
|
||||
DeviceSoftware.objects.filter(
|
||||
device = device,
|
||||
software = software,
|
||||
).delete()
|
||||
|
||||
device.inventorydate = timezone.now()
|
||||
|
||||
device.save()
|
||||
|
||||
if status != Http.Status.CREATED:
|
||||
|
||||
status = Http.Status.OK
|
||||
|
||||
status = Http.Status.BAD_REQUEST
|
||||
response_data = e.message
|
||||
|
||||
except Exception as e:
|
||||
|
||||
print(f'An error occured{e}')
|
||||
|
||||
status = Http.Status.SERVER_ERROR
|
||||
response_data = 'Unknown Server Error occured'
|
||||
|
||||
|
||||
return Response(data='OK',status=status)
|
||||
return Response(data=response_data,status=status)
|
||||
|
||||
|
||||
|
||||
|
@ -3,6 +3,8 @@ from django.shortcuts import get_object_or_404
|
||||
|
||||
from rest_framework import generics, viewsets
|
||||
|
||||
from access.mixin import OrganizationMixin
|
||||
|
||||
from api.serializers.itam.software import SoftwareSerializer
|
||||
from api.views.mixin import OrganizationPermissionAPI
|
||||
|
||||
@ -10,7 +12,7 @@ from itam.models.software import Software
|
||||
|
||||
|
||||
|
||||
class SoftwareViewSet(viewsets.ModelViewSet):
|
||||
class SoftwareViewSet(OrganizationMixin, viewsets.ModelViewSet):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
|
@ -1,6 +1,7 @@
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.forms import ValidationError
|
||||
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.permissions import DjangoObjectPermissions
|
||||
|
||||
from access.mixin import OrganizationMixin
|
||||
@ -28,12 +29,16 @@ class OrganizationPermissionAPI(DjangoObjectPermissions, OrganizationMixin):
|
||||
|
||||
self.request = request
|
||||
|
||||
method = self.request._request.method.lower()
|
||||
|
||||
if method.upper() not in view.allowed_methods:
|
||||
|
||||
view.http_method_not_allowed(request._request)
|
||||
|
||||
if hasattr(view, 'queryset'):
|
||||
if view.queryset.model._meta:
|
||||
self.obj = view.queryset.model
|
||||
|
||||
method = self.request._request.method.lower()
|
||||
|
||||
object_organization = None
|
||||
|
||||
if method == 'get':
|
||||
|
@ -0,0 +1,3 @@
|
||||
from .celery import worker as celery_app
|
||||
|
||||
__all__ = ('celery_app',)
|
||||
|
18
app/app/celery.py
Normal file
18
app/app/celery.py
Normal file
@ -0,0 +1,18 @@
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from celery import Celery
|
||||
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')
|
||||
|
||||
worker = Celery('app')
|
||||
|
||||
worker.config_from_object(f'django.conf:settings', namespace='CELERY')
|
||||
|
||||
worker.autodiscover_tasks()
|
||||
|
||||
|
||||
@worker.task(bind=True, ignore_result=True)
|
||||
def debug_task(self):
|
||||
print(f'Request: {self!r}')
|
@ -5,6 +5,8 @@ from app.urls import urlpatterns
|
||||
from django.conf import settings
|
||||
from django.urls import URLPattern, URLResolver
|
||||
|
||||
from access.models import Organization
|
||||
|
||||
from settings.models.user_settings import UserSettings
|
||||
|
||||
|
||||
@ -72,6 +74,23 @@ def user_settings(context) -> int:
|
||||
return None
|
||||
|
||||
|
||||
def user_default_organization(context) -> int:
|
||||
""" Provides the users default organization.
|
||||
|
||||
Returns:
|
||||
int: Users Default Organization
|
||||
"""
|
||||
if context.user.is_authenticated:
|
||||
|
||||
settings = UserSettings.objects.filter(user=context.user)
|
||||
|
||||
if settings[0].default_organization:
|
||||
|
||||
return settings[0].default_organization.id
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def nav_items(context) -> list(dict()):
|
||||
""" Fetch All Project URLs
|
||||
|
||||
@ -88,7 +107,7 @@ def nav_items(context) -> list(dict()):
|
||||
is_active: {bool} if this link is the active URL
|
||||
|
||||
Returns:
|
||||
_type_: _description_
|
||||
list: Items user has view access to
|
||||
"""
|
||||
|
||||
dnav = []
|
||||
@ -142,11 +161,45 @@ def nav_items(context) -> list(dict()):
|
||||
|
||||
name = str(pattern.name)
|
||||
|
||||
nav_items = nav_items + [ {
|
||||
'name': name,
|
||||
'url': url,
|
||||
'is_active': is_active
|
||||
} ]
|
||||
if hasattr(pattern.callback.view_class, 'permission_required'):
|
||||
|
||||
permissions_required = pattern.callback.view_class.permission_required
|
||||
|
||||
user_has_perm = False
|
||||
|
||||
if type(permissions_required) is list:
|
||||
|
||||
user_has_perm = context.user.has_perms(permissions_required)
|
||||
|
||||
else:
|
||||
|
||||
user_has_perm = context.user.has_perm(permissions_required)
|
||||
|
||||
if hasattr(pattern.callback.view_class, 'model'):
|
||||
|
||||
if pattern.callback.view_class.model is Organization and context.user.is_authenticated:
|
||||
|
||||
organizations = Organization.objects.filter(manager = context.user)
|
||||
|
||||
if len(organizations) > 0:
|
||||
|
||||
user_has_perm = True
|
||||
|
||||
if str(nav_group.app_name).lower() == 'settings':
|
||||
|
||||
user_has_perm = True
|
||||
|
||||
if context.user.is_superuser:
|
||||
|
||||
user_has_perm = True
|
||||
|
||||
if user_has_perm:
|
||||
|
||||
nav_items = nav_items + [ {
|
||||
'name': name,
|
||||
'url': url,
|
||||
'is_active': is_active
|
||||
} ]
|
||||
|
||||
if len(nav_items) > 0:
|
||||
|
||||
@ -167,4 +220,5 @@ def common(context):
|
||||
'nav_items': nav_items(context),
|
||||
'social_backends': social_backends(context),
|
||||
'user_settings': user_settings(context),
|
||||
'user_default_organization': user_default_organization(context)
|
||||
}
|
||||
|
@ -24,14 +24,61 @@ SETTINGS_DIR = '/etc/itsm' # Primary Settings Directory
|
||||
BUILD_REPO = os.getenv('CI_PROJECT_URL')
|
||||
BUILD_SHA = os.getenv('CI_COMMIT_SHA')
|
||||
BUILD_VERSION = os.getenv('CI_COMMIT_TAG')
|
||||
DOCS_ROOT = 'https://nofusscomputing.com/projects/django-template/user/'
|
||||
DOCS_ROOT = 'https://nofusscomputing.com/projects/centurion_erp/user/'
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
||||
|
||||
|
||||
# Celery settings
|
||||
|
||||
CELERY_ACCEPT_CONTENT = ['json']
|
||||
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True # broker_connection_retry_on_startup
|
||||
CELERY_BROKER_URL = 'amqp://guest:guest@172.16.10.102:30712/itsm'
|
||||
|
||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#broker-use-ssl
|
||||
# import ssl
|
||||
# broker_use_ssl = {
|
||||
# 'keyfile': '/var/ssl/private/worker-key.pem',
|
||||
# 'certfile': '/var/ssl/amqp-server-cert.pem',
|
||||
# 'ca_certs': '/var/ssl/myca.pem',
|
||||
# 'cert_reqs': ssl.CERT_REQUIRED
|
||||
# }
|
||||
|
||||
CELERY_BROKER_POOL_LIMIT = 3 # broker_pool_limit
|
||||
CELERY_CACHE_BACKEND = 'django-cache'
|
||||
CELERY_ENABLE_UTC = True
|
||||
|
||||
CELERY_RESULT_BACKEND = 'django-db'
|
||||
CELERY_RESULT_EXTENDED = True
|
||||
CELERY_TASK_SERIALIZER = 'json'
|
||||
|
||||
CELERY_TIMEZONE = 'UTC'
|
||||
CELERY_TASK_DEFAULT_EXCHANGE = 'ITSM' # task_default_exchange
|
||||
CELERY_TASK_DEFAULT_PRIORITY = 10 # 1-10=LOW-HIGH task_default_priority
|
||||
# CELERY_TASK_DEFAULT_QUEUE = 'background'
|
||||
CELERY_TASK_TIME_LIMIT = 3600 # task_time_limit
|
||||
CELERY_TASK_TRACK_STARTED = True # task_track_started
|
||||
|
||||
# dont set concurrency for docer as it defaults to CPU count
|
||||
CELERY_WORKER_CONCURRENCY = 2 # worker_concurrency - Default: Number of CPU cores
|
||||
CELERY_WORKER_DEDUPLICATE_SUCCESSFUL_TASKS = True # worker_deduplicate_successful_tasks
|
||||
CELERY_WORKER_MAX_TASKS_PER_CHILD = 1 # worker_max_tasks_per_child
|
||||
# CELERY_WORKER_MAX_MEMORY_PER_CHILD = 10000 # 10000=10mb worker_max_memory_per_child - Default: No limit. Type: int (kilobytes)
|
||||
# CELERY_TASK_SEND_SENT_EVENT = True
|
||||
CELERY_WORKER_SEND_TASK_EVENTS = True # worker_send_task_events
|
||||
|
||||
# django setting.
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
|
||||
'LOCATION': 'my_cache_table',
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# Defaults
|
||||
#
|
||||
|
||||
ALLOWED_HOSTS = [ '*' ] # Site host to serve
|
||||
DEBUG = False # SECURITY WARNING: don't run with debug turned on in production!
|
||||
SITE_URL = 'http://127.0.0.1' # domain with HTTP method for the sites URL
|
||||
@ -61,11 +108,13 @@ INSTALLED_APPS = [
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'rest_framework_json_api',
|
||||
'rest_framework.authtoken',
|
||||
'social_django',
|
||||
'django_celery_results',
|
||||
'core.apps.CoreConfig',
|
||||
'access.apps.AccessConfig',
|
||||
'itam.apps.ItamConfig',
|
||||
'itim.apps.ItimConfig',
|
||||
'assistance.apps.AssistanceConfig',
|
||||
'settings.apps.SettingsConfig',
|
||||
'drf_spectacular',
|
||||
'drf_spectacular_sidecar',
|
||||
@ -170,7 +219,7 @@ STATICFILES_DIRS = [
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
SITE_TITLE = "Site Title"
|
||||
SITE_TITLE = "Centurion ERP"
|
||||
|
||||
|
||||
API_ENABLED = True
|
||||
@ -188,7 +237,7 @@ if API_ENABLED:
|
||||
'rest_framework.permissions.IsAuthenticated',
|
||||
),
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework.authentication.TokenAuthentication',
|
||||
'api.auth.TokenAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
],
|
||||
'DEFAULT_PAGINATION_CLASS':
|
||||
@ -228,11 +277,15 @@ if API_ENABLED:
|
||||
|
||||
## Authentication
|
||||
|
||||
Authentication with the api is via Token. The token is placed in header `Authorization` with a value of `Token <Your Token>`.
|
||||
Access to the API is restricted and requires authentication. Available authentication methods are:
|
||||
|
||||
## Token Generation
|
||||
- Session
|
||||
- Token
|
||||
|
||||
To generate a token, run `python3 manage.py drf_create_token <username>` from the CLI.
|
||||
Session authentication is made available after logging into the application via the login interface.
|
||||
|
||||
Token authentication is via an API token that a user will generate within their
|
||||
[settings panel](https://nofusscomputing.com/projects/django-template/user/user_settings/#api-tokens).
|
||||
|
||||
## Examples
|
||||
|
||||
@ -256,6 +309,9 @@ curl:
|
||||
'SWAGGER_UI_DIST': 'SIDECAR',
|
||||
'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
|
||||
'REDOC_DIST': 'SIDECAR',
|
||||
'PREPROCESSING_HOOKS': [
|
||||
'drf_spectacular.hooks.preprocess_exclude_path_format'
|
||||
],
|
||||
}
|
||||
|
||||
DATETIME_FORMAT = 'j N Y H:i:s'
|
||||
@ -303,7 +359,6 @@ if DEBUG:
|
||||
|
||||
# Apps Under Development
|
||||
INSTALLED_APPS += [
|
||||
'information.apps.InformationConfig',
|
||||
'project_management.apps.ProjectManagementConfig',
|
||||
]
|
||||
|
||||
|
@ -118,6 +118,16 @@ class ModelPermissionsAdd:
|
||||
add_data: dict = None
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="ToDO: write test")
|
||||
def test_model_requires_attribute_parent_model(self):
|
||||
""" Child model requires 'django view' attribute 'parent_model'
|
||||
|
||||
When a child-model is added the parent model is required so that the organization can be detrmined.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def test_model_add_user_anon_denied(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
|
101
app/app/tests/abstract/models.py
Normal file
101
app/app/tests/abstract/models.py
Normal file
@ -0,0 +1,101 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from access.models import TenancyObject
|
||||
from access.tests.abstract.tenancy_object import TenancyObject as TenancyObjectTestCases
|
||||
|
||||
from app.tests.abstract.views import AddView, ChangeView, DeleteView, DisplayView, IndexView
|
||||
|
||||
from core.mixin.history_save import SaveHistory
|
||||
from core.tests.abstract.models import Models
|
||||
|
||||
|
||||
|
||||
class BaseModel:
|
||||
""" Test cases for all models """
|
||||
|
||||
model = None
|
||||
""" Model to test """
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="figure out how to test sub-sub-class")
|
||||
def test_class_inherits_save_history(self):
|
||||
""" Confirm class inheritence
|
||||
|
||||
TenancyObject must inherit SaveHistory
|
||||
"""
|
||||
|
||||
assert issubclass(self.model, TenancyObject)
|
||||
|
||||
|
||||
|
||||
class TenancyModel(
|
||||
BaseModel,
|
||||
TenancyObjectTestCases,
|
||||
Models
|
||||
):
|
||||
""" Test cases for tenancy models"""
|
||||
|
||||
model = None
|
||||
""" Model to test """
|
||||
|
||||
|
||||
|
||||
class ModelAdd(
|
||||
AddView
|
||||
):
|
||||
""" Unit Tests for Model Add """
|
||||
|
||||
|
||||
|
||||
class ModelChange(
|
||||
ChangeView
|
||||
):
|
||||
""" Unit Tests for Model Change """
|
||||
|
||||
|
||||
|
||||
class ModelDelete(
|
||||
DeleteView
|
||||
):
|
||||
""" Unit Tests for Model delete """
|
||||
|
||||
|
||||
|
||||
class ModelDisplay(
|
||||
DisplayView
|
||||
):
|
||||
""" Unit Tests for Model display """
|
||||
|
||||
|
||||
|
||||
class ModelIndex(
|
||||
IndexView
|
||||
):
|
||||
""" Unit Tests for Model index """
|
||||
|
||||
|
||||
|
||||
class ModelCommon(
|
||||
ModelAdd,
|
||||
ModelChange,
|
||||
ModelDelete,
|
||||
ModelDisplay
|
||||
):
|
||||
""" Unit Tests for all models """
|
||||
|
||||
|
||||
|
||||
class PrimaryModel(
|
||||
ModelCommon,
|
||||
ModelIndex
|
||||
):
|
||||
""" Tests for Primary Models
|
||||
|
||||
A Primary model is a model that is deemed a model that has the following views:
|
||||
- Add
|
||||
- Change
|
||||
- Delete
|
||||
- Display
|
||||
- Index
|
||||
"""
|
595
app/app/tests/abstract/views.py
Normal file
595
app/app/tests/abstract/views.py
Normal file
@ -0,0 +1,595 @@
|
||||
import inspect
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
|
||||
|
||||
class AddView:
|
||||
""" Testing of Display view """
|
||||
|
||||
add_module: str = None
|
||||
""" Full module path to test """
|
||||
|
||||
add_view: str = None
|
||||
""" View Class name to test """
|
||||
|
||||
|
||||
def test_view_add_attribute_not_exists_fields(self):
|
||||
""" Attribute does not exists test
|
||||
|
||||
Ensure that `fields` attribute is not defined as the expectation is that a form will be used.
|
||||
"""
|
||||
|
||||
module = __import__(self.add_module, fromlist=[self.add_view])
|
||||
|
||||
assert hasattr(module, self.add_view)
|
||||
|
||||
viewclass = getattr(module, self.add_view)
|
||||
|
||||
assert viewclass.fields is None
|
||||
|
||||
|
||||
def test_view_add_attribute_exists_form_class(self):
|
||||
""" Attribute exists test
|
||||
|
||||
Ensure that `form_class` attribute is defined as it's required.
|
||||
"""
|
||||
|
||||
module = __import__(self.add_module, fromlist=[self.add_view])
|
||||
|
||||
assert hasattr(module, self.add_view)
|
||||
|
||||
viewclass = getattr(module, self.add_view)
|
||||
|
||||
assert hasattr(viewclass, 'form_class')
|
||||
|
||||
|
||||
def test_view_add_attribute_type_form_class(self):
|
||||
""" Attribute Type Test
|
||||
|
||||
Ensure that `form_class` attribute is a class.
|
||||
"""
|
||||
|
||||
module = __import__(self.add_module, fromlist=[self.add_view])
|
||||
|
||||
assert hasattr(module, self.add_view)
|
||||
|
||||
viewclass = getattr(module, self.add_view)
|
||||
|
||||
assert inspect.isclass(viewclass.form_class)
|
||||
|
||||
|
||||
def test_view_add_attribute_exists_model(self):
|
||||
""" Attribute exists test
|
||||
|
||||
Ensure that `model` attribute is defined as it's required .
|
||||
"""
|
||||
|
||||
module = __import__(self.add_module, fromlist=[self.add_view])
|
||||
|
||||
assert hasattr(module, self.add_view)
|
||||
|
||||
viewclass = getattr(module, self.add_view)
|
||||
|
||||
assert hasattr(viewclass, 'model')
|
||||
|
||||
|
||||
def test_view_add_attribute_exists_permission_required(self):
|
||||
""" Attribute exists test
|
||||
|
||||
Ensure that `permission_required` attribute is defined as it's required.
|
||||
"""
|
||||
|
||||
module = __import__(self.add_module, fromlist=[self.add_view])
|
||||
|
||||
assert hasattr(module, self.add_view)
|
||||
|
||||
viewclass = getattr(module, self.add_view)
|
||||
|
||||
assert hasattr(viewclass, 'permission_required')
|
||||
|
||||
|
||||
def test_view_add_attribute_type_permission_required(self):
|
||||
""" Attribute Type Test
|
||||
|
||||
Ensure that `permission_required` attribute is a list
|
||||
"""
|
||||
|
||||
module = __import__(self.add_module, fromlist=[self.add_view])
|
||||
|
||||
assert hasattr(module, self.add_view)
|
||||
|
||||
viewclass = getattr(module, self.add_view)
|
||||
|
||||
assert type(viewclass.permission_required) is list
|
||||
|
||||
|
||||
def test_view_add_attribute_exists_template_name(self):
|
||||
""" Attribute exists test
|
||||
|
||||
Ensure that `template_name` attribute is defined as it's required.
|
||||
"""
|
||||
|
||||
module = __import__(self.add_module, fromlist=[self.add_view])
|
||||
|
||||
assert hasattr(module, self.add_view)
|
||||
|
||||
viewclass = getattr(module, self.add_view)
|
||||
|
||||
assert hasattr(viewclass, 'template_name')
|
||||
|
||||
|
||||
def test_view_add_attribute_type_template_name(self):
|
||||
""" Attribute Type Test
|
||||
|
||||
Ensure that `template_name` attribute is a string.
|
||||
"""
|
||||
|
||||
module = __import__(self.add_module, fromlist=[self.add_view])
|
||||
|
||||
assert hasattr(module, self.add_view)
|
||||
|
||||
viewclass = getattr(module, self.add_view)
|
||||
|
||||
assert type(viewclass.template_name) is str
|
||||
|
||||
|
||||
|
||||
class ChangeView:
|
||||
""" Testing of Display view """
|
||||
|
||||
change_module: str = None
|
||||
""" Full module path to test """
|
||||
|
||||
change_view: str = None
|
||||
""" Change Class name to test """
|
||||
|
||||
|
||||
def test_view_change_attribute_not_exists_fields(self):
|
||||
""" Attribute does not exists test
|
||||
|
||||
Ensure that `fields` attribute is not defined as the expectation is that a form will be used.
|
||||
"""
|
||||
|
||||
module = __import__(self.change_module, fromlist=[self.change_view])
|
||||
|
||||
assert hasattr(module, self.change_view)
|
||||
|
||||
viewclass = getattr(module, self.change_view)
|
||||
|
||||
assert viewclass.fields is None
|
||||
|
||||
|
||||
def test_view_change_attribute_exists_form_class(self):
|
||||
""" Attribute exists test
|
||||
|
||||
Ensure that `form_class` attribute is defined as it's required.
|
||||
"""
|
||||
|
||||
module = __import__(self.change_module, fromlist=[self.change_view])
|
||||
|
||||
assert hasattr(module, self.change_view)
|
||||
|
||||
viewclass = getattr(module, self.change_view)
|
||||
|
||||
assert hasattr(viewclass, 'form_class')
|
||||
|
||||
|
||||
def test_view_change_attribute_type_form_class(self):
|
||||
""" Attribute Type Test
|
||||
|
||||
Ensure that `form_class` attribute is a string.
|
||||
"""
|
||||
|
||||
module = __import__(self.change_module, fromlist=[self.change_view])
|
||||
|
||||
assert hasattr(module, self.change_view)
|
||||
|
||||
viewclass = getattr(module, self.change_view)
|
||||
|
||||
assert inspect.isclass(viewclass.form_class)
|
||||
|
||||
|
||||
def test_view_change_attribute_exists_model(self):
|
||||
""" Attribute exists test
|
||||
|
||||
Ensure that `model` attribute is defined as it's required .
|
||||
"""
|
||||
|
||||
module = __import__(self.change_module, fromlist=[self.change_view])
|
||||
|
||||
assert hasattr(module, self.change_view)
|
||||
|
||||
viewclass = getattr(module, self.change_view)
|
||||
|
||||
assert hasattr(viewclass, 'model')
|
||||
|
||||
|
||||
def test_view_change_attribute_exists_permission_required(self):
|
||||
""" Attribute exists test
|
||||
|
||||
Ensure that `permission_required` attribute is defined as it's required.
|
||||
"""
|
||||
|
||||
module = __import__(self.change_module, fromlist=[self.change_view])
|
||||
|
||||
assert hasattr(module, self.change_view)
|
||||
|
||||
viewclass = getattr(module, self.change_view)
|
||||
|
||||
assert hasattr(viewclass, 'permission_required')
|
||||
|
||||
|
||||
def test_view_change_attribute_type_permission_required(self):
|
||||
""" Attribute Type Test
|
||||
|
||||
Ensure that `permission_required` attribute is a list
|
||||
"""
|
||||
|
||||
module = __import__(self.change_module, fromlist=[self.change_view])
|
||||
|
||||
assert hasattr(module, self.change_view)
|
||||
|
||||
viewclass = getattr(module, self.change_view)
|
||||
|
||||
assert type(viewclass.permission_required) is list
|
||||
|
||||
|
||||
def test_view_change_attribute_exists_template_name(self):
|
||||
""" Attribute exists test
|
||||
|
||||
Ensure that `template_name` attribute is defined as it's required.
|
||||
"""
|
||||
|
||||
module = __import__(self.change_module, fromlist=[self.change_view])
|
||||
|
||||
assert hasattr(module, self.change_view)
|
||||
|
||||
viewclass = getattr(module, self.change_view)
|
||||
|
||||
assert hasattr(viewclass, 'template_name')
|
||||
|
||||
|
||||
def test_view_change_attribute_type_template_name(self):
|
||||
""" Attribute Type Test
|
||||
|
||||
Ensure that `template_name` attribute is a string.
|
||||
"""
|
||||
|
||||
module = __import__(self.change_module, fromlist=[self.change_view])
|
||||
|
||||
assert hasattr(module, self.change_view)
|
||||
|
||||
viewclass = getattr(module, self.change_view)
|
||||
|
||||
assert type(viewclass.template_name) is str
|
||||
|
||||
|
||||
|
||||
class DeleteView:
|
||||
""" Testing of Display view """
|
||||
|
||||
delete_module: str = None
|
||||
""" Full module path to test """
|
||||
|
||||
delete_view: str = None
|
||||
""" Delete Class name to test """
|
||||
|
||||
|
||||
def test_view_delete_attribute_exists_model(self):
|
||||
""" Attribute exists test
|
||||
|
||||
Ensure that `model` attribute is defined as it's required .
|
||||
"""
|
||||
|
||||
module = __import__(self.delete_module, fromlist=[self.delete_view])
|
||||
|
||||
assert hasattr(module, self.delete_view)
|
||||
|
||||
viewclass = getattr(module, self.delete_view)
|
||||
|
||||
assert hasattr(viewclass, 'model')
|
||||
|
||||
|
||||
def test_view_delete_attribute_exists_permission_required(self):
|
||||
""" Attribute exists test
|
||||
|
||||
Ensure that `model` attribute is defined as it's required .
|
||||
"""
|
||||
|
||||
module = __import__(self.delete_module, fromlist=[self.delete_view])
|
||||
|
||||
assert hasattr(module, self.delete_view)
|
||||
|
||||
viewclass = getattr(module, self.delete_view)
|
||||
|
||||
assert hasattr(viewclass, 'permission_required')
|
||||
|
||||
|
||||
def test_view_delete_attribute_type_permission_required(self):
|
||||
""" Attribute Type Test
|
||||
|
||||
Ensure that `permission_required` attribute is a list
|
||||
"""
|
||||
|
||||
module = __import__(self.delete_module, fromlist=[self.delete_view])
|
||||
|
||||
assert hasattr(module, self.delete_view)
|
||||
|
||||
viewclass = getattr(module, self.delete_view)
|
||||
|
||||
assert type(viewclass.permission_required) is list
|
||||
|
||||
|
||||
def test_view_delete_attribute_exists_template_name(self):
|
||||
""" Attribute exists test
|
||||
|
||||
Ensure that `template_name` attribute is defined as it's required.
|
||||
"""
|
||||
|
||||
module = __import__(self.delete_module, fromlist=[self.delete_view])
|
||||
|
||||
assert hasattr(module, self.delete_view)
|
||||
|
||||
viewclass = getattr(module, self.delete_view)
|
||||
|
||||
assert hasattr(viewclass, 'template_name')
|
||||
|
||||
|
||||
def test_view_delete_attribute_type_template_name(self):
|
||||
""" Attribute Type Test
|
||||
|
||||
Ensure that `template_name` attribute is a string.
|
||||
"""
|
||||
|
||||
module = __import__(self.delete_module, fromlist=[self.delete_view])
|
||||
|
||||
assert hasattr(module, self.delete_view)
|
||||
|
||||
viewclass = getattr(module, self.delete_view)
|
||||
|
||||
assert type(viewclass.template_name) is str
|
||||
|
||||
|
||||
|
||||
class DisplayView:
|
||||
""" Testing of Display view """
|
||||
|
||||
display_module: str = None
|
||||
""" Full module path to test """
|
||||
|
||||
display_view: str = None
|
||||
""" Change Class name to test """
|
||||
|
||||
|
||||
def test_view_display_attribute_exists_model(self):
|
||||
""" Attribute exists test
|
||||
|
||||
Ensure that `model` attribute is defined as it's required .
|
||||
"""
|
||||
|
||||
module = __import__(self.display_module, fromlist=[self.display_view])
|
||||
|
||||
assert hasattr(module, self.display_view)
|
||||
|
||||
viewclass = getattr(module, self.display_view)
|
||||
|
||||
assert hasattr(viewclass, 'model')
|
||||
|
||||
|
||||
def test_view_display_attribute_exists_permission_required(self):
|
||||
""" Attribute exists test
|
||||
|
||||
Ensure that `permission_required` attribute is defined as it's required.
|
||||
"""
|
||||
|
||||
module = __import__(self.display_module, fromlist=[self.display_view])
|
||||
|
||||
assert hasattr(module, self.display_view)
|
||||
|
||||
viewclass = getattr(module, self.display_view)
|
||||
|
||||
assert hasattr(viewclass, 'permission_required')
|
||||
|
||||
|
||||
def test_view_display_attribute_type_permission_required(self):
|
||||
""" Attribute Type Test
|
||||
|
||||
Ensure that `permission_required` attribute is a list
|
||||
"""
|
||||
|
||||
module = __import__(self.display_module, fromlist=[self.display_view])
|
||||
|
||||
assert hasattr(module, self.display_view)
|
||||
|
||||
viewclass = getattr(module, self.display_view)
|
||||
|
||||
assert type(viewclass.permission_required) is list
|
||||
|
||||
|
||||
def test_view_display_attribute_exists_template_name(self):
|
||||
""" Attribute exists test
|
||||
|
||||
Ensure that `template_name` attribute is defined as it's required.
|
||||
"""
|
||||
|
||||
module = __import__(self.display_module, fromlist=[self.display_view])
|
||||
|
||||
assert hasattr(module, self.display_view)
|
||||
|
||||
viewclass = getattr(module, self.display_view)
|
||||
|
||||
assert hasattr(viewclass, 'template_name')
|
||||
|
||||
|
||||
def test_view_display_attribute_type_template_name(self):
|
||||
""" Attribute Type Test
|
||||
|
||||
Ensure that `template_name` attribute is a string.
|
||||
"""
|
||||
|
||||
module = __import__(self.display_module, fromlist=[self.display_view])
|
||||
|
||||
assert hasattr(module, self.display_view)
|
||||
|
||||
viewclass = getattr(module, self.display_view)
|
||||
|
||||
assert type(viewclass.template_name) is str
|
||||
|
||||
|
||||
|
||||
class IndexView:
|
||||
""" Testing of Display view """
|
||||
|
||||
index_module: str = None
|
||||
""" Full module path to test """
|
||||
|
||||
index_view: str = None
|
||||
""" Index Class name to test """
|
||||
|
||||
|
||||
def test_view_index_attribute_exists_model(self):
|
||||
""" Attribute exists test
|
||||
|
||||
Ensure that `model` attribute is defined as it's required .
|
||||
"""
|
||||
|
||||
module = __import__(self.index_module, fromlist=[self.index_view])
|
||||
|
||||
assert hasattr(module, self.index_view)
|
||||
|
||||
viewclass = getattr(module, self.index_view)
|
||||
|
||||
assert hasattr(viewclass, 'model')
|
||||
|
||||
|
||||
def test_view_index_attribute_exists_permission_required(self):
|
||||
""" Attribute exists test
|
||||
|
||||
Ensure that `model` attribute is defined as it's required .
|
||||
"""
|
||||
|
||||
module = __import__(self.index_module, fromlist=[self.index_view])
|
||||
|
||||
assert hasattr(module, self.index_view)
|
||||
|
||||
viewclass = getattr(module, self.index_view)
|
||||
|
||||
assert hasattr(viewclass, 'permission_required')
|
||||
|
||||
|
||||
def test_view_index_attribute_type_permission_required(self):
|
||||
""" Attribute Type Test
|
||||
|
||||
Ensure that `permission_required` attribute is a list
|
||||
"""
|
||||
|
||||
module = __import__(self.index_module, fromlist=[self.index_view])
|
||||
|
||||
assert hasattr(module, self.index_view)
|
||||
|
||||
viewclass = getattr(module, self.index_view)
|
||||
|
||||
assert type(viewclass.permission_required) is list
|
||||
|
||||
|
||||
def test_view_index_attribute_exists_template_name(self):
|
||||
""" Attribute exists test
|
||||
|
||||
Ensure that `template_name` attribute is defined as it's required.
|
||||
"""
|
||||
|
||||
module = __import__(self.index_module, fromlist=[self.index_view])
|
||||
|
||||
assert hasattr(module, self.index_view)
|
||||
|
||||
viewclass = getattr(module, self.index_view)
|
||||
|
||||
assert hasattr(viewclass, 'template_name')
|
||||
|
||||
|
||||
def test_view_index_attribute_type_template_name(self):
|
||||
""" Attribute Type Test
|
||||
|
||||
Ensure that `template_name` attribute is a string.
|
||||
"""
|
||||
|
||||
module = __import__(self.index_module, fromlist=[self.index_view])
|
||||
|
||||
assert hasattr(module, self.index_view)
|
||||
|
||||
viewclass = getattr(module, self.index_view)
|
||||
|
||||
assert type(viewclass.template_name) is str
|
||||
|
||||
|
||||
|
||||
class AllViews(
|
||||
AddView,
|
||||
ChangeView,
|
||||
DeleteView,
|
||||
DisplayView,
|
||||
IndexView
|
||||
):
|
||||
""" Abstract test class containing ALL view tests """
|
||||
|
||||
add_module: str = None
|
||||
""" Full module path to test """
|
||||
|
||||
add_view: str = None
|
||||
""" View Class name to test """
|
||||
|
||||
change_module: str = None
|
||||
""" Full module path to test """
|
||||
|
||||
change_view: str = None
|
||||
""" Change Class name to test """
|
||||
|
||||
delete_module: str = None
|
||||
""" Full module path to test """
|
||||
|
||||
delete_view: str = None
|
||||
""" Delete Class name to test """
|
||||
|
||||
display_module: str = None
|
||||
""" Full module path to test """
|
||||
|
||||
display_view: str = None
|
||||
""" Change Class name to test """
|
||||
|
||||
index_module: str = None
|
||||
""" Full module path to test """
|
||||
|
||||
index_view: str = None
|
||||
""" Index Class name to test """
|
||||
|
||||
|
||||
@pytest.mark.skip(reason='write test')
|
||||
def test_view_index_attribute_missing_permission_required(self):
|
||||
""" Attribute missing Test
|
||||
|
||||
Ensure that `permission_required` attribute is not defined within the view.
|
||||
|
||||
this can be done by mocking the inherited class with the `permission_required` attribute
|
||||
set to a value that if it changed would be considered defined in the created view.
|
||||
|
||||
## Why?
|
||||
|
||||
This attribute can be dynamically added based of of the view name along with attributes
|
||||
`model._meta.model_name` and `str(__class__.__name__).lower()`.
|
||||
|
||||
Additional test:
|
||||
- ensure that the attribute does get automagically created.
|
||||
- ensure that the classes name is one of add, change, delete, display or index.
|
||||
"""
|
||||
|
||||
|
||||
@pytest.mark.skip(reason='write test')
|
||||
def test_view_index_attribute_missing_template_name(self):
|
||||
""" Attribute missing Test
|
||||
|
||||
Ensure that `template_name` attribute is not defined within the view if the value
|
||||
is `form.html.j2`
|
||||
|
||||
this valuse is already defined in the base form
|
||||
"""
|
86
app/app/tests/ui/conftest.py
Normal file
86
app/app/tests/ui/conftest.py
Normal file
@ -0,0 +1,86 @@
|
||||
from app.urls import urlpatterns
|
||||
|
||||
class Data:
|
||||
|
||||
|
||||
def parse_urls(self, patterns, parent_route = None) -> list:
|
||||
|
||||
urls = []
|
||||
|
||||
root_paths = [
|
||||
'access',
|
||||
# 'account',
|
||||
# 'api',
|
||||
'config_management',
|
||||
'history',
|
||||
'itam',
|
||||
'organization',
|
||||
'settings'
|
||||
]
|
||||
|
||||
for url in patterns:
|
||||
|
||||
if hasattr(url, 'pattern'):
|
||||
|
||||
route = None
|
||||
|
||||
if hasattr(url.pattern, '_route'):
|
||||
|
||||
if parent_route:
|
||||
|
||||
route = parent_route + url.pattern._route
|
||||
|
||||
route = str(route).replace('<int:device_id>', '1')
|
||||
route = str(route).replace('<int:group_id>', '1')
|
||||
route = str(route).replace('<int:operating_system_id>', '1')
|
||||
route = str(route).replace('<int:organization_id>', '1')
|
||||
route = str(route).replace('<int:pk>', '1')
|
||||
route = str(route).replace('<int:software_id>', '1')
|
||||
route = str(route).replace('<int:team_id>', '1')
|
||||
|
||||
if route != '' and route not in urls:
|
||||
|
||||
urls += [ route ]
|
||||
|
||||
else:
|
||||
|
||||
route = url.pattern._route
|
||||
|
||||
route = str(route).replace('<int:device_id>', '1')
|
||||
route = str(route).replace('<int:group_id>', '1')
|
||||
route = str(route).replace('<int:operating_system_id>', '1')
|
||||
route = str(route).replace('<int:organization_id>', '1')
|
||||
route = str(route).replace('<int:pk>', '1')
|
||||
route = str(route).replace('<int:software_id>', '1')
|
||||
route = str(route).replace('<int:team_id>', '1')
|
||||
|
||||
if str(url.pattern._route).replace('/', '') in root_paths:
|
||||
|
||||
if route != '' and route not in urls:
|
||||
|
||||
urls += [ route ]
|
||||
|
||||
if hasattr(url, 'url_patterns'):
|
||||
|
||||
if str(url.pattern._route).replace('/', '') in root_paths:
|
||||
|
||||
urls += self.parse_urls(patterns=url.url_patterns, parent_route=url.pattern._route)
|
||||
|
||||
return urls
|
||||
|
||||
|
||||
def __init__(self):
|
||||
|
||||
urls = []
|
||||
|
||||
patterns = urlpatterns
|
||||
|
||||
urls_found = self.parse_urls(patterns=patterns)
|
||||
|
||||
for url in urls_found:
|
||||
|
||||
if url not in urls:
|
||||
|
||||
urls += [ url ]
|
||||
|
||||
self.urls = urls
|
141
app/app/tests/ui/test_page_links.py
Normal file
141
app/app/tests/ui/test_page_links.py
Normal file
@ -0,0 +1,141 @@
|
||||
import pytest
|
||||
import re
|
||||
import requests
|
||||
import unittest
|
||||
|
||||
from django.test import LiveServerTestCase
|
||||
|
||||
from app.urls import urlpatterns
|
||||
|
||||
from conftest import Data
|
||||
|
||||
@pytest.mark.skip(reason="test server required to be setup so tests work.")
|
||||
class TestRenderedTemplateLinks:
|
||||
"""UI Links tests """
|
||||
|
||||
server_host: str = '127.0.0.1'
|
||||
# server_host: str = '192.168.1.172'
|
||||
server_url: str = 'http://' + server_host + ':8002/'
|
||||
|
||||
|
||||
data = Data()
|
||||
|
||||
driver = None
|
||||
""" Chrome webdriver """
|
||||
|
||||
session = None
|
||||
""" Client session that is logged into the dejango site """
|
||||
|
||||
|
||||
def setup_class(self):
|
||||
""" Set up the test
|
||||
|
||||
1. fetch session cookie
|
||||
2. login to site
|
||||
3. save session for use in tests
|
||||
"""
|
||||
|
||||
self.session = requests.Session()
|
||||
|
||||
# fetch the csrf token
|
||||
self.session.get(
|
||||
url = self.server_url + 'account/login/',
|
||||
)
|
||||
|
||||
# login
|
||||
self.client = self.session.post(
|
||||
url = self.server_url + 'account/login/',
|
||||
data = {
|
||||
'username': 'admin',
|
||||
'password': 'admin',
|
||||
'csrfmiddlewaretoken': self.session.cookies._cookies[self.server_host]['/']['csrftoken'].value
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
argnames='url',
|
||||
argvalues=[link for link in data.urls],
|
||||
ids=[link for link in data.urls]
|
||||
)
|
||||
def test_ui_no_http_forbidden(self, url):
|
||||
""" Test Page Links
|
||||
|
||||
Scrape the page for links and ensure none return HTTP/403.
|
||||
|
||||
Test failure denotes a link on a page that should have been filtered out by testing for user
|
||||
permissions within the template.
|
||||
|
||||
Args:
|
||||
url (str): Page to test
|
||||
"""
|
||||
|
||||
response = self.session.get(
|
||||
url = str(self.server_url + url)
|
||||
)
|
||||
|
||||
# Failsafe to ensure no redirection and that page exists
|
||||
assert len(response.history) == 0
|
||||
assert response.status_code == 200
|
||||
|
||||
page_urls = []
|
||||
|
||||
page = str(response.content)
|
||||
|
||||
links = re.findall('href=\"([a-z\/0-9]+)\"', page)
|
||||
|
||||
for link in links:
|
||||
|
||||
page_link_response = self.session.get(
|
||||
url = str(self.server_url + link)
|
||||
)
|
||||
|
||||
# Failsafe to ensure no redirection
|
||||
assert len(response.history) == 0
|
||||
|
||||
assert page_link_response.status_code != 403
|
||||
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
argnames='url',
|
||||
argvalues=[link for link in data.urls],
|
||||
ids=[link for link in data.urls]
|
||||
)
|
||||
def test_ui_no_http_not_found(self, url):
|
||||
""" Test Page Links
|
||||
|
||||
Scrape the page for links and ensure none return HTTP/404.
|
||||
|
||||
Test failure denotes a link on a page that should not exist within the template.
|
||||
|
||||
Args:
|
||||
url (str): Page to test
|
||||
"""
|
||||
|
||||
response = self.session.get(
|
||||
url = str(self.server_url + url)
|
||||
)
|
||||
|
||||
# Failsafe to ensure no redirection and that page exists
|
||||
assert len(response.history) == 0
|
||||
assert response.status_code == 200
|
||||
|
||||
page_urls = []
|
||||
|
||||
page = str(response.content)
|
||||
|
||||
links = re.findall('href=\"([a-z\/0-9]+)\"', page)
|
||||
|
||||
for link in links:
|
||||
|
||||
page_link_response = self.session.get(
|
||||
url = str(self.server_url + link)
|
||||
)
|
||||
|
||||
# Failsafe to ensure no redirection
|
||||
assert len(response.history) == 0
|
||||
|
||||
assert page_link_response.status_code != 404
|
||||
|
0
app/app/tests/unit/__init__.py
Normal file
0
app/app/tests/unit/__init__.py
Normal file
@ -36,10 +36,15 @@ urlpatterns = [
|
||||
|
||||
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/settings/<int:pk>/edit', user_settings.Change.as_view(), name="_settings_user_change"),
|
||||
path('account/settings/<int:user_id>/token/add', user_settings.TokenAdd.as_view(), name="_user_auth_token_add"),
|
||||
path('account/settings/<int:user_id>/token/<int:pk>/delete', user_settings.TokenDelete.as_view(), name="_user_auth_token_delete"),
|
||||
path("account/", include("django.contrib.auth.urls")),
|
||||
|
||||
path("organization/", include("access.urls")),
|
||||
path("assistance/", include("assistance.urls")),
|
||||
path("itam/", include("itam.urls")),
|
||||
path("itim/", include("itim.urls")),
|
||||
path("config_management/", include("config_management.urls")),
|
||||
|
||||
path("history/<str:model_name>/<int:model_pk>", history.View.as_view(), name='_history'),
|
||||
@ -69,9 +74,6 @@ if settings.DEBUG:
|
||||
urlpatterns += [
|
||||
|
||||
path("__debug__/", include("debug_toolbar.urls"), name='_debug'),
|
||||
# Apps Under Development
|
||||
path("itim/", include("itim.urls")),
|
||||
path("information/", include("information.urls")),
|
||||
path("project_management/", include("project_management.urls")),
|
||||
]
|
||||
|
||||
|
0
app/assistance/__init__.py
Normal file
0
app/assistance/__init__.py
Normal file
@ -1,6 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class InformationConfig(AppConfig):
|
||||
class AssistanceConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'information'
|
||||
name = 'assistance'
|
65
app/assistance/forms/knowledge_base.py
Normal file
65
app/assistance/forms/knowledge_base.py
Normal file
@ -0,0 +1,65 @@
|
||||
|
||||
from django import forms
|
||||
from django.forms import ValidationError
|
||||
|
||||
from app import settings
|
||||
|
||||
from assistance.models.knowledge_base import KnowledgeBase
|
||||
|
||||
from core.forms.common import CommonModelForm
|
||||
|
||||
|
||||
|
||||
class KnowledgeBaseForm(CommonModelForm):
|
||||
|
||||
__name__ = 'asdsa'
|
||||
|
||||
class Meta:
|
||||
|
||||
fields = '__all__'
|
||||
|
||||
model = KnowledgeBase
|
||||
|
||||
prefix = 'knowledgebase'
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['expiry_date'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local', 'format': "%Y-%m-%dT%H:%M"})
|
||||
self.fields['expiry_date'].input_formats = settings.DATETIME_FORMAT
|
||||
self.fields['expiry_date'].format="%Y-%m-%dT%H:%M"
|
||||
|
||||
self.fields['release_date'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local', 'format': "%Y-%m-%dT%H:%M"})
|
||||
self.fields['release_date'].input_formats = settings.DATETIME_FORMAT
|
||||
self.fields['release_date'].format="%Y-%m-%dT%H:%M"
|
||||
|
||||
|
||||
def clean(self):
|
||||
|
||||
cleaned_data = super().clean()
|
||||
|
||||
responsible_user = cleaned_data.get("responsible_user")
|
||||
responsible_teams = cleaned_data.get("responsible_teams")
|
||||
|
||||
|
||||
if not responsible_user and not responsible_teams:
|
||||
|
||||
raise ValidationError('A Responsible User or Team must be assigned.')
|
||||
|
||||
|
||||
target_team = cleaned_data.get("target_team")
|
||||
target_user = cleaned_data.get("target_user")
|
||||
|
||||
|
||||
if not target_team and not target_user:
|
||||
|
||||
raise ValidationError('A Target Team or Target User must be assigned.')
|
||||
|
||||
|
||||
if target_team and target_user:
|
||||
|
||||
raise ValidationError('Both a Target Team or Target User Cant be assigned at the same time. Use one or the other')
|
||||
|
||||
|
||||
return cleaned_data
|
36
app/assistance/forms/knowledge_base_category.py
Normal file
36
app/assistance/forms/knowledge_base_category.py
Normal file
@ -0,0 +1,36 @@
|
||||
from django.forms import ValidationError
|
||||
|
||||
from assistance.models.knowledge_base import KnowledgeBaseCategory
|
||||
|
||||
from core.forms.common import CommonModelForm
|
||||
|
||||
|
||||
|
||||
class KnowledgeBaseCategoryForm(CommonModelForm):
|
||||
|
||||
__name__ = 'asdsa'
|
||||
|
||||
class Meta:
|
||||
|
||||
fields = '__all__'
|
||||
|
||||
model = KnowledgeBaseCategory
|
||||
|
||||
prefix = 'knowledgebase_category'
|
||||
|
||||
|
||||
def clean(self):
|
||||
|
||||
cleaned_data = super().clean()
|
||||
|
||||
target_team = cleaned_data.get("target_team")
|
||||
target_user = cleaned_data.get("target_user")
|
||||
|
||||
|
||||
if target_team and target_user:
|
||||
|
||||
raise ValidationError('Both a Target Team or Target User Cant be assigned at the same time. Use one or the other or None')
|
||||
|
||||
|
||||
return cleaned_data
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user