Compare commits
476 Commits
Author | SHA1 | Date | |
---|---|---|---|
ca3b99cb3a | |||
8d4f686f6c | |||
6f57bd84e7 | |||
6351acabe5 | |||
9d6ea1d7c3 | |||
035c6ed60c | |||
31e88b2f96 | |||
28f51d3bb6 | |||
c634695e4e | |||
983921fc22 | |||
9fc5b0eb09 | |||
b1fc8e0f98 | |||
551473feb7 | |||
105cb63d71 | |||
1dda4a9fb5 | |||
c53ec9ec5d | |||
a44b2479e3 | |||
ec26e16132 | |||
59a930f934 | |||
c7701bb2df | |||
4ac0da6ba2 | |||
19ad262617 | |||
0e987088a3 | |||
c74b89e0d6 | |||
e762713416 | |||
facdd0111b | |||
280abb8841 | |||
064f74736f | |||
d26868eced | |||
b5ec42fc56 | |||
104575780a | |||
1c1f4ecdfa | |||
582ee4031d | |||
fca8ad5a78 | |||
fff3a96889 | |||
dfdc5bac9d | |||
c022551427 | |||
76954c019b | |||
c3de79050e | |||
3cce436938 | |||
ed2bf96626 | |||
3693ddadad | |||
8866f94fab | |||
0ae1395d92 | |||
cf73323dd3 | |||
f019c50e44 | |||
3e56c9861c | |||
d6f475a009 | |||
b2f20766de | |||
81e98cfd6f | |||
8cae85badc | |||
d6e0fd0b46 | |||
8d7d79c29b | |||
92d3692f85 | |||
7f094a9f96 | |||
7c9d6ced6a | |||
9b747b08da | |||
f4695bad1e | |||
744f2f380f | |||
22c6b9d3fe | |||
c1aeb3a258 | |||
0eb6a8bde9 | |||
477f089de3 | |||
64faffa7b6 | |||
2196db9479 | |||
14d949228d | |||
f8e96a556d | |||
5ad974f947 | |||
9c9009ff52 | |||
bd09412f6f | |||
82d48fe27b | |||
1315cc584b | |||
17bed1ef7a | |||
f0dd7bc256 | |||
5b356c3c11 | |||
fbbae64ff2 | |||
34b85441e2 | |||
e9b122cf8c | |||
3f0853654e | |||
0de451af70 | |||
a4926f8a0d | |||
1314f9a1ff | |||
d5e344f67c | |||
d8654fae6d | |||
033f47b6b9 | |||
383bca4ff9 | |||
22f7b1e7c5 | |||
d5ad03546c | |||
f79076ddef | |||
7a31498e91 | |||
95f9d90877 | |||
3bac0c19ac | |||
b8f4123185 | |||
d2e9c838de | |||
5cd51ba00e | |||
a3fafdafbd | |||
df9ad069c4 | |||
2ff3dab014 | |||
ae9526ef57 | |||
5e235617e0 | |||
a373247cda | |||
c81e319aac | |||
d05537a619 | |||
7894ac5522 | |||
64577cf806 | |||
c3307152e8 | |||
a7e99eb5b4 | |||
c45aae7048 | |||
9cb3afeb30 | |||
6e7e6587c2 | |||
5f3c7296b7 | |||
2e15e61059 | |||
574357b60a | |||
1576605acb | |||
9d564ffbb2 | |||
b56f3236fd | |||
6e566b8840 | |||
34a1a19089 | |||
80dc797651 | |||
f2a4223d25 | |||
902aaf31dd | |||
1be23148d7 | |||
88d6a73454 | |||
01c57b37ad | |||
9fbb88fa5f | |||
a0b0d79777 | |||
f3dccd3b84 | |||
56b715797e | |||
11948c9500 | |||
1161bf79aa | |||
40f564b32a | |||
4fdabc16ba | |||
e68dbdfb4c | |||
f2898037b0 | |||
5d116c7224 | |||
2a31815267 | |||
ded6a72072 | |||
7d80857d8d | |||
297e318243 | |||
6cc992f6d6 | |||
6402897329 | |||
5f7d0e474e | |||
09bb2d8e27 | |||
e28dbea05b | |||
9d8c894cff | |||
9942348ba3 | |||
2e98eda8a4 | |||
2ab2b65fc2 | |||
3e684b117f | |||
51f28a6cf8 | |||
212e864db1 | |||
0adfd95ced | |||
c9d05152c9 | |||
a8b21d7c74 | |||
97874b73f6 | |||
948713d13d | |||
63146aa41c | |||
c7f69ad7c1 | |||
008f8c1554 | |||
122216dbe4 | |||
c0ac09b928 | |||
cfda7e5e1e | |||
91af43adba | |||
bfb7176db3 | |||
411cd5d4a3 | |||
91aa87d122 | |||
14bdc67a4a | |||
b86b1fd1ad | |||
00ec5179f9 | |||
200c9d8d8d | |||
b69d210759 | |||
eb4a58ed01 | |||
bc2f30ac9b | |||
e87bbe9ed8 | |||
68785ef6c0 | |||
b78e2adb09 | |||
26c985e683 | |||
58e2b9f7f5 | |||
34f2d4c4d4 | |||
56c3b9d7de | |||
c83ffe542e | |||
b07872c8c2 | |||
1cc196fd06 | |||
dd68bfbea8 | |||
ea5888f39f | |||
78607a0bf9 | |||
fa9cff390a | |||
2613a132a6 | |||
7d8b54a980 | |||
6371fa03a1 | |||
daa872d2e7 | |||
4d1600e396 | |||
63d33c287c | |||
ae72d4ab6a | |||
8a747d1d1f | |||
55e512efb8 | |||
f09e7b77db | |||
4177f71972 | |||
0c3e38c543 | |||
94dd555e9b | |||
10bffe0f0f | |||
aa6baf94a6 | |||
44604d98ab | |||
63077dfa26 | |||
0794e5b58f | |||
c67e1430bd | |||
f3b249d18f | |||
857b8781cb | |||
118d41a53b | |||
2cb21ae4a7 | |||
7e0bd630b5 | |||
a57e977131 | |||
082a351c17 | |||
a47e1977f0 | |||
b0a4d2ca84 | |||
afceaca736 | |||
c59dc7d2bf | |||
e7015570d5 | |||
a68a9e7ef3 | |||
3ea84f008b | |||
47aeac846b | |||
69124cff08 | |||
a99c1bb418 | |||
b80ca93ced | |||
8998292a0f | |||
bc39b1b8b5 | |||
b93d3d2175 | |||
b1277c98ab | |||
c2eaf120b6 | |||
41158e495f | |||
3261342c4f | |||
8b4068ac7e | |||
27958f5e7a | |||
819dc01451 | |||
8277e05205 | |||
f4d96c78e7 | |||
a08d74cd3c | |||
878d2509cd | |||
09247246bb | |||
685b8266e4 | |||
7a2f7fdf3d | |||
ecaa24192f | |||
f49cc9c286 | |||
53ae19eda8 | |||
d8361bf741 | |||
d70f04c63d | |||
0f4b9fef9e | |||
05c18702ff | |||
754c311580 | |||
f6dd5a3156 | |||
53489ec43b | |||
da8d97a274 | |||
8161d67a1f | |||
381d59c18f | |||
55a40fcf4d | |||
cfc690f1c2 | |||
a3bfa921e8 | |||
c670f017a0 | |||
f70c5a28af | |||
c3d64a031d | |||
eb94729277 | |||
c339f17c5c | |||
d7dd2d6d8b | |||
342fe7da9e | |||
910a002201 | |||
5f6c36e823 | |||
cf577bbb4f | |||
b8253ae9ba | |||
978bcf3b45 | |||
f76f81a312 | |||
5793295e1a | |||
6df22314c9 | |||
d7c3e051de | |||
057a39091d | |||
ba8b618b7d | |||
0b86ded4f5 | |||
523341cf4a | |||
ee17095a14 | |||
7829f4b7d8 | |||
058e057088 | |||
c8d7b52fbf | |||
9732656556 | |||
967b9251e2 | |||
3ba89a926b | |||
b04b6fe645 | |||
1829395a8a | |||
6f2d431ae1 | |||
9132608aaf | |||
f0b604b5dc | |||
d1b9283a9a | |||
fe353904d8 | |||
011a6c156e | |||
8662feb1c7 | |||
097b3fe8b6 | |||
7f138d4b68 | |||
6532d0e0d7 | |||
8242d9f269 | |||
28fe89e048 | |||
b709839c38 | |||
638ea466f0 | |||
31bc1e4e76 | |||
0535674a96 | |||
5f3b12a472 | |||
6ec16cbeb0 | |||
1665e519a4 | |||
95979c6095 | |||
8b004466d1 | |||
3f1f2fd8d4 | |||
96ed198efc | |||
6a52730b49 | |||
5c4a802017 | |||
e59a08b351 | |||
2a7857b60d | |||
09afd7f165 | |||
e63bec83e8 | |||
5d74ddfee5 | |||
3c44561b19 | |||
8edb209d16 | |||
81bd635ca4 | |||
6ff3fe5949 | |||
31067aab95 | |||
7b3a007862 | |||
c5a5c393a8 | |||
52db44eac7 | |||
cb9c782d0c | |||
b8c4a540fa | |||
91d85d93c7 | |||
73ca0feb55 | |||
64fd8b5686 | |||
58a9532c70 | |||
3c206f5aef | |||
9bc4f186d5 | |||
f883d4190a | |||
c6fc2d3e7c | |||
e4d1bb4d3c | |||
376faf3d5a | |||
6c0ca9cb86 | |||
326753d0ff | |||
47f95ddae2 | |||
86fc1448a6 | |||
a91ae337c4 | |||
58466fa490 | |||
de78a30a5d | |||
f7d61696d1 | |||
e35ccd360b | |||
5f3a778002 | |||
dcba456af3 | |||
6d98006a37 | |||
4ac0c157bc | |||
e696129f0b | |||
cf5c512a64 | |||
32f45f2d5f | |||
66b8bd5a74 | |||
bfb20dab0f | |||
6b28569bca | |||
79b2c668fa | |||
5d660694c3 | |||
e70d0392c0 | |||
caa47a3bb6 | |||
75203c022a | |||
b65e577017 | |||
45ef81481f | |||
f9dee4465b | |||
8ec1ea2a4c | |||
17df9d1fa3 | |||
24967ae3a6 | |||
30bd8aa483 | |||
efce9c0219 | |||
0020550dde | |||
04a9cde47e | |||
e472022c91 | |||
d778cd0e83 | |||
1f76da8709 | |||
7ddc0abce6 | |||
a2af58ae09 | |||
8e71bb932e | |||
8c1f033b1c | |||
eb4df77614 | |||
fed6eee951 | |||
6a0b507c3b | |||
47b2e61987 | |||
28259b329e | |||
4391aa3ea8 | |||
4a4c8e94e4 | |||
d41cc312bb | |||
12abc741d2 | |||
a8262e0a54 | |||
2011c212ba | |||
564871ca3c | |||
95bb15238a | |||
cafc5ce6e2 | |||
68c3b64424 | |||
300fe283d6 | |||
ac6408c3bb | |||
750e323947 | |||
4cca9d9904 | |||
955081f155 | |||
01e47c889b | |||
2cd4d387a7 | |||
ea8c60ccc5 | |||
4ecf5236c1 | |||
eb919f2d5e | |||
485dd43b58 | |||
fd4da657fb | |||
acc6879fb1 | |||
d339fdb645 | |||
53a720a802 | |||
0b04cdcfbf | |||
b5d2fe70ff | |||
6d6f1c5401 | |||
7b8b8a6394 | |||
2a3373a19b | |||
eb320c4e95 | |||
0b220424bb | |||
a948ec7bd7 | |||
56196f721d | |||
3d06112860 | |||
05484d9e02 | |||
b73807a140 | |||
215c5e464c | |||
cf2dce320c | |||
32cdcc38b5 | |||
4b3ea06f70 | |||
2e7a6a42b4 | |||
be0ec86c48 | |||
8c493e8fa3 | |||
9668e811c5 | |||
28ce99f46a | |||
9b4dbc58f3 | |||
f295f15034 | |||
4c41994068 | |||
6f7b3ffad6 | |||
cc97128e25 | |||
b6ba3d38dc | |||
18b788844a | |||
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 |
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-b7
|
||||
version: 1.2.2
|
||||
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
|
91
.github/workflows/ci.yaml
vendored
91
.github/workflows/ci.yaml
vendored
@ -10,10 +10,23 @@ on:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
env:
|
||||
GIT_SYNC_URL: "https://${{ secrets.GITLAB_USERNAME_ROBOT }}:${{ secrets.GITLAB_TOKEN_ROBOT }}@gitlab.com/nofusscomputing/projects/centurion_erp.git"
|
||||
|
||||
jobs:
|
||||
|
||||
|
||||
mkdocs:
|
||||
name: 'MKDocs'
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
statuses: write
|
||||
checks: write
|
||||
actions: write
|
||||
uses: nofusscomputing/action_mkdocs/.github/workflows/reusable_mkdocs.yaml@development
|
||||
|
||||
|
||||
docker:
|
||||
name: 'Docker'
|
||||
uses: nofusscomputing/action_docker/.github/workflows/docker.yaml@development
|
||||
@ -22,8 +35,8 @@ jobs:
|
||||
DOCKER_PUBLISH_REGISTRY: "docker.io"
|
||||
DOCKER_PUBLISH_IMAGE_NAME: "nofusscomputing/centurion-erp"
|
||||
secrets:
|
||||
DOCKER_PUBLISH_USERNAME: ${{ secrets.NFC_DOCKERHUB_TOKEN }}
|
||||
DOCKER_PUBLISH_PASSWORD: ${{ secrets.NFC_DOCKERHUB_USERNAME }}
|
||||
DOCKER_PUBLISH_USERNAME: ${{ secrets.NFC_DOCKERHUB_USERNAME }}
|
||||
DOCKER_PUBLISH_PASSWORD: ${{ secrets.NFC_DOCKERHUB_TOKEN }}
|
||||
|
||||
|
||||
python:
|
||||
@ -31,3 +44,77 @@ jobs:
|
||||
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;
|
||||
|
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 }}
|
||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -9,3 +9,9 @@ artifacts/
|
||||
volumes/
|
||||
build/
|
||||
pages/
|
||||
node_modules/
|
||||
.markdownlint-cli2.jsonc
|
||||
.markdownlint.json
|
||||
package-lock.json
|
||||
package.json
|
||||
**.junit.xml
|
||||
|
@ -32,9 +32,9 @@ include:
|
||||
file:
|
||||
- .gitlab-ci_common.yaml
|
||||
# - template/automagic.gitlab-ci.yaml
|
||||
- local: gitlab-ci/automation/.gitlab-ci-ansible.yaml
|
||||
- local: gitlab-ci/template/mkdocs-documentation.gitlab-ci.yaml
|
||||
- local: gitlab-ci/lint/ansible.gitlab-ci.yaml
|
||||
- automation/.gitlab-ci-ansible.yaml
|
||||
- template/mkdocs-documentation.gitlab-ci.yaml
|
||||
- lint/ansible.gitlab-ci.yaml
|
||||
|
||||
|
||||
|
||||
|
@ -35,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
|
||||
|
679
CHANGELOG.md
679
CHANGELOG.md
@ -1,65 +1,528 @@
|
||||
## 1.2.2 (2024-10-29)
|
||||
|
||||
### Fixes
|
||||
|
||||
- **docker**: adjust pyyaml to >-6.0.1
|
||||
|
||||
## 1.2.1 (2024-10-22)
|
||||
|
||||
### Fixes
|
||||
|
||||
- **project_management**: Ensure user cant see projects for organizations they are apart of
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **project_management**: dont order queryset for project
|
||||
|
||||
## 1.2.0 (2024-10-11)
|
||||
|
||||
### feat
|
||||
|
||||
- update django 5.0.8 -> 5.1.2
|
||||
- **settings**: Add API filter and search
|
||||
- **core**: Add API filter of fields external_system and external_ref for projects
|
||||
- **core**: Add API filter of fields external_system and external_ref to tickets
|
||||
- **project_management**: increase project field length 50 -> 100 chars
|
||||
- **core**: increase ticket title field length 50 -> 100 chars
|
||||
- **core**: Add ability track ticket estimation time for completion
|
||||
- **core**: Add ability to delete a ticket
|
||||
- **core**: [Templating Engine] Add template tag concat_strings
|
||||
- **itim**: Add ticket tab to services
|
||||
- **itim**: Add ticket tab to clusters
|
||||
- **itam**: Add ticket tab to software
|
||||
- **itam**: Add ticket tab to operating systems
|
||||
- **itam**: Add ticket tab to devices
|
||||
- **config_management**: Add ticket tab to conf groups
|
||||
- **core**: Add slash command `link` for linking items to tickets
|
||||
- **core**: Add to markdown rendering model references
|
||||
- **core**: Ability to link items to all ticket types
|
||||
- **core**: add model ticket linked items
|
||||
- **project_management**: Add project milestones api endpoint
|
||||
- **project_management**: Add import_project permission and add api serializer
|
||||
- **core**: great odins beard, remove the checkbox formatting
|
||||
- **project_management**: Add field is_deleted to projects
|
||||
- **project_management**: Calculate project completion percentage and display
|
||||
- **core**: order project categories with parent name if applicable
|
||||
- **project_management**: Add Project Type to the UI
|
||||
- **project_management**: Add Project State to the UI
|
||||
- **project_management**: add priority field to project model, form and api endpoint
|
||||
- **project_management**: add organization field to project form and api endpoint
|
||||
- **project_management**: add project_type field to project form
|
||||
- **project_management**: add external_ref and external_system field to project model
|
||||
- **project_management**: add project type field to project model
|
||||
- **project_management**: add project type api endpoint
|
||||
- **project_management**: new model project type
|
||||
- **project_management**: add project state api endpoint
|
||||
- **project_management**: add project state field to project model
|
||||
- **project_managemenet**: new model project state
|
||||
- **project_management**: add field external system to projects
|
||||
- **core**: validate field milestone for all ticket types
|
||||
- **core**: Add field milestone to all ticket types
|
||||
- **project_management**: Add project milestones
|
||||
- **core**: Add slash command "related ticket" for ticket and ticket comments
|
||||
- **core**: Suffix username to action comments
|
||||
- **core**: Add slash command `/spend` for ticket and ticket comments
|
||||
- **core**: Disable HTML tag rendering for markdown
|
||||
- **project_management**: remove requirement for code field to be populated
|
||||
- **core**: Add ticket comment category API endpoint
|
||||
- **core**: Ability to assign categories to ticket comments
|
||||
- **core**: Add ticket comment categories
|
||||
- **core**: Extend all ticket endpoints to contain ticket categories
|
||||
- **core**: Add ticket category API endpoint
|
||||
- **core**: Ability to assign categories to tickets
|
||||
- **core**: Addpage titles to view abstract classes
|
||||
- **core**: Add ticket categories
|
||||
- **core**: during markdown render, if ticket ID not found return the tag
|
||||
- **core**: Add heading anchor plugin to markdown
|
||||
- **core**: correct markdown formatting for KB articles
|
||||
- **core**: remove project field from being editable when creating project task
|
||||
- **core**: Add admonition style
|
||||
- **project_management**: Validate project task has project set
|
||||
- **core**: set project ID to match url kwarg
|
||||
- **core**: Add action comment on title change
|
||||
- **core**: Add task listts plugin to markdowm
|
||||
- **core**: Add footnote plugin to markdowm
|
||||
- **core**: Add admonition plugin to markdowm
|
||||
- **core**: Add table extension to markdowm
|
||||
- **core**: Add strikethrough extension to markdowm
|
||||
- **core**: Add linkify extension to markdowm
|
||||
- **core**: move markdown parser py-markdown -> markdown-it
|
||||
- **core**: Add organization column to ticket pages
|
||||
- **core**: Allow super-user to edit ticket comment source
|
||||
- **core**: Render linked tickets the same as the rendered markdown link
|
||||
- **core**: Add project task link for related project task
|
||||
- **project_management**: Add project duration field
|
||||
- **core**: Add external ref to tickets if populated
|
||||
- **core**: Add project task permissions
|
||||
- **project_management**: Add project tasks
|
||||
- **api**: Add project tasks endpoint
|
||||
- **api**: Add projects endpoint
|
||||
- **api**: Add project management endpoint
|
||||
- **core**: support negative numbers when Calculating ticket duration for ticket meta and comments
|
||||
- **core**: Caclulate ticket duration for ticket meta and comments
|
||||
- **core**: Add edit details to ticket and comments
|
||||
- **core**: Don't save model history for ticket models
|
||||
- **core**: add option to allow the prevention of history saving for tenancy models
|
||||
- **core**: Add project field to tickets allowed fields
|
||||
- **core**: Update ticket status when assigned/unassigned users/teams
|
||||
- **core**: Create action comment for subscribed users/teams
|
||||
- **core**: Create action comment for assigned users/teams
|
||||
- **core**: adding of more ticket status icons
|
||||
- **api**: Ticket endpoint dynamic permissions
|
||||
- **core**: add ticket status badge
|
||||
- **access**: add ability to fetch dynamic permissions
|
||||
- **core**: Add delete view for ticket types: request, incident, change and problem
|
||||
- **api**: when attempting to create a device and it's found within DB, dont recreate, return it.
|
||||
- **core**: When solution comment posted to ticket update status to solved
|
||||
- **core**: Add opened by column to ticket indexes
|
||||
- **core**: permit user to add comment to own ticket
|
||||
- **core**: Allow OP to edit own Ticket Comment
|
||||
- **core**: Ticket Comment form submission validation
|
||||
- **core**: Ticket Comment can be edited by owner
|
||||
- **core**: Ticket Comment source hidden for non-triage users
|
||||
- **core**: When fetching allowed ticket comment fields, check against permissions
|
||||
- **core**: pass request to ticket comment form
|
||||
- **itam**: Accept device UUID in any case.
|
||||
- **core**: Add ticket status icon
|
||||
- **core**: Enable ticket comment created date can be set when an import user
|
||||
- **api**: Set default values for ticket comment form to match ticket
|
||||
- **core**: render ticket number `#\d+` links within markdown
|
||||
- **core**: Use common function for markdown rendering for ticket objects
|
||||
- **api**: Ensure device can add/edit organization
|
||||
- **core**: Add api validation for ticket
|
||||
- **core**: Ensure for tenancy objects that the organization is set
|
||||
- **core**: Ticket comment orgaanization set to ticket organization
|
||||
- **core**: colour code related ticket background to ticket type
|
||||
- **core**: Validate ticket related and prevent duel related entries
|
||||
- **core**: Validate ticket status field for all ticket types
|
||||
- **core**: Add ticket action comments on ticket update
|
||||
- **core**: Add Title bar to ticket form
|
||||
- **core**: Add field level permission and validation checks
|
||||
- **core**: Add permission checking to Tickets form
|
||||
- **access**: add dynamic permissions to Tenancy Permissions
|
||||
- **api**: Add Tickets endpoint
|
||||
- **itim**: Add Problem ticket to navigation
|
||||
- **itim**: Add Incident ticket to navigation
|
||||
- **itim**: Add Change ticket to navigation
|
||||
- **assistance**: Add Request ticket to navigation
|
||||
- **core**: add basic ticketing system
|
||||
- **development**: add option for including additional stylesheets
|
||||
- **ui**: add project management icon
|
||||
- **project_management**: Add manager and users for projects and tasks
|
||||
- **project_management**: Project task view "view"
|
||||
- **project_management**: Project task edit view
|
||||
- **project_management**: Project task delete view
|
||||
- **project_management**: Project task add view
|
||||
- **project_management**: Add project task model
|
||||
- **project_management**: save project history
|
||||
- **project_management**: add project delete page
|
||||
- **project_management**: add project edit page
|
||||
- **project_management**: add project view page
|
||||
- **project_management**: add project add page
|
||||
- **project_management**: add project index page
|
||||
- **project_management**: add interim project model
|
||||
|
||||
### Fixes
|
||||
|
||||
- ensure model mandatory fields don't specify a default value
|
||||
- **api**: Ensure user is set to current user for ticket comment
|
||||
- **core**: remove org field when editing a ticket
|
||||
- **core**: during validation, if subscribed users not specified, use empty list
|
||||
- **core**: add missing pagination to ticket comment categories index
|
||||
- **core**: add missing pagination to ticket categories index
|
||||
- **project_management**: Ensure project type and state show on index page
|
||||
- **core**: Add replacement function within ticket validation as `cleaned_data` attribute replacement
|
||||
- **core**: Ensure the ticket clears project field on project removal
|
||||
- **core**: Remove ticket fields user has no access to
|
||||
- **core**: correct logic for slash command `/spend`
|
||||
- **project_management**: correct project view permissions
|
||||
- **core**: Correct view permissions for ticket comment category
|
||||
- **core**: correct url typo for ticket category API endpoint
|
||||
- **core**: dont attempt to modify field for ticket category API list
|
||||
- **core**: Dont attempt to render ticket category if none
|
||||
- **core**: Correct the delete permission
|
||||
- **core**: correct project task reply link for comments
|
||||
- **core**: correct project task comment buttons
|
||||
- **project_management**: correct comment reply url name
|
||||
- **core**: Generate the correct edit url for tickets
|
||||
- **core**: Generate the correct comment urls for tickets
|
||||
- **core**: Redirect to correct url for itim tickets after adding comment
|
||||
- **core**: correct linked tickets hyperlink address
|
||||
- **core**: order ticket comments by creation date
|
||||
- **core**: Ensure for both ticket and comment, external details are unique.
|
||||
- **core**: Ensure on ticket comment create and update a response is returned
|
||||
- **core**: Ensure related tricket action comment is trimmed
|
||||
- **core**: Team assigned to ticket status update
|
||||
- **api**: ensure ticket_type is set from view var
|
||||
- **core**: Add ticket fields to ticket types
|
||||
- **core**: During ticket form validation confirm if value specified/different then default
|
||||
- **core**: Correctly set the ticket type initial value
|
||||
- **core**: prevent import user from having permssions within UI
|
||||
- **api**: correct ticket view links
|
||||
- **core**: Correct display of ticket status within ticket interface
|
||||
- **api**: Ensure if device found it is returned
|
||||
- **core**: Ensure status field remains as part of ticket
|
||||
- **core**: Correct modified field to correct type for ticket comment
|
||||
- **api**: Filter ticket comments to match ticket
|
||||
- **core**: Correct modified field to correct type
|
||||
- **core**: Ensure new ticket can be created
|
||||
- **core**: Add `ticket_type` field to import_permissions
|
||||
- **core**: Ensure that the organization field is available
|
||||
- **core**: dont remove hidden fields on ticket comment form
|
||||
- **core**: Correct ticket comment permissions
|
||||
- **access**: correct permission check to cater for is_global=None
|
||||
- **core**: return correct redirect path for related ticket form
|
||||
- **core**: use from ticket title for "blocked by"
|
||||
- **access**: Don't query for `is_global=None` within `TenancyManager`
|
||||
- **core**: ensure is_global check does not process null value
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **core**: Ticket Linked ref render as template
|
||||
- **core**: migrate ticket enums to own class
|
||||
- **core**: Ticket validation errors setup for both api and ui
|
||||
- **core**: for tickets use validation for organization field
|
||||
- **core**: refine ticket field permission and validation
|
||||
- reduce action comment spacing
|
||||
- **core**: update markdown styles
|
||||
- **core**: migrate ticket number rendering as markdown_it plugin
|
||||
- **core**: move markdown functions out of ticket model
|
||||
- **core**: Adjust test layout for itsm and project field based permissions
|
||||
- **project_management**: migrate projects to new style for views
|
||||
- **core**: REmove constraint on setting user for ticket comment
|
||||
- **core**: cache fields allowed during ticket validation
|
||||
- **core**: dont require specifying ticket status
|
||||
- **core**: move id to end for rendered ticket link.
|
||||
- **api**: Ticket (change, incident, problem and request) to static api endpoints
|
||||
- **api**: make ticket status field mandatory
|
||||
- **api**: Move core tickets to own ticket endpoints
|
||||
- **core**: During form validation for a ticket, use defaults if not defined for mandatory fields
|
||||
- **core**: Ticket form ticket_type to use class var
|
||||
- **core**: cache permission check for ticket types
|
||||
- **core**: Move allowed fields logic to own function
|
||||
- **access**: Add definable parameters to organization mixin
|
||||
- **access**: cache user_organizations on lookup
|
||||
- **access**: cache object_organization on lookup
|
||||
|
||||
### Tests
|
||||
|
||||
- **core**: Ticket Linked item view checks
|
||||
- **core**: Ticket Linked item permission checks
|
||||
- **project_management**: Project Milestone api permission checks
|
||||
- **project_management**: Project TYpe tenancy model checks
|
||||
- **project_management**: Project Type view checks
|
||||
- **project_management**: Project Type permission checks
|
||||
- **project_management**: Project Type core history checks
|
||||
- **project_management**: Project Type tenancy object checks
|
||||
- **project_management**: Project State permission checks
|
||||
- **project_management**: Project State tenancy model checks
|
||||
- **project_management**: Project State view checks
|
||||
- **project_management**: Project State core history checks
|
||||
- **project_management**: Project State tenancy object checks
|
||||
- **project_management**: Project type API permission checks
|
||||
- **project_management**: Project state API permission checks
|
||||
- **project_management**: Project miletone skipped api checks
|
||||
- **project_management**: Project Milestone tenancy model checks
|
||||
- **project_management**: Project Milestone view checks
|
||||
- **project_management**: Project Milestone ui permission checks
|
||||
- **project_management**: Project Milestone core history checks
|
||||
- **project_management**: Project Milestone Tenancy object checks
|
||||
- **core**: Project tenancy model checks
|
||||
- **core**: Project view checks
|
||||
- **core**: Project UI permission checks
|
||||
- **core**: Project API permission checks
|
||||
- **core**: Project history checks
|
||||
- **core**: Project Tenancy object checks
|
||||
- **core**: Ticket comment category API permission checks
|
||||
- **core**: add missing ticket category view checks
|
||||
- **core**: ticket comment category tenancy model checks
|
||||
- **core**: ticket comment category view checks
|
||||
- **core**: ticket comment category ui permission checks
|
||||
- **core**: ticket comment category history checks
|
||||
- **core**: ticket comment category tenancy model checks
|
||||
- **core**: ticket category API permission checks
|
||||
- **core**: ticket category history checks
|
||||
- **core**: ticket category tenancy model checks
|
||||
- **core**: ticket category model checks
|
||||
- **core**: view checks
|
||||
- **core**: ui permissions
|
||||
- **core**: correct project tests for triage user
|
||||
- **core**: Project task permission checks
|
||||
- **core**: Ticket comment API permission checks
|
||||
- **core**: Ticket comment permission checks
|
||||
- **core**: Ticket comment Views
|
||||
- **core**: Tenancy model tests for ticket comment
|
||||
- **core**: ensure history for ticket models is not saved
|
||||
- Ensure tenancy models save model history
|
||||
- **core**: remove duplicated tenancy object tests for ticket model
|
||||
- **core**: correct triage user test names for allowed field permissions
|
||||
- **core**: project field permission check for triage user
|
||||
- **core**: Ticket Action comment checks for related tickets
|
||||
- **core**: Ticket Action comment checks for subscribing team
|
||||
- **core**: Ticket Action comment checks for subscribing user
|
||||
- **core**: Ticket Action comment checks for unassigning team
|
||||
- **core**: Ticket Action comment checks for assigning team
|
||||
- **core**: Ticket Action comment checks for un-assigning user
|
||||
- **core**: Ticket Action comment checks for assigning user
|
||||
- **core**: Add ticket project field permission check
|
||||
- **core**: ensure ticket_type tests dont have change value that matches ticket type
|
||||
- **core**: field based permission tests for add, change, import and triage user
|
||||
- **api**: Ticket (change, incident, problem and request) api permission checks
|
||||
- **core**: interim ticket unit tests
|
||||
- **itam**: Ensure if an attempt to add an existing device via API, it's not recreated and is returned.
|
||||
- correct typo in test description for `test_model_add_has_permission`
|
||||
- Add view must have function `get_initial`
|
||||
- **itam**: Refactor Device tests organization field to be editable.
|
||||
- Ensure tests add organization to tenancy objects on creation
|
||||
|
||||
## 1.1.0 (2024-08-23)
|
||||
|
||||
### feat
|
||||
|
||||
- **itim**: Dont attempt to apply cluster type config if no type specified.
|
||||
- **itim**: Service config rendered as part of cluster config
|
||||
- **itim**: dont force config key, validate when it's required
|
||||
- **itim**: Services assignable to cluster
|
||||
- **itim**: Ability to add configuration to cluster type
|
||||
- **itim**: Ability to add external link to cluster
|
||||
- **itim**: Ability to add and configure Cluster Types
|
||||
- **itim**: Add cluster to history save
|
||||
- **itim**: prevent cluster from setting itself as parent
|
||||
- **itim**: Ability to add and configure cluster
|
||||
- **itam**: Track if device is virtual
|
||||
- **api**: Endpoint to fetch user permissions
|
||||
- **development**: Add function to filter permissions to those used by centurion
|
||||
- **development**: Add new template tag `choice_ids` for string list casting
|
||||
- **development**: Render `model_name_plural` as part of back button
|
||||
- **development**: add to form field `model_name_plural`
|
||||
- **development**: render heading if section included
|
||||
- **base**: create detail view templates
|
||||
- **itam**: Render Service Config with device config
|
||||
- **itam**: Display deployed services for devices
|
||||
- **itim**: Prevent circular service dependencies
|
||||
- **itim**: Port number validation to check for valid port numbers
|
||||
- **itim**: Prevent Service template from being assigned as dependent service
|
||||
- **itim**: Add service template support
|
||||
- **itim**: Ports for service management
|
||||
- **itim**: Service Management
|
||||
- **assistance**: Filter KB articles to target user
|
||||
- **assistance**: Add date picker to date fields for KB articles
|
||||
- **assistance**: Dont display expired articles for "view" users
|
||||
- **base**: add code highlighting to markdown
|
||||
- **assistance**: Categorised Knowledge base articles
|
||||
- **itim**: Add menu entry
|
||||
- **itam**: Ability to add device configuration
|
||||
- **settings**: New model to allow adding templated links to devices and software
|
||||
|
||||
### Fixes
|
||||
|
||||
- **settings**: return the rendering of external links to models
|
||||
- **core**: Ensure when saving history json is correctly formatted
|
||||
- **itim**: Fix name typo in Add Service button
|
||||
- Ensure tenancy models have `Meta.verbose_name_plural` attribute
|
||||
- **base**: Use correct url for back button
|
||||
- **itim**: ensure that the service template config is also rendered as part of device config
|
||||
- **itim**: dont render link if no device
|
||||
- **itim**: Dont show self within service dependencies
|
||||
- **assistance**: Only return distinct values when limiting KB articles
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **itim**: Add Cluster type to index page
|
||||
- **itam**: Knowledge Base now uses details template
|
||||
- **itam**: Device Type now uses details template
|
||||
- **itam**: Operating System now uses details template
|
||||
- **itim**: Service Port now uses details template
|
||||
- **itam**: Device Model now uses details template
|
||||
- **config_management**: Config Groups now uses details template
|
||||
- **itam**: Software Categories now uses details template
|
||||
- **itam**: manufacturer now uses details template
|
||||
- **itam**: software now uses details template
|
||||
- **itam**: device now use details template
|
||||
- **itim**: services now use details template
|
||||
|
||||
### Tests
|
||||
|
||||
- **itim**: Cluster Types unit tests
|
||||
- **itim**: Cluster unit tests
|
||||
- **itam**: Correct Device Type Model permissions test to use "change" view
|
||||
- **itam**: Correct Operating System Model permissions test to use "change" view
|
||||
- **config_management**: Correct Device Model permissions test to use "change" view
|
||||
- **config_management**: Correct Config Group permissions test to use "change" view
|
||||
- **itam**: Correct Software Category permissions test to use "change" view
|
||||
- **core**: Correct manufacturer permissions test to use "change" view
|
||||
- **itam**: Correct software permissions test to use "change" view
|
||||
- **model**: test for checking if Meta sub-class has variable verbose_name_plural
|
||||
- **external_link**: add tests
|
||||
|
||||
## 1.0.0 (2024-08-23)
|
||||
|
||||
## 1.0.0-b14 (2024-08-12)
|
||||
|
||||
### Fixes
|
||||
|
||||
- **api**: ensure model_notes is an available field
|
||||
|
||||
### Tests
|
||||
|
||||
- **access**: test field model_notes
|
||||
|
||||
## 1.0.0-b13 (2024-08-11)
|
||||
|
||||
### Fixes
|
||||
|
||||
- Audit models for validations
|
||||
- **itam**: Ensure device name is formatted according to RFC1035 2.3.1
|
||||
- **itam**: Ensure device UUID is correctly formatted
|
||||
- **config_management**: Ensure that config group can't set self as parent
|
||||
- **settings**: ensure that the api token cant be saved to notes field
|
||||
|
||||
### Tests
|
||||
|
||||
- api field checks
|
||||
- **software**: api field checks
|
||||
|
||||
## 1.0.0-b12 (2024-08-10)
|
||||
|
||||
### Fixes
|
||||
|
||||
- **api**: ensure org mixin is inherited by software view
|
||||
- **base**: correct project links to github
|
||||
|
||||
### Tests
|
||||
|
||||
- api field checks
|
||||
|
||||
#128 #162
|
||||
- **teams**: api field checks
|
||||
- **organization**: api field checks
|
||||
|
||||
## 1.0.0-b11 (2024-08-10)
|
||||
|
||||
## 1.0.0-b10 (2024-08-09)
|
||||
|
||||
## 1.0.0-b9 (2024-08-09)
|
||||
|
||||
## 1.0.0-b8 (2024-08-09)
|
||||
|
||||
## 1.0.0-b7 (2024-08-09)
|
||||
|
||||
## 1.0.0-b6 (2024-08-09)
|
||||
|
||||
## 1.0.0-b5 (2024-07-31)
|
||||
|
||||
### Feat
|
||||
### feat
|
||||
|
||||
- add Config groups to API
|
||||
- **api**: Add device config groups to devices
|
||||
- **api**: Ability to fetch configgroups from api along with config
|
||||
|
||||
### Fix
|
||||
### Fixes
|
||||
|
||||
- **api**: Ensure device groups is read only
|
||||
|
||||
### Tests
|
||||
|
||||
- **api**: Field existence and type checks for device
|
||||
- **api**: test configgroups API fields
|
||||
|
||||
## 1.0.0-b4 (2024-07-29)
|
||||
|
||||
### Feat
|
||||
### feat
|
||||
|
||||
- **swagger**: remove `{format}` suffixed doc entries
|
||||
|
||||
### Fix
|
||||
### Fixes
|
||||
|
||||
- release-b3 fixes
|
||||
- **api**: cleanup team post/get
|
||||
- **api**: confirm HTTP method is allowed before permission check
|
||||
- **api**: Ensure that organizations can't be created via the API
|
||||
- **access**: Team model class inheritance order corrected
|
||||
|
||||
### Tests
|
||||
|
||||
- confirm that the tenancymanager is called
|
||||
|
||||
## 1.0.0-b3 (2024-07-21)
|
||||
|
||||
### Fix
|
||||
### Fixes
|
||||
|
||||
- **itam**: Limit os version count to devices user has access to
|
||||
|
||||
## 1.0.0-b2 (2024-07-19)
|
||||
|
||||
### Fix
|
||||
### Fixes
|
||||
|
||||
- **itam**: only show os version once
|
||||
|
||||
## 1.0.0-b1 (2024-07-19)
|
||||
|
||||
### Fix
|
||||
### Fixes
|
||||
|
||||
- **itam**: ensure installed operating system count is limited to users organizations
|
||||
- **itam**: ensure installed software count is limited to users organizations
|
||||
|
||||
## 1.0.0-a4 (2024-07-18)
|
||||
|
||||
### Feat
|
||||
### feat
|
||||
|
||||
- **api**: When processing uploaded inventory and name does not match, update name to one within inventory file
|
||||
- **config_management**: Group name to be entire breadcrumb
|
||||
|
||||
### Tests
|
||||
|
||||
- ensure inventory upload matches by both serial number and uuid if device name different
|
||||
- placeholder for moving organization
|
||||
|
||||
## 1.0.0-a3 (2024-07-18)
|
||||
|
||||
### Feat
|
||||
### feat
|
||||
|
||||
- **config_management**: Prevent a config group from being able to change organization
|
||||
- **itam**: On device organization change remove config groups
|
||||
|
||||
### Fix
|
||||
### Fixes
|
||||
|
||||
- **config_management**: dont attempt to do action during save if group being created
|
||||
- **itam**: remove org filter for device so that user can see installations
|
||||
@ -70,13 +533,13 @@
|
||||
|
||||
## 1.0.0-a2 (2024-07-17)
|
||||
|
||||
### Feat
|
||||
### feat
|
||||
|
||||
- **api**: Inventory matching of device second by uuid
|
||||
- **api**: Inventory matching of device first by serial number
|
||||
- **base**: show warning bar if the user has not set a default organization
|
||||
|
||||
### Fix
|
||||
### Fixes
|
||||
|
||||
- **base**: dont show user warning bar for non-authenticated user
|
||||
- **api**: correct inventory operating system selection by name
|
||||
@ -89,25 +552,31 @@
|
||||
|
||||
- squashed DB migrations in preparation for v1.0 release.
|
||||
|
||||
### Feat
|
||||
### feat
|
||||
|
||||
- Administratively set global items org/is_global field now read-only
|
||||
- **access**: Add multi-tennant manager
|
||||
|
||||
### Fix
|
||||
### Fixes
|
||||
|
||||
- **core**: migrate manufacturer to use new form/view logic
|
||||
- **settings**: correct the permission to view manufacturers
|
||||
- **access**: Correct team form fields
|
||||
- **config_management**: don't exclude parent from field, only self
|
||||
|
||||
### Refactor
|
||||
### Refactoring
|
||||
|
||||
- repo preperation for v1.0.0-Alpha-1
|
||||
- Squash database migrations
|
||||
|
||||
### Tests
|
||||
|
||||
- tenancy objects
|
||||
- refactor to single abstract model for inclusion.
|
||||
|
||||
## 0.7.0 (2024-07-14)
|
||||
|
||||
### Feat
|
||||
### feat
|
||||
|
||||
- **core**: Filter every form field if associated with an organization to users organizations only
|
||||
- **core**: add var `template_name` to common view template for all views that require it
|
||||
@ -122,13 +591,14 @@
|
||||
- **ui**: add some navigation icons
|
||||
- **itam**: update inventory status icon
|
||||
- **itam**: ensure device software pagination links keep interface on software tab
|
||||
- "Migrate inventory processing to background worker"
|
||||
- **access**: enable non-organization django permission checks
|
||||
- **settings**: Add celery task results index and view page
|
||||
- **base**: Add background worker
|
||||
- **itam**: Update Serial Number from inventory if present and Serial Number not set
|
||||
- **itam**: Update UUID from inventory if present and UUID not set
|
||||
|
||||
### Fix
|
||||
### Fixes
|
||||
|
||||
- **config_management**: Don't allow a config group to assign itself as its parent
|
||||
- **config_management**: correct permission for deleting a host from config group
|
||||
@ -165,11 +635,14 @@
|
||||
- **itam**: show device model name instead of ID
|
||||
- **api**: Ensure if serial number from inventory is `null` that it's not used
|
||||
- **api**: ensure checked uuid and serial number is used for updating
|
||||
- inventory
|
||||
- **itam**: only remove device software when not found during inventory upload
|
||||
- **itam**: only update software version if different
|
||||
- existing device without uuid not updated when uploading an inventory
|
||||
- Device Software tab pagination does not work
|
||||
- **itam**: correct device software pagination
|
||||
|
||||
### Refactor
|
||||
### Refactoring
|
||||
|
||||
- adjust views missing add/change form to now use forms
|
||||
- add navigation menu expand arrows
|
||||
@ -184,31 +657,57 @@
|
||||
- **api**: migrate inventory processing to background worker
|
||||
- **itam**: only perform actions on device inventory if DB matches inventory item
|
||||
|
||||
### Tests
|
||||
|
||||
- add test test_view_*_attribute_not_exists_fields for add and change views
|
||||
- fix test_view_change_attribute_type_form_class to test if type class
|
||||
- **views**: add test cases for model views
|
||||
- Add Test case abstract classes to models
|
||||
- **inventory**: add mocks?? for calling background worker
|
||||
- **view**: view permission checks
|
||||
- **inventory**: update tests for background worker changes
|
||||
|
||||
## 0.6.0 (2024-06-30)
|
||||
|
||||
### Feat
|
||||
### feat
|
||||
|
||||
- user api token
|
||||
- **api**: API token authentication
|
||||
- **api**: abilty for user to create/delete api token
|
||||
- **api**: create token model
|
||||
|
||||
### Fix
|
||||
### Fixes
|
||||
|
||||
- **user_token**: conduct user check on token view access
|
||||
- **itam**: use same form for edit and add
|
||||
- **itam**: dont add field inventorydate if adding new item
|
||||
- **api**: inventory upload requires sanitization
|
||||
|
||||
### Refactor
|
||||
### Refactoring
|
||||
|
||||
- **settings**: use seperate change/view views
|
||||
- **settings**: use form for user settings
|
||||
- **tests**: move unit tests to unit test sub-directory
|
||||
|
||||
### Tests
|
||||
|
||||
- **token_auth**: test authentication method token
|
||||
- more tests
|
||||
- add .coveragerc to remove non-code files from coverage report
|
||||
- Unit Tests TenancyObjects
|
||||
- Test Cases for TenancyObjects
|
||||
- tests for checking links from rendered templetes
|
||||
- **core**: test cases for notes permissions
|
||||
- **config_management**: config groups history permissions
|
||||
- **api**: Majority of Inventory upload tests
|
||||
- **access**: TenancyObject field tests
|
||||
- **access**: remove skipped api tests for team users
|
||||
|
||||
## 0.5.0 (2024-06-17)
|
||||
|
||||
### Feat
|
||||
### feat
|
||||
|
||||
- Setup Organization Managers
|
||||
- **access**: add notes field to organization
|
||||
- **access**: add organization manger
|
||||
- **config_management**: Use breadcrumbs for child group name display
|
||||
@ -216,6 +715,7 @@
|
||||
- **itam**: add a status of "bad" for devices
|
||||
- **itam**: paginate device software tab
|
||||
- **itam**: status of device visible on device index page
|
||||
- API Browser
|
||||
- **core**: add skeleton http browser
|
||||
- **core**: Add a notes field to manufacturer/ publisher
|
||||
- **itam**: Add a notes field to software category
|
||||
@ -232,24 +732,28 @@
|
||||
- **itam**: add docs icon to devices page
|
||||
- **config_management**: add docs icon to config groups page
|
||||
- **base**: add dynamic docs icon
|
||||
- config group software
|
||||
- **models**: add property parent_object to models that have a parent
|
||||
- **config_management**: add config group software to group history
|
||||
- **itam**: render group software config within device rendered config
|
||||
- **config_management**: assign software action to config group
|
||||
- sso
|
||||
- add configuration value 'SESSION_COOKIE_AGE'
|
||||
- remove development SECRET_KEY and enforce checking for user configured one
|
||||
- **base**: build CSRF trusted origins from configuration
|
||||
- **base**: Enforceable SSO ONLY
|
||||
- **base**: configurable SSO
|
||||
|
||||
### Fix
|
||||
### Fixes
|
||||
|
||||
- **itam**: remove requirement that user needs change device to add notes
|
||||
- **core**: dont attempt to access parent_object if 'None' during history save
|
||||
- **config_management**: Add missing parent item getter to model
|
||||
- **core**: overridden save within SaveHistory to use default attributes
|
||||
- **access**: overridden save to use default attributes
|
||||
- History does not delete when item deleted
|
||||
- **core**: on object delete remove history entries
|
||||
- inventory upload cant determin object organization
|
||||
- **api**: ensure proper permission checking
|
||||
- dont throw an exception during settings load for an item django already checks
|
||||
- **core**: Add overrides for delete so delete history saved for items with parent model
|
||||
@ -257,7 +761,7 @@
|
||||
- **base**: remove social auth from nav menu
|
||||
- **access**: add a team user permissions to use team organization
|
||||
|
||||
### Refactor
|
||||
### Refactoring
|
||||
|
||||
- **access**: relocate permission check to own function
|
||||
- **itam**: move device os tab to details tab
|
||||
@ -271,14 +775,58 @@
|
||||
- login to use base template
|
||||
- adjust template block names
|
||||
|
||||
### Tests
|
||||
|
||||
- **access**: team user model permission check for organization manager
|
||||
- **access**: team model permission check for organization manager
|
||||
- **access**: organization model permission check for organization manager
|
||||
- **access**: add test cases for model delete as organization manager
|
||||
- **access**: add test cases for model addd as organization manager
|
||||
- **access**: add test cases for model change as organization manager
|
||||
- **access**: add test cases for model view as organization manager
|
||||
- write some more
|
||||
- **core**: skip invalid tests
|
||||
- **itam**: tests for device type history entries
|
||||
- **core**: tests for manufacturer history entries
|
||||
- move manufacturer to it's parent
|
||||
- refactor api model permission tests to use an abstract class of test cases
|
||||
- move tests to the module they belong to
|
||||
- refactor history permission tests to use an abstract class of test cases
|
||||
- refactor model permission tests to use an abstract class of test cases
|
||||
- refactor history entry to have test cases in abstract classes
|
||||
- **itam**: history entry tests for software category
|
||||
- **itam**: history entry tests for device operating system version
|
||||
- **itam**: history entry tests for device operating system
|
||||
- **itam**: history entry tests for device software
|
||||
- **itam**: ensure child history is removed on config group software delete
|
||||
- add placeholder tests
|
||||
- **itam**: ensure history is removed on software delete
|
||||
- **itam**: ensure history is removed on operating system delete
|
||||
- **itam**: ensure history is removed on device model delete
|
||||
- **config_management**: test history on delete for config groups
|
||||
- **itam**: ensure history is removed on device delete
|
||||
- **access**: test team history
|
||||
- **access**: ensure team user history is created and removed as required
|
||||
- **access**: ensure history is removed on team delete
|
||||
- **access**: ensure history is removed on item delete
|
||||
- **api**: Inventory upload permission checks
|
||||
- **config_management**: testing of config_groups rendered config
|
||||
- **config_management**: history save tests for config groups software
|
||||
- **config_management**: config group software permission for add, change and delete
|
||||
- **base**: placeholder tests for config groups software
|
||||
- **base**: basic test for merge_software helper
|
||||
- during unit tests add SECRET_KEY
|
||||
|
||||
## 0.4.0 (2024-06-05)
|
||||
|
||||
### Feat
|
||||
### feat
|
||||
|
||||
- 2024 06 05
|
||||
- **database**: add mysql support
|
||||
- **api**: move invneotry api endpoint to '/api/device/inventory'
|
||||
- **core**: support more history types
|
||||
- **core**: function to fetch history entry item
|
||||
- 2024 06 02
|
||||
- **config_management**: Add button to groups ui for adding child group
|
||||
- **access**: throw error if no organization added
|
||||
- **itam**: add delete button to config group within ui
|
||||
@ -291,10 +839,12 @@
|
||||
- **api**: add swagger ui for documentation
|
||||
- **api**: filter software to users organizations
|
||||
- **api**: filter devices to users organizations
|
||||
- randomz
|
||||
- **api**: add org team view page
|
||||
- API configuration of permissions
|
||||
- **api**: configure team permissions
|
||||
|
||||
### Fix
|
||||
### Fixes
|
||||
|
||||
- **itam**: ensure device type saves history
|
||||
- **core**: correct history view permissions
|
||||
@ -311,7 +861,7 @@
|
||||
- **api**: correct reverse url lookup to use NS API
|
||||
- **api**: permissions for organization
|
||||
|
||||
### Refactor
|
||||
### Refactoring
|
||||
|
||||
- **access**: cache object so it doesnt have to be called multiple times
|
||||
- **config_management**: move groups to nav menu
|
||||
@ -319,20 +869,49 @@
|
||||
- **api**: move permission check to mixin
|
||||
- **access**: add team option to org permission check
|
||||
|
||||
### Tests
|
||||
|
||||
- **api**: placeholder test for inventory
|
||||
- **settings**: access permission check for app settings
|
||||
- **settings**: history view permission check for software category
|
||||
- **settings**: history view permission check for manufacturer
|
||||
- **settings**: history view permission check for device type
|
||||
- **settings**: user settings
|
||||
- **settings**: view permission check for user settings
|
||||
- refactor core test layout
|
||||
- **itam**: view permission check for software
|
||||
- **itam**: view permission check for operating system
|
||||
- **itam**: view permission check for device model
|
||||
- **itam**: view permission check for device
|
||||
- **config_management**: view permission check for config_groups
|
||||
- **access**: view permission check for team
|
||||
- **access**: view permission check for organization
|
||||
- add history entry creation tests for most models
|
||||
- **config_management**: when adding a host to config group filter out host that are already members of the group
|
||||
- **config_management**: unit test for config groups model to ensure permissions are working
|
||||
- **api**: remove tests for os and manufacturer as they are not used in api
|
||||
- **api**: check model permissions for software
|
||||
- **api**: check model permissions for devices
|
||||
- **api**: check model permissions for teams
|
||||
- **api**: check model permissions for organizations
|
||||
|
||||
## 0.3.0 (2024-05-29)
|
||||
|
||||
### Feat
|
||||
### feat
|
||||
|
||||
- Randomz
|
||||
- **access**: during organization permission check, check to ensure user is logged on
|
||||
- **history**: always create an entry even if user=none
|
||||
- **itam**: device uuid must be unique
|
||||
- **itam**: device serial number must be unique
|
||||
- 2024 05 26
|
||||
- **setting**: Enable super admin to set ALL manufacturer/publishers as global
|
||||
- **setting**: Enable super admin to set ALL device types as global
|
||||
- **setting**: Enable super admin to set ALL device models as global
|
||||
- **setting**: Enable super admin to set ALL software categories as global
|
||||
- **UI**: show build details with page footer
|
||||
- **software**: Add output to stdout to show what is and has occurred
|
||||
- 2024 05 25
|
||||
- **base**: Add delete icon to content header
|
||||
- **itam**: Populate initial organization value from user default organization for software category creation
|
||||
- **itam**: Populate initial organization value from user default organization for device type creation
|
||||
@ -344,17 +923,20 @@
|
||||
- Add management command software
|
||||
- **setting**: Enable super admin to set ALL software as global
|
||||
- **user**: Add user settings panel
|
||||
- Manufacturer and Model Information
|
||||
- **itam**: Add publisher to software
|
||||
- **itam**: Add publisher to operating system
|
||||
- **itam**: Add device model
|
||||
- **core**: Add manufacturers
|
||||
- **settings**: add dummy model for permissions
|
||||
- **settings**: new module for whole of application settings/globals
|
||||
- 2024 05 21-23
|
||||
- **access**: Save changes to history for organization and teams
|
||||
- **software**: Save changes to history
|
||||
- **operating_system**: Save changes to history
|
||||
- **device**: Save changes to history
|
||||
- **core**: history model for saving model history
|
||||
- 2024 05 19/20
|
||||
- **itam**: Ability to add notes to software
|
||||
- **itam**: Ability to add notes to operating systems
|
||||
- **itam**: Ability to add notes on devices
|
||||
@ -363,7 +945,7 @@
|
||||
- **ui**: Show inventory details if they exist
|
||||
- **api**: API accept computer inventory
|
||||
|
||||
### Fix
|
||||
### Fixes
|
||||
|
||||
- **settings**: Add correct permissions for team user delete
|
||||
- **settings**: Add correct permissions for team user view/change
|
||||
@ -414,7 +996,7 @@
|
||||
- correct typo in notes templates
|
||||
- **ui**: Ensure navigation menu entry highlighted for sub items
|
||||
|
||||
### Refactor
|
||||
### Refactoring
|
||||
|
||||
- **access**: add to models a get_organization function
|
||||
- **access**: remove change view
|
||||
@ -426,13 +1008,33 @@
|
||||
- **itam**: move device types to settings app
|
||||
- **template**: content_title can be rendered in base
|
||||
|
||||
### Tests
|
||||
|
||||
- cleanup duplicate tests and minor reshuffle
|
||||
- **access**: unit testing team user permissions
|
||||
- **access**: unit testing team permissions
|
||||
- **settings**: unit testing manufacturer permissions
|
||||
- **settings**: unit testing software category permissions
|
||||
- **device_model**: unit testing device type permissions
|
||||
- **device_model**: unit testing device model permissions
|
||||
- **organization**: unit testing organization permissions
|
||||
- **operating_system**: unit testing operating system permissions
|
||||
- **software**: unit testing software permissions
|
||||
- **device**: unit testing device permissions
|
||||
- adjust test layout and update contributing
|
||||
- **core**: placeholder tests for history component
|
||||
- **core**: place holder tests for notes model
|
||||
- **api**: add placeholder tests for inventory
|
||||
|
||||
## 0.2.0 (2024-05-18)
|
||||
|
||||
### Feat
|
||||
### feat
|
||||
|
||||
- 2024 05 18
|
||||
- **itam**: Add Operating System to ITAM models
|
||||
- **api**: force content type to be JSON for req/resp
|
||||
- **software**: view software
|
||||
- 2024 05 17
|
||||
- **device**: Prevent devices from being set global
|
||||
- **software**: if no installations found, denote
|
||||
- **device**: configurable software version
|
||||
@ -444,21 +1046,24 @@
|
||||
- **software**: add pagination for index
|
||||
- **device**: add pagination for index
|
||||
|
||||
### Fix
|
||||
### Fixes
|
||||
|
||||
- **device**: correct software link
|
||||
|
||||
## 0.1.0 (2024-05-17)
|
||||
|
||||
### Feat
|
||||
### feat
|
||||
|
||||
- API token auth
|
||||
- **api**: initial token authentication implementation
|
||||
- itam and API setup
|
||||
- **docker**: add settings to store data in separate volume
|
||||
- **django**: add split settings for specifying additional settings paths
|
||||
- **api**: Add device config to device
|
||||
- **itam**: add organization to device installs
|
||||
- **itam**: migrate app from own repo
|
||||
- Enable API by default
|
||||
- Genesis
|
||||
- **admin**: remove team management
|
||||
- **admin**: remove group management
|
||||
- **access**: adjustable team permissions
|
||||
@ -492,14 +1097,14 @@
|
||||
- **template**: add base template
|
||||
- **django**: add organizations app
|
||||
|
||||
### Fix
|
||||
### Fixes
|
||||
|
||||
- **itam**: device software to come from device org or global not users orgs
|
||||
- **access**: correct team required permissions
|
||||
- **fields**: correct autoslug field so it works
|
||||
- **docker**: build wheels then install
|
||||
|
||||
### Refactor
|
||||
### Refactoring
|
||||
|
||||
- button to use same selection colour
|
||||
- **access**: remove inline form for org teams
|
||||
@ -508,4 +1113,8 @@
|
||||
- **views**: move views to own directory
|
||||
- **access**: addjust org and teams to use different view per action
|
||||
|
||||
### Tests
|
||||
|
||||
- interim unit tests
|
||||
|
||||
## 0.0.1 (2024-05-06)
|
||||
|
@ -1,6 +1,76 @@
|
||||
# Contribution Guide
|
||||
|
||||
|
||||
Development of this project has been setup to be done from VSCodium. The following additional requirements need to be met:
|
||||
|
||||
- npm has been installed. _required for `markdown` linting_
|
||||
|
||||
`sudo apt install -y --no-install-recommends npm`
|
||||
|
||||
- setup of other requirements can be done with `make prepare`
|
||||
|
||||
- **ALL** Linting must pass for Merge to be conducted.
|
||||
|
||||
_`make lint`_
|
||||
|
||||
## TL;DR
|
||||
|
||||
|
||||
from the root of the project to start a test server use:
|
||||
|
||||
``` bash
|
||||
|
||||
# activate python venv
|
||||
source /tmp/centurion_erp/bin/activate
|
||||
|
||||
# enter app dir
|
||||
cd app
|
||||
|
||||
# Start dev server can be viewed at http://127.0.0.1:8002
|
||||
python manage.py runserver 8002
|
||||
|
||||
# Run any migrations, if required
|
||||
python manage.py migrate
|
||||
|
||||
# Create a super suer if required
|
||||
python manage.py createsuperuser
|
||||
|
||||
```
|
||||
|
||||
## Makefile
|
||||
|
||||
!!! tip "TL;DR"
|
||||
Common make commands are `make prepare` then `make docs` and `make lint`
|
||||
|
||||
Included within the root of the repository is a makefile that can be used during development to check/run different items as is required during development. The following make targets are available:
|
||||
|
||||
- `prepare`
|
||||
|
||||
_prepare the repository. init's all git submodules and sets up a python virtual env and other make targets_
|
||||
|
||||
- `docs`
|
||||
|
||||
_builds the docs and places them within a directory called build, which can be viewed within a web browser_
|
||||
|
||||
- `lint`
|
||||
|
||||
_conducts all required linting_
|
||||
|
||||
- `docs-lint`
|
||||
|
||||
_lints the markdown documents within the docs directory for formatting errors that MKDocs may/will have an issue with._
|
||||
|
||||
- `clean`
|
||||
|
||||
_cleans up build artifacts and removes the python virtual environment_
|
||||
|
||||
|
||||
> this doc is yet to receive a re-write
|
||||
|
||||
|
||||
# Old working docs
|
||||
|
||||
|
||||
## Dev Environment
|
||||
|
||||
It's advised to setup a python virtual env for development. this can be done with the following commands.
|
||||
@ -30,6 +100,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.
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
<br>
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
[](https://hub.docker.com/r/nofusscomputing/centurion-erp) [](https://artifacthub.io/packages/container/centurion-erp/centurion-erp)
|
||||
@ -32,17 +32,18 @@ This project is hosted on [Github](https://github.com/NofussComputing/centurion_
|
||||
|
||||
**Stable Branch**
|
||||
|
||||
  
|
||||
  
|
||||

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

|
||||
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.db.models import Q
|
||||
from django.forms import inlineformset_factory
|
||||
|
||||
from app import settings
|
||||
|
||||
from .team_users import TeamUsersForm, TeamUsers
|
||||
|
||||
from access.models import Team
|
||||
from access.functions import permissions
|
||||
|
||||
from app import settings
|
||||
|
||||
from core.forms.common import CommonModelForm
|
||||
|
||||
@ -66,37 +66,4 @@ class TeamForm(CommonModelForm):
|
||||
|
||||
self.fields['permissions'].widget.attrs = {'style': "height: 200px;"}
|
||||
|
||||
apps = [
|
||||
'access',
|
||||
'config_management',
|
||||
'core',
|
||||
'django_celery_results',
|
||||
'itam',
|
||||
'settings',
|
||||
]
|
||||
|
||||
exclude_models = [
|
||||
'appsettings',
|
||||
'chordcounter',
|
||||
'groupresult',
|
||||
'organization'
|
||||
'settings',
|
||||
'usersettings',
|
||||
]
|
||||
|
||||
exclude_permissions = [
|
||||
'add_organization',
|
||||
'add_taskresult',
|
||||
'change_organization',
|
||||
'change_taskresult',
|
||||
'delete_organization',
|
||||
'delete_taskresult',
|
||||
]
|
||||
|
||||
self.fields['permissions'].queryset = Permission.objects.filter(
|
||||
content_type__app_label__in=apps,
|
||||
).exclude(
|
||||
content_type__model__in=exclude_models
|
||||
).exclude(
|
||||
codename__in = exclude_permissions
|
||||
)
|
||||
self.fields['permissions'].queryset = permissions.permission_queryset()
|
||||
|
46
app/access/functions/permissions.py
Normal file
46
app/access/functions/permissions.py
Normal file
@ -0,0 +1,46 @@
|
||||
from django.contrib.auth.models import Permission
|
||||
|
||||
def permission_queryset():
|
||||
"""Filter Permissions to those used within the application
|
||||
|
||||
Returns:
|
||||
list: Filtered queryset that only contains the used permissions
|
||||
"""
|
||||
|
||||
apps = [
|
||||
'access',
|
||||
'assistance',
|
||||
'config_management',
|
||||
'core',
|
||||
'django_celery_results',
|
||||
'itam',
|
||||
'itim',
|
||||
'settings',
|
||||
]
|
||||
|
||||
exclude_models = [
|
||||
'appsettings',
|
||||
'chordcounter',
|
||||
'comment',
|
||||
'groupresult',
|
||||
'organization'
|
||||
'settings',
|
||||
'usersettings',
|
||||
]
|
||||
|
||||
exclude_permissions = [
|
||||
'add_organization',
|
||||
'add_taskresult',
|
||||
'change_organization',
|
||||
'change_taskresult',
|
||||
'delete_organization',
|
||||
'delete_taskresult',
|
||||
]
|
||||
|
||||
return Permission.objects.filter(
|
||||
content_type__app_label__in=apps,
|
||||
).exclude(
|
||||
content_type__model__in=exclude_models
|
||||
).exclude(
|
||||
codename__in = exclude_permissions
|
||||
)
|
@ -10,6 +10,19 @@ from .models import Organization, Team
|
||||
class OrganizationMixin():
|
||||
"""Base Organization class"""
|
||||
|
||||
parent_model: str = None
|
||||
""" Parent Model
|
||||
|
||||
This attribute defines the parent model for the model in question. The parent model when defined
|
||||
will be used as the object to obtain the permissions from.
|
||||
"""
|
||||
|
||||
parent_model_pk_kwarg: str = 'pk'
|
||||
"""Parent Model kwarg
|
||||
|
||||
This value is used to define the kwarg that is used as the parent objects primary key (pk).
|
||||
"""
|
||||
|
||||
request = None
|
||||
|
||||
user_groups = []
|
||||
@ -26,20 +39,24 @@ class OrganizationMixin():
|
||||
parent_model (Model): with PK from kwargs['pk']
|
||||
"""
|
||||
|
||||
return self.parent_model.objects.get(pk=self.kwargs['pk'])
|
||||
return self.parent_model.objects.get(pk=self.kwargs[self.parent_model_pk_kwarg])
|
||||
|
||||
|
||||
def object_organization(self) -> int:
|
||||
|
||||
id = None
|
||||
|
||||
if hasattr(self, '_object_organization'):
|
||||
|
||||
return self._object_organization
|
||||
|
||||
try:
|
||||
|
||||
if hasattr(self, 'get_queryset'):
|
||||
self.get_queryset()
|
||||
|
||||
|
||||
if hasattr(self, 'parent_model'):
|
||||
if self.parent_model:
|
||||
obj = self.get_parent_obj()
|
||||
|
||||
id = obj.get_organization().id
|
||||
@ -61,6 +78,10 @@ class OrganizationMixin():
|
||||
|
||||
id = 0
|
||||
|
||||
if hasattr(self, 'instance') and id is None: # Form Instance
|
||||
|
||||
id = self.instance.get_organization()
|
||||
|
||||
|
||||
except AttributeError:
|
||||
|
||||
@ -84,6 +105,10 @@ class OrganizationMixin():
|
||||
|
||||
pass
|
||||
|
||||
if id is not None:
|
||||
|
||||
self._object_organization = id
|
||||
|
||||
|
||||
return id
|
||||
|
||||
@ -147,6 +172,10 @@ class OrganizationMixin():
|
||||
|
||||
user_organizations = []
|
||||
|
||||
if hasattr(self, '_user_organizations'):
|
||||
|
||||
return self._user_organizations
|
||||
|
||||
teams = Team.objects
|
||||
|
||||
for group in self.request.user.groups.all():
|
||||
@ -157,14 +186,32 @@ class OrganizationMixin():
|
||||
|
||||
user_organizations = user_organizations + [team.organization.id]
|
||||
|
||||
if len(user_organizations) > 0:
|
||||
|
||||
self._user_organizations = user_organizations
|
||||
|
||||
|
||||
return user_organizations
|
||||
|
||||
|
||||
# ToDo: Ensure that the group has access to item
|
||||
def has_organization_permission(self, organization: int=None) -> bool:
|
||||
def has_organization_permission(self, organization: int = None, permissions_required: list = None) -> bool:
|
||||
""" Check if user has permission within organization.
|
||||
|
||||
Args:
|
||||
organization (int, optional): Organization to check. Defaults to None.
|
||||
permissions_required (list, optional): if doing object level permissions, pass in required permission. Defaults to None.
|
||||
|
||||
Returns:
|
||||
bool: True for yes.
|
||||
"""
|
||||
|
||||
has_permission = False
|
||||
|
||||
if permissions_required is None:
|
||||
|
||||
permissions_required = self.get_permission_required()
|
||||
|
||||
if not organization:
|
||||
|
||||
organization = self.object_organization()
|
||||
@ -182,7 +229,7 @@ class OrganizationMixin():
|
||||
|
||||
assembled_permission = str(permission["content_type__app_label"]) + '.' + str(permission["codename"])
|
||||
|
||||
if assembled_permission in self.get_permission_required() and (team['organization_id'] == organization or organization == 0):
|
||||
if assembled_permission in permissions_required and (team['organization_id'] == organization or organization == 0):
|
||||
|
||||
return True
|
||||
|
||||
@ -242,15 +289,23 @@ class OrganizationMixin():
|
||||
|
||||
return True
|
||||
|
||||
perms = self.get_permission_required()
|
||||
if permissions_required:
|
||||
|
||||
if self.has_organization_permission():
|
||||
perms = permissions_required
|
||||
|
||||
else:
|
||||
|
||||
perms = self.get_permission_required()
|
||||
|
||||
if self.has_organization_permission(permissions_required = perms):
|
||||
|
||||
return True
|
||||
|
||||
if self.request.user.has_perms(perms) and len(self.kwargs) == 0 and str(self.request.method).lower() == 'get':
|
||||
if self.request.user.has_perms(perms) and str(self.request.method).lower() == 'get':
|
||||
|
||||
return True
|
||||
if len(self.kwargs) == 0 or (len(self.kwargs) == 1 and 'ticket_type' in self.kwargs):
|
||||
|
||||
return True
|
||||
|
||||
for required_permission in self.permission_required:
|
||||
|
||||
@ -327,6 +382,12 @@ class OrganizationPermission(AccessMixin, OrganizationMixin):
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return self.handle_no_permission()
|
||||
|
||||
if len(self.permission_required) == 0:
|
||||
|
||||
if hasattr(self, 'get_dynamic_permissions'):
|
||||
|
||||
self.permission_required = self.get_dynamic_permissions()
|
||||
|
||||
if len(self.permission_required) > 0:
|
||||
|
||||
|
@ -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):
|
||||
@ -124,13 +124,21 @@ class TenancyManager(models.Manager):
|
||||
user_organizations += [ team_user.team.organization.id ]
|
||||
|
||||
|
||||
if len(user_organizations) > 0 and not user.is_superuser:
|
||||
if len(user_organizations) > 0 and not user.is_superuser and self.model.is_global is not None:
|
||||
|
||||
return super().get_queryset().filter(
|
||||
models.Q(organization__in=user_organizations)
|
||||
|
|
||||
models.Q(is_global = True)
|
||||
)
|
||||
if self.model.is_global:
|
||||
|
||||
return super().get_queryset().filter(
|
||||
models.Q(organization__in=user_organizations)
|
||||
|
|
||||
models.Q(is_global = True)
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
return super().get_queryset().filter(
|
||||
models.Q(organization__in=user_organizations)
|
||||
)
|
||||
|
||||
return super().get_queryset()
|
||||
|
||||
@ -188,6 +196,15 @@ class TenancyObject(SaveHistory):
|
||||
def get_organization(self) -> Organization:
|
||||
return self.organization
|
||||
|
||||
|
||||
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||
|
||||
if self.organization is None:
|
||||
|
||||
raise ValidationError('Organization not defined')
|
||||
|
||||
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
|
||||
|
||||
|
||||
|
||||
class Team(Group, TenancyObject):
|
||||
@ -196,9 +213,6 @@ class Team(Group, TenancyObject):
|
||||
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 +255,10 @@ class Team(Group, TenancyObject):
|
||||
return [permission_list, self.permissions.all()]
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.team_name
|
||||
|
||||
|
||||
|
||||
class TeamUsers(SaveHistory):
|
||||
|
||||
@ -318,3 +336,6 @@ class TeamUsers(SaveHistory):
|
||||
|
||||
return self.team
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username
|
||||
|
||||
|
@ -11,6 +11,19 @@ class TenancyObject:
|
||||
model = None
|
||||
""" Model to be tested """
|
||||
|
||||
should_model_history_be_saved: bool = True
|
||||
""" Should model history be saved.
|
||||
|
||||
By default this should always be 'True', however in special
|
||||
circumstances, this may not be desired.
|
||||
"""
|
||||
|
||||
|
||||
def test_history_save(self):
|
||||
"""Confirm the desired intent for saving model history."""
|
||||
|
||||
assert self.model.save_model_history == self.should_model_history_be_saved
|
||||
|
||||
|
||||
def test_has_attr_get_organization(self):
|
||||
""" TenancyObject attribute check
|
||||
|
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
|
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
|
@ -91,3 +91,13 @@ class TenancyObjectTests(TestCase):
|
||||
"""
|
||||
|
||||
assert self.item.objects is not None
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="write test")
|
||||
def test_field_not_none_organzation(self):
|
||||
""" Ensure field is set
|
||||
|
||||
Field organization must be defined for all tenancy objects
|
||||
"""
|
||||
|
||||
assert self.item.objects is not None
|
||||
|
@ -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,
|
||||
|
@ -15,6 +15,7 @@ class TeamSerializerBase(serializers.ModelSerializer):
|
||||
model = Team
|
||||
fields = (
|
||||
'team_name',
|
||||
'model_notes',
|
||||
'permissions',
|
||||
'url',
|
||||
)
|
||||
@ -75,6 +76,7 @@ class TeamSerializer(TeamSerializerBase):
|
||||
fields = (
|
||||
"id",
|
||||
"team_name",
|
||||
'model_notes',
|
||||
'permissions',
|
||||
'permissions_url',
|
||||
'url',
|
||||
|
63
app/api/serializers/assistance/request.py
Normal file
63
app/api/serializers/assistance/request.py
Normal file
@ -0,0 +1,63 @@
|
||||
from rest_framework.fields import empty
|
||||
|
||||
from api.serializers.core.ticket import TicketSerializer
|
||||
|
||||
from core.models.ticket.ticket import Ticket
|
||||
|
||||
|
||||
|
||||
class RequestTicketSerializer(
|
||||
TicketSerializer,
|
||||
):
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Ticket
|
||||
|
||||
fields = [
|
||||
'id',
|
||||
'assigned_teams',
|
||||
'assigned_users',
|
||||
'category',
|
||||
'created',
|
||||
'modified',
|
||||
'status',
|
||||
'title',
|
||||
'description',
|
||||
'estimate',
|
||||
'urgency',
|
||||
'impact',
|
||||
'priority',
|
||||
'external_ref',
|
||||
'external_system',
|
||||
'ticket_type',
|
||||
'is_deleted',
|
||||
'date_closed',
|
||||
# 'planned_start_date',
|
||||
# 'planned_finish_date',
|
||||
# 'real_start_date',
|
||||
# 'real_finish_date',
|
||||
'opened_by',
|
||||
'organization',
|
||||
'project',
|
||||
'milestone',
|
||||
'subscribed_teams',
|
||||
'subscribed_users',
|
||||
'ticket_comments',
|
||||
'url',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'ticket_type',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
def __init__(self, instance=None, data=empty, **kwargs):
|
||||
|
||||
super().__init__(instance=instance, data=data, **kwargs)
|
||||
|
||||
self.fields.fields['category'].queryset = self.fields.fields['category'].queryset.filter(
|
||||
request = True
|
||||
)
|
195
app/api/serializers/core/ticket.py
Normal file
195
app/api/serializers/core/ticket.py
Normal file
@ -0,0 +1,195 @@
|
||||
from django.urls import reverse
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.fields import empty
|
||||
|
||||
from api.serializers.core.ticket_comment import TicketCommentSerializer
|
||||
|
||||
from core.forms.validate_ticket import TicketValidation
|
||||
from core.models.ticket.ticket import Ticket
|
||||
|
||||
|
||||
|
||||
class TicketSerializer(
|
||||
serializers.ModelSerializer,
|
||||
TicketValidation,
|
||||
):
|
||||
|
||||
url = serializers.SerializerMethodField('get_url_ticket')
|
||||
|
||||
|
||||
def get_url_ticket(self, item):
|
||||
|
||||
request = self.context.get('request')
|
||||
|
||||
kwargs: dict = {
|
||||
'pk': item.id
|
||||
}
|
||||
|
||||
if item.ticket_type == self.Meta.model.TicketType.CHANGE.value:
|
||||
|
||||
view_name = '_api_itim_change'
|
||||
|
||||
elif item.ticket_type == self.Meta.model.TicketType.INCIDENT.value:
|
||||
|
||||
view_name = '_api_itim_incident'
|
||||
|
||||
elif item.ticket_type == self.Meta.model.TicketType.PROBLEM.value:
|
||||
|
||||
view_name = '_api_itim_problem'
|
||||
|
||||
elif item.ticket_type == self.Meta.model.TicketType.REQUEST.value:
|
||||
|
||||
view_name = '_api_assistance_request'
|
||||
|
||||
elif item.ticket_type == self.Meta.model.TicketType.PROJECT_TASK.value:
|
||||
|
||||
view_name = '_api_project_tasks'
|
||||
|
||||
kwargs.update({'project_id': item.project.id})
|
||||
else:
|
||||
|
||||
raise ValueError('Serializer unable to obtain ticket type')
|
||||
|
||||
|
||||
return request.build_absolute_uri(
|
||||
reverse(
|
||||
'API:' + view_name + '-detail',
|
||||
kwargs = kwargs
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
ticket_comments = serializers.SerializerMethodField('get_url_ticket_comments')
|
||||
|
||||
|
||||
def get_url_ticket_comments(self, item):
|
||||
|
||||
request = self.context.get('request')
|
||||
|
||||
kwargs: dict = {
|
||||
'ticket_id': item.id
|
||||
}
|
||||
|
||||
if item.ticket_type == self.Meta.model.TicketType.CHANGE.value:
|
||||
|
||||
view_name = '_api_itim_change_ticket_comments'
|
||||
|
||||
elif item.ticket_type == self.Meta.model.TicketType.INCIDENT.value:
|
||||
|
||||
view_name = '_api_itim_incident_ticket_comments'
|
||||
|
||||
elif item.ticket_type == self.Meta.model.TicketType.PROBLEM.value:
|
||||
|
||||
view_name = '_api_itim_problem_ticket_comments'
|
||||
|
||||
elif item.ticket_type == self.Meta.model.TicketType.REQUEST.value:
|
||||
|
||||
view_name = '_api_assistance_request_ticket_comments'
|
||||
|
||||
elif item.ticket_type == self.Meta.model.TicketType.PROJECT_TASK.value:
|
||||
|
||||
view_name = '_api_project_tasks_comments'
|
||||
|
||||
kwargs.update({'project_id': item.project.id})
|
||||
|
||||
else:
|
||||
|
||||
raise ValueError('Serializer unable to obtain ticket type')
|
||||
|
||||
|
||||
return request.build_absolute_uri(
|
||||
reverse(
|
||||
'API:' + view_name + '-list',
|
||||
kwargs = kwargs
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Ticket
|
||||
|
||||
|
||||
fields = [
|
||||
'id',
|
||||
'assigned_teams',
|
||||
'assigned_users',
|
||||
'category',
|
||||
'created',
|
||||
'modified',
|
||||
'status',
|
||||
'title',
|
||||
'description',
|
||||
'urgency',
|
||||
'impact',
|
||||
'priority',
|
||||
'external_ref',
|
||||
'external_system',
|
||||
'ticket_type',
|
||||
'is_deleted',
|
||||
'date_closed',
|
||||
'planned_start_date',
|
||||
'planned_finish_date',
|
||||
'real_start_date',
|
||||
'real_finish_date',
|
||||
'opened_by',
|
||||
'organization',
|
||||
'project',
|
||||
'subscribed_teams',
|
||||
'subscribed_users',
|
||||
'ticket_comments',
|
||||
'url',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
def __init__(self, instance=None, data=empty, **kwargs):
|
||||
|
||||
self.fields.fields['status'].initial = Ticket.TicketStatus.All.NEW
|
||||
self.fields.fields['status'].default = Ticket.TicketStatus.All.NEW
|
||||
|
||||
self.ticket_type_fields = self.Meta.fields
|
||||
|
||||
super().__init__(instance=instance, data=data, **kwargs)
|
||||
|
||||
self.fields['organization'].required = True
|
||||
|
||||
|
||||
def is_valid(self, *, raise_exception=True) -> bool:
|
||||
|
||||
is_valid = False
|
||||
|
||||
try:
|
||||
|
||||
self.request = self._context['request']
|
||||
|
||||
is_valid = super().is_valid(raise_exception=raise_exception)
|
||||
|
||||
self._ticket_type = str(self.fields['ticket_type'].choices[self._context['view']._ticket_type_value]).lower().replace(' ', '_')
|
||||
|
||||
is_valid = self.validate_ticket()
|
||||
|
||||
self.validated_data['ticket_type'] = int(self._context['view']._ticket_type_value)
|
||||
|
||||
if self.instance is None:
|
||||
|
||||
subscribed_users: list = []
|
||||
|
||||
if 'subscribed_users' in self.validated_data:
|
||||
|
||||
subscribed_users = self.validated_data['subscribed_users']
|
||||
|
||||
self.validated_data['subscribed_users'] = subscribed_users + [ self.validated_data['opened_by'] ]
|
||||
|
||||
except Exception as unhandled_exception:
|
||||
|
||||
serializers.ParseError(
|
||||
detail=f"Server encountered an error during validation, Traceback: {unhandled_exception.with_traceback}"
|
||||
)
|
||||
|
||||
return is_valid
|
44
app/api/serializers/core/ticket_category.py
Normal file
44
app/api/serializers/core/ticket_category.py
Normal file
@ -0,0 +1,44 @@
|
||||
from django.urls import reverse
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.fields import empty
|
||||
|
||||
from api.serializers.core.ticket_comment import TicketCommentSerializer
|
||||
|
||||
from core.forms.validate_ticket import TicketValidation
|
||||
from core.models.ticket.ticket_category import TicketCategory
|
||||
|
||||
|
||||
|
||||
class TicketCategorySerializer(
|
||||
serializers.ModelSerializer,
|
||||
):
|
||||
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="API:_api_ticket_category-detail", format="html"
|
||||
)
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
model = TicketCategory
|
||||
|
||||
fields = '__all__'
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
def __init__(self, instance=None, data=empty, **kwargs):
|
||||
|
||||
if instance is not None:
|
||||
|
||||
if hasattr(instance, 'id'):
|
||||
|
||||
self.fields.fields['parent'].queryset = self.fields.fields['parent'].queryset.exclude(
|
||||
id=instance.id
|
||||
)
|
||||
|
||||
super().__init__(instance=instance, data=data, **kwargs)
|
74
app/api/serializers/core/ticket_comment.py
Normal file
74
app/api/serializers/core/ticket_comment.py
Normal file
@ -0,0 +1,74 @@
|
||||
from django.urls import reverse
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.fields import empty
|
||||
|
||||
from core.models.ticket.ticket_comment import Ticket, TicketComment
|
||||
|
||||
|
||||
|
||||
class TicketCommentSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
url = serializers.SerializerMethodField('get_url_ticket_comment')
|
||||
|
||||
def get_url_ticket_comment(self, item):
|
||||
|
||||
request = self.context.get('request')
|
||||
|
||||
if item.ticket.ticket_type == item.ticket.__class__.TicketType.CHANGE:
|
||||
|
||||
view_name = '_api_itim_change_ticket_comments'
|
||||
|
||||
elif item.ticket.ticket_type == item.ticket.__class__.TicketType.INCIDENT:
|
||||
|
||||
view_name = '_api_itim_incident_ticket_comments'
|
||||
|
||||
elif item.ticket.ticket_type == item.ticket.__class__.TicketType.PROBLEM:
|
||||
|
||||
view_name = '_api_itim_problem_ticket_comments'
|
||||
|
||||
elif item.ticket.ticket_type == item.ticket.__class__.TicketType.REQUEST:
|
||||
|
||||
view_name = '_api_assistance_request_ticket_comments'
|
||||
|
||||
else:
|
||||
|
||||
raise ValueError('Serializer unable to obtain ticket type')
|
||||
|
||||
|
||||
return request.build_absolute_uri(
|
||||
reverse('API:' + view_name + '-detail',
|
||||
kwargs={
|
||||
'ticket_id': item.ticket.id,
|
||||
'pk': item.id
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
class Meta:
|
||||
model = TicketComment
|
||||
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
def __init__(self, instance=None, data=empty, **kwargs):
|
||||
|
||||
if 'context' in self._kwargs:
|
||||
|
||||
if 'view' in self._kwargs['context']:
|
||||
|
||||
if 'ticket_id' in self._kwargs['context']['view'].kwargs:
|
||||
|
||||
ticket = Ticket.objects.get(pk=int(self._kwargs['context']['view'].kwargs['ticket_id']))
|
||||
self.fields.fields['organization'].initial = ticket.organization.id
|
||||
|
||||
self.fields.fields['ticket'].initial = int(self._kwargs['context']['view'].kwargs['ticket_id'])
|
||||
|
||||
self.fields.fields['comment_type'].initial = TicketComment.CommentType.COMMENT
|
||||
|
||||
self.fields.fields['user'].initial = kwargs['context']['request']._user.id
|
||||
|
||||
super().__init__(instance=instance, data=data, **kwargs)
|
42
app/api/serializers/core/ticket_comment_category.py
Normal file
42
app/api/serializers/core/ticket_comment_category.py
Normal file
@ -0,0 +1,42 @@
|
||||
from django.urls import reverse
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.fields import empty
|
||||
|
||||
|
||||
from core.models.ticket.ticket_comment_category import TicketCommentCategory
|
||||
|
||||
|
||||
|
||||
class TicketCommentCategorySerializer(
|
||||
serializers.ModelSerializer,
|
||||
):
|
||||
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="API:_api_ticket_comment_category-detail", format="html"
|
||||
)
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
model = TicketCommentCategory
|
||||
|
||||
fields = '__all__'
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
def __init__(self, instance=None, data=empty, **kwargs):
|
||||
|
||||
if instance is not None:
|
||||
|
||||
if hasattr(instance, 'id'):
|
||||
|
||||
self.fields.fields['parent'].queryset = self.fields.fields['parent'].queryset.exclude(
|
||||
id=instance.id
|
||||
)
|
||||
|
||||
super().__init__(instance=instance, data=data, **kwargs)
|
@ -53,7 +53,6 @@ class DeviceSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
depth = 1
|
||||
fields = [
|
||||
'id',
|
||||
'is_global',
|
||||
|
63
app/api/serializers/itim/change.py
Normal file
63
app/api/serializers/itim/change.py
Normal file
@ -0,0 +1,63 @@
|
||||
from rest_framework.fields import empty
|
||||
|
||||
from api.serializers.core.ticket import TicketSerializer
|
||||
|
||||
from core.models.ticket.ticket import Ticket
|
||||
|
||||
|
||||
|
||||
class ChangeTicketSerializer(
|
||||
TicketSerializer,
|
||||
):
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Ticket
|
||||
|
||||
fields = [
|
||||
'id',
|
||||
'assigned_teams',
|
||||
'assigned_users',
|
||||
'category',
|
||||
'created',
|
||||
'modified',
|
||||
'status',
|
||||
'title',
|
||||
'description',
|
||||
'estimate',
|
||||
'urgency',
|
||||
'impact',
|
||||
'priority',
|
||||
'external_ref',
|
||||
'external_system',
|
||||
'ticket_type',
|
||||
'is_deleted',
|
||||
'date_closed',
|
||||
# 'planned_start_date',
|
||||
# 'planned_finish_date',
|
||||
# 'real_start_date',
|
||||
# 'real_finish_date',
|
||||
'opened_by',
|
||||
'organization',
|
||||
'project',
|
||||
'milestone',
|
||||
'subscribed_teams',
|
||||
'subscribed_users',
|
||||
'ticket_comments',
|
||||
'url',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'ticket_type',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
def __init__(self, instance=None, data=empty, **kwargs):
|
||||
|
||||
super().__init__(instance=instance, data=data, **kwargs)
|
||||
|
||||
self.fields.fields['category'].queryset = self.fields.fields['category'].queryset.filter(
|
||||
project_task = True
|
||||
)
|
63
app/api/serializers/itim/incident.py
Normal file
63
app/api/serializers/itim/incident.py
Normal file
@ -0,0 +1,63 @@
|
||||
from rest_framework.fields import empty
|
||||
|
||||
from api.serializers.core.ticket import TicketSerializer
|
||||
|
||||
from core.models.ticket.ticket import Ticket
|
||||
|
||||
|
||||
|
||||
class IncidentTicketSerializer(
|
||||
TicketSerializer,
|
||||
):
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Ticket
|
||||
|
||||
fields = [
|
||||
'id',
|
||||
'assigned_teams',
|
||||
'assigned_users',
|
||||
'category',
|
||||
'created',
|
||||
'modified',
|
||||
'status',
|
||||
'title',
|
||||
'description',
|
||||
'estimate',
|
||||
'urgency',
|
||||
'impact',
|
||||
'priority',
|
||||
'external_ref',
|
||||
'external_system',
|
||||
'ticket_type',
|
||||
'is_deleted',
|
||||
'date_closed',
|
||||
# 'planned_start_date',
|
||||
# 'planned_finish_date',
|
||||
# 'real_start_date',
|
||||
# 'real_finish_date',
|
||||
'opened_by',
|
||||
'organization',
|
||||
'project',
|
||||
'milestone',
|
||||
'subscribed_teams',
|
||||
'subscribed_users',
|
||||
'ticket_comments',
|
||||
'url',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'ticket_type',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
def __init__(self, instance=None, data=empty, **kwargs):
|
||||
|
||||
super().__init__(instance=instance, data=data, **kwargs)
|
||||
|
||||
self.fields.fields['category'].queryset = self.fields.fields['category'].queryset.filter(
|
||||
incident = True
|
||||
)
|
63
app/api/serializers/itim/problem.py
Normal file
63
app/api/serializers/itim/problem.py
Normal file
@ -0,0 +1,63 @@
|
||||
from rest_framework.fields import empty
|
||||
|
||||
from api.serializers.core.ticket import TicketSerializer
|
||||
|
||||
from core.models.ticket.ticket import Ticket
|
||||
|
||||
|
||||
|
||||
class ProblemTicketSerializer(
|
||||
TicketSerializer,
|
||||
):
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Ticket
|
||||
|
||||
fields = [
|
||||
'id',
|
||||
'assigned_teams',
|
||||
'assigned_users',
|
||||
'category',
|
||||
'created',
|
||||
'modified',
|
||||
'status',
|
||||
'title',
|
||||
'description',
|
||||
'estimate',
|
||||
'urgency',
|
||||
'impact',
|
||||
'priority',
|
||||
'external_ref',
|
||||
'external_system',
|
||||
'ticket_type',
|
||||
'is_deleted',
|
||||
'date_closed',
|
||||
# 'planned_start_date',
|
||||
# 'planned_finish_date',
|
||||
# 'real_start_date',
|
||||
# 'real_finish_date',
|
||||
'opened_by',
|
||||
'organization',
|
||||
'project',
|
||||
'milestone',
|
||||
'subscribed_teams',
|
||||
'subscribed_users',
|
||||
'ticket_comments',
|
||||
'url',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'ticket_type',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
def __init__(self, instance=None, data=empty, **kwargs):
|
||||
|
||||
super().__init__(instance=instance, data=data, **kwargs)
|
||||
|
||||
self.fields.fields['category'].queryset = self.fields.fields['category'].queryset.filter(
|
||||
problem = True
|
||||
)
|
74
app/api/serializers/project_management/project_milestone.py
Normal file
74
app/api/serializers/project_management/project_milestone.py
Normal file
@ -0,0 +1,74 @@
|
||||
from django.urls import reverse
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.fields import empty
|
||||
|
||||
from project_management.models.projects import Project
|
||||
from project_management.models.project_milestone import ProjectMilestone
|
||||
|
||||
|
||||
|
||||
class ProjectMilestoneSerializer(
|
||||
serializers.ModelSerializer,
|
||||
):
|
||||
|
||||
url = serializers.SerializerMethodField('get_url_project_milestone')
|
||||
|
||||
def get_url_project_milestone(self, item):
|
||||
|
||||
request = self.context.get('request')
|
||||
|
||||
return request.build_absolute_uri(
|
||||
reverse('API:_api_project_milestone-detail',
|
||||
kwargs={
|
||||
'project_id': item.project.id,
|
||||
'pk': item.id
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
model = ProjectMilestone
|
||||
|
||||
fields = [
|
||||
'name',
|
||||
'description',
|
||||
'organization',
|
||||
'project',
|
||||
'start_date',
|
||||
'finish_date',
|
||||
'created',
|
||||
'modified',
|
||||
'url',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
def __init__(self, instance=None, data=empty, **kwargs):
|
||||
|
||||
self.fields.fields['organization'].read_only = True
|
||||
self.fields.fields['project'].read_only = True
|
||||
|
||||
super().__init__(instance=instance, data=data, **kwargs)
|
||||
|
||||
|
||||
def is_valid(self, *, raise_exception=False):
|
||||
|
||||
is_valid = super().is_valid(raise_exception=raise_exception)
|
||||
|
||||
project = Project.objects.get(
|
||||
pk = int(self._kwargs['context']['view'].kwargs['project_id'])
|
||||
)
|
||||
|
||||
self._validated_data.update({
|
||||
'organization': project.organization,
|
||||
'project': project
|
||||
})
|
||||
|
||||
return is_valid
|
33
app/api/serializers/project_management/project_state.py
Normal file
33
app/api/serializers/project_management/project_state.py
Normal file
@ -0,0 +1,33 @@
|
||||
from django.urls import reverse
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.fields import empty
|
||||
|
||||
from project_management.models.project_states import ProjectState
|
||||
|
||||
|
||||
|
||||
class ProjectStateSerializer(
|
||||
serializers.ModelSerializer,
|
||||
):
|
||||
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="API:_api_project_state-detail", format="html"
|
||||
)
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
model = ProjectState
|
||||
|
||||
fields = '__all__'
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
def __init__(self, instance=None, data=empty, **kwargs):
|
||||
|
||||
super().__init__(instance=instance, data=data, **kwargs)
|
63
app/api/serializers/project_management/project_task.py
Normal file
63
app/api/serializers/project_management/project_task.py
Normal file
@ -0,0 +1,63 @@
|
||||
from rest_framework.fields import empty
|
||||
|
||||
from api.serializers.core.ticket import TicketSerializer
|
||||
|
||||
from core.models.ticket.ticket import Ticket
|
||||
|
||||
|
||||
|
||||
class ProjectTaskSerializer(
|
||||
TicketSerializer,
|
||||
):
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Ticket
|
||||
|
||||
fields = [
|
||||
'id',
|
||||
'assigned_teams',
|
||||
'assigned_users',
|
||||
'category',
|
||||
'created',
|
||||
'modified',
|
||||
'status',
|
||||
'title',
|
||||
'description',
|
||||
'estimate',
|
||||
'urgency',
|
||||
'impact',
|
||||
'priority',
|
||||
'external_ref',
|
||||
'external_system',
|
||||
'ticket_type',
|
||||
'is_deleted',
|
||||
'date_closed',
|
||||
'planned_start_date',
|
||||
'planned_finish_date',
|
||||
'real_start_date',
|
||||
'real_finish_date',
|
||||
'opened_by',
|
||||
'organization',
|
||||
'project',
|
||||
'milestone',
|
||||
'subscribed_teams',
|
||||
'subscribed_users',
|
||||
'ticket_comments',
|
||||
'url',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'ticket_type',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
def __init__(self, instance=None, data=empty, **kwargs):
|
||||
|
||||
super().__init__(instance=instance, data=data, **kwargs)
|
||||
|
||||
self.fields.fields['category'].queryset = self.fields.fields['category'].queryset.filter(
|
||||
project_task = True
|
||||
)
|
33
app/api/serializers/project_management/project_type.py
Normal file
33
app/api/serializers/project_management/project_type.py
Normal file
@ -0,0 +1,33 @@
|
||||
from django.urls import reverse
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.fields import empty
|
||||
|
||||
from project_management.models.project_types import ProjectType
|
||||
|
||||
|
||||
|
||||
class ProjectTypeSerializer(
|
||||
serializers.ModelSerializer,
|
||||
):
|
||||
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="API:_api_project_state-detail", format="html"
|
||||
)
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
model = ProjectType
|
||||
|
||||
fields = '__all__'
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
def __init__(self, instance=None, data=empty, **kwargs):
|
||||
|
||||
super().__init__(instance=instance, data=data, **kwargs)
|
134
app/api/serializers/project_management/projects.py
Normal file
134
app/api/serializers/project_management/projects.py
Normal file
@ -0,0 +1,134 @@
|
||||
from django.urls import reverse
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.fields import empty
|
||||
|
||||
from project_management.models.projects import Project
|
||||
|
||||
|
||||
|
||||
class ProjectSerializer(
|
||||
serializers.ModelSerializer,
|
||||
):
|
||||
|
||||
percent_completed = serializers.CharField(
|
||||
read_only = True,
|
||||
)
|
||||
|
||||
url = serializers.SerializerMethodField('get_url')
|
||||
|
||||
|
||||
def get_url(self, item):
|
||||
|
||||
request = self.context.get('request')
|
||||
|
||||
return request.build_absolute_uri(reverse("API:_api_projects-detail", args=[item.pk]))
|
||||
|
||||
|
||||
project_tasks_url = serializers.SerializerMethodField('get_url_project_tasks')
|
||||
|
||||
|
||||
def get_url_project_tasks(self, item):
|
||||
|
||||
request = self.context.get('request')
|
||||
|
||||
return request.build_absolute_uri(
|
||||
reverse(
|
||||
'API:_api_project_tasks-list',
|
||||
kwargs={
|
||||
'project_id': item.id
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
project_milestone_url = serializers.SerializerMethodField('get_url_project_milestone')
|
||||
|
||||
def get_url_project_milestone(self, item):
|
||||
|
||||
request = self.context.get('request')
|
||||
|
||||
return request.build_absolute_uri(
|
||||
reverse(
|
||||
'API:_api_project_milestone-list',
|
||||
kwargs={
|
||||
'project_id': item.id
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Project
|
||||
|
||||
fields = [
|
||||
'id',
|
||||
'organization',
|
||||
'state',
|
||||
'project_type',
|
||||
'priority',
|
||||
'name',
|
||||
'description',
|
||||
'code',
|
||||
'planned_start_date',
|
||||
'planned_finish_date',
|
||||
'real_start_date',
|
||||
'real_finish_date',
|
||||
'manager_user',
|
||||
'manager_team',
|
||||
'team_members',
|
||||
'project_tasks_url',
|
||||
'project_milestone_url',
|
||||
'percent_completed',
|
||||
'created',
|
||||
'modified',
|
||||
'url',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'url',
|
||||
'created',
|
||||
'modified',
|
||||
]
|
||||
|
||||
|
||||
|
||||
class ProjectImportSerializer(ProjectSerializer):
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Project
|
||||
|
||||
fields = [
|
||||
'id',
|
||||
'organization',
|
||||
'state',
|
||||
'project_type',
|
||||
'priority',
|
||||
'name',
|
||||
'description',
|
||||
'code',
|
||||
'planned_start_date',
|
||||
'planned_finish_date',
|
||||
'real_start_date',
|
||||
'real_finish_date',
|
||||
'manager_user',
|
||||
'manager_team',
|
||||
'team_members',
|
||||
'project_tasks_url',
|
||||
'project_milestone_url',
|
||||
'percent_completed',
|
||||
'created',
|
||||
'modified',
|
||||
'external_ref',
|
||||
'external_system',
|
||||
'is_deleted',
|
||||
'url',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'url',
|
||||
]
|
@ -194,7 +194,7 @@ class APIPermissionAdd:
|
||||
def test_add_has_permission(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
Attempt to add as user with no permission
|
||||
Attempt to add as user with permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
|
@ -502,7 +502,8 @@ class InventoryAPIDifferentNameSerialNumberMatch(TestCase):
|
||||
|
||||
Device.objects.create(
|
||||
name='random device name',
|
||||
serial_number='serial_number_123'
|
||||
serial_number='serial_number_123',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
add_permissions = Permission.objects.get(
|
||||
@ -537,7 +538,7 @@ class InventoryAPIDifferentNameSerialNumberMatch(TestCase):
|
||||
process_inventory(json.dumps(self.inventory), organization.id)
|
||||
|
||||
|
||||
self.device = Device.objects.get(name=self.inventory['details']['name'])
|
||||
self.device = Device.objects.get(name=self.inventory['details']['name'], organization = organization)
|
||||
|
||||
self.operating_system = OperatingSystem.objects.get(name=self.inventory['os']['name'])
|
||||
|
||||
@ -778,7 +779,8 @@ class InventoryAPIDifferentNameUUIDMatch(TestCase):
|
||||
|
||||
Device.objects.create(
|
||||
name='random device name',
|
||||
uuid='123-456-789'
|
||||
uuid='123-456-789',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
add_permissions = Permission.objects.get(
|
||||
|
@ -5,6 +5,25 @@ from rest_framework.urlpatterns import format_suffix_patterns
|
||||
|
||||
from .views import access, config, index
|
||||
|
||||
from api.views.settings import permissions
|
||||
from api.views.settings import index as settings
|
||||
|
||||
from api.views import assistance, itim, project_management
|
||||
from api.views.assistance import request_ticket
|
||||
from api.views.core import (
|
||||
ticket_categories,
|
||||
ticket_comment_categories,
|
||||
ticket_comments as core_ticket_comments
|
||||
)
|
||||
from api.views.itim import change_ticket, incident_ticket, problem_ticket
|
||||
from api.views.project_management import (
|
||||
projects,
|
||||
project_milestone,
|
||||
project_state,
|
||||
project_type,
|
||||
project_task
|
||||
)
|
||||
|
||||
from .views.itam import software, config as itam_config
|
||||
from .views.itam.device import DeviceViewSet
|
||||
from .views.itam import inventory
|
||||
@ -13,15 +32,47 @@ from .views.itam import inventory
|
||||
app_name = "API"
|
||||
|
||||
|
||||
router = DefaultRouter()
|
||||
router = DefaultRouter(trailing_slash=False)
|
||||
|
||||
router.register('', index.Index, basename='_api_home')
|
||||
|
||||
router.register('assistance/request', request_ticket.View, basename='_api_assistance_request')
|
||||
router.register('assistance/request/(?P<ticket_id>[0-9]+)/comments', core_ticket_comments.View, basename='_api_assistance_request_ticket_comments')
|
||||
|
||||
router.register('device', DeviceViewSet, basename='device')
|
||||
|
||||
router.register('itim/change', change_ticket.View, basename='_api_itim_change')
|
||||
router.register('itim/change/(?P<ticket_id>[0-9]+)/comments', core_ticket_comments.View, basename='_api_itim_change_ticket_comments')
|
||||
|
||||
router.register('itim/incident', incident_ticket.View, basename='_api_itim_incident')
|
||||
router.register('itim/incident/(?P<ticket_id>[0-9]+)/comments', core_ticket_comments.View, basename='_api_itim_incident_ticket_comments')
|
||||
|
||||
router.register('itim/problem', problem_ticket.View, basename='_api_itim_problem')
|
||||
router.register('itim/problem/(?P<ticket_id>[0-9]+)/comments', core_ticket_comments.View, basename='_api_itim_problem_ticket_comments')
|
||||
|
||||
router.register('project_management/projects', projects.View, basename='_api_projects')
|
||||
router.register('project_management/projects/(?P<project_id>[0-9]+)/milestones', project_milestone.View, basename='_api_project_milestone')
|
||||
router.register('project_management/projects/(?P<project_id>[0-9]+)/tasks', project_task.View, basename='_api_project_tasks')
|
||||
router.register('project_management/projects/(?P<project_id>[0-9]+)/tasks/(?P<ticket_id>[0-9]+)/comments', core_ticket_comments.View, basename='_api_project_tasks_comments')
|
||||
|
||||
router.register('settings/ticket_categories', ticket_categories.View, basename='_api_ticket_category')
|
||||
|
||||
router.register('settings/project_state', project_state.View, basename='_api_project_state')
|
||||
router.register('settings/project_type', project_type.View, basename='_api_project_type')
|
||||
router.register('settings/ticket_comment_categories', ticket_comment_categories.View, basename='_api_ticket_comment_category')
|
||||
|
||||
router.register('software', software.SoftwareViewSet, basename='software')
|
||||
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
path("assistance", assistance.index.Index.as_view(), name="_api_assistance"),
|
||||
|
||||
#
|
||||
# Sof Old Paths to be refactored
|
||||
#
|
||||
|
||||
path("config/<slug:slug>/", itam_config.View.as_view(), name="_api_device_config"),
|
||||
|
||||
path("configuration/", config.ConfigGroupsList.as_view(), name='_api_config_groups'),
|
||||
@ -29,6 +80,8 @@ urlpatterns = [
|
||||
|
||||
path("device/inventory", inventory.Collect.as_view(), name="_api_device_inventory"),
|
||||
|
||||
path("itim", itim.index.Index.as_view(), name="_api_itim"),
|
||||
|
||||
path("organization/", access.OrganizationList.as_view(), name='_api_orgs'),
|
||||
path("organization/<int:pk>/", access.OrganizationDetail.as_view(), name='_api_organization'),
|
||||
path("organization/<int:organization_id>/team", access.TeamList.as_view(), name='_api_organization_teams'),
|
||||
@ -36,6 +89,11 @@ urlpatterns = [
|
||||
path("organization/<int:organization_id>/team/<int:group_ptr_id>/permissions", access.TeamPermissionDetail.as_view(), name='_api_team_permission'),
|
||||
path("organization/team/", access.TeamList.as_view(), name='_api_teams'),
|
||||
|
||||
path("project_management", project_management.index.Index.as_view(), name="_api_project_management"),
|
||||
|
||||
path("settings", settings.View.as_view(), name='_settings'),
|
||||
path("settings/permissions", permissions.View.as_view(), name='_settings_permissions'),
|
||||
|
||||
]
|
||||
|
||||
urlpatterns = format_suffix_patterns(urlpatterns)
|
||||
|
1
app/api/views/assistance/__init__.py
Normal file
1
app/api/views/assistance/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .index import *
|
35
app/api/views/assistance/index.py
Normal file
35
app/api/views/assistance/index.py
Normal file
@ -0,0 +1,35 @@
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from rest_framework import generics, permissions, routers, views
|
||||
# from rest_framework.decorators import api_view
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
|
||||
|
||||
class Index(views.APIView):
|
||||
|
||||
permission_classes = [
|
||||
IsAuthenticated,
|
||||
]
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
return "Assistance"
|
||||
|
||||
def get_view_description(self, html=False) -> str:
|
||||
text = "Assistance Module"
|
||||
if html:
|
||||
return mark_safe(f"<p>{text}</p>")
|
||||
else:
|
||||
return text
|
||||
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
body: dict = {
|
||||
'requests': reverse('API:_api_assistance_request-list', request=request)
|
||||
}
|
||||
|
||||
return Response(body)
|
77
app/api/views/assistance/request_ticket.py
Normal file
77
app/api/views/assistance/request_ticket.py
Normal file
@ -0,0 +1,77 @@
|
||||
from drf_spectacular.utils import extend_schema, OpenApiResponse
|
||||
|
||||
from api.serializers.assistance.request import RequestTicketSerializer
|
||||
from api.views.core.tickets import View
|
||||
|
||||
|
||||
class View(View):
|
||||
|
||||
_ticket_type:str = 'request'
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Create a ticket',
|
||||
description = """This model includes all of the ticket types.
|
||||
Due to this not all fields will be available and what fields are available
|
||||
depends upon the comment type. see
|
||||
[administration docs](https://nofusscomputing.com/projects/centurion_erp/administration/core/ticketing/index.html) for more info.
|
||||
""",
|
||||
request = RequestTicketSerializer,
|
||||
responses = {
|
||||
201: OpenApiResponse(
|
||||
response = RequestTicketSerializer,
|
||||
),
|
||||
}
|
||||
)
|
||||
def create(self, request, *args, **kwargs):
|
||||
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Fetch all tickets',
|
||||
description = """This model includes all of the ticket comment types.
|
||||
Due to this not all fields will be available and what fields are available
|
||||
depends upon the comment type. see
|
||||
[administration docs](https://nofusscomputing.com/projects/centurion_erp/administration/core/ticketing/index.html) for more info.
|
||||
""",
|
||||
methods=["GET"],
|
||||
responses = {
|
||||
200: OpenApiResponse(
|
||||
description='Success',
|
||||
response = RequestTicketSerializer
|
||||
)
|
||||
}
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Fetch the selected ticket',
|
||||
description = """This model includes all of the ticket comment types.
|
||||
Due to this not all fields will be available and what fields are available
|
||||
depends upon the comment type. see
|
||||
[administration docs](https://nofusscomputing.com/projects/centurion_erp/administration/core/ticketing/index.html) for more info.
|
||||
""",
|
||||
methods=["GET"],
|
||||
responses = {
|
||||
200: OpenApiResponse(
|
||||
description='Success',
|
||||
response = RequestTicketSerializer
|
||||
)
|
||||
}
|
||||
)
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
|
||||
return super().retrieve(request, *args, **kwargs)
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
|
||||
if self.detail:
|
||||
return "Request Ticket"
|
||||
|
||||
return 'Request Tickets'
|
79
app/api/views/core/ticket_categories.py
Normal file
79
app/api/views/core/ticket_categories.py
Normal file
@ -0,0 +1,79 @@
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from drf_spectacular.utils import extend_schema, OpenApiResponse
|
||||
|
||||
from rest_framework import generics, viewsets
|
||||
|
||||
from access.mixin import OrganizationMixin
|
||||
|
||||
from api.serializers.core.ticket_category import TicketCategory, TicketCategorySerializer
|
||||
from api.views.mixin import OrganizationPermissionAPI
|
||||
|
||||
|
||||
|
||||
class View(OrganizationMixin, viewsets.ModelViewSet):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
]
|
||||
|
||||
queryset = TicketCategory.objects.all()
|
||||
|
||||
serializer_class = TicketCategorySerializer
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Create a ticket category',
|
||||
request = TicketCategorySerializer,
|
||||
responses = {
|
||||
201: OpenApiResponse(description='Ticket category created', response=TicketCategorySerializer),
|
||||
403: OpenApiResponse(description='User tried to edit field they dont have access to'),
|
||||
}
|
||||
)
|
||||
def create(self, request, *args, **kwargs):
|
||||
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Fetch all of a tickets category',
|
||||
methods=["GET"],
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Success', response=TicketCategorySerializer),
|
||||
}
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Fetch the selected ticket category',
|
||||
methods=["GET"],
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Success', response=TicketCategorySerializer),
|
||||
}
|
||||
)
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
|
||||
return super().retrieve(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Update a ticket category',
|
||||
methods=["PUT"],
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Ticket comment updated', response=TicketCategorySerializer),
|
||||
403: OpenApiResponse(description='User tried to edit field they dont have access to'),
|
||||
}
|
||||
)
|
||||
def update(self, request, *args, **kwargs):
|
||||
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
if self.detail:
|
||||
return "Ticket Category"
|
||||
|
||||
return 'Ticket Categories'
|
79
app/api/views/core/ticket_comment_categories.py
Normal file
79
app/api/views/core/ticket_comment_categories.py
Normal file
@ -0,0 +1,79 @@
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from drf_spectacular.utils import extend_schema, OpenApiResponse
|
||||
|
||||
from rest_framework import generics, viewsets
|
||||
|
||||
from access.mixin import OrganizationMixin
|
||||
|
||||
from api.serializers.core.ticket_comment_category import TicketCommentCategory, TicketCommentCategorySerializer
|
||||
from api.views.mixin import OrganizationPermissionAPI
|
||||
|
||||
|
||||
|
||||
class View(OrganizationMixin, viewsets.ModelViewSet):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
]
|
||||
|
||||
queryset = TicketCommentCategory.objects.all()
|
||||
|
||||
serializer_class = TicketCommentCategorySerializer
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Create a ticket comment category',
|
||||
request = TicketCommentCategorySerializer,
|
||||
responses = {
|
||||
201: OpenApiResponse(description='Ticket category created', response=TicketCommentCategorySerializer),
|
||||
403: OpenApiResponse(description='User tried to edit field they dont have access to'),
|
||||
}
|
||||
)
|
||||
def create(self, request, *args, **kwargs):
|
||||
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Fetch all of the ticket comment categories',
|
||||
methods=["GET"],
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Success', response=TicketCommentCategorySerializer),
|
||||
}
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Fetch the selected ticket comment category',
|
||||
methods=["GET"],
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Success', response=TicketCommentCategorySerializer),
|
||||
}
|
||||
)
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
|
||||
return super().retrieve(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Update a ticket comment category',
|
||||
methods=["PUT"],
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Ticket comment updated', response=TicketCommentCategorySerializer),
|
||||
403: OpenApiResponse(description='User tried to edit field they dont have access to'),
|
||||
}
|
||||
)
|
||||
def update(self, request, *args, **kwargs):
|
||||
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
if self.detail:
|
||||
return "Ticket Comment Category"
|
||||
|
||||
return 'Ticket Comment Categories'
|
102
app/api/views/core/ticket_comments.py
Normal file
102
app/api/views/core/ticket_comments.py
Normal file
@ -0,0 +1,102 @@
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from drf_spectacular.utils import extend_schema, OpenApiResponse
|
||||
|
||||
from rest_framework import generics, viewsets
|
||||
|
||||
from access.mixin import OrganizationMixin
|
||||
|
||||
from api.serializers.core.ticket_comment import TicketCommentSerializer
|
||||
from api.views.mixin import OrganizationPermissionAPI
|
||||
|
||||
from core.models.ticket.ticket_comment import TicketComment
|
||||
|
||||
|
||||
|
||||
class View(OrganizationMixin, viewsets.ModelViewSet):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
]
|
||||
|
||||
queryset = TicketComment.objects.all()
|
||||
|
||||
serializer_class = TicketCommentSerializer
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Create a ticket comment',
|
||||
description = """This model includes all of the ticket comment types.
|
||||
Due to this not all fields will be available and what fields are available
|
||||
depends upon the comment type.
|
||||
""",
|
||||
request = TicketCommentSerializer,
|
||||
responses = {
|
||||
201: OpenApiResponse(description='Ticket comment created', response=TicketCommentSerializer),
|
||||
403: OpenApiResponse(description='User tried to edit field they dont have access to'),
|
||||
}
|
||||
)
|
||||
def create(self, request, *args, **kwargs):
|
||||
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Fetch all of a tickets comments',
|
||||
methods=["GET"],
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Success', response=TicketCommentSerializer),
|
||||
}
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Fetch the selected ticket Comment',
|
||||
methods=["GET"],
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Success', response=TicketCommentSerializer),
|
||||
}
|
||||
)
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
|
||||
return super().retrieve(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Update a ticket Comment',
|
||||
description = """This model includes all of the ticket comment types.
|
||||
Due to this not all fields will be available and what fields are available
|
||||
depends upon the comment type.
|
||||
""",
|
||||
methods=["PUT"],
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Ticket comment updated', response=TicketCommentSerializer),
|
||||
403: OpenApiResponse(description='User tried to edit field they dont have access to'),
|
||||
}
|
||||
)
|
||||
def update(self, request, *args, **kwargs):
|
||||
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
if 'ticket_id' in self.kwargs:
|
||||
|
||||
self.queryset = self.queryset.filter(ticket=self.kwargs['ticket_id']).order_by('created')
|
||||
|
||||
if 'pk' in self.kwargs:
|
||||
|
||||
self.queryset = self.queryset.filter(pk = self.kwargs['pk'])
|
||||
|
||||
return self.queryset
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
if self.detail:
|
||||
return "Ticket Comment"
|
||||
|
||||
return 'Ticket Comments'
|
145
app/api/views/core/tickets.py
Normal file
145
app/api/views/core/tickets.py
Normal file
@ -0,0 +1,145 @@
|
||||
from django.db.models import Q
|
||||
|
||||
from rest_framework import generics, viewsets
|
||||
|
||||
from access.mixin import OrganizationMixin
|
||||
|
||||
from api.serializers.assistance.request import RequestTicketSerializer
|
||||
from api.serializers.itim.change import ChangeTicketSerializer
|
||||
from api.serializers.itim.incident import IncidentTicketSerializer
|
||||
from api.serializers.itim.problem import ProblemTicketSerializer
|
||||
from api.serializers.project_management.project_task import ProjectTaskSerializer
|
||||
from api.views.mixin import OrganizationPermissionAPI
|
||||
|
||||
from core.models.ticket.ticket import Ticket
|
||||
|
||||
|
||||
|
||||
class View(OrganizationMixin, viewsets.ModelViewSet):
|
||||
|
||||
filterset_fields = [
|
||||
'external_system',
|
||||
'external_ref',
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
'title',
|
||||
'description',
|
||||
]
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
]
|
||||
|
||||
def get_dynamic_permissions(self):
|
||||
|
||||
if self.action == 'create':
|
||||
|
||||
action_keyword = 'add'
|
||||
|
||||
elif self.action == 'destroy':
|
||||
|
||||
action_keyword = 'delete'
|
||||
|
||||
elif self.action == 'list':
|
||||
|
||||
action_keyword = 'view'
|
||||
|
||||
elif self.action == 'partial_update':
|
||||
|
||||
action_keyword = 'change'
|
||||
|
||||
elif self.action == 'retrieve':
|
||||
|
||||
action_keyword = 'view'
|
||||
|
||||
elif self.action == 'update':
|
||||
|
||||
action_keyword = 'change'
|
||||
|
||||
elif self.action is None:
|
||||
|
||||
action_keyword = 'view'
|
||||
|
||||
else:
|
||||
|
||||
raise ValueError('unable to determin the action_keyword')
|
||||
|
||||
self.permission_required = [
|
||||
'core.' + action_keyword + '_ticket_' + self._ticket_type,
|
||||
]
|
||||
|
||||
return super().get_permission_required()
|
||||
|
||||
|
||||
queryset = Ticket.objects.all()
|
||||
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
|
||||
if self._ticket_type == 'change':
|
||||
|
||||
self.serializer_class = ChangeTicketSerializer
|
||||
|
||||
self._ticket_type_value = Ticket.TicketType.CHANGE.value
|
||||
|
||||
elif self._ticket_type == 'incident':
|
||||
|
||||
self.serializer_class = IncidentTicketSerializer
|
||||
self._ticket_type_value = Ticket.TicketType.INCIDENT.value
|
||||
|
||||
elif self._ticket_type == 'problem':
|
||||
|
||||
self.serializer_class = ProblemTicketSerializer
|
||||
self._ticket_type_value = Ticket.TicketType.PROBLEM.value
|
||||
|
||||
elif self._ticket_type == 'request':
|
||||
|
||||
self.serializer_class = RequestTicketSerializer
|
||||
self._ticket_type_value = Ticket.TicketType.REQUEST.value
|
||||
|
||||
elif self._ticket_type == 'project_task':
|
||||
|
||||
self.serializer_class = ProjectTaskSerializer
|
||||
self._ticket_type_value = Ticket.TicketType.PROJECT_TASK.value
|
||||
|
||||
else:
|
||||
|
||||
raise ValueError('unable to determin the serializer_class')
|
||||
|
||||
return super().get_serializer(*args, **kwargs)
|
||||
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
if self._ticket_type == 'change':
|
||||
|
||||
ticket_type = self.queryset.model.TicketType.CHANGE.value
|
||||
|
||||
elif self._ticket_type == 'incident':
|
||||
|
||||
ticket_type = self.queryset.model.TicketType.INCIDENT.value
|
||||
|
||||
elif self._ticket_type == 'problem':
|
||||
|
||||
ticket_type = self.queryset.model.TicketType.PROBLEM.value
|
||||
|
||||
elif self._ticket_type == 'request':
|
||||
|
||||
ticket_type = self.queryset.model.TicketType.REQUEST.value
|
||||
|
||||
elif self._ticket_type == 'project_task':
|
||||
|
||||
ticket_type = self.queryset.model.TicketType.REQUEST.value
|
||||
|
||||
return self.queryset.filter(
|
||||
project = self.kwargs['project_id']
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
raise ValueError('Unknown ticket type. kwarg `ticket_type` must be set')
|
||||
|
||||
return self.queryset.filter(
|
||||
ticket_type = ticket_type
|
||||
)
|
@ -1,15 +1,19 @@
|
||||
# from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from rest_framework import generics, permissions, routers, viewsets
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
|
||||
|
||||
class Index(viewsets.ViewSet):
|
||||
|
||||
# permission_required = 'access.view_organization'
|
||||
permission_classes = [
|
||||
IsAuthenticated,
|
||||
]
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
return "API Index"
|
||||
@ -26,9 +30,13 @@ class Index(viewsets.ViewSet):
|
||||
return Response(
|
||||
{
|
||||
# "teams": reverse("_api_teams", request=request),
|
||||
'assistance': reverse("API:_api_assistance", request=request),
|
||||
"devices": reverse("API:device-list", request=request),
|
||||
"config_groups": reverse("API:_api_config_groups", request=request),
|
||||
'itim': reverse("API:_api_itim", request=request),
|
||||
"organizations": reverse("API:_api_orgs", request=request),
|
||||
'project_management': reverse("API:_api_project_management", request=request),
|
||||
"settings": reverse('API:_settings', request=request),
|
||||
"software": reverse("API:software-list", request=request),
|
||||
}
|
||||
)
|
||||
|
@ -1,9 +1,10 @@
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from drf_spectacular.utils import extend_schema, OpenApiResponse
|
||||
|
||||
from rest_framework import generics, viewsets
|
||||
from rest_framework.response import Response
|
||||
|
||||
from access.mixin import OrganizationMixin
|
||||
|
||||
@ -24,6 +25,46 @@ class DeviceViewSet(OrganizationMixin, viewsets.ModelViewSet):
|
||||
|
||||
serializer_class = DeviceSerializer
|
||||
|
||||
@extend_schema(
|
||||
summary = 'Create a device',
|
||||
description="""Add a new device to the ITAM database.
|
||||
If you attempt to create a device and a device with a matching name and uuid or name and serial number
|
||||
is found within the database, it will not re-create it. The device will be returned within the message body.
|
||||
""",
|
||||
methods=["POST"],
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Device allready exists', response=DeviceSerializer),
|
||||
201: OpenApiResponse(description='Device created', response=DeviceSerializer),
|
||||
400: OpenApiResponse(description='Validation failed.'),
|
||||
403: OpenApiResponse(description='User is missing create permissions'),
|
||||
}
|
||||
)
|
||||
def create(self, request, *args, **kwargs):
|
||||
|
||||
current_device = []
|
||||
|
||||
if 'uuid' in self.request.POST:
|
||||
|
||||
current_device = self.serializer_class.Meta.model.objects.filter(
|
||||
organization = int(self.request.POST['organization']),
|
||||
uuid = str(self.request.POST['uuid'])
|
||||
)
|
||||
|
||||
if 'serial_number' in self.request.POST and len(current_device) == 0:
|
||||
|
||||
current_device = self.serializer_class.Meta.model.objects.filter(
|
||||
organization = int(self.request.POST['organization']),
|
||||
serial_number = str(self.request.POST['serial_number'])
|
||||
)
|
||||
|
||||
if len(current_device) == 1:
|
||||
|
||||
instance = current_device.get()
|
||||
serializer = self.get_serializer(instance)
|
||||
return Response(serializer.data)
|
||||
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema( description='Fetch devices that are from the users assigned organization(s)', methods=["GET"])
|
||||
def list(self, 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
app/api/views/itim/__init__.py
Normal file
1
app/api/views/itim/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .index import *
|
82
app/api/views/itim/change_ticket.py
Normal file
82
app/api/views/itim/change_ticket.py
Normal file
@ -0,0 +1,82 @@
|
||||
from drf_spectacular.utils import extend_schema, OpenApiResponse
|
||||
|
||||
from api.serializers.itim.change import ChangeTicketSerializer
|
||||
|
||||
from api.views.core.tickets import View
|
||||
|
||||
|
||||
|
||||
class View(View):
|
||||
|
||||
_ticket_type:str = 'change'
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Create a ticket',
|
||||
description = """This model includes all of the ticket types.
|
||||
Due to this not all fields will be available and what fields are available
|
||||
depends upon the comment type. see
|
||||
[administration docs](https://nofusscomputing.com/projects/centurion_erp/administration/core/ticketing/index.html) for more info.
|
||||
""",
|
||||
request = ChangeTicketSerializer,
|
||||
responses = {
|
||||
201: OpenApiResponse(
|
||||
response = ChangeTicketSerializer,
|
||||
),
|
||||
}
|
||||
)
|
||||
def create(self, request, *args, **kwargs):
|
||||
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Fetch all tickets',
|
||||
description = """This model includes all of the ticket comment types.
|
||||
Due to this not all fields will be available and what fields are available
|
||||
depends upon the comment type. see
|
||||
[administration docs](https://nofusscomputing.com/projects/centurion_erp/administration/core/ticketing/index.html) for more info.
|
||||
""",
|
||||
methods=["GET"],
|
||||
responses = {
|
||||
200: OpenApiResponse(
|
||||
description='Success',
|
||||
response = ChangeTicketSerializer
|
||||
)
|
||||
}
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Fetch the selected ticket',
|
||||
description = """This model includes all of the ticket comment types.
|
||||
Due to this not all fields will be available and what fields are available
|
||||
depends upon the comment type. see
|
||||
[administration docs](https://nofusscomputing.com/projects/centurion_erp/administration/core/ticketing/index.html) for more info.
|
||||
""",
|
||||
methods=["GET"],
|
||||
responses = {
|
||||
200: OpenApiResponse(
|
||||
description='Success',
|
||||
response = ChangeTicketSerializer
|
||||
)
|
||||
}
|
||||
)
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
|
||||
return super().retrieve(request, *args, **kwargs)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
|
||||
if self.detail:
|
||||
return "Change Ticket"
|
||||
|
||||
return 'Change Tickets'
|
81
app/api/views/itim/incident_ticket.py
Normal file
81
app/api/views/itim/incident_ticket.py
Normal file
@ -0,0 +1,81 @@
|
||||
from drf_spectacular.utils import extend_schema, OpenApiResponse
|
||||
|
||||
from api.serializers.itim.incident import IncidentTicketSerializer
|
||||
from api.views.core.tickets import View
|
||||
|
||||
|
||||
|
||||
class View(View):
|
||||
|
||||
_ticket_type:str = 'incident'
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Create a ticket',
|
||||
description = """This model includes all of the ticket types.
|
||||
Due to this not all fields will be available and what fields are available
|
||||
depends upon the comment type. see
|
||||
[administration docs](https://nofusscomputing.com/projects/centurion_erp/administration/core/ticketing/index.html) for more info.
|
||||
""",
|
||||
request = IncidentTicketSerializer,
|
||||
responses = {
|
||||
201: OpenApiResponse(
|
||||
response = IncidentTicketSerializer,
|
||||
),
|
||||
}
|
||||
)
|
||||
def create(self, request, *args, **kwargs):
|
||||
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Fetch all tickets',
|
||||
description = """This model includes all of the ticket comment types.
|
||||
Due to this not all fields will be available and what fields are available
|
||||
depends upon the comment type. see
|
||||
[administration docs](https://nofusscomputing.com/projects/centurion_erp/administration/core/ticketing/index.html) for more info.
|
||||
""",
|
||||
methods=["GET"],
|
||||
responses = {
|
||||
200: OpenApiResponse(
|
||||
description='Success',
|
||||
response = IncidentTicketSerializer
|
||||
)
|
||||
}
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Fetch the selected ticket',
|
||||
description = """This model includes all of the ticket comment types.
|
||||
Due to this not all fields will be available and what fields are available
|
||||
depends upon the comment type. see
|
||||
[administration docs](https://nofusscomputing.com/projects/centurion_erp/administration/core/ticketing/index.html) for more info.
|
||||
""",
|
||||
methods=["GET"],
|
||||
responses = {
|
||||
200: OpenApiResponse(
|
||||
description='Success',
|
||||
response = IncidentTicketSerializer
|
||||
)
|
||||
}
|
||||
)
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
|
||||
return super().retrieve(request, *args, **kwargs)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
|
||||
if self.detail:
|
||||
return "Incident Ticket"
|
||||
|
||||
return 'Incident Tickets'
|
36
app/api/views/itim/index.py
Normal file
36
app/api/views/itim/index.py
Normal file
@ -0,0 +1,36 @@
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from rest_framework import views
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
|
||||
|
||||
class Index(views.APIView):
|
||||
|
||||
permission_classes = [
|
||||
IsAuthenticated,
|
||||
]
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
return "ITIM"
|
||||
|
||||
def get_view_description(self, html=False) -> str:
|
||||
text = "ITIM Module"
|
||||
if html:
|
||||
return mark_safe(f"<p>{text}</p>")
|
||||
else:
|
||||
return text
|
||||
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
body: dict = {
|
||||
'changes': reverse('API:_api_itim_change-list', request=request),
|
||||
'incidents': reverse('API:_api_itim_incident-list', request=request),
|
||||
'problems': reverse('API:_api_itim_problem-list', request=request),
|
||||
}
|
||||
|
||||
return Response(body)
|
81
app/api/views/itim/problem_ticket.py
Normal file
81
app/api/views/itim/problem_ticket.py
Normal file
@ -0,0 +1,81 @@
|
||||
from drf_spectacular.utils import extend_schema, OpenApiResponse
|
||||
|
||||
from api.serializers.itim.problem import ProblemTicketSerializer
|
||||
from api.views.core.tickets import View
|
||||
|
||||
|
||||
|
||||
class View(View):
|
||||
|
||||
_ticket_type:str = 'problem'
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Create a ticket',
|
||||
description = """This model includes all of the ticket types.
|
||||
Due to this not all fields will be available and what fields are available
|
||||
depends upon the comment type. see
|
||||
[administration docs](https://nofusscomputing.com/projects/centurion_erp/administration/core/ticketing/index.html) for more info.
|
||||
""",
|
||||
request = ProblemTicketSerializer,
|
||||
responses = {
|
||||
201: OpenApiResponse(
|
||||
response = ProblemTicketSerializer,
|
||||
),
|
||||
}
|
||||
)
|
||||
def create(self, request, *args, **kwargs):
|
||||
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Fetch all tickets',
|
||||
description = """This model includes all of the ticket comment types.
|
||||
Due to this not all fields will be available and what fields are available
|
||||
depends upon the comment type. see
|
||||
[administration docs](https://nofusscomputing.com/projects/centurion_erp/administration/core/ticketing/index.html) for more info.
|
||||
""",
|
||||
methods=["GET"],
|
||||
responses = {
|
||||
200: OpenApiResponse(
|
||||
description='Success',
|
||||
response = ProblemTicketSerializer
|
||||
)
|
||||
}
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Fetch the selected ticket',
|
||||
description = """This model includes all of the ticket comment types.
|
||||
Due to this not all fields will be available and what fields are available
|
||||
depends upon the comment type. see
|
||||
[administration docs](https://nofusscomputing.com/projects/centurion_erp/administration/core/ticketing/index.html) for more info.
|
||||
""",
|
||||
methods=["GET"],
|
||||
responses = {
|
||||
200: OpenApiResponse(
|
||||
description='Success',
|
||||
response = ProblemTicketSerializer
|
||||
)
|
||||
}
|
||||
)
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
|
||||
return super().retrieve(request, *args, **kwargs)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
|
||||
if self.detail:
|
||||
return "Problem Ticket"
|
||||
|
||||
return 'Problem Tickets'
|
@ -75,6 +75,12 @@ class OrganizationPermissionAPI(DjangoObjectPermissions, OrganizationMixin):
|
||||
|
||||
self.permission_required = [ permission ]
|
||||
|
||||
if hasattr(view, 'get_dynamic_permissions'):
|
||||
|
||||
self.permission_required = view.get_dynamic_permissions()
|
||||
|
||||
|
||||
|
||||
|
||||
if view:
|
||||
if 'organization_id' in view.kwargs:
|
||||
|
1
app/api/views/project_management/__init__.py
Normal file
1
app/api/views/project_management/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .index import *
|
34
app/api/views/project_management/index.py
Normal file
34
app/api/views/project_management/index.py
Normal file
@ -0,0 +1,34 @@
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from rest_framework import generics, permissions, routers, views
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
|
||||
|
||||
class Index(views.APIView):
|
||||
|
||||
permission_classes = [
|
||||
IsAuthenticated,
|
||||
]
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
return "Projects"
|
||||
|
||||
def get_view_description(self, html=False) -> str:
|
||||
text = "Projects Managementn Module"
|
||||
if html:
|
||||
return mark_safe(f"<p>{text}</p>")
|
||||
else:
|
||||
return text
|
||||
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
body: dict = {
|
||||
'projects': reverse('API:_api_projects-list', request=request)
|
||||
}
|
||||
|
||||
return Response(body)
|
90
app/api/views/project_management/project_milestone.py
Normal file
90
app/api/views/project_management/project_milestone.py
Normal file
@ -0,0 +1,90 @@
|
||||
from drf_spectacular.utils import extend_schema, OpenApiResponse
|
||||
|
||||
from rest_framework import viewsets
|
||||
|
||||
from access.mixin import OrganizationMixin
|
||||
|
||||
from api.serializers.project_management.project_milestone import ProjectMilestone, ProjectMilestoneSerializer
|
||||
# from api.views.core.tickets import View
|
||||
from api.views.mixin import OrganizationPermissionAPI
|
||||
|
||||
|
||||
|
||||
class View(OrganizationMixin, viewsets.ModelViewSet):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
]
|
||||
|
||||
queryset = ProjectMilestone.objects.all()
|
||||
|
||||
serializer_class = ProjectMilestoneSerializer
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Create a project milestone',
|
||||
request = ProjectMilestoneSerializer,
|
||||
responses = {
|
||||
201: OpenApiResponse(
|
||||
response = ProjectMilestoneSerializer,
|
||||
),
|
||||
}
|
||||
)
|
||||
def create(self, request, *args, **kwargs):
|
||||
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Fetch all project milestones',
|
||||
methods=["GET"],
|
||||
responses = {
|
||||
200: OpenApiResponse(
|
||||
description='Success',
|
||||
response = ProjectMilestoneSerializer
|
||||
)
|
||||
}
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Fetch the selected project milestone',
|
||||
methods=["GET"],
|
||||
responses = {
|
||||
200: OpenApiResponse(
|
||||
description='Success',
|
||||
response = ProjectMilestoneSerializer
|
||||
)
|
||||
}
|
||||
)
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
|
||||
return super().retrieve(request, *args, **kwargs)
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
|
||||
if self.detail:
|
||||
return ProjectMilestone._meta.verbose_name
|
||||
|
||||
return ProjectMilestone._meta.verbose_name_plural
|
||||
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
if 'project_id' in self.kwargs:
|
||||
|
||||
self.queryset = self.queryset.filter(
|
||||
project=self.kwargs['project_id']
|
||||
)
|
||||
|
||||
if 'pk' in self.kwargs:
|
||||
|
||||
self.queryset = self.queryset.filter(
|
||||
pk = self.kwargs['pk']
|
||||
)
|
||||
|
||||
return self.queryset
|
73
app/api/views/project_management/project_state.py
Normal file
73
app/api/views/project_management/project_state.py
Normal file
@ -0,0 +1,73 @@
|
||||
from drf_spectacular.utils import extend_schema, OpenApiResponse
|
||||
|
||||
from rest_framework import viewsets
|
||||
|
||||
from access.mixin import OrganizationMixin
|
||||
|
||||
from api.serializers.project_management.project_state import ProjectState, ProjectStateSerializer
|
||||
from api.views.core.tickets import View
|
||||
from api.views.mixin import OrganizationPermissionAPI
|
||||
|
||||
|
||||
|
||||
class View(OrganizationMixin, viewsets.ModelViewSet):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
]
|
||||
|
||||
queryset = ProjectState.objects.all()
|
||||
|
||||
serializer_class = ProjectStateSerializer
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Create a project state',
|
||||
request = ProjectStateSerializer,
|
||||
responses = {
|
||||
201: OpenApiResponse(
|
||||
response = ProjectStateSerializer,
|
||||
),
|
||||
}
|
||||
)
|
||||
def create(self, request, *args, **kwargs):
|
||||
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Fetch all project states',
|
||||
methods=["GET"],
|
||||
responses = {
|
||||
200: OpenApiResponse(
|
||||
description='Success',
|
||||
response = ProjectStateSerializer
|
||||
)
|
||||
}
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Fetch the selected project state',
|
||||
methods=["GET"],
|
||||
responses = {
|
||||
200: OpenApiResponse(
|
||||
description='Success',
|
||||
response = ProjectStateSerializer
|
||||
)
|
||||
}
|
||||
)
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
|
||||
return super().retrieve(request, *args, **kwargs)
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
|
||||
if self.detail:
|
||||
return ProjectState._meta.verbose_name
|
||||
|
||||
return ProjectState._meta.verbose_name_plural
|
64
app/api/views/project_management/project_task.py
Normal file
64
app/api/views/project_management/project_task.py
Normal file
@ -0,0 +1,64 @@
|
||||
from drf_spectacular.utils import extend_schema, OpenApiResponse
|
||||
|
||||
from api.serializers.project_management.project_task import ProjectTaskSerializer
|
||||
|
||||
from api.views.core.tickets import View
|
||||
|
||||
|
||||
|
||||
class View(View):
|
||||
|
||||
_ticket_type:str = 'project_task'
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Create a Project Task',
|
||||
request = ProjectTaskSerializer,
|
||||
responses = {
|
||||
201: OpenApiResponse(
|
||||
response = ProjectTaskSerializer,
|
||||
),
|
||||
}
|
||||
)
|
||||
def create(self, request, *args, **kwargs):
|
||||
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Fetch all project tasks',
|
||||
methods=["GET"],
|
||||
responses = {
|
||||
200: OpenApiResponse(
|
||||
description='Success',
|
||||
response = ProjectTaskSerializer
|
||||
)
|
||||
}
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Fetch the selected project task',
|
||||
methods=["GET"],
|
||||
responses = {
|
||||
200: OpenApiResponse(
|
||||
description='Success',
|
||||
response = ProjectTaskSerializer
|
||||
)
|
||||
}
|
||||
)
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
|
||||
return super().retrieve(request, *args, **kwargs)
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
|
||||
if self.detail:
|
||||
return "Project Task"
|
||||
|
||||
return 'Project Tasks'
|
72
app/api/views/project_management/project_type.py
Normal file
72
app/api/views/project_management/project_type.py
Normal file
@ -0,0 +1,72 @@
|
||||
from drf_spectacular.utils import extend_schema, OpenApiResponse
|
||||
|
||||
from rest_framework import viewsets
|
||||
|
||||
from access.mixin import OrganizationMixin
|
||||
|
||||
from api.serializers.project_management.project_type import ProjectType, ProjectTypeSerializer
|
||||
from api.views.mixin import OrganizationPermissionAPI
|
||||
|
||||
|
||||
|
||||
class View(OrganizationMixin, viewsets.ModelViewSet):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
]
|
||||
|
||||
queryset = ProjectType.objects.all()
|
||||
|
||||
serializer_class = ProjectTypeSerializer
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Create a project type',
|
||||
request = ProjectTypeSerializer,
|
||||
responses = {
|
||||
201: OpenApiResponse(
|
||||
response = ProjectTypeSerializer,
|
||||
),
|
||||
}
|
||||
)
|
||||
def create(self, request, *args, **kwargs):
|
||||
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Fetch all project types',
|
||||
methods=["GET"],
|
||||
responses = {
|
||||
200: OpenApiResponse(
|
||||
description='Success',
|
||||
response = ProjectTypeSerializer
|
||||
)
|
||||
}
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Fetch the selected project type',
|
||||
methods=["GET"],
|
||||
responses = {
|
||||
200: OpenApiResponse(
|
||||
description='Success',
|
||||
response = ProjectTypeSerializer
|
||||
)
|
||||
}
|
||||
)
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
|
||||
return super().retrieve(request, *args, **kwargs)
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
|
||||
if self.detail:
|
||||
return ProjectType._meta.verbose_name
|
||||
|
||||
return ProjectType._meta.verbose_name_plural
|
116
app/api/views/project_management/projects.py
Normal file
116
app/api/views/project_management/projects.py
Normal file
@ -0,0 +1,116 @@
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from drf_spectacular.utils import extend_schema, OpenApiResponse, OpenApiRequest, PolymorphicProxySerializer
|
||||
|
||||
from rest_framework import generics, viewsets
|
||||
from rest_framework.response import Response
|
||||
|
||||
from access.mixin import OrganizationMixin
|
||||
|
||||
from api.serializers.project_management.projects import ProjectSerializer, ProjectImportSerializer
|
||||
from api.views.mixin import OrganizationPermissionAPI
|
||||
|
||||
from project_management.models.projects import Project
|
||||
|
||||
from settings.models.user_settings import UserSettings
|
||||
|
||||
|
||||
|
||||
class View(OrganizationMixin, viewsets.ModelViewSet):
|
||||
|
||||
filterset_fields = [
|
||||
'external_system',
|
||||
'external_ref',
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
'name',
|
||||
'description',
|
||||
]
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
]
|
||||
|
||||
queryset = Project.objects.all()
|
||||
|
||||
# serializer_class = ProjectSerializer
|
||||
|
||||
def get_serializer_class(self):
|
||||
|
||||
if self.has_organization_permission(
|
||||
organization = UserSettings.objects.get(user = self.request.user).default_organization,
|
||||
permissions_required = ['project_management.import_project']
|
||||
) or self.request.user.is_superuser:
|
||||
|
||||
return ProjectImportSerializer
|
||||
|
||||
return ProjectSerializer
|
||||
|
||||
@extend_schema(
|
||||
summary = 'Create a project',
|
||||
description = """**Note:** Users whom lack permssion `import_project`,
|
||||
will be unable to add, edit and view fields: `created`, `external_ref`, `external_system`,
|
||||
and `is_deleted`.
|
||||
""",
|
||||
methods=["POST"],
|
||||
request = ProjectImportSerializer,
|
||||
responses = {
|
||||
201: OpenApiResponse(description='project created', response=ProjectImportSerializer),
|
||||
403: OpenApiResponse(description='User is missing create permissions'),
|
||||
}
|
||||
)
|
||||
def create(self, request, *args, **kwargs):
|
||||
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Fetch projects',
|
||||
description = """**Note:** Users whom lack permssion `import_project`,
|
||||
will be unable to add, edit and view fields: `created`, `external_ref`, `external_system`,
|
||||
and `is_deleted`.
|
||||
""",
|
||||
methods=["GET"],
|
||||
responses = {
|
||||
200: OpenApiResponse(description='projects', response=ProjectImportSerializer)
|
||||
}
|
||||
)
|
||||
def list(self, request):
|
||||
|
||||
return super().list(request)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='Fetch the selected project',
|
||||
description = """**Note:** Users whom lack permssion `import_project`,
|
||||
will be unable to add, edit and view fields: `created`, `external_ref`, `external_system`,
|
||||
and `is_deleted`.
|
||||
""",
|
||||
methods=["GET"],
|
||||
responses = {
|
||||
200: OpenApiResponse(description='projects', response=ProjectImportSerializer)
|
||||
}
|
||||
)
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
|
||||
return super().retrieve(request, *args, **kwargs)
|
||||
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
if self.request.user.is_superuser:
|
||||
|
||||
return self.queryset.filter()
|
||||
|
||||
else:
|
||||
|
||||
return self.queryset.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True))
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
if self.detail:
|
||||
return "Project"
|
||||
|
||||
return 'Projects'
|
51
app/api/views/settings/index.py
Normal file
51
app/api/views/settings/index.py
Normal file
@ -0,0 +1,51 @@
|
||||
from django.contrib.auth.models import Permission
|
||||
|
||||
from drf_spectacular.utils import extend_schema, OpenApiResponse
|
||||
|
||||
from rest_framework import views
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from core.http.common import Http
|
||||
|
||||
|
||||
|
||||
class View(views.APIView):
|
||||
|
||||
permission_classes = [
|
||||
IsAuthenticated,
|
||||
]
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary = "Settings Index Page",
|
||||
description = """This endpoint provides the available settings as available via the API.
|
||||
""",
|
||||
|
||||
methods=["GET"],
|
||||
parameters = None,
|
||||
tags = ['settings',],
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Inventory upload successful'),
|
||||
401: OpenApiResponse(description='User Not logged in'),
|
||||
500: OpenApiResponse(description='Exception occured. View server logs for the Stack Trace'),
|
||||
}
|
||||
)
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
status = Http.Status.OK
|
||||
|
||||
response_data: dict = {
|
||||
"permissions": reverse('API:_settings_permissions', request=request),
|
||||
"project_state": reverse('API:_api_project_state-list', request=request),
|
||||
"project_type": reverse('API:_api_project_type-list', request=request),
|
||||
"ticket_categories": reverse('API:_api_ticket_category-list', request=request),
|
||||
"ticket_comment_categories": reverse('API:_api_ticket_comment_category-list', request=request)
|
||||
}
|
||||
|
||||
return Response(data=response_data,status=status)
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
return "Settings"
|
67
app/api/views/settings/permissions.py
Normal file
67
app/api/views/settings/permissions.py
Normal file
@ -0,0 +1,67 @@
|
||||
from drf_spectacular.utils import extend_schema, OpenApiResponse
|
||||
|
||||
from rest_framework import views
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from access.functions import permissions
|
||||
|
||||
from core.http.common import Http
|
||||
|
||||
|
||||
|
||||
class View(views.APIView):
|
||||
|
||||
permission_classes = [
|
||||
IsAuthenticated,
|
||||
]
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary = "Fetch available permissions",
|
||||
description = """This endpoint provides a list of permissions that are available within
|
||||
Centurion ERP. The format of each permission is `<app name>.<permission>_<model>`.
|
||||
|
||||
This endpoint is available to **all** authenticated users.
|
||||
""",
|
||||
|
||||
methods=["GET"],
|
||||
parameters = None,
|
||||
tags = ['settings',],
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Inventory upload successful'),
|
||||
401: OpenApiResponse(description='User Not logged in'),
|
||||
500: OpenApiResponse(description='Exception occured. View server logs for the Stack Trace'),
|
||||
}
|
||||
)
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
status = Http.Status.OK
|
||||
|
||||
response_data: list = []
|
||||
|
||||
try:
|
||||
|
||||
for permission in permissions.permission_queryset():
|
||||
|
||||
response_data += [ str(f"{permission.content_type.app_label}.{permission.codename}") ]
|
||||
|
||||
except PermissionDenied as e:
|
||||
|
||||
status = Http.Status.FORBIDDEN
|
||||
response_data = ''
|
||||
|
||||
|
||||
except Exception as e:
|
||||
|
||||
print(f'An error occured{e}')
|
||||
|
||||
status = Http.Status.SERVER_ERROR
|
||||
response_data = 'Unknown Server Error occured'
|
||||
|
||||
|
||||
return Response(data=response_data,status=status)
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
return "Permissions"
|
@ -108,15 +108,19 @@ INSTALLED_APPS = [
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'rest_framework_json_api',
|
||||
'django_filters',
|
||||
'social_django',
|
||||
'django_celery_results',
|
||||
'core.apps.CoreConfig',
|
||||
'access.apps.AccessConfig',
|
||||
'itam.apps.ItamConfig',
|
||||
'itim.apps.ItimConfig',
|
||||
'assistance.apps.AssistanceConfig',
|
||||
'settings.apps.SettingsConfig',
|
||||
'drf_spectacular',
|
||||
'drf_spectacular_sidecar',
|
||||
'config_management.apps.ConfigManagementConfig',
|
||||
'project_management.apps.ProjectManagementConfig',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@ -255,7 +259,9 @@ if API_ENABLED:
|
||||
# ),
|
||||
'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata',
|
||||
'DEFAULT_FILTER_BACKENDS': (
|
||||
'rest_framework_json_api.filters.QueryParameterValidationFilter',
|
||||
# 'rest_framework_json_api.filters.QueryParameterValidationFilter',
|
||||
'rest_framework.filters.SearchFilter',
|
||||
'rest_framework_json_api.django_filters.DjangoFilterBackend',
|
||||
'rest_framework_json_api.filters.OrderingFilter',
|
||||
'rest_framework_json_api.django_filters.DjangoFilterBackend',
|
||||
'rest_framework.filters.SearchFilter',
|
||||
@ -355,12 +361,6 @@ if DEBUG:
|
||||
"127.0.0.1",
|
||||
]
|
||||
|
||||
# Apps Under Development
|
||||
INSTALLED_APPS += [
|
||||
'information.apps.InformationConfig',
|
||||
'project_management.apps.ProjectManagementConfig',
|
||||
]
|
||||
|
||||
|
||||
if SSO_ENABLED:
|
||||
|
||||
|
@ -135,7 +135,14 @@ class ModelPermissionsAdd:
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
|
||||
|
||||
if self.app_namespace:
|
||||
|
||||
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
|
||||
|
||||
else:
|
||||
|
||||
url = reverse(self.url_name_add, kwargs=self.url_add_kwargs)
|
||||
|
||||
|
||||
response = client.put(url, data=self.add_data)
|
||||
@ -150,7 +157,14 @@ class ModelPermissionsAdd:
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
|
||||
|
||||
if self.app_namespace:
|
||||
|
||||
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
|
||||
|
||||
else:
|
||||
|
||||
url = reverse(self.url_name_add, kwargs=self.url_add_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.no_permissions_user)
|
||||
@ -167,7 +181,14 @@ class ModelPermissionsAdd:
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
|
||||
|
||||
if self.app_namespace:
|
||||
|
||||
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
|
||||
|
||||
else:
|
||||
|
||||
url = reverse(self.url_name_add, kwargs=self.url_add_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.different_organization_user)
|
||||
@ -183,7 +204,14 @@ class ModelPermissionsAdd:
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
|
||||
|
||||
if self.app_namespace:
|
||||
|
||||
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
|
||||
|
||||
else:
|
||||
|
||||
url = reverse(self.url_name_add, kwargs=self.url_add_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.view_user)
|
||||
@ -195,11 +223,18 @@ class ModelPermissionsAdd:
|
||||
def test_model_add_has_permission(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
Attempt to add as user with no permission
|
||||
Attempt to add as user with permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
|
||||
|
||||
if self.app_namespace:
|
||||
|
||||
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
|
||||
|
||||
else:
|
||||
|
||||
url = reverse(self.url_name_add, kwargs=self.url_add_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.add_user)
|
||||
|
@ -1,6 +1,8 @@
|
||||
import importlib
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
|
||||
from access.models import TenancyObject
|
||||
from access.tests.abstract.tenancy_object import TenancyObject as TenancyObjectTestCases
|
||||
|
||||
@ -40,6 +42,40 @@ class TenancyModel(
|
||||
""" Model to test """
|
||||
|
||||
|
||||
def test_field_exists_verbose_name_plural(self):
|
||||
"""Test for existance of field in `<model>.Meta`
|
||||
|
||||
Field is required for `templates/detail.html.js`
|
||||
|
||||
Attribute `verbose_name_plural` must be defined in `Meta` class.
|
||||
"""
|
||||
|
||||
assert 'verbose_name_plural' in self.model._meta.original_attrs
|
||||
|
||||
|
||||
def test_field_not_empty_verbose_name_plural(self):
|
||||
"""Test field `<model>.Meta` is not empty
|
||||
|
||||
Field is required for `templates/detail.html.js`
|
||||
|
||||
Attribute `verbose_name_plural` must be defined in `Meta` class.
|
||||
"""
|
||||
|
||||
assert self.model._meta.original_attrs['verbose_name_plural'] is not None
|
||||
|
||||
|
||||
def test_field_type_verbose_name_plural(self):
|
||||
"""Test field `<model>.Meta` is not empty
|
||||
|
||||
Field is required for `templates/detail.html.js`
|
||||
|
||||
Attribute `verbose_name_plural` must be of type str.
|
||||
"""
|
||||
|
||||
assert type(self.model._meta.original_attrs['verbose_name_plural']) is str
|
||||
|
||||
|
||||
|
||||
|
||||
class ModelAdd(
|
||||
AddView
|
||||
|
@ -134,6 +134,34 @@ class AddView:
|
||||
assert type(viewclass.template_name) is str
|
||||
|
||||
|
||||
def test_view_add_function_get_initial_exists(self):
|
||||
"""Ensure that get_initial exists
|
||||
|
||||
Field `get_initial` must be defined as the base class is used for setup.
|
||||
"""
|
||||
|
||||
module = __import__(self.add_module, fromlist=[self.add_view])
|
||||
|
||||
view_class = getattr(module, 'Add')
|
||||
|
||||
assert hasattr(view_class, 'get_initial')
|
||||
|
||||
|
||||
def test_view_add_function_get_initial_callable(self):
|
||||
"""Ensure that get_initial is a function
|
||||
|
||||
Field `get_initial` must be callable as it's used for setup.
|
||||
"""
|
||||
|
||||
module = __import__(self.add_module, fromlist=[self.add_view])
|
||||
|
||||
view_class = getattr(module, 'Add')
|
||||
|
||||
func = getattr(view_class, 'get_initial')
|
||||
|
||||
assert callable(func)
|
||||
|
||||
|
||||
|
||||
class ChangeView:
|
||||
""" Testing of Display view """
|
||||
@ -524,6 +552,9 @@ class IndexView:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class AllViews(
|
||||
AddView,
|
||||
ChangeView,
|
||||
@ -563,3 +594,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
|
||||
"""
|
||||
|
@ -24,7 +24,7 @@ from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
|
||||
|
||||
from .views import home
|
||||
|
||||
from core.views import history
|
||||
from core.views import history, related_ticket, ticket_linked_item
|
||||
|
||||
from settings.views import user_settings
|
||||
|
||||
@ -42,12 +42,19 @@ 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'),
|
||||
re_path(r'^static/(?P<path>.*)$', serve,{'document_root': settings.STATIC_ROOT}),
|
||||
|
||||
|
||||
path('ticket/<str:ticket_type>/<int:ticket_id>/relate/add', related_ticket.Add.as_view(), name="_ticket_related_add"),
|
||||
|
||||
path('ticket/<str:ticket_type>/<int:ticket_id>/linked_item/add', ticket_linked_item.Add.as_view(), name="_ticket_linked_item_add"),
|
||||
|
||||
]
|
||||
|
||||
|
||||
@ -72,15 +79,13 @@ 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")),
|
||||
]
|
||||
|
||||
# must be after above
|
||||
urlpatterns += [
|
||||
|
||||
path("project_management/", include("project_management.urls")),
|
||||
|
||||
path("settings/", include("settings.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'
|
147
app/assistance/forms/knowledge_base.py
Normal file
147
app/assistance/forms/knowledge_base.py
Normal file
@ -0,0 +1,147 @@
|
||||
|
||||
from django import forms
|
||||
from django.urls import reverse
|
||||
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
|
||||
|
||||
|
||||
|
||||
class DetailForm(KnowledgeBaseForm):
|
||||
|
||||
tabs: dict = {
|
||||
"details": {
|
||||
"name": "Details",
|
||||
"slug": "details",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "double",
|
||||
"left": [
|
||||
'title',
|
||||
'category',
|
||||
'responsible_user',
|
||||
'organization',
|
||||
'is_global',
|
||||
'c_created',
|
||||
'c_modified',
|
||||
],
|
||||
"right": [
|
||||
'release_date',
|
||||
'expiry_date',
|
||||
'target_user',
|
||||
'target_team',
|
||||
]
|
||||
},
|
||||
{
|
||||
"layout": "single",
|
||||
"name": "Summary",
|
||||
"fields": [
|
||||
'summary',
|
||||
],
|
||||
"markdown": [
|
||||
'summary',
|
||||
]
|
||||
},
|
||||
{
|
||||
"layout": "single",
|
||||
"name": "Content",
|
||||
"fields": [
|
||||
'content',
|
||||
],
|
||||
"markdown": [
|
||||
'content',
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"notes": {
|
||||
"name": "Notes",
|
||||
"slug": "notes",
|
||||
"sections": []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
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('Assistance:_knowledge_base_change', args=(self.instance.pk,))
|
||||
})
|
||||
|
||||
self.url_index_view = reverse('Assistance:Knowledge Base')
|
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
|
40
app/assistance/templates/assistance/kb_article.html.j2
Normal file
40
app/assistance/templates/assistance/kb_article.html.j2
Normal file
@ -0,0 +1,40 @@
|
||||
{% extends 'detail.html.j2' %}
|
||||
|
||||
{% load json %}
|
||||
{% load markdown %}
|
||||
|
||||
|
||||
{% block tabs %}
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<div id="details" class="content-tab">
|
||||
|
||||
{% include 'content/section.html.j2' with tab=form.tabs.details %}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% if perms.assistance.change_knowledgebase %}
|
||||
<div id="notes" class="content-tab">
|
||||
|
||||
{% include 'content/section.html.j2' with tab=form.tabs.notes %}
|
||||
|
||||
{{ 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'
|
27
app/assistance/urls.py
Normal file
27
app/assistance/urls.py
Normal file
@ -0,0 +1,27 @@
|
||||
from django.urls import path
|
||||
|
||||
from assistance.views import knowledge_base
|
||||
|
||||
from core.views import ticket, ticket_comment
|
||||
|
||||
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"),
|
||||
|
||||
path('ticket/request', ticket.Index.as_view(), kwargs={'ticket_type': 'request'}, name="Requests"),
|
||||
path('ticket/<str:ticket_type>/add', ticket.Add.as_view(), name="_ticket_request_add"),
|
||||
path('ticket/<str:ticket_type>/<int:pk>/edit', ticket.Change.as_view(), name="_ticket_request_change"),
|
||||
path('ticket/<str:ticket_type>/<int:pk>/delete', ticket.Delete.as_view(), name="_ticket_request_delete"),
|
||||
path('ticket/<str:ticket_type>/<int:pk>', ticket.View.as_view(), name="_ticket_request_view"),
|
||||
|
||||
path('ticket/<str:ticket_type>/<int:ticket_id>/comment/add', ticket_comment.Add.as_view(), name="_ticket_comment_request_add"),
|
||||
path('ticket/<str:ticket_type>/<int:ticket_id>/comment/<int:pk>/edit', ticket_comment.Change.as_view(), name="_ticket_comment_request_change"),
|
||||
path('ticket/<str:ticket_type>/<int:ticket_id>/comment/<int:parent_id>/add', ticket_comment.Add.as_view(), name="_ticket_comment_request_reply_add"),
|
||||
|
||||
]
|
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 DetailForm, 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 = DetailForm
|
||||
|
||||
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')
|
@ -1,4 +1,7 @@
|
||||
from django.db.models import Q
|
||||
from django import forms
|
||||
from django.urls import reverse
|
||||
|
||||
from app import settings
|
||||
|
||||
from config_management.models.groups import ConfigGroups
|
||||
|
||||
@ -32,3 +35,94 @@ class ConfigGroupForm(CommonModelForm):
|
||||
).exclude(
|
||||
id=int(kwargs['instance'].id)
|
||||
)
|
||||
|
||||
|
||||
|
||||
class DetailForm(ConfigGroupForm):
|
||||
|
||||
tabs: dict = {
|
||||
"details": {
|
||||
"name": "Details",
|
||||
"slug": "details",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "double",
|
||||
"left": [
|
||||
'name',
|
||||
'parent',
|
||||
'is_global',
|
||||
'organization',
|
||||
'c_created',
|
||||
'c_modified',
|
||||
],
|
||||
"right": [
|
||||
'model_notes',
|
||||
]
|
||||
},
|
||||
{
|
||||
"layout": "single",
|
||||
"fields": [
|
||||
'config',
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"child_groups": {
|
||||
"name": "Child Groups",
|
||||
"slug": "child_groups",
|
||||
"sections": []
|
||||
},
|
||||
"hosts": {
|
||||
"name": "Hosts",
|
||||
"slug": "hosts",
|
||||
"sections": []
|
||||
},
|
||||
"software": {
|
||||
"name": "Software",
|
||||
"slug": "software",
|
||||
"sections": []
|
||||
},
|
||||
"configuration": {
|
||||
"name": "Configuration",
|
||||
"slug": "configuration",
|
||||
"sections": []
|
||||
},
|
||||
"tickets": {
|
||||
"name": "Tickets",
|
||||
"slug": "tickets",
|
||||
"sections": []
|
||||
},
|
||||
"notes": {
|
||||
"name": "Notes",
|
||||
"slug": "notes",
|
||||
"sections": []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
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('Config Management:_group_change', args=(self.instance.pk,))
|
||||
})
|
||||
|
||||
self.url_index_view = reverse('Config Management:Groups')
|
||||
|
||||
|
@ -0,0 +1,21 @@
|
||||
# Generated by Django 5.0.7 on 2024-08-17 08:05
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config_management', '0002_configgrouphosts_configgroupsoftware'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='configgroups',
|
||||
options={'verbose_name_plural': 'Config Groups'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='configgroupsoftware',
|
||||
options={'ordering': ['-action', 'software'], 'verbose_name_plural': 'Config Group Softwares'},
|
||||
),
|
||||
]
|
@ -35,6 +35,12 @@ class GroupsCommonFields(TenancyObject, models.Model):
|
||||
|
||||
class ConfigGroups(GroupsCommonFields, SaveHistory):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name_plural = 'Config Groups'
|
||||
|
||||
|
||||
reserved_config_keys: list = [
|
||||
'software'
|
||||
]
|
||||
@ -195,6 +201,12 @@ class ConfigGroups(GroupsCommonFields, SaveHistory):
|
||||
# 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)
|
||||
|
||||
|
||||
@ -258,6 +270,8 @@ class ConfigGroupSoftware(GroupsCommonFields, SaveHistory):
|
||||
'software'
|
||||
]
|
||||
|
||||
verbose_name_plural = 'Config Group Softwares'
|
||||
|
||||
|
||||
config_group = models.ForeignKey(
|
||||
ConfigGroups,
|
||||
|
@ -1,47 +1,208 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
{% extends 'detail.html.j2' %}
|
||||
|
||||
{% block content %}
|
||||
{% load json %}
|
||||
{% load markdown %}
|
||||
|
||||
<script>
|
||||
|
||||
function openCity(evt, cityName) {
|
||||
var i, tabcontent, tablinks;
|
||||
{% block tabs %}
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
tabcontent = document.getElementsByClassName("tabcontent");
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].style.display = "none";
|
||||
}
|
||||
<div id="details" class="content-tab">
|
||||
|
||||
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>
|
||||
|
||||
<div class="tab">
|
||||
<button
|
||||
onclick="window.location='{% if group.parent %}{% url 'Config Management:_group_view' pk=group.parent.id %}{% else %}{% url 'Config Management:Groups' %}{% endif %}';"
|
||||
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 {% if group.parent %}Parent{% else %}Groups{% endif %}</button>
|
||||
|
||||
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
|
||||
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Children')">Child Groups</button>
|
||||
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Hosts')">Hosts</button>
|
||||
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Software')">Software</button>
|
||||
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Configuration')">Configuration</button>
|
||||
<button class="tablinks" onclick="openCity(event, 'Notes')">Notes</button>
|
||||
{% include 'content/section.html.j2' with tab=form.tabs.details %}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div id="child_groups" class="content-tab">
|
||||
|
||||
{% include 'content/section.html.j2' with tab=form.tabs.child_groups %}
|
||||
|
||||
<input type="button" value="Add Child Group" onclick="window.location='{% url 'Config Management:_group_add_child' group.id %}';">
|
||||
|
||||
<table class="data">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Sub-Groups</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
{% if child_groups %}
|
||||
{% for group in child_groups %}
|
||||
<tr>
|
||||
<td><a href="{% url 'Config Management:_group_view' pk=group.id %}">{{ group.name }}</a></td>
|
||||
<td>{{ group.count_children }}</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4">Nothing Found</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="hosts" class="content-tab">
|
||||
|
||||
{% include 'content/section.html.j2' with tab=form.tabs.hosts %}
|
||||
|
||||
<input type="button" value="Add Host" onclick="window.location='{% url 'Config Management:_group_add_host' group.id %}';">
|
||||
|
||||
<table class="data">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Organization</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
{% if config_group_hosts %}
|
||||
{% for host in config_group_hosts %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ITAM:_device_view' pk=host.host.id %}">{{ host.host }}</a></td>
|
||||
<td>{{ host.host.organization }}</td>
|
||||
<td><a href="{% url 'Config Management:_group_delete_host' group_id=group.id pk=host.id %}">Delete</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="3">Nothing Found</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="software" class="content-tab">
|
||||
|
||||
{% include 'content/section.html.j2' with tab=form.tabs.software %}
|
||||
|
||||
<input type="button" value="Add Software Action" onclick="window.location='{% url 'Config Management:_group_software_add' model_pk %}';">
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<th>Name</th>
|
||||
<th>Category</th>
|
||||
<th>Action</th>
|
||||
<th>Desired Version</th>
|
||||
<th> </th>
|
||||
</thead>
|
||||
{% if softwares %}
|
||||
{% for software in softwares %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ITAM:_software_view' pk=software.software_id %}">{{ software.software }}</a></td>
|
||||
<td>{{ software.software.category }}</td>
|
||||
<td>
|
||||
{% url 'Config Management:_group_software_change' group_id=group.id pk=software.id as icon_link %}
|
||||
{% if software.get_action_display == 'Install' %}
|
||||
{% include 'icons/success_text.html.j2' with icon_text=software.get_action_display icon_link=icon_link %}
|
||||
{% elif software.get_action_display == 'Remove'%}
|
||||
{% include 'icons/cross_text.html.j2' with icon_text=software.get_action_display %}
|
||||
{% else %}
|
||||
{% include 'icons/add_link.html.j2' with icon_text='Add' %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if software.version %}
|
||||
{{ software.version }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<td colspan="5">Nothing Found</td>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div id="configuration" class="content-tab">
|
||||
|
||||
{% include 'content/section.html.j2' with tab=form.tabs.configuration %}
|
||||
|
||||
<div>
|
||||
<textarea cols="90" rows="30" readonly>{{ config }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="tickets" class="content-tab">
|
||||
|
||||
{% include 'content/section.html.j2' with tab=form.tabs.tickets %}
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th> </th>
|
||||
</thead>
|
||||
{% if tickets %}
|
||||
{% for ticket in tickets %}
|
||||
<tr>
|
||||
<td>{% concat_strings "#" ticket.ticket.id as ticket_ref %}{{ ticket_ref | markdown | safe}}</td>
|
||||
<td>{% include 'core/ticket/badge_ticket_status.html.j2' with ticket_status_text=ticket.ticket.get_status_display ticket_status=ticket.ticket.get_status_display|ticket_status %}</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="3">No related tickets exist</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div id="notes" class="content-tab">
|
||||
|
||||
{% include 'content/section.html.j2' with tab=form.tabs.notes %}
|
||||
|
||||
{{ 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>
|
||||
|
||||
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{% block contents %}
|
||||
|
||||
|
||||
<form method="post">
|
||||
<div id="Details" class="tabcontent">
|
||||
<h3>Details</h3>
|
||||
@ -60,28 +221,6 @@
|
||||
<div id="Children" class="tabcontent">
|
||||
<h3>Child Groups</h3>
|
||||
|
||||
<input type="button" value="Add Child Group" onclick="window.location='{% url 'Config Management:_group_add_child' group.id %}';">
|
||||
|
||||
<table class="data">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Sub-Groups</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
{% if child_groups %}
|
||||
{% for group in child_groups %}
|
||||
<tr>
|
||||
<td><a href="{% url 'Config Management:_group_view' pk=group.id %}">{{ group.name }}</a></td>
|
||||
<td>{{ group.count_children }}</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4">Nothing Found</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
@ -90,28 +229,6 @@
|
||||
Hosts
|
||||
</h3>
|
||||
|
||||
<input type="button" value="Add Host" onclick="window.location='{% url 'Config Management:_group_add_host' group.id %}';">
|
||||
|
||||
<table class="data">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Organization</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
{% if config_group_hosts %}
|
||||
{% for host in config_group_hosts %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ITAM:_device_view' pk=host.host.id %}">{{ host.host }}</a></td>
|
||||
<td>{{ host.host.organization }}</td>
|
||||
<td><a href="{% url 'Config Management:_group_delete_host' group_id=group.id pk=host.id %}">Delete</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="3">Nothing Found</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
@ -120,52 +237,11 @@
|
||||
Software
|
||||
</h3>
|
||||
|
||||
<input type="button" value="Add Software Action" onclick="window.location='{% url 'Config Management:_group_software_add' model_pk %}';">
|
||||
<table>
|
||||
<thead>
|
||||
<th>Name</th>
|
||||
<th>Category</th>
|
||||
<th>Action</th>
|
||||
<th>Desired Version</th>
|
||||
<th> </th>
|
||||
</thead>
|
||||
{% if softwares %}
|
||||
{% for software in softwares %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ITAM:_software_view' pk=software.software_id %}">{{ software.software }}</a></td>
|
||||
<td>{{ software.software.category }}</td>
|
||||
<td>
|
||||
{% url 'Config Management:_group_software_change' group_id=group.id pk=software.id as icon_link %}
|
||||
{% if software.get_action_display == 'Install' %}
|
||||
{% include 'icons/success_text.html.j2' with icon_text=software.get_action_display icon_link=icon_link %}
|
||||
{% elif software.get_action_display == 'Remove'%}
|
||||
{% include 'icons/cross_text.html.j2' with icon_text=software.get_action_display %}
|
||||
{% else %}
|
||||
{% include 'icons/add_link.html.j2' with icon_text='Add' %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if software.version %}
|
||||
{{ software.version }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<td colspan="5">Nothing Found</td>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="Configuration" class="tabcontent">
|
||||
<h3>Configuration</h3>
|
||||
<div>
|
||||
<textarea cols="90" rows="30" readonly>{{ config }}</textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="Notes" class="tabcontent">
|
||||
|
@ -27,7 +27,7 @@ class ConfigGroupPermissions(TestCase, ModelPermissions):
|
||||
|
||||
url_name_add = '_group_add'
|
||||
|
||||
url_name_change = '_group_view'
|
||||
url_name_change = '_group_change'
|
||||
|
||||
url_name_delete = '_group_delete'
|
||||
|
||||
|
@ -14,16 +14,16 @@ class ConfigManagementViews(
|
||||
):
|
||||
|
||||
add_module = 'config_management.views.groups.groups'
|
||||
add_view = 'GroupAdd'
|
||||
add_view = 'Add'
|
||||
|
||||
change_module = add_module
|
||||
change_view = 'GroupView'
|
||||
change_view = 'View'
|
||||
|
||||
delete_module = add_module
|
||||
delete_view = 'GroupDelete'
|
||||
delete_view = 'Delete'
|
||||
|
||||
display_module = add_module
|
||||
display_view = 'GroupView'
|
||||
display_view = 'View'
|
||||
|
||||
index_module = add_module
|
||||
index_view = 'GroupIndexView'
|
||||
index_view = 'Index'
|
||||
|
@ -16,13 +16,13 @@ class ConfigGroupsSoftwareViews(
|
||||
):
|
||||
|
||||
add_module = 'config_management.views.groups.software'
|
||||
add_view = 'GroupSoftwareAdd'
|
||||
add_view = 'Add'
|
||||
|
||||
change_module = add_module
|
||||
change_view = 'GroupSoftwareChange'
|
||||
change_view = 'Change'
|
||||
|
||||
delete_module = add_module
|
||||
delete_view = 'GroupSoftwareDelete'
|
||||
delete_view = 'Delete'
|
||||
|
||||
# display_module = add_module
|
||||
# display_view = 'GroupView'
|
||||
|
@ -1,21 +1,25 @@
|
||||
from django.urls import path
|
||||
|
||||
from config_management.views.groups.groups import GroupIndexView, GroupAdd, GroupDelete, GroupView, GroupHostAdd, GroupHostDelete
|
||||
from config_management.views.groups.software import GroupSoftwareAdd, GroupSoftwareChange, GroupSoftwareDelete
|
||||
from config_management.views.groups import groups
|
||||
from config_management.views.groups.groups import GroupHostAdd, GroupHostDelete
|
||||
|
||||
from config_management.views.groups import software
|
||||
# from config_management.views.groups.software import GroupSoftwareAdd, GroupSoftwareChange, GroupSoftwareDelete
|
||||
|
||||
app_name = "Config Management"
|
||||
|
||||
urlpatterns = [
|
||||
path('group', GroupIndexView.as_view(), name='Groups'),
|
||||
path('group/add', GroupAdd.as_view(), name='_group_add'),
|
||||
path('group/<int:pk>', GroupView.as_view(), name='_group_view'),
|
||||
path('group', groups.Index.as_view(), name='Groups'),
|
||||
path('group/add', groups.Add.as_view(), name='_group_add'),
|
||||
path('group/<int:pk>', groups.View.as_view(), name='_group_view'),
|
||||
path('group/<int:pk>/edit', groups.Change.as_view(), name='_group_change'),
|
||||
|
||||
path('group/<int:pk>/child', GroupAdd.as_view(), name='_group_add_child'),
|
||||
path('group/<int:pk>/delete', GroupDelete.as_view(), name='_group_delete'),
|
||||
path('group/<int:pk>/child', groups.Add.as_view(), name='_group_add_child'),
|
||||
path('group/<int:pk>/delete', groups.Delete.as_view(), name='_group_delete'),
|
||||
|
||||
path("group/<int:pk>/software/add", GroupSoftwareAdd.as_view(), name="_group_software_add"),
|
||||
path("group/<int:group_id>/software/<int:pk>", GroupSoftwareChange.as_view(), name="_group_software_change"),
|
||||
path("group/<int:group_id>/software/<int:pk>/delete", GroupSoftwareDelete.as_view(), name="_group_software_delete"),
|
||||
path("group/<int:pk>/software/add", software.Add.as_view(), name="_group_software_add"),
|
||||
path("group/<int:group_id>/software/<int:pk>", software.Change.as_view(), name="_group_software_change"),
|
||||
path("group/<int:group_id>/software/<int:pk>/delete", software.Delete.as_view(), name="_group_software_delete"),
|
||||
|
||||
path('group/<int:pk>/host', GroupHostAdd.as_view(), name='_group_add_host'),
|
||||
path('group/<int:group_id>/host/<int:pk>/delete', GroupHostDelete.as_view(), name='_group_delete_host'),
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user