Compare commits

...

67 Commits

Author SHA1 Message Date
Jon
8d071c68df chore: add Merge/Pull request template
#226
2024-08-14 01:52:22 +09:30
Jon
3b1691ff62 docs(roadmap): update completed features
#226
2024-08-14 01:46:21 +09:30
Jon
a77c43d213 docs(base): detail view template
. #24 #226 closes #22
2024-08-14 01:33:48 +09:30
Jon
086959b431 refactor(itim): services now use details template
. #22 #226
2024-08-14 01:23:17 +09:30
Jon
3f117f9d83 feat(base): create detail view templates
purpose is to aid in the development of a detail form

#22 #24 #226
2024-08-14 00:02:25 +09:30
Jon
6a23845a4f docs: initial adding of template page
#22
2024-08-13 15:03:10 +09:30
Jon
b9c6d04e04 chore: add services navigation icon
!43 #69
2024-08-13 13:23:45 +09:30
Jon
32c0027ecf fix(itim): ensure that the service template config is also rendered as part of device config
!43 #69
2024-08-13 13:23:45 +09:30
Jon
dae52e8646 docs: fluff the port and services
!43 closes #69
2024-08-13 13:23:45 +09:30
Jon
890a5651a0 fix(itim): dont render link if no device
!43 #69
2024-08-13 13:23:45 +09:30
Jon
4cb37f8347 feat(itam): Render Service Config with device config
!43 #69
2024-08-13 13:23:45 +09:30
Jon
a2010b9517 feat(itam): Display deployed services for devices
!43 #69
2024-08-13 13:23:45 +09:30
Jon
c95736ce14 feat(itim): Prevent circular service dependencies
!43 #69
2024-08-13 13:23:45 +09:30
Jon
b46c61954c feat(itim): Port number validation to check for valid port numbers
!43 #69
2024-08-13 13:23:45 +09:30
Jon
afe4266600 feat(itim): Prevent Service template from being assigned as dependent service
!43 #69
2024-08-13 13:23:45 +09:30
Jon
0c8d1c8da1 feat(itim): Add service template support
!43 #69
2024-08-13 13:23:45 +09:30
Jon
eac998b5cc fix(itim): Dont show self within service dependencies
!43 #69
2024-08-13 13:23:45 +09:30
Jon
5914782252 feat(itim): Ports for service management
!43 #69
2024-08-13 13:23:45 +09:30
Jon
73d875c4ac feat(itim): Service Management
!43 #69
2024-08-13 13:23:45 +09:30
Jon
8f439f0675 fix(assistance): Only return distinct values when limiting KB articles
!43 #10
2024-08-13 13:23:45 +09:30
Jon
0f102c6aaf docs(assistance): document kb categories for user
!43 closes #10
2024-08-13 13:23:45 +09:30
Jon
4852c6caeb feat(assistance): Filter KB articles to target user
only intended to filter for users whom dont have change perm.

!43 #10
2024-08-13 13:23:45 +09:30
Jon
3fffba2eba feat(assistance): Add date picker to date fields for KB articles
!43 #10
2024-08-13 13:23:45 +09:30
Jon
a1293984ea feat(assistance): Dont display expired articles for "view" users
!43 #10
2024-08-13 13:23:45 +09:30
Jon
4876db50c1 docs(assistance): document kb for user
!43 #10
2024-08-13 13:23:45 +09:30
Jon
425cc066af feat(base): add code highlighting to markdown
!43 #10
2024-08-13 13:23:45 +09:30
Jon
1086f517fa feat(assistance): Categorised Knowledge base articles
!43 #10
2024-08-13 13:23:45 +09:30
Jon
2fdbf87ddd docs(assistance): added pages for knowledgebase
!43 #10
2024-08-13 13:23:45 +09:30
Jon
86228836c7 chore(base): rename information -> assistance
!43 #10
2024-08-13 13:23:45 +09:30
Jon
a6e6c948a5 feat(itim): Add menu entry
!43 #69 #71
2024-08-13 13:23:45 +09:30
Jon
dcdfa8feb7 feat(itam): Ability to add device configuration
!43 fixes #44
2024-08-13 13:23:45 +09:30
Jon
8388d2e695 test(external_link): add tests
!43 fixes #6
2024-08-13 13:23:45 +09:30
Jon
29f269050f feat(settings): New model to allow adding templated links to devices and software
!43 #6
2024-08-13 13:23:45 +09:30
Jon
93c4fc2009 docs: move settings pages into sub-directory
!43 #6
2024-08-13 13:23:45 +09:30
9d5464b5a9 build: bump version 1.0.0-b13 -> 1.0.0-b14 2024-08-12 06:02:07 +00:00
Jon
7848397ae2 Merge pull request #224 from nofusscomputing/223-fix-api-team-mnotes
CRUD operation for team notes
2024-08-12 15:29:21 +09:30
Jon
f298ce94bf test(access): test field model_notes
closes #223
2024-08-12 15:15:28 +09:30
Jon
3cace8943e fix(api): ensure model_notes is an available field
#223
2024-08-12 15:14:58 +09:30
Jon
aa40d68c88 build: add test to changelog
#209
2024-08-11 19:30:45 +09:30
c0f186db89 build: bump version 1.0.0-b12 -> 1.0.0-b13 2024-08-11 08:01:20 +00:00
Jon
6b35e7808c Merge pull request #222 from nofusscomputing/153-model-validations
fix: Audit models for validations
2024-08-11 17:21:20 +09:30
Jon
467f6fca6b fix(itam): Ensure device name is formatted according to RFC1035 2.3.1
see https://datatracker.ietf.org/doc/html/rfc1035#autoid-6

#222 closes #153
2024-08-11 17:09:06 +09:30
Jon
f86b2d5216 fix(itam): Ensure device UUID is correctly formatted
#153 #222
2024-08-11 17:08:56 +09:30
Jon
e29d8e1ec1 fix(config_management): Ensure that config group can't set self as parent
interface already filters self out, however check still to be done.

. #153 #222
2024-08-11 17:07:39 +09:30
Jon
0fc5f41391 fix(settings): ensure that the api token cant be saved to notes field
#153
2024-08-11 16:26:19 +09:30
Jon
4b29448d84 Merge pull request #220 from nofusscomputing/162-api-field-validtion
test: api field checks
2024-08-11 14:37:46 +09:30
Jon
e9fe4896df ci: mirror repo to gitlab
. #220 closes #214
2024-08-11 14:29:02 +09:30
Jon
b9d32a2c16 docs(tests): update testing docs explaining test types
#220 closes #162
2024-08-11 12:57:33 +09:30
Jon
d6bd99c5de docs: update project badges to reflect github hosted project
. #220
2024-08-11 12:38:20 +09:30
Jon
7de5ab12bf docs(readme): correct status badge icon to github
. #220
2024-08-11 12:37:52 +09:30
Jon
3fe09fb8f9 test(software): api field checks
. #162 #220
2024-08-11 12:27:57 +09:30
Jon
eb6b03f731 docs(development): added api field test note
. #220
2024-08-11 12:05:46 +09:30
40e3078a58 build: bump version 1.0.0-b11 -> 1.0.0-b12 2024-08-10 11:23:21 +00:00
Jon
4ba79c6ae9 Merge pull request #218 from nofusscomputing/162-api-field-validtion
test: api field checks

#128 #162
2024-08-10 20:51:51 +09:30
Jon
b5c31d81d3 fix(api): ensure org mixin is inherited by software view
. #218 fixes #219
2024-08-10 20:35:06 +09:30
Jon
c3b585d416 fix(base): correct project links to github
. #218
2024-08-10 20:24:43 +09:30
Jon
84d21f4af8 test(teams): api field checks
. #162 #218
2024-08-10 19:58:04 +09:30
Jon
262e431834 test(organization): api field checks
. #162
2024-08-10 19:39:28 +09:30
cde2562048 build: bump version 1.0.0-b10 -> 1.0.0-b11 2024-08-10 08:30:02 +00:00
Jon
67d853cf25 Merge pull request #215 from nofusscomputing/dependabot/pip/django-5.0.8
chore(deps): bump django from 5.0.7 to 5.0.8

#209
2024-08-10 17:54:55 +09:30
Jon
3ba6bb5b4b docs(readme): correct build badge
#209 #214
2024-08-10 17:35:53 +09:30
84d4f48c63 chore(deps): bump django from 5.0.7 to 5.0.8
Bumps [django](https://github.com/django/django) from 5.0.7 to 5.0.8.
- [Commits](https://github.com/django/django/compare/5.0.7...5.0.8)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-10 17:22:47 +09:30
Jon
5e8bebbeb1 build(python): update installed packages as part of the build
#209
2024-08-10 17:22:07 +09:30
Jon
b66a8644a0 Merge pull request #217 from nofusscomputing/ci-adjustments
ci: migration items
2024-08-10 15:03:45 +09:30
Jon
f0ae185fc5 docs(readme): fix version badges
#217 #214
2024-08-10 14:02:27 +09:30
Jon
43b7e413a6 ci(project): add issue/pr project triage
https://github.com/nofusscomputing/action_project/pull/1 https://github.com/nofusscomputing/centurion_erp/issues/214 #217
2024-08-10 13:45:58 +09:30
Jon
04dc00d79d ci(gitlab): fix includes
https://github.com/nofusscomputing/centurion_erp/issues/214
2024-08-10 13:35:37 +09:30
138 changed files with 8325 additions and 254 deletions

View File

@ -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-b10
version: 1.0.0-b14
version_scheme: semver

39
.github/pull_request_template.md vendored Normal file
View 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

View File

@ -10,6 +10,8 @@ on:
tags:
- '*'
env:
GIT_SYNC_URL: "https://${{ secrets.GITLAB_USERNAME_ROBOT }}:${{ secrets.GITLAB_TOKEN_ROBOT }}@gitlab.com/nofusscomputing/projects/centurion_erp.git"
jobs:
@ -31,3 +33,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
View 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 }}

View File

@ -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

View File

@ -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

View File

@ -1,3 +1,45 @@
## 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)
@ -10,62 +52,78 @@
## 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
@ -76,13 +134,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
@ -95,25 +153,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
@ -128,13 +192,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
@ -171,11 +236,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
@ -190,31 +258,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
@ -222,6 +316,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
@ -238,24 +333,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
@ -263,7 +362,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
@ -277,14 +376,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
@ -297,10 +440,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
@ -317,7 +462,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
@ -325,20 +470,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
@ -350,17 +524,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
@ -369,7 +546,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
@ -420,7 +597,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
@ -432,13 +609,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
@ -450,21 +647,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
@ -498,14 +698,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
@ -514,4 +714,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)

