Compare commits
146 Commits
1.0.0-a1
...
8d071c68df
Author | SHA1 | Date | |
---|---|---|---|
8d071c68df | |||
3b1691ff62 | |||
a77c43d213 | |||
086959b431 | |||
3f117f9d83 | |||
6a23845a4f | |||
b9c6d04e04 | |||
32c0027ecf | |||
dae52e8646 | |||
890a5651a0 | |||
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 |
17
.cz.yaml
17
.cz.yaml
@ -1,8 +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: 1.0.0-a1
|
||||
version: 1.0.0-b14
|
||||
version_scheme: semver
|
||||
|
39
.github/pull_request_template.md
vendored
Normal file
39
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
### :books: Summary
|
||||
<!-- your summary here emojis ref: https://github.com/yodamad/gitlab-emoji -->
|
||||
|
||||
|
||||
|
||||
### :link: Links / References
|
||||
<!--
|
||||
|
||||
using a list as any links to other references or links as required. if relevant, describe the link/reference
|
||||
|
||||
Include any issues or related merge requests. Note: dependent MR's also to be added to "Merge request dependencies"
|
||||
|
||||
-->
|
||||
|
||||
|
||||
|
||||
### :construction_worker: Tasks
|
||||
|
||||
- [ ] Add your tasks here if required (delete)
|
||||
|
||||
<!-- dont remove tasks below strike through including the checkbox by enclosing in double tidle '~~' -->
|
||||
|
||||
- [ ] :firecracker: 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/)._
|
||||
|
||||
- [ ] :notebook: Release notes updated
|
||||
|
||||
- [ ] :blue_book: Documentation written
|
||||
|
||||
_All features to be documented within the correct section(s). Administration, Development and/or User_
|
||||
|
||||
- [ ] :checkered_flag: Milestone assigned
|
||||
|
||||
- [ ] :test_tube: [Unit Test(s) Written](https://nofusscomputing.com/projects/centurion_erp/development/testing/)
|
||||
|
||||
_ensure test coverage delta is not less than zero_
|
||||
|
||||
- [ ] :page_facing_up: Roadmap updated
|
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 }}
|
||||
|
442
.gitlab-ci.yml
442
.gitlab-ci.yml
@ -2,18 +2,21 @@
|
||||
|
||||
variables:
|
||||
MY_PROJECT_ID: "57560288"
|
||||
GIT_SYNC_URL: "https://$GITHUB_USERNAME_ROBOT:$GITHUB_TOKEN_ROBOT@github.com/NoFussComputing/centurion_erp.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: centurion-erp
|
||||
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: centurion-erp
|
||||
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/centurion_erp/
|
||||
@ -22,96 +25,101 @@ variables:
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
Update Git Submodules:
|
||||
extends: .ansible_playbook_git_submodule
|
||||
|
||||
|
||||
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;
|
||||
# Update Git Submodules:
|
||||
# extends: .ansible_playbook_git_submodule
|
||||
|
||||
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
|
||||
# Docker Container:
|
||||
# extends: .build_docker_container
|
||||
# resource_group: build
|
||||
# needs: []
|
||||
# script:
|
||||
# - update-binfmts --display
|
||||
# - |
|
||||
|
||||
- if:
|
||||
$CI_COMMIT_AUTHOR =='nfc_bot <helpdesk@nofusscomputing.com>'
|
||||
&&
|
||||
$CI_COMMIT_BRANCH == "development"
|
||||
when: never
|
||||
# echo "[DEBUG] building multiarch/specified arch image";
|
||||
|
||||
- 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
|
||||
# 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;
|
||||
|
||||
- if: $CI_COMMIT_TAG
|
||||
exists:
|
||||
- '{dockerfile,dockerfile.j2}'
|
||||
when: always
|
||||
# docker buildx imagetools inspect $DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG;
|
||||
|
||||
- if: # condition_dev_branch_push
|
||||
(
|
||||
$CI_COMMIT_BRANCH == "development"
|
||||
||
|
||||
$CI_COMMIT_BRANCH == "v-1-0-0-alpha"
|
||||
) &&
|
||||
$CI_PIPELINE_SOURCE == "push"
|
||||
exists:
|
||||
- '{dockerfile,dockerfile.j2}'
|
||||
allow_failure: true
|
||||
when: on_success
|
||||
# # 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}}");
|
||||
|
||||
- when: never
|
||||
# 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
|
||||
|
||||
|
||||
|
||||
@ -119,83 +127,107 @@ Docker Container:
|
||||
|
||||
|
||||
|
||||
.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
|
||||
- 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:
|
||||
- "$MY_COMMAND"
|
||||
- if [ "$CI_COMMIT_BRANCH" == "development" ] ; then RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout --prerelease alpha); 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] 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
|
||||
# .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: "$CI_COMMIT_AUTHOR =='nfc_bot <helpdesk@nofusscomputing.com>'"
|
||||
when: never
|
||||
# - |
|
||||
# if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then
|
||||
|
||||
- if: # condition_master_branch_push
|
||||
$CI_COMMIT_BRANCH == "master" &&
|
||||
$CI_PIPELINE_SOURCE == "push"
|
||||
allow_failure: false
|
||||
when: on_success
|
||||
# echo "[DEBUG] not running extra actions, no new version";
|
||||
|
||||
- if: # condition_dev_branch_push
|
||||
$CI_COMMIT_BRANCH == "development" &&
|
||||
$CI_PIPELINE_SOURCE == "push"
|
||||
when: always
|
||||
allow_failure: true
|
||||
# else
|
||||
|
||||
# for testing
|
||||
# - if: '$CI_COMMIT_BRANCH != "master"'
|
||||
# when: always
|
||||
# allow_failure: true
|
||||
- when: never
|
||||
# echo "[DEBUG] Creating new Version Label";
|
||||
|
||||
#
|
||||
# Release
|
||||
#
|
||||
Gitlab Release:
|
||||
extends:
|
||||
- .gitlab_release
|
||||
# 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
|
||||
|
||||
|
||||
|
||||
@ -204,63 +236,59 @@ 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
|
||||
# Docker.Hub.Branch.Publish:
|
||||
# extends: .publish-docker-hub
|
||||
# needs: [ "Docker Container" ]
|
||||
# resource_group: build
|
||||
# rules: # rules manually synced from docker/publish.gitlab-ci.yaml removing git tag
|
||||
|
||||
# - if: # condition_master_branch_push
|
||||
# $CI_COMMIT_BRANCH == "master" &&
|
||||
# $CI_PIPELINE_SOURCE == "push"
|
||||
# exists:
|
||||
# - '{dockerfile,dockerfile.j2}'
|
||||
# when: always
|
||||
# # - if: # condition_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_AUTHOR =='nfc_bot <helpdesk@nofusscomputing.com>'
|
||||
# &&
|
||||
# $CI_COMMIT_BRANCH == "development"
|
||||
# when: never
|
||||
|
||||
- if: $CI_COMMIT_TAG
|
||||
exists:
|
||||
- '{dockerfile,dockerfile.j2}'
|
||||
when: always
|
||||
# - if: $CI_COMMIT_TAG
|
||||
# exists:
|
||||
# - '{dockerfile,dockerfile.j2}'
|
||||
# when: always
|
||||
|
||||
- if: # condition_dev_branch_push
|
||||
(
|
||||
$CI_COMMIT_BRANCH == "development"
|
||||
||
|
||||
$CI_COMMIT_BRANCH == "v-1-0-0-alpha"
|
||||
) &&
|
||||
$CI_PIPELINE_SOURCE == "push"
|
||||
exists:
|
||||
- '{dockerfile,dockerfile.j2}'
|
||||
allow_failure: true
|
||||
when: on_success
|
||||
# - 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
|
||||
# - when: never
|
||||
|
||||
|
||||
Github (Push --mirror):
|
||||
extends:
|
||||
- .git_push_mirror
|
||||
needs: []
|
||||
rules:
|
||||
- if: '$JOB_STOP_GIT_PUSH_MIRROR'
|
||||
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: $GIT_SYNC_URL == null
|
||||
# when: never
|
||||
|
||||
- if: # condition_master_or_dev_push
|
||||
$CI_COMMIT_BRANCH
|
||||
&&
|
||||
$CI_PIPELINE_SOURCE == "push"
|
||||
when: always
|
||||
# - if: # condition_master_or_dev_push
|
||||
# $CI_COMMIT_BRANCH
|
||||
# &&
|
||||
# $CI_PIPELINE_SOURCE == "push"
|
||||
# when: always
|
||||
|
||||
- when: never
|
||||
# - when: never
|
||||
|
||||
|
||||
Website.Submodule.Deploy:
|
||||
@ -272,11 +300,7 @@ Website.Submodule.Deploy:
|
||||
name: Documentation
|
||||
rules:
|
||||
- if: # condition_dev_branch_push
|
||||
(
|
||||
$CI_COMMIT_BRANCH == "development"
|
||||
||
|
||||
$CI_COMMIT_BRANCH == "v-1-0-0-alpha"
|
||||
) &&
|
||||
$CI_COMMIT_BRANCH == "development" &&
|
||||
$CI_PIPELINE_SOURCE == "push"
|
||||
exists:
|
||||
- '{docs/**,pages/**}/*.md'
|
||||
|
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"
|
@ -24,6 +24,8 @@
|
||||
|
||||
_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_
|
||||
@ -33,3 +35,5 @@
|
||||
- [ ] [Unit Test(s) Written](https://nofusscomputing.com/projects/centurion_erp/development/testing/)
|
||||
|
||||
_ensure test coverage delta is not less than zero_
|
||||
|
||||
- [ ] :page_facing_up: Roadmap updated
|
||||
|
1073
CHANGELOG.md
1073
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -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.
|
||||
|
33
README.md
33
README.md
@ -4,10 +4,10 @@
|
||||
|
||||
<br>
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
[](https://hub.docker.com/r/nofusscomputing/centurion-erp)
|
||||
[](https://hub.docker.com/r/nofusscomputing/centurion-erp) [](https://artifacthub.io/packages/container/centurion-erp/centurion-erp)
|
||||
|
||||
|
||||
|
||||
@ -15,27 +15,36 @@
|
||||
|
||||
<br>
|
||||
|
||||
  [](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues) [](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/?sort=created_date&state=opened&label_name%5B%5D=type%3A%3Abug)
|
||||
|
||||
|
||||
|
||||
  
|
||||
|
||||
|
||||
|
||||
 
|
||||
|
||||
<br>
|
||||
|
||||
This project is hosted on [gitlab](https://gitlab.com/nofusscomputing/projects/centurion_erp) and has a read-only copy hosted on [Github](https://github.com/NofussComputing/centurion_erp).
|
||||
 
|
||||
|
||||
|
||||
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**
|
||||
|
||||
  [](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/jobs/artifacts/master/browse/artifacts/coverage/?job=Unit)
|
||||
  
|
||||

|
||||
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Development Branch**
|
||||
|
||||
  [](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/jobs/artifacts/development/browse/artifacts/coverage/?job=Unit)
|
||||
|
||||
|
||||
  
|
||||

|
||||
|
||||
|
||||
----
|
||||
@ -45,9 +54,9 @@ This project is hosted on [gitlab](https://gitlab.com/nofusscomputing/projects/c
|
||||
|
||||
links:
|
||||
|
||||
- [Issues](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues)
|
||||
- [Issues](https://github.com/nofusscomputing/centurion_erp/issues)
|
||||
|
||||
- [Merge Requests (Pull Requests)](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests)
|
||||
- [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.
|
||||
@ -55,7 +64,7 @@ An ERP with a large emphasis on the IT Service Management (ITSM) and Automation.
|
||||
|
||||
## Contributing
|
||||
|
||||
All contributions for this project must conducted from [Gitlab](https://gitlab.com/nofusscomputing/projects/centurion_erp).
|
||||
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).
|
||||
|
||||
|
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
|
@ -68,6 +68,7 @@ class TeamForm(CommonModelForm):
|
||||
|
||||
apps = [
|
||||
'access',
|
||||
'assistance',
|
||||
'config_management',
|
||||
'core',
|
||||
'django_celery_results',
|
||||
|
@ -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 == '_':
|
||||
@ -62,6 +59,9 @@ class Organization(SaveHistory):
|
||||
def get_organization(self):
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
|
||||
class TenancyManager(models.Manager):
|
||||
@ -190,15 +190,12 @@ class TenancyObject(SaveHistory):
|
||||
|
||||
|
||||
|
||||
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):
|
||||
|
||||
@ -241,6 +238,10 @@ class Team(Group, TenancyObject, SaveHistory):
|
||||
return [permission_list, self.permissions.all()]
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.team_name
|
||||
|
||||
|
||||
|
||||
class TeamUsers(SaveHistory):
|
||||
|
||||
@ -318,3 +319,6 @@ class TeamUsers(SaveHistory):
|
||||
|
||||
return self.team
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username
|
||||
|
||||
|
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
|
@ -5,7 +5,7 @@ from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
@ -24,7 +24,7 @@ class OrganizationPermissionsAPI(TestCase, APIPermissionChange, APIPermissionVie
|
||||
|
||||
url_name = '_api_organization'
|
||||
|
||||
url_list = 'device-list'
|
||||
url_list = '_api_orgs'
|
||||
|
||||
change_data = {'name': 'device'}
|
||||
|
||||
@ -124,6 +124,8 @@ class OrganizationPermissionsAPI(TestCase, APIPermissionChange, APIPermissionVie
|
||||
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")
|
||||
|
||||
|
||||
@ -171,3 +173,67 @@ class OrganizationPermissionsAPI(TestCase, APIPermissionChange, APIPermissionVie
|
||||
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
|
||||
|
@ -63,4 +63,8 @@ class TeamModel(
|
||||
|
||||
@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
|
@ -5,6 +5,8 @@ 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
|
||||
@ -14,6 +16,37 @@ 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,
|
||||
|
@ -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',
|
||||
]
|
||||
|
@ -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',
|
||||
]
|
||||
|
||||
|
175
app/api/tasks.py
175
app/api/tasks.py
@ -37,13 +37,6 @@ def process_inventory(self, data, organization: int):
|
||||
|
||||
organization = Organization.objects.get(id=organization)
|
||||
|
||||
if Device.objects.filter(slug=str(data.details.name).lower()).exists():
|
||||
|
||||
device = Device.objects.get(slug=str(data.details.name).lower())
|
||||
|
||||
# device = self.obj
|
||||
|
||||
|
||||
app_settings = AppSettings.objects.get(owner_organization = None)
|
||||
|
||||
device_serial_number = None
|
||||
@ -57,10 +50,62 @@ def process_inventory(self, data, organization: int):
|
||||
|
||||
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,
|
||||
@ -74,25 +119,75 @@ def process_inventory(self, data, organization: int):
|
||||
|
||||
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.save()
|
||||
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()
|
||||
|
||||
|
||||
if OperatingSystem.objects.filter( slug=data.operating_system.name ).exists():
|
||||
operating_system = OperatingSystem.objects.filter(
|
||||
name=data.operating_system.name,
|
||||
is_global = True
|
||||
)
|
||||
|
||||
operating_system = OperatingSystem.objects.get( slug=data.operating_system.name )
|
||||
if operating_system.exists():
|
||||
|
||||
else: # Create Operating System
|
||||
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,
|
||||
@ -101,16 +196,24 @@ def process_inventory(self, data, organization: int):
|
||||
)
|
||||
|
||||
|
||||
if OperatingSystemVersion.objects.filter( name=data.operating_system.version_major, operating_system=operating_system ).exists():
|
||||
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(
|
||||
organization = organization,
|
||||
is_global = True,
|
||||
name = data.operating_system.version_major,
|
||||
operating_system = operating_system
|
||||
name=data.operating_system.version_major,
|
||||
operating_system=operating_system
|
||||
)
|
||||
|
||||
else: # Create Operating System Version
|
||||
else:
|
||||
|
||||
operating_system_version = None
|
||||
|
||||
|
||||
if not operating_system_version:
|
||||
|
||||
operating_system_version = OperatingSystemVersion.objects.create(
|
||||
organization = organization,
|
||||
@ -119,22 +222,22 @@ def process_inventory(self, data, organization: int):
|
||||
operating_system = operating_system,
|
||||
)
|
||||
|
||||
device_operating_system = DeviceOperatingSystem.objects.filter(
|
||||
device=device,
|
||||
)
|
||||
|
||||
if DeviceOperatingSystem.objects.filter( version=data.operating_system.version, device=device, operating_system_version=operating_system_version ).exists():
|
||||
if device_operating_system.exists():
|
||||
|
||||
device_operating_system = DeviceOperatingSystem.objects.get(
|
||||
device=device,
|
||||
version = data.operating_system.version,
|
||||
operating_system_version = operating_system_version,
|
||||
)
|
||||
|
||||
if not device_operating_system.installdate: # Only update install date if empty
|
||||
else:
|
||||
|
||||
device_operating_system.installdate = timezone.now()
|
||||
device_operating_system = None
|
||||
|
||||
device_operating_system.save()
|
||||
|
||||
else: # Create Operating System Version
|
||||
if not device_operating_system:
|
||||
|
||||
device_operating_system = DeviceOperatingSystem.objects.create(
|
||||
organization = organization,
|
||||
@ -144,6 +247,26 @@ def process_inventory(self, data, organization: int):
|
||||
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:
|
||||
|
||||
|
@ -255,6 +255,20 @@ class InventoryAPI(TestCase):
|
||||
|
||||
|
||||
|
||||
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 """
|
||||
|
||||
@ -424,3 +438,552 @@ class InventoryAPI(TestCase):
|
||||
"""
|
||||
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
|
||||
|
||||
|
@ -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),
|
||||
}
|
||||
|
@ -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':
|
||||
|
@ -74,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
|
||||
|
||||
@ -203,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)
|
||||
}
|
||||
|
@ -113,6 +113,8 @@ INSTALLED_APPS = [
|
||||
'core.apps.CoreConfig',
|
||||
'access.apps.AccessConfig',
|
||||
'itam.apps.ItamConfig',
|
||||
'itim.apps.ItimConfig',
|
||||
'assistance.apps.AssistanceConfig',
|
||||
'settings.apps.SettingsConfig',
|
||||
'drf_spectacular',
|
||||
'drf_spectacular_sidecar',
|
||||
@ -307,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'
|
||||
@ -354,7 +359,6 @@ if DEBUG:
|
||||
|
||||
# Apps Under Development
|
||||
INSTALLED_APPS += [
|
||||
'information.apps.InformationConfig',
|
||||
'project_management.apps.ProjectManagementConfig',
|
||||
]
|
||||
|
||||
|
@ -7,6 +7,7 @@ from access.tests.abstract.tenancy_object import TenancyObject as TenancyObjectT
|
||||
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
|
||||
|
||||
|
||||
|
||||
@ -30,7 +31,8 @@ class BaseModel:
|
||||
|
||||
class TenancyModel(
|
||||
BaseModel,
|
||||
TenancyObjectTestCases
|
||||
TenancyObjectTestCases,
|
||||
Models
|
||||
):
|
||||
""" Test cases for tenancy models"""
|
||||
|
||||
|
@ -563,3 +563,33 @@ class AllViews(
|
||||
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
|
||||
"""
|
||||
|
@ -42,7 +42,9 @@ urlpatterns = [
|
||||
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'),
|
||||
@ -72,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")),
|
||||
]
|
||||
|
||||
|
@ -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
|
||||
|
68
app/assistance/migrations/0001_initial.py
Normal file
68
app/assistance/migrations/0001_initial.py
Normal file
@ -0,0 +1,68 @@
|
||||
# Generated by Django 5.0.7 on 2024-07-20 14:37
|
||||
|
||||
import access.fields
|
||||
import access.models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('access', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='KnowledgeBaseCategory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('is_global', models.BooleanField(default=False)),
|
||||
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
|
||||
('name', models.CharField(help_text='Name/Title of the Category', max_length=50, verbose_name='Title')),
|
||||
('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)),
|
||||
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
|
||||
('parent_category', models.ForeignKey(blank=True, default=None, help_text='Category this category belongs to', null=True, on_delete=django.db.models.deletion.SET_NULL, to='assistance.knowledgebasecategory', verbose_name='Parent Category')),
|
||||
('target_team', models.ManyToManyField(blank=True, default=None, help_text='Team(s) to grant access to the article', to='access.team', verbose_name='Target Team(s)')),
|
||||
('target_user', models.ForeignKey(blank=True, default=None, help_text='User(s) to grant access to the article', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Target Users(s)')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Category',
|
||||
'verbose_name_plural': 'Categorys',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='KnowledgeBase',
|
||||
fields=[
|
||||
('is_global', models.BooleanField(default=False)),
|
||||
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
|
||||
('title', models.CharField(help_text='Title of the article', max_length=50, verbose_name='Title')),
|
||||
('summary', models.TextField(blank=True, default=None, help_text='Short Summary of the article', null=True, verbose_name='Summary')),
|
||||
('content', models.TextField(blank=True, default=None, help_text='Content of the article. Markdown is supported', null=True, verbose_name='Article Content')),
|
||||
('release_date', models.DateTimeField(blank=True, default=None, help_text='Date the article will be published', null=True, verbose_name='Publish Date')),
|
||||
('expiry_date', models.DateTimeField(blank=True, default=None, help_text='Date the article will be removed from published articles', null=True, verbose_name='End Date')),
|
||||
('public', models.BooleanField(default=False, help_text='Is this article to be made available publically', verbose_name='Public Article')),
|
||||
('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(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
|
||||
('responsible_teams', models.ManyToManyField(blank=True, default=None, help_text='Team(s) whom is considered the articles owner.', related_name='responsible_teams', to='access.team', verbose_name='Responsible Team(s)')),
|
||||
('responsible_user', models.ForeignKey(default=None, help_text='User(s) whom is considered the articles owner.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='responsible_user', to=settings.AUTH_USER_MODEL, verbose_name='Responsible User')),
|
||||
('target_team', models.ManyToManyField(blank=True, default=None, help_text='Team(s) to grant access to the article', to='access.team', verbose_name='Target Team(s)')),
|
||||
('target_user', models.ForeignKey(blank=True, default=None, help_text='User(s) to grant access to the article', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Target Users(s)')),
|
||||
('category', models.ForeignKey(default=None, help_text='Article Category', max_length=50, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assistance.knowledgebasecategory', verbose_name='Category')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Article',
|
||||
'verbose_name_plural': 'Articles',
|
||||
'ordering': ['title'],
|
||||
},
|
||||
),
|
||||
]
|
0
app/assistance/migrations/__init__.py
Normal file
0
app/assistance/migrations/__init__.py
Normal file
219
app/assistance/models/knowledge_base.py
Normal file
219
app/assistance/models/knowledge_base.py
Normal file
@ -0,0 +1,219 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.forms import ValidationError
|
||||
|
||||
from access.fields import *
|
||||
from access.models import Team, TenancyObject
|
||||
|
||||
|
||||
|
||||
class KnowledgeBaseCategory(TenancyObject):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
ordering = [
|
||||
'name',
|
||||
]
|
||||
|
||||
verbose_name = "Category"
|
||||
|
||||
verbose_name_plural = "Categorys"
|
||||
|
||||
|
||||
parent_category = models.ForeignKey(
|
||||
'self',
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Category this category belongs to',
|
||||
null = True,
|
||||
on_delete = models.SET_NULL,
|
||||
verbose_name = 'Parent Category',
|
||||
)
|
||||
|
||||
|
||||
name = models.CharField(
|
||||
blank = False,
|
||||
help_text = 'Name/Title of the Category',
|
||||
max_length = 50,
|
||||
unique = False,
|
||||
verbose_name = 'Title',
|
||||
)
|
||||
|
||||
|
||||
slug = AutoSlugField()
|
||||
|
||||
|
||||
target_team = models.ManyToManyField(
|
||||
Team,
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Team(s) to grant access to the article',
|
||||
verbose_name = 'Target Team(s)',
|
||||
)
|
||||
|
||||
|
||||
target_user = models.ForeignKey(
|
||||
User,
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'User(s) to grant access to the article',
|
||||
null = True,
|
||||
on_delete = models.SET_NULL,
|
||||
verbose_name = 'Target Users(s)',
|
||||
)
|
||||
|
||||
|
||||
created = AutoCreatedField()
|
||||
|
||||
|
||||
modified = AutoLastModifiedField()
|
||||
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return self.name
|
||||
|
||||
|
||||
|
||||
class KnowledgeBase(TenancyObject):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
ordering = [
|
||||
'title',
|
||||
]
|
||||
|
||||
verbose_name = "Article"
|
||||
|
||||
verbose_name_plural = "Articles"
|
||||
|
||||
|
||||
model_notes = None
|
||||
|
||||
|
||||
id = models.AutoField(
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
blank=False
|
||||
)
|
||||
|
||||
|
||||
title = models.CharField(
|
||||
blank = False,
|
||||
help_text = 'Title of the article',
|
||||
max_length = 50,
|
||||
unique = False,
|
||||
verbose_name = 'Title',
|
||||
)
|
||||
|
||||
|
||||
summary = models.TextField(
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Short Summary of the article',
|
||||
null = True,
|
||||
verbose_name = 'Summary',
|
||||
)
|
||||
|
||||
|
||||
content = models.TextField(
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Content of the article. Markdown is supported',
|
||||
null = True,
|
||||
verbose_name = 'Article Content',
|
||||
)
|
||||
|
||||
|
||||
category = models.ForeignKey(
|
||||
KnowledgeBaseCategory,
|
||||
blank = False,
|
||||
default = None,
|
||||
help_text = 'Article Category',
|
||||
max_length = 50,
|
||||
null = True,
|
||||
on_delete = models.SET_NULL,
|
||||
unique = False,
|
||||
verbose_name = 'Category',
|
||||
)
|
||||
|
||||
|
||||
release_date = models.DateTimeField(
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Date the article will be published',
|
||||
null = True,
|
||||
verbose_name = 'Publish Date',
|
||||
)
|
||||
|
||||
|
||||
expiry_date = models.DateTimeField(
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Date the article will be removed from published articles',
|
||||
null = True,
|
||||
verbose_name = 'End Date',
|
||||
)
|
||||
|
||||
|
||||
target_team = models.ManyToManyField(
|
||||
Team,
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Team(s) to grant access to the article',
|
||||
verbose_name = 'Target Team(s)',
|
||||
)
|
||||
|
||||
|
||||
target_user = models.ForeignKey(
|
||||
User,
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'User(s) to grant access to the article',
|
||||
null = True,
|
||||
on_delete = models.SET_NULL,
|
||||
verbose_name = 'Target Users(s)',
|
||||
)
|
||||
|
||||
|
||||
responsible_user = models.ForeignKey(
|
||||
User,
|
||||
blank = False,
|
||||
default = None,
|
||||
help_text = 'User(s) whom is considered the articles owner.',
|
||||
null = True,
|
||||
on_delete = models.SET_NULL,
|
||||
related_name = 'responsible_user',
|
||||
verbose_name = 'Responsible User',
|
||||
)
|
||||
|
||||
|
||||
responsible_teams = models.ManyToManyField(
|
||||
Team,
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Team(s) whom is considered the articles owner.',
|
||||
related_name = 'responsible_teams',
|
||||
verbose_name = 'Responsible Team(s)',
|
||||
)
|
||||
|
||||
|
||||
public = models.BooleanField(
|
||||
blank = False,
|
||||
default = False,
|
||||
help_text = 'Is this article to be made available publically',
|
||||
verbose_name = 'Public Article',
|
||||
)
|
||||
|
||||
|
||||
created = AutoCreatedField()
|
||||
|
||||
|
||||
modified = AutoLastModifiedField()
|
||||
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return self.title
|
232
app/assistance/templates/assistance/kb_article.html.j2
Normal file
232
app/assistance/templates/assistance/kb_article.html.j2
Normal file
@ -0,0 +1,232 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% load markdown %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<script>
|
||||
|
||||
function openCity(evt, cityName) {
|
||||
var i, tabcontent, tablinks;
|
||||
|
||||
tabcontent = document.getElementsByClassName("tabcontent");
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].style.display = "none";
|
||||
}
|
||||
|
||||
tablinks = document.getElementsByClassName("tablinks");
|
||||
for (i = 0; i < tablinks.length; i++) {
|
||||
tablinks[i].className = tablinks[i].className.replace(" active", "");
|
||||
}
|
||||
|
||||
document.getElementById(cityName).style.display = "block";
|
||||
evt.currentTarget.className += " active";
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
.detail-view-field {
|
||||
display:unset;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 0px 20px 40px 20px;
|
||||
|
||||
}
|
||||
|
||||
.detail-view-field label {
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
width: 200px;
|
||||
margin: 10px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
|
||||
}
|
||||
|
||||
.detail-view-field span {
|
||||
display: inline-block;
|
||||
width: 340px;
|
||||
margin: 10px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
|
||||
}
|
||||
|
||||
pre {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
</style>
|
||||
<div class="tab">
|
||||
<button
|
||||
onclick="window.location='{% url 'Assistance:Knowledge Base' %}';"
|
||||
style="vertical-align: middle; padding: auto; margin: 0px">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="25px" viewBox="0 -960 960 960" width="25px"
|
||||
style="vertical-align: middle; margin: 0px; padding: 0px border: none; " fill="#6a6e73">
|
||||
<path
|
||||
d="m313-480 155 156q11 11 11.5 27.5T468-268q-11 11-28 11t-28-11L228-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T468-692q11 11 11 28t-11 28L313-480Zm264 0 155 156q11 11 11.5 27.5T732-268q-11 11-28 11t-28-11L492-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T732-692q11 11 11 28t-11 28L577-480Z" />
|
||||
</svg>Back to Articles</button>
|
||||
|
||||
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
|
||||
{% if perms.assistance.change_knowledgebase %}
|
||||
<button class="tablinks" onclick="openCity(event, 'Notes')">Notes</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
<div id="Details" class="tabcontent">
|
||||
{% if perms.assistance.change_knowledgebase %}
|
||||
<h3>Details</h3>
|
||||
|
||||
{% csrf_token %}
|
||||
|
||||
|
||||
<div style="align-items:flex-start; align-content: center; display: flexbox; width: 100%">
|
||||
|
||||
<div style="display: inline; width: 40%; margin: 30px;">
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.title.label }}</label>
|
||||
<span>{{ form.title.value }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.category.label }}</label>
|
||||
<span>
|
||||
{% if kb.category %}
|
||||
<a href="{% url 'Settings:_knowledge_base_category_view' kb.category.id %}">{{ kb.category }}</a>
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.responsible_user.label }}</label>
|
||||
<span>
|
||||
{% if form.responsible_user.value %}
|
||||
{{ kb.responsible_user }}
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.organization.label }}</label>
|
||||
<span>
|
||||
{% if form.organization.value %}
|
||||
{{ kb.organization }}
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div style="display: inline; width: 40%; margin: 30px; text-align: left;">
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.release_date.label }}</label>
|
||||
<span>
|
||||
{% if form.release_date.value %}
|
||||
{{ form.release_date.value }}
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.expiry_date.label }}</label>
|
||||
<span>
|
||||
{% if form.expiry_date.value %}
|
||||
{{ form.expiry_date.value }}
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.target_user.label }}</label>
|
||||
<span>
|
||||
{% if form.target_user.value %}
|
||||
{{ kb.target_user }}
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.target_team.label }}</label>
|
||||
<span>
|
||||
{% if form.target_team.value %}
|
||||
{{ form.target_team.value }} {{ kb.target_team }}
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<input type="button" value="Edit" onclick="window.location='{% url 'Assistance:_knowledge_base_change' kb.id %}';">
|
||||
{% endif %}
|
||||
|
||||
{% if form.summary.value %}
|
||||
<div style="display: block; width: 100%;">
|
||||
<h3>Summary</h3>
|
||||
{{ form.summary.value | safe }}
|
||||
<br>
|
||||
<hr />
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div style="display: block; width: 100%;">
|
||||
<h3>Content</h3>
|
||||
<hr />
|
||||
<br>
|
||||
{{ form.content.value | markdown | safe }}
|
||||
<br>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<script>
|
||||
document.getElementById("defaultOpen").click();
|
||||
</script>
|
||||
|
||||
</div>
|
||||
|
||||
{% if perms.assistance.change_knowledgebase %}
|
||||
<div id="Notes" class="tabcontent">
|
||||
<h3>
|
||||
Notes
|
||||
</h3>
|
||||
{{ notes_form }}
|
||||
<input type="submit" name="{{notes_form.prefix}}" value="Submit" />
|
||||
<div class="comments">
|
||||
{% if notes %}
|
||||
{% for note in notes %}
|
||||
{% include 'note.html.j2' %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
213
app/assistance/templates/assistance/kb_category.html.j2
Normal file
213
app/assistance/templates/assistance/kb_category.html.j2
Normal file
@ -0,0 +1,213 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% load markdown %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<script>
|
||||
|
||||
function openCity(evt, cityName) {
|
||||
var i, tabcontent, tablinks;
|
||||
|
||||
tabcontent = document.getElementsByClassName("tabcontent");
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].style.display = "none";
|
||||
}
|
||||
|
||||
tablinks = document.getElementsByClassName("tablinks");
|
||||
for (i = 0; i < tablinks.length; i++) {
|
||||
tablinks[i].className = tablinks[i].className.replace(" active", "");
|
||||
}
|
||||
|
||||
document.getElementById(cityName).style.display = "block";
|
||||
evt.currentTarget.className += " active";
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
.detail-view-field {
|
||||
display:unset;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 0px 20px 40px 20px;
|
||||
|
||||
}
|
||||
|
||||
.detail-view-field label {
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
width: 200px;
|
||||
margin: 10px;
|
||||
/*padding: 10px;*/
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
|
||||
}
|
||||
|
||||
.detail-view-field span {
|
||||
display: inline-block;
|
||||
width: 340px;
|
||||
margin: 10px;
|
||||
/*padding: 10px;*/
|
||||
border-bottom: 1px solid #ccc;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
|
||||
}
|
||||
|
||||
pre {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
</style>
|
||||
<div class="tab">
|
||||
<button
|
||||
onclick="window.location='{% url 'Settings:KB Categories' %}';"
|
||||
style="vertical-align: middle; padding: auto; margin: 0px">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="25px" viewBox="0 -960 960 960" width="25px"
|
||||
style="vertical-align: middle; margin: 0px; padding: 0px border: none; " fill="#6a6e73">
|
||||
<path
|
||||
d="m313-480 155 156q11 11 11.5 27.5T468-268q-11 11-28 11t-28-11L228-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T468-692q11 11 11 28t-11 28L313-480Zm264 0 155 156q11 11 11.5 27.5T732-268q-11 11-28 11t-28-11L492-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T732-692q11 11 11 28t-11 28L577-480Z" />
|
||||
</svg>Back to Articles</button>
|
||||
|
||||
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
|
||||
<button class="tablinks" onclick="openCity(event, 'Articles')">Articles</button>
|
||||
{% if perms.assistance.change_knowledgebase %}
|
||||
<button class="tablinks" onclick="openCity(event, 'Notes')">Notes</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
<div id="Details" class="tabcontent">
|
||||
|
||||
<h3>Details</h3>
|
||||
|
||||
{% csrf_token %}
|
||||
|
||||
|
||||
<div style="align-items:flex-start; align-content: center; display: flexbox; width: 100%">
|
||||
|
||||
<div style="display: inline; width: 40%; margin: 30px;">
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.name.label }}</label>
|
||||
<span>{{ form.name.value }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.parent_category.label }}</label>
|
||||
<span>
|
||||
{% if item.parent_category %}
|
||||
{{ item.parent_category }}
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>Created</label>
|
||||
<span>{{ item.created }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>Modified</label>
|
||||
<span>{{ item.modified }}</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div style="display: inline; width: 40%; margin: 30px; text-align: left;">
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.organization.label }}</label>
|
||||
<span>
|
||||
{% if form.organization.value %}
|
||||
{{ item.organization }}
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.target_user.label }}</label>
|
||||
<span>
|
||||
{% if form.target_user.value %}
|
||||
{{ form.target_user.value }}
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.target_team.label }}</label>
|
||||
<span>
|
||||
{% if form.target_team.value %}
|
||||
{{ form.target_team.value }}
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<input type="button" value="Edit" onclick="window.location='{% url 'Settings:_knowledge_base_category_change' item.id %}';">
|
||||
|
||||
<br>
|
||||
|
||||
<script>
|
||||
document.getElementById("defaultOpen").click();
|
||||
</script>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="Articles" class="tabcontent">
|
||||
<h3>
|
||||
Articles
|
||||
</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Organization</th>
|
||||
</tr>
|
||||
{% for article in articles %}
|
||||
<tr>
|
||||
<td><a href="{% url 'Assistance:_knowledge_base_view' article.id %}">{{ article.title }}</a></td>
|
||||
<td>{{ article.organization }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
{% if perms.assistance.change_knowledgebase %}
|
||||
<div id="Notes" class="tabcontent">
|
||||
<h3>
|
||||
Notes
|
||||
</h3>
|
||||
{{ notes_form }}
|
||||
<input type="submit" name="{{notes_form.prefix}}" value="Submit" />
|
||||
<div class="comments">
|
||||
{% if notes %}
|
||||
{% for note in notes %}
|
||||
{% include 'note.html.j2' %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
@ -0,0 +1,47 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<input type="button" value="New Article" onclick="window.location='{% url 'Settings:_knowledge_base_category_add' %}';">
|
||||
<table class="data">
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Parent</th>
|
||||
<th>Organization</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
{% if items %}
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td><a href="{% url 'Settings:_knowledge_base_category_view' pk=item.id %}">{{ item.name }}</a></td>
|
||||
<td>{{ item.parent_category }}</td>
|
||||
<td>{{ item.organization }}</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4">Nothing Found</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
<br>
|
||||
<div class="pagination">
|
||||
<span class="step-links">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?page=1">« first</a>
|
||||
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="current">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
|
||||
</span>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?page={{ page_obj.next_page_number }}">next</a>
|
||||
<a href="?page={{ page_obj.paginator.num_pages }}">last »</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
47
app/assistance/templates/assistance/kb_index.html.j2
Normal file
47
app/assistance/templates/assistance/kb_index.html.j2
Normal file
@ -0,0 +1,47 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<input type="button" value="New Article" onclick="window.location='{% url 'Assistance:_knowledge_base_add' %}';">
|
||||
<table class="data">
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Category</th>
|
||||
<th>Organization</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
{% if items %}
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td><a href="{% url 'Assistance:_knowledge_base_view' pk=item.id %}">{{ item.title }}</a></td>
|
||||
<td><a href="{% url 'Settings:_knowledge_base_category_view' pk=item.category.id %}">{{ item.category }}</a></td>
|
||||
<td>{{ item.organization }}</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4">Nothing Found</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
<br>
|
||||
<div class="pagination">
|
||||
<span class="step-links">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?page=1">« first</a>
|
||||
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="current">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
|
||||
</span>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?page={{ page_obj.next_page_number }}">next</a>
|
||||
<a href="?page={{ page_obj.paginator.num_pages }}">last »</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -0,0 +1,44 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from access.models import Organization
|
||||
|
||||
from app.tests.abstract.models import TenancyModel
|
||||
|
||||
from assistance.models.knowledge_base import KnowledgeBase
|
||||
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class KnowledgeBaseModel(
|
||||
TestCase,
|
||||
TenancyModel
|
||||
):
|
||||
|
||||
model = KnowledgeBase
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
2. Create an item
|
||||
|
||||
"""
|
||||
|
||||
self.organization = Organization.objects.create(name='test_org')
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
title = 'one',
|
||||
content = 'dict({"key": "one", "existing": "dont_over_write"})'
|
||||
)
|
||||
|
||||
self.second_item = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
title = 'one_two',
|
||||
content = 'dict({"key": "two"})',
|
||||
)
|
@ -0,0 +1,78 @@
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from django.test import TestCase, Client
|
||||
|
||||
from access.models import Organization
|
||||
|
||||
from core.models.history import History
|
||||
from core.tests.abstract.history_entry import HistoryEntry
|
||||
from core.tests.abstract.history_entry_parent_model import HistoryEntryParentItem
|
||||
|
||||
from assistance.models.knowledge_base import KnowledgeBase
|
||||
|
||||
|
||||
|
||||
class KnowledgeBaseHistory(TestCase, HistoryEntry, HistoryEntryParentItem):
|
||||
|
||||
|
||||
model = KnowledgeBase
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
""" Setup Test """
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
self.item_parent = self.model.objects.create(
|
||||
title = 'test_item_parent_' + self.model._meta.model_name,
|
||||
organization = self.organization
|
||||
)
|
||||
|
||||
self.item_create = self.model.objects.create(
|
||||
title = 'test_item_' + self.model._meta.model_name,
|
||||
organization = self.organization,
|
||||
)
|
||||
|
||||
|
||||
self.history_create = History.objects.get(
|
||||
action = History.Actions.ADD[0],
|
||||
item_pk = self.item_create.pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
self.item_change = self.item_create
|
||||
self.item_change.title = 'test_item_' + self.model._meta.model_name + '_changed'
|
||||
self.item_change.save()
|
||||
|
||||
self.field_after_expected_value = '{"title": "' + self.item_change.title + '"}'
|
||||
|
||||
self.history_change = History.objects.get(
|
||||
action = History.Actions.UPDATE[0],
|
||||
item_pk = self.item_change.pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
self.item_delete = self.model.objects.create(
|
||||
title = 'test_item_delete_' + self.model._meta.model_name,
|
||||
organization = self.organization,
|
||||
)
|
||||
|
||||
self.deleted_pk = self.item_delete.pk
|
||||
|
||||
self.item_delete.delete()
|
||||
|
||||
self.history_delete = History.objects.filter(
|
||||
item_pk = self.deleted_pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
self.history_delete_children = History.objects.filter(
|
||||
item_parent_pk = self.deleted_pk,
|
||||
item_parent_class = self.item_parent._meta.model_name,
|
||||
)
|
@ -0,0 +1,95 @@
|
||||
# from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from assistance.models.knowledge_base import KnowledgeBase
|
||||
|
||||
from core.tests.abstract.history_permissions import HistoryPermissions
|
||||
|
||||
|
||||
|
||||
class KnowledgeBaseHistoryPermissions(TestCase, HistoryPermissions):
|
||||
|
||||
|
||||
item_model = KnowledgeBase
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
2. create an organization that is different to item
|
||||
3. Create a device
|
||||
4. Add history device history entry as item
|
||||
5. create a user
|
||||
6. create user in different organization (with the required permission)
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
self.item = self.item_model.objects.create(
|
||||
organization=organization,
|
||||
title = 'deviceone'
|
||||
)
|
||||
|
||||
self.history = self.model.objects.get(
|
||||
item_pk = self.item.id,
|
||||
item_class = self.item._meta.model_name,
|
||||
action = self.model.Actions.ADD,
|
||||
)
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
view_team = Team.objects.create(
|
||||
team_name = 'view_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
view_team.permissions.set([view_permissions])
|
||||
|
||||
|
||||
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
|
||||
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = view_team,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
|
||||
|
||||
|
||||
different_organization_team = Team.objects.create(
|
||||
team_name = 'different_organization_team',
|
||||
organization = different_organization,
|
||||
)
|
||||
|
||||
different_organization_team.permissions.set([
|
||||
view_permissions,
|
||||
])
|
||||
|
||||
TeamUsers.objects.create(
|
||||
team = different_organization_team,
|
||||
user = self.different_organization_user
|
||||
)
|
@ -0,0 +1,189 @@
|
||||
# from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from app.tests.abstract.model_permissions import ModelPermissions
|
||||
|
||||
from assistance.models.knowledge_base import KnowledgeBase
|
||||
|
||||
|
||||
class KnowledgeBasePermissions(TestCase, ModelPermissions):
|
||||
|
||||
|
||||
model = KnowledgeBase
|
||||
|
||||
app_namespace = 'Assistance'
|
||||
|
||||
url_name_view = '_knowledge_base_view'
|
||||
|
||||
url_name_add = '_knowledge_base_add'
|
||||
|
||||
url_name_change = '_knowledge_base_change'
|
||||
|
||||
url_name_delete = '_knowledge_base_delete'
|
||||
|
||||
url_delete_response = reverse('Assistance:Knowledge Base')
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
. create an organization that is different to item
|
||||
2. Create a device
|
||||
3. create teams with each permission: view, add, change, delete
|
||||
4. create a user per team
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization=organization,
|
||||
title = 'deviceone'
|
||||
)
|
||||
|
||||
|
||||
self.url_view_kwargs = {'pk': self.item.id}
|
||||
|
||||
# self.url_add_kwargs = {'pk': self.item.id}
|
||||
|
||||
self.add_data = {'device': 'device', 'organization': self.organization.id}
|
||||
|
||||
self.url_change_kwargs = {'pk': self.item.id}
|
||||
|
||||
self.change_data = {'device': 'device', 'organization': self.organization.id}
|
||||
|
||||
self.url_delete_kwargs = {'pk': self.item.id}
|
||||
|
||||
self.delete_data = {'device': '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.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
|
||||
)
|
@ -0,0 +1,29 @@
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from app.tests.abstract.models import PrimaryModel
|
||||
|
||||
|
||||
|
||||
class KnowledgeBaseViews(
|
||||
TestCase,
|
||||
PrimaryModel
|
||||
):
|
||||
|
||||
add_module = 'assistance.views.knowledge_base'
|
||||
add_view = 'Add'
|
||||
|
||||
change_module = add_module
|
||||
change_view = 'Change'
|
||||
|
||||
delete_module = add_module
|
||||
delete_view = 'Delete'
|
||||
|
||||
display_module = add_module
|
||||
display_view = 'View'
|
||||
|
||||
index_module = add_module
|
||||
index_view = 'Index'
|
@ -0,0 +1,42 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from access.models import Organization
|
||||
|
||||
from app.tests.abstract.models import TenancyModel
|
||||
|
||||
from assistance.models.knowledge_base import KnowledgeBaseCategory
|
||||
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class KnowledgeBaseModel(
|
||||
TestCase,
|
||||
TenancyModel
|
||||
):
|
||||
|
||||
model = KnowledgeBaseCategory
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
2. Create an item
|
||||
|
||||
"""
|
||||
|
||||
self.organization = Organization.objects.create(name='test_org')
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
name = 'one',
|
||||
)
|
||||
|
||||
self.second_item = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
name = 'one_two',
|
||||
)
|
@ -0,0 +1,75 @@
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from django.test import TestCase, Client
|
||||
|
||||
from access.models import Organization
|
||||
|
||||
from core.models.history import History
|
||||
from core.tests.abstract.history_entry import HistoryEntry
|
||||
from core.tests.abstract.history_entry_parent_model import HistoryEntryParentItem
|
||||
|
||||
from assistance.models.knowledge_base import KnowledgeBaseCategory
|
||||
|
||||
|
||||
|
||||
class KnowledgeBaseHistory(TestCase, HistoryEntry, HistoryEntryParentItem):
|
||||
|
||||
|
||||
model = KnowledgeBaseCategory
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
""" Setup Test """
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
|
||||
self.item_create = self.model.objects.create(
|
||||
name = 'test_item_' + self.model._meta.model_name,
|
||||
organization = self.organization,
|
||||
)
|
||||
|
||||
|
||||
self.history_create = History.objects.get(
|
||||
action = History.Actions.ADD[0],
|
||||
item_pk = self.item_create.pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
self.item_change = self.item_create
|
||||
self.item_change.name = 'test_item_' + self.model._meta.model_name + '_changed'
|
||||
self.item_change.save()
|
||||
|
||||
self.field_after_expected_value = '{"name": "' + self.item_change.name + '"}'
|
||||
|
||||
self.history_change = History.objects.get(
|
||||
action = History.Actions.UPDATE[0],
|
||||
item_pk = self.item_change.pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
self.item_delete = self.model.objects.create(
|
||||
name = 'test_item_delete_' + self.model._meta.model_name,
|
||||
organization = self.organization,
|
||||
)
|
||||
|
||||
self.deleted_pk = self.item_delete.pk
|
||||
|
||||
self.item_delete.delete()
|
||||
|
||||
self.history_delete = History.objects.filter(
|
||||
item_pk = self.deleted_pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
|
||||
def test_history_entry_children_delete(self):
|
||||
""" Model has no child items """
|
||||
pass
|
||||
|
@ -0,0 +1,95 @@
|
||||
# from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from assistance.models.knowledge_base import KnowledgeBaseCategory
|
||||
|
||||
from core.tests.abstract.history_permissions import HistoryPermissions
|
||||
|
||||
|
||||
|
||||
class KnowledgeBaseHistoryPermissions(TestCase, HistoryPermissions):
|
||||
|
||||
|
||||
item_model = KnowledgeBaseCategory
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
2. create an organization that is different to item
|
||||
3. Create a device
|
||||
4. Add history device history entry as item
|
||||
5. create a user
|
||||
6. create user in different organization (with the required permission)
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
self.item = self.item_model.objects.create(
|
||||
organization=organization,
|
||||
name = 'deviceone'
|
||||
)
|
||||
|
||||
self.history = self.model.objects.get(
|
||||
item_pk = self.item.id,
|
||||
item_class = self.item._meta.model_name,
|
||||
action = self.model.Actions.ADD,
|
||||
)
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
view_team = Team.objects.create(
|
||||
team_name = 'view_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
view_team.permissions.set([view_permissions])
|
||||
|
||||
|
||||
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
|
||||
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = view_team,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
|
||||
|
||||
|
||||
different_organization_team = Team.objects.create(
|
||||
team_name = 'different_organization_team',
|
||||
organization = different_organization,
|
||||
)
|
||||
|
||||
different_organization_team.permissions.set([
|
||||
view_permissions,
|
||||
])
|
||||
|
||||
TeamUsers.objects.create(
|
||||
team = different_organization_team,
|
||||
user = self.different_organization_user
|
||||
)
|
@ -0,0 +1,189 @@
|
||||
# from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from app.tests.abstract.model_permissions import ModelPermissions
|
||||
|
||||
from assistance.models.knowledge_base import KnowledgeBaseCategory
|
||||
|
||||
|
||||
class KnowledgeBasePermissions(TestCase, ModelPermissions):
|
||||
|
||||
|
||||
model = KnowledgeBaseCategory
|
||||
|
||||
app_namespace = 'Settings'
|
||||
|
||||
url_name_view = '_knowledge_base_category_view'
|
||||
|
||||
url_name_add = '_knowledge_base_category_add'
|
||||
|
||||
url_name_change = '_knowledge_base_category_change'
|
||||
|
||||
url_name_delete = '_knowledge_base_category_delete'
|
||||
|
||||
url_delete_response = reverse('Settings:KB Categories')
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
. create an organization that is different to item
|
||||
2. Create a device
|
||||
3. create teams with each permission: view, add, change, delete
|
||||
4. create a user per team
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization=organization,
|
||||
name = 'deviceone'
|
||||
)
|
||||
|
||||
|
||||
self.url_view_kwargs = {'pk': self.item.id}
|
||||
|
||||
# self.url_add_kwargs = {'pk': self.item.id}
|
||||
|
||||
self.add_data = {'device': 'device', 'organization': self.organization.id}
|
||||
|
||||
self.url_change_kwargs = {'pk': self.item.id}
|
||||
|
||||
self.change_data = {'device': 'device', 'organization': self.organization.id}
|
||||
|
||||
self.url_delete_kwargs = {'pk': self.item.id}
|
||||
|
||||
self.delete_data = {'device': '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.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
|
||||
)
|
@ -0,0 +1,29 @@
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from app.tests.abstract.models import PrimaryModel
|
||||
|
||||
|
||||
|
||||
class ConfigManagementViews(
|
||||
TestCase,
|
||||
PrimaryModel
|
||||
):
|
||||
|
||||
add_module = 'assistance.views.knowledge_base_category'
|
||||
add_view = 'Add'
|
||||
|
||||
change_module = add_module
|
||||
change_view = 'Change'
|
||||
|
||||
delete_module = add_module
|
||||
delete_view = 'Delete'
|
||||
|
||||
display_module = add_module
|
||||
display_view = 'View'
|
||||
|
||||
index_module = add_module
|
||||
index_view = 'Index'
|
15
app/assistance/urls.py
Normal file
15
app/assistance/urls.py
Normal file
@ -0,0 +1,15 @@
|
||||
from django.urls import path
|
||||
|
||||
from assistance.views import knowledge_base
|
||||
|
||||
app_name = "Assistance"
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
path("information", knowledge_base.Index.as_view(), name="Knowledge Base"),
|
||||
path("information/add", knowledge_base.Add.as_view(), name="_knowledge_base_add"),
|
||||
path("information/<int:pk>/edit", knowledge_base.Change.as_view(), name="_knowledge_base_change"),
|
||||
path("information/<int:pk>/delete", knowledge_base.Delete.as_view(), name="_knowledge_base_delete"),
|
||||
path("information/<int:pk>", knowledge_base.View.as_view(), name="_knowledge_base_view"),
|
||||
|
||||
]
|
0
app/assistance/views/__init__.py
Normal file
0
app/assistance/views/__init__.py
Normal file
215
app/assistance/views/knowledge_base.py
Normal file
215
app/assistance/views/knowledge_base.py
Normal file
@ -0,0 +1,215 @@
|
||||
from datetime import datetime
|
||||
|
||||
from django.contrib.auth import decorators as auth_decorator
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from access.models import TeamUsers
|
||||
|
||||
from assistance.forms.knowledge_base import KnowledgeBaseForm
|
||||
from assistance.models.knowledge_base import KnowledgeBase
|
||||
|
||||
from core.forms.comment import AddNoteForm
|
||||
from core.models.notes import Notes
|
||||
from core.views.common import AddView, ChangeView, DeleteView, DisplayView, IndexView
|
||||
|
||||
from settings.models.user_settings import UserSettings
|
||||
|
||||
|
||||
|
||||
class Index(IndexView):
|
||||
|
||||
context_object_name = "items"
|
||||
|
||||
model = KnowledgeBase
|
||||
|
||||
paginate_by = 10
|
||||
|
||||
permission_required = [
|
||||
'assistance.view_knowledgebase'
|
||||
]
|
||||
|
||||
template_name = 'assistance/kb_index.html.j2'
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
if not self.request.user.has_perm('assistance.change_knowledgebase') and not self.request.user.is_superuser:
|
||||
|
||||
user_teams = []
|
||||
for team_user in TeamUsers.objects.filter(user=self.request.user):
|
||||
|
||||
if team_user.team.id not in user_teams:
|
||||
|
||||
user_teams += [ team_user.team.id ]
|
||||
|
||||
|
||||
context['items'] = self.get_queryset().filter(
|
||||
Q(expiry_date__lte=datetime.now())
|
||||
|
|
||||
Q(expiry_date=None)
|
||||
).filter(
|
||||
Q(target_team__in=user_teams)
|
||||
|
|
||||
Q(target_user=self.request.user.id)
|
||||
).distinct()
|
||||
|
||||
context['model_docs_path'] = self.model._meta.app_label + '/knowledge_base/'
|
||||
|
||||
context['content_title'] = 'Knowledge Base Articles'
|
||||
|
||||
return context
|
||||
|
||||
|
||||
|
||||
class Add(AddView):
|
||||
|
||||
form_class = KnowledgeBaseForm
|
||||
|
||||
model = KnowledgeBase
|
||||
|
||||
permission_required = [
|
||||
'assistance.add_knowledgebase',
|
||||
]
|
||||
|
||||
|
||||
def get_initial(self):
|
||||
|
||||
initial: dict = {
|
||||
'organization': UserSettings.objects.get(user = self.request.user).default_organization
|
||||
}
|
||||
|
||||
if 'pk' in self.kwargs:
|
||||
|
||||
if self.kwargs['pk']:
|
||||
|
||||
initial.update({'parent': self.kwargs['pk']})
|
||||
|
||||
self.model.parent.field.hidden = True
|
||||
|
||||
return initial
|
||||
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
|
||||
return reverse('Assistance:Knowledge Base')
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['content_title'] = 'New Group'
|
||||
|
||||
return context
|
||||
|
||||
|
||||
|
||||
class Change(ChangeView):
|
||||
|
||||
context_object_name = "group"
|
||||
|
||||
form_class = KnowledgeBaseForm
|
||||
|
||||
model = KnowledgeBase
|
||||
|
||||
permission_required = [
|
||||
'assistance.change_knowledgebase',
|
||||
]
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['content_title'] = self.object.title
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
|
||||
return reverse('Assistance:_knowledge_base_view', args=(self.kwargs['pk'],))
|
||||
|
||||
|
||||
|
||||
class View(ChangeView):
|
||||
|
||||
context_object_name = "kb"
|
||||
|
||||
form_class = KnowledgeBaseForm
|
||||
|
||||
model = KnowledgeBase
|
||||
|
||||
permission_required = [
|
||||
'assistance.view_knowledgebase',
|
||||
]
|
||||
|
||||
template_name = 'assistance/kb_article.html.j2'
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['notes_form'] = AddNoteForm(prefix='note')
|
||||
context['notes'] = Notes.objects.filter(config_group=self.kwargs['pk'])
|
||||
|
||||
context['model_pk'] = self.kwargs['pk']
|
||||
context['model_name'] = self.model._meta.model_name
|
||||
|
||||
context['model_delete_url'] = reverse('Assistance:_knowledge_base_delete', args=(self.kwargs['pk'],))
|
||||
|
||||
|
||||
context['content_title'] = self.object.title
|
||||
|
||||
return context
|
||||
|
||||
|
||||
@method_decorator(auth_decorator.permission_required("assistance.change_knowledgebase", raise_exception=True))
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
item = KnowledgeBase.objects.get(pk=self.kwargs['pk'])
|
||||
|
||||
notes = AddNoteForm(request.POST, prefix='note')
|
||||
|
||||
if notes.is_bound and notes.is_valid() and notes.instance.note != '':
|
||||
|
||||
notes.instance.organization = item.organization
|
||||
|
||||
notes.save()
|
||||
|
||||
# dont allow saving any post data outside notes.
|
||||
# todo: figure out what needs to be returned
|
||||
# return super().post(request, *args, **kwargs)
|
||||
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
|
||||
return reverse('Assistance:_knowledge_base_view', args=(self.kwargs['pk'],))
|
||||
|
||||
|
||||
|
||||
class Delete(DeleteView):
|
||||
|
||||
model = KnowledgeBase
|
||||
|
||||
permission_required = [
|
||||
'assistance.delete_knowledgebase',
|
||||
]
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['content_title'] = 'Delete ' + self.object.title
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
|
||||
return reverse('Assistance:Knowledge Base')
|
191
app/assistance/views/knowledge_base_category.py
Normal file
191
app/assistance/views/knowledge_base_category.py
Normal file
@ -0,0 +1,191 @@
|
||||
from django.contrib.auth import decorators as auth_decorator
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from assistance.forms.knowledge_base_category import KnowledgeBaseCategoryForm
|
||||
from assistance.models.knowledge_base import KnowledgeBase, KnowledgeBaseCategory
|
||||
|
||||
from core.forms.comment import AddNoteForm
|
||||
from core.models.notes import Notes
|
||||
from core.views.common import AddView, ChangeView, DeleteView, DisplayView, IndexView
|
||||
|
||||
from settings.models.user_settings import UserSettings
|
||||
|
||||
|
||||
|
||||
class Index(IndexView):
|
||||
|
||||
context_object_name = "items"
|
||||
|
||||
model = KnowledgeBaseCategory
|
||||
|
||||
paginate_by = 10
|
||||
|
||||
permission_required = [
|
||||
'assistance.view_knowledgebasecategory'
|
||||
]
|
||||
|
||||
template_name = 'assistance/kb_category_index.html.j2'
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['model_docs_path'] = self.model._meta.app_label + '/knowledge_base/'
|
||||
|
||||
context['content_title'] = 'Knowledge Base Categories'
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class Add(AddView):
|
||||
|
||||
form_class = KnowledgeBaseCategoryForm
|
||||
|
||||
model = KnowledgeBaseCategory
|
||||
|
||||
permission_required = [
|
||||
'assistance.add_knowledgebasecategory',
|
||||
]
|
||||
|
||||
|
||||
def get_initial(self):
|
||||
|
||||
initial: dict = {
|
||||
'organization': UserSettings.objects.get(user = self.request.user).default_organization
|
||||
}
|
||||
|
||||
if 'pk' in self.kwargs:
|
||||
|
||||
if self.kwargs['pk']:
|
||||
|
||||
initial.update({'parent': self.kwargs['pk']})
|
||||
|
||||
self.model.parent.field.hidden = True
|
||||
|
||||
return initial
|
||||
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
|
||||
return reverse('Settings:KB Categories')
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['content_title'] = 'New Group'
|
||||
|
||||
return context
|
||||
|
||||
|
||||
|
||||
class Change(ChangeView):
|
||||
|
||||
context_object_name = "group"
|
||||
|
||||
form_class = KnowledgeBaseCategoryForm
|
||||
|
||||
model = KnowledgeBaseCategory
|
||||
|
||||
permission_required = [
|
||||
'assistance.change_knowledgebasecategory',
|
||||
]
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['content_title'] = self.object.name
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
|
||||
return reverse('Settings:_knowledge_base_category_view', args=(self.kwargs['pk'],))
|
||||
|
||||
|
||||
|
||||
class View(ChangeView):
|
||||
|
||||
context_object_name = "item"
|
||||
|
||||
form_class = KnowledgeBaseCategoryForm
|
||||
|
||||
model = KnowledgeBaseCategory
|
||||
|
||||
permission_required = [
|
||||
'assistance.view_knowledgebasecategory',
|
||||
]
|
||||
|
||||
template_name = 'assistance/kb_category.html.j2'
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['articles'] = KnowledgeBase.objects.filter(category=self.kwargs['pk'])
|
||||
|
||||
context['notes_form'] = AddNoteForm(prefix='note')
|
||||
context['notes'] = Notes.objects.filter(config_group=self.kwargs['pk'])
|
||||
|
||||
context['model_pk'] = self.kwargs['pk']
|
||||
context['model_name'] = self.model._meta.model_name
|
||||
|
||||
context['model_delete_url'] = reverse('Settings:_knowledge_base_category_delete', args=(self.kwargs['pk'],))
|
||||
|
||||
|
||||
context['content_title'] = self.object.name
|
||||
|
||||
return context
|
||||
|
||||
|
||||
@method_decorator(auth_decorator.permission_required("assistance.change_knowledgebasecategory", raise_exception=True))
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
item = KnowledgeBase.objects.get(pk=self.kwargs['pk'])
|
||||
|
||||
notes = AddNoteForm(request.POST, prefix='note')
|
||||
|
||||
if notes.is_bound and notes.is_valid() and notes.instance.note != '':
|
||||
|
||||
notes.instance.organization = item.organization
|
||||
|
||||
notes.save()
|
||||
|
||||
# dont allow saving any post data outside notes.
|
||||
# todo: figure out what needs to be returned
|
||||
# return super().post(request, *args, **kwargs)
|
||||
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
|
||||
return reverse('Settings:_knowledge_base_category_view', args=(self.kwargs['pk'],))
|
||||
|
||||
|
||||
|
||||
class Delete(DeleteView):
|
||||
|
||||
model = KnowledgeBaseCategory
|
||||
|
||||
permission_required = [
|
||||
'assistance.delete_knowledgebasecategory',
|
||||
]
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['content_title'] = 'Delete ' + self.object.name
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
|
||||
return reverse('Settings:KB Categories')
|
@ -186,6 +186,21 @@ class ConfigGroups(GroupsCommonFields, SaveHistory):
|
||||
if self.parent:
|
||||
self.organization = ConfigGroups.objects.get(id=self.parent.id).organization
|
||||
|
||||
if self.pk:
|
||||
|
||||
obj = ConfigGroups.objects.get(
|
||||
id = self.id,
|
||||
)
|
||||
|
||||
# Prevent organization change. ToDo: add feature so that config can change organizations
|
||||
self.organization = obj.organization
|
||||
|
||||
if self.parent is not None:
|
||||
|
||||
if self.pk == self.parent.pk:
|
||||
|
||||
raise ValidationError('Can not set self as parent')
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
@ -193,7 +208,7 @@ class ConfigGroups(GroupsCommonFields, SaveHistory):
|
||||
|
||||
if self.parent:
|
||||
|
||||
return f'{self.parent.name} > {self.name}'
|
||||
return f'{self.parent} > {self.name}'
|
||||
|
||||
return self.name
|
||||
|
||||
|
@ -0,0 +1,224 @@
|
||||
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 Client, TestCase
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from app.tests.abstract.models import TenancyModel
|
||||
|
||||
from config_management.models.groups import ConfigGroups
|
||||
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class ConfigGroupsAPI(
|
||||
TestCase,
|
||||
):
|
||||
|
||||
model = ConfigGroups
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
2. Create an item
|
||||
|
||||
"""
|
||||
|
||||
self.organization = Organization.objects.create(name='test_org')
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
name = 'one',
|
||||
config = dict({"key": "one", "existing": "dont_over_write"})
|
||||
)
|
||||
|
||||
self.second_item = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
name = 'one_two',
|
||||
config = dict({"key": "two"}),
|
||||
parent = self.item
|
||||
)
|
||||
|
||||
self.url_view_kwargs = {'pk': self.second_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 = self.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('API:_api_config_group', 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_parent(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
parent field must exist
|
||||
"""
|
||||
|
||||
assert 'parent' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_parent(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
parent field must be dict
|
||||
"""
|
||||
|
||||
assert type(self.api_data['parent']) is dict
|
||||
|
||||
|
||||
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_config(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
config field must exist
|
||||
"""
|
||||
|
||||
assert 'config' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_config(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
config field must be dict
|
||||
"""
|
||||
|
||||
assert type(self.api_data['config']) is dict
|
||||
|
||||
|
||||
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_parent_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
parent.id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data['parent']
|
||||
|
||||
|
||||
def test_api_field_type_parent_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
parent.id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['parent']['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_parent_name(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
parent.name field must exist
|
||||
"""
|
||||
|
||||
assert 'name' in self.api_data['parent']
|
||||
|
||||
|
||||
def test_api_field_type_parent_name(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
parent.name field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['parent']['name']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_parent_url(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
parent.url field must exist
|
||||
"""
|
||||
|
||||
assert 'url' in self.api_data['parent']
|
||||
|
||||
|
||||
def test_api_field_type_parent_url(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
parent.url field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['parent']['url']) is str
|
@ -1,7 +1,6 @@
|
||||
import json
|
||||
|
||||
from django.contrib.auth import decorators as auth_decorator
|
||||
from django.db.models import Count, Q
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
@ -47,13 +46,7 @@ class GroupIndexView(IndexView):
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
if self.request.user.is_superuser:
|
||||
|
||||
return self.model.objects.filter(parent=None).order_by('name')
|
||||
|
||||
else:
|
||||
|
||||
return self.model.objects.filter(Q(parent=None, organization__in=self.user_organizations()) | Q(parent=None, is_global = True)).order_by('name')
|
||||
return self.model.objects.filter(parent=None).order_by('name')
|
||||
|
||||
|
||||
|
||||
|
20
app/core/migrations/0003_notes_service.py
Normal file
20
app/core/migrations/0003_notes_service.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.0.7 on 2024-07-21 02:35
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0002_notes'),
|
||||
('itim', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='notes',
|
||||
name='service',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itim.service'),
|
||||
),
|
||||
]
|
@ -10,6 +10,9 @@ from itam.models.device import Device
|
||||
from itam.models.software import Software
|
||||
from itam.models.operating_system import OperatingSystem
|
||||
|
||||
from itim.models.services import Service
|
||||
|
||||
|
||||
|
||||
class NotesCommonFields(TenancyObject, models.Model):
|
||||
|
||||
@ -88,6 +91,14 @@ class Notes(NotesCommonFields):
|
||||
blank= True
|
||||
)
|
||||
|
||||
service = models.ForeignKey(
|
||||
Service,
|
||||
on_delete=models.CASCADE,
|
||||
default = None,
|
||||
null = True,
|
||||
blank= True
|
||||
)
|
||||
|
||||
software = models.ForeignKey(
|
||||
Software,
|
||||
on_delete=models.CASCADE,
|
||||
|
@ -14,4 +14,4 @@ def json_pretty(value):
|
||||
|
||||
return str('{}')
|
||||
|
||||
return json.dumps(json.loads(value), indent=4, sort_keys=True)
|
||||
return json.dumps(json.loads(value.replace("'", '"')), indent=4, sort_keys=True)
|
||||
|
@ -9,4 +9,4 @@ register = template.Library()
|
||||
@register.filter()
|
||||
@stringfilter
|
||||
def markdown(value):
|
||||
return md.markdown(value, extensions=['markdown.extensions.fenced_code'])
|
||||
return md.markdown(value, extensions=['markdown.extensions.fenced_code', 'codehilite'])
|
@ -12,7 +12,12 @@ from itam.models.device import Device
|
||||
|
||||
|
||||
class HistoryPermissions:
|
||||
"""Test cases for accessing History """
|
||||
"""Test cases for accessing History
|
||||
|
||||
For this test to function properly you must add the history items model to
|
||||
`app.core.views.history.View.get_object()`. specifically an entry to the switch in the middle
|
||||
of the function.
|
||||
"""
|
||||
|
||||
|
||||
item: object
|
||||
|
@ -3,21 +3,38 @@ import unittest
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from access.models import TenancyManager
|
||||
|
||||
|
||||
class Models:
|
||||
""" Test cases for Model Abstract Classes """
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="write test")
|
||||
|
||||
def test_model_class_tenancy_manager_function_get_queryset(self):
|
||||
""" Function Check
|
||||
|
||||
function `get_queryset()` must exist
|
||||
"""
|
||||
|
||||
pass
|
||||
assert hasattr(self.model.objects, 'get_queryset')
|
||||
|
||||
assert callable(self.model.objects.get_queryset)
|
||||
|
||||
|
||||
@patch.object(TenancyManager, 'get_queryset')
|
||||
def test_model_class_tenancy_manager_function_get_queryset_called(self, get_queryset):
|
||||
""" Function Check
|
||||
|
||||
function `access.models.TenancyManager.get_queryset()` within the Tenancy manager must
|
||||
be called as this function limits queries to the current users organizations.
|
||||
"""
|
||||
|
||||
self.model.objects.filter()
|
||||
|
||||
assert get_queryset.called
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="write test")
|
||||
|
@ -1,9 +1,12 @@
|
||||
from django.template import Template, Context
|
||||
from django.utils.html import escape
|
||||
from django.views import generic
|
||||
|
||||
from access.mixin import OrganizationPermission
|
||||
|
||||
from core.exceptions import MissingAttribute
|
||||
|
||||
from settings.models.external_link import ExternalLink
|
||||
from settings.models.user_settings import UserSettings
|
||||
|
||||
|
||||
@ -50,6 +53,68 @@ class ChangeView(View, generic.UpdateView):
|
||||
|
||||
template_name:str = 'form.html.j2'
|
||||
|
||||
# ToDo: on migrating all views to seperate display and change views, external_links will not be required in `ChangView`
|
||||
def get_context_data(self, **kwargs):
|
||||
""" Get template context
|
||||
|
||||
For items that have the ability to have external links, this function
|
||||
adds the external link details to the context.
|
||||
|
||||
!!! Danger "Requirement"
|
||||
This function may be overridden with the caveat that this function is still called.
|
||||
by the overriding function. i.e. `super().get_context_data(skwargs)`
|
||||
|
||||
!!! note
|
||||
The adding of `external_links` within this view is scheduled to be removed.
|
||||
|
||||
Returns:
|
||||
(dict): Context for the template to use inclusive of 'external_links'
|
||||
"""
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
external_links_query = None
|
||||
|
||||
if 'tab' in self.request.GET:
|
||||
|
||||
context['open_tab'] = str(self.request.GET.get("tab")).lower()
|
||||
|
||||
else:
|
||||
context['open_tab'] = None
|
||||
|
||||
|
||||
if self.model._meta.model_name == 'device':
|
||||
|
||||
external_links_query = ExternalLink.objects.filter(devices=True)
|
||||
|
||||
elif self.model._meta.model_name == 'software':
|
||||
|
||||
external_links_query = ExternalLink.objects.filter(software=True)
|
||||
|
||||
|
||||
if external_links_query:
|
||||
|
||||
external_links: list = []
|
||||
|
||||
user_context = Context(context)
|
||||
|
||||
for external_link in external_links_query:
|
||||
|
||||
user_string = Template(external_link)
|
||||
external_link_context: dict = {
|
||||
'name': escape(external_link.name),
|
||||
'link': escape(user_string.render(user_context)),
|
||||
}
|
||||
|
||||
if external_link.colour:
|
||||
|
||||
external_link_context.update({'colour': external_link.colour })
|
||||
external_links += [ external_link_context ]
|
||||
|
||||
context['external_links'] = external_links
|
||||
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class DeleteView(OrganizationPermission, generic.DeleteView):
|
||||
@ -64,6 +129,60 @@ class DisplayView(OrganizationPermission, generic.DetailView):
|
||||
template_name:str = 'form.html.j2'
|
||||
|
||||
|
||||
# ToDo: on migrating all views to seperate display and change views, external_links will not be required in `ChangView`
|
||||
def get_context_data(self, **kwargs):
|
||||
""" Get template context
|
||||
|
||||
For items that have the ability to have external links, this function
|
||||
adds the external link details to the context.
|
||||
|
||||
!!! Danger "Requirement"
|
||||
This function may be overridden with the caveat that this function is still called.
|
||||
by the overriding function. i.e. `super().get_context_data(skwargs)`
|
||||
|
||||
Returns:
|
||||
(dict): Context for the template to use inclusive of 'external_links'
|
||||
"""
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
external_links_query = None
|
||||
|
||||
|
||||
if self.model._meta.model_name == 'device':
|
||||
|
||||
external_links_query = ExternalLink.objects.filter(devices=True)
|
||||
|
||||
elif self.model._meta.model_name == 'software':
|
||||
|
||||
external_links_query = ExternalLink.objects.filter(software=True)
|
||||
|
||||
|
||||
if external_links_query:
|
||||
|
||||
external_links: list = []
|
||||
|
||||
user_context = Context(context)
|
||||
|
||||
for external_link in external_links_query:
|
||||
|
||||
user_string = Template(external_link)
|
||||
external_link_context: dict = {
|
||||
'name': escape(external_link.name),
|
||||
'link': escape(user_string.render(user_context)),
|
||||
}
|
||||
|
||||
if external_link.colour:
|
||||
|
||||
external_link_context.update({'colour': external_link.colour })
|
||||
external_links += [ external_link_context ]
|
||||
|
||||
context['external_links'] = external_links
|
||||
|
||||
|
||||
return context
|
||||
|
||||
|
||||
|
||||
class IndexView(View, generic.ListView):
|
||||
|
||||
|
@ -41,6 +41,8 @@ class View(OrganizationPermission, generic.View):
|
||||
|
||||
from config_management.models.groups import ConfigGroups
|
||||
|
||||
from settings.models.external_link import ExternalLink
|
||||
|
||||
if not hasattr(self, 'model'):
|
||||
|
||||
match self.kwargs['model_name']:
|
||||
@ -61,6 +63,22 @@ class View(OrganizationPermission, generic.View):
|
||||
|
||||
self.model = DeviceType
|
||||
|
||||
case 'externallink':
|
||||
|
||||
self.model = ExternalLink
|
||||
|
||||
case 'knowledgebase':
|
||||
|
||||
from assistance.models.knowledge_base import KnowledgeBase
|
||||
|
||||
self.model = KnowledgeBase
|
||||
|
||||
case 'knowledgebasecategory':
|
||||
|
||||
from assistance.models.knowledge_base import KnowledgeBaseCategory
|
||||
|
||||
self.model = KnowledgeBaseCategory
|
||||
|
||||
case 'manufacturer':
|
||||
|
||||
self.model = Manufacturer
|
||||
@ -81,10 +99,22 @@ class View(OrganizationPermission, generic.View):
|
||||
|
||||
self.model = Organization
|
||||
|
||||
case 'port':
|
||||
|
||||
from itim.models.services import Port
|
||||
|
||||
self.model = Port
|
||||
|
||||
case 'team':
|
||||
|
||||
self.model = Team
|
||||
|
||||
case 'service':
|
||||
|
||||
from itim.models.services import Service
|
||||
|
||||
self.model = Service
|
||||
|
||||
case _:
|
||||
raise Exception('Unable to determine history items model')
|
||||
|
||||
|
@ -1,13 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
from .views import knowledge_base, playbooks
|
||||
|
||||
app_name = "Information"
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
path("kb/", knowledge_base.Index.as_view(), name="Knowledge Base"),
|
||||
path("playbook/", playbooks.Index.as_view(), name="Playbooks"),
|
||||
|
||||
]
|
@ -1,31 +0,0 @@
|
||||
import json
|
||||
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import render
|
||||
from django.template import Template, Context
|
||||
from django.views import generic
|
||||
|
||||
from access.mixin import OrganizationPermission
|
||||
|
||||
|
||||
|
||||
class Index(generic.View):
|
||||
|
||||
# permission_required = [
|
||||
# 'itil.view_knowledge_base'
|
||||
# ]
|
||||
|
||||
template_name = 'form.html.j2'
|
||||
|
||||
|
||||
def get(self, request):
|
||||
context = {}
|
||||
|
||||
user_string = Template("{% include 'icons/issue_link.html.j2' with issue=10 %}")
|
||||
user_context = Context(context)
|
||||
context['form'] = user_string.render(user_context)
|
||||
|
||||
|
||||
context['content_title'] = 'Knowledge Base'
|
||||
|
||||
return render(request, self.template_name, context)
|
@ -1,29 +0,0 @@
|
||||
import json
|
||||
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import render
|
||||
from django.template import Template, Context
|
||||
from django.views import generic
|
||||
|
||||
from access.mixin import OrganizationPermission
|
||||
|
||||
|
||||
|
||||
class Index(generic.View):
|
||||
|
||||
# permission_required = [
|
||||
# 'itil.view_playbook'
|
||||
# ]
|
||||
|
||||
template_name = 'form.html.j2'
|
||||
|
||||
def get(self, request):
|
||||
context = {}
|
||||
|
||||
user_string = Template("{% include 'icons/issue_link.html.j2' with issue=11 %}")
|
||||
user_context = Context(context)
|
||||
context['form'] = user_string.render(user_context)
|
||||
|
||||
context['content_title'] = 'Playbooks'
|
||||
|
||||
return render(request, self.template_name, context)
|
@ -23,6 +23,7 @@ class DeviceForm(CommonModelForm):
|
||||
'device_type',
|
||||
'organization',
|
||||
'model_notes',
|
||||
'config',
|
||||
]
|
||||
|
||||
|
||||
|
19
app/itam/migrations/0002_device_config.py
Normal file
19
app/itam/migrations/0002_device_config.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.0.7 on 2024-07-17 07:17
|
||||
|
||||
import itam.models.device
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('itam', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='config',
|
||||
field=models.JSONField(blank=True, default=None, help_text='Configuration for this device', null=True, validators=[itam.models.device.Device.validate_config_keys_not_reserved], verbose_name='Host Configuration'),
|
||||
),
|
||||
]
|
@ -1,8 +1,10 @@
|
||||
import json
|
||||
import re
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.db import models
|
||||
from django.forms import ValidationError
|
||||
|
||||
from access.fields import *
|
||||
from access.models import TenancyObject
|
||||
@ -18,6 +20,8 @@ from itam.models.operating_system import OperatingSystemVersion
|
||||
|
||||
from settings.models.app_settings import AppSettings
|
||||
|
||||
|
||||
|
||||
class DeviceType(DeviceCommonFieldsName, SaveHistory):
|
||||
|
||||
|
||||
@ -39,6 +43,49 @@ class DeviceType(DeviceCommonFieldsName, SaveHistory):
|
||||
|
||||
class Device(DeviceCommonFieldsName, SaveHistory):
|
||||
|
||||
|
||||
reserved_config_keys: list = [
|
||||
'software'
|
||||
]
|
||||
|
||||
def validate_config_keys_not_reserved(self):
|
||||
|
||||
value: dict = self
|
||||
|
||||
for invalid_key in Device.reserved_config_keys:
|
||||
|
||||
if invalid_key in value.keys():
|
||||
raise ValidationError(f'json key "{invalid_key}" is a reserved configuration key')
|
||||
|
||||
|
||||
def validate_uuid_format(self):
|
||||
|
||||
pattern = r'[0-9|a-f]{8}\-[0-9|a-f]{4}\-[0-9|a-f]{4}\-[0-9|a-f]{4}\-[0-9|a-f]{12}'
|
||||
|
||||
if not re.match(pattern, str(self)):
|
||||
|
||||
raise ValidationError(f'UUID Must be in {str(pattern)}')
|
||||
|
||||
|
||||
def validate_hostname_format(self):
|
||||
|
||||
pattern = r'^[a-z]{1}[a-z|0-9|\-]+[a-z|0-9]{1}$'
|
||||
|
||||
if not re.match(pattern, str(self).lower()):
|
||||
|
||||
raise ValidationError(
|
||||
'''[RFC1035 2.3.1] A hostname must start with a letter, end with a letter or digit,
|
||||
and have as interior characters only letters, digits, and hyphen.'''
|
||||
)
|
||||
|
||||
|
||||
name = models.CharField(
|
||||
blank = False,
|
||||
max_length = 50,
|
||||
unique = True,
|
||||
validators = [ validate_hostname_format ]
|
||||
)
|
||||
|
||||
serial_number = models.CharField(
|
||||
verbose_name = 'Serial Number',
|
||||
max_length = 50,
|
||||
@ -58,6 +105,7 @@ class Device(DeviceCommonFieldsName, SaveHistory):
|
||||
blank = True,
|
||||
unique = True,
|
||||
help_text = 'System GUID/UUID.',
|
||||
validators = [ validate_uuid_format ]
|
||||
)
|
||||
|
||||
device_model = models.ForeignKey(
|
||||
@ -76,16 +124,61 @@ class Device(DeviceCommonFieldsName, SaveHistory):
|
||||
null = True,
|
||||
blank= True,
|
||||
help_text = 'Type of device.',
|
||||
|
||||
)
|
||||
|
||||
|
||||
config = models.JSONField(
|
||||
blank = True,
|
||||
default = None,
|
||||
null = True,
|
||||
validators=[ validate_config_keys_not_reserved ],
|
||||
verbose_name = 'Host Configuration',
|
||||
help_text = 'Configuration for this device'
|
||||
)
|
||||
|
||||
inventorydate = models.DateTimeField(
|
||||
verbose_name = 'Last Inventory Date',
|
||||
null = True,
|
||||
blank = True,
|
||||
)
|
||||
|
||||
def save(
|
||||
self, force_insert=False, force_update=False, using=None, update_fields=None
|
||||
):
|
||||
""" Save Device Model
|
||||
|
||||
After saving the device update the related items so that they are a part
|
||||
of the same organization as the device.
|
||||
"""
|
||||
|
||||
super().save(
|
||||
force_insert=False, force_update=False, using=None, update_fields=None
|
||||
)
|
||||
|
||||
models_to_update =[
|
||||
DeviceSoftware,
|
||||
DeviceOperatingSystem
|
||||
]
|
||||
|
||||
for update_model in models_to_update:
|
||||
|
||||
obj = update_model.objects.filter(
|
||||
device = self.id,
|
||||
)
|
||||
|
||||
if obj.exists():
|
||||
|
||||
obj.update(
|
||||
is_global = False,
|
||||
organization = self.organization,
|
||||
)
|
||||
|
||||
from config_management.models.groups import ConfigGroupHosts
|
||||
|
||||
ConfigGroupHosts.objects.filter(
|
||||
host = self.id,
|
||||
).delete()
|
||||
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@ -184,6 +277,25 @@ class Device(DeviceCommonFieldsName, SaveHistory):
|
||||
|
||||
config['software'] = merge_software(group_software, host_software)
|
||||
|
||||
if self.config:
|
||||
|
||||
config.update(self.config)
|
||||
|
||||
from itim.models.services import Service
|
||||
services = Service.objects.filter(
|
||||
device = self.pk
|
||||
)
|
||||
|
||||
for service in services:
|
||||
|
||||
if service.config_variables:
|
||||
|
||||
service_config:dict = {
|
||||
service.config_key_variable: service.config_variables
|
||||
}
|
||||
|
||||
config.update(service_config)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@ -258,6 +370,16 @@ class DeviceSoftware(DeviceCommonFields, SaveHistory):
|
||||
return self.device
|
||||
|
||||
|
||||
def save(
|
||||
self, force_insert=False, force_update=False, using=None, update_fields=None
|
||||
):
|
||||
|
||||
self.is_global = False
|
||||
|
||||
super().save(
|
||||
force_insert=False, force_update=False, using=None, update_fields=None
|
||||
)
|
||||
|
||||
|
||||
class DeviceOperatingSystem(DeviceCommonFields, SaveHistory):
|
||||
|
||||
@ -300,3 +422,14 @@ class DeviceOperatingSystem(DeviceCommonFields, SaveHistory):
|
||||
""" Fetch the parent object """
|
||||
|
||||
return self.device
|
||||
|
||||
|
||||
def save(
|
||||
self, force_insert=False, force_update=False, using=None, update_fields=None
|
||||
):
|
||||
|
||||
self.is_global = False
|
||||
|
||||
super().save(
|
||||
force_insert=False, force_update=False, using=None, update_fields=None
|
||||
)
|
||||
|
@ -83,7 +83,9 @@
|
||||
<div id="Details" class="tabcontent">
|
||||
<h3>
|
||||
Details
|
||||
<span style="font-weight: normal; float: right;">{% include 'icons/issue_link.html.j2' with issue=6 %}</span>
|
||||
{% for external_link in external_links %}
|
||||
<span style="font-weight: normal; float: right;">{% include 'icons/external_link.html.j2' with external_link=external_link %}</span>
|
||||
{% endfor %}
|
||||
</h3>
|
||||
<div style="align-items:flex-start; align-content: center; display: flexbox; width: 100%">
|
||||
|
||||
@ -182,6 +184,35 @@
|
||||
<input type="submit" name="{{operating_system.prefix}}" value="Submit" />
|
||||
</div>
|
||||
|
||||
<div style="display: block; width: 100%;">
|
||||
<h3>Dependent Services</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Ports</th>
|
||||
</tr>
|
||||
{% if services %}
|
||||
{% for service in services %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ITIM:_service_view' service.pk %}">{{ service }}</a></td>
|
||||
<td>{% for port in service.port.all %}{{ port }} ({{ port.description}}), {% endfor %}</td>
|
||||
</tr>
|
||||
{% endfor%}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="2"> Nothing Found</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="display: block; width: 100%;">
|
||||
<h3>Device Config</h3>
|
||||
<br>
|
||||
<textarea cols="90" rows="30" readonly>{{ device.config }}</textarea>
|
||||
</div>
|
||||
|
||||
<input type="button" value="Edit" onclick="window.location='{% url 'ITAM:_device_change' device.id %}';">
|
||||
{% if not tab %}
|
||||
<script>
|
||||
// Get the element with id="defaultOpen" and click on it
|
||||
|
@ -43,8 +43,12 @@
|
||||
|
||||
<form method="post">
|
||||
<div id="Details" class="tabcontent">
|
||||
<h3>Details</h3>
|
||||
|
||||
<h3>
|
||||
Details
|
||||
{% for external_link in external_links %}
|
||||
<span style="font-weight: normal; float: right;">{% include 'icons/external_link.html.j2' with external_link=external_link %}</span>
|
||||
{% endfor %}
|
||||
</h3>
|
||||
{% csrf_token %}
|
||||
{{ form }}
|
||||
<br>
|
||||
|
@ -38,6 +38,16 @@ class Device(
|
||||
# name = 'deviceone'
|
||||
# )
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_device_move_organization(user):
|
||||
"""Move Organization test
|
||||
|
||||
When a device moves organization, devicesoftware and devicesoftware table data
|
||||
must also move organizations
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_device_software_action(user):
|
||||
"""Ensure only software that is from the same organization or is global can be added to the device
|
||||
|
522
app/itam/tests/unit/device/test_device_api.py
Normal file
522
app/itam/tests/unit/device/test_device_api.py
Normal file
@ -0,0 +1,522 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
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
|
||||
|
||||
from config_management.models.groups import ConfigGroups, ConfigGroupHosts
|
||||
|
||||
from itam.models.device import Device
|
||||
|
||||
|
||||
class DeviceAPI(TestCase):
|
||||
|
||||
|
||||
model = 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 = self.model.objects.create(
|
||||
organization=organization,
|
||||
name = 'deviceone',
|
||||
uuid = 'val',
|
||||
serial_number = 'another val'
|
||||
)
|
||||
|
||||
config_group = ConfigGroups.objects.create(
|
||||
organization = self.organization,
|
||||
name = 'one',
|
||||
config = dict({"key": "one", "existing": "dont_over_write"})
|
||||
)
|
||||
|
||||
config_group_second_item = ConfigGroups.objects.create(
|
||||
organization = self.organization,
|
||||
name = 'one_two',
|
||||
config = dict({"key": "two"}),
|
||||
parent = config_group
|
||||
)
|
||||
|
||||
config_group_hosts = ConfigGroupHosts.objects.create(
|
||||
organization = organization,
|
||||
host = self.item,
|
||||
group = config_group,
|
||||
)
|
||||
|
||||
|
||||
config_group_hosts_two = ConfigGroupHosts.objects.create(
|
||||
organization = organization,
|
||||
host = self.item,
|
||||
group = config_group_second_item,
|
||||
)
|
||||
|
||||
|
||||
# self.url_kwargs = {'pk': self.item.id}
|
||||
|
||||
self.url_view_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.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
|
||||
# )
|
||||
|
||||
|
||||
client = Client()
|
||||
url = reverse('API:device-detail', 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_is_global(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
is_global field must exist
|
||||
"""
|
||||
|
||||
assert 'is_global' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_is_global(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
is_global field must be boolean
|
||||
"""
|
||||
|
||||
assert type(self.api_data['is_global']) is bool
|
||||
|
||||
|
||||
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_config(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
config field must exist
|
||||
"""
|
||||
|
||||
assert 'config' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_config(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
config field must be dict
|
||||
"""
|
||||
|
||||
assert type(self.api_data['config']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_serial_number(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
serial_number field must exist
|
||||
"""
|
||||
|
||||
assert 'serial_number' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_serial_number(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
serial_number field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['serial_number']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_uuid(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
uuid field must exist
|
||||
"""
|
||||
|
||||
assert 'uuid' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_uuid(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
uuid field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['uuid']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_inventorydate(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
inventorydate field must exist
|
||||
"""
|
||||
|
||||
assert 'inventorydate' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_inventorydate(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
inventorydate field must be str
|
||||
"""
|
||||
|
||||
assert (
|
||||
type(self.api_data['inventorydate']) is str
|
||||
or
|
||||
self.api_data['inventorydate'] is None
|
||||
)
|
||||
|
||||
|
||||
def test_api_field_exists_created(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
created field must exist
|
||||
"""
|
||||
|
||||
assert 'created' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_created(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
created field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['created']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_modified(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
modified field must exist
|
||||
"""
|
||||
|
||||
assert 'modified' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_modified(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
modified field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['modified']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_groups(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
groups field must exist
|
||||
"""
|
||||
|
||||
assert 'groups' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_groups(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
groups field must be list
|
||||
"""
|
||||
|
||||
assert type(self.api_data['groups']) is list
|
||||
|
||||
|
||||
def test_api_field_exists_organization(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
organization field must exist
|
||||
"""
|
||||
|
||||
assert 'organization' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_organization(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
organization field must be dict
|
||||
"""
|
||||
|
||||
assert type(self.api_data['organization']) is dict
|
||||
|
||||
|
||||
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_organization_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
organization.id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data['organization']
|
||||
|
||||
|
||||
def test_api_field_type_organization_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
organization.id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['organization']['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_organization_name(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
organization.name field must exist
|
||||
"""
|
||||
|
||||
assert 'name' in self.api_data['organization']
|
||||
|
||||
|
||||
def test_api_field_type_organization_name(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
organization.name field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['organization']['name']) is str
|
||||
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_groups_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
groups.id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data['groups'][0]
|
||||
|
||||
|
||||
def test_api_field_type_groups_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
groups.id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['groups'][0]['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_groups_name(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
groups.name field must exist
|
||||
"""
|
||||
|
||||
assert 'name' in self.api_data['groups'][0]
|
||||
|
||||
|
||||
def test_api_field_type_groups_name(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
groups.name field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['groups'][0]['name']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_groups_url(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
groups.url field must exist
|
||||
"""
|
||||
|
||||
assert 'url' in self.api_data['groups'][0]
|
||||
|
||||
|
||||
def test_api_field_type_groups_url(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
groups.url field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['groups'][0]['url']) is Hyperlink
|
294
app/itam/tests/unit/software/test_software_api.py
Normal file
294
app/itam/tests/unit/software/test_software_api.py
Normal file
@ -0,0 +1,294 @@
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from rest_framework.relations import Hyperlink
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from core.models.manufacturer import Manufacturer
|
||||
|
||||
from itam.models.software import Software, SoftwareCategory
|
||||
|
||||
|
||||
class SoftwareAPI(TestCase):
|
||||
|
||||
|
||||
model = Software
|
||||
|
||||
app_namespace = 'API'
|
||||
|
||||
url_name = 'software-detail'
|
||||
|
||||
|
||||
@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 software
|
||||
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')
|
||||
|
||||
category = SoftwareCategory.objects.create(
|
||||
name='a category'
|
||||
)
|
||||
|
||||
publisher = Manufacturer.objects.create(
|
||||
name='a manufacturer'
|
||||
)
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization=organization,
|
||||
name = 'softwareone',
|
||||
model_notes = 'random str',
|
||||
category = category,
|
||||
publisher = publisher
|
||||
)
|
||||
|
||||
self.url_view_kwargs = {'pk': self.item.id}
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
view_team = Team.objects.create(
|
||||
team_name = 'view_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
view_team.permissions.set([view_permissions])
|
||||
|
||||
|
||||
self.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_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_is_global(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
is_global field must exist
|
||||
"""
|
||||
|
||||
assert 'is_global' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_is_global(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
is_global field must be boolean
|
||||
"""
|
||||
|
||||
assert type(self.api_data['is_global']) is bool
|
||||
|
||||
|
||||
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_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_slug(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
slug field must exist
|
||||
"""
|
||||
|
||||
assert 'slug' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_slug(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
slug field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['slug']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_created(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
created field must exist
|
||||
"""
|
||||
|
||||
assert 'created' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_created(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
created field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['created']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_modified(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
modified field must exist
|
||||
"""
|
||||
|
||||
assert 'modified' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_modified(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
modified field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['modified']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_organization(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
organization field must exist
|
||||
"""
|
||||
|
||||
assert 'organization' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_organization(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
organization field must be intt
|
||||
"""
|
||||
|
||||
assert type(self.api_data['organization']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_publisher(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
publisher field must exist
|
||||
"""
|
||||
|
||||
assert 'publisher' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_publisher(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
publisher field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['publisher']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_category(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
category field must exist
|
||||
"""
|
||||
|
||||
assert 'category' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_category(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
category field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['category']) is int
|
||||
|
@ -3,7 +3,6 @@ import markdown
|
||||
|
||||
from django.contrib.auth import decorators as auth_decorator
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
@ -22,10 +21,11 @@ from core.views.common import AddView, ChangeView, DeleteView, IndexView
|
||||
|
||||
from itam.forms.device_softwareadd import SoftwareAdd
|
||||
from itam.forms.device_softwareupdate import SoftwareUpdate
|
||||
|
||||
from itam.forms.device.device import DeviceForm
|
||||
from itam.forms.device.operating_system import Update as OperatingSystemForm
|
||||
|
||||
from itim.models.services import Service
|
||||
|
||||
from settings.models.user_settings import UserSettings
|
||||
|
||||
|
||||
@ -61,7 +61,7 @@ class IndexView(IndexView):
|
||||
|
||||
else:
|
||||
|
||||
return Device.objects.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True)).order_by('name')
|
||||
return Device.objects.filter().order_by('name')
|
||||
|
||||
|
||||
|
||||
@ -105,6 +105,8 @@ class View(ChangeView):
|
||||
|
||||
context['operating_system'] = OperatingSystemForm(prefix='operating_system')
|
||||
|
||||
context['services'] = Service.objects.filter(device=self.kwargs['pk'])
|
||||
|
||||
|
||||
softwares = DeviceSoftware.objects.filter(device=self.kwargs['pk'])
|
||||
softwares = Paginator(softwares, 10)
|
||||
|
@ -1,5 +1,5 @@
|
||||
from django.contrib.auth import decorators as auth_decorator
|
||||
from django.db.models import Q, Count
|
||||
from django.db.models import Count, Q
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
@ -40,7 +40,7 @@ class IndexView(IndexView):
|
||||
|
||||
else:
|
||||
|
||||
return OperatingSystem.objects.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True)).order_by('name')
|
||||
return OperatingSystem.objects.filter().order_by('name')
|
||||
|
||||
|
||||
|
||||
@ -62,7 +62,21 @@ class View(ChangeView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
operating_system_versions = OperatingSystemVersion.objects.filter(operating_system=self.kwargs['pk']).order_by('name').annotate(installs=Count("deviceoperatingsystem"))
|
||||
operating_system_versions = OperatingSystemVersion.objects.filter(
|
||||
operating_system=self.kwargs['pk']
|
||||
).order_by(
|
||||
'name'
|
||||
).annotate(
|
||||
installs=Count(
|
||||
"deviceoperatingsystem",
|
||||
filter=Q(deviceoperatingsystem__device__organization__in = self.user_organizations())
|
||||
),
|
||||
# filter=Q(deviceoperatingsystem__operating_system_version__organization__in = self.user_organizations())
|
||||
# filter=Q(deviceoperatingsystem__operating_system_version__deviceoperatingsystem__device__organization__in = self.user_organizations()),
|
||||
filter=Q(deviceoperatingsystem__operating_system_version__organization__in = self.user_organizations()),
|
||||
|
||||
)
|
||||
|
||||
context['operating_system_versions'] = operating_system_versions
|
||||
|
||||
installs = DeviceOperatingSystem.objects.filter(operating_system_version__operating_system_id=self.kwargs['pk'])
|
||||
|
@ -47,7 +47,7 @@ class IndexView(IndexView):
|
||||
|
||||
else:
|
||||
|
||||
return Software.objects.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True)).order_by('name')
|
||||
return Software.objects.filter().order_by('name')
|
||||
|
||||
|
||||
|
||||
@ -71,9 +71,12 @@ class View(ChangeView):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
software_versions = SoftwareVersion.objects.filter(
|
||||
software=self.kwargs['pk']
|
||||
software=self.kwargs['pk'],
|
||||
).annotate(
|
||||
installs=Count("installedversion")
|
||||
installs=Count(
|
||||
"installedversion",
|
||||
filter=Q(installedversion__organization__in = self.user_organizations())
|
||||
)
|
||||
)
|
||||
|
||||
context['software_versions'] = software_versions
|
||||
@ -98,9 +101,9 @@ class View(ChangeView):
|
||||
)
|
||||
|
||||
elif not self.request.user.is_superuser:
|
||||
|
||||
context['device_software'] = DeviceSoftware.objects.filter(
|
||||
Q(device__in=self.user_organizations(),
|
||||
software=self.kwargs['pk'])
|
||||
software=self.kwargs['pk']
|
||||
).order_by(
|
||||
'device',
|
||||
'organization'
|
||||
|
24
app/itim/forms/ports.py
Normal file
24
app/itim/forms/ports.py
Normal file
@ -0,0 +1,24 @@
|
||||
|
||||
# from django import forms
|
||||
# from django.forms import ValidationError
|
||||
|
||||
# from app import settings
|
||||
|
||||
from itim.models.services import Port
|
||||
|
||||
from core.forms.common import CommonModelForm
|
||||
|
||||
from settings.models.user_settings import UserSettings
|
||||
|
||||
|
||||
|
||||
class PortForm(CommonModelForm):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
fields = '__all__'
|
||||
|
||||
model = Port
|
||||
|
||||
prefix = 'port'
|
162
app/itim/forms/services.py
Normal file
162
app/itim/forms/services.py
Normal file
@ -0,0 +1,162 @@
|
||||
from django import forms
|
||||
from django.forms import ValidationError
|
||||
from django.urls import reverse
|
||||
|
||||
from itim.models.services import Service
|
||||
|
||||
from app import settings
|
||||
|
||||
from core.forms.common import CommonModelForm
|
||||
|
||||
|
||||
|
||||
class ServiceForm(CommonModelForm):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
fields = '__all__'
|
||||
|
||||
model = Service
|
||||
|
||||
prefix = 'service'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['dependent_service'].queryset = self.fields['dependent_service'].queryset.exclude(
|
||||
id=self.instance.pk
|
||||
).exclude(
|
||||
is_template=True
|
||||
)
|
||||
|
||||
self.fields['template'].queryset = self.fields['template'].queryset.exclude(
|
||||
id=self.instance.pk
|
||||
)
|
||||
|
||||
|
||||
def clean(self):
|
||||
|
||||
cleaned_data = super().clean()
|
||||
|
||||
pk = self.instance.id
|
||||
dependent_service = cleaned_data.get("dependent_service")
|
||||
device = cleaned_data.get("device")
|
||||
cluster = cleaned_data.get("cluster")
|
||||
is_template = cleaned_data.get("is_template")
|
||||
template = cleaned_data.get("template")
|
||||
port = cleaned_data.get("port")
|
||||
|
||||
|
||||
if not is_template and not template:
|
||||
|
||||
if not device and not cluster:
|
||||
|
||||
raise ValidationError('A Service must be assigned to either a "Cluster" or a "Device".')
|
||||
|
||||
|
||||
if device and cluster:
|
||||
|
||||
raise ValidationError('A Service must only be assigned to either a "Cluster" or a "Device". Not both.')
|
||||
|
||||
|
||||
if not port:
|
||||
|
||||
raise ValidationError('Port(s) must be assigned to a service.')
|
||||
|
||||
if dependent_service:
|
||||
|
||||
for dependency in dependent_service:
|
||||
|
||||
query = Service.objects.filter(
|
||||
dependent_service = pk,
|
||||
id = dependency.id,
|
||||
)
|
||||
|
||||
if query.exists():
|
||||
|
||||
raise ValidationError('A dependent service already depends upon this service. Circular dependencies are not allowed.')
|
||||
|
||||
|
||||
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
|
||||
class DetailForm(ServiceForm):
|
||||
|
||||
|
||||
tabs: dict = {
|
||||
"details": {
|
||||
"name": "Details",
|
||||
"slug": "details",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "double",
|
||||
"left": [
|
||||
'name',
|
||||
'config_key_variable',
|
||||
'template',
|
||||
'organization',
|
||||
'c_created',
|
||||
'c_modified'
|
||||
],
|
||||
"right": [
|
||||
'model_notes',
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"rendered_config": {
|
||||
"name": "Rendered Config",
|
||||
"slug": "rendered_config",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "single",
|
||||
"fields": [
|
||||
'config_variables',
|
||||
],
|
||||
"json": [
|
||||
'config_variables'
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
self.fields['config_variables'] = forms.fields.JSONField(
|
||||
widget = forms.Textarea(
|
||||
attrs = {
|
||||
"cols": "80",
|
||||
"rows": "100"
|
||||
}
|
||||
),
|
||||
label = 'Rendered Configuration',
|
||||
initial = self.instance.config_variables,
|
||||
)
|
||||
|
||||
self.fields['c_created'] = forms.DateTimeField(
|
||||
label = 'Created',
|
||||
input_formats=settings.DATETIME_FORMAT,
|
||||
disabled = True,
|
||||
initial = self.instance.created,
|
||||
)
|
||||
|
||||
self.fields['c_modified'] = forms.DateTimeField(
|
||||
label = 'Modified',
|
||||
input_formats=settings.DATETIME_FORMAT,
|
||||
disabled = True,
|
||||
initial = self.instance.modified,
|
||||
)
|
||||
|
||||
self.tabs['details'].update({
|
||||
"edit_url": reverse('ITIM:_service_change', args=(self.instance.pk,))
|
||||
})
|
102
app/itim/migrations/0001_initial.py
Normal file
102
app/itim/migrations/0001_initial.py
Normal file
@ -0,0 +1,102 @@
|
||||
# Generated by Django 5.0.7 on 2024-07-21 02:35
|
||||
|
||||
import access.fields
|
||||
import access.models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import itim.models.services
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('access', '0001_initial'),
|
||||
('itam', '0002_device_config'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ClusterType',
|
||||
fields=[
|
||||
('is_global', models.BooleanField(default=False)),
|
||||
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
|
||||
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
|
||||
('name', models.CharField(help_text='Name of the Cluster Type', max_length=50, verbose_name='Name')),
|
||||
('slug', access.fields.AutoSlugField()),
|
||||
('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': 'ClusterType',
|
||||
'verbose_name_plural': 'ClusterTypes',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Cluster',
|
||||
fields=[
|
||||
('is_global', models.BooleanField(default=False)),
|
||||
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
|
||||
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
|
||||
('name', models.CharField(help_text='Name of the Cluster', max_length=50, verbose_name='Name')),
|
||||
('slug', access.fields.AutoSlugField()),
|
||||
('config', models.JSONField(blank=True, default=None, help_text='Cluster Configuration', null=True, verbose_name='Configuration')),
|
||||
('devices', models.ManyToManyField(blank=True, default=None, help_text='Devices that are deployed upon the cluster.', related_name='cluster_device', to='itam.device', verbose_name='Devices')),
|
||||
('node', models.ManyToManyField(blank=True, default=None, help_text='Hosts for resource consumption that the cluster is deployed upon', related_name='cluster_node', to='itam.device', verbose_name='Nodes')),
|
||||
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
|
||||
('parent_cluster', models.ForeignKey(blank=True, default=None, help_text='Parent Cluster for this cluster', null=True, on_delete=django.db.models.deletion.CASCADE, to='itim.cluster', verbose_name='Parent Cluster')),
|
||||
('cluster_type', models.ForeignKey(blank=True, default=None, help_text='Parent Cluster for this cluster', null=True, on_delete=django.db.models.deletion.CASCADE, to='itim.clustertype', verbose_name='Parent Cluster')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Cluster',
|
||||
'verbose_name_plural': 'Clusters',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Port',
|
||||
fields=[
|
||||
('is_global', models.BooleanField(default=False)),
|
||||
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
|
||||
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
|
||||
('number', models.IntegerField(help_text='The port number', validators=[itim.models.services.Port.validation_port_number], verbose_name='Port Number')),
|
||||
('description', models.CharField(blank=True, default=None, help_text='Short description of port', max_length=80, null=True, verbose_name='Description')),
|
||||
('protocol', models.CharField(choices=[('TCP', 'TCP'), ('UDP', 'UDP')], default='TCP', help_text='Layer 4 Network Protocol', max_length=3, verbose_name='Protocol')),
|
||||
('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(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Protocol',
|
||||
'verbose_name_plural': 'Protocols',
|
||||
'ordering': ['number', 'protocol'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Service',
|
||||
fields=[
|
||||
('is_global', models.BooleanField(default=False)),
|
||||
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
|
||||
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
|
||||
('is_template', models.BooleanField(default=False, help_text='Is this service to be used as a template', verbose_name='Template')),
|
||||
('name', models.CharField(help_text='Name of the Service', max_length=50, verbose_name='Name')),
|
||||
('config', models.JSONField(blank=True, default=None, help_text='Cluster Configuration', null=True, verbose_name='Configuration')),
|
||||
('config_key_variable', models.CharField(help_text='Key name to use when merging with cluster/device config.', max_length=50, null=True, validators=[itim.models.services.Service.validate_config_key_variable], verbose_name='Configuration Key')),
|
||||
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
|
||||
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
|
||||
('cluster', models.ForeignKey(blank=True, default=None, help_text='Cluster the service is assigned to', null=True, on_delete=django.db.models.deletion.CASCADE, to='itim.cluster', verbose_name='Cluster')),
|
||||
('dependent_service', models.ManyToManyField(blank=True, default=None, help_text='Services that this service depends upon', related_name='dependentservice', to='itim.service', verbose_name='Dependent Services')),
|
||||
('device', models.ForeignKey(blank=True, default=None, help_text='Device the service is assigned to', null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.device', verbose_name='Device')),
|
||||
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
|
||||
('port', models.ManyToManyField(blank=True, help_text='Port the service is available on', to='itim.port', verbose_name='Port')),
|
||||
('template', models.ForeignKey(blank=True, default=None, help_text='Template this service uses', null=True, on_delete=django.db.models.deletion.CASCADE, to='itim.service', verbose_name='Template Name')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Service',
|
||||
'verbose_name_plural': 'Services',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
]
|
0
app/itim/models/__init__.py
Normal file
0
app/itim/models/__init__.py
Normal file
123
app/itim/models/clusters.py
Normal file
123
app/itim/models/clusters.py
Normal file
@ -0,0 +1,123 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.forms import ValidationError
|
||||
|
||||
from access.fields import *
|
||||
from access.models import Team, TenancyObject
|
||||
|
||||
from itam.models.device import Device
|
||||
|
||||
|
||||
|
||||
class ClusterType(TenancyObject):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
ordering = [
|
||||
'name',
|
||||
]
|
||||
|
||||
verbose_name = "ClusterType"
|
||||
|
||||
verbose_name_plural = "ClusterTypes"
|
||||
|
||||
|
||||
id = models.AutoField(
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
blank=False
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
blank = False,
|
||||
help_text = 'Name of the Cluster Type',
|
||||
max_length = 50,
|
||||
unique = False,
|
||||
verbose_name = 'Name',
|
||||
)
|
||||
|
||||
slug = AutoSlugField()
|
||||
|
||||
|
||||
|
||||
class Cluster(TenancyObject):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
ordering = [
|
||||
'name',
|
||||
]
|
||||
|
||||
verbose_name = "Cluster"
|
||||
|
||||
verbose_name_plural = "Clusters"
|
||||
|
||||
|
||||
id = models.AutoField(
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
blank=False
|
||||
)
|
||||
|
||||
parent_cluster = models.ForeignKey(
|
||||
'self',
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Parent Cluster for this cluster',
|
||||
null = True,
|
||||
on_delete = models.CASCADE,
|
||||
verbose_name = 'Parent Cluster',
|
||||
)
|
||||
|
||||
cluster_type = models.ForeignKey(
|
||||
ClusterType,
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Parent Cluster for this cluster',
|
||||
null = True,
|
||||
on_delete = models.CASCADE,
|
||||
verbose_name = 'Parent Cluster',
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
blank = False,
|
||||
help_text = 'Name of the Cluster',
|
||||
max_length = 50,
|
||||
unique = False,
|
||||
verbose_name = 'Name',
|
||||
)
|
||||
|
||||
slug = AutoSlugField()
|
||||
|
||||
config = models.JSONField(
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Cluster Configuration',
|
||||
null = True,
|
||||
verbose_name = 'Configuration',
|
||||
)
|
||||
|
||||
node = models.ManyToManyField(
|
||||
Device,
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Hosts for resource consumption that the cluster is deployed upon',
|
||||
related_name = 'cluster_node',
|
||||
verbose_name = 'Nodes',
|
||||
)
|
||||
|
||||
devices = models.ManyToManyField(
|
||||
Device,
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Devices that are deployed upon the cluster.',
|
||||
related_name = 'cluster_device',
|
||||
verbose_name = 'Devices',
|
||||
)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return self.name
|
235
app/itim/models/services.py
Normal file
235
app/itim/models/services.py
Normal file
@ -0,0 +1,235 @@
|
||||
import re
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.forms import ValidationError
|
||||
|
||||
from access.fields import *
|
||||
from access.models import Team, TenancyObject
|
||||
|
||||
from itam.models.device import Device
|
||||
|
||||
from itim.models.clusters import Cluster
|
||||
|
||||
|
||||
|
||||
class Port(TenancyObject):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
ordering = [
|
||||
'number',
|
||||
'protocol',
|
||||
]
|
||||
|
||||
verbose_name = "Protocol"
|
||||
|
||||
verbose_name_plural = "Protocols"
|
||||
|
||||
|
||||
class Protocol(models.TextChoices):
|
||||
TCP = 'TCP', 'TCP'
|
||||
UDP = 'UDP', 'UDP'
|
||||
|
||||
def validation_port_number(number: int):
|
||||
|
||||
if number < 1 or number > 65535:
|
||||
|
||||
raise ValidationError('A Valid port number is between 1-65535')
|
||||
|
||||
|
||||
id = models.AutoField(
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
blank=False
|
||||
)
|
||||
|
||||
number = models.IntegerField(
|
||||
blank = False,
|
||||
help_text = 'The port number',
|
||||
unique = False,
|
||||
validators = [ validation_port_number ],
|
||||
verbose_name = 'Port Number',
|
||||
)
|
||||
|
||||
description = models.CharField(
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Short description of port',
|
||||
max_length = 80,
|
||||
null = True,
|
||||
verbose_name = 'Description',
|
||||
)
|
||||
|
||||
protocol = models.CharField(
|
||||
blank = False,
|
||||
choices=Protocol.choices,
|
||||
default = Protocol.TCP,
|
||||
help_text = 'Layer 4 Network Protocol',
|
||||
max_length = 3,
|
||||
verbose_name = 'Protocol',
|
||||
)
|
||||
|
||||
created = AutoCreatedField()
|
||||
|
||||
modified = AutoLastModifiedField()
|
||||
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return str(self.protocol) + '/' + str(self.number)
|
||||
|
||||
|
||||
|
||||
class Service(TenancyObject):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
ordering = [
|
||||
'name',
|
||||
]
|
||||
|
||||
verbose_name = "Service"
|
||||
|
||||
verbose_name_plural = "Services"
|
||||
|
||||
def validate_config_key_variable(value):
|
||||
|
||||
if not value:
|
||||
|
||||
raise ValidationError('You must enter a config key.')
|
||||
|
||||
valid_chars = search=re.compile(r'[^a-z_]').search
|
||||
|
||||
if bool(valid_chars(value)):
|
||||
|
||||
raise ValidationError('config key must only contain [a-z_].')
|
||||
|
||||
|
||||
id = models.AutoField(
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
blank=False
|
||||
)
|
||||
|
||||
is_template = models.BooleanField(
|
||||
blank = False,
|
||||
default = False,
|
||||
help_text = 'Is this service to be used as a template',
|
||||
verbose_name = 'Template',
|
||||
)
|
||||
|
||||
template = models.ForeignKey(
|
||||
'self',
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Template this service uses',
|
||||
null = True,
|
||||
on_delete = models.CASCADE,
|
||||
verbose_name = 'Template Name',
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
blank = False,
|
||||
help_text = 'Name of the Service',
|
||||
max_length = 50,
|
||||
unique = False,
|
||||
verbose_name = 'Name',
|
||||
)
|
||||
|
||||
device = models.ForeignKey(
|
||||
Device,
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Device the service is assigned to',
|
||||
null = True,
|
||||
on_delete = models.CASCADE,
|
||||
verbose_name = 'Device',
|
||||
)
|
||||
|
||||
cluster = models.ForeignKey(
|
||||
'Cluster',
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Cluster the service is assigned to',
|
||||
null = True,
|
||||
on_delete = models.CASCADE,
|
||||
unique = False,
|
||||
verbose_name = 'Cluster',
|
||||
)
|
||||
|
||||
config = models.JSONField(
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Cluster Configuration',
|
||||
null = True,
|
||||
verbose_name = 'Configuration',
|
||||
)
|
||||
|
||||
config_key_variable = models.CharField(
|
||||
blank = False,
|
||||
help_text = 'Key name to use when merging with cluster/device config.',
|
||||
max_length = 50,
|
||||
null = True,
|
||||
unique = False,
|
||||
validators = [ validate_config_key_variable ],
|
||||
verbose_name = 'Configuration Key',
|
||||
)
|
||||
|
||||
port = models.ManyToManyField(
|
||||
Port,
|
||||
blank = True,
|
||||
help_text = 'Port the service is available on',
|
||||
verbose_name = 'Port',
|
||||
)
|
||||
|
||||
dependent_service = models.ManyToManyField(
|
||||
'self',
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Services that this service depends upon',
|
||||
related_name = 'dependentservice',
|
||||
symmetrical = False,
|
||||
verbose_name = 'Dependent Services',
|
||||
)
|
||||
|
||||
created = AutoCreatedField()
|
||||
|
||||
modified = AutoLastModifiedField()
|
||||
|
||||
@property
|
||||
def config_variables(self):
|
||||
|
||||
if self.is_template:
|
||||
|
||||
return self.config
|
||||
|
||||
if self.template:
|
||||
|
||||
template_config: dict = Service.objects.get(id=self.template.id).config
|
||||
|
||||
template_config.update(self.config)
|
||||
|
||||
return template_config
|
||||
|
||||
else:
|
||||
|
||||
return self.config
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||
|
||||
if self.config_key_variable:
|
||||
|
||||
self.config_key_variable = self.config_key_variable.lower()
|
||||
|
||||
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return self.name
|
196
app/itim/templates/itim/port.html.j2
Normal file
196
app/itim/templates/itim/port.html.j2
Normal file
@ -0,0 +1,196 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% load markdown %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<script>
|
||||
|
||||
function openCity(evt, cityName) {
|
||||
var i, tabcontent, tablinks;
|
||||
|
||||
tabcontent = document.getElementsByClassName("tabcontent");
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].style.display = "none";
|
||||
}
|
||||
|
||||
tablinks = document.getElementsByClassName("tablinks");
|
||||
for (i = 0; i < tablinks.length; i++) {
|
||||
tablinks[i].className = tablinks[i].className.replace(" active", "");
|
||||
}
|
||||
|
||||
document.getElementById(cityName).style.display = "block";
|
||||
evt.currentTarget.className += " active";
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.detail-view-field {
|
||||
display: unset;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 0px 20px 40px 20px;
|
||||
|
||||
}
|
||||
|
||||
.detail-view-field label {
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
width: 200px;
|
||||
margin: 10px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
|
||||
}
|
||||
|
||||
.detail-view-field span {
|
||||
display: inline-block;
|
||||
width: 340px;
|
||||
margin: 10px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
|
||||
}
|
||||
|
||||
pre {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
<div class="tab">
|
||||
<button onclick="window.location='{% url 'Settings:_ports' %}';"
|
||||
style="vertical-align: middle; padding: auto; margin: 0px">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="25px" viewBox="0 -960 960 960" width="25px"
|
||||
style="vertical-align: middle; margin: 0px; padding: 0px border: none; " fill="#6a6e73">
|
||||
<path d="m313-480 155 156q11 11 11.5 27.5T468-268q-11 11-28 11t-28-11L228-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T468-692q11 11 11 28t-11 28L313-480Zm264 0 155 156q11 11 11.5 27.5T732-268q-11 11-28 11t-28-11L492-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T732-692q11 11 11 28t-11 28L577-480Z" />
|
||||
</svg>Back to Ports</button>
|
||||
|
||||
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
|
||||
<button class="tablinks" onclick="openCity(event, 'Services')">Services</button>
|
||||
{% if perms.assistance.change_service %}
|
||||
<button class="tablinks" onclick="openCity(event, 'Notes')">Notes</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
<div id="Details" class="tabcontent">
|
||||
<h3>Details</h3>
|
||||
|
||||
{% csrf_token %}
|
||||
|
||||
|
||||
<div style="align-items:flex-start; align-content: center; display: flexbox; width: 100%">
|
||||
|
||||
<div style="display: inline; width: 40%; margin: 30px;">
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.number.label }}</label>
|
||||
<span>{{ form.number.value }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.description.label }}</label>
|
||||
<span>
|
||||
{% if form.description.value %}
|
||||
{{ form.description.value }}
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.protocol.label }}</label>
|
||||
<span>{{ form.protocol.value }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.organization.label }}</label>
|
||||
<span>{{ item.organization }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>Created</label>
|
||||
<span>{{ item.created }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>Modified</label>
|
||||
<span>{{ item.modified }}</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div style="display: inline; width: 40%; margin: 30px; text-align: left;">
|
||||
<div>
|
||||
<label
|
||||
style="font-weight: bold; width: 100%; border-bottom: 1px solid #ccc; display: block; text-align: inherit;">{{ form.model_notes.label }}</label>
|
||||
|
||||
<div style="display: inline-block; text-align: left;">
|
||||
{% if form.model_notes.value %}
|
||||
{{ form.model_notes.value | markdown | safe }}
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<input type="button" value="Edit" onclick="window.location='{% url 'Settings:_port_change' item.pk %}';">
|
||||
|
||||
<br>
|
||||
|
||||
<script>
|
||||
document.getElementById("defaultOpen").click();
|
||||
</script>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="Services" class="tabcontent">
|
||||
<h3>
|
||||
Services
|
||||
</h3>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Organization</th>
|
||||
</tr>
|
||||
{% for service in services %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ITIM:_service_view' service.pk %}">{{ service.name }}</a></td>
|
||||
<td>{{ service.organization }}</td>
|
||||
</tr>
|
||||
{% endfor%}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% if perms.assistance.change_knowledgebase %}
|
||||
<div id="Notes" class="tabcontent">
|
||||
<h3>
|
||||
Notes
|
||||
</h3>
|
||||
{{ notes_form }}
|
||||
<input type="submit" name="{{ notes_form.prefix }}" value="Submit" />
|
||||
<div class="comments">
|
||||
{% if notes %}
|
||||
{% for note in notes %}
|
||||
{% include 'note.html.j2' %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
53
app/itim/templates/itim/port_index.html.j2
Normal file
53
app/itim/templates/itim/port_index.html.j2
Normal file
@ -0,0 +1,53 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<input type="button" value="New Port" onclick="window.location='{% url 'Settings:_port_add' %}';">
|
||||
<table class="data">
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Cluster / Device</th>
|
||||
<th>Organization</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
{% if items %}
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td><a href="{% url 'Settings:_port_view' pk=item.id %}">{{ item.protocol }}/{{ item.number }}</a></td>
|
||||
<td>
|
||||
{% if item.device %}
|
||||
{{ item.device }}
|
||||
{% else %}
|
||||
{{ item.cluster }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.organization }}</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4">Nothing Found</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
<br>
|
||||
<div class="pagination">
|
||||
<span class="step-links">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?page=1">« first</a>
|
||||
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="current">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
|
||||
</span>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?page={{ page_obj.next_page_number }}">next</a>
|
||||
<a href="?page={{ page_obj.paginator.num_pages }}">last »</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
75
app/itim/templates/itim/service.html.j2
Normal file
75
app/itim/templates/itim/service.html.j2
Normal file
@ -0,0 +1,75 @@
|
||||
{% extends 'detail.html.j2' %}
|
||||
|
||||
{% load json %}
|
||||
{% load markdown %}
|
||||
|
||||
|
||||
{% block tabs %}
|
||||
|
||||
<div id="details" class="content-tab">
|
||||
|
||||
{% include 'content/section.html.j2' with tab=form.tabs.details %}
|
||||
|
||||
<hr />
|
||||
|
||||
<div style="display: block; width: 100%;">
|
||||
<h3>Ports</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
{% if item.port.all and not item.template %}
|
||||
{% for port in item.port.all %}
|
||||
<tr>
|
||||
<td><a href="{% url 'Settings:_port_view' item.pk %}">{{ port }}</a></td>
|
||||
<td>{{ port.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% elif not item.port.all and item.template %}
|
||||
{% for port in item.template.port.all %}
|
||||
<tr>
|
||||
<td><a href="{% url 'Settings:_port_view' item.pk %}">{{ port }}</a></td>
|
||||
<td>{{ port.description }}</td>
|
||||
</tr>
|
||||
{% endfor%}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="2"> Nothing Found</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="display: block; width: 100%;">
|
||||
<h3>Dependent Services</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Organization</th>
|
||||
</tr>
|
||||
{% if item.dependent_service.all %}
|
||||
{% for service in item.dependent_service.all %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ITIM:_service_view' service.pk %}">{{ service }}</a></td>
|
||||
<td>{{ service.organization }}</td>
|
||||
</tr>
|
||||
{% endfor%}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="2"> Nothing Found</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div id="rendered_config" class="content-tab">
|
||||
|
||||
{% include 'content/section.html.j2' with tab=form.tabs.rendered_config %}
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
53
app/itim/templates/itim/service_index.html.j2
Normal file
53
app/itim/templates/itim/service_index.html.j2
Normal file
@ -0,0 +1,53 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<input type="button" value="New Article" onclick="window.location='{% url 'ITIM:_service_add' %}';">
|
||||
<table class="data">
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Cluster / Device</th>
|
||||
<th>Organization</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
{% if items %}
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ITIM:_service_view' pk=item.id %}">{{ item.name }}</a></td>
|
||||
<td>
|
||||
{% if item.device %}
|
||||
{{ item.device }}
|
||||
{% else %}
|
||||
{{ item.cluster }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.organization }}</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4">Nothing Found</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
<br>
|
||||
<div class="pagination">
|
||||
<span class="step-links">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?page=1">« first</a>
|
||||
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="current">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
|
||||
</span>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?page={{ page_obj.next_page_number }}">next</a>
|
||||
<a href="?page={{ page_obj.paginator.num_pages }}">last »</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
42
app/itim/tests/unit/port/test_port.py
Normal file
42
app/itim/tests/unit/port/test_port.py
Normal file
@ -0,0 +1,42 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from access.models import Organization
|
||||
|
||||
from app.tests.abstract.models import TenancyModel
|
||||
|
||||
from itim.models.services import Port
|
||||
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class PortModel(
|
||||
TestCase,
|
||||
TenancyModel
|
||||
):
|
||||
|
||||
model = Port
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
2. Create an item
|
||||
|
||||
"""
|
||||
|
||||
self.organization = Organization.objects.create(name='test_org')
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
number = 1,
|
||||
)
|
||||
|
||||
self.second_item = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
number = 2,
|
||||
)
|
78
app/itim/tests/unit/port/test_port_core_history.py
Normal file
78
app/itim/tests/unit/port/test_port_core_history.py
Normal file
@ -0,0 +1,78 @@
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from django.test import TestCase, Client
|
||||
|
||||
from access.models import Organization
|
||||
|
||||
from core.models.history import History
|
||||
from core.tests.abstract.history_entry import HistoryEntry
|
||||
from core.tests.abstract.history_entry_parent_model import HistoryEntryParentItem
|
||||
|
||||
from itim.models.services import Port
|
||||
|
||||
|
||||
|
||||
class PortHistory(TestCase, HistoryEntry, HistoryEntryParentItem):
|
||||
|
||||
|
||||
model = Port
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
""" Setup Test """
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
self.item_parent = self.model.objects.create(
|
||||
number = 1,
|
||||
organization = self.organization
|
||||
)
|
||||
|
||||
self.item_create = self.model.objects.create(
|
||||
number = 2,
|
||||
organization = self.organization,
|
||||
)
|
||||
|
||||
|
||||
self.history_create = History.objects.get(
|
||||
action = History.Actions.ADD[0],
|
||||
item_pk = self.item_create.pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
self.item_change = self.item_create
|
||||
self.item_change.number = 3
|
||||
self.item_change.save()
|
||||
|
||||
self.field_after_expected_value = '{"number": ' + str(self.item_change.number) + '}'
|
||||
|
||||
self.history_change = History.objects.get(
|
||||
action = History.Actions.UPDATE[0],
|
||||
item_pk = self.item_change.pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
self.item_delete = self.model.objects.create(
|
||||
number = 4,
|
||||
organization = self.organization,
|
||||
)
|
||||
|
||||
self.deleted_pk = self.item_delete.pk
|
||||
|
||||
self.item_delete.delete()
|
||||
|
||||
self.history_delete = History.objects.filter(
|
||||
item_pk = self.deleted_pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
self.history_delete_children = History.objects.filter(
|
||||
item_parent_pk = self.deleted_pk,
|
||||
item_parent_class = self.item_parent._meta.model_name,
|
||||
)
|
95
app/itim/tests/unit/port/test_port_history_permission.py
Normal file
95
app/itim/tests/unit/port/test_port_history_permission.py
Normal file
@ -0,0 +1,95 @@
|
||||
# from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from itim.models.services import Port
|
||||
|
||||
from core.tests.abstract.history_permissions import HistoryPermissions
|
||||
|
||||
|
||||
|
||||
class PortHistoryPermissions(TestCase, HistoryPermissions):
|
||||
|
||||
|
||||
item_model = Port
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
2. create an organization that is different to item
|
||||
3. Create a device
|
||||
4. Add history device history entry as item
|
||||
5. create a user
|
||||
6. create user in different organization (with the required permission)
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
self.item = self.item_model.objects.create(
|
||||
organization=organization,
|
||||
number = 1
|
||||
)
|
||||
|
||||
self.history = self.model.objects.get(
|
||||
item_pk = self.item.id,
|
||||
item_class = self.item._meta.model_name,
|
||||
action = self.model.Actions.ADD,
|
||||
)
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
view_team = Team.objects.create(
|
||||
team_name = 'view_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
view_team.permissions.set([view_permissions])
|
||||
|
||||
|
||||
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
|
||||
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = view_team,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
|
||||
|
||||
|
||||
different_organization_team = Team.objects.create(
|
||||
team_name = 'different_organization_team',
|
||||
organization = different_organization,
|
||||
)
|
||||
|
||||
different_organization_team.permissions.set([
|
||||
view_permissions,
|
||||
])
|
||||
|
||||
TeamUsers.objects.create(
|
||||
team = different_organization_team,
|
||||
user = self.different_organization_user
|
||||
)
|
189
app/itim/tests/unit/port/test_port_permission.py
Normal file
189
app/itim/tests/unit/port/test_port_permission.py
Normal file
@ -0,0 +1,189 @@
|
||||
# from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from app.tests.abstract.model_permissions import ModelPermissions
|
||||
|
||||
from itim.models.services import Port
|
||||
|
||||
|
||||
class PortPermissions(TestCase, ModelPermissions):
|
||||
|
||||
|
||||
model = Port
|
||||
|
||||
app_namespace = 'Settings'
|
||||
|
||||
url_name_view = '_port_view'
|
||||
|
||||
url_name_add = '_port_add'
|
||||
|
||||
url_name_change = '_port_change'
|
||||
|
||||
url_name_delete = '_port_delete'
|
||||
|
||||
url_delete_response = reverse('Settings:_ports')
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
. create an organization that is different to item
|
||||
2. Create a device
|
||||
3. create teams with each permission: view, add, change, delete
|
||||
4. create a user per team
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization=organization,
|
||||
number = 1
|
||||
)
|
||||
|
||||
|
||||
self.url_view_kwargs = {'pk': self.item.id}
|
||||
|
||||
# self.url_add_kwargs = {'pk': self.item.id}
|
||||
|
||||
self.add_data = {'device': 'device', 'organization': self.organization.id}
|
||||
|
||||
self.url_change_kwargs = {'pk': self.item.id}
|
||||
|
||||
self.change_data = {'device': 'device', 'organization': self.organization.id}
|
||||
|
||||
self.url_delete_kwargs = {'pk': self.item.id}
|
||||
|
||||
self.delete_data = {'device': '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.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
|
||||
)
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user