Compare commits
130 Commits
Author | SHA1 | Date | |
---|---|---|---|
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.1.0
|
||||
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 }}
|
||||
|
@ -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
|
||||
|
362
CHANGELOG.md
362
CHANGELOG.md
@ -1,65 +1,211 @@
|
||||
## 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 +216,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 +235,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 +274,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 +318,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 +340,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 +398,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 +415,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 +444,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 +458,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 +522,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 +544,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 +552,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 +606,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 +628,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 +679,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 +691,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 +729,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 +780,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 +796,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)
|
||||
|
@ -30,6 +30,9 @@ python3 manage.py createsuperuser
|
||||
# If model changes
|
||||
python3 manage.py makemigrations --noinput
|
||||
|
||||
# To update code highlight run
|
||||
pygmentize -S default -f html -a .codehilite > project-static/code.css
|
||||
|
||||
```
|
||||
|
||||
Updates to python modules will need to be captured with SCM. This can be done by running `pip freeze > requirements.txt` from the running virtual environment.
|
||||
|
@ -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()
|
||||
|
45
app/access/functions/permissions.py
Normal file
45
app/access/functions/permissions.py
Normal file
@ -0,0 +1,45 @@
|
||||
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',
|
||||
'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
|
||||
)
|
@ -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):
|
||||
@ -196,9 +196,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 +238,10 @@ class Team(Group, TenancyObject):
|
||||
return [permission_list, self.permissions.all()]
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.team_name
|
||||
|
||||
|
||||
|
||||
class TeamUsers(SaveHistory):
|
||||
|
||||
@ -318,3 +319,6 @@ class TeamUsers(SaveHistory):
|
||||
|
||||
return self.team
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username
|
||||
|
||||
|
371
app/access/tests/unit/organization/test_organizaiton_api.py
Normal file
371
app/access/tests/unit/organization/test_organizaiton_api.py
Normal file
@ -0,0 +1,371 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from rest_framework.relations import Hyperlink
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
|
||||
|
||||
class OrganizationAPI(TestCase):
|
||||
|
||||
model = Organization
|
||||
|
||||
app_namespace = 'API'
|
||||
|
||||
url_name = '_api_organization'
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
. create an organization that is different to item
|
||||
2. Create a device
|
||||
3. create teams with each permission: view, add, change, delete
|
||||
4. create a user per team
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
|
||||
self.item = organization
|
||||
|
||||
self.url_view_kwargs = {'pk': self.item.id}
|
||||
|
||||
self.url_kwargs = {'pk': self.item.id}
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
view_team = Team.objects.create(
|
||||
team_name = 'view_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
view_team.permissions.set([view_permissions])
|
||||
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = view_team,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.view_user)
|
||||
response = client.get(url)
|
||||
|
||||
self.api_data = response.data
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_name(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
name field must exist
|
||||
"""
|
||||
|
||||
assert 'name' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_name(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
name field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['name']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_teams(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams field must exist
|
||||
"""
|
||||
|
||||
assert 'teams' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_teams(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams field must be list
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams']) is list
|
||||
|
||||
|
||||
def test_api_field_exists_url(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
url field must exist
|
||||
"""
|
||||
|
||||
assert 'url' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_url(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
url field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['url']) is Hyperlink
|
||||
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_teams_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data['teams'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_teams_team_name(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.team_name field must exist
|
||||
"""
|
||||
|
||||
assert 'team_name' in self.api_data['teams'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_team_name(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.team_name field must be string
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['team_name']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions field must exist
|
||||
"""
|
||||
|
||||
assert 'permissions' in self.api_data['teams'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions field must be list
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions']) is list
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions_url(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions_url field must exist
|
||||
"""
|
||||
|
||||
assert 'permissions_url' in self.api_data['teams'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions_url(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions_url field must be url
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions_url']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_teams_url(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.url field must exist
|
||||
"""
|
||||
|
||||
assert 'url' in self.api_data['teams'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_url(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.url field must be url
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['url']) is str
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions.id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data['teams'][0]['permissions'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions.id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions'][0]['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions_name(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions.name field must exist
|
||||
"""
|
||||
|
||||
assert 'name' in self.api_data['teams'][0]['permissions'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions_name(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions.name field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions'][0]['name']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions_codename(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions.codename field must exist
|
||||
"""
|
||||
|
||||
assert 'codename' in self.api_data['teams'][0]['permissions'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions_codename(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions.codename field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions'][0]['codename']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions_content_type(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions.content_type field must exist
|
||||
"""
|
||||
|
||||
assert 'content_type' in self.api_data['teams'][0]['permissions'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions_content_type(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions.content_type field must be dict
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions'][0]['content_type']) is dict
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions_content_type_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions.content_type.id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data['teams'][0]['permissions'][0]['content_type']
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions_content_type_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions.content_type.id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions'][0]['content_type']['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions_content_type_app_label(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions.content_type.app_label field must exist
|
||||
"""
|
||||
|
||||
assert 'app_label' in self.api_data['teams'][0]['permissions'][0]['content_type']
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions_content_type_app_label(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions.content_type.app_label field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions'][0]['content_type']['app_label']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions_content_type_model(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions.content_type.model field must exist
|
||||
"""
|
||||
|
||||
assert 'model' in self.api_data['teams'][0]['permissions'][0]['content_type']
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions_content_type_model(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions.content_type.model field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions'][0]['content_type']['model']) is str
|
313
app/access/tests/unit/team/test_team_api.py
Normal file
313
app/access/tests/unit/team/test_team_api.py
Normal file
@ -0,0 +1,313 @@
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from rest_framework.relations import Hyperlink
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
# from api.tests.abstract.api_permissions import APIPermissions
|
||||
|
||||
|
||||
|
||||
class TeamAPI(TestCase):
|
||||
|
||||
model = Team
|
||||
|
||||
app_namespace = 'API'
|
||||
|
||||
url_name = '_api_team'
|
||||
|
||||
# url_list = '_api_organization_teams'
|
||||
|
||||
# change_data = {'name': 'device'}
|
||||
|
||||
# delete_data = {'device': 'device'}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
. create an organization that is different to item
|
||||
2. Create a team
|
||||
3. create teams with each permission: view, add, change, delete
|
||||
4. create a user per team
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization=organization,
|
||||
team_name = 'teamone',
|
||||
model_notes = 'random note'
|
||||
)
|
||||
|
||||
|
||||
self.url_kwargs = {'organization_id': self.organization.id}
|
||||
|
||||
self.url_view_kwargs = {'organization_id': self.organization.id, 'group_ptr_id': self.item.id}
|
||||
|
||||
self.add_data = {'team_name': 'team_post'}
|
||||
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
# view_team = Team.objects.create(
|
||||
# team_name = 'view_team',
|
||||
# organization = organization,
|
||||
# )
|
||||
|
||||
self.item.permissions.set([view_permissions])
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = self.item,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.view_user)
|
||||
response = client.get(url)
|
||||
|
||||
self.api_data = response.data
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_team_name(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
team_name field must exist
|
||||
"""
|
||||
|
||||
assert 'team_name' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_name(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
team_name field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['team_name']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_model_notes(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
model_notes field must exist
|
||||
"""
|
||||
|
||||
assert 'model_notes' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_model_notes(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
model_notes field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['model_notes']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_url(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
url field must exist
|
||||
"""
|
||||
|
||||
assert 'url' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_url(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
url field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['url']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_permissions(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
permissions field must exist
|
||||
"""
|
||||
|
||||
assert 'permissions' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_permissions(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
url field must be list
|
||||
"""
|
||||
|
||||
assert type(self.api_data['permissions']) is list
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_permissions_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
permissions.id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data['permissions'][0]
|
||||
|
||||
|
||||
def test_api_field_type_permissions_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
permissions.id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['permissions'][0]['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_permissions_name(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
permissions.name field must exist
|
||||
"""
|
||||
|
||||
assert 'name' in self.api_data['permissions'][0]
|
||||
|
||||
|
||||
def test_api_field_type_permissions_name(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
permissions.name field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['permissions'][0]['name']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_permissions_codename(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
permissions.codename field must exist
|
||||
"""
|
||||
|
||||
assert 'codename' in self.api_data['permissions'][0]
|
||||
|
||||
|
||||
def test_api_field_type_permissions_codename(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
permissions.codename field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['permissions'][0]['codename']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_permissions_content_type(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
permissions.content_type field must exist
|
||||
"""
|
||||
|
||||
assert 'content_type' in self.api_data['permissions'][0]
|
||||
|
||||
|
||||
def test_api_field_type_permissions_content_type(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
permissions.content_type field must be dict
|
||||
"""
|
||||
|
||||
assert type(self.api_data['permissions'][0]['content_type']) is dict
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_permissions_content_type_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
permissions.content_type.id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data['permissions'][0]['content_type']
|
||||
|
||||
|
||||
def test_api_field_type_permissions_content_type_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
permissions.content_type.id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['permissions'][0]['content_type']['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_permissions_content_type_app_label(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
permissions.content_type.app_label field must exist
|
||||
"""
|
||||
|
||||
assert 'app_label' in self.api_data['permissions'][0]['content_type']
|
||||
|
||||
|
||||
def test_api_field_type_permissions_content_type_app_label(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
permissions.content_type.app_label field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['permissions'][0]['content_type']['app_label']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_permissions_content_type_model(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
permissions.content_type.model field must exist
|
||||
"""
|
||||
|
||||
assert 'model' in self.api_data['permissions'][0]['content_type']
|
||||
|
||||
|
||||
def test_api_field_type_permissions_content_type_model(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
permissions.content_type.model field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['permissions'][0]['content_type']['model']) is str
|
@ -5,6 +5,8 @@ import string
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.db.models import Field
|
||||
from django.forms import ValidationError
|
||||
|
||||
from access.fields import *
|
||||
from access.models import TenancyObject
|
||||
@ -14,6 +16,37 @@ from access.models import TenancyObject
|
||||
class AuthToken(models.Model):
|
||||
|
||||
|
||||
def validate_note_no_token(self, note, token):
|
||||
""" Ensure plaintext token cant be saved to notes field.
|
||||
|
||||
called from app.settings.views.user_settings.TokenAdd.form_valid()
|
||||
|
||||
Args:
|
||||
note (Field): _Note field_
|
||||
token (Field): _Token field_
|
||||
|
||||
Raises:
|
||||
ValidationError: _Validation failed_
|
||||
"""
|
||||
|
||||
validation: bool = True
|
||||
|
||||
|
||||
if str(note) == str(token):
|
||||
|
||||
validation = False
|
||||
|
||||
|
||||
if str(token)[:9] in str(note): # Allow user to use up to 8 chars so they can reference it.
|
||||
|
||||
validation = False
|
||||
|
||||
if not validation:
|
||||
|
||||
raise ValidationError('Token can not be placed in the notes field.')
|
||||
|
||||
|
||||
|
||||
id = models.AutoField(
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
|
@ -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',
|
||||
|
@ -5,6 +5,9 @@ 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 .views.itam import software, config as itam_config
|
||||
from .views.itam.device import DeviceViewSet
|
||||
from .views.itam import inventory
|
||||
@ -36,6 +39,9 @@ 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("settings", settings.View.as_view(), name='_settings'),
|
||||
path("settings/permissions", permissions.View.as_view(), name='_settings_permissions'),
|
||||
|
||||
]
|
||||
|
||||
urlpatterns = format_suffix_patterns(urlpatterns)
|
||||
|
@ -1,15 +1,20 @@
|
||||
# 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"
|
||||
@ -29,6 +34,7 @@ class Index(viewsets.ViewSet):
|
||||
"devices": reverse("API:device-list", request=request),
|
||||
"config_groups": reverse("API:_api_config_groups", request=request),
|
||||
"organizations": reverse("API:_api_orgs", request=request),
|
||||
"settings": reverse('API:_settings', request=request),
|
||||
"software": reverse("API:software-list", request=request),
|
||||
}
|
||||
)
|
||||
|
@ -3,6 +3,8 @@ from django.shortcuts import get_object_or_404
|
||||
|
||||
from rest_framework import generics, viewsets
|
||||
|
||||
from access.mixin import OrganizationMixin
|
||||
|
||||
from api.serializers.itam.software import SoftwareSerializer
|
||||
from api.views.mixin import OrganizationPermissionAPI
|
||||
|
||||
@ -10,7 +12,7 @@ from itam.models.software import Software
|
||||
|
||||
|
||||
|
||||
class SoftwareViewSet(viewsets.ModelViewSet):
|
||||
class SoftwareViewSet(OrganizationMixin, viewsets.ModelViewSet):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
|
47
app/api/views/settings/index.py
Normal file
47
app/api/views/settings/index.py
Normal file
@ -0,0 +1,47 @@
|
||||
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)
|
||||
}
|
||||
|
||||
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"
|
@ -113,6 +113,8 @@ INSTALLED_APPS = [
|
||||
'core.apps.CoreConfig',
|
||||
'access.apps.AccessConfig',
|
||||
'itam.apps.ItamConfig',
|
||||
'itim.apps.ItimConfig',
|
||||
'assistance.apps.AssistanceConfig',
|
||||
'settings.apps.SettingsConfig',
|
||||
'drf_spectacular',
|
||||
'drf_spectacular_sidecar',
|
||||
@ -357,7 +359,6 @@ if DEBUG:
|
||||
|
||||
# Apps Under Development
|
||||
INSTALLED_APPS += [
|
||||
'information.apps.InformationConfig',
|
||||
'project_management.apps.ProjectManagementConfig',
|
||||
]
|
||||
|
||||
|
@ -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
|
||||
|
@ -563,3 +563,33 @@ class AllViews(
|
||||
index_view: str = None
|
||||
""" Index Class name to test """
|
||||
|
||||
|
||||
@pytest.mark.skip(reason='write test')
|
||||
def test_view_index_attribute_missing_permission_required(self):
|
||||
""" Attribute missing Test
|
||||
|
||||
Ensure that `permission_required` attribute is not defined within the view.
|
||||
|
||||
this can be done by mocking the inherited class with the `permission_required` attribute
|
||||
set to a value that if it changed would be considered defined in the created view.
|
||||
|
||||
## Why?
|
||||
|
||||
This attribute can be dynamically added based of of the view name along with attributes
|
||||
`model._meta.model_name` and `str(__class__.__name__).lower()`.
|
||||
|
||||
Additional test:
|
||||
- ensure that the attribute does get automagically created.
|
||||
- ensure that the classes name is one of add, change, delete, display or index.
|
||||
"""
|
||||
|
||||
|
||||
@pytest.mark.skip(reason='write test')
|
||||
def test_view_index_attribute_missing_template_name(self):
|
||||
""" Attribute missing Test
|
||||
|
||||
Ensure that `template_name` attribute is not defined within the view if the value
|
||||
is `form.html.j2`
|
||||
|
||||
this valuse is already defined in the base form
|
||||
"""
|
||||
|
@ -42,7 +42,9 @@ urlpatterns = [
|
||||
path("account/", include("django.contrib.auth.urls")),
|
||||
|
||||
path("organization/", include("access.urls")),
|
||||
path("assistance/", include("assistance.urls")),
|
||||
path("itam/", include("itam.urls")),
|
||||
path("itim/", include("itim.urls")),
|
||||
path("config_management/", include("config_management.urls")),
|
||||
|
||||
path("history/<str:model_name>/<int:model_pk>", history.View.as_view(), name='_history'),
|
||||
@ -72,9 +74,6 @@ if settings.DEBUG:
|
||||
urlpatterns += [
|
||||
|
||||
path("__debug__/", include("debug_toolbar.urls"), name='_debug'),
|
||||
# Apps Under Development
|
||||
path("itim/", include("itim.urls")),
|
||||
path("information/", include("information.urls")),
|
||||
path("project_management/", include("project_management.urls")),
|
||||
]
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class InformationConfig(AppConfig):
|
||||
class AssistanceConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'information'
|
||||
name = 'assistance'
|
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'
|
15
app/assistance/urls.py
Normal file
15
app/assistance/urls.py
Normal file
@ -0,0 +1,15 @@
|
||||
from django.urls import path
|
||||
|
||||
from assistance.views import knowledge_base
|
||||
|
||||
app_name = "Assistance"
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
path("information", knowledge_base.Index.as_view(), name="Knowledge Base"),
|
||||
path("information/add", knowledge_base.Add.as_view(), name="_knowledge_base_add"),
|
||||
path("information/<int:pk>/edit", knowledge_base.Change.as_view(), name="_knowledge_base_change"),
|
||||
path("information/<int:pk>/delete", knowledge_base.Delete.as_view(), name="_knowledge_base_delete"),
|
||||
path("information/<int:pk>", knowledge_base.View.as_view(), name="_knowledge_base_view"),
|
||||
|
||||
]
|
0
app/assistance/views/__init__.py
Normal file
0
app/assistance/views/__init__.py
Normal file
215
app/assistance/views/knowledge_base.py
Normal file
215
app/assistance/views/knowledge_base.py
Normal file
@ -0,0 +1,215 @@
|
||||
from datetime import datetime
|
||||
|
||||
from django.contrib.auth import decorators as auth_decorator
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from access.models import TeamUsers
|
||||
|
||||
from assistance.forms.knowledge_base import 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,89 @@ 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": []
|
||||
},
|
||||
"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,180 @@
|
||||
{% 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="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 +193,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 +201,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 +209,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'
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
from django.urls import path
|
||||
|
||||
from config_management.views.groups.groups import GroupIndexView, GroupAdd, GroupDelete, GroupView, GroupHostAdd, GroupHostDelete
|
||||
from config_management.views.groups.groups import GroupIndexView, GroupAdd, GroupChange, GroupDelete, GroupView, GroupHostAdd, GroupHostDelete
|
||||
from config_management.views.groups.software import GroupSoftwareAdd, GroupSoftwareChange, GroupSoftwareDelete
|
||||
|
||||
app_name = "Config Management"
|
||||
@ -9,6 +9,7 @@ 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/<int:pk>/edit', GroupChange.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'),
|
||||
|
@ -13,7 +13,7 @@ from itam.models.device import Device
|
||||
from settings.models.user_settings import UserSettings
|
||||
|
||||
from config_management.forms.group_hosts import ConfigGroupHostsForm
|
||||
from config_management.forms.group.group import ConfigGroupForm
|
||||
from config_management.forms.group.group import ConfigGroupForm, DetailForm
|
||||
from config_management.models.groups import ConfigGroups, ConfigGroupHosts, ConfigGroupSoftware
|
||||
|
||||
|
||||
@ -102,7 +102,7 @@ class GroupAdd(AddView):
|
||||
|
||||
|
||||
|
||||
class GroupView(ChangeView):
|
||||
class GroupChange(ChangeView):
|
||||
|
||||
context_object_name = "group"
|
||||
|
||||
@ -111,10 +111,39 @@ class GroupView(ChangeView):
|
||||
model = ConfigGroups
|
||||
|
||||
permission_required = [
|
||||
'config_management.view_configgroups',
|
||||
'config_management.change_configgroups',
|
||||
]
|
||||
|
||||
template_name = 'form.html.j2'
|
||||
|
||||
|
||||
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('Config Management:_group_view', args=(self.kwargs['pk'],))
|
||||
|
||||
|
||||
|
||||
class GroupView(ChangeView):
|
||||
|
||||
context_object_name = "group"
|
||||
|
||||
form_class = DetailForm
|
||||
|
||||
model = ConfigGroups
|
||||
|
||||
permission_required = [
|
||||
'config_management.view_configgroups',
|
||||
]
|
||||
|
||||
template_name = 'config_management/group.html.j2'
|
||||
|
||||
|
||||
|
@ -98,3 +98,7 @@ class CommonModelForm(forms.ModelForm):
|
||||
|
|
||||
Q(manager=user)
|
||||
)
|
||||
|
||||
if hasattr(self, 'instance'):
|
||||
|
||||
self.model_name_plural = self.instance._meta.verbose_name_plural
|
||||
|
@ -1,4 +1,7 @@
|
||||
from django import forms
|
||||
from django.urls import reverse
|
||||
|
||||
from app import settings
|
||||
|
||||
from core.forms.common import CommonModelForm
|
||||
from core.models.manufacturer import Manufacturer
|
||||
@ -24,3 +27,64 @@ class ManufacturerForm(
|
||||
]
|
||||
|
||||
model = Manufacturer
|
||||
|
||||
|
||||
|
||||
class DetailForm(ManufacturerForm):
|
||||
|
||||
tabs: dict = {
|
||||
"details": {
|
||||
"name": "Details",
|
||||
"slug": "details",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "double",
|
||||
"left": [
|
||||
'name',
|
||||
'slug',
|
||||
'organization',
|
||||
'is_global',
|
||||
'c_created',
|
||||
'c_modified',
|
||||
],
|
||||
"right": [
|
||||
'model_notes',
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
# "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('Settings:_manufacturer_change', args=(self.instance.pk,))
|
||||
})
|
||||
|
||||
self.url_index_view = reverse('Settings:_manufacturers')
|
||||
|
||||
|
||||
|
@ -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 = [
|
||||
('core', '0002_notes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='manufacturer',
|
||||
options={'ordering': ['name'], 'verbose_name_plural': 'Manufacturers'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='notes',
|
||||
options={'ordering': ['-created'], 'verbose_name_plural': 'Notes'},
|
||||
),
|
||||
]
|
20
app/core/migrations/0004_notes_service.py
Normal file
20
app/core/migrations/0004_notes_service.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.0.7 on 2024-08-17 08:05
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0003_alter_manufacturer_options_alter_notes_options'),
|
||||
('itim', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='notes',
|
||||
name='service',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itim.service'),
|
||||
),
|
||||
]
|
@ -41,6 +41,18 @@ class SaveHistory(models.Model):
|
||||
|
||||
value = bool(before[entry])
|
||||
|
||||
elif (
|
||||
"{" in str(after[entry])
|
||||
and
|
||||
"}" in str(after[entry])
|
||||
) or (
|
||||
"[" in str(after[entry])
|
||||
and
|
||||
"]" in str(after[entry])
|
||||
):
|
||||
|
||||
value = str(after[entry]).replace("'", '\"')
|
||||
|
||||
else:
|
||||
|
||||
value = str(before[entry])
|
||||
@ -62,6 +74,18 @@ class SaveHistory(models.Model):
|
||||
|
||||
value = bool(after[entry])
|
||||
|
||||
elif (
|
||||
"{" in str(after[entry])
|
||||
and
|
||||
"}" in str(after[entry])
|
||||
) or (
|
||||
"[" in str(after[entry])
|
||||
and
|
||||
"]" in str(after[entry])
|
||||
):
|
||||
|
||||
value = str(after[entry]).replace("'", '\"')
|
||||
|
||||
else:
|
||||
|
||||
value = str(after[entry])
|
||||
|
@ -34,6 +34,9 @@ class Manufacturer(TenancyObject, ManufacturerCommonFields, SaveHistory):
|
||||
'name'
|
||||
]
|
||||
|
||||
verbose_name_plural = 'Manufacturers'
|
||||
|
||||
|
||||
name = models.CharField(
|
||||
blank = False,
|
||||
max_length = 50,
|
||||
|
@ -10,6 +10,9 @@ from itam.models.device import Device
|
||||
from itam.models.software import Software
|
||||
from itam.models.operating_system import OperatingSystem
|
||||
|
||||
from itim.models.services import Service
|
||||
|
||||
|
||||
|
||||
class NotesCommonFields(TenancyObject, models.Model):
|
||||
|
||||
@ -43,6 +46,9 @@ class Notes(NotesCommonFields):
|
||||
'-created'
|
||||
]
|
||||
|
||||
verbose_name_plural = 'Notes'
|
||||
|
||||
|
||||
|
||||
note = models.TextField(
|
||||
verbose_name = 'Note',
|
||||
@ -88,6 +94,14 @@ class Notes(NotesCommonFields):
|
||||
blank= True
|
||||
)
|
||||
|
||||
service = models.ForeignKey(
|
||||
Service,
|
||||
on_delete=models.CASCADE,
|
||||
default = None,
|
||||
null = True,
|
||||
blank= True
|
||||
)
|
||||
|
||||
software = models.ForeignKey(
|
||||
Software,
|
||||
on_delete=models.CASCADE,
|
||||
|
34
app/core/templates/core/manufacturer.html.j2
Normal file
34
app/core/templates/core/manufacturer.html.j2
Normal file
@ -0,0 +1,34 @@
|
||||
{% 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>
|
||||
|
||||
|
||||
<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 %}
|
64
app/core/templatetags/choice_ids.py
Normal file
64
app/core/templatetags/choice_ids.py
Normal file
@ -0,0 +1,64 @@
|
||||
from django import template
|
||||
from django.template.defaultfilters import stringfilter
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter()
|
||||
@stringfilter
|
||||
def choice_ids(value):
|
||||
"""Convert choice field value to list
|
||||
|
||||
Provide from `{{ field.field.choices }}` the `field.value` and have it converted to a loop
|
||||
|
||||
Args:
|
||||
value (string): for field that has `field.field.choices`, provide `field.value`
|
||||
|
||||
Returns:
|
||||
list: `field.value` casted to a useable list
|
||||
"""
|
||||
|
||||
if value == 'None':
|
||||
|
||||
return ''
|
||||
|
||||
alist: list = []
|
||||
|
||||
if '[' in value:
|
||||
|
||||
value = str(value).replace('[', '').replace(']', '')
|
||||
|
||||
if ',' in value:
|
||||
|
||||
for item in value.split(','):
|
||||
|
||||
try:
|
||||
|
||||
alist += [ int(item) ]
|
||||
|
||||
except:
|
||||
|
||||
alist += [ str(item) ]
|
||||
|
||||
else:
|
||||
|
||||
try:
|
||||
|
||||
alist += [ int(item) ]
|
||||
|
||||
except:
|
||||
|
||||
alist += [ str(item) ]
|
||||
|
||||
else:
|
||||
|
||||
try:
|
||||
|
||||
alist += [ int(value) ]
|
||||
|
||||
except:
|
||||
|
||||
alist += [ str(value) ]
|
||||
|
||||
return alist
|
@ -14,4 +14,4 @@ def json_pretty(value):
|
||||
|
||||
return str('{}')
|
||||
|
||||
return json.dumps(json.loads(value), indent=4, sort_keys=True)
|
||||
return json.dumps(json.loads(value.replace("'", '"')), indent=4, sort_keys=True)
|
||||
|
@ -9,4 +9,4 @@ register = template.Library()
|
||||
@register.filter()
|
||||
@stringfilter
|
||||
def markdown(value):
|
||||
return md.markdown(value, extensions=['markdown.extensions.fenced_code'])
|
||||
return md.markdown(value, extensions=['markdown.extensions.fenced_code', 'codehilite'])
|
@ -12,7 +12,12 @@ from itam.models.device import Device
|
||||
|
||||
|
||||
class HistoryPermissions:
|
||||
"""Test cases for accessing History """
|
||||
"""Test cases for accessing History
|
||||
|
||||
For this test to function properly you must add the history items model to
|
||||
`app.core.views.history.View.get_object()`. specifically an entry to the switch in the middle
|
||||
of the function.
|
||||
"""
|
||||
|
||||
|
||||
item: object
|
||||
|
@ -26,7 +26,7 @@ class ManufacturerPermissions(TestCase, ModelPermissions):
|
||||
|
||||
url_name_add = '_manufacturer_add'
|
||||
|
||||
url_name_change = '_manufacturer_view'
|
||||
url_name_change = '_manufacturer_change'
|
||||
|
||||
url_name_delete = '_manufacturer_delete'
|
||||
|
||||
|
@ -1,9 +1,12 @@
|
||||
from django.template import Template, Context
|
||||
from django.utils.html import escape
|
||||
from django.views import generic
|
||||
|
||||
from access.mixin import OrganizationPermission
|
||||
|
||||
from core.exceptions import MissingAttribute
|
||||
|
||||
from settings.models.external_link import ExternalLink
|
||||
from settings.models.user_settings import UserSettings
|
||||
|
||||
|
||||
@ -50,6 +53,72 @@ class ChangeView(View, generic.UpdateView):
|
||||
|
||||
template_name:str = 'form.html.j2'
|
||||
|
||||
# ToDo: on migrating all views to seperate display and change views, external_links will not be required in `ChangView`
|
||||
def get_context_data(self, **kwargs):
|
||||
""" Get template context
|
||||
|
||||
For items that have the ability to have external links, this function
|
||||
adds the external link details to the context.
|
||||
|
||||
!!! Danger "Requirement"
|
||||
This function may be overridden with the caveat that this function is still called.
|
||||
by the overriding function. i.e. `super().get_context_data(skwargs)`
|
||||
|
||||
!!! note
|
||||
The adding of `external_links` within this view is scheduled to be removed.
|
||||
|
||||
Returns:
|
||||
(dict): Context for the template to use inclusive of 'external_links'
|
||||
"""
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
external_links_query = None
|
||||
|
||||
if 'tab' in self.request.GET:
|
||||
|
||||
context['open_tab'] = str(self.request.GET.get("tab")).lower()
|
||||
|
||||
else:
|
||||
context['open_tab'] = None
|
||||
|
||||
|
||||
if self.model._meta.model_name == 'cluster':
|
||||
|
||||
external_links_query = ExternalLink.objects.filter(cluster=True)
|
||||
|
||||
elif self.model._meta.model_name == 'device':
|
||||
|
||||
external_links_query = ExternalLink.objects.filter(devices=True)
|
||||
|
||||
elif self.model._meta.model_name == 'software':
|
||||
|
||||
external_links_query = ExternalLink.objects.filter(software=True)
|
||||
|
||||
|
||||
if external_links_query:
|
||||
|
||||
external_links: list = []
|
||||
|
||||
user_context = Context(context)
|
||||
|
||||
for external_link in external_links_query:
|
||||
|
||||
user_string = Template(external_link)
|
||||
external_link_context: dict = {
|
||||
'name': escape(external_link.name),
|
||||
'link': escape(user_string.render(user_context)),
|
||||
}
|
||||
|
||||
if external_link.colour:
|
||||
|
||||
external_link_context.update({'colour': external_link.colour })
|
||||
external_links += [ external_link_context ]
|
||||
|
||||
context['external_links'] = external_links
|
||||
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class DeleteView(OrganizationPermission, generic.DeleteView):
|
||||
@ -64,6 +133,60 @@ class DisplayView(OrganizationPermission, generic.DetailView):
|
||||
template_name:str = 'form.html.j2'
|
||||
|
||||
|
||||
# ToDo: on migrating all views to seperate display and change views, external_links will not be required in `ChangView`
|
||||
def get_context_data(self, **kwargs):
|
||||
""" Get template context
|
||||
|
||||
For items that have the ability to have external links, this function
|
||||
adds the external link details to the context.
|
||||
|
||||
!!! Danger "Requirement"
|
||||
This function may be overridden with the caveat that this function is still called.
|
||||
by the overriding function. i.e. `super().get_context_data(skwargs)`
|
||||
|
||||
Returns:
|
||||
(dict): Context for the template to use inclusive of 'external_links'
|
||||
"""
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
external_links_query = None
|
||||
|
||||
|
||||
if self.model._meta.model_name == 'device':
|
||||
|
||||
external_links_query = ExternalLink.objects.filter(devices=True)
|
||||
|
||||
elif self.model._meta.model_name == 'software':
|
||||
|
||||
external_links_query = ExternalLink.objects.filter(software=True)
|
||||
|
||||
|
||||
if external_links_query:
|
||||
|
||||
external_links: list = []
|
||||
|
||||
user_context = Context(context)
|
||||
|
||||
for external_link in external_links_query:
|
||||
|
||||
user_string = Template(external_link)
|
||||
external_link_context: dict = {
|
||||
'name': escape(external_link.name),
|
||||
'link': escape(user_string.render(user_context)),
|
||||
}
|
||||
|
||||
if external_link.colour:
|
||||
|
||||
external_link_context.update({'colour': external_link.colour })
|
||||
external_links += [ external_link_context ]
|
||||
|
||||
context['external_links'] = external_links
|
||||
|
||||
|
||||
return context
|
||||
|
||||
|
||||
|
||||
class IndexView(View, generic.ListView):
|
||||
|
||||
|
@ -41,10 +41,24 @@ class View(OrganizationPermission, generic.View):
|
||||
|
||||
from config_management.models.groups import ConfigGroups
|
||||
|
||||
from settings.models.external_link import ExternalLink
|
||||
|
||||
if not hasattr(self, 'model'):
|
||||
|
||||
match self.kwargs['model_name']:
|
||||
|
||||
case 'cluster':
|
||||
|
||||
from itim.models.clusters import Cluster
|
||||
|
||||
self.model = Cluster
|
||||
|
||||
case 'clustertype':
|
||||
|
||||
from itim.models.clusters import ClusterType
|
||||
|
||||
self.model = ClusterType
|
||||
|
||||
case 'configgroups':
|
||||
|
||||
self.model = ConfigGroups
|
||||
@ -61,6 +75,22 @@ class View(OrganizationPermission, generic.View):
|
||||
|
||||
self.model = DeviceType
|
||||
|
||||
case 'externallink':
|
||||
|
||||
self.model = ExternalLink
|
||||
|
||||
case 'knowledgebase':
|
||||
|
||||
from assistance.models.knowledge_base import KnowledgeBase
|
||||
|
||||
self.model = KnowledgeBase
|
||||
|
||||
case 'knowledgebasecategory':
|
||||
|
||||
from assistance.models.knowledge_base import KnowledgeBaseCategory
|
||||
|
||||
self.model = KnowledgeBaseCategory
|
||||
|
||||
case 'manufacturer':
|
||||
|
||||
self.model = Manufacturer
|
||||
@ -81,10 +111,22 @@ class View(OrganizationPermission, generic.View):
|
||||
|
||||
self.model = Organization
|
||||
|
||||
case 'port':
|
||||
|
||||
from itim.models.services import Port
|
||||
|
||||
self.model = Port
|
||||
|
||||
case 'team':
|
||||
|
||||
self.model = Team
|
||||
|
||||
case 'service':
|
||||
|
||||
from itim.models.services import Service
|
||||
|
||||
self.model = Service
|
||||
|
||||
case _:
|
||||
raise Exception('Unable to determine history items model')
|
||||
|
||||
|
@ -1,13 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
from .views import knowledge_base, playbooks
|
||||
|
||||
app_name = "Information"
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
path("kb/", knowledge_base.Index.as_view(), name="Knowledge Base"),
|
||||
path("playbook/", playbooks.Index.as_view(), name="Playbooks"),
|
||||
|
||||
]
|
@ -1,31 +0,0 @@
|
||||
import json
|
||||
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import render
|
||||
from django.template import Template, Context
|
||||
from django.views import generic
|
||||
|
||||
from access.mixin import OrganizationPermission
|
||||
|
||||
|
||||
|
||||
class Index(generic.View):
|
||||
|
||||
# permission_required = [
|
||||
# 'itil.view_knowledge_base'
|
||||
# ]
|
||||
|
||||
template_name = 'form.html.j2'
|
||||
|
||||
|
||||
def get(self, request):
|
||||
context = {}
|
||||
|
||||
user_string = Template("{% include 'icons/issue_link.html.j2' with issue=10 %}")
|
||||
user_context = Context(context)
|
||||
context['form'] = user_string.render(user_context)
|
||||
|
||||
|
||||
context['content_title'] = 'Knowledge Base'
|
||||
|
||||
return render(request, self.template_name, context)
|
@ -1,29 +0,0 @@
|
||||
import json
|
||||
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import render
|
||||
from django.template import Template, Context
|
||||
from django.views import generic
|
||||
|
||||
from access.mixin import OrganizationPermission
|
||||
|
||||
|
||||
|
||||
class Index(generic.View):
|
||||
|
||||
# permission_required = [
|
||||
# 'itil.view_playbook'
|
||||
# ]
|
||||
|
||||
template_name = 'form.html.j2'
|
||||
|
||||
def get(self, request):
|
||||
context = {}
|
||||
|
||||
user_string = Template("{% include 'icons/issue_link.html.j2' with issue=11 %}")
|
||||
user_context = Context(context)
|
||||
context['form'] = user_string.render(user_context)
|
||||
|
||||
context['content_title'] = 'Playbooks'
|
||||
|
||||
return render(request, self.template_name, context)
|
@ -1,5 +1,5 @@
|
||||
from django import forms
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
|
||||
from app import settings
|
||||
|
||||
@ -22,15 +22,78 @@ class DeviceForm(CommonModelForm):
|
||||
'uuid',
|
||||
'device_type',
|
||||
'organization',
|
||||
'is_virtual',
|
||||
'model_notes',
|
||||
'config',
|
||||
]
|
||||
|
||||
|
||||
|
||||
class DetailForm(DeviceForm):
|
||||
|
||||
tabs: dict = {
|
||||
"details": {
|
||||
"name": "Details",
|
||||
"slug": "details",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "double",
|
||||
"left": [
|
||||
'name',
|
||||
'device_model',
|
||||
'serial_number',
|
||||
'uuid',
|
||||
'device_type',
|
||||
'organization',
|
||||
'c_created',
|
||||
'c_modified',
|
||||
'lastinventory',
|
||||
],
|
||||
"right": [
|
||||
'model_notes',
|
||||
'is_virtual',
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"software": {
|
||||
"name": "Software",
|
||||
"slug": "software",
|
||||
"sections": []
|
||||
},
|
||||
"notes": {
|
||||
"name": "Notes",
|
||||
"slug": "notes",
|
||||
"sections": []
|
||||
},
|
||||
"config_management": {
|
||||
"name": "Config Management",
|
||||
"slug": "config_management",
|
||||
"sections": []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if hasattr(kwargs['instance'], 'inventorydate'):
|
||||
self.fields['lastinventory'] = forms.DateTimeField(
|
||||
|
||||
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.fields['lastinventory'] = forms.DateTimeField(
|
||||
label="Last Inventory Date",
|
||||
input_formats=settings.DATETIME_FORMAT,
|
||||
initial=kwargs['instance'].inventorydate,
|
||||
@ -38,5 +101,8 @@ class DeviceForm(CommonModelForm):
|
||||
required=False,
|
||||
)
|
||||
|
||||
# for key in self.fields.keys():
|
||||
# self.fields[key].widget.attrs['disabled'] = True
|
||||
self.tabs['details'].update({
|
||||
"edit_url": reverse('ITAM:_device_change', args=(self.instance.pk,))
|
||||
})
|
||||
|
||||
self.url_index_view = reverse('ITAM:Devices')
|
||||
|
@ -1,4 +1,7 @@
|
||||
from django.db.models import Q
|
||||
from django import forms
|
||||
from django.urls import reverse
|
||||
|
||||
from app import settings
|
||||
|
||||
from core.forms.common import CommonModelForm
|
||||
|
||||
@ -27,3 +30,62 @@ class DeviceModelForm(
|
||||
]
|
||||
|
||||
model = DeviceModel
|
||||
|
||||
|
||||
|
||||
class DetailForm(DeviceModelForm):
|
||||
|
||||
tabs: dict = {
|
||||
"details": {
|
||||
"name": "Details",
|
||||
"slug": "details",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "double",
|
||||
"left": [
|
||||
'name',
|
||||
'slug',
|
||||
'manufacturer',
|
||||
'organization',
|
||||
'is_global',
|
||||
'c_created',
|
||||
'c_modified',
|
||||
],
|
||||
"right": [
|
||||
'model_notes',
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
# "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('Settings:_device_model_change', args=(self.instance.pk,))
|
||||
})
|
||||
|
||||
self.url_index_view = reverse('Settings:_device_models')
|
||||
|
@ -1,4 +1,7 @@
|
||||
from django.db.models import Q
|
||||
from django import forms
|
||||
from django.urls import reverse
|
||||
|
||||
from app import settings
|
||||
|
||||
from core.forms.common import CommonModelForm
|
||||
|
||||
@ -26,3 +29,61 @@ class DeviceTypeForm(
|
||||
]
|
||||
|
||||
model = DeviceType
|
||||
|
||||
|
||||
|
||||
class DetailForm(DeviceTypeForm):
|
||||
|
||||
tabs: dict = {
|
||||
"details": {
|
||||
"name": "Details",
|
||||
"slug": "details",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "double",
|
||||
"left": [
|
||||
'name',
|
||||
'slug',
|
||||
'organization',
|
||||
'is_global',
|
||||
'c_created',
|
||||
'c_modified',
|
||||
],
|
||||
"right": [
|
||||
'model_notes',
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
# "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('Settings:_device_type_change', args=(self.instance.pk,))
|
||||
})
|
||||
|
||||
self.url_index_view = reverse('Settings:_device_types')
|
||||
|
@ -1,5 +1,5 @@
|
||||
from django import forms
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
|
||||
from app import settings
|
||||
|
||||
@ -9,7 +9,7 @@ from itam.models.operating_system import OperatingSystem
|
||||
|
||||
|
||||
|
||||
class OperatingSystemFormCommon(CommonModelForm):
|
||||
class OperatingSystemForm(CommonModelForm):
|
||||
|
||||
class Meta:
|
||||
|
||||
@ -27,27 +27,99 @@ class OperatingSystemFormCommon(CommonModelForm):
|
||||
|
||||
|
||||
|
||||
class Update(OperatingSystemFormCommon):
|
||||
# class Update(OperatingSystemFormCommon):
|
||||
|
||||
|
||||
# def __init__(self, *args, **kwargs):
|
||||
# super().__init__(*args, **kwargs)
|
||||
|
||||
# self.fields['_created'] = forms.DateTimeField(
|
||||
# label="Created",
|
||||
# input_formats=settings.DATETIME_FORMAT,
|
||||
# initial=kwargs['instance'].created,
|
||||
# disabled=True
|
||||
# )
|
||||
|
||||
# self.fields['_modified'] = forms.DateTimeField(
|
||||
# label="Modified",
|
||||
# input_formats=settings.DATETIME_FORMAT,
|
||||
# initial=kwargs['instance'].modified,
|
||||
# disabled=True
|
||||
# )
|
||||
|
||||
|
||||
# if kwargs['instance'].is_global:
|
||||
|
||||
# self.fields['is_global'].widget.attrs['disabled'] = True
|
||||
|
||||
|
||||
class DetailForm(OperatingSystemForm):
|
||||
|
||||
tabs: dict = {
|
||||
"details": {
|
||||
"name": "Details",
|
||||
"slug": "details",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "double",
|
||||
"left": [
|
||||
'name',
|
||||
'publisher',
|
||||
'serial_number',
|
||||
'organization',
|
||||
'c_created',
|
||||
'c_modified',
|
||||
],
|
||||
"right": [
|
||||
'model_notes',
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"versions": {
|
||||
"name": "Versions",
|
||||
"slug": "versions",
|
||||
"sections": []
|
||||
},
|
||||
"licences": {
|
||||
"name": "Licences",
|
||||
"slug": "licences",
|
||||
"sections": []
|
||||
},
|
||||
"installations": {
|
||||
"name": "Installations",
|
||||
"slug": "installations",
|
||||
"sections": []
|
||||
},
|
||||
"notes": {
|
||||
"name": "Notes",
|
||||
"slug": "notes",
|
||||
"sections": []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['_created'] = forms.DateTimeField(
|
||||
label="Created",
|
||||
|
||||
self.fields['c_created'] = forms.DateTimeField(
|
||||
label = 'Created',
|
||||
input_formats=settings.DATETIME_FORMAT,
|
||||
initial=kwargs['instance'].created,
|
||||
disabled=True
|
||||
disabled = True,
|
||||
initial = self.instance.created,
|
||||
)
|
||||
|
||||
self.fields['_modified'] = forms.DateTimeField(
|
||||
label="Modified",
|
||||
self.fields['c_modified'] = forms.DateTimeField(
|
||||
label = 'Modified',
|
||||
input_formats=settings.DATETIME_FORMAT,
|
||||
initial=kwargs['instance'].modified,
|
||||
disabled=True
|
||||
disabled = True,
|
||||
initial = self.instance.modified,
|
||||
)
|
||||
|
||||
self.tabs['details'].update({
|
||||
"edit_url": reverse('ITAM:_operating_system_change', args=(self.instance.pk,))
|
||||
})
|
||||
|
||||
if kwargs['instance'].is_global:
|
||||
|
||||
self.fields['is_global'].widget.attrs['disabled'] = True
|
||||
self.url_index_view = reverse('ITAM:Operating Systems')
|
||||
|
@ -1,4 +1,8 @@
|
||||
from django import forms
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
|
||||
from app import settings
|
||||
|
||||
from core.forms.common import CommonModelForm
|
||||
|
||||
@ -11,22 +15,108 @@ class SoftwareForm(CommonModelForm):
|
||||
class Meta:
|
||||
model = Software
|
||||
fields = [
|
||||
"name",
|
||||
'name',
|
||||
'publisher',
|
||||
'slug',
|
||||
'id',
|
||||
'organization',
|
||||
'is_global',
|
||||
'category',
|
||||
'model_notes',
|
||||
]
|
||||
|
||||
|
||||
class SoftwareChange(SoftwareForm):
|
||||
|
||||
class SoftwareFormUpdate(SoftwareForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if not self.instance.is_global:
|
||||
|
||||
|
||||
self.fields['is_global'] = forms.BooleanField(
|
||||
label = 'Is Global',
|
||||
initial = self.instance.is_global
|
||||
)
|
||||
|
||||
self.fields['organization'] = forms.CharField(
|
||||
label = 'Organization',
|
||||
initial = self.instance.organization
|
||||
)
|
||||
|
||||
|
||||
class DetailForm(SoftwareForm):
|
||||
|
||||
tabs: dict = {
|
||||
"details": {
|
||||
"name": "Details",
|
||||
"slug": "details",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "double",
|
||||
"left": [
|
||||
'name',
|
||||
'publisher',
|
||||
'slug',
|
||||
'organization',
|
||||
'is_global',
|
||||
'category',
|
||||
'c_created',
|
||||
'c_modified',
|
||||
],
|
||||
"right": [
|
||||
'model_notes',
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"versions": {
|
||||
"name": "Versions",
|
||||
"slug": "versions",
|
||||
"sections": []
|
||||
},
|
||||
"licences": {
|
||||
"name": "Licences",
|
||||
"slug": "licences",
|
||||
"sections": []
|
||||
},
|
||||
"notes": {
|
||||
"name": "Notes",
|
||||
"slug": "notes",
|
||||
"sections": []
|
||||
},
|
||||
"installations": {
|
||||
"name": "Installations",
|
||||
"slug": "installations",
|
||||
"sections": []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['is_global'].widget.attrs['disabled'] = True
|
||||
|
||||
self.fields[ 'organization' ] = forms.CharField(
|
||||
label = 'Organization',
|
||||
initial = self.instance.organization
|
||||
)
|
||||
|
||||
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('ITAM:_software_change', args=(self.instance.pk,))
|
||||
})
|
||||
|
||||
self.url_index_view = reverse('ITAM:Software')
|
||||
|
@ -1,4 +1,7 @@
|
||||
from django.db.models import Q
|
||||
from django import forms
|
||||
from django.urls import reverse
|
||||
|
||||
from app import settings
|
||||
|
||||
from core.forms.common import CommonModelForm
|
||||
|
||||
@ -25,3 +28,61 @@ class SoftwareCategoryForm(
|
||||
]
|
||||
|
||||
model = SoftwareCategory
|
||||
|
||||
|
||||
|
||||
class DetailForm(SoftwareCategoryForm):
|
||||
|
||||
tabs: dict = {
|
||||
"details": {
|
||||
"name": "Details",
|
||||
"slug": "details",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "double",
|
||||
"left": [
|
||||
'name',
|
||||
'slug',
|
||||
'organization',
|
||||
'is_global',
|
||||
'c_created',
|
||||
'c_modified',
|
||||
],
|
||||
"right": [
|
||||
'model_notes',
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
# "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('Settings:_software_category_change', args=(self.instance.pk,))
|
||||
})
|
||||
|
||||
self.url_index_view = reverse('Settings:_software_categories')
|
||||
|
19
app/itam/migrations/0002_device_config.py
Normal file
19
app/itam/migrations/0002_device_config.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.0.7 on 2024-07-17 07:17
|
||||
|
||||
import itam.models.device
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('itam', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='config',
|
||||
field=models.JSONField(blank=True, default=None, help_text='Configuration for this device', null=True, validators=[itam.models.device.Device.validate_config_keys_not_reserved], verbose_name='Host Configuration'),
|
||||
),
|
||||
]
|
@ -0,0 +1,69 @@
|
||||
# Generated by Django 5.0.7 on 2024-08-17 08:05
|
||||
|
||||
import itam.models.device
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('itam', '0002_device_config'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='device',
|
||||
options={'verbose_name_plural': 'Devices'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='devicemodel',
|
||||
options={'ordering': ['manufacturer', 'name'], 'verbose_name_plural': 'Device Models'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='deviceoperatingsystem',
|
||||
options={'verbose_name_plural': 'Device Operating Systems'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='devicesoftware',
|
||||
options={'ordering': ['-action', 'software'], 'verbose_name_plural': 'Device Softwares'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='devicetype',
|
||||
options={'verbose_name_plural': 'Device Types'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='operatingsystem',
|
||||
options={'verbose_name_plural': 'Operating Systems'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='operatingsystemversion',
|
||||
options={'verbose_name_plural': 'Operating System Versions'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='software',
|
||||
options={'verbose_name_plural': 'Softwares'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='softwarecategory',
|
||||
options={'verbose_name_plural': 'Software Categories'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='softwareversion',
|
||||
options={'verbose_name_plural': 'Software Versions'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='is_virtual',
|
||||
field=models.BooleanField(blank=True, default=False, help_text='Is this device a virtual machine', verbose_name='Is Virtual'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50, unique=True, validators=[itam.models.device.Device.validate_hostname_format]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='uuid',
|
||||
field=models.CharField(blank=True, default=None, help_text='System GUID/UUID.', max_length=50, null=True, unique=True, validators=[itam.models.device.Device.validate_uuid_format], verbose_name='UUID'),
|
||||
),
|
||||
]
|
@ -1,8 +1,10 @@
|
||||
import json
|
||||
import re
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.db import models
|
||||
from django.forms import ValidationError
|
||||
|
||||
from access.fields import *
|
||||
from access.models import TenancyObject
|
||||
@ -18,9 +20,16 @@ from itam.models.operating_system import OperatingSystemVersion
|
||||
|
||||
from settings.models.app_settings import AppSettings
|
||||
|
||||
|
||||
|
||||
class DeviceType(DeviceCommonFieldsName, SaveHistory):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name_plural = 'Device Types'
|
||||
|
||||
|
||||
def clean(self):
|
||||
|
||||
app_settings = AppSettings.objects.get(owner_organization=None)
|
||||
@ -39,6 +48,54 @@ class DeviceType(DeviceCommonFieldsName, SaveHistory):
|
||||
|
||||
class Device(DeviceCommonFieldsName, SaveHistory):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name_plural = 'Devices'
|
||||
|
||||
|
||||
reserved_config_keys: list = [
|
||||
'software'
|
||||
]
|
||||
|
||||
def validate_config_keys_not_reserved(self):
|
||||
|
||||
value: dict = self
|
||||
|
||||
for invalid_key in Device.reserved_config_keys:
|
||||
|
||||
if invalid_key in value.keys():
|
||||
raise ValidationError(f'json key "{invalid_key}" is a reserved configuration key')
|
||||
|
||||
|
||||
def validate_uuid_format(self):
|
||||
|
||||
pattern = r'[0-9|a-f]{8}\-[0-9|a-f]{4}\-[0-9|a-f]{4}\-[0-9|a-f]{4}\-[0-9|a-f]{12}'
|
||||
|
||||
if not re.match(pattern, str(self)):
|
||||
|
||||
raise ValidationError(f'UUID Must be in {str(pattern)}')
|
||||
|
||||
|
||||
def validate_hostname_format(self):
|
||||
|
||||
pattern = r'^[a-z]{1}[a-z|0-9|\-]+[a-z|0-9]{1}$'
|
||||
|
||||
if not re.match(pattern, str(self).lower()):
|
||||
|
||||
raise ValidationError(
|
||||
'''[RFC1035 2.3.1] A hostname must start with a letter, end with a letter or digit,
|
||||
and have as interior characters only letters, digits, and hyphen.'''
|
||||
)
|
||||
|
||||
|
||||
name = models.CharField(
|
||||
blank = False,
|
||||
max_length = 50,
|
||||
unique = True,
|
||||
validators = [ validate_hostname_format ]
|
||||
)
|
||||
|
||||
serial_number = models.CharField(
|
||||
verbose_name = 'Serial Number',
|
||||
max_length = 50,
|
||||
@ -58,6 +115,7 @@ class Device(DeviceCommonFieldsName, SaveHistory):
|
||||
blank = True,
|
||||
unique = True,
|
||||
help_text = 'System GUID/UUID.',
|
||||
validators = [ validate_uuid_format ]
|
||||
)
|
||||
|
||||
device_model = models.ForeignKey(
|
||||
@ -79,12 +137,30 @@ class Device(DeviceCommonFieldsName, SaveHistory):
|
||||
)
|
||||
|
||||
|
||||
config = models.JSONField(
|
||||
blank = True,
|
||||
default = None,
|
||||
null = True,
|
||||
validators=[ validate_config_keys_not_reserved ],
|
||||
verbose_name = 'Host Configuration',
|
||||
help_text = 'Configuration for this device'
|
||||
)
|
||||
|
||||
inventorydate = models.DateTimeField(
|
||||
verbose_name = 'Last Inventory Date',
|
||||
null = True,
|
||||
blank = True,
|
||||
)
|
||||
|
||||
is_virtual = models.BooleanField(
|
||||
blank = True,
|
||||
default = False,
|
||||
help_text = 'Is this device a virtual machine',
|
||||
null = False,
|
||||
verbose_name = 'Is Virtual',
|
||||
)
|
||||
|
||||
|
||||
def save(
|
||||
self, force_insert=False, force_update=False, using=None, update_fields=None
|
||||
):
|
||||
@ -220,6 +296,25 @@ class Device(DeviceCommonFieldsName, SaveHistory):
|
||||
|
||||
config['software'] = merge_software(group_software, host_software)
|
||||
|
||||
if self.config:
|
||||
|
||||
config.update(self.config)
|
||||
|
||||
from itim.models.services import Service
|
||||
services = Service.objects.filter(
|
||||
device = self.pk
|
||||
)
|
||||
|
||||
for service in services:
|
||||
|
||||
if service.config_variables:
|
||||
|
||||
service_config:dict = {
|
||||
service.config_key_variable: service.config_variables
|
||||
}
|
||||
|
||||
config.update(service_config)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@ -232,6 +327,9 @@ class DeviceSoftware(DeviceCommonFields, SaveHistory):
|
||||
'software'
|
||||
]
|
||||
|
||||
verbose_name_plural = 'Device Softwares'
|
||||
|
||||
|
||||
|
||||
class Actions(models.TextChoices):
|
||||
INSTALL = '1', 'Install'
|
||||
@ -307,6 +405,12 @@ class DeviceSoftware(DeviceCommonFields, SaveHistory):
|
||||
|
||||
class DeviceOperatingSystem(DeviceCommonFields, SaveHistory):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name_plural = 'Device Operating Systems'
|
||||
|
||||
|
||||
device = models.ForeignKey(
|
||||
Device,
|
||||
on_delete = models.CASCADE,
|
||||
|
@ -20,6 +20,9 @@ class DeviceModel(DeviceCommonFieldsName, SaveHistory):
|
||||
'name',
|
||||
]
|
||||
|
||||
verbose_name_plural = 'Device Models'
|
||||
|
||||
|
||||
manufacturer = models.ForeignKey(
|
||||
Manufacturer,
|
||||
on_delete=models.CASCADE,
|
||||
|
@ -42,6 +42,12 @@ class OperatingSystemFieldsName(OperatingSystemCommonFields):
|
||||
|
||||
class OperatingSystem(OperatingSystemFieldsName, SaveHistory):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name_plural = 'Operating Systems'
|
||||
|
||||
|
||||
publisher = models.ForeignKey(
|
||||
Manufacturer,
|
||||
on_delete=models.CASCADE,
|
||||
@ -57,6 +63,12 @@ class OperatingSystem(OperatingSystemFieldsName, SaveHistory):
|
||||
|
||||
class OperatingSystemVersion(OperatingSystemCommonFields, SaveHistory):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name_plural = 'Operating System Versions'
|
||||
|
||||
|
||||
operating_system = models.ForeignKey(
|
||||
OperatingSystem,
|
||||
on_delete=models.CASCADE,
|
||||
|
@ -37,6 +37,12 @@ class SoftwareCommonFields(TenancyObject, models.Model):
|
||||
class SoftwareCategory(SoftwareCommonFields, SaveHistory):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name_plural = 'Software Categories'
|
||||
|
||||
|
||||
|
||||
def clean(self):
|
||||
|
||||
app_settings = AppSettings.objects.get(owner_organization=None)
|
||||
@ -55,6 +61,12 @@ class SoftwareCategory(SoftwareCommonFields, SaveHistory):
|
||||
|
||||
class Software(SoftwareCommonFields, SaveHistory):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name_plural = 'Softwares'
|
||||
|
||||
|
||||
publisher = models.ForeignKey(
|
||||
Manufacturer,
|
||||
on_delete=models.CASCADE,
|
||||
@ -91,6 +103,12 @@ class Software(SoftwareCommonFields, SaveHistory):
|
||||
|
||||
class SoftwareVersion(SoftwareCommonFields, SaveHistory):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name_plural = 'Software Versions'
|
||||
|
||||
|
||||
software = models.ForeignKey(
|
||||
Software,
|
||||
on_delete=models.CASCADE,
|
||||
|
@ -1,199 +1,64 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
{% extends 'detail.html.j2' %}
|
||||
|
||||
{% load json %}
|
||||
{% load markdown %}
|
||||
|
||||
{% block title %}{{ device.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<script>
|
||||
|
||||
function openCity(evt, cityName) {
|
||||
// Declare all variables
|
||||
var i, tabcontent, tablinks;
|
||||
|
||||
// Get all elements with class="tabcontent" and hide them
|
||||
tabcontent = document.getElementsByClassName("tabcontent");
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].style.display = "none";
|
||||
}
|
||||
|
||||
// Get all elements with class="tablinks" and remove the class "active"
|
||||
tablinks = document.getElementsByClassName("tablinks");
|
||||
for (i = 0; i < tablinks.length; i++) {
|
||||
tablinks[i].className = tablinks[i].className.replace(" active", "");
|
||||
}
|
||||
|
||||
// Show the current tab, and add an "active" class to the button that opened the tab
|
||||
document.getElementById(cityName).style.display = "block";
|
||||
evt.currentTarget.className += " active";
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="tab">
|
||||
<button onclick="window.location='{% url 'ITAM:Devices' %}';"
|
||||
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 Devices</button>
|
||||
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
|
||||
<button id="SoftwareOpen" class="tablinks" onclick="openCity(event, 'Software')">Software</button>
|
||||
<button id="NotesOpen" class="tablinks" onclick="openCity(event, 'Notes')">Notes</button>
|
||||
<button id="ConfigManagementOpen" class="tablinks" onclick="openCity(event, 'ConfigManagement')">Config Management</button>
|
||||
<!-- <button class="tablinks" onclick="openCity(event, 'Installations')">Installations</button> -->
|
||||
</div>
|
||||
<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;
|
||||
|
||||
}
|
||||
</style>
|
||||
{% 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 id="Details" class="tabcontent">
|
||||
<h3>
|
||||
Details
|
||||
<span style="font-weight: normal; float: right;">{% include 'icons/issue_link.html.j2' with issue=6 %}</span>
|
||||
</h3>
|
||||
<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.device_model.label }}</label>
|
||||
<span>
|
||||
{% if device.device_model %}
|
||||
{{ device.device_model }}
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.serial_number.label }}</label>
|
||||
<span>
|
||||
{% if form.serial_number.value %}
|
||||
{{ form.serial_number.value }}
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.uuid.label }}</label>
|
||||
<span>
|
||||
{% if form.uuid.value %}
|
||||
{{ form.uuid.value }}
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.device_type.label }}</label>
|
||||
<span>
|
||||
{% if device.device_type %}
|
||||
{{ device.device_type }}
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.organization.label }}</label>
|
||||
<span>{{ device.organization }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.lastinventory.label }}</label>
|
||||
<span>
|
||||
{% if form.lastinventory.value %}
|
||||
{{ form.lastinventory.value }}
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div style="display: inline; width: 40%; margin: 30px; text-align: left;">
|
||||
<div>
|
||||
<label style="font-weight: bold; width: 100%; border-bottom: 1px solid #ccc; display: block; text-align: inherit;">{{ form.model_notes.label }}</label>
|
||||
|
||||
<div style="display: inline-block; text-align: left;">
|
||||
{% if form.model_notes.value %}
|
||||
{{ form.model_notes.value | markdown | safe }}
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<input type="button" value="Edit" onclick="window.location='{% url 'ITAM:_device_change' device.id %}';">
|
||||
<hr />
|
||||
|
||||
<div style="display: block; width: 100%;">
|
||||
<h3>Operating System</h3>
|
||||
<br>
|
||||
{{ operating_system.as_p }}
|
||||
<input type="submit" name="{{operating_system.prefix}}" value="Submit" />
|
||||
<input type="submit" name="{{ operating_system.prefix }}" value="Submit" />
|
||||
</div>
|
||||
|
||||
{% if not tab %}
|
||||
<script>
|
||||
// Get the element with id="defaultOpen" and click on it
|
||||
document.getElementById("defaultOpen").click();
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
<div style="display: block; width: 100%;">
|
||||
<h3>Dependent Services</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Ports</th>
|
||||
</tr>
|
||||
{% if services %}
|
||||
{% for service in services %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ITIM:_service_view' service.pk %}">{{ service }}</a></td>
|
||||
<td>{% for port in service.port.all %}{{ port }} ({{ port.description}}), {% endfor %}</td>
|
||||
</tr>
|
||||
{% endfor%}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="2"> Nothing Found</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="Software" class="tabcontent">
|
||||
<h3>Software</h3>
|
||||
<div style="display: block; width: 100%;">
|
||||
<h3>Device Config</h3>
|
||||
<br>
|
||||
<textarea cols="90" rows="30" readonly>{{ device.config }}</textarea>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div id="software" class="content-tab">
|
||||
|
||||
{% include 'content/section.html.j2' with tab=form.tabs.software %}
|
||||
|
||||
<hr>
|
||||
Installed Software: {{ installed_software }}
|
||||
<input type="button" value="Add Software Action" onclick="window.location='{% url 'ITAM:_device_software_add' device.id %}';">
|
||||
@ -269,19 +134,14 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if tab == 'software' %}
|
||||
<script>
|
||||
// Get the element with id="defaultOpen" and click on it
|
||||
document.getElementById("SoftwareOpen").click();
|
||||
</script>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="Notes" class="tabcontent">
|
||||
<h3>
|
||||
Notes
|
||||
</h3>
|
||||
|
||||
<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">
|
||||
@ -292,17 +152,14 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if tab == 'notes' %}
|
||||
<script>
|
||||
// Get the element with id="defaultOpen" and click on it
|
||||
document.getElementById("NotesOpen").click();
|
||||
</script>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="ConfigManagement" class="tabcontent">
|
||||
<h3>Configuration Management</h3>
|
||||
|
||||
<div id="config_management" class="content-tab">
|
||||
|
||||
{% include 'content/section.html.j2' with tab=form.tabs.config_management %}
|
||||
|
||||
<div>
|
||||
<textarea cols="90" rows="30" readonly>{{ config }}</textarea>
|
||||
</div>
|
||||
@ -329,13 +186,6 @@
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
{% if tab == 'configmanagement' %}
|
||||
<script>
|
||||
// Get the element with id="defaultOpen" and click on it
|
||||
document.getElementById("ConfigManagementOpen").click();
|
||||
</script>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
34
app/itam/templates/itam/device_model.html.j2
Normal file
34
app/itam/templates/itam/device_model.html.j2
Normal file
@ -0,0 +1,34 @@
|
||||
{% 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>
|
||||
|
||||
|
||||
<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 %}
|
36
app/itam/templates/itam/device_type.html.j2
Normal file
36
app/itam/templates/itam/device_type.html.j2
Normal file
@ -0,0 +1,36 @@
|
||||
{% 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>
|
||||
|
||||
|
||||
<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 %}
|
@ -1,118 +1,116 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
{% extends 'detail.html.j2' %}
|
||||
|
||||
{% block title %}{{ operating_system.name }}{% endblock %}
|
||||
{% load json %}
|
||||
{% 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>
|
||||
|
||||
<div class="tab">
|
||||
<button onclick="window.location='{% url 'ITAM:Operating Systems' %}';" 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 Operating Systems</button>
|
||||
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
|
||||
<button class="tablinks" onclick="openCity(event, 'Versions')">Versions</button>
|
||||
<button class="tablinks" onclick="openCity(event, 'Licences')">Licences</button>
|
||||
<button class="tablinks" onclick="openCity(event, 'Notes')">Notes</button>
|
||||
<button class="tablinks" onclick="openCity(event, 'Installations')">Installations</button>
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
<div id="Details" class="tabcontent">
|
||||
<h3>Details</h3>
|
||||
|
||||
{% block tabs %}
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form }}
|
||||
<br>
|
||||
<input type="submit" value="Submit">
|
||||
|
||||
<script>
|
||||
document.getElementById("defaultOpen").click();
|
||||
</script>
|
||||
<div id="details" class="content-tab">
|
||||
|
||||
{% include 'content/section.html.j2' with tab=form.tabs.details %}
|
||||
|
||||
</div>
|
||||
|
||||
<div id="Versions" class="tabcontent">
|
||||
<h3>Versions</h3>
|
||||
<input type="button" value="New Operating System Version" onclick="window.location='{% url 'ITAM:_operating_system_version_add' pk=operating_system.id %}';">
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<th>Version</th>
|
||||
<th>Installations</th>
|
||||
<th>Vulnerable</th>
|
||||
<th> </th>
|
||||
</thead>
|
||||
{% for version in operating_system_versions %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ITAM:_operating_system_version_view' operating_system_id=operating_system.id pk=version.id %}">{{ version.name }}</a></td>
|
||||
<td>{% if version.installs == 0%}-{% else %}{{ version.installs }}{% endif %}</td>
|
||||
<td> </td>
|
||||
<td><a href="{% url 'ITAM:_operating_system_version_delete' operating_system_id=operating_system.id pk=version.id %}">DELETE</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<div id="versions" class="content-tab">
|
||||
|
||||
{% include 'content/section.html.j2' with tab=form.tabs.versions %}
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<th>Version</th>
|
||||
<th>Installations</th>
|
||||
<th>Vulnerable</th>
|
||||
<th> </th>
|
||||
</thead>
|
||||
{% for version in operating_system_versions %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ITAM:_operating_system_version_view' operating_system_id=operating_system.id pk=version.id %}">{{ version.name }}</a></td>
|
||||
<td>{% if version.installs == 0%}-{% else %}{{ version.installs }}{% endif %}</td>
|
||||
<td> </td>
|
||||
<td><a href="{% url 'ITAM:_operating_system_version_delete' operating_system_id=operating_system.id pk=version.id %}">DELETE</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="Licences" class="tabcontent">
|
||||
<h3>Licences</h3>
|
||||
{% include 'icons/issue_link.html.j2' with issue=4 %}
|
||||
<table>
|
||||
<thead>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Available</th>
|
||||
<th> </th>
|
||||
</thead>
|
||||
<tr>
|
||||
<td>GPL-3</td>
|
||||
<td>Open Source</td>
|
||||
<td>1 / 5</td>
|
||||
<th> </th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MIT</td>
|
||||
<td>Open Source</td>
|
||||
<td>Unlimited</td>
|
||||
<th> </th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Windows Device</td>
|
||||
<td>CAL</td>
|
||||
<td>11 / 15</td>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<div id="licences" class="content-tab">
|
||||
|
||||
{% include 'content/section.html.j2' with tab=form.tabs.licences %}
|
||||
|
||||
{% include 'icons/issue_link.html.j2' with issue=4 %}
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Available</th>
|
||||
<th> </th>
|
||||
</thead>
|
||||
<tr>
|
||||
<td>GPL-3</td>
|
||||
<td>Open Source</td>
|
||||
<td>1 / 5</td>
|
||||
<th> </th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MIT</td>
|
||||
<td>Open Source</td>
|
||||
<td>Unlimited</td>
|
||||
<th> </th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Windows Device</td>
|
||||
<td>CAL</td>
|
||||
<td>11 / 15</td>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div id="Notes" class="tabcontent">
|
||||
<h3>
|
||||
Notes
|
||||
</h3>
|
||||
<div id="installations" class="content-tab">
|
||||
|
||||
{% include 'content/section.html.j2' with tab=form.tabs.installations %}
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<th>Device</th>
|
||||
<th>Organization</th>
|
||||
<th>Version</th>
|
||||
<th title="Date Software Installed">Installed</th>
|
||||
<th> </th>
|
||||
</thead>
|
||||
{% for install in installs %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ITAM:_device_view' pk=install.device_id %}">{{ install.device }}</a></td>
|
||||
<td>{{ install.organization }}</td>
|
||||
<td>{{ install.version }}</td>
|
||||
<td>
|
||||
{% if install.installdate %}
|
||||
{{ install.installdate }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</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">
|
||||
@ -123,36 +121,8 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div id="Installations" class="tabcontent">
|
||||
<h3>Installations</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<th>Device</th>
|
||||
<th>Organization</th>
|
||||
<th>Version</th>
|
||||
<th title="Date Software Installed">Installed</th>
|
||||
<th> </th>
|
||||
</thead>
|
||||
{% for install in installs %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ITAM:_device_view' pk=install.device_id %}">{{ install.device }}</a></td>
|
||||
<td>{{ install.organization }}</td>
|
||||
<td>{{ install.version }}</td>
|
||||
<td>
|
||||
{% if install.installdate %}
|
||||
{{ install.installdate }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -1,119 +1,85 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
{% extends 'detail.html.j2' %}
|
||||
|
||||
{% block title %}{{ software.name }}{% endblock %}
|
||||
{% load json %}
|
||||
{% load markdown %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<script>
|
||||
{% block tabs %}
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<div id="details" class="content-tab">
|
||||
|
||||
function openCity(evt, cityName) {
|
||||
// Declare all variables
|
||||
var i, tabcontent, tablinks;
|
||||
|
||||
// Get all elements with class="tabcontent" and hide them
|
||||
tabcontent = document.getElementsByClassName("tabcontent");
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].style.display = "none";
|
||||
}
|
||||
|
||||
// Get all elements with class="tablinks" and remove the class "active"
|
||||
tablinks = document.getElementsByClassName("tablinks");
|
||||
for (i = 0; i < tablinks.length; i++) {
|
||||
tablinks[i].className = tablinks[i].className.replace(" active", "");
|
||||
}
|
||||
|
||||
// Show the current tab, and add an "active" class to the button that opened the tab
|
||||
document.getElementById(cityName).style.display = "block";
|
||||
evt.currentTarget.className += " active";
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="tab">
|
||||
<button onclick="window.location='{% url 'ITAM:Software' %}';" 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 Software</button>
|
||||
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
|
||||
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Versions')">Versions</button>
|
||||
<button class="tablinks" onclick="openCity(event, 'Licences')">Licences</button>
|
||||
<button class="tablinks" onclick="openCity(event, 'Notes')">Notes</button>
|
||||
<button class="tablinks" onclick="openCity(event, 'Installations')">Installations</button>
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
<div id="Details" class="tabcontent">
|
||||
<h3>Details</h3>
|
||||
|
||||
{% csrf_token %}
|
||||
{{ form }}
|
||||
<br>
|
||||
<input type="submit" value="Submit">
|
||||
|
||||
<script>
|
||||
// Get the element with id="defaultOpen" and click on it
|
||||
document.getElementById("defaultOpen").click();
|
||||
</script>
|
||||
{% include 'content/section.html.j2' with tab=form.tabs.details %}
|
||||
|
||||
</div>
|
||||
|
||||
<div id="Versions" class="tabcontent">
|
||||
<h3>Versions</h3>
|
||||
<input type="button" value="New Software Version" onclick="window.location='{% url 'ITAM:_software_version_add' pk=software.id %}';">
|
||||
<table>
|
||||
<thead>
|
||||
<th>Version</th>
|
||||
<th>Installations</th>
|
||||
<th>Vulnerable</th>
|
||||
<th> </th>
|
||||
</thead>
|
||||
{% for version in software_versions %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ITAM:_software_version_view' software_id=software.id pk=version.id %}">{{ version.name }}</a></td>
|
||||
<td>{{ version.installs }}</td>
|
||||
<td>{% include 'icons/issue_link.html.j2' with issue=3 %}</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="Licences" class="tabcontent">
|
||||
<h3>Licences</h3>
|
||||
{% include 'icons/issue_link.html.j2' with issue=4 %}
|
||||
<table>
|
||||
<thead>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Available</th>
|
||||
<th> </th>
|
||||
</thead>
|
||||
<tr>
|
||||
<td>GPL-3</td>
|
||||
<td>Open Source</td>
|
||||
<td>1 / 5</td>
|
||||
<th> </th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MIT</td>
|
||||
<td>Open Source</td>
|
||||
<td>Unlimited</td>
|
||||
<th> </th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Windows Device</td>
|
||||
<td>CAL</td>
|
||||
<td>11 / 15</td>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</table>
|
||||
<div id="versions" class="content-tab">
|
||||
|
||||
{% include 'content/section.html.j2' with tab=form.tabs.versions %}
|
||||
|
||||
<input type="button" value="New Software Version" onclick="window.location='{% url 'ITAM:_software_version_add' pk=software.id %}';">
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<th>Version</th>
|
||||
<th>Installations</th>
|
||||
<th>Vulnerable</th>
|
||||
<th> </th>
|
||||
</thead>
|
||||
{% for version in software_versions %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ITAM:_software_version_view' software_id=software.id pk=version.id %}">{{ version.name }}</a></td>
|
||||
<td>{{ version.installs }}</td>
|
||||
<td>{% include 'icons/issue_link.html.j2' with issue=3 %}</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div id="Notes" class="tabcontent">
|
||||
<h3>
|
||||
Notes
|
||||
</h3>
|
||||
<div id="licences" class="content-tab">
|
||||
|
||||
{% include 'content/section.html.j2' with tab=form.tabs.licences %}
|
||||
|
||||
{% include 'icons/issue_link.html.j2' with issue=4 %}
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Available</th>
|
||||
<th> </th>
|
||||
</thead>
|
||||
<tr>
|
||||
<td>GPL-3</td>
|
||||
<td>Open Source</td>
|
||||
<td>1 / 5</td>
|
||||
<th> </th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MIT</td>
|
||||
<td>Open Source</td>
|
||||
<td>Unlimited</td>
|
||||
<th> </th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Windows Device</td>
|
||||
<td>CAL</td>
|
||||
<td>11 / 15</td>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</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">
|
||||
@ -124,58 +90,61 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div id="Installations" class="tabcontent">
|
||||
<h3>Installations</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<th>Device</th>
|
||||
<th>Organization</th>
|
||||
<th title="Not Set/Install/Remove">Action</th>
|
||||
<th>Installed Version</th>
|
||||
<th title="Date Software Installed">Install Date</th>
|
||||
<th> </th>
|
||||
</thead>
|
||||
{% if device_software %}
|
||||
{% for device in device_software %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ITAM:_device_view' pk=device.device.id %}">{{ device.device }}</a></td>
|
||||
<td>{{ device.organization }}</td>
|
||||
<td>
|
||||
{% if device.get_action_display == 'Install' %}
|
||||
{% include 'icons/success_text.html.j2' with icon_text=device.get_action_display %}
|
||||
{% elif device.get_action_display == 'Remove'%}
|
||||
{% include 'icons/cross_text.html.j2' with icon_text=device.get_action_display %}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if device.installedversion %}
|
||||
{{ device.installedversion }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if device.installed %}
|
||||
{{ device.installed }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6">Nothing Found</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
<div id="installations" class="content-tab">
|
||||
|
||||
{% include 'content/section.html.j2' with tab=form.tabs.installations %}
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<th>Device</th>
|
||||
<th>Organization</th>
|
||||
<th title="Not Set/Install/Remove">Action</th>
|
||||
<th>Installed Version</th>
|
||||
<th title="Date Software Installed">Install Date</th>
|
||||
<th> </th>
|
||||
</thead>
|
||||
{% if device_software %}
|
||||
{% for device in device_software %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ITAM:_device_view' pk=device.device.id %}">{{ device.device }}</a></td>
|
||||
<td>{{ device.organization }}</td>
|
||||
<td>
|
||||
{% if device.get_action_display == 'Install' %}
|
||||
{% include 'icons/success_text.html.j2' with icon_text=device.get_action_display %}
|
||||
{% elif device.get_action_display == 'Remove'%}
|
||||
{% include 'icons/cross_text.html.j2' with icon_text=device.get_action_display %}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if device.installedversion %}
|
||||
{{ device.installedversion }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if device.installed %}
|
||||
{{ device.installed }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6">Nothing Found</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
150
app/itam/templates/itam/software_categories.html.j2
Normal file
150
app/itam/templates/itam/software_categories.html.j2
Normal file
@ -0,0 +1,150 @@
|
||||
{% 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>
|
||||
|
||||
|
||||
<div id="versions" class="content-tab">
|
||||
|
||||
{% include 'content/section.html.j2' with tab=form.tabs.versions %}
|
||||
|
||||
<input type="button" value="New Software Version" onclick="window.location='{% url 'ITAM:_software_version_add' pk=software.id %}';">
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<th>Version</th>
|
||||
<th>Installations</th>
|
||||
<th>Vulnerable</th>
|
||||
<th> </th>
|
||||
</thead>
|
||||
{% for version in software_versions %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ITAM:_software_version_view' software_id=software.id pk=version.id %}">{{ version.name }}</a></td>
|
||||
<td>{{ version.installs }}</td>
|
||||
<td>{% include 'icons/issue_link.html.j2' with issue=3 %}</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div id="licences" class="content-tab">
|
||||
|
||||
{% include 'content/section.html.j2' with tab=form.tabs.licences %}
|
||||
|
||||
{% include 'icons/issue_link.html.j2' with issue=4 %}
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Available</th>
|
||||
<th> </th>
|
||||
</thead>
|
||||
<tr>
|
||||
<td>GPL-3</td>
|
||||
<td>Open Source</td>
|
||||
<td>1 / 5</td>
|
||||
<th> </th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MIT</td>
|
||||
<td>Open Source</td>
|
||||
<td>Unlimited</td>
|
||||
<th> </th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Windows Device</td>
|
||||
<td>CAL</td>
|
||||
<td>11 / 15</td>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</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>
|
||||
|
||||
|
||||
<div id="installations" class="content-tab">
|
||||
|
||||
{% include 'content/section.html.j2' with tab=form.tabs.installations %}
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<th>Device</th>
|
||||
<th>Organization</th>
|
||||
<th title="Not Set/Install/Remove">Action</th>
|
||||
<th>Installed Version</th>
|
||||
<th title="Date Software Installed">Install Date</th>
|
||||
<th> </th>
|
||||
</thead>
|
||||
{% if device_software %}
|
||||
{% for device in device_software %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ITAM:_device_view' pk=device.device.id %}">{{ device.device }}</a></td>
|
||||
<td>{{ device.organization }}</td>
|
||||
<td>
|
||||
{% if device.get_action_display == 'Install' %}
|
||||
{% include 'icons/success_text.html.j2' with icon_text=device.get_action_display %}
|
||||
{% elif device.get_action_display == 'Remove'%}
|
||||
{% include 'icons/cross_text.html.j2' with icon_text=device.get_action_display %}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if device.installedversion %}
|
||||
{{ device.installedversion }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if device.installed %}
|
||||
{{ device.installed }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6">Nothing Found</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
{% endblock %}
|
@ -30,7 +30,7 @@ class DeviceModelPermissions(TestCase, ModelPermissions):
|
||||
|
||||
url_name_add = '_device_model_add'
|
||||
|
||||
url_name_change = '_device_model_view'
|
||||
url_name_change = '_device_model_change'
|
||||
|
||||
url_name_delete = '_device_model_delete'
|
||||
|
||||
|
@ -26,7 +26,7 @@ class DeviceTypePermissions(TestCase, ModelPermissions):
|
||||
|
||||
url_name_add = '_device_type_add'
|
||||
|
||||
url_name_change = '_device_type_view'
|
||||
url_name_change = '_device_type_change'
|
||||
|
||||
url_name_delete = '_device_type_delete'
|
||||
|
||||
|
@ -27,7 +27,7 @@ class OperatingSystemPermissions(TestCase, ModelPermissions):
|
||||
|
||||
url_name_add = '_operating_system_add'
|
||||
|
||||
url_name_change = '_operating_system_view'
|
||||
url_name_change = '_operating_system_change'
|
||||
|
||||
url_name_delete = '_operating_system_delete'
|
||||
|
||||
|
294
app/itam/tests/unit/software/test_software_api.py
Normal file
294
app/itam/tests/unit/software/test_software_api.py
Normal file
@ -0,0 +1,294 @@
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from rest_framework.relations import Hyperlink
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from core.models.manufacturer import Manufacturer
|
||||
|
||||
from itam.models.software import Software, SoftwareCategory
|
||||
|
||||
|
||||
class SoftwareAPI(TestCase):
|
||||
|
||||
|
||||
model = Software
|
||||
|
||||
app_namespace = 'API'
|
||||
|
||||
url_name = 'software-detail'
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
. create an organization that is different to item
|
||||
2. Create a software
|
||||
3. create teams with each permission: view, add, change, delete
|
||||
4. create a user per team
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
category = SoftwareCategory.objects.create(
|
||||
name='a category'
|
||||
)
|
||||
|
||||
publisher = Manufacturer.objects.create(
|
||||
name='a manufacturer'
|
||||
)
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization=organization,
|
||||
name = 'softwareone',
|
||||
model_notes = 'random str',
|
||||
category = category,
|
||||
publisher = publisher
|
||||
)
|
||||
|
||||
self.url_view_kwargs = {'pk': self.item.id}
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
view_team = Team.objects.create(
|
||||
team_name = 'view_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
view_team.permissions.set([view_permissions])
|
||||
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = view_team,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.view_user)
|
||||
response = client.get(url)
|
||||
|
||||
self.api_data = response.data
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_url(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
url field must exist
|
||||
"""
|
||||
|
||||
assert 'url' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_url(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
url field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['url']) is Hyperlink
|
||||
|
||||
|
||||
def test_api_field_exists_is_global(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
is_global field must exist
|
||||
"""
|
||||
|
||||
assert 'is_global' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_is_global(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
is_global field must be boolean
|
||||
"""
|
||||
|
||||
assert type(self.api_data['is_global']) is bool
|
||||
|
||||
|
||||
def test_api_field_exists_model_notes(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
model_notes field must exist
|
||||
"""
|
||||
|
||||
assert 'model_notes' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_model_notes(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
model_notes field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['model_notes']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_name(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
name field must exist
|
||||
"""
|
||||
|
||||
assert 'name' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_name(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
name field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['name']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_slug(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
slug field must exist
|
||||
"""
|
||||
|
||||
assert 'slug' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_slug(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
slug field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['slug']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_created(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
created field must exist
|
||||
"""
|
||||
|
||||
assert 'created' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_created(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
created field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['created']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_modified(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
modified field must exist
|
||||
"""
|
||||
|
||||
assert 'modified' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_modified(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
modified field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['modified']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_organization(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
organization field must exist
|
||||
"""
|
||||
|
||||
assert 'organization' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_organization(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
organization field must be intt
|
||||
"""
|
||||
|
||||
assert type(self.api_data['organization']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_publisher(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
publisher field must exist
|
||||
"""
|
||||
|
||||
assert 'publisher' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_publisher(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
publisher field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['publisher']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_category(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
category field must exist
|
||||
"""
|
||||
|
||||
assert 'category' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_category(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
category field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['category']) is int
|
||||
|
@ -26,7 +26,7 @@ class SoftwarePermissions(TestCase, ModelPermissions):
|
||||
|
||||
url_name_add = '_software_add'
|
||||
|
||||
url_name_change = '_software_view'
|
||||
url_name_change = '_software_change'
|
||||
|
||||
url_name_delete = '_software_delete'
|
||||
|
||||
|
@ -29,7 +29,7 @@ class SoftwareCategoryPermissions(TestCase, ModelPermissions):
|
||||
|
||||
url_name_add = '_software_category_add'
|
||||
|
||||
url_name_change = '_software_category_view'
|
||||
url_name_change = '_software_category_change'
|
||||
|
||||
url_name_delete = '_software_category_delete'
|
||||
|
||||
|
@ -18,6 +18,7 @@ urlpatterns = [
|
||||
|
||||
path("operating_system", operating_system.IndexView.as_view(), name="Operating Systems"),
|
||||
path("operating_system/<int:pk>", operating_system.View.as_view(), name="_operating_system_view"),
|
||||
path("operating_system/<int:pk>/edit", operating_system.Change.as_view(), name="_operating_system_change"),
|
||||
path("operating_system/add", operating_system.Add.as_view(), name="_operating_system_add"),
|
||||
path("operating_system/delete/<int:pk>", operating_system.Delete.as_view(), name="_operating_system_delete"),
|
||||
|
||||
@ -30,6 +31,7 @@ urlpatterns = [
|
||||
|
||||
path("software/", software.IndexView.as_view(), name="Software"),
|
||||
path("software/<int:pk>/", software.View.as_view(), name="_software_view"),
|
||||
path("software/<int:pk>/change", software.Change.as_view(), name="_software_change"),
|
||||
path("software/<int:pk>/delete", software.Delete.as_view(), name="_software_delete"),
|
||||
path("software/<int:pk>/version/add", software_version.Add.as_view(), name="_software_version_add"),
|
||||
path("software/<int:software_id>/version/<int:pk>", software_version.View.as_view(), name="_software_version_view"),
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user