View File

@ -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.

View File

@ -4,7 +4,7 @@
<br>
![Project Status - Active](https://img.shields.io/badge/Project%20Status-Active-green?logo=gitlab&style=plastic)
![Project Status - Active](https://img.shields.io/badge/Project%20Status-Active-green?logo=github&style=plastic)
[![Docker Pulls](https://img.shields.io/docker/pulls/nofusscomputing/centurion-erp?style=plastic&logo=docker&color=0db7ed)](https://hub.docker.com/r/nofusscomputing/centurion-erp) [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/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**
![GitHub branch status](https://img.shields.io/github/check-runs/nofusscomputing/centurion_erp/master?style=plastic&logo=github&label=Build&color=000) ![GitHub Release](https://img.shields.io/github/v/release/nofusscomputing/centurion_erp?sort=semver&display_name=release&style=plastic&logo=github&label=Build&color=000) ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_coverage.json&style=plastic)
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nofusscomputing/centurion_erp/ci.yaml?branch=master&style=plastic&logo=github&label=Build&color=%23000) ![GitHub Release](https://img.shields.io/github/v/release/nofusscomputing/centurion_erp?sort=date&style=plastic&logo=github&label=Release&color=000) ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_coverage.json&style=plastic)
![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_unit_test.json)
----
**Development Branch**
![GitHub branch status](https://img.shields.io/github/check-runs/nofusscomputing/centurion_erp/development?style=plastic&logo=github&label=Build&color=000) ![GitHub Release](https://img.shields.io/github/v/release/nofusscomputing/centurion_erp?include_prereleases&sort=semver&display_name=release&style=plastic&logo=github&label=Build&color=000) ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fdevelopment%2Fbadge_endpoint_coverage.json&style=plastic)
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nofusscomputing/centurion_erp/ci.yaml?branch=development&style=plastic&logo=github&label=Build&color=%23000) ![GitHub Release](https://img.shields.io/github/v/release/nofusscomputing/centurion_erp?include_prereleases&sort=date&style=plastic&logo=github&label=Release&color=000) ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fdevelopment%2Fbadge_endpoint_coverage.json&style=plastic)
![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fdevelopment%2Fbadge_endpoint_unit_test.json)

View File

@ -68,6 +68,7 @@ class TeamForm(CommonModelForm):
apps = [
'access',
'assistance',
'config_management',
'core',
'django_celery_results',

View File

@ -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

View 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

View 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

View File

@ -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,

View File

@ -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',

View File

@ -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

View File

@ -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',
]

View File

@ -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
"""

View File

@ -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")),
]

View File

@ -1,6 +1,6 @@
from django.apps import AppConfig
class InformationConfig(AppConfig):
class AssistanceConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'information'
name = 'assistance'

View File

@ -0,0 +1,65 @@
from django import forms
from django.forms import ValidationError
from app import settings
from assistance.models.knowledge_base import KnowledgeBase
from core.forms.common import CommonModelForm
class KnowledgeBaseForm(CommonModelForm):
__name__ = 'asdsa'
class Meta:
fields = '__all__'
model = KnowledgeBase
prefix = 'knowledgebase'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['expiry_date'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local', 'format': "%Y-%m-%dT%H:%M"})
self.fields['expiry_date'].input_formats = settings.DATETIME_FORMAT
self.fields['expiry_date'].format="%Y-%m-%dT%H:%M"
self.fields['release_date'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local', 'format': "%Y-%m-%dT%H:%M"})
self.fields['release_date'].input_formats = settings.DATETIME_FORMAT
self.fields['release_date'].format="%Y-%m-%dT%H:%M"
def clean(self):
cleaned_data = super().clean()
responsible_user = cleaned_data.get("responsible_user")
responsible_teams = cleaned_data.get("responsible_teams")
if not responsible_user and not responsible_teams:
raise ValidationError('A Responsible User or Team must be assigned.')
target_team = cleaned_data.get("target_team")
target_user = cleaned_data.get("target_user")
if not target_team and not target_user:
raise ValidationError('A Target Team or Target User must be assigned.')
if target_team and target_user:
raise ValidationError('Both a Target Team or Target User Cant be assigned at the same time. Use one or the other')
return cleaned_data

View 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

View 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'],
},
),
]

View File

View 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

View File

@ -0,0 +1,232 @@
{% extends 'base.html.j2' %}
{% load markdown %}
{% block content %}
<script>
function openCity(evt, cityName) {
var i, tabcontent, tablinks;
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(cityName).style.display = "block";
evt.currentTarget.className += " active";
}
</script>
<style>
.detail-view-field {
display:unset;
height: 30px;
line-height: 30px;
padding: 0px 20px 40px 20px;
}
.detail-view-field label {
display: inline-block;
font-weight: bold;
width: 200px;
margin: 10px;
height: 30px;
line-height: 30px;
}
.detail-view-field span {
display: inline-block;
width: 340px;
margin: 10px;
border-bottom: 1px solid #ccc;
height: 30px;
line-height: 30px;
}
pre {
word-wrap: break-word;
white-space: pre-wrap;
}
</style>
<div class="tab">
<button
onclick="window.location='{% url 'Assistance:Knowledge Base' %}';"
style="vertical-align: middle; padding: auto; margin: 0px">
<svg xmlns="http://www.w3.org/2000/svg" height="25px" viewBox="0 -960 960 960" width="25px"
style="vertical-align: middle; margin: 0px; padding: 0px border: none; " fill="#6a6e73">
<path
d="m313-480 155 156q11 11 11.5 27.5T468-268q-11 11-28 11t-28-11L228-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T468-692q11 11 11 28t-11 28L313-480Zm264 0 155 156q11 11 11.5 27.5T732-268q-11 11-28 11t-28-11L492-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T732-692q11 11 11 28t-11 28L577-480Z" />
</svg>Back to Articles</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
{% if perms.assistance.change_knowledgebase %}
<button class="tablinks" onclick="openCity(event, 'Notes')">Notes</button>
{% endif %}
</div>
<form method="post">
<div id="Details" class="tabcontent">
{% if perms.assistance.change_knowledgebase %}
<h3>Details</h3>
{% csrf_token %}
<div style="align-items:flex-start; align-content: center; display: flexbox; width: 100%">
<div style="display: inline; width: 40%; margin: 30px;">
<div class="detail-view-field">
<label>{{ form.title.label }}</label>
<span>{{ form.title.value }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.category.label }}</label>
<span>
{% if kb.category %}
<a href="{% url 'Settings:_knowledge_base_category_view' kb.category.id %}">{{ kb.category }}</a>
{% else %}
&nbsp;
{% endif %}
</span>
</div>
<div class="detail-view-field">
<label>{{ form.responsible_user.label }}</label>
<span>
{% if form.responsible_user.value %}
{{ kb.responsible_user }}
{% else %}
&nbsp;
{% endif %}
</span>
</div>
<div class="detail-view-field">
<label>{{ form.organization.label }}</label>
<span>
{% if form.organization.value %}
{{ kb.organization }}
{% else %}
&nbsp;
{% endif %}
</span>
</div>
</div>
<div style="display: inline; width: 40%; margin: 30px; text-align: left;">
<div class="detail-view-field">
<label>{{ form.release_date.label }}</label>
<span>
{% if form.release_date.value %}
{{ form.release_date.value }}
{% else %}
&nbsp;
{% endif %}
</span>
</div>
<div class="detail-view-field">
<label>{{ form.expiry_date.label }}</label>
<span>
{% if form.expiry_date.value %}
{{ form.expiry_date.value }}
{% else %}
&nbsp;
{% endif %}
</span>
</div>
<div class="detail-view-field">
<label>{{ form.target_user.label }}</label>
<span>
{% if form.target_user.value %}
{{ kb.target_user }}
{% else %}
&nbsp;
{% endif %}
</span>
</div>
<div class="detail-view-field">
<label>{{ form.target_team.label }}</label>
<span>
{% if form.target_team.value %}
{{ form.target_team.value }} {{ kb.target_team }}
{% else %}
&nbsp;
{% endif %}
</span>
</div>
</div>
</div>
<input type="button" value="Edit" onclick="window.location='{% url 'Assistance:_knowledge_base_change' kb.id %}';">
{% endif %}
{% if form.summary.value %}
<div style="display: block; width: 100%;">
<h3>Summary</h3>
{{ form.summary.value | safe }}
<br>
<hr />
</div>
{% endif %}
<div style="display: block; width: 100%;">
<h3>Content</h3>
<hr />
<br>
{{ form.content.value | markdown | safe }}
<br>
</div>
<br>
<script>
document.getElementById("defaultOpen").click();
</script>
</div>
{% if perms.assistance.change_knowledgebase %}
<div id="Notes" class="tabcontent">
<h3>
Notes
</h3>
{{ notes_form }}
<input type="submit" name="{{notes_form.prefix}}" value="Submit" />
<div class="comments">
{% if notes %}
{% for note in notes %}
{% include 'note.html.j2' %}
{% endfor %}
{% endif %}
</div>
</div>
{% endif %}
</form>
{% endblock %}

View 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 %}
&nbsp;
{% 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 %}
&nbsp;
{% 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 %}
&nbsp;
{% 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 %}
&nbsp;
{% 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 %}

View File

@ -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>&nbsp;</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>&nbsp;</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">&laquo; 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 &raquo;</a>
{% endif %}
</span>
</div>
{% endblock %}

View 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>&nbsp;</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>&nbsp;</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">&laquo; 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 &raquo;</a>
{% endif %}
</span>
</div>
{% endblock %}

View File

@ -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"})',
)

View File

@ -0,0 +1,78 @@
import pytest
import unittest
import requests
from django.test import TestCase, Client
from access.models import Organization
from core.models.history import History
from core.tests.abstract.history_entry import HistoryEntry
from core.tests.abstract.history_entry_parent_model import HistoryEntryParentItem
from 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,
)

View File

@ -0,0 +1,95 @@
# from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
import requests
from access.models import Organization, Team, TeamUsers, Permission
from 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
)

View File

@ -0,0 +1,189 @@
# from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
import requests
from access.models import Organization, Team, TeamUsers, Permission
from app.tests.abstract.model_permissions import ModelPermissions
from 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
)

View File

@ -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'

View File

@ -0,0 +1,42 @@
import pytest
import unittest
from django.test import TestCase
from access.models import Organization
from app.tests.abstract.models import TenancyModel
from 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',
)

View File

@ -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

View File

@ -0,0 +1,95 @@
# from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
import requests
from access.models import Organization, Team, TeamUsers, Permission
from 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
)

View File

@ -0,0 +1,189 @@
# from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
import requests
from access.models import Organization, Team, TeamUsers, Permission
from app.tests.abstract.model_permissions import ModelPermissions
from 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
)

View File

@ -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
View 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"),
]

View File

View File

@ -0,0 +1,215 @@
from datetime import datetime
from django.contrib.auth import decorators as auth_decorator
from django.db.models import Q
from django.urls import reverse
from django.utils.decorators import method_decorator
from access.models import TeamUsers
from assistance.forms.knowledge_base import KnowledgeBaseForm
from assistance.models.knowledge_base import KnowledgeBase
from core.forms.comment import AddNoteForm
from core.models.notes import Notes
from core.views.common import AddView, ChangeView, DeleteView, DisplayView, IndexView
from settings.models.user_settings import UserSettings
class Index(IndexView):
context_object_name = "items"
model = KnowledgeBase
paginate_by = 10
permission_required = [
'assistance.view_knowledgebase'
]
template_name = 'assistance/kb_index.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if not self.request.user.has_perm('assistance.change_knowledgebase') and not self.request.user.is_superuser:
user_teams = []
for team_user in TeamUsers.objects.filter(user=self.request.user):
if team_user.team.id not in user_teams:
user_teams += [ team_user.team.id ]
context['items'] = self.get_queryset().filter(
Q(expiry_date__lte=datetime.now())
|
Q(expiry_date=None)
).filter(
Q(target_team__in=user_teams)
|
Q(target_user=self.request.user.id)
).distinct()
context['model_docs_path'] = self.model._meta.app_label + '/knowledge_base/'
context['content_title'] = 'Knowledge Base Articles'
return context
class Add(AddView):
form_class = KnowledgeBaseForm
model = KnowledgeBase
permission_required = [
'assistance.add_knowledgebase',
]
def get_initial(self):
initial: dict = {
'organization': UserSettings.objects.get(user = self.request.user).default_organization
}
if 'pk' in self.kwargs:
if self.kwargs['pk']:
initial.update({'parent': self.kwargs['pk']})
self.model.parent.field.hidden = True
return initial
def get_success_url(self, **kwargs):
return reverse('Assistance:Knowledge Base')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'New Group'
return context
class Change(ChangeView):
context_object_name = "group"
form_class = KnowledgeBaseForm
model = KnowledgeBase
permission_required = [
'assistance.change_knowledgebase',
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = self.object.title
return context
def get_success_url(self, **kwargs):
return reverse('Assistance:_knowledge_base_view', args=(self.kwargs['pk'],))
class View(ChangeView):
context_object_name = "kb"
form_class = KnowledgeBaseForm
model = KnowledgeBase
permission_required = [
'assistance.view_knowledgebase',
]
template_name = 'assistance/kb_article.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['notes_form'] = AddNoteForm(prefix='note')
context['notes'] = Notes.objects.filter(config_group=self.kwargs['pk'])
context['model_pk'] = self.kwargs['pk']
context['model_name'] = self.model._meta.model_name
context['model_delete_url'] = reverse('Assistance:_knowledge_base_delete', args=(self.kwargs['pk'],))
context['content_title'] = self.object.title
return context
@method_decorator(auth_decorator.permission_required("assistance.change_knowledgebase", raise_exception=True))
def post(self, request, *args, **kwargs):
item = KnowledgeBase.objects.get(pk=self.kwargs['pk'])
notes = AddNoteForm(request.POST, prefix='note')
if notes.is_bound and notes.is_valid() and notes.instance.note != '':
notes.instance.organization = item.organization
notes.save()
# dont allow saving any post data outside notes.
# todo: figure out what needs to be returned
# return super().post(request, *args, **kwargs)
def get_success_url(self, **kwargs):
return reverse('Assistance:_knowledge_base_view', args=(self.kwargs['pk'],))
class Delete(DeleteView):
model = KnowledgeBase
permission_required = [
'assistance.delete_knowledgebase',
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Delete ' + self.object.title
return context
def get_success_url(self, **kwargs):
return reverse('Assistance:Knowledge Base')

View 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')

View File

@ -195,6 +195,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)

View File

@ -0,0 +1,20 @@
# Generated by Django 5.0.7 on 2024-07-21 02:35
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0002_notes'),
('itim', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='notes',
name='service',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itim.service'),
),
]

View File

@ -10,6 +10,9 @@ from itam.models.device import Device
from itam.models.software import Software
from itam.models.operating_system import OperatingSystem
from itim.models.services import Service
class NotesCommonFields(TenancyObject, models.Model):
@ -88,6 +91,14 @@ class Notes(NotesCommonFields):
blank= True
)
service = models.ForeignKey(
Service,
on_delete=models.CASCADE,
default = None,
null = True,
blank= True
)
software = models.ForeignKey(
Software,
on_delete=models.CASCADE,

View File

@ -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)

View File

@ -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'])

View File

@ -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

View File

@ -1,9 +1,12 @@
from django.template import Template, Context
from django.utils.html import escape
from django.views import generic
from access.mixin import OrganizationPermission
from core.exceptions import MissingAttribute
from settings.models.external_link import ExternalLink
from settings.models.user_settings import UserSettings
@ -50,6 +53,68 @@ class ChangeView(View, generic.UpdateView):
template_name:str = 'form.html.j2'
# ToDo: on migrating all views to seperate display and change views, external_links will not be required in `ChangView`
def get_context_data(self, **kwargs):
""" Get template context
For items that have the ability to have external links, this function
adds the external link details to the context.
!!! Danger "Requirement"
This function may be overridden with the caveat that this function is still called.
by the overriding function. i.e. `super().get_context_data(skwargs)`
!!! note
The adding of `external_links` within this view is scheduled to be removed.
Returns:
(dict): Context for the template to use inclusive of 'external_links'
"""
context = super().get_context_data(**kwargs)
external_links_query = None
if 'tab' in self.request.GET:
context['open_tab'] = str(self.request.GET.get("tab")).lower()
else:
context['open_tab'] = None
if self.model._meta.model_name == 'device':
external_links_query = ExternalLink.objects.filter(devices=True)
elif self.model._meta.model_name == 'software':
external_links_query = ExternalLink.objects.filter(software=True)
if external_links_query:
external_links: list = []
user_context = Context(context)
for external_link in external_links_query:
user_string = Template(external_link)
external_link_context: dict = {
'name': escape(external_link.name),
'link': escape(user_string.render(user_context)),
}
if external_link.colour:
external_link_context.update({'colour': external_link.colour })
external_links += [ external_link_context ]
context['external_links'] = external_links
return context
class DeleteView(OrganizationPermission, generic.DeleteView):
@ -64,6 +129,60 @@ class DisplayView(OrganizationPermission, generic.DetailView):
template_name:str = 'form.html.j2'
# ToDo: on migrating all views to seperate display and change views, external_links will not be required in `ChangView`
def get_context_data(self, **kwargs):
""" Get template context
For items that have the ability to have external links, this function
adds the external link details to the context.
!!! Danger "Requirement"
This function may be overridden with the caveat that this function is still called.
by the overriding function. i.e. `super().get_context_data(skwargs)`
Returns:
(dict): Context for the template to use inclusive of 'external_links'
"""
context = super().get_context_data(**kwargs)
external_links_query = None
if self.model._meta.model_name == 'device':
external_links_query = ExternalLink.objects.filter(devices=True)
elif self.model._meta.model_name == 'software':
external_links_query = ExternalLink.objects.filter(software=True)
if external_links_query:
external_links: list = []
user_context = Context(context)
for external_link in external_links_query:
user_string = Template(external_link)
external_link_context: dict = {
'name': escape(external_link.name),
'link': escape(user_string.render(user_context)),
}
if external_link.colour:
external_link_context.update({'colour': external_link.colour })
external_links += [ external_link_context ]
context['external_links'] = external_links
return context
class IndexView(View, generic.ListView):

View File

@ -41,6 +41,8 @@ class View(OrganizationPermission, generic.View):
from config_management.models.groups import ConfigGroups
from settings.models.external_link import ExternalLink
if not hasattr(self, 'model'):
match self.kwargs['model_name']:
@ -61,6 +63,22 @@ class View(OrganizationPermission, generic.View):
self.model = DeviceType
case 'externallink':
self.model = ExternalLink
case 'knowledgebase':
from assistance.models.knowledge_base import KnowledgeBase
self.model = KnowledgeBase
case 'knowledgebasecategory':
from assistance.models.knowledge_base import KnowledgeBaseCategory
self.model = KnowledgeBaseCategory
case 'manufacturer':
self.model = Manufacturer
@ -81,10 +99,22 @@ class View(OrganizationPermission, generic.View):
self.model = Organization
case 'port':
from itim.models.services import Port
self.model = Port
case 'team':
self.model = Team
case 'service':
from itim.models.services import Service
self.model = Service
case _:
raise Exception('Unable to determine history items model')

View File

@ -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"),
]

View File

@ -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)

View File

@ -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)

View File

@ -23,6 +23,7 @@ class DeviceForm(CommonModelForm):
'device_type',
'organization',
'model_notes',
'config',
]

View 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'),
),
]

View File

@ -1,8 +1,10 @@
import json
import re
from datetime import timedelta
from django.db import models
from django.forms import ValidationError
from access.fields import *
from access.models import TenancyObject
@ -18,6 +20,8 @@ from itam.models.operating_system import OperatingSystemVersion
from settings.models.app_settings import AppSettings
class DeviceType(DeviceCommonFieldsName, SaveHistory):
@ -39,6 +43,49 @@ class DeviceType(DeviceCommonFieldsName, SaveHistory):
class Device(DeviceCommonFieldsName, SaveHistory):
reserved_config_keys: list = [
'software'
]
def validate_config_keys_not_reserved(self):
value: dict = self
for invalid_key in Device.reserved_config_keys:
if invalid_key in value.keys():
raise ValidationError(f'json key "{invalid_key}" is a reserved configuration key')
def validate_uuid_format(self):
pattern = r'[0-9|a-f]{8}\-[0-9|a-f]{4}\-[0-9|a-f]{4}\-[0-9|a-f]{4}\-[0-9|a-f]{12}'
if not re.match(pattern, str(self)):
raise ValidationError(f'UUID Must be in {str(pattern)}')
def validate_hostname_format(self):
pattern = r'^[a-z]{1}[a-z|0-9|\-]+[a-z|0-9]{1}$'
if not re.match(pattern, str(self).lower()):
raise ValidationError(
'''[RFC1035 2.3.1] A hostname must start with a letter, end with a letter or digit,
and have as interior characters only letters, digits, and hyphen.'''
)
name = models.CharField(
blank = False,
max_length = 50,
unique = True,
validators = [ validate_hostname_format ]
)
serial_number = models.CharField(
verbose_name = 'Serial Number',
max_length = 50,
@ -58,6 +105,7 @@ class Device(DeviceCommonFieldsName, SaveHistory):
blank = True,
unique = True,
help_text = 'System GUID/UUID.',
validators = [ validate_uuid_format ]
)
device_model = models.ForeignKey(
@ -79,6 +127,15 @@ 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,
@ -220,6 +277,25 @@ class Device(DeviceCommonFieldsName, SaveHistory):
config['software'] = merge_software(group_software, host_software)
if self.config:
config.update(self.config)
from itim.models.services import Service
services = Service.objects.filter(
device = self.pk
)
for service in services:
if service.config_variables:
service_config:dict = {
service.config_key_variable: service.config_variables
}
config.update(service_config)
return config

View File

@ -83,7 +83,9 @@
<div id="Details" class="tabcontent">
<h3>
Details
<span style="font-weight: normal; float: right;">{% include 'icons/issue_link.html.j2' with issue=6 %}</span>
{% for external_link in external_links %}
<span style="font-weight: normal; float: right;">{% include 'icons/external_link.html.j2' with external_link=external_link %}</span>
{% endfor %}
</h3>
<div style="align-items:flex-start; align-content: center; display: flexbox; width: 100%">
@ -182,6 +184,35 @@
<input type="submit" name="{{operating_system.prefix}}" value="Submit" />
</div>
<div style="display: block; width: 100%;">
<h3>Dependent Services</h3>
<table>
<tr>
<th>Name</th>
<th>Ports</th>
</tr>
{% if services %}
{% for service in services %}
<tr>
<td><a href="{% url 'ITIM:_service_view' service.pk %}">{{ service }}</a></td>
<td>{% for port in service.port.all %}{{ port }} ({{ port.description}}), {% endfor %}</td>
</tr>
{% endfor%}
{% else %}
<tr>
<td colspan="2"> Nothing Found</td>
</tr>
{% endif %}
</table>
</div>
<div style="display: block; width: 100%;">
<h3>Device Config</h3>
<br>
<textarea cols="90" rows="30" readonly>{{ device.config }}</textarea>
</div>
<input type="button" value="Edit" onclick="window.location='{% url 'ITAM:_device_change' device.id %}';">
{% if not tab %}
<script>
// Get the element with id="defaultOpen" and click on it

View File

@ -43,8 +43,12 @@
<form method="post">
<div id="Details" class="tabcontent">
<h3>Details</h3>
<h3>
Details
{% for external_link in external_links %}
<span style="font-weight: normal; float: right;">{% include 'icons/external_link.html.j2' with external_link=external_link %}</span>
{% endfor %}
</h3>
{% csrf_token %}
{{ form }}
<br>

View 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

View File

@ -21,10 +21,11 @@ from core.views.common import AddView, ChangeView, DeleteView, IndexView
from itam.forms.device_softwareadd import SoftwareAdd
from itam.forms.device_softwareupdate import SoftwareUpdate
from itam.forms.device.device import DeviceForm
from itam.forms.device.operating_system import Update as OperatingSystemForm
from itim.models.services import Service
from settings.models.user_settings import UserSettings
@ -104,6 +105,8 @@ class View(ChangeView):
context['operating_system'] = OperatingSystemForm(prefix='operating_system')
context['services'] = Service.objects.filter(device=self.kwargs['pk'])
softwares = DeviceSoftware.objects.filter(device=self.kwargs['pk'])
softwares = Paginator(softwares, 10)

24
app/itim/forms/ports.py Normal file
View File

@ -0,0 +1,24 @@
# from django import forms
# from django.forms import ValidationError
# from app import settings
from itim.models.services import Port
from core.forms.common import CommonModelForm
from settings.models.user_settings import UserSettings
class PortForm(CommonModelForm):
class Meta:
fields = '__all__'
model = Port
prefix = 'port'

162
app/itim/forms/services.py Normal file
View File

@ -0,0 +1,162 @@
from django import forms
from django.forms import ValidationError
from django.urls import reverse
from itim.models.services import Service
from app import settings
from core.forms.common import CommonModelForm
class ServiceForm(CommonModelForm):
class Meta:
fields = '__all__'
model = Service
prefix = 'service'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['dependent_service'].queryset = self.fields['dependent_service'].queryset.exclude(
id=self.instance.pk
).exclude(
is_template=True
)
self.fields['template'].queryset = self.fields['template'].queryset.exclude(
id=self.instance.pk
)
def clean(self):
cleaned_data = super().clean()
pk = self.instance.id
dependent_service = cleaned_data.get("dependent_service")
device = cleaned_data.get("device")
cluster = cleaned_data.get("cluster")
is_template = cleaned_data.get("is_template")
template = cleaned_data.get("template")
port = cleaned_data.get("port")
if not is_template and not template:
if not device and not cluster:
raise ValidationError('A Service must be assigned to either a "Cluster" or a "Device".')
if device and cluster:
raise ValidationError('A Service must only be assigned to either a "Cluster" or a "Device". Not both.')
if not port:
raise ValidationError('Port(s) must be assigned to a service.')
if dependent_service:
for dependency in dependent_service:
query = Service.objects.filter(
dependent_service = pk,
id = dependency.id,
)
if query.exists():
raise ValidationError('A dependent service already depends upon this service. Circular dependencies are not allowed.')
return cleaned_data
class DetailForm(ServiceForm):
tabs: dict = {
"details": {
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'name',
'config_key_variable',
'template',
'organization',
'c_created',
'c_modified'
],
"right": [
'model_notes',
]
}
]
},
"rendered_config": {
"name": "Rendered Config",
"slug": "rendered_config",
"sections": [
{
"layout": "single",
"fields": [
'config_variables',
],
"json": [
'config_variables'
]
}
]
}
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['config_variables'] = forms.fields.JSONField(
widget = forms.Textarea(
attrs = {
"cols": "80",
"rows": "100"
}
),
label = 'Rendered Configuration',
initial = self.instance.config_variables,
)
self.fields['c_created'] = forms.DateTimeField(
label = 'Created',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.created,
)
self.fields['c_modified'] = forms.DateTimeField(
label = 'Modified',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.modified,
)
self.tabs['details'].update({
"edit_url": reverse('ITIM:_service_change', args=(self.instance.pk,))
})

View File

@ -0,0 +1,102 @@
# Generated by Django 5.0.7 on 2024-07-21 02:35
import access.fields
import access.models
import django.db.models.deletion
import django.utils.timezone
import itim.models.services
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('access', '0001_initial'),
('itam', '0002_device_config'),
]
operations = [
migrations.CreateModel(
name='ClusterType',
fields=[
('is_global', models.BooleanField(default=False)),
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('name', models.CharField(help_text='Name of the Cluster Type', max_length=50, verbose_name='Name')),
('slug', access.fields.AutoSlugField()),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
],
options={
'verbose_name': 'ClusterType',
'verbose_name_plural': 'ClusterTypes',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Cluster',
fields=[
('is_global', models.BooleanField(default=False)),
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('name', models.CharField(help_text='Name of the Cluster', max_length=50, verbose_name='Name')),
('slug', access.fields.AutoSlugField()),
('config', models.JSONField(blank=True, default=None, help_text='Cluster Configuration', null=True, verbose_name='Configuration')),
('devices', models.ManyToManyField(blank=True, default=None, help_text='Devices that are deployed upon the cluster.', related_name='cluster_device', to='itam.device', verbose_name='Devices')),
('node', models.ManyToManyField(blank=True, default=None, help_text='Hosts for resource consumption that the cluster is deployed upon', related_name='cluster_node', to='itam.device', verbose_name='Nodes')),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
('parent_cluster', models.ForeignKey(blank=True, default=None, help_text='Parent Cluster for this cluster', null=True, on_delete=django.db.models.deletion.CASCADE, to='itim.cluster', verbose_name='Parent Cluster')),
('cluster_type', models.ForeignKey(blank=True, default=None, help_text='Parent Cluster for this cluster', null=True, on_delete=django.db.models.deletion.CASCADE, to='itim.clustertype', verbose_name='Parent Cluster')),
],
options={
'verbose_name': 'Cluster',
'verbose_name_plural': 'Clusters',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Port',
fields=[
('is_global', models.BooleanField(default=False)),
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('number', models.IntegerField(help_text='The port number', validators=[itim.models.services.Port.validation_port_number], verbose_name='Port Number')),
('description', models.CharField(blank=True, default=None, help_text='Short description of port', max_length=80, null=True, verbose_name='Description')),
('protocol', models.CharField(choices=[('TCP', 'TCP'), ('UDP', 'UDP')], default='TCP', help_text='Layer 4 Network Protocol', max_length=3, verbose_name='Protocol')),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
],
options={
'verbose_name': 'Protocol',
'verbose_name_plural': 'Protocols',
'ordering': ['number', 'protocol'],
},
),
migrations.CreateModel(
name='Service',
fields=[
('is_global', models.BooleanField(default=False)),
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('is_template', models.BooleanField(default=False, help_text='Is this service to be used as a template', verbose_name='Template')),
('name', models.CharField(help_text='Name of the Service', max_length=50, verbose_name='Name')),
('config', models.JSONField(blank=True, default=None, help_text='Cluster Configuration', null=True, verbose_name='Configuration')),
('config_key_variable', models.CharField(help_text='Key name to use when merging with cluster/device config.', max_length=50, null=True, validators=[itim.models.services.Service.validate_config_key_variable], verbose_name='Configuration Key')),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('cluster', models.ForeignKey(blank=True, default=None, help_text='Cluster the service is assigned to', null=True, on_delete=django.db.models.deletion.CASCADE, to='itim.cluster', verbose_name='Cluster')),
('dependent_service', models.ManyToManyField(blank=True, default=None, help_text='Services that this service depends upon', related_name='dependentservice', to='itim.service', verbose_name='Dependent Services')),
('device', models.ForeignKey(blank=True, default=None, help_text='Device the service is assigned to', null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.device', verbose_name='Device')),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
('port', models.ManyToManyField(blank=True, help_text='Port the service is available on', to='itim.port', verbose_name='Port')),
('template', models.ForeignKey(blank=True, default=None, help_text='Template this service uses', null=True, on_delete=django.db.models.deletion.CASCADE, to='itim.service', verbose_name='Template Name')),
],
options={
'verbose_name': 'Service',
'verbose_name_plural': 'Services',
'ordering': ['name'],
},
),
]

View File

123
app/itim/models/clusters.py Normal file
View File

@ -0,0 +1,123 @@
from django.contrib.auth.models import User
from django.db import models
from django.forms import ValidationError
from access.fields import *
from access.models import Team, TenancyObject
from itam.models.device import Device
class ClusterType(TenancyObject):
class Meta:
ordering = [
'name',
]
verbose_name = "ClusterType"
verbose_name_plural = "ClusterTypes"
id = models.AutoField(
primary_key=True,
unique=True,
blank=False
)
name = models.CharField(
blank = False,
help_text = 'Name of the Cluster Type',
max_length = 50,
unique = False,
verbose_name = 'Name',
)
slug = AutoSlugField()
class Cluster(TenancyObject):
class Meta:
ordering = [
'name',
]
verbose_name = "Cluster"
verbose_name_plural = "Clusters"
id = models.AutoField(
primary_key=True,
unique=True,
blank=False
)
parent_cluster = models.ForeignKey(
'self',
blank = True,
default = None,
help_text = 'Parent Cluster for this cluster',
null = True,
on_delete = models.CASCADE,
verbose_name = 'Parent Cluster',
)
cluster_type = models.ForeignKey(
ClusterType,
blank = True,
default = None,
help_text = 'Parent Cluster for this cluster',
null = True,
on_delete = models.CASCADE,
verbose_name = 'Parent Cluster',
)
name = models.CharField(
blank = False,
help_text = 'Name of the Cluster',
max_length = 50,
unique = False,
verbose_name = 'Name',
)
slug = AutoSlugField()
config = models.JSONField(
blank = True,
default = None,
help_text = 'Cluster Configuration',
null = True,
verbose_name = 'Configuration',
)
node = models.ManyToManyField(
Device,
blank = True,
default = None,
help_text = 'Hosts for resource consumption that the cluster is deployed upon',
related_name = 'cluster_node',
verbose_name = 'Nodes',
)
devices = models.ManyToManyField(
Device,
blank = True,
default = None,
help_text = 'Devices that are deployed upon the cluster.',
related_name = 'cluster_device',
verbose_name = 'Devices',
)
def __str__(self):
return self.name

235
app/itim/models/services.py Normal file
View File

@ -0,0 +1,235 @@
import re
from django.contrib.auth.models import User
from django.db import models
from django.forms import ValidationError
from access.fields import *
from access.models import Team, TenancyObject
from itam.models.device import Device
from itim.models.clusters import Cluster
class Port(TenancyObject):
class Meta:
ordering = [
'number',
'protocol',
]
verbose_name = "Protocol"
verbose_name_plural = "Protocols"
class Protocol(models.TextChoices):
TCP = 'TCP', 'TCP'
UDP = 'UDP', 'UDP'
def validation_port_number(number: int):
if number < 1 or number > 65535:
raise ValidationError('A Valid port number is between 1-65535')
id = models.AutoField(
primary_key=True,
unique=True,
blank=False
)
number = models.IntegerField(
blank = False,
help_text = 'The port number',
unique = False,
validators = [ validation_port_number ],
verbose_name = 'Port Number',
)
description = models.CharField(
blank = True,
default = None,
help_text = 'Short description of port',
max_length = 80,
null = True,
verbose_name = 'Description',
)
protocol = models.CharField(
blank = False,
choices=Protocol.choices,
default = Protocol.TCP,
help_text = 'Layer 4 Network Protocol',
max_length = 3,
verbose_name = 'Protocol',
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
def __str__(self):
return str(self.protocol) + '/' + str(self.number)
class Service(TenancyObject):
class Meta:
ordering = [
'name',
]
verbose_name = "Service"
verbose_name_plural = "Services"
def validate_config_key_variable(value):
if not value:
raise ValidationError('You must enter a config key.')
valid_chars = search=re.compile(r'[^a-z_]').search
if bool(valid_chars(value)):
raise ValidationError('config key must only contain [a-z_].')
id = models.AutoField(
primary_key=True,
unique=True,
blank=False
)
is_template = models.BooleanField(
blank = False,
default = False,
help_text = 'Is this service to be used as a template',
verbose_name = 'Template',
)
template = models.ForeignKey(
'self',
blank = True,
default = None,
help_text = 'Template this service uses',
null = True,
on_delete = models.CASCADE,
verbose_name = 'Template Name',
)
name = models.CharField(
blank = False,
help_text = 'Name of the Service',
max_length = 50,
unique = False,
verbose_name = 'Name',
)
device = models.ForeignKey(
Device,
blank = True,
default = None,
help_text = 'Device the service is assigned to',
null = True,
on_delete = models.CASCADE,
verbose_name = 'Device',
)
cluster = models.ForeignKey(
'Cluster',
blank = True,
default = None,
help_text = 'Cluster the service is assigned to',
null = True,
on_delete = models.CASCADE,
unique = False,
verbose_name = 'Cluster',
)
config = models.JSONField(
blank = True,
default = None,
help_text = 'Cluster Configuration',
null = True,
verbose_name = 'Configuration',
)
config_key_variable = models.CharField(
blank = False,
help_text = 'Key name to use when merging with cluster/device config.',
max_length = 50,
null = True,
unique = False,
validators = [ validate_config_key_variable ],
verbose_name = 'Configuration Key',
)
port = models.ManyToManyField(
Port,
blank = True,
help_text = 'Port the service is available on',
verbose_name = 'Port',
)
dependent_service = models.ManyToManyField(
'self',
blank = True,
default = None,
help_text = 'Services that this service depends upon',
related_name = 'dependentservice',
symmetrical = False,
verbose_name = 'Dependent Services',
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
@property
def config_variables(self):
if self.is_template:
return self.config
if self.template:
template_config: dict = Service.objects.get(id=self.template.id).config
template_config.update(self.config)
return template_config
else:
return self.config
return None
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
if self.config_key_variable:
self.config_key_variable = self.config_key_variable.lower()
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
def __str__(self):
return self.name

View File

@ -0,0 +1,196 @@
{% extends 'base.html.j2' %}
{% load markdown %}
{% block content %}
<script>
function openCity(evt, cityName) {
var i, tabcontent, tablinks;
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(cityName).style.display = "block";
evt.currentTarget.className += " active";
}
</script>
<style>
.detail-view-field {
display: unset;
height: 30px;
line-height: 30px;
padding: 0px 20px 40px 20px;
}
.detail-view-field label {
display: inline-block;
font-weight: bold;
width: 200px;
margin: 10px;
height: 30px;
line-height: 30px;
}
.detail-view-field span {
display: inline-block;
width: 340px;
margin: 10px;
border-bottom: 1px solid #ccc;
height: 30px;
line-height: 30px;
}
pre {
word-wrap: break-word;
white-space: pre-wrap;
}
</style>
<div class="tab">
<button onclick="window.location='{% url 'Settings:_ports' %}';"
style="vertical-align: middle; padding: auto; margin: 0px">
<svg xmlns="http://www.w3.org/2000/svg" height="25px" viewBox="0 -960 960 960" width="25px"
style="vertical-align: middle; margin: 0px; padding: 0px border: none; " fill="#6a6e73">
<path d="m313-480 155 156q11 11 11.5 27.5T468-268q-11 11-28 11t-28-11L228-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T468-692q11 11 11 28t-11 28L313-480Zm264 0 155 156q11 11 11.5 27.5T732-268q-11 11-28 11t-28-11L492-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T732-692q11 11 11 28t-11 28L577-480Z" />
</svg>Back to Ports</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
<button class="tablinks" onclick="openCity(event, 'Services')">Services</button>
{% if perms.assistance.change_service %}
<button class="tablinks" onclick="openCity(event, 'Notes')">Notes</button>
{% endif %}
</div>
<form method="post">
<div id="Details" class="tabcontent">
<h3>Details</h3>
{% csrf_token %}
<div style="align-items:flex-start; align-content: center; display: flexbox; width: 100%">
<div style="display: inline; width: 40%; margin: 30px;">
<div class="detail-view-field">
<label>{{ form.number.label }}</label>
<span>{{ form.number.value }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.description.label }}</label>
<span>
{% if form.description.value %}
{{ form.description.value }}
{% else %}
&nbsp;
{% endif %}
</span>
</div>
<div class="detail-view-field">
<label>{{ form.protocol.label }}</label>
<span>{{ form.protocol.value }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.organization.label }}</label>
<span>{{ item.organization }}</span>
</div>
<div class="detail-view-field">
<label>Created</label>
<span>{{ item.created }}</span>
</div>
<div class="detail-view-field">
<label>Modified</label>
<span>{{ item.modified }}</span>
</div>
</div>
<div style="display: inline; width: 40%; margin: 30px; text-align: left;">
<div>
<label
style="font-weight: bold; width: 100%; border-bottom: 1px solid #ccc; display: block; text-align: inherit;">{{ form.model_notes.label }}</label>
<div style="display: inline-block; text-align: left;">
{% if form.model_notes.value %}
{{ form.model_notes.value | markdown | safe }}
{% else %}
&nbsp;
{% endif %}
</div>
</div>
</div>
</div>
<input type="button" value="Edit" onclick="window.location='{% url 'Settings:_port_change' item.pk %}';">
<br>
<script>
document.getElementById("defaultOpen").click();
</script>
</div>
<div id="Services" class="tabcontent">
<h3>
Services
</h3>
<table>
<tr>
<th>Name</th>
<th>Organization</th>
</tr>
{% for service in services %}
<tr>
<td><a href="{% url 'ITIM:_service_view' service.pk %}">{{ service.name }}</a></td>
<td>{{ service.organization }}</td>
</tr>
{% endfor%}
</table>
</div>
</div>
{% if perms.assistance.change_knowledgebase %}
<div id="Notes" class="tabcontent">
<h3>
Notes
</h3>
{{ notes_form }}
<input type="submit" name="{{ notes_form.prefix }}" value="Submit" />
<div class="comments">
{% if notes %}
{% for note in notes %}
{% include 'note.html.j2' %}
{% endfor %}
{% endif %}
</div>
</div>
{% endif %}
</form>
{% endblock %}

View File

@ -0,0 +1,53 @@
{% extends 'base.html.j2' %}
{% block content %}
<input type="button" value="New Port" onclick="window.location='{% url 'Settings:_port_add' %}';">
<table class="data">
<tr>
<th>Title</th>
<th>Cluster / Device</th>
<th>Organization</th>
<th>&nbsp;</th>
</tr>
{% if items %}
{% for item in items %}
<tr>
<td><a href="{% url 'Settings:_port_view' pk=item.id %}">{{ item.protocol }}/{{ item.number }}</a></td>
<td>
{% if item.device %}
{{ item.device }}
{% else %}
{{ item.cluster }}
{% endif %}
</td>
<td>{{ item.organization }}</td>
<td>&nbsp;</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">&laquo; 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 &raquo;</a>
{% endif %}
</span>
</div>
{% endblock %}

View File

@ -0,0 +1,75 @@
{% extends 'detail.html.j2' %}
{% load json %}
{% load markdown %}
{% block tabs %}
<div id="details" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.details %}
<hr />
<div style="display: block; width: 100%;">
<h3>Ports</h3>
<table>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
{% if item.port.all and not item.template %}
{% for port in item.port.all %}
<tr>
<td><a href="{% url 'Settings:_port_view' item.pk %}">{{ port }}</a></td>
<td>{{ port.description }}</td>
</tr>
{% endfor %}
{% elif not item.port.all and item.template %}
{% for port in item.template.port.all %}
<tr>
<td><a href="{% url 'Settings:_port_view' item.pk %}">{{ port }}</a></td>
<td>{{ port.description }}</td>
</tr>
{% endfor%}
{% else %}
<tr>
<td colspan="2"> Nothing Found</td>
</tr>
{% endif %}
</table>
</div>
<div style="display: block; width: 100%;">
<h3>Dependent Services</h3>
<table>
<tr>
<th>Name</th>
<th>Organization</th>
</tr>
{% if item.dependent_service.all %}
{% for service in item.dependent_service.all %}
<tr>
<td><a href="{% url 'ITIM:_service_view' service.pk %}">{{ service }}</a></td>
<td>{{ service.organization }}</td>
</tr>
{% endfor%}
{% else %}
<tr>
<td colspan="2"> Nothing Found</td>
</tr>
{% endif %}
</table>
</div>
</div>
<div id="rendered_config" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.rendered_config %}
</div>
{% endblock %}

View File

@ -0,0 +1,53 @@
{% extends 'base.html.j2' %}
{% block content %}
<input type="button" value="New Article" onclick="window.location='{% url 'ITIM:_service_add' %}';">
<table class="data">
<tr>
<th>Title</th>
<th>Cluster / Device</th>
<th>Organization</th>
<th>&nbsp;</th>
</tr>
{% if items %}
{% for item in items %}
<tr>
<td><a href="{% url 'ITIM:_service_view' pk=item.id %}">{{ item.name }}</a></td>
<td>
{% if item.device %}
{{ item.device }}
{% else %}
{{ item.cluster }}
{% endif %}
</td>
<td>{{ item.organization }}</td>
<td>&nbsp;</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">&laquo; 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 &raquo;</a>
{% endif %}
</span>
</div>
{% endblock %}

View File

@ -0,0 +1,42 @@
import pytest
import unittest
from django.test import TestCase
from access.models import Organization
from app.tests.abstract.models import TenancyModel
from itim.models.services import Port
@pytest.mark.django_db
class PortModel(
TestCase,
TenancyModel
):
model = Port
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
2. Create an item
"""
self.organization = Organization.objects.create(name='test_org')
self.item = self.model.objects.create(
organization = self.organization,
number = 1,
)
self.second_item = self.model.objects.create(
organization = self.organization,
number = 2,
)

View File

@ -0,0 +1,78 @@
import pytest
import unittest
import requests
from django.test import TestCase, Client
from access.models import Organization
from core.models.history import History
from core.tests.abstract.history_entry import HistoryEntry
from core.tests.abstract.history_entry_parent_model import HistoryEntryParentItem
from itim.models.services import Port
class PortHistory(TestCase, HistoryEntry, HistoryEntryParentItem):
model = Port
@classmethod
def setUpTestData(self):
""" Setup Test """
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.item_parent = self.model.objects.create(
number = 1,
organization = self.organization
)
self.item_create = self.model.objects.create(
number = 2,
organization = self.organization,
)
self.history_create = History.objects.get(
action = History.Actions.ADD[0],
item_pk = self.item_create.pk,
item_class = self.model._meta.model_name,
)
self.item_change = self.item_create
self.item_change.number = 3
self.item_change.save()
self.field_after_expected_value = '{"number": ' + str(self.item_change.number) + '}'
self.history_change = History.objects.get(
action = History.Actions.UPDATE[0],
item_pk = self.item_change.pk,
item_class = self.model._meta.model_name,
)
self.item_delete = self.model.objects.create(
number = 4,
organization = self.organization,
)
self.deleted_pk = self.item_delete.pk
self.item_delete.delete()
self.history_delete = History.objects.filter(
item_pk = self.deleted_pk,
item_class = self.model._meta.model_name,
)
self.history_delete_children = History.objects.filter(
item_parent_pk = self.deleted_pk,
item_parent_class = self.item_parent._meta.model_name,
)

View File

@ -0,0 +1,95 @@
# from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
import requests
from access.models import Organization, Team, TeamUsers, Permission
from itim.models.services import Port
from core.tests.abstract.history_permissions import HistoryPermissions
class PortHistoryPermissions(TestCase, HistoryPermissions):
item_model = Port
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
2. create an organization that is different to item
3. Create a device
4. Add history device history entry as item
5. create a user
6. create user in different organization (with the required permission)
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
different_organization = Organization.objects.create(name='test_different_organization')
self.item = self.item_model.objects.create(
organization=organization,
number = 1
)
self.history = self.model.objects.get(
item_pk = self.item.id,
item_class = self.item._meta.model_name,
action = self.model.Actions.ADD,
)
view_permissions = Permission.objects.get(
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = organization,
)
view_team.permissions.set([view_permissions])
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
self.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
different_organization_team = Team.objects.create(
team_name = 'different_organization_team',
organization = different_organization,
)
different_organization_team.permissions.set([
view_permissions,
])
TeamUsers.objects.create(
team = different_organization_team,
user = self.different_organization_user
)

View File

@ -0,0 +1,189 @@
# from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
import requests
from access.models import Organization, Team, TeamUsers, Permission
from app.tests.abstract.model_permissions import ModelPermissions
from itim.models.services import Port
class PortPermissions(TestCase, ModelPermissions):
model = Port
app_namespace = 'Settings'
url_name_view = '_port_view'
url_name_add = '_port_add'
url_name_change = '_port_change'
url_name_delete = '_port_delete'
url_delete_response = reverse('Settings:_ports')
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
. create an organization that is different to item
2. Create a device
3. create teams with each permission: view, add, change, delete
4. create a user per team
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
different_organization = Organization.objects.create(name='test_different_organization')
self.item = self.model.objects.create(
organization=organization,
number = 1
)
self.url_view_kwargs = {'pk': self.item.id}
# self.url_add_kwargs = {'pk': self.item.id}
self.add_data = {'device': 'device', 'organization': self.organization.id}
self.url_change_kwargs = {'pk': self.item.id}
self.change_data = {'device': 'device', 'organization': self.organization.id}
self.url_delete_kwargs = {'pk': self.item.id}
self.delete_data = {'device': 'device', 'organization': self.organization.id}
view_permissions = Permission.objects.get(
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = organization,
)
view_team.permissions.set([view_permissions])
add_permissions = Permission.objects.get(
codename = 'add_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
add_team = Team.objects.create(
team_name = 'add_team',
organization = organization,
)
add_team.permissions.set([add_permissions])
change_permissions = Permission.objects.get(
codename = 'change_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
change_team = Team.objects.create(
team_name = 'change_team',
organization = organization,
)
change_team.permissions.set([change_permissions])
delete_permissions = Permission.objects.get(
codename = 'delete_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
delete_team = Team.objects.create(
team_name = 'delete_team',
organization = organization,
)
delete_team.permissions.set([delete_permissions])
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
self.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
self.add_user = User.objects.create_user(username="test_user_add", password="password")
teamuser = TeamUsers.objects.create(
team = add_team,
user = self.add_user
)
self.change_user = User.objects.create_user(username="test_user_change", password="password")
teamuser = TeamUsers.objects.create(
team = change_team,
user = self.change_user
)
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
teamuser = TeamUsers.objects.create(
team = delete_team,
user = self.delete_user
)
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
different_organization_team = Team.objects.create(
team_name = 'different_organization_team',
organization = different_organization,
)
different_organization_team.permissions.set([
view_permissions,
add_permissions,
change_permissions,
delete_permissions,
])
TeamUsers.objects.create(
team = different_organization_team,
user = self.different_organization_user
)

View File

@ -0,0 +1,29 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import PrimaryModel
class ServiceViews(
TestCase,
PrimaryModel
):
add_module = 'itim.views.ports'
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'

View File

@ -0,0 +1,42 @@
import pytest
import unittest
from django.test import TestCase
from access.models import Organization
from app.tests.abstract.models import TenancyModel
from itim.models.services import Service
@pytest.mark.django_db
class ServiceModel(
TestCase,
TenancyModel
):
model = Service
@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',
)

View File

@ -0,0 +1,78 @@
import pytest
import unittest
import requests
from django.test import TestCase, Client
from access.models import Organization
from core.models.history import History
from core.tests.abstract.history_entry import HistoryEntry
from core.tests.abstract.history_entry_parent_model import HistoryEntryParentItem
from itim.models.services import Service
class ServiceHistory(TestCase, HistoryEntry, HistoryEntryParentItem):
model = Service
@classmethod
def setUpTestData(self):
""" Setup Test """
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.item_parent = self.model.objects.create(
name = 'test_item_parent_' + self.model._meta.model_name,
organization = self.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,
)
self.history_delete_children = History.objects.filter(
item_parent_pk = self.deleted_pk,
item_parent_class = self.item_parent._meta.model_name,
)

View File

@ -0,0 +1,95 @@
# from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
import requests
from access.models import Organization, Team, TeamUsers, Permission
from itim.models.services import Service
from core.tests.abstract.history_permissions import HistoryPermissions
class ServiceHistoryPermissions(TestCase, HistoryPermissions):
item_model = Service
@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
)

View File

@ -0,0 +1,189 @@
# from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
import requests
from access.models import Organization, Team, TeamUsers, Permission
from app.tests.abstract.model_permissions import ModelPermissions
from itim.models.services import Service
class ServicePermissions(TestCase, ModelPermissions):
model = Service
app_namespace = 'ITIM'
url_name_view = '_service_view'
url_name_add = '_service_add'
url_name_change = '_service_change'
url_name_delete = '_service_delete'
url_delete_response = reverse('ITIM:Services')
@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
)

View File

@ -0,0 +1,29 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import PrimaryModel
class ServiceViews(
TestCase,
PrimaryModel
):
add_module = 'itim.views.services'
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'

View File

@ -1,12 +1,15 @@
from django.urls import path
from itam import views
from itam.views import device, device_type, software, software_category, software_version, operating_system, operating_system_version
from itim.views import services
app_name = "ITIM"
urlpatterns = [
# path("clusters", device.IndexView.as_view(), name="Clusters"),
# path("services", device.IndexView.as_view(), name="Services"),
path("services", services.Index.as_view(), name="Services"),
path("service/add", services.Add.as_view(), name="_service_add"),
path("service/<int:pk>/edit", services.Change.as_view(), name="_service_change"),
path("service/<int:pk>/delete", services.Delete.as_view(), name="_service_delete"),
path("service/<int:pk>", services.View.as_view(), name="_service_view"),
]

188
app/itim/views/ports.py Normal file
View File

@ -0,0 +1,188 @@
from django.contrib.auth import decorators as auth_decorator
from django.urls import reverse
from django.utils.decorators import method_decorator
from core.forms.comment import AddNoteForm
from core.models.notes import Notes
from core.views.common import AddView, ChangeView, DeleteView, IndexView
from itim.forms.ports import PortForm
from itim.models.services import Port, Service
from settings.models.user_settings import UserSettings
class Add(AddView):
form_class = PortForm
model = Port
permission_required = [
'itim.add_port',
]
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:_ports')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'New Port'
return context
class Change(ChangeView):
context_object_name = "item"
form_class = PortForm
model = Port
permission_required = [
'itim.change_port',
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = str(self.object)
return context
def get_success_url(self, **kwargs):
return reverse('Settings:_port_view', args=(self.kwargs['pk'],))
class Delete(DeleteView):
model = Port
permission_required = [
'itim.delete_port',
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Delete ' + str(self.object)
return context
def get_success_url(self, **kwargs):
return reverse('Settings:_ports')
class Index(IndexView):
context_object_name = "items"
model = Port
paginate_by = 10
permission_required = [
'itim.view_port'
]
template_name = 'itim/port_index.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['model_docs_path'] = self.model._meta.app_label + self.model._meta.model_name
context['content_title'] = 'Ports'
return context
class View(ChangeView):
context_object_name = "item"
form_class = PortForm
model = Port
permission_required = [
'itim.view_port',
]
template_name = 'itim/port.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['services'] = Service.objects.filter(port=self.kwargs['pk']).order_by('name', 'organization')
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:_port_delete', args=(self.kwargs['pk'],))
context['content_title'] = self.object
return context
@method_decorator(auth_decorator.permission_required("itim.change_service", raise_exception=True))
def post(self, request, *args, **kwargs):
item = Port.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()
def get_success_url(self, **kwargs):
return reverse('Settings:_port_view', args=(self.kwargs['pk'],))

186
app/itim/views/services.py Normal file
View File

@ -0,0 +1,186 @@
from django.contrib.auth import decorators as auth_decorator
from django.urls import reverse
from django.utils.decorators import method_decorator
from core.forms.comment import AddNoteForm
from core.models.notes import Notes
from core.views.common import AddView, ChangeView, DeleteView, IndexView
from itim.forms.services import ServiceForm, DetailForm
from itim.models.services import Service
from settings.models.user_settings import UserSettings
class Add(AddView):
form_class = ServiceForm
model = Service
permission_required = [
'itim.add_service',
]
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('ITIM:Services')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'New Service'
return context
class Change(ChangeView):
form_class = ServiceForm
model = Service
permission_required = [
'itim.change_service',
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = str(self.object)
return context
def get_success_url(self, **kwargs):
return reverse('ITIM:_service_view', args=(self.kwargs['pk'],))
class Delete(DeleteView):
model = Service
permission_required = [
'itim.delete_service',
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Delete ' + str(self.object)
return context
def get_success_url(self, **kwargs):
return reverse('ITIM:Services')
class Index(IndexView):
context_object_name = "items"
model = Service
paginate_by = 10
permission_required = [
'itim.view_service'
]
template_name = 'itim/service_index.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['model_docs_path'] = self.model._meta.app_label + '/' + self.model._meta.model_name
context['content_title'] = 'Services'
return context
class View(ChangeView):
context_object_name = "item"
form_class = DetailForm
model = Service
permission_required = [
'itim.view_service',
]
template_name = 'itim/service.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(service=self.kwargs['pk'])
context['model_pk'] = self.kwargs['pk']
context['model_name'] = self.model._meta.model_name
context['model_delete_url'] = reverse('ITIM:_service_delete', args=(self.kwargs['pk'],))
context['content_title'] = self.object.name
return context
@method_decorator(auth_decorator.permission_required("itim.change_service", raise_exception=True))
def post(self, request, *args, **kwargs):
item = Service.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.service = item
notes.instance.organization = item.organization
notes.save()
def get_success_url(self, **kwargs):
return reverse('ITIM:_service_view', args=(self.kwargs['pk'],))

View File

@ -31,6 +31,10 @@ h2 {
padding-left: 50px
}
.codehilite {
display: inline;
}
span#content_header_icon {
float: right;
width: 30px;
@ -52,6 +56,7 @@ span.icon-text {
padding-right: 10px;
height: 30px;
display: inline-block;
margin-left: 5px;
}
span.icon-text a {
@ -142,6 +147,16 @@ span.icon-issue {
display: inline-block;
}
span.icon-external-link {
height: 30px;
line-height: 30px;
margin: 0px;
padding: 0px;
vertical-align: middle;
display: inline-block;
width: 25px;
}
/* .icon {
display: block;
content: none;

View File

@ -0,0 +1,75 @@
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.codehilite .hll { background-color: #ffffcc }
.codehilite { background: #f8f8f8; }
.codehilite .c { color: #3D7B7B; font-style: italic } /* Comment */
.codehilite .err { border: 1px solid #FF0000 } /* Error */
.codehilite .k { color: #008000; font-weight: bold } /* Keyword */
.codehilite .o { color: #666666 } /* Operator */
.codehilite .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */
.codehilite .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */
.codehilite .cp { color: #9C6500 } /* Comment.Preproc */
.codehilite .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */
.codehilite .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */
.codehilite .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */
.codehilite .gd { color: #A00000 } /* Generic.Deleted */
.codehilite .ge { font-style: italic } /* Generic.Emph */
.codehilite .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.codehilite .gr { color: #E40000 } /* Generic.Error */
.codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */
.codehilite .gi { color: #008400 } /* Generic.Inserted */
.codehilite .go { color: #717171 } /* Generic.Output */
.codehilite .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
.codehilite .gs { font-weight: bold } /* Generic.Strong */
.codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
.codehilite .gt { color: #0044DD } /* Generic.Traceback */
.codehilite .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
.codehilite .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
.codehilite .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
.codehilite .kp { color: #008000 } /* Keyword.Pseudo */
.codehilite .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
.codehilite .kt { color: #B00040 } /* Keyword.Type */
.codehilite .m { color: #666666 } /* Literal.Number */
.codehilite .s { color: #BA2121 } /* Literal.String */
.codehilite .na { color: #687822 } /* Name.Attribute */
.codehilite .nb { color: #008000 } /* Name.Builtin */
.codehilite .nc { color: #0000FF; font-weight: bold } /* Name.Class */
.codehilite .no { color: #880000 } /* Name.Constant */
.codehilite .nd { color: #AA22FF } /* Name.Decorator */
.codehilite .ni { color: #717171; font-weight: bold } /* Name.Entity */
.codehilite .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */
.codehilite .nf { color: #0000FF } /* Name.Function */
.codehilite .nl { color: #767600 } /* Name.Label */
.codehilite .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
.codehilite .nt { color: #008000; font-weight: bold } /* Name.Tag */
.codehilite .nv { color: #19177C } /* Name.Variable */
.codehilite .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
.codehilite .w { color: #bbbbbb } /* Text.Whitespace */
.codehilite .mb { color: #666666 } /* Literal.Number.Bin */
.codehilite .mf { color: #666666 } /* Literal.Number.Float */
.codehilite .mh { color: #666666 } /* Literal.Number.Hex */
.codehilite .mi { color: #666666 } /* Literal.Number.Integer */
.codehilite .mo { color: #666666 } /* Literal.Number.Oct */
.codehilite .sa { color: #BA2121 } /* Literal.String.Affix */
.codehilite .sb { color: #BA2121 } /* Literal.String.Backtick */
.codehilite .sc { color: #BA2121 } /* Literal.String.Char */
.codehilite .dl { color: #BA2121 } /* Literal.String.Delimiter */
.codehilite .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
.codehilite .s2 { color: #BA2121 } /* Literal.String.Double */
.codehilite .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */
.codehilite .sh { color: #BA2121 } /* Literal.String.Heredoc */
.codehilite .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */
.codehilite .sx { color: #008000 } /* Literal.String.Other */
.codehilite .sr { color: #A45A77 } /* Literal.String.Regex */
.codehilite .s1 { color: #BA2121 } /* Literal.String.Single */
.codehilite .ss { color: #19177C } /* Literal.String.Symbol */
.codehilite .bp { color: #008000 } /* Name.Builtin.Pseudo */
.codehilite .fm { color: #0000FF } /* Name.Function.Magic */
.codehilite .vc { color: #19177C } /* Name.Variable.Class */
.codehilite .vg { color: #19177C } /* Name.Variable.Global */
.codehilite .vi { color: #19177C } /* Name.Variable.Instance */
.codehilite .vm { color: #19177C } /* Name.Variable.Magic */
.codehilite .il { color: #666666 } /* Literal.Number.Integer.Long */

View File

@ -65,6 +65,119 @@ input[type=submit] {
height: 30px;
margin: 10px;
}
/* Style the navigation tabs at the top of a content page */
.content-navigation-tabs {
display: block;
overflow: hidden;
border-bottom: 1px solid #ccc;
/* background-color: #f1f1f1; */
width: 100%;
text-align: left;
padding: 0px;
margin: 0px
}
.content-navigation-tabs-link {
border: 0px;
margin: none;
padding: none;
}
/* Style the buttons that are used to open the tab content */
.content-navigation-tabs button {
display: inline;
background-color: inherit;
float: left;
border: none;
outline: none;
cursor: pointer;
margin: 0px;
padding: 0px;
padding: 14px 16px;
transition: 0.3s;
font-size: inherit;
color: #6a6e73;
}
/* Change background color of buttons on hover */
.content-navigation-tabs button:hover {
/* background-color: #ddd; */
border-bottom: 3px solid #ccc;
}
/* Create an active/current tablink class */
.content-navigation-tabs button.active {
/* background-color: #ccc; */
border-bottom: 3px solid #177ee6;
}
/* Style content for each tab */
.content-tab {
width: 100%;
display: none;
padding-bottom: 0px;
border: none;
border-top: none;
}
.content-tab hr {
border: none;
border-top: 1px solid #ccc;
}
.content-tab pre {
word-wrap: break-word;
white-space: pre-wrap;
}
/* Style for section fields on details page */
.detail-view-field {
display: unset;
height: 30px;
line-height: 30px;
padding: 0px 20px 40px 20px;
}
.detail-view-field label {
display: inline-block;
font-weight: bold;
width: 200px;
margin: 10px;
height: 30px;
line-height: 30px;
}
.detail-view-field span {
display: inline-block;
width: 340px;
margin: 10px;
border-bottom: 1px solid #ccc;
height: 30px;
line-height: 30px;
}
/*******************************************************************************
EoF refactored
@ -124,61 +237,6 @@ input[type=checkbox]:checked::after {
/* Style the tab */
.tab {
display: block;
overflow: hidden;
border-bottom: 1px solid #ccc;
/* background-color: #f1f1f1; */
width: 100%;
text-align: left;
padding: 0px;
margin: 0px
}
.tablinks {
border: 0px;
margin: none;
padding: none;
}
/* Style the buttons that are used to open the tab content */
.tab button {
display: inline;
background-color: inherit;
float: left;
border: none;
outline: none;
cursor: pointer;
margin: 0px;
padding: 0px;
padding: 14px 16px;
transition: 0.3s;
font-size: inherit;
color: #6a6e73;
}
/* Change background color of buttons on hover */
.tab button:hover {
/* background-color: #ddd; */
border-bottom: 3px solid #ccc;
}
/* Create an active/current tablink class */
.tab button.active {
/* background-color: #ccc; */
border-bottom: 3px solid #177ee6;
}
/* Style the tab content */
.tabcontent {
width: 100%;
display: none;
/* padding: 6px 12px; */
padding-bottom: 0px;
border: none;
border-top: none;
}
table {
width: 100%;

View File

@ -0,0 +1,16 @@
function openContentNavigationTab(evt, TabName) {
var i, tabcontent, tablinks;
tabcontent = document.getElementsByClassName("content-tab");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("content-navigation-tabs-link");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(TabName).style.display = "block";
evt.currentTarget.className += " active";
}

View File

@ -0,0 +1,21 @@
from django import forms
from django.db.models import Q
from django.contrib.auth.models import User
from access.models import Organization, TeamUsers
from core.forms.common import CommonModelForm
from settings.models.external_link import ExternalLink
class ExternalLinksForm(CommonModelForm):
prefix = 'external_links'
class Meta:
fields = '__all__'
model = ExternalLink

View File

@ -0,0 +1,37 @@
# Generated by Django 5.0.7 on 2024-07-17 05:02
import access.fields
import access.models
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0001_initial'),
('settings', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='ExternalLink',
fields=[
('is_global', models.BooleanField(default=False)),
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('name', models.CharField(help_text='Name to display on link button', max_length=30, unique=True, verbose_name='Button Name')),
('template', models.CharField(help_text='External Link template', max_length=180, verbose_name='Link Template')),
('colour', models.CharField(blank=True, default=None, help_text='Colour to render the link button. Use HTML colour code', max_length=80, null=True, verbose_name='Button Colour')),
('devices', models.BooleanField(default=False, help_text='Render link for devices', verbose_name='Devices')),
('software', models.BooleanField(default=False, help_text='Render link for software', verbose_name='Software')),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
],
options={
'abstract': False,
},
),
]

View File

View File

@ -0,0 +1,66 @@
from django.template import Template
from access.fields import *
from access.models import TenancyObject
class ExternalLink(TenancyObject):
id = models.AutoField(
primary_key=True,
unique=True,
blank=False
)
name = models.CharField(
blank = False,
max_length = 30,
unique = True,
help_text = 'Name to display on link button',
verbose_name = 'Button Name',
)
slug = None
template = models.CharField(
blank = False,
max_length = 180,
unique = False,
help_text = 'External Link template',
verbose_name = 'Link Template',
)
colour = models.CharField(
blank = True,
null = True,
default = None,
max_length = 80,
unique = False,
help_text = 'Colour to render the link button. Use HTML colour code',
verbose_name = 'Button Colour',
)
devices = models.BooleanField(
default = False,
blank = False,
help_text = 'Render link for devices',
verbose_name = 'Devices',
)
software = models.BooleanField(
default = False,
blank = False,
help_text = 'Render link for software',
verbose_name = 'Software',
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
def __str__(self):
""" Return the Template to render """
return str(self.template)

View File

@ -0,0 +1,18 @@
<style>
.inner-text {
background-color: #fff;
border-top-right-radius: 15px;
border-bottom-right-radius: 15px;
border-right: 15px;
margin-right: -5px;
padding: 1px 5px 1px 5px;
}
</style>
<span class="icon-text external-link" style="background-color: {% if external_link.colour %}{{ external_link.colour }}{% else %}#177ee6{% endif %};">
<span class="icon-external-link" style="margin-left: 5px;">
{% include 'icons/link.svg' %}
</span>
<a class="inner-text" href="{{ external_link.link }}" target="_blank"> {{ external_link.name }}</a>
</span>

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10.59,13.41C11,13.8 11,14.44 10.59,14.83C10.2,15.22 9.56,15.22 9.17,14.83C7.22,12.88 7.22,9.71 9.17,7.76V7.76L12.71,4.22C14.66,2.27 17.83,2.27 19.78,4.22C21.73,6.17 21.73,9.34 19.78,11.29L18.29,12.78C18.3,11.96 18.17,11.14 17.89,10.36L18.36,9.88C19.54,8.71 19.54,6.81 18.36,5.64C17.19,4.46 15.29,4.46 14.12,5.64L10.59,9.17C9.41,10.34 9.41,12.24 10.59,13.41M13.41,9.17C13.8,8.78 14.44,8.78 14.83,9.17C16.78,11.12 16.78,14.29 14.83,16.24V16.24L11.29,19.78C9.34,21.73 6.17,21.73 4.22,19.78C2.27,17.83 2.27,14.66 4.22,12.71L5.71,11.22C5.7,12.04 5.83,12.86 6.11,13.65L5.64,14.12C4.46,15.29 4.46,17.19 5.64,18.36C6.81,19.54 8.71,19.54 9.88,18.36L13.41,14.83C14.59,13.66 14.59,11.76 13.41,10.59C13,10.2 13,9.56 13.41,9.17Z" /></svg>

After

Width:  |  Height:  |  Size: 795 B

View File

@ -0,0 +1,194 @@
{% extends 'base.html.j2' %}
{% load markdown %}
{% block title %}{{ externallink.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 'Settings:External Links' %}';"
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 External Links</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
<button id="NotesOpen" class="tablinks" onclick="openCity(event, 'Notes')">Notes</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;
border-bottom: 1px solid #ccc;
height: 30px;
line-height: 30px;
}
</style>
<form action="" method="post">
{% csrf_token %}
<div id="Details" class="tabcontent">
<h3>
Details
{% for external_link in external_links %}
<span style="font-weight: normal; float: right;">{% include 'icons/external_link.html.j2' with external_link=external_link %}</span>
{% endfor %}
</h3>
<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.organization.label }}</label>
<span>{{ externallink.organization }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.name.label }}</label>
<span>{{ form.name.value }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.template.label }}</label>
<span>{{ externallink.template }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.colour.label }}</label>
<span>
{% if form.colour.value %}
{{ form.colour.value }}
{% else %}
&nbsp;
{% endif %}
</span>
</div>
<div class="detail-view-field">
<label>{{ form.devices.label }}</label>
<span> {{ form.devices.value }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.software.label }}</label>
<span>{{ externallink.software }}</span>
</div>
<div class="detail-view-field">
<label>Created</label>
<span>{{ externallink.created }}</span>
</div>
<div class="detail-view-field">
<label>Modified</label>
<span>{{ externallink.modified }}</span>
</div>
</div>
<div style="display: inline; width: 40%; margin: 30px; text-align: left;">
<div>
<label style="font-weight: bold; width: 100%; border-bottom: 1px solid #ccc; display: block; text-align: inherit;">{{ form.model_notes.label }}</label>
<div style="display: inline-block; text-align: left;">
{% if form.model_notes.value %}
{{ form.model_notes.value | markdown | safe }}
{% else %}
&nbsp;
{% endif %}
</div>
</div>
</div>
</div>
<input type="button" value="Edit" onclick="window.location='{% url 'Settings:_external_link_change' externallink.id %}';">
{% if not tab %}
<script>
// Get the element with id="defaultOpen" and click on it
document.getElementById("defaultOpen").click();
</script>
{% endif %}
</div>
<div id="Notes" class="tabcontent">
<h3>
Notes
</h3>
{{ notes_form }}
<input type="submit" name="{{notes_form.prefix}}" value="Submit" />
<div class="comments">
{% if notes %}
{% for note in notes%}
{% include 'note.html.j2' %}
{% endfor %}
{% endif %}
</div>
{% if tab == 'notes' %}
<script>
// Get the element with id="defaultOpen" and click on it
document.getElementById("NotesOpen").click();
</script>
{% endif %}
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,42 @@
{% extends 'base.html.j2' %}
{% block content_header_icon %}{% endblock %}
{% block content %}
<input type="button" value="New External Link" onclick="window.location='{% url 'Settings:_external_link_add' %}';">
<table class="data">
<tr>
<th>Name</th>
<th>Organization</th>
<th>&nbsp;</th>
</tr>
{% for item in list %}
<tr>
<td><a href="{% url 'Settings:_external_link_view' pk=item.id %}">{{ item.name }}</a></td>
<td>{% if item.is_global %}Global{% else %}{{ item.organization }}{% endif %}</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
</table>
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page=1">&laquo; 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 &raquo;</a>
{% endif %}
</span>
</div>
{% endblock %}

View File

@ -59,6 +59,13 @@ div#content article h3 {
</ul>
</article>
<article style="">
<h3>ITIM</h3>
<ul>
<li><a href="{% url 'Settings:_ports' %}">Service Ports</a></li>
</ul>
</article>
</div>

View File

@ -0,0 +1,18 @@
import pytest
import unittest
import requests
from django.test import TestCase, Client
from app.tests.abstract.models import TenancyModel
from settings.models.external_link import ExternalLink
class ExternalLinkTests(
TestCase,
TenancyModel,
):
model = ExternalLink

View File

@ -0,0 +1,72 @@
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 settings.models.external_link import ExternalLink
class ExternalLinkHistory(TestCase, HistoryEntry, HistoryEntryParentItem):
model = ExternalLink
@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": "test_item_' + self.model._meta.model_name + '_changed"}'
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,
)
self.history_delete_children = History.objects.filter(
item_parent_pk = self.deleted_pk,
item_parent_class = self.model._meta.model_name,
)

Some files were not shown because too many files have changed in this diff Show More