Compare commits

...

233 Commits

Author SHA1 Message Date
04780767c0 build: bump version 1.15.0 -> 1.15.1 2025-04-10 05:12:45 +00:00
Jon
2d8275d2af fix(python): Downgrade django 5.2 -> 5.1.8
5.2 removes support for postgres 13

ref: #717
2025-04-10 14:20:27 +09:30
db25eabfdd build: bump version 1.14.0 -> 1.15.0 2025-04-10 03:21:54 +00:00
Jon
71bfb5b9e4 Merge pull request #702 from nofusscomputing/feature-next-release 2025-04-10 12:24:17 +09:30
Jon
3146417001 chore: update release notes
ref: #702
2025-04-10 12:09:41 +09:30
Jon
32d97932d1 chore: squash migrations
reduces the amount required

ref: #702
2025-04-10 12:05:43 +09:30
Jon
32251d0a08 fix(api): Correct documentation link to use models verbose name
ref: #702
2025-04-09 14:41:10 +09:30
Jon
b694df0330 test(settings): Correct nav menu entry for Ticket Category and Ticket Comment Category
ref: #702 #565 #566
2025-04-09 13:59:05 +09:30
Jon
52ffb58276 feat(settings): Move Ticket Comment Category from settings to ITOps menu
ref: #702 closes #566
2025-04-09 13:38:03 +09:30
Jon
9ebc65f3b3 feat(settings): Move Ticket Category from settings to ITOps menu
ref: #702 closes #565
2025-04-09 13:37:27 +09:30
Jon
bdc4066a17 chore(accounting): add feature Flag 2025-00004 to settings ready for use
ref: #702 #714
2025-04-09 12:35:16 +09:30
Jon
ee018802a9 feat(access): place roles nav behind feature flag 2025-00003
ref: #702 #551
2025-04-09 12:31:47 +09:30
Jon
d4221913d2 feat(access): place directory nav behind feature flag 2025-00002
ref: #702 #704
2025-04-09 12:31:00 +09:30
Jon
ab4ebdab24 feat(accounting): add new module
ref: #702 closes #714
2025-04-09 12:28:54 +09:30
Jon
a18d9c2789 chore: add squash link to pr template
ref: #702
2025-04-09 12:12:16 +09:30
Jon
f2b13e5a0c fix(feature_flag): cater for settings flag overrides
ref: #702
2025-04-09 11:22:05 +09:30
Jon
3763eb8727 Merge pull request #713 from nofusscomputing/tests-entities-person-contact 2025-04-08 23:36:57 +09:30
Jon
b99b38f623 test(access): Ensure Model Contacts inherits from Person Model
ref: #713 closes #703
2025-04-08 22:56:42 +09:30
Jon
7aae162453 test(access): Functional Test Suite for Contact API Metadata, API Permissions and ViewSet
ref: #713 #703
2025-04-08 22:51:00 +09:30
Jon
aad1407647 test(access): Functional Test Suite for Contact serializer
ref: #713 #703
2025-04-08 22:50:34 +09:30
Jon
79efc2258f test(access): Functional Test Suite for Contact history
ref: #713 #703
2025-04-08 22:50:24 +09:30
Jon
8b32faa736 test(access): Correct Entity and person functional Test Suite so sub-model testing works
ref: #713 #703
2025-04-08 22:49:49 +09:30
Jon
3b04b0c9a4 test(access): Correct table_fields test case to cater for dynamic field
ref: #713 #703
2025-04-08 22:49:16 +09:30
Jon
4518449232 test(access): Unit Test for Contact ViewSet
ref: #713 #703
2025-04-08 21:55:22 +09:30
Jon
6d708d0c00 test(access): Unit Test for Contact model
ref: #713 #703
2025-04-08 21:55:10 +09:30
Jon
5bf89e1ca6 test(access): Unit Test for Contact history API field checks
ref: #713 #703
2025-04-08 21:55:04 +09:30
Jon
23213c148c test(access): Unit Test for Contact API field checks
ref: #713 #703
2025-04-08 21:54:57 +09:30
Jon
dd4175a6ba test(access): Unit Test for Person Tenancy Object
ref: #713 #703
2025-04-08 21:53:41 +09:30
Jon
1d9ce9d310 test(access): Correct Entity and person unit Test Suite so sub-model testing works
ref: #713 #703
2025-04-08 21:53:21 +09:30
Jon
813470f084 fix(access): Add missing field directory to contact model
ref: #713 #703
2025-04-08 21:52:07 +09:30
Jon
bb27bcc280 feat(access): Ensure that the same person cant be created more than once
based off of combined fields f_name, l_name and dob (if dob is not empty)

ref: #713 #571
2025-04-08 21:09:57 +09:30
Jon
1b53faf9ce test(access): Entity Function Serializer test cases
ref: #713 #571
2025-04-08 21:07:57 +09:30
Jon
33d20a4c0a test(access): Person Model field test cases
ref: #713 #571
2025-04-08 19:43:01 +09:30
Jon
fe7e2b9d22 test(access): Functional Test for Person ViewSet, Permissions and Metadata
ref: #713 closes #571
2025-04-08 19:10:01 +09:30
Jon
c563c07345 test(access): Functional Test for Person History
ref: #713 #571
2025-04-08 19:07:58 +09:30
Jon
3b72df61a4 test(access): Correct Entity Function Test Suite so sub-model testing works
ref: #713 #571
2025-04-08 19:07:41 +09:30
Jon
c3da9503d2 test(access): Unit Test for Person ViewSet
ref: #713 #571
2025-04-08 17:53:59 +09:30
Jon
c42037cef6 test(access): Unit Test for Person Model
ref: #713 #571
2025-04-08 17:53:40 +09:30
Jon
192f46022c test(access): Unit Test for Person History API fields
ref: #713 #571
2025-04-08 17:53:32 +09:30
Jon
81860cac84 test(access): Unit Test for Person API fields
ref: #713 #571
2025-04-08 17:53:16 +09:30
Jon
b66feadb5a test(access): Unit Test for Person Tenancy Object
ref: #713 #571
2025-04-08 17:52:50 +09:30
Jon
b01a2a9a47 test(access): Correct Entity Test Suite so sub-model testing works
ref: #713 #571
2025-04-08 17:49:27 +09:30
Jon
f7d3cdd463 test(app): exclude any field check that ends in _ptr_id
this field is for linking tables and a django field

ref: #713
2025-04-08 17:31:18 +09:30
Jon
3bb63b2a5b Merge pull request #712 from nofusscomputing/model-roles 2025-04-08 16:16:51 +09:30
Jon
05ec53331a fix(settings): Add Application Settings to Admin page
Allows the webmaster to set application settings

ref: #712 fixes #440
2025-04-08 16:00:54 +09:30
Jon
6aebde7845 test(access): Remove teardown from Function Test cases for Role serializer
ref: #712 #683
2025-04-08 15:22:49 +09:30
Jon
4de24a7d88 docs(user): Roles Initial
ref: #712 closes #683
2025-04-07 14:21:19 +09:30
Jon
322b7a1c41 test(access): Test cases for Role serializer
ref: #712 #683
2025-04-07 14:07:08 +09:30
Jon
825683e162 test(access): Function Test cases for Role SPI Permissions, ViewSet and Metadata
ref: #712 #683
2025-04-06 15:17:19 +09:30
Jon
6bb5a47dd3 test(access): Function Test cases for Role History
ref: #712 #683
2025-04-06 15:16:27 +09:30
Jon
47381c7bf7 test(access): Unit Test case to ensure Role is by organization
ref: #712 #683
2025-04-06 14:51:03 +09:30
Jon
8b08d03d95 test(access): Unit Test case to ensure Role cant be set as global object
ref: #712 #683
2025-04-06 14:46:49 +09:30
Jon
360bf60578 test(access): Unit Test cases for Role ViewSet
ref: #712 #683
2025-04-06 14:41:29 +09:30
Jon
c8c2fcabd2 test(access): Unit Test cases for Role model
ref: #712 #683
2025-04-06 14:41:15 +09:30
Jon
1830b86309 test(access): Unit Test cases for Role History API v2
ref: #712 #683
2025-04-06 14:41:07 +09:30
Jon
9e712d3624 test(access): Unit Test cases for Role API v2
ref: #712 #683
2025-04-06 14:40:58 +09:30
Jon
a2a79be7c1 test(access): Unit Test cases for Role Tenancy Object
ref: #712 #683
2025-04-06 14:40:47 +09:30
Jon
d5a2adc3a9 feat(access): Place Roles Model behind feature flag 2025-00003
ref: #712 #683 #551
2025-04-06 14:13:19 +09:30
Jon
779335458e docs(github): model issue template updated to remove model_tag requirement that is not required
ref: #712
2025-04-06 14:01:15 +09:30
Jon
8bc0b3338c refactor(core): When saving history, ensure field _prefetched_objects_cache is not included
ref: #712 #683
2025-04-06 13:51:08 +09:30
Jon
9d8bcff2a0 feat(access): When querying permissions, select related field content_type
speeds up query and reduces required sql queries

ref: #712 #683
2025-04-06 13:50:24 +09:30
Jon
8a8023f510 feat(core): Model tag for Access/Role
ref: #712 #683
2025-04-06 13:49:20 +09:30
Jon
7289758ed9 feat(access): Model Role notes endpoint
ref: #712 #683
2025-04-06 13:46:21 +09:30
Jon
d7405a500d feat(access): Add navigation entry for roles
ref: #712 #683
2025-04-06 13:45:52 +09:30
Jon
d63c249120 feat(access): Model Role History migrations
ref: #712 #683
2025-04-06 13:45:18 +09:30
Jon
e920a66a47 feat(access): Add model Role History
ref: #712 #683
2025-04-06 13:45:07 +09:30
Jon
cb4a12f2a0 feat(access): Role Notes model viewset
ref: #712 #683
2025-04-06 13:44:26 +09:30
Jon
d8263b36f7 feat(access): Role Notes model serializer
ref: #712 #683
2025-04-06 13:44:11 +09:30
Jon
74482956a4 feat(access): Model Role Notes migrations
ref: #712 #683
2025-04-06 13:43:58 +09:30
Jon
1bc199493c feat(access): Add model Role Notes
ref: #712 #683
2025-04-06 13:43:47 +09:30
Jon
5cccca865e feat(access): Role model viewset
ref: #712 #683
2025-04-06 13:43:20 +09:30
Jon
03d258ca57 feat(access): Role model serializer
ref: #712 #683
2025-04-06 13:40:16 +09:30
Jon
dfea1fdba0 feat(access): Model Role migrations
ref: #712 #683
2025-04-06 13:39:37 +09:30
Jon
edff3eb889 feat(access): Add model Role
ref: #712 #683
2025-04-06 13:38:38 +09:30
Jon
bdd55a4df6 Merge pull request #707 from nofusscomputing/new-model-entity 2025-04-04 15:53:04 +09:30
Jon
ca0bb96808 test: During testing add debug_feature_flags so object behind can be tested
ref: #707
2025-04-04 14:56:22 +09:30
Jon
5b0d9d8d81 feat(python): Upgrade Django 5.1.7 -> 5.2
ref: #707
2025-04-04 13:39:45 +09:30
Jon
dd264920a6 feat(access): Place Entity URLs behind feature flag 2025-00002
ref: #707 #704 closes #706
2025-04-04 13:30:49 +09:30
Jon
050b2d7602 test(access): Notes ViewSet Functional Tests for Entity Model
ref: #707 #706
2025-04-04 13:06:27 +09:30
Jon
ea7c359cc8 test(access): Notes API Field Functional Tests for Entity Model
ref: #707 #706
2025-04-04 12:47:04 +09:30
Jon
b3391c7e3e test(access): Correct functional ViewSet test suite for Entity model
ref: #707 #706
2025-04-04 12:11:08 +09:30
Jon
22e369aa3e test(access): History functional Tests for Entity model
ref: #707 #706
2025-04-04 12:02:55 +09:30
Jon
d0f710d9f8 test(access): PermissionsAPI, ViewSet and Metadata Tests for Entity model
ref: #707 #706
2025-04-04 11:39:06 +09:30
Jon
bd58d1d9cf feat(access): Add detail page layout for contact model
ref: #707 #705
2025-04-04 11:27:53 +09:30
Jon
184820780a feat(access): Add Menu entry for corporate directory
ref: #707 #705
2025-04-04 11:27:24 +09:30
Jon
6941668709 feat(access): Add back_url to Entity metadata
ref: #707 #705
2025-04-04 11:26:55 +09:30
Jon
f2cdd403e3 fix(access): Remove app_namespace from Entity
ref: #707 #706
2025-04-04 11:26:10 +09:30
Jon
cb43c56efb docs(user): Add initial contacts
ref: #707 #705
2025-04-04 11:25:33 +09:30
Jon
9bbfbbba4b feat(core): Add Entity model tag
ref: #707 #706
2025-04-04 07:46:08 +09:30
Jon
296188b202 feat(access): Update Entity field entity_type if it does not match the entity type
ref: #707 #706
2025-04-03 19:56:17 +09:30
Jon
de26f4b4e9 docs(development): Entity
ref: #707 #706
2025-04-03 19:29:41 +09:30
Jon
600a12177f feat(access): All Entity models to use the entity history endpoint
ref: #707 #706
2025-04-03 18:33:14 +09:30
Jon
167f4140ba feat(access): Enable specifying the history model to use for audit history for a model
ref: #707
2025-04-03 18:32:50 +09:30
Jon
2d6c347859 fix(access): add missing tenancy object fields to non-tenancy object models
ref: #707
2025-04-03 18:31:51 +09:30
Jon
2998f48e33 fix(core): Dont attempt to fetch history related objects if no history exists
ref: #707
2025-04-03 18:25:08 +09:30
Jon
03a9582703 feat(access): Enable specifying the kb model to use for linking kb article to a model
ref: #707
2025-04-03 18:11:03 +09:30
Jon
3417e37317 feat(access): All Entity models to use the entity notes endpoint
ref: #707 #706
2025-04-03 18:09:48 +09:30
Jon
aba9f44552 feat(access): Enable specifying the notes basename for a model
ref: #707
2025-04-03 17:44:18 +09:30
Jon
569a256dd5 feat(access): ViewSet for Entity Notes model
ref: #707 #706
2025-04-03 17:23:28 +09:30
Jon
22785a095d feat(access): Serializer for Entity Notes model
ref: #707 #706
2025-04-03 17:22:45 +09:30
Jon
209e67e0b1 feat(access): new model Entity Notes
ref: #707 #706
2025-04-03 17:22:28 +09:30
Jon
b50b0bbd8b test(access): Model test cases for Entity
ref: #707 #706
2025-04-03 17:00:00 +09:30
Jon
bbbf7cb38a test(access): API Rendering test cases for Entity model
ref: #707 #706
2025-04-03 17:00:00 +09:30
Jon
3624885f1e test(api): Ensure that when mocking the request the viewset is instantiated
ref: #707
2025-04-03 17:00:00 +09:30
Jon
1f817109ae test(access): History tests for Entity model
ref: #707 #706
2025-04-03 17:00:00 +09:30
Jon
711c546e69 test(access): ViewSet tests for Entity model
ref: #707 #706
2025-04-03 17:00:00 +09:30
Jon
11e32435a2 test(access): Tenancy object test for Entity model
ref: #707 #706
2025-04-03 17:00:00 +09:30
Jon
8c0b9bf182 feat(access): New model Entity History
ref: #707 #706
2025-04-03 17:00:00 +09:30
Jon
44ec81c3ae feat(access): Add Entity URL routes
ref: #707 #706
2025-04-03 17:00:00 +09:30
Jon
24132a0b1c fix(api): Dont attempt to access kwargs if not exists within common serializer
ref: #707
2025-04-03 17:00:00 +09:30
Jon
b887bb6169 feat(access): new serializer Contact
ref: #707 #703
2025-04-03 16:59:59 +09:30
Jon
93ce31d2cf feat(access): new model Contact
ref: #707 #703
2025-04-03 16:59:59 +09:30
Jon
7f4ff50ceb feat(access): new serializer Person
ref: #707 #571
2025-04-03 16:59:59 +09:30
Jon
534dab2ca6 feat(access): new model Person
ref: #707 #571
2025-04-03 16:59:59 +09:30
Jon
705a775ddd feat(access): new ViewSet for for Entity and sub-entities
ref: #707 #706
2025-04-03 16:59:59 +09:30
Jon
7e79385558 feat(access): new serializer Entity
ref: #707 #706
2025-04-03 16:59:59 +09:30
Jon
798cfbe975 feat(access): new model Entity
ref: #707 #706
2025-04-03 16:59:59 +09:30
Jon
bfefbae686 docs: update roadmap
ref: #707
2025-04-03 13:07:16 +09:30
Jon
8363cd379f chore: update issue template for new model
ref: #702
2025-03-29 15:46:55 +09:30
Jon
9496ae6aaf feat(human_resources): Add navigation menu entry for Human Resources (HR)
ref: #702 #569
2025-03-29 15:24:56 +09:30
Jon
5208340370 feat(human_resources): Add module Human Resources (HR) to API Urls
ref: #702 #569
2025-03-29 15:18:37 +09:30
Jon
9b58e5913f feat(base): Add module Human Resources (HR) to installed apps
ref: #702 #569
2025-03-29 15:18:13 +09:30
Jon
748dbea515 feat: Add module Human Resources (HR)
ref: #702 closes #569
2025-03-29 15:10:58 +09:30
b7880de54d build: bump version 1.13.1 -> 1.14.0 2025-03-29 05:22:02 +00:00
Jon
69b727a06c Merge pull request #690 from nofusscomputing/feature-next-release 2025-03-29 14:29:23 +09:30
Jon
26cdd7495a docs(administration): Add reverse proxy paths for containers
ref: #690 closes #674
2025-03-29 13:57:35 +09:30
Jon
e2182fe37e Merge pull request #696 from nofusscomputing/test-viewset 2025-03-29 13:40:57 +09:30
Jon
e8b30796ab docs(development): update testing docs to reflect change in writing tests
ref: #696 #672
2025-03-29 13:14:38 +09:30
Jon
36f314fc6f test(api): Correct test cases for view_name and view_description
cheking type is N/A due to those attributes being set via a getter

ref: #696 #672
2025-03-29 12:35:45 +09:30
Jon
1d1c76e033 test: Refactor all ViewSet Unit Test cases to use new test cases class
ref: #696 closes #672
2025-03-25 02:22:47 +09:30
Jon
e8bc98c315 test(api): Common ViewSet classes Tests and Test cases for classes that inherit them
migrated old viewset test cases to this test file so all common viewset classes
are tested in the one location.

ref: #696 #672
2025-03-25 02:22:26 +09:30
Jon
853906e9ee Merge pull request #694 from nofusscomputing/new-module-itops 2025-03-23 11:06:46 +09:30
Jon
403b6be252 feat(itops): Add navigation menu
ref: #693 closes #567
2025-03-23 10:41:26 +09:30
Jon
ac78032cca feat: New Module ITOps
ref: #694 #567
2025-03-23 10:40:21 +09:30
Jon
08fd187692 Merge pull request #693 from nofusscomputing/model-git-repository 2025-03-23 08:09:03 +09:30
Jon
8bd90df582 feat(devops): Ensure GitHub Groups can't be nested
ref: #693 #515 closes #249
2025-03-23 07:55:47 +09:30
Jon
85a2779563 feat(devops): Models Git Repository must use organization from git_group as must group if parent set
ref: #693 #515
2025-03-23 07:47:05 +09:30
Jon
9cc5db7869 refactor(devops): remove model unique_together constraint for git group and repository
this field is used for sync only

ref: #693 #515
2025-03-23 07:19:49 +09:30
Jon
e65b2531ed refactor(devops): Field provider_id must not be user editable for git group or repository
this field is used for sync only

ref: #693 #515
2025-03-23 07:19:19 +09:30
Jon
3a9198f63c fix(devops): Correct git_group serializer parameter name
ref: #693 #515
2025-03-22 21:25:51 +09:30
Jon
4d8fc508d4 fix(devops): Correct field path to no be unique for git_repository
ref: #693 #515
2025-03-22 21:25:28 +09:30
Jon
67b0187a58 test(api): correct nav menu setup to use mock request
ref: #693
2025-03-22 21:24:40 +09:30
Jon
12ef8918ba fix(feature_flag): if over_rides not set ensure val set to empty dict
ref: #693
2025-03-22 20:53:17 +09:30
Jon
5f3990e15a feat(devops): Add git provider badge to git_group table fields
ref: #693
2025-03-22 20:45:13 +09:30
Jon
491e0dba64 feat(devops): Add git provider badge to git_repository table fields
ref: #693
2025-03-22 20:45:02 +09:30
Jon
50cb54ab0c feat(devops): Add Git GRoup to navigation
ref: #693
2025-03-22 19:25:13 +09:30
Jon
7638fa39da feat(itam): Add back_url to Software Version ViewSet
ref: #693
2025-03-22 19:24:51 +09:30
Jon
b0df5713b2 feat(itam): Add back_url to Operating System ViewSet
ref: #693
2025-03-22 19:24:27 +09:30
Jon
57c5947c55 feat(devops): Add page_layout to Git Group model
ref: #693 #515
2025-03-22 19:23:49 +09:30
Jon
bfd54c112b feat(devops): Add page_layout to GitLab repository model
ref: #693 #515
2025-03-22 19:23:31 +09:30
Jon
e2ca5b8587 feat(devops): Add page_layout to GitHub repository model
ref: #693 #515
2025-03-22 19:23:20 +09:30
Jon
f406e7bf3b feat(devops): git_repository ViewSet updated to fetch queryset based off of repository provider
ref: #693 #515
2025-03-22 19:22:49 +09:30
Jon
1e127d7180 feat(devops): Add ti git_repository ViewSet return and back urls
ref: #693 #515
2025-03-22 19:21:58 +09:30
Jon
a0dd0384bf fix(devops): git_group serializers must define fields
ref: #693 #515
2025-03-22 19:21:04 +09:30
Jon
60cc64ba19 fix(devops): git_group serializers must return urls
ref: #693 #515
2025-03-22 19:20:42 +09:30
Jon
2e9470be83 fix(devops): Correct git_repository notes urls
ref: #693 #515
2025-03-22 19:19:53 +09:30
Jon
ade836911f fix(devops): Correct git_repository url regex
ref: #693 #515
2025-03-22 19:19:40 +09:30
Jon
84f2e8d8c3 feat(devops): Make fields provider and provider_id unique_together for git_repository model
ref: #693 #515
2025-03-22 19:17:33 +09:30
Jon
64b677eaa9 fix(devops): Correct ViewSerializer for GitLab Repository
ref: #693 #515
2025-03-22 19:15:58 +09:30
Jon
4bd5a890db fix(devops): Correct ViewSerializer for GitHib Repository
ref: #693 #515
2025-03-22 19:15:44 +09:30
Jon
48a7a206d2 feat(devops): Add fields to ALL git_repository serializers
ref: #693 #515
2025-03-22 18:55:03 +09:30
Jon
b837338140 fix(devops): Correct model git_group modified field name part 2
ref: #693 #515
2025-03-22 18:53:56 +09:30
Jon
0ad80a6f9a feat(devops): Add fetching of URL to base git_repository model
ref: #693 #515
2025-03-22 18:53:33 +09:30
Jon
a30cad25bc fix(devops): Correct model git_group modified field name
ref: #693 #515
2025-03-22 18:52:35 +09:30
Jon
668a64bb79 fix(api): Fetching of serializer_class must be dynamic
ref: #693
2025-03-22 18:52:00 +09:30
Jon
9d67624e9d feat(api): Enable fetching of app_namespace from model
ref: #693
2025-03-22 18:51:05 +09:30
Jon
ca2e4e00fa feat(access): Add function get_page_layout
enables dynamic page_layout

ref: #693
2025-03-22 18:50:28 +09:30
Jon
57cd4851a8 chore(feature_flag): [2025-00001] add feature flag as enabled when DEBUG=True
so that development can occur

ref: #693 #515
2025-03-22 14:41:53 +09:30
Jon
e6f576ef1a feat(feature_flag): Provide user with ability to override feature flags
ref: #693 #493 #575
2025-03-22 14:40:41 +09:30
Jon
b34c76afde feat(base): Add middleware feature_flag
ref: #693 #515
2025-03-22 13:51:22 +09:30
Jon
f0171fcfda refactor(api): mv _nav property to function get_nav_items
required for dynamic nav menu creation

ref: #693 #515
2025-03-22 13:50:28 +09:30
Jon
04b5b4dc24 feat(devops): Disable notes for GIT Repository Base Model
ref: #693 #515
2025-03-20 16:48:36 +09:30
Jon
77e42db3c9 feat(devops): Add git_repository model tag migration
ref: #693 #515
2025-03-20 16:48:09 +09:30
Jon
25e0f6d950 feat(devops): Add git_repository as a model that can be linked to a ticket
ref: #693 #515
2025-03-20 16:47:39 +09:30
Jon
e725efb9b7 chore: Add missing devops imports
ref: #693 #515
2025-03-20 15:58:26 +09:30
Jon
f6bf6df31c docs(user): Add Git repository and groups
ref: #693 #515
2025-03-20 15:48:54 +09:30
Jon
2994cfd783 feat(devops): Git Group Notes Migration
ref: #693 #515
2025-03-20 15:23:13 +09:30
Jon
d006da803f feat(devops): Git Group Notes ViewSet
ref: #693 #515
2025-03-20 15:23:02 +09:30
Jon
b1f80cb1b2 feat(devops): Git Group Notes Serializer
ref: #693 #515
2025-03-20 15:22:17 +09:30
Jon
9485d4fce7 feat(devops): Git Group Notes Model
ref: #693 #515
2025-03-20 15:22:10 +09:30
Jon
f5e8dd95db feat(devops): GitHub and GitLab Repository Notes Migrations
ref: #693 #515
2025-03-20 15:15:34 +09:30
Jon
83d937ce7a feat(devops): GitLab Repository Notes Viewset
ref: #693 #515
2025-03-20 15:14:31 +09:30
Jon
ee4ff23618 feat(devops): GitHub Repository Notes Viewset
ref: #693 #515
2025-03-20 15:14:15 +09:30
Jon
2b19f466f2 feat(devops): GitLab Repository Notes Serializer
ref: #693 #515
2025-03-20 15:12:06 +09:30
Jon
8caa8646b4 feat(devops): GitHub Repository Notes Serializer
ref: #693 #515
2025-03-20 15:11:58 +09:30
Jon
dc9d1d283f feat(devops): GitLab Repository Notes Model
ref: #693 #515
2025-03-20 15:11:44 +09:30
Jon
0c82fd2bb1 feat(devops): GitHub Repository Notes Model
ref: #693 #515
2025-03-20 15:11:34 +09:30
Jon
5483c1878d feat(devops): Git Group History Migrations
ref: #693 #515
2025-03-20 14:54:10 +09:30
Jon
bd22604d9d feat(devops): Git Group History
ref: #693 #515
2025-03-20 14:54:02 +09:30
Jon
e8c246a949 feat(devops): GitLab and GitHub Repository History Migrations
ref: #693 #515
2025-03-20 14:51:25 +09:30
Jon
69c631d59b feat(devops): GitLab Repository History
ref: #693 #515
2025-03-20 14:51:04 +09:30
Jon
561e175723 feat(devops): GitHub Repository History
ref: #693 #515
2025-03-20 14:50:48 +09:30
Jon
4c6c27a4bd feat(devops): [2025-00001] Git Group and Repositories URLs
behind FF 2025-00001

ref: #693 #515
2025-03-20 14:31:24 +09:30
Jon
cb95cb506a feat(devops): Git Group and Repositories Migrations
ref: #693 #515
2025-03-20 14:24:49 +09:30
Jon
2b3070c5c2 feat(devops): GIT Group ViewSet
ref: #693 #515
2025-03-20 14:14:07 +09:30
Jon
9229036b72 feat(devops): GIT Group Serializer
ref: #693 #515
2025-03-20 14:13:10 +09:30
Jon
b5b8030c81 feat(devops): GIT Group Model
ref: #693 #515
2025-03-20 14:12:58 +09:30
Jon
8d4aad7745 feat(devops): GIT Repositories Viewset
ref: #693 #515
2025-03-20 14:12:29 +09:30
Jon
c5e33c4e3d feat(devops): GitLab Serializer for git repositories
ref: #693 #515
2025-03-20 14:09:12 +09:30
Jon
23773a8776 feat(devops): GitHub Serializer for git repositories
ref: #693 #515
2025-03-20 14:09:00 +09:30
Jon
ef0b024a12 feat(devops): Base Serializer for git repositories
ref: #693 #515
2025-03-20 14:08:16 +09:30
Jon
3cdf0c7324 feat(devops): GitLab Repository Model
ref: #693 #515
2025-03-20 14:07:38 +09:30
Jon
051995efca feat(devops): GitHub Repository Model
ref: #693 #515
2025-03-20 14:07:32 +09:30
Jon
717bd1e221 feat(devops): Base model for git repositories
ref: #693 #515
2025-03-20 14:07:02 +09:30
Jon
e158be3cb2 chore: update issue template new model
ref: #693
2025-03-20 13:39:59 +09:30
Jon
7a7281ecfc Merge pull request #691 from nofusscomputing/test-ticket-slash-commands 2025-03-19 15:50:44 +09:30
Jon
88d1abaef7 fix(core): Don't create an empty ticket comment if the body is empty when slash commands removed
ref: #691 #681
2025-03-19 15:36:13 +09:30
Jon
5804abc367 docs(core): Update slash command docs
ref: #691 #681
2025-03-19 14:51:37 +09:30
Jon
96f8949be2 feat(core): Enable slash command related ticket to have multiple ticket references
ref: #691 #681
2025-03-19 14:38:30 +09:30
Jon
cce802ea9e test(core): un-mark tests as skipped so that multiple linked items per ticket can be tested
ref: #691 closes #681
2025-03-19 13:55:57 +09:30
Jon
c767a528eb fix(core): when processing slash commands trim each line prior to processing
ref: #691 #681
2025-03-19 13:28:11 +09:30
Jon
895cfb8b22 test(core): correct ticket linked item to prevent duplicate creation
ref: #691 #681
2025-03-19 13:27:48 +09:30
Jon
8dfaec8df7 fix(core): slash command NL char is \r\n not \n, however support both
ref: #691 #681
2025-03-19 12:50:56 +09:30
Jon
69cabda78e fix(core): When processing slash commands trim whitespace on return
ref: #691 #681
2025-03-19 12:50:34 +09:30
Jon
8abced87de fix(core): Ensure linked ticket models are unique
ref: #691 #681
2025-03-19 12:00:05 +09:30
Jon
983311dda5 feat(core): Enable slash command linked model to have multiple models
ref: #691 #681
2025-03-19 12:00:05 +09:30
Jon
2becbeef87 feat(core): process ticket slash commands by line
ref: #691 #681
2025-03-19 12:00:05 +09:30
Jon
9583631473 feat(core): Migrations for new slash commands
ref: #690 #592 #598 #600
2025-03-17 15:59:04 +09:30
Jon
44e1461c6b feat(project_management): Add project_state slash command
ref: #690 closes #592
2025-03-17 15:52:18 +09:30
Jon
105e6509b0 feat(core): Add ticket_comment_category slash command
ref: #690 closes #600
2025-03-17 15:51:30 +09:30
Jon
20d979963a feat(core): Add ticket_category slash command
ref: #690 closes #598
2025-03-17 15:38:55 +09:30
Jon
928dee74b5 feat(itam): when displaying software version, add prefix with software name
ref: #690 #596
2025-03-17 15:26:01 +09:30
Jon
bde2b2e758 feat(itam): Add markdown tag $software_version
ref: #690 closes #596
2025-03-17 15:22:53 +09:30
Jon
54b97b43ed feat(itam): Enable ticket tab on software version page
ref: #690 #596
2025-03-17 15:22:07 +09:30
Jon
405538fa35 fix(itam): Add back url to software_version model
ref: #690
2025-03-17 15:19:59 +09:30
7a9680d988 build: bump version 1.13.0 -> 1.13.1 2025-03-17 04:16:28 +00:00
Jon
d7d85bd01d Merge pull request #688 from nofusscomputing/fix-feature-flag-crash 2025-03-17 13:34:04 +09:30
Jon
becb1eef26 fix(devops): After fetching feature flags dont attempt to access results unless status=200
ref: #688 #687
2025-03-17 13:12:29 +09:30
Jon
bf973d3765 fix(docker): only download feature flags when not a worker
ref: #688 fixes #687
2025-03-17 12:23:35 +09:30
Jon
7912a67ab7 refactor(docker): Use crontabs not cron.d
ref: #688 fixes #687
2025-03-17 12:17:59 +09:30
Jon
6272eef45f fix(devops): Use correct stderr function when using feature_flag management command
ref: #688 #687
2025-03-17 11:44:05 +09:30
Jon
176537d583 fix(devops): Cater for connection timeout when fetching feature flags
ref: #688 #687
2025-03-17 11:43:37 +09:30
Jon
2491ab611b fix: when building feature flag version, use first 8 chars of build hash
ref: #688 #687
2025-03-17 11:42:01 +09:30
267 changed files with 12981 additions and 4011 deletions

View File

@ -17,5 +17,5 @@ commitizen:
prerelease_offset: 1
tag_format: $version
update_changelog_on_bump: false
version: 1.13.0
version: 1.15.1
version_scheme: semver

View File

@ -25,7 +25,7 @@ Describe in detail the following:
-->
### 🚧 Tasks
## 🚧 Tasks
<!-- Don't remove tasks strike them out. use `~~` before and after the item. i.e. `- ~~[ ] Model Created~~` note: don't include the list dash-->
@ -36,7 +36,6 @@ Describe in detail the following:
- [ ] 🏷️ Model tag added to `app/core/lib/slash_commands/linked_model.CommandLinkedModel.get_model()` function
- [ ] 📘 Tag updated in the [docs](https://nofusscomputing.com/projects/centurion_erp/user/core/markdown/#model-reference)
- [ ] tag added to `app/core/models/ticket/ticket_linked_items.TicketLinkedItem.__str__()`
- [ ] tag added to `app/core/lib/slash_commands/linked_model.CommandLinkedModel.get_model()`
- [ ] ⚒️ Migration _Ticket Linked Item item_type choices update_
@ -51,7 +50,7 @@ Describe in detail the following:
- [ ] 🆕 Model Created
- [ ] 🛠️ Migrations added
- [ ] Add `app_label` to KB Models `app/assistance/models/model_knowledge_base_article.all_models().model_apps`
- [ ] _(Notes not used/required) - _ Add `model_name` to KB Models `app/assistance/models/model_knowledge_base_article.all_models().excluded_models`
- [ ] _(Notes not used/required) -_ Add `model_name` to KB Models `app/assistance/models/model_knowledge_base_article.all_models().excluded_models`
- [ ] 🧪 [Unit tested](https://nofusscomputing.com/projects/centurion_erp/development/core/model_notes/#testing)
- [ ] 🧪 [Functional tested](https://nofusscomputing.com/projects/centurion_erp/development/core/model_notes/#testing)
@ -65,7 +64,7 @@ Describe in detail the following:
#### 🧪 Tests
### 🧪 Tests
- [ ] Unit Test Model
- [ ] Unit Test Tenancy Object
@ -79,7 +78,7 @@ Describe in detail the following:
- [ ] Function Test History API Render (fields)
### ✅ Requirements
## ✅ Requirements
A Requirement is a must have. In addition will also be tested.

View File

@ -20,7 +20,7 @@
<!-- dont remove tasks below strike through including the checkbox by enclosing in double tidle '~~' -->
- [ ] **Feature Release ONLY** :red_square: Squash migration files :red_square:
- [ ] **Feature Release ONLY** :red_square: [Squash migration files](https://docs.djangoproject.com/en/5.2/topics/migrations/#squashing-migrations) :red_square:
_Multiple migration files created as part of this release are to be sqauashed into a few files as possible so as to limit the number of migrations_
- [ ] :firecracker: Contains breaking-change Any Breaking change(s)?

View File

@ -1,3 +1,246 @@
## 1.15.1 (2025-04-10)
### Fixes
- **python**: Downgrade django 5.2 -> 5.1.8
## 1.15.0 (2025-04-10)
### feat
- **settings**: Move Ticket Comment Category from settings to ITOps menu
- **settings**: Move Ticket Category from settings to ITOps menu
- **access**: place roles nav behind feature flag 2025-00003
- **access**: place directory nav behind feature flag 2025-00002
- **accounting**: add new module
- **access**: Ensure that the same person cant be created more than once
- **access**: Place Roles Model behind feature flag `2025-00003`
- **access**: When querying permissions, select related field `content_type`
- **core**: Model tag for Access/Role
- **access**: Model Role notes endpoint
- **access**: Add navigation entry for roles
- **access**: Model Role History migrations
- **access**: Add model Role History
- **access**: Role Notes model viewset
- **access**: Role Notes model serializer
- **access**: Model Role Notes migrations
- **access**: Add model Role Notes
- **access**: Role model viewset
- **access**: Role model serializer
- **access**: Model Role migrations
- **access**: Add model Role
- **python**: Upgrade Django 5.1.7 -> 5.2
- **access**: Place Entity URLs behind feature flag `2025-00002`
- **access**: Add detail page layout for contact model
- **access**: Add Menu entry for corporate directory
- **access**: Add back_url to Entity metadata
- **core**: Add Entity model tag
- **access**: Update Entity field `entity_type` if it does not match the entity type
- **access**: All Entity models to use the entity history endpoint
- **access**: Enable specifying the history model to use for audit history for a model
- **access**: Enable specifying the kb model to use for linking kb article to a model
- **access**: All Entity models to use the entity notes endpoint
- **access**: Enable specifying the notes `basename` for a model
- **access**: ViewSet for Entity Notes model
- **access**: Serializer for Entity Notes model
- **access**: new model Entity Notes
- **access**: New model Entity History
- **access**: Add Entity URL routes
- **access**: new serializer Contact
- **access**: new model Contact
- **access**: new serializer Person
- **access**: new model Person
- **access**: new ViewSet for for Entity and sub-entities
- **access**: new serializer Entity
- **access**: new model Entity
- **human_resources**: Add navigation menu entry for Human Resources (HR)
- **human_resources**: Add module Human Resources (HR) to API Urls
- **base**: Add module Human Resources (HR) to installed apps
- Add module Human Resources (HR)
### Fixes
- **api**: Correct documentation link to use models verbose name
- **feature_flag**: cater for settings flag overrides
- **access**: Add missing field directory to contact model
- **settings**: Add Application Settings to Admin page
- **access**: Remove app_namespace from Entity
- **access**: add missing tenancy object fields to non-tenancy object models
- **core**: Dont attempt to fetch history related objects if no history exists
- **api**: Dont attempt to access kwargs if not exists within common serializer
### Refactoring
- **core**: When saving history, ensure field `_prefetched_objects_cache` is not included
### Tests
- **settings**: Correct nav menu entry for Ticket Category and Ticket Comment Category
- **access**: Ensure Model Contacts inherits from Person Model
- **access**: Functional Test Suite for Contact API Metadata, API Permissions and ViewSet
- **access**: Functional Test Suite for Contact serializer
- **access**: Functional Test Suite for Contact history
- **access**: Correct Entity and person functional Test Suite so sub-model testing works
- **access**: Correct table_fields test case to cater for dynamic field
- **access**: Unit Test for Contact ViewSet
- **access**: Unit Test for Contact model
- **access**: Unit Test for Contact history API field checks
- **access**: Unit Test for Contact API field checks
- **access**: Unit Test for Person Tenancy Object
- **access**: Correct Entity and person unit Test Suite so sub-model testing works
- **access**: Entity Function Serializer test cases
- **access**: Person Model field test cases
- **access**: Functional Test for Person ViewSet, Permissions and Metadata
- **access**: Functional Test for Person History
- **access**: Correct Entity Function Test Suite so sub-model testing works
- **access**: Unit Test for Person ViewSet
- **access**: Unit Test for Person Model
- **access**: Unit Test for Person History API fields
- **access**: Unit Test for Person API fields
- **access**: Unit Test for Person Tenancy Object
- **access**: Correct Entity Test Suite so sub-model testing works
- **app**: exclude any field check that ends in `_ptr_id`
- **access**: Remove teardown from Function Test cases for Role serializer
- **access**: Test cases for Role serializer
- **access**: Function Test cases for Role SPI Permissions, ViewSet and Metadata
- **access**: Function Test cases for Role History
- **access**: Unit Test case to ensure Role is by organization
- **access**: Unit Test case to ensure Role cant be set as global object
- **access**: Unit Test cases for Role ViewSet
- **access**: Unit Test cases for Role model
- **access**: Unit Test cases for Role History API v2
- **access**: Unit Test cases for Role API v2
- **access**: Unit Test cases for Role Tenancy Object
- During testing add debug_feature_flags so object behind can be tested
- **access**: Notes ViewSet Functional Tests for Entity Model
- **access**: Notes API Field Functional Tests for Entity Model
- **access**: Correct functional ViewSet test suite for Entity model
- **access**: History functional Tests for Entity model
- **access**: PermissionsAPI, ViewSet and Metadata Tests for Entity model
- **access**: Model test cases for Entity
- **access**: API Rendering test cases for Entity model
- **api**: Ensure that when mocking the request the viewset is instantiated
- **access**: History tests for Entity model
- **access**: ViewSet tests for Entity model
- **access**: Tenancy object test for Entity model
## 1.14.0 (2025-03-29)
### feat
- **itops**: Add navigation menu
- New Module ITOps
- **devops**: Ensure GitHub Groups can't be nested
- **devops**: Models Git Repository must use organization from `git_group` as must group if parent set
- **devops**: Add git provider badge to git_group table fields
- **devops**: Add git provider badge to git_repository table fields
- **devops**: Add Git GRoup to navigation
- **itam**: Add `back_url` to Software Version ViewSet
- **itam**: Add `back_url` to Operating System ViewSet
- **devops**: Add `page_layout` to Git Group model
- **devops**: Add `page_layout` to GitLab repository model
- **devops**: Add `page_layout` to GitHub repository model
- **devops**: git_repository ViewSet updated to fetch queryset based off of repository provider
- **devops**: Add ti git_repository ViewSet return and back urls
- **devops**: Make fields `provider` and `provider_id` unique_together for git_repository model
- **devops**: Add fields to ALL git_repository serializers
- **devops**: Add fetching of URL to base git_repository model
- **api**: Enable fetching of app_namespace from model
- **access**: Add function get_page_layout
- **feature_flag**: Provide user with ability to override feature flags
- **base**: Add middleware feature_flag
- **devops**: Disable notes for GIT Repository Base Model
- **devops**: Add git_repository model tag migration
- **devops**: Add git_repository as a model that can be linked to a ticket
- **devops**: Git Group Notes Migration
- **devops**: Git Group Notes ViewSet
- **devops**: Git Group Notes Serializer
- **devops**: Git Group Notes Model
- **devops**: GitHub and GitLab Repository Notes Migrations
- **devops**: GitLab Repository Notes Viewset
- **devops**: GitHub Repository Notes Viewset
- **devops**: GitLab Repository Notes Serializer
- **devops**: GitHub Repository Notes Serializer
- **devops**: GitLab Repository Notes Model
- **devops**: GitHub Repository Notes Model
- **devops**: Git Group History Migrations
- **devops**: Git Group History
- **devops**: GitLab and GitHub Repository History Migrations
- **devops**: GitLab Repository History
- **devops**: GitHub Repository History
- **devops**: [2025-00001] Git Group and Repositories URLs
- **devops**: Git Group and Repositories Migrations
- **devops**: GIT Group ViewSet
- **devops**: GIT Group Serializer
- **devops**: GIT Group Model
- **devops**: GIT Repositories Viewset
- **devops**: GitLab Serializer for git repositories
- **devops**: GitHub Serializer for git repositories
- **devops**: Base Serializer for git repositories
- **devops**: GitLab Repository Model
- **devops**: GitHub Repository Model
- **devops**: Base model for git repositories
- **core**: Enable slash command related ticket to have multiple ticket references
- **core**: Enable slash command linked model to have multiple models
- **core**: process ticket slash commands by line
- **core**: Migrations for new slash commands
- **project_management**: Add project_state slash command
- **core**: Add ticket_comment_category slash command
- **core**: Add ticket_category slash command
- **itam**: when displaying software version, add prefix with software name
- **itam**: Add markdown tag $software_version
- **itam**: Enable ticket tab on software version page
### Fixes
- **devops**: Correct git_group serializer parameter name
- **devops**: Correct field path to no be unique for git_repository
- **feature_flag**: if over_rides not set ensure val set to empty dict
- **devops**: git_group serializers must define fields
- **devops**: git_group serializers must return urls
- **devops**: Correct git_repository notes urls
- **devops**: Correct git_repository url regex
- **devops**: Correct ViewSerializer for GitLab Repository
- **devops**: Correct ViewSerializer for GitHib Repository
- **devops**: Correct model git_group modified field name part 2
- **devops**: Correct model git_group modified field name
- **api**: Fetching of serializer_class must be dynamic
- **core**: Don't create an empty ticket comment if the body is empty when slash commands removed
- **core**: when processing slash commands trim each line prior to processing
- **core**: slash command NL char is `\r\n` not `\n`, however support both
- **core**: When processing slash commands trim whitespace on return
- **core**: Ensure linked ticket models are unique
- **itam**: Add back url to software_version model
### Refactoring
- **devops**: remove model unique_together constraint for git group and repository
- **devops**: Field `provider_id` must not be user editable for git group or repository
- **api**: mv _nav property to function get_nav_items
### Tests
- **api**: Correct test cases for view_name and view_description
- Refactor all ViewSet Unit Test cases to use new test cases class
- **api**: Common ViewSet classes Tests and Test cases for classes that inherit them
- **api**: correct nav menu setup to use mock request
- **core**: un-mark tests as skipped so that multiple linked items per ticket can be tested
- **core**: correct ticket linked item to prevent duplicate creation
## 1.13.1 (2025-03-17)
### Fixes
- **devops**: After fetching feature flags dont attempt to access results unless status=200
- **docker**: only download feature flags when not a worker
- **devops**: Use correct stderr function when using feature_flag management command
- **devops**: Cater for connection timeout when fetching feature flags
- when building feature flag version, use first 8 chars of build hash
### Refactoring
- **docker**: Use crontabs not cron.d
## 1.13.0 (2025-03-16)
### feat

View File

@ -1,3 +1,17 @@
## Version 1.15.0
- Entities model added behind feature flag `2025-00002` and will remain behind this flag until production ready.
- Roles model added behind feature flag `2025-00003` and will remain behind this flag until production ready.
- Accounting Module added behind feature flag `2025-00004` and will remain behind this flag until production ready.
## Version 1.14.0
- Git Repository and Git Group Models added behind feature flag `2025-00001`. They will remain behind this feature flag until the Git features are fully developed and ready for use.
## Version 1.13.0
- DevOps Module added.

View File

@ -47,7 +47,7 @@ def permission_queryset():
'view_history',
]
return Permission.objects.filter(
return Permission.objects.select_related('content_type').filter(
content_type__app_label__in=apps,
).exclude(
content_type__model__in=exclude_models

View File

@ -0,0 +1,140 @@
# Generated by Django 5.2 on 2025-04-10 02:34
import access.fields
import access.models.tenancy
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0004_organizationhistory_teamhistory'),
('auth', '0012_alter_user_first_name_max_length'),
('core', '0021_alter_ticketlinkeditem_item_type'),
]
operations = [
migrations.CreateModel(
name='Entity',
fields=[
('is_global', models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object')),
('model_notes', models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes')),
('id', models.AutoField(help_text='Primary key of the entry', primary_key=True, serialize=False, unique=True, verbose_name='ID')),
('entity_type', models.CharField(default='entity', help_text='Type this entity is', max_length=30, verbose_name='Entity Type')),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, help_text='Date and time of creation', verbose_name='Created')),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, help_text='Date and time of last modification', verbose_name='Modified')),
('organization', models.ForeignKey(help_text='Organization this belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='+', to='access.organization', validators=[access.models.tenancy.TenancyObject.validatate_organization_exists], verbose_name='Organization')),
],
options={
'verbose_name': 'Entity',
'verbose_name_plural': 'Entities',
'ordering': ['created', 'modified', 'organization'],
},
),
migrations.CreateModel(
name='Person',
fields=[
('entity_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='access.entity')),
('f_name', models.CharField(help_text='The persons first name', max_length=64, verbose_name='First Name')),
('m_name', models.CharField(blank=True, default=None, help_text='The persons middle name(s)', max_length=100, null=True, verbose_name='Middle Name(s)')),
('l_name', models.CharField(help_text='The persons Last name', max_length=64, verbose_name='Last Name')),
('dob', models.DateField(blank=True, default=None, help_text='The Persons Date of Birth (DOB)', null=True, verbose_name='DOB')),
],
options={
'verbose_name': 'Person',
'verbose_name_plural': 'People',
'ordering': ['l_name', 'm_name', 'f_name', 'dob'],
},
bases=('access.entity',),
),
migrations.CreateModel(
name='EntityHistory',
fields=[
('modelhistory_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.modelhistory')),
('model', models.ForeignKey(help_text='Model this note belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='history', to='access.entity', verbose_name='Model')),
],
options={
'verbose_name': 'Entity History',
'verbose_name_plural': 'Entity History',
'db_table': 'access_entity_history',
'ordering': ['-created'],
},
bases=('core.modelhistory',),
),
migrations.CreateModel(
name='EntityNotes',
fields=[
('modelnotes_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.modelnotes')),
('model', models.ForeignKey(help_text='Model this note belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='access.entity', verbose_name='Model')),
],
options={
'verbose_name': 'Entity Note',
'verbose_name_plural': 'Entity Notes',
'db_table': 'access_entity_notes',
'ordering': ['-created'],
},
bases=('core.modelnotes',),
),
migrations.CreateModel(
name='Role',
fields=[
('model_notes', models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes')),
('id', models.AutoField(help_text='Primary key of the entry', primary_key=True, serialize=False, unique=True, verbose_name='ID')),
('name', models.CharField(help_text='Name of this role', max_length=30, verbose_name='Name')),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, help_text='Date and time of creation', verbose_name='Created')),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, help_text='Date and time of last modification', verbose_name='Modified')),
('organization', models.ForeignKey(help_text='Organization this belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='+', to='access.organization', validators=[access.models.tenancy.TenancyObject.validatate_organization_exists], verbose_name='Organization')),
('permissions', models.ManyToManyField(blank=True, help_text='Permissions part of this role', related_name='roles', to='auth.permission', verbose_name='Permissions')),
],
options={
'verbose_name': 'Role',
'verbose_name_plural': 'Roles',
'ordering': ['organization', 'name'],
'unique_together': {('organization', 'name')},
},
),
migrations.CreateModel(
name='RoleHistory',
fields=[
('modelhistory_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.modelhistory')),
('model', models.ForeignKey(help_text='Model this note belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='history', to='access.role', verbose_name='Model')),
],
options={
'verbose_name': 'Role History',
'verbose_name_plural': 'Role History',
'db_table': 'access_role_history',
'ordering': ['-created'],
},
bases=('core.modelhistory',),
),
migrations.CreateModel(
name='RoleNotes',
fields=[
('modelnotes_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.modelnotes')),
('model', models.ForeignKey(help_text='Model this note belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='access.role', verbose_name='Model')),
],
options={
'verbose_name': 'Role Note',
'verbose_name_plural': 'Role Notes',
'db_table': 'access_role_notes',
'ordering': ['-created'],
},
bases=('core.modelnotes',),
),
migrations.CreateModel(
name='Contact',
fields=[
('person_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='access.person')),
('directory', models.BooleanField(blank=True, default=True, help_text='Show contact details in directory', verbose_name='Show in Directory')),
('email', models.EmailField(help_text='E-mail address for this person', max_length=254, unique=True, verbose_name='E-Mail')),
],
options={
'verbose_name': 'Contact',
'verbose_name_plural': 'Contacts',
'ordering': ['email'],
},
bases=('access.person',),
),
]

View File

@ -0,0 +1,2 @@
from . import contact
from . import person

View File

@ -0,0 +1,115 @@
from django.db import models
from access.models.person import Person
class Contact(
Person
):
class Meta:
ordering = [
'email',
]
verbose_name = 'Contact'
verbose_name_plural = 'Contacts'
directory = models.BooleanField(
blank = True,
default = True,
help_text = 'Show contact details in directory',
null = False,
verbose_name = 'Show in Directory',
)
email = models.EmailField(
blank = False,
help_text = 'E-mail address for this person',
unique = True,
verbose_name = 'E-Mail',
)
def __str__(self) -> str:
return self.f_name + ' ' + self.l_name
documentation = ''
page_layout: list = [
{
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'organization',
'created',
'modified',
],
"right": [
'model_notes',
'directory',
]
},
{
"name": "Personal Details",
"layout": "double",
"left": [
'display_name',
'dob',
],
"right": [
'f_name',
'm_name',
'l_name',
]
},
{
"name": "",
"layout": "double",
"left": [
'email',
],
"right": [
'',
]
}
]
},
{
"name": "Knowledge Base",
"slug": "kb_articles",
"sections": [
{
"layout": "table",
"field": "knowledge_base",
}
]
},
{
"name": "Notes",
"slug": "notes",
"sections": []
},
]
table_fields: list = [
{
"field": "display_name",
"type": "link",
"key": "_self"
},
'f_name',
'l_name',
'email',
'organization',
'created',
]

241
app/access/models/entity.py Normal file
View File

@ -0,0 +1,241 @@
from django.db import models
from rest_framework.reverse import reverse
from access.fields import AutoCreatedField, AutoLastModifiedField
from access.models.tenancy import TenancyObject
from core.lib.feature_not_used import FeatureNotUsed
class Entity(
TenancyObject
):
class Meta:
ordering = [
'created',
'modified',
'organization',
]
verbose_name = 'Entity'
verbose_name_plural = 'Entities'
id = models.AutoField(
blank=False,
help_text = 'Primary key of the entry',
primary_key=True,
unique=True,
verbose_name = 'ID'
)
entity_type = models.CharField(
blank = False,
default = Meta.verbose_name.lower(),
help_text = 'Type this entity is',
max_length = 30,
unique = False,
verbose_name = 'Entity Type'
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
def __str__(self) -> str:
related_model = self.get_related_model()
if related_model is None:
return f'{self.entity_type} {self.pk}'
return str( related_model )
# app_namespace = 'access'
history_app_label = 'access'
history_model_name = 'entity'
kb_model_name = 'entity'
note_basename = '_api_v2_entity_note'
documentation = ''
page_layout: dict = []
table_fields: list = [
'organization',
'entity_type',
'display_name',
'created',
'modified',
]
def get_related_field_name(self) -> str:
meta = getattr(self, '_meta')
for related_object in getattr(meta, 'related_objects', []):
if getattr(self, related_object.name, None):
if(
not str(related_object.name).endswith('history')
and not str(related_object.name).endswith('notes')
):
return related_object.name
break
return ''
def get_related_model(self):
"""Recursive model Fetch
Returns the lowest model found in a chain of inherited models.
Args:
model (models.Model, optional): Model to fetch the child model from. Defaults to None.
Returns:
models.Model: Lowset model found in inherited model chain
"""
related_model_name = self.get_related_field_name()
related_model = getattr(self, related_model_name, None)
if related_model_name == '':
return None
elif related_model is None:
return self
elif related_model.get_related_field_name() != '':
related_model = related_model.get_related_model()
return related_model
def get_url_kwargs(self) -> dict:
model = self.get_related_model()
if len(self._meta.parents) == 0 and model is None:
return {
'pk': self.id
}
if model is None:
model = self
kwargs = {
'entity_model': str(model._meta.verbose_name).lower().replace(' ', '_'),
}
if model.pk:
kwargs.update({
'pk': model.id
})
return kwargs
def get_url( self, request = None ) -> str:
"""Fetch the models URL
If URL kwargs are required to generate the URL, define a `get_url_kwargs` that returns them.
Args:
request (object, optional): The request object that was made by the end user. Defaults to None.
Returns:
str: Canonical URL of the model if the `request` object was provided. Otherwise the relative URL.
"""
model = None
if getattr(self, 'get_related_model', None):
model = self.get_related_model()
if model is None:
model = self
sub_entity = ''
if model._meta.model_name != 'entity':
sub_entity = '_sub'
kwargs = self.get_url_kwargs()
view = 'list'
if 'pk' in kwargs:
view = 'detail'
if request:
return reverse(f"v2:" + model.get_app_namespace() + f"_api_v2_entity" + sub_entity + "-" + view, request=request, kwargs = kwargs )
return reverse(f"v2:" + model.get_app_namespace() + f"_api_v2_entity" + sub_entity + "-" + view, kwargs = kwargs )
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
related_model = self.get_related_model()
if related_model is None:
related_model = self
if self.entity_type != str(related_model._meta.verbose_name).lower().replace(' ', '_'):
self.entity_type = str(related_model._meta.verbose_name).lower().replace(' ', '_')
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
def save_history(self, before: dict, after: dict) -> bool:
from access.models.entity_history import EntityHistory
history = super().save_history(
before = before,
after = after,
history_model = EntityHistory
)
return history

View File

@ -0,0 +1,55 @@
from django.db import models
from access.models.entity import Entity
from core.models.model_history import ModelHistory
from devops.models.feature_flag import FeatureFlag
class EntityHistory(
ModelHistory
):
class Meta:
db_table = 'access_entity_history'
ordering = ModelHistory._meta.ordering
verbose_name = 'Entity History'
verbose_name_plural = 'Entity History'
model = models.ForeignKey(
Entity,
blank = False,
help_text = 'Model this note belongs to',
null = False,
on_delete = models.CASCADE,
related_name = 'history',
verbose_name = 'Model',
)
table_fields: list = []
page_layout: dict = []
def get_object(self):
return self
def get_serialized_model(self, serializer_context):
model = None
from access.serializers.entity import BaseSerializer
model = BaseSerializer(self.model, context = serializer_context)
return model

View File

@ -0,0 +1,45 @@
from django.db import models
from access.models.entity import Entity
from core.models.model_notes import ModelNotes
class EntityNotes(
ModelNotes
):
class Meta:
db_table = 'access_entity_notes'
ordering = ModelNotes._meta.ordering
verbose_name = 'Entity Note'
verbose_name_plural = 'Entity Notes'
model = models.ForeignKey(
Entity,
blank = False,
help_text = 'Model this note belongs to',
null = False,
on_delete = models.CASCADE,
related_name = 'notes',
verbose_name = 'Model',
)
table_fields: list = []
page_layout: dict = []
def get_url_kwargs(self) -> dict:
return {
'model_id': self.model.pk,
'pk': self.pk
}

117
app/access/models/person.py Normal file
View File

@ -0,0 +1,117 @@
from django.db import models
from core.exceptions import ValidationError
from access.models.entity import Entity
class Person(
Entity
):
class Meta:
ordering = [
'l_name',
'm_name',
'f_name',
'dob',
]
verbose_name = 'Person'
verbose_name_plural = 'People'
f_name = models.CharField(
blank = False,
help_text = 'The persons first name',
max_length = 64,
unique = False,
verbose_name = 'First Name'
)
m_name = models.CharField(
blank = True,
default = None,
help_text = 'The persons middle name(s)',
max_length = 100,
null = True,
unique = False,
verbose_name = 'Middle Name(s)'
)
l_name = models.CharField(
blank = False,
help_text = 'The persons Last name',
max_length = 64,
unique = False,
verbose_name = 'Last Name'
)
dob = models.DateField(
blank = True,
default = None,
help_text = 'The Persons Date of Birth (DOB)',
null = True,
unique = False,
verbose_name = 'DOB',
)
def __str__(self) -> str:
return self.f_name + ' ' + self.l_name + f' (DOB: {self.dob})'
documentation = ''
page_layout: dict = []
table_fields: list = [
'organization',
'f_name',
'l_name',
'dob',
'created',
]
def clean(self):
super().clean()
if self.dob is not None:
if self.pk:
duplicate_entry = Person.objects.filter(
f_name = self.f_name,
l_name = self.l_name,
).exclude(
pk = self.pk
)
else:
duplicate_entry = Person.objects.filter(
f_name = self.f_name,
l_name = self.l_name,
)
for entry in duplicate_entry:
if(
entry.f_name == self.f_name
and entry.m_name == self.m_name
and entry.l_name == self.l_name
and entry.dob == self.dob
):
raise ValidationError(
detail = {
'dob': f'Person {self.f_name} {self.l_name} already exists with this birthday {entry.dob}'
},
code = 'duplicate_person_on_dob'
)

143
app/access/models/role.py Normal file
View File

@ -0,0 +1,143 @@
from django.contrib.auth.models import Permission
from django.db import models
from access.fields import AutoCreatedField, AutoLastModifiedField
from access.models.tenancy import TenancyObject
class Role(
TenancyObject
):
class Meta:
ordering = [
'organization',
'name',
]
unique_together = [
'organization',
'name'
]
verbose_name = 'Role'
verbose_name_plural = 'Roles'
id = models.AutoField(
blank=False,
help_text = 'Primary key of the entry',
primary_key=True,
unique=True,
verbose_name = 'ID'
)
name = models.CharField(
blank = False,
help_text = 'Name of this role',
max_length = 30,
unique = False,
verbose_name = 'Name'
)
permissions = models.ManyToManyField(
Permission,
blank = True,
help_text = 'Permissions part of this role',
related_name = 'roles',
symmetrical = False,
verbose_name = 'Permissions'
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
is_global = None
def __str__(self) -> str:
return str( self.organization ) + ' / ' + self.name
documentation = ''
page_layout: dict = [
{
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'organization',
'name',
'created',
'modified',
],
"right": [
'model_notes',
]
},
{
"layout": "single",
"name": "Permissions",
"fields": [
"permissions",
]
},
]
},
{
"name": "Knowledge Base",
"slug": "kb_articles",
"sections": [
{
"layout": "table",
"field": "knowledge_base",
}
]
},
{
"name": "Tickets",
"slug": "tickets",
"sections": [
{
"layout": "table",
"field": "tickets",
}
],
},
{
"name": "Notes",
"slug": "notes",
"sections": []
},
]
table_fields: list = [
'organization',
'name',
'created',
'modified',
]
def save_history(self, before: dict, after: dict) -> bool:
from access.models.role_history import RoleHistory
history = super().save_history(
before = before,
after = after,
history_model = RoleHistory
)
return history

View File

@ -0,0 +1,53 @@
from django.db import models
from core.models.model_history import ModelHistory
from access.models.role import Role
class RoleHistory(
ModelHistory
):
class Meta:
db_table = 'access_role_history'
ordering = ModelHistory._meta.ordering
verbose_name = 'Role History'
verbose_name_plural = 'Role History'
model = models.ForeignKey(
Role,
blank = False,
help_text = 'Model this note belongs to',
null = False,
on_delete = models.CASCADE,
related_name = 'history',
verbose_name = 'Model',
)
table_fields: list = []
page_layout: dict = []
def get_object(self):
return self
def get_serialized_model(self, serializer_context):
model = None
from access.serializers.role import BaseSerializer
model = BaseSerializer(self.model, context = serializer_context)
return model

View File

@ -0,0 +1,45 @@
from django.db import models
from access.models.role import Role
from core.models.model_notes import ModelNotes
class RoleNotes(
ModelNotes
):
class Meta:
db_table = 'access_role_notes'
ordering = ModelNotes._meta.ordering
verbose_name = 'Role Note'
verbose_name_plural = 'Role Notes'
model = models.ForeignKey(
Role,
blank = False,
help_text = 'Model this note belongs to',
null = False,
on_delete = models.CASCADE,
related_name = 'notes',
verbose_name = 'Model',
)
table_fields: list = []
page_layout: dict = []
def get_url_kwargs(self) -> dict:
return {
'model_id': self.model.pk,
'pk': self.pk
}

View File

@ -73,6 +73,11 @@ class TeamUsers(SaveHistory):
'manager'
]
history_app_label: str = None
history_model_name: str = None
kb_model_name: str = None
note_basename: str = None
def delete(self, using=None, keep_parents=False):
""" Delete Team

View File

@ -172,6 +172,40 @@ class TenancyObject(SaveHistory):
the API version, i.e. `v2:devops`.
"""
history_app_label: str = None
"""History Model Application Label
This value is derived from `<model>._meta.app_label`. This value should
only be used when there is model inheritence.
"""
history_model_name: str = None
"""History Model Model Name
This value is derived from `<model>._meta.model_name`. This value should
only be used when there is model inheritence.
"""
kb_model_name: str = None
"""Model name to use for KB article linking
This value is derived from `<model>._meta.model_name`. This value should
only be used when there is model inheritence.
"""
note_basename: str = None
"""URL BaseName for the notes endpoint.
Don't specify the `app_namespace`, use property `app_namespace` above.
"""
def get_page_layout(self):
""" FEtch the page layout"""
return self.page_layout
def get_app_namespace(self) -> str:
"""Fetch the Application namespace if specified.

View File

View File

@ -0,0 +1,75 @@
from drf_spectacular.utils import extend_schema_serializer
from access.models.contact import Contact
from access.serializers.person import (
BaseSerializer as BaseBaseSerializer,
ModelSerializer as BaseModelSerializer,
)
from access.serializers.organization import OrganizationBaseSerializer
class BaseSerializer(
BaseBaseSerializer,
):
pass
@extend_schema_serializer(component_name = 'ContactEntityModelSerializer')
class ModelSerializer(
BaseSerializer,
BaseModelSerializer,
):
"""Contact Model
This model first inherits from Person then inherits from the Entity Base model.
"""
class Meta:
model = Contact
fields = [
'id',
'person_ptr_id',
'organization',
'entity_type',
'display_name',
'f_name',
'm_name',
'l_name',
'dob',
'email',
'directory',
'model_notes',
'is_global',
'created',
'modified',
'_urls',
]
read_only_fields = [
'id',
'display_name',
'entity_type',
'created',
'modified',
'_urls',
]
@extend_schema_serializer(component_name = 'ContactEntityViewSerializer')
class ViewSerializer(
ModelSerializer,
):
"""Contact View Model
This model inherits from the Person model.
"""
organization = OrganizationBaseSerializer(many=False, read_only=True)

View File

@ -0,0 +1,90 @@
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_serializer
from access.models.entity import Entity
from api.serializers import common
from access.serializers.organization import OrganizationBaseSerializer
@extend_schema_serializer(component_name = 'EntityBaseBaseSerializer')
class BaseSerializer(serializers.ModelSerializer):
display_name = serializers.SerializerMethodField('get_display_name')
def get_display_name(self, item) -> str:
return str( item )
url = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> str:
return item.get_url( request = self.context['view'].request )
class Meta:
model = Entity
fields = [
'id',
'display_name',
'url',
]
read_only_fields = [
'id',
'display_name',
'url',
]
@extend_schema_serializer(component_name = 'EntityBaseModelSerializer')
class ModelSerializer(
common.CommonModelSerializer,
BaseSerializer
):
"""Entity Base Model"""
_urls = serializers.SerializerMethodField('get_url')
class Meta:
model = Entity
fields = [
'id',
'organization',
'entity_type',
'display_name',
'model_notes',
'is_global',
'created',
'modified',
'_urls',
]
read_only_fields = [
'id',
'display_name',
'entity_type',
'created',
'modified',
'_urls',
]
@extend_schema_serializer(component_name = 'EntityBaseViewSerializer')
class ViewSerializer(ModelSerializer):
"""Entity Base View Model"""
organization = OrganizationBaseSerializer(many=False, read_only=True)

View File

@ -0,0 +1,41 @@
from core.serializers.model_notes import (
ModelNoteBaseSerializer,
ModelNoteModelSerializer,
ModelNoteViewSerializer
)
from access.models.entity_notes import EntityNotes
class EntityNoteBaseSerializer(ModelNoteBaseSerializer):
pass
class EntityNoteModelSerializer(
ModelNoteModelSerializer
):
class Meta:
model = EntityNotes
fields = ModelNoteModelSerializer.Meta.fields + [
'model',
]
read_only_fields = ModelNoteModelSerializer.Meta.read_only_fields + [
'model',
'content_type',
]
class EntityNoteViewSerializer(
ModelNoteViewSerializer,
EntityNoteModelSerializer,
):
pass

View File

@ -0,0 +1,73 @@
from drf_spectacular.utils import extend_schema_serializer
from access.models.person import Person
from access.serializers.entity import (
BaseSerializer as BaseBaseSerializer,
ModelSerializer as BaseModelSerializer,
)
from access.serializers.organization import OrganizationBaseSerializer
class BaseSerializer(
BaseBaseSerializer,
):
pass
@extend_schema_serializer(component_name = 'PersonEntityModelSerializer')
class ModelSerializer(
BaseSerializer,
BaseModelSerializer,
):
"""Person Model
This model inherits from the Entity base model.
"""
class Meta:
model = Person
fields = [
'id',
'entity_ptr_id',
'organization',
'entity_type',
'display_name',
'f_name',
'm_name',
'l_name',
'dob',
'model_notes',
'is_global',
'created',
'modified',
'_urls',
]
read_only_fields = [
'id',
'display_name',
'entity_type',
'created',
'modified',
'_urls',
]
@extend_schema_serializer(component_name = 'PersonEntityViewSerializer')
class ViewSerializer(
ModelSerializer,
):
"""Person View Model
This model inherits from the Entity base model.
"""
organization = OrganizationBaseSerializer(many=False, read_only=True)

View File

@ -0,0 +1,114 @@
from rest_framework import serializers
from rest_framework.reverse import reverse
from drf_spectacular.utils import extend_schema_serializer
from access.functions.permissions import permission_queryset
from access.models.role import Role
from access.serializers.organization import OrganizationBaseSerializer
from api.serializers import common
from app.serializers.permission import PermissionBaseSerializer
@extend_schema_serializer(component_name = 'RoleBaseSerializer')
class BaseSerializer(serializers.ModelSerializer):
display_name = serializers.SerializerMethodField('get_display_name')
def get_display_name(self, item) -> str:
return str( item )
url = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> str:
return item.get_url( request = self.context['view'].request )
class Meta:
model = Role
fields = [
'id',
'display_name',
'url',
]
read_only_fields = [
'id',
'display_name',
'url',
]
@extend_schema_serializer(component_name = 'RoleModelSerializer')
class ModelSerializer(
common.CommonModelSerializer,
BaseSerializer
):
"""Role Base Model"""
_urls = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> dict:
get_url = super().get_url( item = item )
get_url.update({
'tickets': reverse(
"v2:_api_v2_item_tickets-list",
request=self._context['view'].request,
kwargs={
'item_class': self.Meta.model._meta.model_name,
'item_id': item.pk
}
)
})
return get_url
permissions = serializers.PrimaryKeyRelatedField(many = True, queryset=permission_queryset(), required = False)
class Meta:
model = Role
fields = [
'id',
'organization',
'display_name',
'name',
'permissions',
'model_notes',
'created',
'modified',
'_urls',
]
read_only_fields = [
'id',
'display_name',
'created',
'modified',
'_urls',
]
@extend_schema_serializer(component_name = 'RoleViewSerializer')
class ViewSerializer(ModelSerializer):
"""Role Base View Model"""
organization = OrganizationBaseSerializer( many=False, read_only=True )
permissions = PermissionBaseSerializer( many=True, read_only=True )

View File

@ -0,0 +1,48 @@
from rest_framework import serializers
from access.models.role_notes import RoleNotes
from api.serializers import common
from app.serializers.user import UserBaseSerializer
from core.serializers.model_notes import (
ModelNotes,
ModelNoteBaseSerializer,
ModelNoteModelSerializer,
ModelNoteViewSerializer
)
class RoleNoteBaseSerializer(ModelNoteBaseSerializer):
pass
class RoleNoteModelSerializer(
ModelNoteModelSerializer
):
class Meta:
model = RoleNotes
fields = ModelNoteModelSerializer.Meta.fields + [
'model',
]
read_only_fields = ModelNoteModelSerializer.Meta.read_only_fields + [
'model',
'content_type',
]
class RoleNoteViewSerializer(
ModelNoteViewSerializer,
RoleNoteModelSerializer,
):
pass

View File

@ -0,0 +1,60 @@
from django.test import TestCase
from access.models.contact import Contact
from access.tests.functional.person.test_functional_person_history import (
PersonHistoryInheritedCases
)
class ContactTestCases(
PersonHistoryInheritedCases,
):
field_name = 'model_notes'
kwargs_create_obj: dict = {
'email': 'ipfunny@unit.test',
}
kwargs_delete_obj: dict = {
'email': 'ipweird@unit.test',
}
model = Contact
class ContactHistoryInheritedCases(
ContactTestCases,
):
model = None
"""Entity model to test"""
kwargs_create_obj: dict = None
kwargs_delete_obj: dict = None
@classmethod
def setUpTestData(self):
self.kwargs_create_obj.update(
super().kwargs_create_obj
)
self.kwargs_delete_obj.update(
super().kwargs_delete_obj
)
super().setUpTestData()
class ContactHistoryTest(
ContactTestCases,
TestCase,
):
pass

View File

@ -0,0 +1,129 @@
import pytest
from django.test import TestCase
from rest_framework.exceptions import ValidationError
from access.serializers.contact import (
Contact,
ModelSerializer
)
from access.tests.functional.person.test_functional_person_serializer import (
PersonSerializerInheritedCases
)
class SerializerTestCases(
PersonSerializerInheritedCases,
):
duplicate_f_name_l_name_dob = {
'email': 'contactentityduplicateone@unit.test',
}
kwargs_create_item: dict = {
'email': 'ipfunny@unit.test',
}
kwargs_create_item_duplicate_f_name_l_name_dob = {
'email': 'contactentityduplicatetwo@unit.test',
}
model = Contact
"""Model to test"""
create_model_serializer = ModelSerializer
"""Serializer to test"""
valid_data: dict = {
'email': 'ipweird@unit.test',
}
def test_serializer_validation_no_email_exception(self):
"""Serializer Validation Check
Ensure that when creating with valid data and field email is missing
a validation error occurs.
"""
data = self.valid_data.copy()
del data['email']
with pytest.raises(ValidationError) as err:
serializer = self.create_model_serializer(
data = data
)
serializer.is_valid(raise_exception = True)
assert err.value.get_codes()['email'][0] == 'required'
class ContactSerializerInheritedCases(
SerializerTestCases,
):
create_model_serializer = None
"""Serializer to test"""
duplicate_f_name_l_name_dob: dict = None
""" Duplicate model serializer dict
used for testing for duplicate f_name, l_name and dob fields.
"""
kwargs_create_item: dict = None
""" Model kwargs to create item"""
kwargs_create_item_duplicate_f_name_l_name_dob: dict = None
"""model kwargs to create object
**None:** Ensure that the fields of sub-model to person do not match
`self.duplicate_f_name_l_name_dob`. if they do the wrong exception will be thrown.
used for testing for duplicate f_name, l_name and dob fields.
"""
model = None
"""Model to test"""
valid_data: dict = None
"""Valid data used by serializer to create object"""
@classmethod
def setUpTestData(self):
"""Setup Test"""
self.duplicate_f_name_l_name_dob.update(
super().duplicate_f_name_l_name_dob
)
self.kwargs_create_item_duplicate_f_name_l_name_dob.update(
super().kwargs_create_item_duplicate_f_name_l_name_dob
)
self.kwargs_create_item.update(
super().kwargs_create_item
)
self.valid_data.update(
super().valid_data
)
super().setUpTestData()
class ContactSerializerTest(
SerializerTestCases,
TestCase,
):
pass

View File

@ -0,0 +1,169 @@
from django.test import TestCase
from access.models.contact import Contact
from access.tests.functional.person.test_functional_person_viewset import (
PersonMetadataInheritedCases,
PersonPermissionsAPIInheritedCases,
PersonViewSetInheritedCases
)
class ViewSetBase:
add_data = {
'email': 'ipfunny@unit.test',
}
kwargs_create_item_diff_org = {
'email': 'ipstrange@unit.test',
}
kwargs_create_item = {
'email': 'ipweird@unit.test',
}
model = Contact
url_kwargs: dict = {}
url_view_kwargs: dict = {}
class PermissionsAPITestCases(
ViewSetBase,
PersonPermissionsAPIInheritedCases,
):
pass
class ContactPermissionsAPIInheritedCases(
PermissionsAPITestCases,
):
add_data: dict = None
model = None
kwargs_create_item: dict = None
kwargs_create_item_diff_org: dict = None
@classmethod
def setUpTestData(self):
self.add_data.update(
super().add_data
)
self.kwargs_create_item.update(
super().kwargs_create_item
)
self.kwargs_create_item_diff_org.update(
super().kwargs_create_item_diff_org
)
super().setUpTestData()
class ContactPermissionsAPITest(
PermissionsAPITestCases,
TestCase,
):
pass
class ViewSetTestCases(
ViewSetBase,
PersonViewSetInheritedCases,
):
pass
class ContactViewSetInheritedCases(
ViewSetTestCases,
):
model = None
kwargs_create_item: dict = None
kwargs_create_item_diff_org: dict = None
@classmethod
def setUpTestData(self):
self.kwargs_create_item.update(
super().kwargs_create_item
)
self.kwargs_create_item_diff_org.update(
super().kwargs_create_item_diff_org
)
super().setUpTestData()
class ContactViewSetTest(
ViewSetTestCases,
TestCase,
):
pass
class MetadataTestCases(
ViewSetBase,
PersonMetadataInheritedCases,
):
pass
class ContactMetadataInheritedCases(
MetadataTestCases,
):
model = None
kwargs_create_item: dict = None
kwargs_create_item_diff_org: dict = None
@classmethod
def setUpTestData(self):
self.kwargs_create_item.update(
super().kwargs_create_item
)
self.kwargs_create_item_diff_org.update(
super().kwargs_create_item_diff_org
)
super().setUpTestData()
class ContactMetadataTest(
MetadataTestCases,
TestCase,
):
pass

View File

@ -0,0 +1,78 @@
from django.test import TestCase
from access.models.entity_history import Entity, EntityHistory
from core.tests.abstract.test_functional_history import HistoryEntriesCommon
class HistoryTestCases(
HistoryEntriesCommon,
):
field_name = 'model_notes'
history_model = EntityHistory
kwargs_create_obj: dict = {}
kwargs_delete_obj: dict = {}
model = Entity
@classmethod
def setUpTestData(self):
super().setUpTestData()
self.obj = self.model.objects.create(
organization = self.organization,
model_notes = self.field_value_original,
**self.kwargs_create_obj,
)
self.obj_delete = self.model.objects.create(
organization = self.organization,
model_notes = 'another note',
**self.kwargs_delete_obj,
)
self.call_the_banners()
class EntityHistoryInheritedCases(
HistoryTestCases,
):
model = None
"""Entity model to test"""
kwargs_create_obj: dict = None
kwargs_delete_obj: dict = None
@classmethod
def setUpTestData(self):
self.kwargs_create_obj.update(
super().kwargs_create_obj
)
self.kwargs_delete_obj.update(
super().kwargs_delete_obj
)
super().setUpTestData()
class EntityHistoryTest(
HistoryTestCases,
TestCase,
):
pass

View File

@ -0,0 +1,106 @@
import pytest
from django.test import TestCase
from access.models.organization import Organization
from access.serializers.entity import (
Entity,
ModelSerializer
)
class SerializerTestCases:
kwargs_create_item: dict = {}
""" Model kwargs to create item"""
model = Entity
"""Model to test"""
create_model_serializer = ModelSerializer
"""Serializer to test"""
valid_data: dict = {}
"""Valid data used by serializer to create object"""
@classmethod
def setUpTestData(self):
"""Setup Test"""
self.organization = Organization.objects.create(name='test_org')
self.kwargs_create_item.update({
'model_notes': 'model notes field'
})
self.valid_data.update({
'organization': self.organization.pk,
'model_notes': 'model notes field'
})
self.item = self.model.objects.create(
organization = self.organization,
**self.kwargs_create_item,
)
def test_serializer_valid_data(self):
"""Serializer Validation Check
Ensure that when creating an object with valid data, no validation
error occurs.
"""
serializer = self.create_model_serializer(
data = self.valid_data
)
assert serializer.is_valid(raise_exception = True)
def test_serializer_validation_no_model_notes(self):
"""Serializer Validation Check
Ensure that if creating and no model_notes is provided no validation
error occurs
"""
data = self.valid_data.copy()
del data['model_notes']
serializer = self.create_model_serializer(
data = data
)
assert serializer.is_valid(raise_exception = True)
class EntitySerializerInheritedCases(
SerializerTestCases,
):
create_model_serializer = None
"""Serializer to test"""
kwargs_create_item: dict = None
""" Model kwargs to create item"""
model = None
"""Model to test"""
valid_data: dict = None
"""Valid data used by serializer to create object"""
class EntitySerializerTest(
SerializerTestCases,
TestCase,
):
pass

View File

@ -0,0 +1,504 @@
from django.contrib.auth.models import Permission, User
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from access.models.entity import Entity
from access.models.organization import Organization
from access.models.team import Team
from access.models.team_user import TeamUsers
from api.tests.abstract.test_metadata_functional import MetadataAttributesFunctional
from api.tests.abstract.api_permissions_viewset import APIPermissions
from api.tests.abstract.api_serializer_viewset import SerializersTestCases
class ViewSetBase:
add_data: dict = None
app_namespace = 'v2'
change_data = None
delete_data = {}
kwargs_create_item: dict = {}
kwargs_create_item_diff_org: dict = {}
model = None
url_kwargs: dict = None
url_view_kwargs: dict = None
url_name = None
@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
self.different_organization = Organization.objects.create(name='test_different_organization')
self.item = self.model.objects.create(
organization = organization,
model_notes = 'some notes',
**self.kwargs_create_item
)
self.other_org_item = self.model.objects.create(
organization = self.different_organization,
model_notes = 'some more notes',
**self.kwargs_create_item_diff_org
)
self.url_view_kwargs.update({ 'pk': self.item.id })
if self.add_data is not None:
self.add_data.update({'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")
TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
self.add_user = User.objects.create_user(username="test_user_add", password="password")
TeamUsers.objects.create(
team = add_team,
user = self.add_user
)
self.change_user = User.objects.create_user(username="test_user_change", password="password")
TeamUsers.objects.create(
team = change_team,
user = self.change_user
)
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
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 = self.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
)
class PermissionsAPITestCases(
ViewSetBase,
APIPermissions,
):
add_data: dict = {}
change_data = {'model_notes': 'device'}
model = None
kwargs_create_item: dict = None
kwargs_create_item_diff_org: dict = None
url_kwargs: dict = None
url_view_kwargs: dict = None
url_name = None
@classmethod
def setUpTestData(self):
self.add_data.update({ 'model_note': 'added model note' })
super().setUpTestData()
def test_returned_data_from_user_and_global_organizations_only(self):
"""Check items returned
This test case is a over-ride of a test case with the same name.
This model is not a tenancy model making this test not-applicable.
Items returned from the query Must be from the users organization and
global ONLY!
"""
pass
class EntityPermissionsAPIInheritedCases(
PermissionsAPITestCases,
):
add_data: dict = None
model = None
kwargs_create_item: dict = None
kwargs_create_item_diff_org: dict = None
url_name = '_api_v2_entity_sub'
@classmethod
def setUpTestData(self):
self.url_kwargs = {
'entity_model': self.model._meta.model_name
}
self.url_view_kwargs = {
'entity_model': self.model._meta.model_name
}
super().setUpTestData()
class EntityPermissionsAPITest(
PermissionsAPITestCases,
TestCase,
):
kwargs_create_item: dict = {}
kwargs_create_item_diff_org: dict = {}
model = Entity
url_kwargs: dict = {}
url_view_kwargs: dict = {}
url_name = '_api_v2_entity'
class ViewSetTestCases(
ViewSetBase,
SerializersTestCases,
):
kwargs_create_item: dict = None
kwargs_create_item_diff_org: dict = None
model = None
url_kwargs: dict = None
url_view_kwargs: dict = None
url_name = None
class EntityViewSetInheritedCases(
ViewSetTestCases,
):
model = None
kwargs_create_item: dict = None
kwargs_create_item_diff_org: dict = None
url_name = '_api_v2_entity_sub'
@classmethod
def setUpTestData(self):
self.url_kwargs = {
'entity_model': self.model._meta.model_name
}
self.url_view_kwargs = {
'entity_model': self.model._meta.model_name
}
super().setUpTestData()
class EntityViewSetTest(
ViewSetTestCases,
TestCase,
):
kwargs_create_item: dict = {}
kwargs_create_item_diff_org: dict = {}
model = Entity
url_kwargs: dict = {}
url_view_kwargs: dict = {}
url_name = '_api_v2_entity'
class MetadataTestCases(
ViewSetBase,
MetadataAttributesFunctional,
):
kwargs_create_item: dict = None
kwargs_create_item_diff_org: dict = None
model = None
url_kwargs: dict = None
url_view_kwargs: dict = None
url_name = None
class EntityMetadataInheritedCases(
MetadataTestCases,
):
model = None
kwargs_create_item: dict = None
kwargs_create_item_diff_org: dict = None
url_name = '_api_v2_entity_sub'
@classmethod
def setUpTestData(self):
self.url_kwargs = {
'entity_model': self.model._meta.model_name
}
self.url_view_kwargs = {
'entity_model': self.model._meta.model_name
}
super().setUpTestData()
class EntityMetadataTest(
MetadataTestCases,
TestCase,
):
kwargs_create_item: dict = {}
kwargs_create_item_diff_org: dict = {}
model = Entity
url_kwargs: dict = {}
url_view_kwargs: dict = {}
url_name = '_api_v2_entity'
# def test_method_options_request_detail_data_has_key_urls_back(self):
# """Test HTTP/Options Method
# Ensure the request data returned has key `urls.back`
# """
# client = Client()
# client.force_login(self.view_user)
# response = client.options(
# reverse(
# self.app_namespace + ':' + self.url_name + '-detail',
# kwargs=self.url_view_kwargs
# ),
# content_type='application/json'
# )
# assert 'back' in response.data['urls']
# def test_method_options_request_detail_data_key_urls_back_is_str(self):
# """Test HTTP/Options Method
# Ensure the request data key `urls.back` is str
# """
# client = Client()
# client.force_login(self.view_user)
# response = client.options(
# reverse(
# self.app_namespace + ':' + self.url_name + '-detail',
# kwargs=self.url_view_kwargs
# ),
# content_type='application/json'
# )
# assert type(response.data['urls']['back']) is str
# def test_method_options_request_list_data_has_key_urls_return_url(self):
# """Test HTTP/Options Method
# Ensure the request data returned has key `urls.return_url`
# """
# client = Client()
# client.force_login(self.view_user)
# if self.url_kwargs:
# url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.url_kwargs)
# else:
# url = reverse(self.app_namespace + ':' + self.url_name + '-list')
# response = client.options( url, content_type='application/json' )
# assert 'return_url' in response.data['urls']
# def test_method_options_request_list_data_key_urls_return_url_is_str(self):
# """Test HTTP/Options Method
# Ensure the request data key `urls.return_url` is str
# """
# client = Client()
# client.force_login(self.view_user)
# if self.url_kwargs:
# url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.url_kwargs)
# else:
# url = reverse(self.app_namespace + ':' + self.url_name + '-list')
# response = client.options( url, content_type='application/json' )
# assert type(response.data['urls']['return_url']) is str

View File

@ -0,0 +1,81 @@
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from access.models.entity_notes import Entity, EntityNotes
from core.tests.abstract.model_notes_api_fields import ModelNotesNotesAPIFields
class NotesAPITestCases(
ModelNotesNotesAPIFields,
):
entity_model = None
model = EntityNotes
kwargs_model_create: dict = None
# url_view_kwargs: dict = None
view_name: str = '_api_v2_entity_note'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Call parent setup
2. Create a model note
3. add url kwargs
4. make the API request
"""
super().setUpTestData()
self.item = self.model.objects.create(
organization = self.organization,
content = 'a random comment',
content_type = ContentType.objects.get(
app_label = str(self.model._meta.app_label).lower(),
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
),
model = self.entity_model.objects.create(
organization = self.organization,
model_notes = 'text',
**self.kwargs_model_create
),
created_by = self.view_user,
modified_by = self.view_user,
)
self.url_view_kwargs = {
'model_id': self.item.model.pk,
'pk': self.item.pk
}
self.make_request()
class EntityNotesAPIInheritedCases(
NotesAPITestCases,
):
entity_model = None
kwargs_model_create = None
class EntityNotesAPITest(
NotesAPITestCases,
TestCase,
):
entity_model = Entity
kwargs_model_create = {}

View File

@ -0,0 +1,162 @@
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from access.viewsets.entity_notes import ViewSet
from core.tests.abstract.test_functional_notes_viewset import (
ModelNotesViewSetBase,
ModelNotesMetadata,
ModelNotesPermissionsAPI,
ModelNotesSerializer
)
class ViewSetBase(
ModelNotesViewSetBase
):
viewset = ViewSet
kwargs_create_model_item: dict = {}
kwargs_create_model_item_other_org: dict = {}
url_name = '_api_v2_entity_note'
@classmethod
def setUpTestData(self):
super().setUpTestData()
self.item = self.viewset.model.objects.create(
organization = self.organization,
content = 'a random comment',
content_type = ContentType.objects.get(
app_label = str(self.model._meta.app_label).lower(),
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
),
model = self.viewset.model.model.field.related_model.objects.create(
organization = self.organization,
model_notes = 'text',
**self.kwargs_create_model_item
),
created_by = self.view_user,
modified_by = self.view_user,
)
self.other_org_item = self.viewset.model.objects.create(
organization = self.different_organization,
content = 'a random comment',
content_type = ContentType.objects.get(
app_label = str(self.model._meta.app_label).lower(),
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
),
model = self.viewset.model.model.field.related_model.objects.create(
organization = self.organization,
model_notes = 'text',
**self.kwargs_create_model_item_other_org
),
created_by = self.view_user,
modified_by = self.view_user,
)
self.url_kwargs = {
'model_id': self.item.model.pk,
}
self.url_view_kwargs = {
'model_id': self.item.model.pk,
'pk': self.item.id
}
class NotesPermissionsAPITestCases(
ViewSetBase,
ModelNotesPermissionsAPI,
):
viewset = None
def test_returned_data_from_user_and_global_organizations_only(self):
"""Check items returned
This test case is a over-ride of a test case with the same name.
This model is not a global model making this test not-applicable.
Items returned from the query Must be from the users organization and
global ONLY!
"""
pass
class EntityNotesPermissionsAPIInheritedCases(
NotesPermissionsAPITestCases,
):
viewset = None
class EntityNotesPermissionsAPITest(
NotesPermissionsAPITestCases,
TestCase,
):
viewset = ViewSet
class NotesSerializerTestCases(
ViewSetBase,
ModelNotesSerializer,
):
viewset = None
class EntityNotesSerializerInheritedCases(
NotesSerializerTestCases,
):
viewset = None
class EntityNotesSerializerTest(
NotesSerializerTestCases,
TestCase,
):
viewset = ViewSet
class NotesMetadataTestCases(
ViewSetBase,
ModelNotesMetadata,
):
viewset = None
class EntityNotesMetadataInheritedCases(
NotesMetadataTestCases,
):
viewset = None
class EntityNotesMetadataTest(
NotesMetadataTestCases,
TestCase,
):
viewset = ViewSet

View File

@ -0,0 +1,65 @@
from django.test import TestCase
from access.models.person import Person
from access.tests.functional.entity.test_functional_entity_history import (
EntityHistoryInheritedCases
)
class PersonTestCases(
EntityHistoryInheritedCases,
):
field_name = 'model_notes'
kwargs_create_obj: dict = {
'f_name': 'Ian',
'm_name': 'Peter',
'l_name': 'Funny',
'dob': '2025-04-08',
}
kwargs_delete_obj: dict = {
'f_name': 'Ian',
'm_name': 'Peter',
'l_name': 'Weird',
'dob': '2025-04-08',
}
model = Person
class PersonHistoryInheritedCases(
PersonTestCases,
):
model = None
"""Entity model to test"""
kwargs_create_obj: dict = None
kwargs_delete_obj: dict = None
@classmethod
def setUpTestData(self):
self.kwargs_create_obj.update(
super().kwargs_create_obj
)
self.kwargs_delete_obj.update(
super().kwargs_delete_obj
)
super().setUpTestData()
class PersonHistoryTest(
PersonTestCases,
TestCase,
):
pass

View File

@ -0,0 +1,230 @@
import pytest
from django.test import TestCase
from rest_framework.exceptions import ValidationError
from access.serializers.person import (
Person,
ModelSerializer
)
from access.tests.functional.entity.test_functional_entity_serializer import (
EntitySerializerInheritedCases
)
class SerializerTestCases(
EntitySerializerInheritedCases,
):
create_model_serializer = ModelSerializer
"""Serializer to test"""
duplicate_f_name_l_name_dob: dict = {
'f_name': 'fred',
'm_name': 'D',
'l_name': 'Flinstone',
'dob': '2025-04-08',
}
kwargs_create_item_duplicate_f_name_l_name_dob: dict = {
'f_name': 'fred',
'm_name': 'D',
'l_name': 'Flinstone',
'dob': '2025-04-08',
}
kwargs_create_item: dict = {
'f_name': 'Ian',
'm_name': 'Peter',
'l_name': 'Funny',
'dob': '2025-04-08',
}
model = Person
"""Model to test"""
valid_data: dict = {
'f_name': 'Ian',
'm_name': 'Peter',
'l_name': 'Strange',
'dob': '2025-04-08',
}
def test_serializer_validation_no_f_name_exception(self):
"""Serializer Validation Check
Ensure that when creating with valid data and field f_name is missing
a validation error occurs.
"""
data = self.valid_data.copy()
del data['f_name']
with pytest.raises(ValidationError) as err:
serializer = self.create_model_serializer(
data = data
)
serializer.is_valid(raise_exception = True)
assert err.value.get_codes()['f_name'][0] == 'required'
def test_serializer_validation_no_m_name(self):
"""Serializer Validation Check
Ensure that when creating with valid data and field f_name is missing
no validation error occurs.
"""
data = self.valid_data.copy()
del data['m_name']
serializer = self.create_model_serializer(
data = data
)
assert serializer.is_valid(raise_exception = True)
def test_serializer_validation_no_l_name_exception(self):
"""Serializer Validation Check
Ensure that when creating with valid data and field l_name is missing
a validation error occurs.
"""
data = self.valid_data.copy()
del data['l_name']
with pytest.raises(ValidationError) as err:
serializer = self.create_model_serializer(
data = data
)
serializer.is_valid(raise_exception = True)
assert err.value.get_codes()['l_name'][0] == 'required'
def test_serializer_validation_no_dob(self):
"""Serializer Validation Check
Ensure that when creating with valid data and field dob is missing
no validation error occurs.
"""
data = self.valid_data.copy()
del data['dob']
serializer = self.create_model_serializer(
data = data
)
assert serializer.is_valid(raise_exception = True)
def test_serializer_validation_duplicate_f_name_l_name_dob(self):
"""Serializer Validation Check
Ensure that when creating with valid data and fields f_name, l_name and
dob already exists in the db a validation error occurs.
"""
self.model.objects.create(
organization = self.organization,
**self.kwargs_create_item_duplicate_f_name_l_name_dob
)
data = self.duplicate_f_name_l_name_dob.copy()
with pytest.raises(ValidationError) as err:
serializer = self.create_model_serializer(
data = data
)
serializer.is_valid(raise_exception = True)
serializer.save()
assert err.value.get_codes()['dob'] == 'duplicate_person_on_dob'
class PersonSerializerInheritedCases(
SerializerTestCases,
):
create_model_serializer = None
"""Serializer to test"""
duplicate_f_name_l_name_dob: dict = None
""" Duplicate model serializer dict
used for testing for duplicate f_name, l_name and dob fields.
"""
kwargs_create_item: dict = None
""" Model kwargs to create item"""
kwargs_create_item_duplicate_f_name_l_name_dob: dict = None
"""model kwargs to create object
**None:** Ensure that the fields of sub-model to person do not match
`self.duplicate_f_name_l_name_dob`. if they do the wrong exception will be thrown.
used for testing for duplicate f_name, l_name and dob fields.
"""
model = None
"""Model to test"""
valid_data: dict = None
"""Valid data used by serializer to create object"""
@classmethod
def setUpTestData(self):
"""Setup Test"""
self.duplicate_f_name_l_name_dob.update(
super().duplicate_f_name_l_name_dob
)
self.kwargs_create_item_duplicate_f_name_l_name_dob.update(
super().kwargs_create_item_duplicate_f_name_l_name_dob
)
self.kwargs_create_item.update(
super().kwargs_create_item
)
self.valid_data.update(
super().valid_data
)
super().setUpTestData()
class PersonSerializerTest(
SerializerTestCases,
TestCase,
):
pass

View File

@ -0,0 +1,178 @@
from django.test import TestCase
from access.models.person import Person
from access.tests.functional.entity.test_functional_entity_viewset import (
EntityMetadataInheritedCases,
EntityPermissionsAPIInheritedCases,
EntityViewSetInheritedCases
)
class ViewSetBase:
add_data = {
'f_name': 'Ian',
'm_name': 'Peter',
'l_name': 'Strange',
'dob': '2025-04-08',
}
kwargs_create_item_diff_org = {
'f_name': 'Ian',
'm_name': 'Peter',
'l_name': 'Funny',
'dob': '2025-04-08',
}
kwargs_create_item = {
'f_name': 'Ian',
'm_name': 'Peter',
'l_name': 'Weird',
'dob': '2025-04-08',
}
model = Person
url_kwargs: dict = {}
url_view_kwargs: dict = {}
class PermissionsAPITestCases(
ViewSetBase,
EntityPermissionsAPIInheritedCases,
):
pass
class PersonPermissionsAPIInheritedCases(
PermissionsAPITestCases,
):
add_data: dict = None
model = None
kwargs_create_item: dict = None
kwargs_create_item_diff_org: dict = None
@classmethod
def setUpTestData(self):
self.add_data.update(
super().add_data
)
self.kwargs_create_item.update(
super().kwargs_create_item
)
self.kwargs_create_item_diff_org.update(
super().kwargs_create_item_diff_org
)
super().setUpTestData()
class PersonPermissionsAPITest(
PermissionsAPITestCases,
TestCase,
):
pass
class ViewSetTestCases(
ViewSetBase,
EntityViewSetInheritedCases,
):
pass
class PersonViewSetInheritedCases(
ViewSetTestCases,
):
model = None
kwargs_create_item: dict = None
kwargs_create_item_diff_org: dict = None
@classmethod
def setUpTestData(self):
self.kwargs_create_item.update(
super().kwargs_create_item
)
self.kwargs_create_item_diff_org.update(
super().kwargs_create_item_diff_org
)
super().setUpTestData()
class PersonViewSetTest(
ViewSetTestCases,
TestCase,
):
pass
class MetadataTestCases(
ViewSetBase,
EntityMetadataInheritedCases,
):
pass
class PersonMetadataInheritedCases(
MetadataTestCases,
):
model = None
kwargs_create_item: dict = None
kwargs_create_item_diff_org: dict = None
@classmethod
def setUpTestData(self):
self.kwargs_create_item.update(
super().kwargs_create_item
)
self.kwargs_create_item_diff_org.update(
super().kwargs_create_item_diff_org
)
super().setUpTestData()
class PersonMetadataTest(
MetadataTestCases,
TestCase,
):
pass

View File

@ -0,0 +1,55 @@
from django.test import TestCase
from access.models.role_history import Role, RoleHistory
from core.tests.abstract.test_functional_history import HistoryEntriesCommon
class HistoryTestCases(
HistoryEntriesCommon,
):
history_model = RoleHistory
kwargs_create_obj: dict = {}
kwargs_delete_obj: dict = {}
model = Role
@classmethod
def setUpTestData(self):
super().setUpTestData()
self.obj = self.model.objects.create(
organization = self.organization,
model_notes = self.field_value_original,
**self.kwargs_create_obj,
)
self.obj_delete = self.model.objects.create(
organization = self.organization,
model_notes = 'another note',
**self.kwargs_delete_obj,
)
self.call_the_banners()
class RoleHistoryTest(
HistoryTestCases,
TestCase,
):
kwargs_create_obj: dict = {
'name': 'original_name'
}
kwargs_delete_obj: dict = {
'name': 'delete obj'
}

View File

@ -0,0 +1,83 @@
import pytest
from django.test import TestCase
from rest_framework.exceptions import ValidationError
from access.models.organization import Organization
from access.serializers.role import Role, ModelSerializer
class ValidationSerializer(
TestCase,
):
model = Role
serializer = ModelSerializer
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an org
2. Create an item
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.diff_organization = Organization.objects.create(name='test_org_diff_org')
self.item = self.model.objects.create(
organization = self.organization,
name = 'one',
)
self.valid_data = {
'organization': self.organization.id,
'name': 'two',
'model_notes': 'dfsdfsd',
}
def test_serializer_validation_valid_data(self):
"""Serializer Validation Check
Ensure that if creating and no name is provided a validation error occurs
"""
serializer = self.serializer(
data = self.valid_data
)
assert serializer.is_valid( raise_exception = True )
def test_serializer_validation_no_name_exception(self):
"""Serializer Validation Check
Ensure that when creating and field name is not provided a
validation error occurs
"""
valid_data = self.valid_data.copy()
del valid_data['name']
with pytest.raises(ValidationError) as err:
serializer = self.serializer(
data = valid_data
)
serializer.is_valid(raise_exception = True)
assert err.value.get_codes()['name'][0] == 'required'

View File

@ -0,0 +1,283 @@
from django.contrib.auth.models import Permission, User
from django.contrib.contenttypes.models import ContentType
from django.test import Client, TestCase
from rest_framework.reverse import reverse
from access.models.role import Role
from access.models.organization import Organization
from access.models.team import Team
from access.models.team_user import TeamUsers
from api.tests.abstract.test_metadata_functional import MetadataAttributesFunctional
from api.tests.abstract.api_permissions_viewset import APIPermissions
from api.tests.abstract.api_serializer_viewset import SerializersTestCases
from settings.models.app_settings import AppSettings
class ViewSetBase:
add_data: dict = None
app_namespace = 'v2'
change_data = { 'name': 'changed name' }
delete_data = {}
kwargs_create_item: dict = {}
kwargs_create_item_diff_org: dict = {}
kwargs_create_item_global_org_org: dict = {}
model = None
url_kwargs: dict = None
url_view_kwargs: dict = None
url_name = None
@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
self.different_organization = Organization.objects.create(name='test_different_organization')
self.global_organization = Organization.objects.create(name='test_global_organization')
app_settings = AppSettings.objects.get(
owner_organization = None
)
app_settings.global_organization = self.global_organization
app_settings.save()
self.item = self.model.objects.create(
organization = organization,
model_notes = 'some notes',
**self.kwargs_create_item
)
self.other_org_item = self.model.objects.create(
organization = self.different_organization,
model_notes = 'some more notes',
**self.kwargs_create_item_diff_org
)
self.global_org_item = self.model.objects.create(
organization = self.global_organization,
model_notes = 'some more notes',
**self.kwargs_create_item_global_org_org
)
# self.url_kwargs = {'organization_id': self.organization.id}
self.url_view_kwargs.update({ 'pk': self.item.id })
if self.add_data is not None:
self.add_data.update({'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")
TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
self.add_user = User.objects.create_user(username="test_user_add", password="password")
TeamUsers.objects.create(
team = add_team,
user = self.add_user
)
self.change_user = User.objects.create_user(username="test_user_change", password="password")
TeamUsers.objects.create(
team = change_team,
user = self.change_user
)
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
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 = self.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
)
class RolePermissionsAPITest(
ViewSetBase,
APIPermissions,
TestCase,
):
add_data: dict = { 'name': 'added model note' }
kwargs_create_item: dict = { 'name': 'create item' }
kwargs_create_item_diff_org: dict = { 'name': 'diff org create' }
kwargs_create_item_global_org_org: dict = { 'name': 'global org create' }
model = Role
url_kwargs: dict = {}
url_view_kwargs: dict = {}
url_name = '_api_v2_role'
class RoleViewSetTest(
ViewSetBase,
SerializersTestCases,
TestCase,
):
kwargs_create_item: dict = { 'name': 'create item' }
kwargs_create_item_diff_org: dict = { 'name': 'diff org create' }
kwargs_create_item_global_org_org: dict = { 'name': 'global org create' }
model = Role
url_kwargs: dict = {}
url_view_kwargs: dict = {}
url_name = '_api_v2_role'
class RoleMetadataTest(
ViewSetBase,
MetadataAttributesFunctional,
TestCase,
):
kwargs_create_item: dict = { 'name': 'create item' }
kwargs_create_item_diff_org: dict = { 'name': 'diff org create' }
kwargs_create_item_global_org_org: dict = { 'name': 'global org create' }
model = Role
url_kwargs: dict = {}
url_view_kwargs: dict = {}
url_name = '_api_v2_role'

View File

@ -0,0 +1,32 @@
from django.test import TestCase
from access.models.contact import Contact
from access.tests.unit.person.test_unit_person_access_tenancy_object import (
PersonTenancyObjectInheritedCases,
)
class TenancyObjectTestCases(
PersonTenancyObjectInheritedCases,
):
model = Contact
class ContactTenancyObjectInheritedCases(
TenancyObjectTestCases,
):
"""Sub-Entity Test Cases
Test Cases for Entity models that inherit from model Contact
"""
model = None
class ContactTenancyObjectTest(
TenancyObjectTestCases,
TestCase,
):
pass

View File

@ -0,0 +1,90 @@
from django.test import TestCase
from access.models.contact import Contact
from access.tests.unit.person.test_unit_person_api_v2 import (
PersonAPIInheritedCases,
)
class APITestCases(
PersonAPIInheritedCases,
):
model = Contact
kwargs_item_create: dict = {
'email': 'ipfunny@unit.test',
}
url_ns_name = '_api_v2_entity_sub'
def test_api_field_exists_email(self):
""" Test for existance of API Field
email field must exist
"""
assert 'email' in self.api_data
def test_api_field_type_email(self):
""" Test for type for API Field
email field must be str
"""
assert type(self.api_data['email']) is str
def test_api_field_exists_directory(self):
""" Test for existance of API Field
directory field must exist
"""
assert 'directory' in self.api_data
def test_api_field_type_directory(self):
""" Test for type for API Field
directory field must be bool
"""
assert type(self.api_data['directory']) is bool
class ContactAPIInheritedCases(
APITestCases,
):
"""Sub-Entity Test Cases
Test Cases for Entity models that inherit from model Contact
"""
kwargs_item_create: dict = None
model = None
@classmethod
def setUpTestData(self):
self.kwargs_item_create.update(
super().kwargs_item_create
)
super().setUpTestData()
class ContactAPITest(
APITestCases,
TestCase,
):
pass

View File

@ -0,0 +1,56 @@
from django.test import TestCase
from access.models.contact import Contact
from access.tests.unit.person.test_unit_person_history_api_v2 import (
PersonHistoryAPIInheritedCases
)
class ContactModelHistoryAPITestCases(
PersonHistoryAPIInheritedCases,
):
""" Model Histoy Test Cases
Test must be setup by creating object `kwargs_create_audit_object` with the
attributes required to create the object.
"""
audit_model = Contact
kwargs_create_audit_object: dict = {
'email': 'ipfunny@unit.test',
}
class ContactHistoryAPIInheritedCases(
ContactModelHistoryAPITestCases,
):
"""Sub-Entity Test Cases
Test Cases for Entity models that inherit from model Contact
"""
audit_model = None
kwargs_create_audit_object: dict = None
@classmethod
def setUpTestData(self):
self.kwargs_create_audit_object.update(
super().kwargs_create_audit_object
)
super().setUpTestData()
class ContactModelHistoryAPITest(
ContactModelHistoryAPITestCases,
TestCase,
):
pass

View File

@ -0,0 +1,100 @@
from django.db.models.fields import NOT_PROVIDED
from django.test import TestCase
from access.models.contact import Contact
from access.tests.unit.person.test_unit_person_model import (
Person,
PersonModelInheritedCases
)
class ModelTestCases(
PersonModelInheritedCases,
):
model = Contact
kwargs_item_create: dict = {
'email': 'ipweird@unit.test',
}
def test_model_field_directory_optional(self):
"""Test Field
Field `dob` must be an optional field
"""
assert self.model._meta.get_field('directory').blank
def test_model_field_directory_optional_default(self):
"""Test Field
Field `directory` default value is `True`
"""
assert (
self.model._meta.get_field('directory').default is True
and self.model._meta.get_field('directory').null is False
)
def test_model_field_email_mandatory(self):
"""Test Field
Field `email` must be a mandatory field
"""
assert(
not (
self.model._meta.get_field('email').blank
and self.model._meta.get_field('email').null
)
and self.model._meta.get_field('email').default is NOT_PROVIDED
)
def test_model_inherits_person(self):
"""Test model inheritence
model must inherit from Entity sub-model `Person`
"""
assert issubclass(self.model, Person)
class ContactModelInheritedCases(
ModelTestCases,
):
"""Sub-Entity Test Cases
Test Cases for Entity models that inherit from model Contact
"""
kwargs_item_create: dict = None
model = None
@classmethod
def setUpTestData(self):
self.kwargs_item_create.update(
super().kwargs_item_create
)
super().setUpTestData()
class ContactModelTest(
ModelTestCases,
TestCase,
):
pass

View File

@ -0,0 +1,36 @@
from django.test import TestCase
from access.models.contact import Contact
from access.tests.unit.entity.test_unit_entity_viewset import (
EntityViewsetInheritedCases
)
class ViewsetTestCases(
EntityViewsetInheritedCases,
):
model: str = Contact
class ContactViewsetInheritedCases(
ViewsetTestCases,
):
"""Sub-Entity Test Cases
Test Cases for Entity models that inherit from model Contact
"""
model: str = None
"""name of the model to test"""
class ContactViewsetTest(
ViewsetTestCases,
TestCase,
):
pass

View File

@ -0,0 +1,29 @@
from django.test import TestCase
from access.models.entity import Entity
from access.tests.abstract.tenancy_object import TenancyObject
class TenancyObjectTestCases(
TenancyObject,
):
model = None
class EntityTenancyObjectInheritedCases(
TenancyObjectTestCases,
):
model = None
class EntityTenancyObjectTest(
TenancyObjectTestCases,
TestCase,
):
model = Entity

View File

@ -0,0 +1,215 @@
from django.contrib.auth.models import Permission, 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.entity import Entity
from access.models.organization import Organization
from access.models.team import Team
from access.models.team_user import TeamUsers
from api.tests.abstract.api_fields import APITenancyObject
class APITestCases(
APITenancyObject,
):
model = None
kwargs_item_create: dict = None
url_ns_name = None
"""Url namespace (optional, if not required) and url name"""
@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,
model_notes = 'random notes',
**self.kwargs_item_create
)
self.url_view_kwargs = {
'pk': self.item.id
}
if self.model._meta.model_name != 'entity':
self.url_view_kwargs.update({
'entity_model': self.item.entity_type,
})
view_permissions = Permission.objects.get(
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = self.organization,
)
view_team.permissions.set([view_permissions])
self.view_user = User.objects.create_user(username="test_user_view", password="password")
TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
client = Client()
url = reverse('v2:' + self.url_ns_name + '-detail', kwargs=self.url_view_kwargs)
client.force_login(self.view_user)
response = client.get(url)
self.api_data = response.data
def test_api_field_exists_entity_type(self):
""" Test for existance of API Field
entity_type field must exist
"""
assert 'entity_type' in self.api_data
def test_api_field_type_entity_type(self):
""" Test for type for API Field
entity_type field must be str
"""
assert type(self.api_data['entity_type']) is str
def test_api_field_exists_url_history(self):
""" Test for existance of API Field
_urls.history field must exist
"""
assert 'history' in self.api_data['_urls']
def test_api_field_type_url_history(self):
""" Test for type for API Field
_urls.history field must be str
"""
assert type(self.api_data['_urls']['history']) is str
def test_api_field_type_url_history_value(self):
""" Test for url value
_urls.history field must use the endpoint for entity model
"""
assert str(self.api_data['_urls']['history']).endswith('/access/entity/' + str(self.item.pk) + '/history')
def test_api_field_exists_url_knowledge_base(self):
""" Test for existance of API Field
_urls.knowledge_base field must exist
"""
assert 'knowledge_base' in self.api_data['_urls']
def test_api_field_type_url_knowledge_base(self):
""" Test for type for API Field
_urls.knowledge_base field must be str
"""
assert type(self.api_data['_urls']['knowledge_base']) is str
def test_api_field_type_url_knowledge_base_value(self):
""" Test for url value
_urls.knowledge_base field must use the endpoint for entity model
"""
assert str(self.api_data['_urls']['knowledge_base']).endswith('/assistance/entity/' + str(self.item.pk) + '/knowledge_base')
class EntityAPIInheritedCases(
APITestCases,
):
kwargs_item_create: dict = None
model = None
url_ns_name = '_api_v2_entity_sub'
@classmethod
def setUpTestData(self):
self.kwargs_item_create.update({
'entity_type': self.model._meta.model_name
})
super().setUpTestData()
def test_api_field_exists_entity_value(self):
""" Test for value of API Field
entity_type field must match model name
"""
assert self.api_data['entity_type'] == self.model._meta.model_name
class EntityAPITest(
APITestCases,
TestCase,
):
kwargs_item_create: dict = None
model = Entity
url_ns_name = '_api_v2_entity'
@classmethod
def setUpTestData(self):
self.kwargs_item_create = {
'entity_type': 'entity'
}
super().setUpTestData()

View File

@ -0,0 +1,82 @@
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from access.models.entity_history import Entity, EntityHistory
from core.tests.abstract.test_unit_model_history_api_v2 import PrimaryModelHistoryAPI
class ModelHistoryAPITestCases(
PrimaryModelHistoryAPI,
):
""" Model Histoy Test Cases
Test must be setup by creating object `kwargs_create_audit_object` with the
attributes required to create the object.
"""
audit_model = None
kwargs_create_audit_object: dict = {}
model = EntityHistory
@classmethod
def setUpTestData(self):
super().setUpTestData()
self.audit_object = self.audit_model.objects.create(
organization = self.organization,
entity_type = self.audit_model._meta.model_name,
**self.kwargs_create_audit_object
)
self.history_entry = self.model.objects.create(
organization = self.audit_object.organization,
action = self.model.Actions.ADD,
user = self.view_user,
before = {},
after = {},
content_type = ContentType.objects.get(
app_label = self.audit_object._meta.app_label,
model = self.audit_object._meta.model_name,
),
model = self.audit_object,
)
self.make_request()
class EntityModelHistoryAPIInheritedCases(
ModelHistoryAPITestCases,
):
audit_model = None
kwargs_create_audit_object: dict = None
@classmethod
def setUpTestData(self):
self.kwargs_create_audit_object.update(
super().kwargs_create_audit_object
)
super().setUpTestData()
class EntityModelHistoryAPITest(
ModelHistoryAPITestCases,
TestCase,
):
audit_model = Entity
kwargs_create_audit_object: dict = {}

View File

@ -0,0 +1,64 @@
from django.test import TestCase
from access.models.entity import Entity
from access.models.organization import Organization
from app.tests.abstract.models import TenancyModel
class ModelTestCases(
TenancyModel,
):
model = Entity
kwargs_item_create: dict = {}
@classmethod
def setUpTestData(self):
"""Setup Test"""
self.organization = Organization.objects.create(name='test_org')
different_organization = Organization.objects.create(name='test_different_organization')
self.item = self.model.objects.create(
organization = self.organization,
model_notes = 'notes',
entity_type = self.model._meta.model_name,
**self.kwargs_item_create,
)
class EntityModelInheritedCases(
ModelTestCases,
):
"""Sub-Entity Test Cases
Test Cases for Entity models that inherit from model Entity
"""
kwargs_item_create: dict = None
model = None
@classmethod
def setUpTestData(self):
self.kwargs_item_create.update(
super().kwargs_item_create
)
super().setUpTestData()
class EntityModelTest(
ModelTestCases,
TestCase,
):
pass

View File

@ -0,0 +1,82 @@
from django.test import Client, TestCase
from rest_framework.reverse import reverse
from access.viewsets.entity import (
NoDocsViewSet,
ViewSet,
)
from api.tests.unit.test_unit_common_viewset import ModelViewSetInheritedCases
class ViewsetTestCases(
ModelViewSetInheritedCases,
):
kwargs = None
viewset = None
route_name = None
@classmethod
def setUpTestData(self):
"""Setup Test
1. make list request
"""
super().setUpTestData()
client = Client()
url = reverse(
self.route_name + '-list',
kwargs = self.kwargs
)
client.force_login(self.view_user)
self.http_options_response_list = client.options(url)
class EntityViewsetInheritedCases(
ViewsetTestCases,
):
model: str = None
"""name of the model to test"""
route_name = 'API:_api_v2_entity_sub'
viewset = ViewSet
@classmethod
def setUpTestData(self):
self.kwargs = {
'entity_model': self.model._meta.model_name
}
super().setUpTestData()
class EntityViewsetTest(
ViewsetTestCases,
TestCase,
):
kwargs = {}
route_name = 'API:_api_v2_entity'
viewset = NoDocsViewSet

View File

@ -1,48 +1,22 @@
import pytest
from django.contrib.auth.models import User
from django.test import Client, TestCase
from rest_framework.reverse import reverse
from access.models.organization import Organization
from access.viewsets.organization import ViewSet
from api.tests.abstract.viewsets import ViewSetModel
from api.tests.unit.test_unit_common_viewset import ModelViewSetInheritedCases
class ViewsetCommon(
ViewSetModel,
class OrganizationViewsetList(
ModelViewSetInheritedCases,
TestCase,
):
viewset = ViewSet
route_name = 'API:_api_v2_organization'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization
3. create super user
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.view_user = User.objects.create_user(username="test_view_user", password="password", is_superuser=True)
self.kwargs = {}
class OrganizationViewsetList(
ViewsetCommon,
TestCase,
):
@classmethod
def setUpTestData(self):

View File

@ -1,47 +1,22 @@
import pytest
from django.contrib.auth.models import User
from django.test import Client, TestCase
from rest_framework.reverse import reverse
from access.models.organization import Organization
from access.viewsets.team_notes import ViewSet
from api.tests.abstract.viewsets import ViewSetModel
from api.tests.unit.test_unit_common_viewset import ModelViewSetInheritedCases
class ViewsetCommon(
ViewSetModel,
class OrganizationNotesViewsetList(
ModelViewSetInheritedCases,
TestCase,
):
viewset = ViewSet
route_name = 'v2:_api_v2_organization_note'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization
3. create super user
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.view_user = User.objects.create_user(username="test_view_user", password="password", is_superuser=True)
class OrganizationNotesViewsetList(
ViewsetCommon,
TestCase,
):
@classmethod
def setUpTestData(self):

View File

@ -0,0 +1,32 @@
from django.test import TestCase
from access.models.person import Person
from access.tests.unit.entity.test_unit_entity_access_tenancy_object import (
EntityTenancyObjectInheritedCases,
)
class TenancyObjectTestCases(
EntityTenancyObjectInheritedCases,
):
model = Person
class PersonTenancyObjectInheritedCases(
TenancyObjectTestCases,
):
"""Sub-Entity Test Cases
Test Cases for Entity models that inherit from model Person
"""
model = None
class PersonTenancyObjectTest(
TenancyObjectTestCases,
TestCase,
):
pass

View File

@ -0,0 +1,127 @@
from django.test import TestCase
from access.models.person import Person
from access.tests.unit.entity.test_unit_entity_api_v2 import (
EntityAPIInheritedCases,
)
class APITestCases(
EntityAPIInheritedCases,
):
model = Person
kwargs_item_create: dict = {}
url_ns_name = '_api_v2_entity_sub'
@classmethod
def setUpTestData(self):
self.kwargs_item_create.update({
'f_name': 'Ian',
'm_name': 'Peter',
'l_name': 'Funny',
'dob': '2025-04-08',
})
super().setUpTestData()
def test_api_field_exists_f_name(self):
""" Test for existance of API Field
f_name field must exist
"""
assert 'f_name' in self.api_data
def test_api_field_type_f_name(self):
""" Test for type for API Field
f_name field must be str
"""
assert type(self.api_data['f_name']) is str
def test_api_field_exists_m_name(self):
""" Test for existance of API Field
m_name field must exist
"""
assert 'm_name' in self.api_data
def test_api_field_type_f_name(self):
""" Test for type for API Field
m_name field must be str
"""
assert type(self.api_data['m_name']) is str
def test_api_field_exists_l_name(self):
""" Test for existance of API Field
l_name field must exist
"""
assert 'l_name' in self.api_data
def test_api_field_type_f_name(self):
""" Test for type for API Field
l_name field must be str
"""
assert type(self.api_data['l_name']) is str
def test_api_field_exists_dob(self):
""" Test for existance of API Field
dob field must exist
"""
assert 'dob' in self.api_data
def test_api_field_type_dob(self):
""" Test for type for API Field
dob field must be str
"""
assert type(self.api_data['dob']) is str
class PersonAPIInheritedCases(
APITestCases,
):
"""Sub-Entity Test Cases
Test Cases for Entity models that inherit from model Person
"""
kwargs_item_create: dict = None
model = None
class PersonAPITest(
APITestCases,
TestCase,
):
pass

View File

@ -0,0 +1,66 @@
from django.test import TestCase
from access.models.person import Person
from access.tests.unit.entity.test_unit_entity_history_api_v2 import (
EntityModelHistoryAPIInheritedCases
)
class PersonModelHistoryAPITestCases(
EntityModelHistoryAPIInheritedCases,
):
""" Model Histoy Test Cases
Test must be setup by creating object `kwargs_create_audit_object` with the
attributes required to create the object.
"""
audit_model = Person
kwargs_create_audit_object: dict = {
'f_name': 'Ian',
'm_name': 'Peter',
'l_name': 'Funny',
'dob': '2025-04-08',
}
class PersonHistoryAPIInheritedCases(
PersonModelHistoryAPITestCases,
):
"""Sub-Entity Test Cases
Test Cases for Entity models that inherit from model Person
"""
audit_model = None
kwargs_create_audit_object: dict = None
@classmethod
def setUpTestData(self):
self.kwargs_create_audit_object.update(
super().kwargs_create_audit_object
)
super().setUpTestData()
class PersonModelHistoryAPITest(
PersonModelHistoryAPITestCases,
TestCase,
):
audit_model = Person
kwargs_create_audit_object: dict = {
'f_name': 'Ian',
'm_name': 'Peter',
'l_name': 'Funny',
'dob': '2025-04-08',
}

View File

@ -0,0 +1,95 @@
from django.db.models.fields import NOT_PROVIDED
from django.test import TestCase
from access.models.person import Person
from access.tests.unit.entity.test_unit_entity_model import (
EntityModelInheritedCases
)
class ModelTestCases(
EntityModelInheritedCases,
):
model = Person
kwargs_item_create: dict = {
'f_name': 'Ian',
'm_name': 'Peter',
'l_name': 'Funny',
'dob': '2025-04-08',
}
def test_model_field_dob_optional(self):
"""Test Field
Field `dob` must be an optional field
"""
assert self.model._meta.get_field('dob').blank
def test_model_field_f_name_mandatory(self):
"""Test Field
Field `f_name` must be a mandatory field
"""
assert(
not (
self.model._meta.get_field('f_name').blank
and self.model._meta.get_field('f_name').null
)
and self.model._meta.get_field('f_name').default is NOT_PROVIDED
)
def test_model_field_l_name_mandatory(self):
"""Test Field
Field `l_name` must be a mandatory field
"""
assert (
not (
self.model._meta.get_field('l_name').blank
and self.model._meta.get_field('l_name').null
)
and self.model._meta.get_field('l_name').default is NOT_PROVIDED
)
class PersonModelInheritedCases(
ModelTestCases,
):
"""Sub-Entity Test Cases
Test Cases for Entity models that inherit from model Person
"""
kwargs_item_create: dict = None
model = None
@classmethod
def setUpTestData(self):
self.kwargs_item_create.update(
super().kwargs_item_create
)
super().setUpTestData()
class PersonModelTest(
ModelTestCases,
TestCase,
):
pass

View File

@ -0,0 +1,36 @@
from django.test import TestCase
from access.models.person import Person
from access.tests.unit.entity.test_unit_entity_viewset import (
EntityViewsetInheritedCases
)
class ViewsetTestCases(
EntityViewsetInheritedCases,
):
model: str = Person
class PersonViewsetInheritedCases(
ViewsetTestCases,
):
"""Sub-Entity Test Cases
Test Cases for Entity models that inherit from model Person
"""
model: str = None
"""name of the model to test"""
class PersonViewsetTest(
ViewsetTestCases,
TestCase,
):
pass

View File

@ -0,0 +1,21 @@
from django.test import TestCase
from access.models.role import Role
from access.tests.abstract.tenancy_object import TenancyObject
class TenancyObjectTestCases(
TenancyObject,
):
model = None
class RoleTenancyObjectTest(
TenancyObjectTestCases,
TestCase,
):
model = Role

View File

@ -0,0 +1,172 @@
from django.contrib.auth.models import Permission, 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.role import Role
from access.models.organization import Organization
from access.models.team import Team
from access.models.team_user import TeamUsers
from api.tests.abstract.api_fields import APITenancyObject
class APITestCases(
APITenancyObject,
):
model = None
kwargs_item_create: dict = None
url_ns_name = None
"""Url namespace (optional, if not required) and url name"""
@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,
model_notes = 'random notes',
**self.kwargs_item_create
)
self.url_view_kwargs = {
'pk': self.item.id
}
# if self.model._meta.model_name != 'entity':
# self.url_view_kwargs.update({
# 'entity_model': self.item.entity_type,
# })
# if self.model._meta.model_name != 'entity':
# self.url_view_kwargs.update({
# 'entity_type': self.model._meta.model_name
# })
view_permissions = Permission.objects.get(
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = self.organization,
)
view_team.permissions.set([view_permissions])
self.view_user = User.objects.create_user(username="test_user_view", password="password")
TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
client = Client()
url = reverse('v2:' + self.url_ns_name + '-detail', kwargs=self.url_view_kwargs)
client.force_login(self.view_user)
response = client.get(url)
self.api_data = response.data
def test_api_field_exists_url_history(self):
""" Test for existance of API Field
_urls.history field must exist
"""
assert 'history' in self.api_data['_urls']
def test_api_field_type_url_history(self):
""" Test for type for API Field
_urls.history field must be str
"""
assert type(self.api_data['_urls']['history']) is str
def test_api_field_type_url_history_value(self):
""" Test for url value
_urls.history field must use the endpoint for entity model
"""
assert str(self.api_data['_urls']['history']).endswith('/access/role/' + str(self.item.pk) + '/history')
def test_api_field_exists_url_knowledge_base(self):
""" Test for existance of API Field
_urls.knowledge_base field must exist
"""
assert 'knowledge_base' in self.api_data['_urls']
def test_api_field_type_url_knowledge_base(self):
""" Test for type for API Field
_urls.knowledge_base field must be str
"""
assert type(self.api_data['_urls']['knowledge_base']) is str
def test_api_field_type_url_knowledge_base_value(self):
""" Test for url value
_urls.knowledge_base field must use the endpoint for role model
"""
assert str(self.api_data['_urls']['knowledge_base']).endswith('/assistance/role/' + str(self.item.pk) + '/knowledge_base')
class RoleAPITest(
APITestCases,
TestCase,
):
kwargs_item_create: dict = None
model = Role
url_ns_name = '_api_v2_role'
@classmethod
def setUpTestData(self):
self.kwargs_item_create = {
'name': 'a role'
}
super().setUpTestData()

View File

@ -0,0 +1,72 @@
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from access.models.role_history import Role, RoleHistory
from core.tests.abstract.test_unit_model_history_api_v2 import PrimaryModelHistoryAPI
class ModelHistoryAPITestCases(
PrimaryModelHistoryAPI,
):
""" Model Histoy Test Cases
Test must be setup by creating object `kwargs_create_audit_object` with the
attributes required to create the object.
"""
audit_model = None
kwargs_create_audit_object: dict = None
model = None
@classmethod
def setUpTestData(self):
super().setUpTestData()
self.audit_object = self.audit_model.objects.create(
organization = self.organization,
**self.kwargs_create_audit_object
)
self.history_entry = self.model.objects.create(
organization = self.audit_object.organization,
action = self.model.Actions.ADD,
user = self.view_user,
before = {},
after = {},
content_type = ContentType.objects.get(
app_label = self.audit_object._meta.app_label,
model = self.audit_object._meta.model_name,
),
model = self.audit_object,
)
self.make_request()
class RoleHistoryAPITest(
ModelHistoryAPITestCases,
TestCase,
):
audit_model = Role
kwargs_create_audit_object: dict = {}
model = RoleHistory
@classmethod
def setUpTestData(self):
self.kwargs_create_audit_object = {
'name': 'a role'
}
super().setUpTestData()

View File

@ -0,0 +1,66 @@
from django.test import TestCase
from access.models.role import Role
from access.models.organization import Organization
from access.models.tenancy import TenancyObject
from app.tests.abstract.models import TenancyModel
class ModelTestCases(
TenancyModel,
):
model = None
kwargs_item_create: dict = None
@classmethod
def setUpTestData(self):
"""Setup Test"""
self.organization = Organization.objects.create(name='test_org')
different_organization = Organization.objects.create(name='test_different_organization')
self.item = self.model.objects.create(
organization = self.organization,
model_notes = 'notes',
**self.kwargs_item_create,
)
def test_field_not_exists_is_global(self):
"""Test model field not used
object must not be settable as a global object
Attribute `is_global` must be defined as None
"""
assert self.model.is_global is None
def test_model_must_be_by_organization(self):
"""Test model must be by organization
This model **must** be by organization.
"""
assert issubclass(self.model, TenancyObject)
class RoleModelTest(
ModelTestCases,
TestCase,
):
model = Role
kwargs_item_create: dict = {
'name': 'a role'
}

View File

@ -0,0 +1,62 @@
import pytest
# from pytest import MonkeyPatch
# from unittest.mock import patch
# from access.functions import permissions
# from access.serializers import role
# def mock_func(**kwargs):
# return 'is_called'
###############################################################################
#
# This test works when run alone, however not when all unit tests are run
# need to figure out how to correctly isolate the test.
#
###############################################################################
@pytest.mark.skip( reason = 'figure out how to isolate so entirety of unit tests can run without this test failing' )
# @pytest.mark.forked
# @pytest.mark.django_db
# @patch("access.functions.permissions.permission_queryset", return_value='no_called', side_effect=mock_func)
# @patch.object(role, "permission_queryset", return_value='no_called', side_effect=mock_func)
# @patch.object(permissions, "permission_queryset", return_value='no_called', side_effect=mock_func)
# @patch.object(role, "permission_queryset", side_effect=mock_func)
# @patch.object(globals()['role'], "permission_queryset", return_value='no_called', side_effect=mock_func)
# @patch.object(globals()['role'], "permission_queryset", side_effect=mock_func)
# @pytest.mark.forked # from `pip install pytest-forked`
# def test_serializer_field_permission_uses_permissions_selector(mocked_obj):
def test_serializer_field_permission_uses_permissions_selector(mocker):
# def test_serializer_field_permission_uses_permissions_selector(monkeypatch):
"""Field Permission Check
field `permission` must be called with `queryset=access.functions.permissions.permission_queryset()`
so that ONLY the designated permissions are visible
"""
def mock_func(**kwargs):
return 'is_called'
mocker.patch("access.functions.permissions.permission_queryset", return_value='no_called', side_effect=mock_func)
from access.serializers import role
# from access.serializers.role import permission_queryset, ModelSerializer
# monkey = MonkeyPatch().setattr(role, 'permission_queryset', mock_func)
# monkeypatch.setattr(role, 'permission_queryset', mock_func)
# monkey = MonkeyPatch.setattr('access.functions.permissions.permission_queryset', mock_func)
# monkeypatch.setattr(permissions, 'permission_queryset', mock_func)
serializer = role.ModelSerializer()
# if `return_value` exists, the function was not called
assert getattr(serializer.fields.fields['permissions'].child_relation.queryset, 'return_value', None) is None
#if `queryset == is_called` the function was called
assert serializer.fields.fields['permissions'].child_relation.queryset == 'is_called'

View File

@ -0,0 +1,56 @@
from django.test import Client, TestCase
from rest_framework.reverse import reverse
from access.viewsets.role import ViewSet
from api.tests.unit.test_unit_common_viewset import ModelViewSetInheritedCases
class ViewsetTestCases(
ModelViewSetInheritedCases,
):
kwargs = None
viewset = None
route_name = None
@classmethod
def setUpTestData(self):
"""Setup Test
1. make list request
"""
super().setUpTestData()
client = Client()
url = reverse(
self.route_name + '-list',
kwargs = self.kwargs
)
client.force_login(self.view_user)
self.http_options_response_list = client.options(url)
class RoleViewsetTest(
ViewsetTestCases,
TestCase,
):
kwargs = {}
route_name = 'v2:_api_v2_role'
viewset = ViewSet

View File

@ -1,48 +1,23 @@
import pytest
from django.contrib.auth.models import User
from django.test import Client, TestCase
from rest_framework.reverse import reverse
from access.models.organization import Organization
from access.viewsets.team import ViewSet
from api.tests.abstract.viewsets import ViewSetModel
from api.tests.unit.test_unit_common_viewset import ModelViewSetInheritedCases
class ViewsetCommon(
ViewSetModel,
class TeamViewsetList(
ModelViewSetInheritedCases,
TestCase,
):
viewset = ViewSet
route_name = 'API:_api_v2_organization_team'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization
3. create super user
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.view_user = User.objects.create_user(username="test_view_user", password="password", is_superuser=True)
self.kwargs = { 'organization_id': self.organization.id }
class TeamViewsetList(
ViewsetCommon,
TestCase,
):
@classmethod
def setUpTestData(self):
@ -54,6 +29,7 @@ class TeamViewsetList(
super().setUpTestData()
self.kwargs = { 'organization_id': self.organization.id }
client = Client()

View File

@ -1,47 +1,22 @@
import pytest
from django.contrib.auth.models import User
from django.test import Client, TestCase
from rest_framework.reverse import reverse
from access.models.organization import Organization
from access.viewsets.team_notes import ViewSet
from api.tests.abstract.viewsets import ViewSetModel
from api.tests.unit.test_unit_common_viewset import ModelViewSetInheritedCases
class ViewsetCommon(
ViewSetModel,
class TeamNotesViewsetList(
ModelViewSetInheritedCases,
TestCase,
):
viewset = ViewSet
route_name = 'v2:_api_v2_team_note'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization
3. create super user
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.view_user = User.objects.create_user(username="test_view_user", password="password", is_superuser=True)
class TeamNotesViewsetList(
ViewsetCommon,
TestCase,
):
@classmethod
def setUpTestData(self):

View File

@ -1,57 +1,23 @@
import pytest
from django.contrib.auth.models import User
from django.test import Client, TestCase
from rest_framework.reverse import reverse
from access.models.organization import Organization
from access.models.team import Team
from access.viewsets.team_user import ViewSet
from api.tests.abstract.viewsets import ViewSetModel
from api.tests.unit.test_unit_common_viewset import ModelViewSetInheritedCases
class ViewsetCommon(
ViewSetModel,
class TeamUserViewsetList(
ModelViewSetInheritedCases,
TestCase,
):
viewset = ViewSet
route_name = 'API:_api_v2_organization_team_user'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization
3. create super user
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.team = Team.objects.create(
organization = self.organization,
name = 'team'
)
self.view_user = User.objects.create_user(username="test_view_user", password="password", is_superuser=True)
self.kwargs = {
'organization_id': self.organization.id,
'team_id': self.team.id
}
class TeamUserViewsetList(
ViewsetCommon,
TestCase,
):
@classmethod
def setUpTestData(self):
@ -63,6 +29,15 @@ class TeamUserViewsetList(
super().setUpTestData()
self.team = Team.objects.create(
organization = self.organization,
name = 'team'
)
self.kwargs = {
'organization_id': self.organization.id,
'team_id': self.team.id
}
client = Client()

View File

@ -1,5 +1,4 @@
import pytest
import unittest
from django.test import TestCase
@ -8,11 +7,9 @@ from access.models.tenancy import TenancyObject
from core.mixin.history_save import SaveHistory
from unittest.mock import patch
class TenancyManagerTests(TestCase):
class TenancyManagerTest(TestCase):
item = TenancyManager
@ -30,7 +27,7 @@ class TenancyManagerTests(TestCase):
class TenancyObjectTests(TestCase):
class TenancyObjectTestCases:
item = TenancyObject
@ -44,6 +41,24 @@ class TenancyObjectTests(TestCase):
assert issubclass(TenancyObject, SaveHistory)
def test_has_attribute_history_app_label(self):
""" Attribute history_app_name exists """
assert hasattr(self.item, 'history_app_label')
def test_has_attribute_history_model_name(self):
""" Attribute history_model_name exists """
assert hasattr(self.item, 'history_model_name')
def test_has_attribute_kb_model_name(self):
"""Attribute _kb_model_name exists """
assert hasattr(self.item, 'kb_model_name')
def test_has_attribute_organization(self):
""" Field organization exists """
@ -62,6 +77,12 @@ class TenancyObjectTests(TestCase):
assert hasattr(self.item, 'model_notes')
def test_has_attribute_note_basename(self):
""" Attribute note_basename exists """
assert hasattr(self.item, 'note_basename')
def test_has_attribute_get_organization(self):
""" Function 'get_organization' Exists """
@ -102,3 +123,12 @@ class TenancyObjectTests(TestCase):
"""
assert self.item.objects is not None
class TenancyObjectTest(
TenancyObjectTestCases,
TestCase,
):
pass

View File

@ -1,19 +1,15 @@
from django.contrib.auth.models import User
from django.shortcuts import reverse
from django.test import Client, TestCase
from rest_framework.permissions import IsAuthenticated
from access.models.organization import Organization
from api.tests.abstract.viewsets import ViewSetCommon
from api.tests.unit.test_unit_common_viewset import IndexViewsetInheritedCases
from access.viewsets.index import Index
class AccessViewset(
IndexViewsetInheritedCases,
TestCase,
ViewSetCommon
):
viewset = Index
@ -29,11 +25,7 @@ class AccessViewset(
3. create super user
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.view_user = User.objects.create_user(username="test_user_add", password="password", is_superuser=True)
super().setUpTestData()
client = Client()
@ -42,19 +34,3 @@ class AccessViewset(
client.force_login(self.view_user)
self.http_options_response_list = client.options(url)
self.kwargs = {}
def test_view_attr_permission_classes_value(self):
"""Attribute Test
Attribute `permission_classes` must be metadata class `ReactUIMetadata`
"""
view_set = self.viewset()
assert view_set.permission_classes[0] is IsAuthenticated
assert len(view_set.permission_classes) == 1

View File

@ -0,0 +1,344 @@
import importlib
from django.apps import apps
from drf_spectacular.utils import (
extend_schema,
extend_schema_view,
OpenApiParameter,
OpenApiResponse,
PolymorphicProxySerializer
)
from rest_framework.reverse import reverse
# THis import only exists so that the migrations can be created
from access.models.entity_history import EntityHistory # pylint: disable=W0611:unused-import
from access.models.entity import (
Entity,
)
from api.viewsets.common import ModelViewSet
def spectacular_request_serializers( serializer_type = 'Model'):
serializers: dict = {}
for model in apps.get_models():
if issubclass(model, Entity):
serializer_module = importlib.import_module(
model._meta.app_label + '.serializers.' + str(
model._meta.verbose_name
).lower().replace(' ', '_')
)
serializers.update({
str(model._meta.verbose_name).lower().replace(' ', '_'): getattr(serializer_module, serializer_type + 'Serializer')
})
return serializers
@extend_schema_view(
create=extend_schema(
summary = 'Create an entity',
description='.',
parameters = [
OpenApiParameter(
name = 'entity_model',
description = 'Enter the entity type. This is the name of the Entity sub-model.',
location = OpenApiParameter.PATH,
type = str,
required = False,
allow_blank = True,
),
],
request = PolymorphicProxySerializer(
component_name = 'Entities',
serializers = spectacular_request_serializers(),
resource_type_field_name = None,
many = False,
),
responses = {
200: OpenApiResponse(
description='Already exists',
response = PolymorphicProxySerializer(
component_name = 'Entities (View)',
serializers = spectacular_request_serializers( 'View' ),
resource_type_field_name = None,
many = False,
)
),
201: OpenApiResponse(
description = 'Created',
response = PolymorphicProxySerializer(
component_name = 'Entities (View)',
serializers = spectacular_request_serializers( 'View' ),
resource_type_field_name = None,
many = False,
)
),
403: OpenApiResponse(description='User is missing add permissions'),
}
),
destroy = extend_schema(
summary = 'Delete an entity',
description = '.',
parameters =[
OpenApiParameter(
name = 'entity_model',
description = 'Enter the entity type. This is the name of the Entity sub-model.',
location = OpenApiParameter.PATH,
type = str,
required = False,
allow_blank = True,
),
],
request = PolymorphicProxySerializer(
component_name = 'Entities',
serializers = spectacular_request_serializers(),
resource_type_field_name = None,
many = False,
),
responses = {
204: OpenApiResponse(description='Object deleted'),
403: OpenApiResponse(description='User is missing delete permissions'),
}
),
list = extend_schema(
summary = 'Fetch all entities',
description='.',
parameters = [
OpenApiParameter(
name = 'entity_model',
description = 'Enter the entity type. This is the name of the Entity sub-model.',
location = OpenApiParameter.PATH,
type = str,
required = False,
allow_blank = True,
),
],
request = PolymorphicProxySerializer(
component_name = 'Entities',
serializers = spectacular_request_serializers(),
resource_type_field_name = None,
many = False,
),
responses = {
200: OpenApiResponse(
description='',
response = PolymorphicProxySerializer(
component_name = 'Entities (View)',
serializers = spectacular_request_serializers( 'View' ),
resource_type_field_name = None,
many = False,
)
),
403: OpenApiResponse(description='User is missing view permissions'),
}
),
retrieve = extend_schema(
summary = 'Fetch a single entity',
description='.',
parameters = [
OpenApiParameter(
name = 'entity_model',
description = 'Enter the entity type. This is the name of the Entity sub-model.',
location = OpenApiParameter.PATH,
type = str,
required = False,
allow_blank = True,
),
],
request = PolymorphicProxySerializer(
component_name = 'Entities',
serializers = spectacular_request_serializers(),
resource_type_field_name = None,
many = False,
),
responses = {
200: OpenApiResponse(
description='',
response = PolymorphicProxySerializer(
component_name = 'Entities (View)',
serializers = spectacular_request_serializers( 'View' ),
resource_type_field_name = None,
many = False,
)
),
403: OpenApiResponse(description='User is missing view permissions'),
}
),
update = extend_schema(exclude = True),
partial_update = extend_schema(
summary = 'Update an entity',
description = '.',
parameters = [
OpenApiParameter(
name = 'entity_model',
description = 'Enter the entity type. This is the name of the Entity sub-model.',
location = OpenApiParameter.PATH,
type = str,
required = False,
allow_blank = True,
),
],
request = PolymorphicProxySerializer(
component_name = 'Entities',
serializers = spectacular_request_serializers(),
resource_type_field_name = None,
many = False,
),
responses = {
200: OpenApiResponse(
description='',
response = PolymorphicProxySerializer(
component_name = 'Entities (View)',
serializers = spectacular_request_serializers( 'View' ),
resource_type_field_name = None,
many = False,
)
),
403: OpenApiResponse(description='User is missing change permissions'),
}
),
)
class ViewSet( ModelViewSet ):
filterset_fields = [
'organization',
'is_global'
]
search_fields = [
'model_notes',
]
def related_objects(self, model, model_kwarg):
"""Recursive relate_objects fetch
Fetch the model that is lowest in the chain of inherited models
Args:
model (django.db.models.Model): The model to obtain the
related_model from.
model_kwarg (str): The URL Kwarg of the model.
Returns:
_type_: _description_
"""
related_model = None
if model_kwarg:
for related_object in model._meta.related_objects:
related_objects = getattr(related_object.related_model._meta, 'related_objects', [])
if(
str(
related_object.related_model._meta.verbose_name
).lower().replace(' ', '_') == model_kwarg
):
related_model = related_object.related_model
break
elif related_objects:
related_model = self.related_objects(model = related_object.related_model, model_kwarg = model_kwarg)
break
return related_model
@property
def model(self):
if getattr(self, '_model', None) is not None:
return self._model
model_kwarg = None
if hasattr(self, 'kwargs'):
model_kwarg = self.kwargs.get('entity_model', None)
if model_kwarg:
self._model = self.related_objects(Entity, model_kwarg)
else:
self._model = Entity
return self._model
view_description = 'All entities'
def get_back_url(self) -> str:
if(
self.back_url is None
and self.kwargs.get('entity_model', None) is not None
):
self.back_url = reverse(
viewname = '_api_v2_entity_sub-list',
request = self.request,
kwargs = {
'entity_model': self.kwargs['entity_model'],
}
)
return self.back_url
def get_serializer_class(self):
serializer_module = importlib.import_module(
self.model._meta.app_label + '.serializers.' + str(
self.model._meta.verbose_name
).lower().replace(' ', '_')
)
if (
self.action == 'list'
or self.action == 'retrieve'
):
self.serializer_class = getattr(serializer_module, 'ViewSerializer')
else:
self.serializer_class = getattr(serializer_module, 'ModelSerializer')
return self.serializer_class
@extend_schema_view( # prevent duplicate documentation of both /access/entity endpoints
create = extend_schema(exclude = True),
destroy = extend_schema(exclude = True),
list = extend_schema(exclude = True),
retrieve = extend_schema(exclude = True),
update = extend_schema(exclude = True),
partial_update = extend_schema(exclude = True),
)
class NoDocsViewSet( ViewSet ):
pass

View File

@ -0,0 +1,60 @@
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse
from access.serializers.entity_notes import (
EntityNotes,
EntityNoteModelSerializer,
EntityNoteViewSerializer
)
from core.viewsets.model_notes import ModelNoteViewSet
@extend_schema_view(
create=extend_schema(
summary = 'Add a note to an Entity',
description = '',
responses = {
201: OpenApiResponse(description='created', response=EntityNoteViewSerializer),
400: OpenApiResponse(description='Validation failed.'),
403: OpenApiResponse(description='User is missing create permissions'),
}
),
destroy = extend_schema(
summary = 'Delete a Entity note',
description = ''
),
list = extend_schema(
summary = 'Fetch all Entity notes',
description='',
),
retrieve = extend_schema(
summary = 'Fetch a single Entity note',
description='',
),
update = extend_schema(exclude = True),
partial_update = extend_schema(
summary = 'Update a Entity note',
description = ''
),
)
class ViewSet(ModelNoteViewSet):
model = EntityNotes
def get_serializer_class(self):
if (
self.action == 'list'
or self.action == 'retrieve'
):
self.serializer_class = EntityNoteViewSerializer
else:
self.serializer_class = EntityNoteModelSerializer
return self.serializer_class

View File

@ -23,8 +23,15 @@ class Index(IndexViewset):
def list(self, request, pk=None):
return Response(
{
"organization": reverse('v2:_api_v2_organization-list', request=request)
response = {
"organization": reverse('v2:_api_v2_organization-list', request=request),
}
)
if self.request.feature_flag['2025-00003']:
response.update({
"role": reverse( 'v2:_api_v2_role-list', request=request ),
})
return Response(response)

103
app/access/viewsets/role.py Normal file
View File

@ -0,0 +1,103 @@
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse
# THis import only exists so that the migrations can be created
from access.models.role_history import RoleHistory # pylint: disable=W0611:unused-import
from access.serializers.role import (
Role,
ModelSerializer,
ViewSerializer,
)
from api.viewsets.common import ModelViewSet
@extend_schema_view(
create=extend_schema(
summary = 'Create a Role',
description='',
responses = {
201: OpenApiResponse(description='Created', response=ViewSerializer),
403: OpenApiResponse(description='User is missing add permissions'),
}
),
destroy = extend_schema(
summary = 'Delete a Role',
description = '',
responses = {
204: OpenApiResponse(description=''),
403: OpenApiResponse(description='User is missing delete permissions'),
}
),
list = extend_schema(
summary = 'Fetch all Role',
description='',
responses = {
200: OpenApiResponse(description='', response=ViewSerializer),
403: OpenApiResponse(description='User is missing view permissions'),
}
),
retrieve = extend_schema(
summary = 'Fetch a single Role',
description='',
responses = {
200: OpenApiResponse(description='', response=ViewSerializer),
403: OpenApiResponse(description='User is missing view permissions'),
}
),
update = extend_schema(exclude = True),
partial_update = extend_schema(
summary = 'Update a Role',
description = '',
responses = {
200: OpenApiResponse(description='', response=ViewSerializer),
403: OpenApiResponse(description='User is missing change permissions'),
}
),
)
class ViewSet(ModelViewSet):
filterset_fields = [
'organization',
'permissions',
]
search_fields = [
'model_notes',
'name',
]
model = Role
view_description: str = 'Available Roles'
def get_queryset(self):
if self.queryset is None:
self.queryset = self.model.objects.prefetch_related('permissions','permissions__content_type')
if 'pk' in getattr(self, 'kwargs', {}):
self.queryset = self.queryset.filter( pk = int( self.kwargs['pk'] ) )
return self.queryset
def get_serializer_class(self):
if (
self.action == 'list'
or self.action == 'retrieve'
):
self.serializer_class = ViewSerializer
else:
self.serializer_class = ModelSerializer
return self.serializer_class

View File

@ -0,0 +1,60 @@
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse
from access.serializers.role_notes import (
RoleNotes,
RoleNoteModelSerializer,
RoleNoteViewSerializer
)
from core.viewsets.model_notes import ModelNoteViewSet
@extend_schema_view(
create=extend_schema(
summary = 'Add a note to a Team',
description = '',
responses = {
201: OpenApiResponse(description='created', response=RoleNoteViewSerializer),
400: OpenApiResponse(description='Validation failed.'),
403: OpenApiResponse(description='User is missing create permissions'),
}
),
destroy = extend_schema(
summary = 'Delete a team note',
description = ''
),
list = extend_schema(
summary = 'Fetch all team notes',
description='',
),
retrieve = extend_schema(
summary = 'Fetch a single team note',
description='',
),
update = extend_schema(exclude = True),
partial_update = extend_schema(
summary = 'Update a team note',
description = ''
),
)
class ViewSet(ModelNoteViewSet):
model = RoleNotes
def get_serializer_class(self):
if (
self.action == 'list'
or self.action == 'retrieve'
):
self.serializer_class = RoleNoteViewSerializer
else:
self.serializer_class = RoleNoteModelSerializer
return self.serializer_class

View File

6
app/accounting/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class AccountingConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'accounting'

View File

8
app/accounting/urls.py Normal file
View File

@ -0,0 +1,8 @@
from centurion_feature_flag.urls.routers import DefaultRouter
app_name = "accounting"
router = DefaultRouter(trailing_slash=False)
urlpatterns = router.urls

View File

@ -113,6 +113,11 @@ class AuthToken(models.Model):
modified = AutoLastModifiedField()
history_app_label: str = None
history_model_name: str = None
kb_model_name: str = None
note_basename: str = None
@property
def generate(self) -> str:

View File

@ -176,7 +176,7 @@ class ReactUIMetadata(OverRideJSONAPIMetadata):
}
metadata['navigation'] = self.get_navigation(request.user)
metadata['navigation'] = self.get_navigation(request)
return metadata
@ -373,154 +373,238 @@ class ReactUIMetadata(OverRideJSONAPIMetadata):
return field_info
_nav = {
'access': {
"display_name": "Access",
"name": "access",
"pages": {
'view_organization': {
"display_name": "Organization",
"name": "organization",
"link": "/access/organization"
def get_nav_items(self, request) -> dict:
nav = {
'access': {
"display_name": "Access",
"name": "access",
"pages": {
'view_organization': {
"display_name": "Organization",
"name": "organization",
"link": "/access/organization"
},
}
}
},
'assistance': {
"display_name": "Assistance",
"name": "assistance",
"pages": {
'core.view_ticket_request': {
"display_name": "Requests",
"name": "request",
"icon": "ticket_request",
"link": "/assistance/ticket/request"
},
'view_knowledgebase': {
"display_name": "Knowledge Base",
"name": "knowledge_base",
"icon": "information",
"link": "/assistance/knowledge_base"
},
'accounting': {
"display_name": "Accounting",
"name": "accounting",
"pages": {}
},
'assistance': {
"display_name": "Assistance",
"name": "assistance",
"pages": {
'core.view_ticket_request': {
"display_name": "Requests",
"name": "request",
"icon": "ticket_request",
"link": "/assistance/ticket/request"
},
'view_knowledgebase': {
"display_name": "Knowledge Base",
"name": "knowledge_base",
"icon": "information",
"link": "/assistance/knowledge_base"
}
}
}
},
'itam': {
"display_name": "ITAM",
"name": "itam",
"pages": {
'view_device': {
"display_name": "Devices",
"name": "device",
"icon": "device",
"link": "/itam/device"
},
'view_operatingsystem': {
"display_name": "Operating System",
"name": "operating_system",
"link": "/itam/operating_system"
},
'view_software': {
"display_name": "Software",
"name": "software",
"link": "/itam/software"
},
'human_resources': {
"display_name": "Human Resources (HR)",
"name": "human_resources",
"pages": {
# 'view_employees': {
# "display_name": "Employees",
# "name": "employees",
# "icon": "employees",
# "link": "/human_resources/employees"
# }
}
}
},
'itim': {
"display_name": "ITIM",
"name": "itim",
"pages": {
'core.view_ticket_change': {
"display_name": "Changes",
"name": "ticket_change",
"link": "/itim/ticket/change"
},
'view_cluster': {
"display_name": "Clusters",
"name": "cluster",
"link": "/itim/cluster"
},
'core.view_ticket_incident': {
"display_name": "Incidents",
"name": "ticket_incident",
"link": "/itim/ticket/incident"
},
'core.view_ticket_problem': {
"display_name": "Problems",
"name": "ticket_problem",
"link": "/itim/ticket/problem"
},
'view_service': {
"display_name": "Services",
"name": "service",
"link": "/itim/service"
},
}
},
'devops': {
"display_name": "DevOPs",
"name": "devops",
"icon": "devops",
"pages": {
'view_featureflag': {
"display_name": "Feature Flags",
"name": "feature_flag",
"icon": 'feature_flag',
"link": "/devops/feature_flag"
},
'itam': {
"display_name": "ITAM",
"name": "itam",
"pages": {
'view_device': {
"display_name": "Devices",
"name": "device",
"icon": "device",
"link": "/itam/device"
},
'view_operatingsystem': {
"display_name": "Operating System",
"name": "operating_system",
"link": "/itam/operating_system"
},
'view_software': {
"display_name": "Software",
"name": "software",
"link": "/itam/software"
}
}
}
},
'config_management': {
"display_name": "Config Management",
"name": "config_management",
"icon": "ansible",
"pages": {
'view_configgroups': {
"display_name": "Groups",
"name": "group",
"icon": 'config_management',
"link": "/config_management/group"
},
'itim': {
"display_name": "ITIM",
"name": "itim",
"pages": {
'core.view_ticket_change': {
"display_name": "Changes",
"name": "ticket_change",
"link": "/itim/ticket/change"
},
'view_cluster': {
"display_name": "Clusters",
"name": "cluster",
"link": "/itim/cluster"
},
'core.view_ticket_incident': {
"display_name": "Incidents",
"name": "ticket_incident",
"link": "/itim/ticket/incident"
},
'core.view_ticket_problem': {
"display_name": "Problems",
"name": "ticket_problem",
"link": "/itim/ticket/problem"
},
'view_service': {
"display_name": "Services",
"name": "service",
"link": "/itim/service"
},
}
}
},
'project_management': {
"display_name": "Project Management",
"name": "project_management",
"icon": 'project',
"pages": {
'view_project': {
"display_name": "Projects",
"name": "project",
"icon": 'kanban',
"link": "/project_management/project"
},
'itops': {
"display_name": "ITOps",
"name": "itops",
"icon": "itops",
"pages": {
'core.view_ticketcategory': {
"display_name": "Ticket Category",
"name": "ticketcategory",
"icon": 'ticketcategory',
"link": "/settings/ticket_category"
},
'core.view_ticketcommentcategory': {
"display_name": "Ticket Comment Category",
"name": "ticketcommentcategory",
"icon": 'ticketcommentcategory',
"link": "/settings/ticket_comment_category"
},
}
}
},
},
'devops': {
"display_name": "DevOPs",
"name": "devops",
"icon": "devops",
"pages": {
'view_featureflag': {
"display_name": "Feature Flags",
"name": "feature_flag",
"icon": 'feature_flag',
"link": "/devops/feature_flag"
}
}
},
'config_management': {
"display_name": "Config Management",
"name": "config_management",
"icon": "ansible",
"pages": {
'view_configgroups': {
"display_name": "Groups",
"name": "group",
"icon": 'config_management',
"link": "/config_management/group"
}
}
},
'project_management': {
"display_name": "Project Management",
"name": "project_management",
"icon": 'project',
"pages": {
'view_project': {
"display_name": "Projects",
"name": "project",
"icon": 'kanban',
"link": "/project_management/project"
}
}
},
'settings': {
"display_name": "Settings",
"name": "settings",
"pages": {
'all_settings': {
"display_name": "System",
"name": "setting",
"icon": "system",
"link": "/settings"
},
'django_celery_results.view_taskresult': {
"display_name": "Task Log",
"name": "celery_log",
# "icon": "settings",
"link": "/settings/celery_log"
'settings': {
"display_name": "Settings",
"name": "settings",
"pages": {
'all_settings': {
"display_name": "System",
"name": "setting",
"icon": "system",
"link": "/settings"
},
'django_celery_results.view_taskresult': {
"display_name": "Task Log",
"name": "celery_log",
# "icon": "settings",
"link": "/settings/celery_log"
}
}
}
}
}
if getattr(request, 'feature_flag', None):
if request.feature_flag['2025-00001']:
nav['devops']['pages'].update({
'view_gitgroup': {
"display_name": "Git Group",
"name": "git_group",
"icon": 'git_group',
"link": "/devops/git_group"
},
'view_gitrepository': {
"display_name": "Git Repositories",
"name": "git_repository",
"icon": 'git',
"link": "/devops/git_repository"
}
})
def get_navigation(self, user) -> list(dict()):
if request.feature_flag['2025-00002']:
nav['access']['pages'].update({
'view_contact': {
"display_name": "Directory",
"name": "directory",
"link": "/access/entity/contact"
}
})
if request.feature_flag['2025-00003']:
nav['access']['pages'].update({
'view_role': {
"display_name": "Roles",
"name": "roles",
"icon": 'roles',
"link": "/access/role"
}
})
return nav
def get_navigation(self, request) -> list(dict()):
"""Render the navigation menu
Check the users permissions agains `_nav`. if they have the permission, add the
Check the users permissions against `get_nav_items()`. if they have the permission, add the
menu entry to the navigation to be rendered,
**No** Menu is to be rendered that contains no menu entries.
@ -536,7 +620,7 @@ class ReactUIMetadata(OverRideJSONAPIMetadata):
processed_permissions: dict = {}
for group in user.groups.all():
for group in request.user.groups.all():
for permission in group.permissions.all():
@ -554,8 +638,6 @@ class ReactUIMetadata(OverRideJSONAPIMetadata):
view_settings: list = [
'assistance.view_knowledgebasecategory',
'core.view_manufacturer',
'core.view_ticketcategory',
'core.view_ticketcommentcategory',
'itam.view_devicemodel',
'itam.view_devicetype',
'itam.view_softwarecategory',
@ -569,11 +651,11 @@ class ReactUIMetadata(OverRideJSONAPIMetadata):
# user = view.request.user
user_orgainzations = Organization.objects.filter(
manager = user
manager = request.user
)
for app, entry in self._nav.items():
for app, entry in self.get_nav_items(request).items():
new_menu_entry: dict = {}

View File

@ -60,24 +60,43 @@ class CommonModelSerializer(CommonBaseSerializer):
get_url = {
'_self': item.get_url( request = self._context['view'].request ),
'knowledge_base': reverse(
"v2:_api_v2_model_kb-list",
request=self._context['view'].request,
kwargs={
'model': self.Meta.model._meta.model_name,
'model_pk': item.pk
}
),
}
kb_model_name = self.Meta.model._meta.model_name
if getattr(item, 'kb_model_name'):
kb_model_name = item.kb_model_name
get_url['knowledge_base'] = reverse(
'v2:_api_v2_model_kb-list',
request=self._context['view'].request,
kwargs={
'model': kb_model_name,
'model_pk': item.pk
}
)
if getattr(self.Meta.model, 'save_model_history', True):
history_app_label = self.Meta.model._meta.app_label
if getattr(item, 'history_app_label'):
history_app_label = item.history_app_label
history_model_name = self.Meta.model._meta.model_name
if getattr(item, 'history_model_name'):
history_model_name = item.history_model_name
get_url['history'] = reverse(
"v2:_api_v2_model_history-list",
request = self._context['view'].request,
kwargs = {
'app_label': self.Meta.model._meta.app_label,
'model_name': self.Meta.model._meta.model_name,
'app_label': history_app_label,
'model_name': history_model_name,
'model_id': item.pk
}
)
@ -94,7 +113,17 @@ class CommonModelSerializer(CommonBaseSerializer):
and obj is not FeatureNotUsed
):
note_basename = '_api_v2_' + str(item._meta.verbose_name).lower().replace(' ', '_') + '_note'
app_namespace = ''
if getattr(item, 'app_namespace', None):
app_namespace = str(item.app_namespace) + ':'
note_basename = app_namespace + '_api_v2_' + str(item._meta.verbose_name).lower().replace(' ', '_') + '_note'
if getattr(item, 'note_basename'):
note_basename = app_namespace + item.note_basename
if getattr(self.Meta, 'note_basename', None):

View File

@ -398,7 +398,16 @@ class MetadataAttributesFunctionalTable:
for item in response.data['table_fields']:
if type(item) is not str:
if(
type(item) is not str
and not (
type(item) is dict
and 'field' in item
and 'type' in item
and item['type'] == 'link'
and 'key' in item
)
):
all_string = False

View File

@ -1,740 +0,0 @@
from django.contrib.auth.models import ContentType, Permission, User
from unittest.mock import patch, PropertyMock
from access.mixins.permissions import OrganizationPermissionMixin
from api.react_ui_metadata import ReactUIMetadata
from access.middleware.request import Tenancy
from access.models.organization import Organization
from access.models.team import Team
from access.models.team_user import TeamUsers
from settings.models.app_settings import AppSettings
class MockRequest:
"""Fake Request
contains the user and tenancy object for permission checks
Some ViewSets rely upon the request object for obtaining the user and
fetching the tenacy object for permission checking.
"""
data = {}
kwargs = {}
tenancy: Tenancy = None
user: User = None
def __init__(self, user: User, organization: Organization, viewset):
self.user = user
view_permission = Permission.objects.get(
codename = 'view_' + viewset.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = viewset.model._meta.app_label,
model = viewset.model._meta.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = organization,
)
view_team.permissions.set([view_permission])
teamuser = TeamUsers.objects.create(
team = view_team,
user = user
)
self.app_settings = AppSettings.objects.select_related('global_organization').get(
owner_organization = None
)
self.tenancy = Tenancy(
user = user,
app_settings = self.app_settings
)
class AllViewSet:
"""Tests specific to the Viewset
**Dont include these tests directly, see below for correct class**
Tests are for ALL viewsets.
"""
viewset = None
"""ViewSet to Test"""
def test_view_attr_allowed_methods_exists(self):
"""Attribute Test
Attribute `allowed_methods` must exist
"""
assert hasattr(self.viewset, 'allowed_methods')
def test_view_attr_allowed_methods_not_empty(self):
"""Attribute Test
Attribute `allowed_methods` must return a value
"""
view_set = self.viewset()
view_set.kwargs = self.kwargs
assert view_set.allowed_methods is not None
def test_view_attr_allowed_methods_type(self):
"""Attribute Test
Attribute `allowed_methods` must be of type list
"""
view_set = self.viewset()
view_set.kwargs = self.kwargs
assert type(view_set.allowed_methods) is list
def test_view_attr_allowed_methods_values(self):
"""Attribute Test
Attribute `allowed_methods` only contains valid values
"""
# Values valid for index views
valid_values: list = [
'GET',
'HEAD',
'OPTIONS',
]
all_valid: bool = True
view_set = self.viewset()
for method in list(view_set.allowed_methods):
if method not in valid_values:
all_valid = False
assert all_valid
def test_view_attr_metadata_class_exists(self):
"""Attribute Test
Attribute `metadata_class` must exist
"""
assert hasattr(self.viewset, 'metadata_class')
def test_view_attr_metadata_class_not_empty(self):
"""Attribute Test
Attribute `metadata_class` must return a value
"""
view_set = self.viewset()
assert view_set.metadata_class is not None
def test_view_attr_metadata_class_type(self):
"""Attribute Test
Attribute `metadata_class` must be metadata class `ReactUIMetadata`
"""
view_set = self.viewset()
assert view_set.metadata_class is ReactUIMetadata
def test_view_attr_permission_classes_exists(self):
"""Attribute Test
Attribute `permission_classes` must exist
"""
assert hasattr(self.viewset, 'permission_classes')
def test_view_attr_permission_classes_not_empty(self):
"""Attribute Test
Attribute `permission_classes` must return a value
"""
view_set = self.viewset()
assert view_set.permission_classes is not None
def test_view_attr_permission_classes_type(self):
"""Attribute Test
Attribute `permission_classes` must be list
"""
view_set = self.viewset()
assert type(view_set.permission_classes) is list
def test_view_attr_permission_classes_value(self):
"""Attribute Test
Attribute `permission_classes` must be metadata class `ReactUIMetadata`
"""
view_set = self.viewset()
assert view_set.permission_classes[0] is OrganizationPermissionMixin
assert len(view_set.permission_classes) == 1
def test_view_attr_view_description_exists(self):
"""Attribute Test
Attribute `view_description` must exist
"""
assert hasattr(self.viewset, 'view_description')
def test_view_attr_view_description_not_empty(self):
"""Attribute Test
Attribute `view_description` must return a value
"""
assert self.viewset.view_description is not None
def test_view_attr_view_description_type(self):
"""Attribute Test
Attribute `view_description` must be of type str
"""
assert type(self.viewset.view_description) is str
def test_view_attr_view_name_exists(self):
"""Attribute Test
Attribute `view_name` must exist
"""
assert hasattr(self.viewset, 'view_name')
def test_view_attr_view_name_not_empty(self):
"""Attribute Test
Attribute `view_name` must return a value
"""
assert self.viewset.view_name is not None
def test_view_attr_view_name_type(self):
"""Attribute Test
Attribute `view_name` must be of type str
"""
view_set = self.viewset()
assert (
type(view_set.view_name) is str
)
class APIRenderViewSet:
"""Function ViewSet test
**Dont include these tests directly, see below for correct class**
These tests ensure that the data from the ViewSet is present for a
HTTP Request
"""
http_options_response_list: dict = None
"""The HTTP/Options Response for the ViewSet"""
def test_api_render_field_allowed_methods_exists(self):
"""Attribute Test
Attribute `allowed_methods` must exist
"""
assert 'allowed_methods' in self.http_options_response_list.data
def test_api_render_field_allowed_methods_not_empty(self):
"""Attribute Test
Attribute `allowed_methods` must return a value
"""
assert len(self.http_options_response_list.data['allowed_methods']) > 0
def test_api_render_field_allowed_methods_type(self):
"""Attribute Test
Attribute `allowed_methods` must be of type list
"""
assert type(self.http_options_response_list.data['allowed_methods']) is list
def test_api_render_field_allowed_methods_values(self):
"""Attribute Test
Attribute `allowed_methods` only contains valid values
"""
# Values valid for index views
valid_values: list = [
'GET',
'HEAD',
'OPTIONS',
]
all_valid: bool = True
for method in list(self.http_options_response_list.data['allowed_methods']):
if method not in valid_values:
all_valid = False
assert all_valid
def test_api_render_field_view_description_exists(self):
"""Attribute Test
Attribute `description` must exist
"""
assert 'description' in self.http_options_response_list.data
def test_api_render_field_view_description_not_empty(self):
"""Attribute Test
Attribute `view_description` must return a value
"""
assert self.http_options_response_list.data['description'] is not None
def test_api_render_field_view_description_type(self):
"""Attribute Test
Attribute `view_description` must be of type str
"""
assert type(self.http_options_response_list.data['description']) is str
def test_api_render_field_view_name_exists(self):
"""Attribute Test
Attribute `view_name` must exist
"""
assert 'name' in self.http_options_response_list.data
def test_api_render_field_view_name_not_empty(self):
"""Attribute Test
Attribute `view_name` must return a value
"""
assert self.http_options_response_list.data['name'] is not None
def test_api_render_field_view_name_type(self):
"""Attribute Test
Attribute `view_name` must be of type str
"""
assert type(self.http_options_response_list.data['name']) is str
class ModelViewSet(AllViewSet):
"""Tests for Model Viewsets
**Dont include these tests directly, see below for correct class**
"""
viewset = None
"""ViewSet to Test"""
def test_view_attr_documentation_exists(self):
"""Attribute Test
Attribute `documentation` must exist
"""
assert hasattr(self.viewset, 'documentation')
def test_view_attr_documentation_type(self):
"""Attribute Test
Attribute `documentation` must be of type str or None.
this attribute is optional.
"""
view_set = self.viewset()
assert (
type(view_set.documentation) is str
or view_set.documentation is None
)
def test_view_attr_filterset_fields_exists(self):
"""Attribute Test
Attribute `filterset_fields` must exist
"""
assert hasattr(self.viewset, 'filterset_fields')
def test_view_attr_filterset_fields_not_empty(self):
"""Attribute Test
Attribute `filterset_fields` must return a value
"""
assert self.viewset.filterset_fields is not None
def test_view_attr_filterset_fields_type(self):
"""Attribute Test
Attribute `filterset_fields` must be of type list
"""
view_set = self.viewset()
assert (
type(view_set.filterset_fields) is list
)
def test_view_attr_allowed_methods_values(self):
"""Attribute Test
Attribute `allowed_methods` only contains valid values
"""
# Values valid for model views
valid_values: list = [
'DELETE',
'GET',
'HEAD',
'OPTIONS',
'PATCH',
'POST',
'PUT',
]
all_valid: bool = True
view_set = self.viewset()
view_set.kwargs = self.kwargs
for method in list(view_set.allowed_methods):
if method not in valid_values:
all_valid = False
assert all_valid
def test_view_attr_model_exists(self):
"""Attribute Test
Attribute `model` must exist
"""
assert hasattr(self.viewset, 'model')
def test_view_attr_model_not_empty(self):
"""Attribute Test
Attribute `model` must return a value
"""
view_set = self.viewset()
assert view_set.model is not None
def test_view_attr_search_fields_exists(self):
"""Attribute Test
Attribute `search_fields` must exist
"""
assert hasattr(self.viewset, 'search_fields')
def test_view_attr_search_fields_not_empty(self):
"""Attribute Test
Attribute `search_fields` must return a value
"""
assert self.viewset.search_fields is not None
def test_view_attr_search_fields_type(self):
"""Attribute Test
Attribute `search_fields` must be of type list
"""
view_set = self.viewset()
assert (
type(view_set.search_fields) is list
)
def test_view_attr_view_name_not_empty(self):
"""Attribute Test
Attribute `view_name` must return a value
"""
view_set = self.viewset()
assert (
view_set.view_name is not None
or view_set.get_view_name() is not None
)
def test_view_attr_view_name_type(self):
"""Attribute Test
Attribute `view_name` must be of type str
"""
view_set = self.viewset()
assert (
type(view_set.view_name) is str
or type(view_set.get_view_name()) is str
)
class APIRenderModelViewSet(APIRenderViewSet):
"""Tests for Model Viewsets
**Dont include these tests directly, see below for correct class**
"""
viewset = None
"""ViewSet to Test"""
def test_api_render_field_allowed_methods_values(self):
"""Attribute Test
Attribute `allowed_methods` only contains valid values
"""
# Values valid for model views
valid_values: list = [
'DELETE',
'GET',
'HEAD',
'OPTIONS',
'PATCH',
'POST',
'PUT',
]
all_valid: bool = True
for method in list(self.http_options_response_list.data['allowed_methods']):
if method not in valid_values:
all_valid = False
assert all_valid
class ViewSetCommon(
AllViewSet,
APIRenderViewSet
):
""" Tests for Non-Model Viewsets
**Include this class directly into Non-Model ViewSets**
Args:
AllViewSet (class): Tests for all Viewsets.
APIRenderViewSet (class): Tests to check API Rendering to ensure data present.
"""
pass
class ViewSetModel(
ModelViewSet,
APIRenderModelViewSet
):
"""Tests for model ViewSets
**Include this class directly into Model ViewSets**
Args:
ModelViewSet (class): Tests for Model Viewsets, includes `AllViewSet` tests.
APIRenderModelViewSet (class): Tests to check API rendering to ensure data is present, includes `APIRenderViewSet` tests.
"""
def test_view_func_get_queryset_cache_result(self):
"""Viewset Test
Ensure that the `get_queryset` function caches the result under
attribute `<viewset>.queryset`
"""
view_set = self.viewset()
view_set.request = MockRequest(
user = self.view_user,
organization = self.organization,
viewset = self.viewset
)
view_set.request.headers = {}
view_set.kwargs = self.kwargs
view_set.action = 'list'
view_set.detail = False
assert view_set.queryset is None # Must be empty before init
q = view_set.get_queryset()
assert view_set.queryset is not None # Must not be empty after init
assert q == view_set.queryset
def test_view_func_get_queryset_cache_result_used(self):
"""Viewset Test
Ensure that the `get_queryset` function caches the result under
attribute `<viewset>.queryset`
"""
view_set = self.viewset()
view_set.request = MockRequest(
user = self.view_user,
organization = self.organization,
viewset = self.viewset
)
view_set.request.headers = {}
view_set.kwargs = self.kwargs
view_set.action = 'list'
view_set.detail = False
mock_return = view_set.get_queryset() # Real item to be used as mock return Some
# functions use `Queryset` for additional filtering
setter_not_called = True
with patch.object(self.viewset, 'queryset', new_callable=PropertyMock) as qs:
qs.return_value = mock_return
mocked_view_set = self.viewset()
mocked_view_set.kwargs = self.kwargs
mocked_view_set.action = 'list'
mocked_view_set.detail = False
qs.reset_mock() # Just in case
mocked_setup = mocked_view_set.get_queryset() # should only add two calls, if exists and the return
for mock_call in list(qs.mock_calls): # mock_calls with args means setter was called
if len(mock_call.args) > 0:
setter_not_called = False
assert setter_not_called
assert qs.call_count == 2

View File

@ -2,18 +2,16 @@ from django.contrib.auth.models import User
from django.shortcuts import reverse
from django.test import Client, TestCase
from rest_framework.permissions import IsAuthenticated
from access.models.organization import Organization
from api.tests.abstract.viewsets import ViewSetCommon
from api.tests.unit.test_unit_common_viewset import IndexViewsetInheritedCases
from api.viewsets.index import Index
class HomeViewset(
TestCase,
ViewSetCommon
IndexViewsetInheritedCases
):
viewset = Index
@ -45,17 +43,3 @@ class HomeViewset(
self.kwargs = {}
def test_view_attr_permission_classes_value(self):
"""Attribute Test
Attribute `permission_classes` must be metadata class `ReactUIMetadata`
"""
view_set = self.viewset()
assert view_set.permission_classes[0] is IsAuthenticated
assert len(view_set.permission_classes) == 1

View File

@ -8,6 +8,13 @@ from access.models.team_user import TeamUsers
from api.react_ui_metadata import ReactUIMetadata
class MockRequst:
user = None
def __init__(self, user ):
self.user = user
class NavigationMenu(
TestCase
@ -150,7 +157,7 @@ class NavigationMenu(
for model_name in model_names:
setattr(self, app_label + "_" + model_name['permission_model'], User.objects.create_user(username= app_label + "_" + model_name['permission_model'], password="password"))
setattr(self, app_label + "_" + model_name['permission_model'], MockRequst( user = User.objects.create_user(username= app_label + "_" + model_name['permission_model'], password="password")))
team = Team.objects.create(
team_name = app_label + "_" + model_name['permission_model'],
@ -169,7 +176,7 @@ class NavigationMenu(
team_user = TeamUsers.objects.create(
team = team,
user = getattr(self, app_label + "_" + model_name['permission_model'])
user = getattr(self, app_label + "_" + model_name['permission_model']).user
)
self.metadata = ReactUIMetadata()
@ -1459,9 +1466,9 @@ class NavigationMenu(
nav_menu = self.metadata.get_navigation(self.core_ticketcategory)
menu_name = 'settings'
menu_name = 'itops'
page_name = 'setting'
page_name = 'ticketcategory'
menu_page_exists: bool = False
@ -1514,9 +1521,9 @@ class NavigationMenu(
nav_menu = self.metadata.get_navigation(self.core_ticketcommentcategory)
menu_name = 'settings'
menu_name = 'itops'
page_name = 'setting'
page_name = 'ticketcommentcategory'
menu_page_exists: bool = False

File diff suppressed because it is too large Load Diff

View File

@ -1,51 +1,27 @@
import pytest
from django.contrib.auth.models import User
from django.test import Client, TestCase
from rest_framework.reverse import reverse
from access.models.organization import Organization
from api.tests.unit.test_unit_common_viewset import (
ModelCreateViewSetInheritedCases,
ModelListRetrieveDeleteViewSetInheritedCases,
)
from api.viewsets.auth_token import ViewSet
from api.tests.abstract.viewsets import ViewSetModel
from settings.viewsets.user_settings import ViewSet
# from settings.viewsets.user_settings import ViewSet
class ViewsetCommon(
ViewSetModel,
class ViewsetList(
ModelCreateViewSetInheritedCases,
ModelListRetrieveDeleteViewSetInheritedCases,
TestCase,
):
viewset = ViewSet
route_name = 'v2:_api_v2_user_settings_token'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization
3. create super user
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.view_user = User.objects.create_user(username="test_view_user", password="password", is_superuser=True)
self.kwargs = {
'model_id': self.view_user.id
}
class ViewsetList(
ViewsetCommon,
TestCase,
):
@classmethod
def setUpTestData(self):
@ -54,9 +30,12 @@ class ViewsetList(
1. make list request
"""
super().setUpTestData()
self.kwargs = {
'model_id': self.view_user.id
}
client = Client()

View File

@ -2,7 +2,7 @@ from django.urls import include, path
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
from rest_framework.routers import DefaultRouter
from centurion_feature_flag.urls.routers import DefaultRouter
from api.viewsets import (
auth_token,
@ -17,9 +17,13 @@ from app.viewsets.base import (
)
from access.viewsets import (
entity,
entity_notes,
index as access_v2,
organization as organization_v2,
organization_notes,
role,
role_notes,
team as team_v2,
team_notes,
team_user as team_user_v2
@ -131,12 +135,18 @@ router = DefaultRouter(trailing_slash=False)
router.register('', v2.Index, basename='_api_v2_home')
router.register('access', access_v2.Index, basename='_api_v2_access_home')
router.register('access/entity/(?P<entity_model>[a-z]+)?', entity.ViewSet, feature_flag = '2025-00002', basename='_api_v2_entity_sub')
router.register('access/entity', entity.NoDocsViewSet, feature_flag = '2025-00002', basename='_api_v2_entity')
router.register('access/entity/(?P<model_id>[0-9]+)/notes', entity_notes.ViewSet, feature_flag = '2025-00002', basename='_api_v2_entity_note')
router.register('access/organization', organization_v2.ViewSet, basename='_api_v2_organization')
router.register('access/organization/(?P<model_id>[0-9]+)/notes', organization_notes.ViewSet, basename='_api_v2_organization_note')
router.register('access/organization/(?P<organization_id>[0-9]+)/team', team_v2.ViewSet, basename='_api_v2_organization_team')
router.register('access/organization/(?P<organization_id>[0-9]+)/team/(?P<model_id>[0-9]+)/notes', team_notes.ViewSet, basename='_api_v2_team_note')
router.register('access/organization/(?P<organization_id>[0-9]+)/team/(?P<team_id>[0-9]+)/user', team_user_v2.ViewSet, basename='_api_v2_organization_team_user')
router.register('access/role', role.ViewSet, feature_flag = '2025-00003', basename='_api_v2_role')
router.register('access/role/(?P<model_id>[0-9]+)/notes', role_notes.ViewSet, feature_flag = '2025-00003', basename='_api_v2_role_note')
router.register('assistance', assistance_index_v2.Index, basename='_api_v2_assistance_home')
router.register('assistance/knowledge_base', knowledge_base_v2.ViewSet, basename='_api_v2_knowledge_base')
@ -247,6 +257,8 @@ urlpatterns = [
urlpatterns += router.urls
urlpatterns += [
path("accounting/", include("accounting.urls")),
path("devops/", include("devops.urls")),
path("hr/", include('human_resources.urls')),
path('public/', include('api.urls_public')),
]

View File

@ -44,21 +44,31 @@ class Create(
response = super().create(request = request, *args, **kwargs)
# Always return using the ViewSerializer
serializer_module = importlib.import_module(self.serializer_class.__module__)
serializer_module = importlib.import_module(self.get_serializer_class().__module__)
view_serializer = getattr(serializer_module, self.get_view_serializer_name())
serializer = view_serializer(
self.get_queryset().get( pk = int(response.data['id']) ),
context = {
'request': request,
'view': self,
},
)
if response.data['id'] is not None:
serializer = view_serializer(
self.get_queryset().get( pk = int(response.data['id']) ),
context = {
'request': request,
'view': self,
},
)
serializer_data = serializer.data
else:
serializer_data = {}
# Mimic ALL details from DRF response except serializer
response = Response(
data = serializer.data,
data = serializer_data,
status = response.status_code,
template_name = response.template_name,
headers = response.headers,
@ -272,7 +282,7 @@ class Update(
response = super().partial_update(request = request, *args, **kwargs)
# Always return using the ViewSerializer
serializer_module = importlib.import_module(self.serializer_class.__module__)
serializer_module = importlib.import_module(self.get_serializer_class().__module__)
view_serializer = getattr(serializer_module, self.get_view_serializer_name())
@ -339,7 +349,7 @@ class Update(
response = super().update(request = request, *args, **kwargs)
# Always return using the ViewSerializer
serializer_module = importlib.import_module(self.serializer_class.__module__)
serializer_module = importlib.import_module(self.get_serializer_class().__module__)
view_serializer = getattr(serializer_module, self.get_view_serializer_name())
@ -515,7 +525,8 @@ class CommonViewSet(
elif getattr(self.model, '_meta', None):
self._model_documentation = self.model._meta.app_label + '/' + self.model._meta.model_name
self._model_documentation = self.model._meta.app_label + '/' + str(
self.model._meta.verbose_name).lower().replace(' ', '_')
return self._model_documentation
@ -604,7 +615,7 @@ class CommonViewSet(
self.view_name = str(self.model._meta.verbose_name)
else:
self.view_name = str(self.model._meta.verbose_name_plural)
return self.view_name
@ -658,11 +669,9 @@ class ModelViewSetBase(
self.queryset = self.model.objects.all()
if 'pk' in self.kwargs:
if 'pk' in getattr(self, 'kwargs', {}):
if self.kwargs['pk']:
self.queryset = self.queryset.filter( pk = int( self.kwargs['pk'] ) )
self.queryset = self.queryset.filter( pk = int( self.kwargs['pk'] ) )
return self.queryset
@ -698,7 +707,7 @@ class ModelViewSetBase(
if self.view_serializer_name is None:
self.view_serializer_name = self.serializer_class.__name__.replace('ModelSerializer', 'ViewSerializer')
self.view_serializer_name = self.get_serializer_class().__name__.replace('ModelSerializer', 'ViewSerializer')
return self.view_serializer_name

View File

@ -69,6 +69,7 @@ CELERY_TASK_SEND_SENT_EVENT = True
CELERY_WORKER_SEND_TASK_EVENTS = True # worker_send_task_events
FEATURE_FLAGGING_ENABLED = True # Turn Feature Flagging on/off
FEATURE_FLAG_OVERRIDES = None # Feature Flags to override fetched feature flags
# PROMETHEUS_METRICS_EXPORT_PORT_RANGE = range(8010, 8010)
# PROMETHEUS_METRICS_EXPORT_PORT = 8010
@ -137,7 +138,10 @@ INSTALLED_APPS = [
'config_management.apps.ConfigManagementConfig',
'project_management.apps.ProjectManagementConfig',
'devops.apps.DevOpsConfig',
'centurion_feature_flag',
'centurion_feature_flag.apps.CenturionFeatureFlagConfig',
'human_resources.apps.HumanResourcesConfig',
'itops.apps.ItOpsConfig',
'accounting.apps.AccountingConfig',
]
MIDDLEWARE = [
@ -152,7 +156,7 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'core.middleware.get_request.RequestMiddleware',
'app.middleware.timezone.TimezoneMiddleware',
# 'centurion_feature_flag.middleware.feature_flag.FeatureFlagMiddleware',
'centurion_feature_flag.middleware.feature_flag.FeatureFlagMiddleware',
]
@ -437,7 +441,7 @@ if SSO_ENABLED:
if BUILD_VERSION:
feature_flag_version = str(BUILD_VERSION) + '+' + str(BUILD_SHA)[8:]
feature_flag_version = str(BUILD_VERSION) + '+' + str(BUILD_SHA)[:8]
else:
@ -488,5 +492,59 @@ if FEATURE_FLAGGING_ENABLED:
'cache_dir': str(BASE_DIR) + '/',
'disable_downloading': False,
'unique_id': unique_id,
'version': feature_flag_version
'version': feature_flag_version,
}
if FEATURE_FLAG_OVERRIDES:
feature_flag.update({
'over_rides': FEATURE_FLAG_OVERRIDES
})
if DEBUG or RUNNING_TESTS:
feature_flag.update({ 'disable_downloading': True, })
debug_feature_flags = [
{
"2025-00001": {
"name": "DevOps/Git Repositories",
"description": "Disables Git Repositories and Git Groups. see https://github.com/nofusscomputing/centurion_erp/issues/515",
"enabled": True,
"created": "",
"modified": ""
}
},
{
"2025-00002": {
"name": "Entities",
"description": "Entities see https://github.com/nofusscomputing/centurion_erp/issues/704",
"enabled": True,
"created": "",
"modified": ""
}
},
{
"2025-00003": {
"name": "Role Based Access Control (RBAC)",
"description": "Refactor of authentication and authorization to be RBAC based. see https://github.com/nofusscomputing/centurion_erp/issues/551",
"enabled": True,
"created": "",
"modified": ""
}
},
{
"2025-00004": {
"name": "Accounting Module",
"description": "Accounting related functions. see https://github.com/nofusscomputing/centurion_erp/issues/88",
"enabled": True,
"created": "",
"modified": ""
}
},
]
feature_flag.update({
'over_rides': debug_feature_flags
})

View File

@ -172,8 +172,11 @@ class BaseModel:
print(f'Checking field {field.attname} is not empty')
if (
field.help_text is None
or field.help_text == ''
(
field.help_text is None
or field.help_text == ''
)
and not str(field.attname).endswith('_ptr_id')
):
print(f' Failure on field {field.attname}')

View File

@ -1,60 +0,0 @@
from django.contrib.auth.models import User
from django.shortcuts import reverse
from django.test import Client, TestCase
from rest_framework.permissions import IsAuthenticated
from access.models.organization import Organization
from api.tests.abstract.viewsets import ViewSetCommon
from assistance.viewsets.index import Index
class AssistanceViewset(
TestCase,
ViewSetCommon
):
viewset = Index
route_name = 'v2:_api_v2_assistance_home'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user
3. create super user
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.view_user = User.objects.create_user(username="test_user_add", password="password", is_superuser=True)
client = Client()
url = reverse(self.route_name + '-list')
client.force_login(self.view_user)
self.http_options_response_list = client.options(url)
self.kwargs = {}
def test_view_attr_permission_classes_value(self):
"""Attribute Test
Attribute `permission_classes` must be metadata class `ReactUIMetadata`
"""
view_set = self.viewset()
assert view_set.permission_classes[0] is IsAuthenticated
assert len(view_set.permission_classes) == 1

View File

@ -1,49 +1,22 @@
import pytest
from django.contrib.auth.models import User
from django.test import Client, TestCase
from rest_framework.reverse import reverse
from access.models.organization import Organization
from api.tests.abstract.viewsets import ViewSetModel
from api.tests.unit.test_unit_common_viewset import ModelViewSetInheritedCases
from assistance.viewsets.knowledge_base import ViewSet
class ViewsetCommon(
ViewSetModel,
class KnowledgeBaseViewsetList(
ModelViewSetInheritedCases,
TestCase,
):
viewset = ViewSet
route_name = 'v2:_api_v2_knowledge_base'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization
3. create super user
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.view_user = User.objects.create_user(username="test_view_user", password="password", is_superuser=True)
self.kwargs = {}
class KnowledgeBaseViewsetList(
ViewsetCommon,
TestCase,
):
@classmethod
def setUpTestData(self):

View File

@ -1,49 +1,22 @@
import pytest
from django.contrib.auth.models import User
from django.test import Client, TestCase
from rest_framework.reverse import reverse
from access.models.organization import Organization
from api.tests.abstract.viewsets import ViewSetModel
from api.tests.unit.test_unit_common_viewset import ModelViewSetInheritedCases
from assistance.viewsets.knowledge_base_category import ViewSet
class ViewsetCommon(
ViewSetModel,
class KnowledgeBaseViewsetList(
ModelViewSetInheritedCases,
TestCase,
):
viewset = ViewSet
route_name = 'v2:_api_v2_knowledge_base_category'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization
3. create super user
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.view_user = User.objects.create_user(username="test_view_user", password="password", is_superuser=True)
self.kwargs = {}
class KnowledgeBaseViewsetList(
ViewsetCommon,
TestCase,
):
@classmethod
def setUpTestData(self):

View File

@ -1,47 +1,22 @@
import pytest
from django.contrib.auth.models import User
from django.test import Client, TestCase
from rest_framework.reverse import reverse
from access.models.organization import Organization
from api.tests.abstract.viewsets import ViewSetModel
from api.tests.unit.test_unit_common_viewset import ModelViewSetInheritedCases
from assistance.viewsets.knowledge_base_category_notes import ViewSet
class ViewsetCommon(
ViewSetModel,
class KnowledgeBaseCategoryNotesViewsetList(
ModelViewSetInheritedCases,
TestCase,
):
viewset = ViewSet
route_name = 'v2:_api_v2_knowledge_base_category_note'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization
3. create super user
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.view_user = User.objects.create_user(username="test_view_user", password="password", is_superuser=True)
class KnowledgeBaseCategoryNotesViewsetList(
ViewsetCommon,
TestCase,
):
@classmethod
def setUpTestData(self):

View File

@ -1,47 +1,22 @@
import pytest
from django.contrib.auth.models import User
from django.test import Client, TestCase
from rest_framework.reverse import reverse
from access.models.organization import Organization
from api.tests.abstract.viewsets import ViewSetModel
from api.tests.unit.test_unit_common_viewset import ModelViewSetInheritedCases
from assistance.viewsets.knowledge_base_notes import ViewSet
class ViewsetCommon(
ViewSetModel,
class KnowledgeBaseNotesViewsetList(
ModelViewSetInheritedCases,
TestCase,
):
viewset = ViewSet
route_name = 'v2:_api_v2_knowledge_base_note'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization
3. create super user
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.view_user = User.objects.create_user(username="test_view_user", password="password", is_superuser=True)
class KnowledgeBaseNotesViewsetList(
ViewsetCommon,
TestCase,
):
@classmethod
def setUpTestData(self):

View File

@ -1,59 +1,23 @@
import pytest
from django.contrib.auth.models import User
from django.test import Client, TestCase
from rest_framework.reverse import reverse
from access.models.organization import Organization
from api.tests.abstract.viewsets import ViewSetModel
from api.tests.unit.test_unit_common_viewset import ModelViewSetInheritedCases
from assistance.viewsets.model_knowledge_base_article import ViewSet
from itam.models.device import Device
class ViewsetCommon(
ViewSetModel,
class ModelKnowledgeBaseArticleViewsetList(
ModelViewSetInheritedCases,
TestCase,
):
viewset = ViewSet
route_name = 'v2:_api_v2_model_kb'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization
3. create super user
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.view_user = User.objects.create_user(username="test_view_user", password="password", is_superuser=True)
device = Device.objects.create(
organization = self.organization,
name = 'device'
)
self.kwargs = {
'model': 'itam.device',
'model_pk': device.id,
}
class ModelKnowledgeBaseArticleViewsetList(
ViewsetCommon,
TestCase,
):
@classmethod
def setUpTestData(self):
@ -65,6 +29,16 @@ class ModelKnowledgeBaseArticleViewsetList(
super().setUpTestData()
device = Device.objects.create(
organization = self.organization,
name = 'device'
)
self.kwargs = {
'model': 'itam.device',
'model_pk': device.id,
}
client = Client()

View File

@ -0,0 +1,37 @@
from django.shortcuts import reverse
from django.test import Client, TestCase
from api.tests.unit.test_unit_common_viewset import IndexViewsetInheritedCases
from assistance.viewsets.index import Index
class AssistanceViewset(
IndexViewsetInheritedCases,
TestCase,
):
viewset = Index
route_name = 'v2:_api_v2_assistance_home'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user
3. create super user
"""
super().setUpTestData()
client = Client()
url = reverse(self.route_name + '-list')
client.force_login(self.view_user)
self.http_options_response_list = client.options(url)
self.kwargs = {}

View File

@ -1,50 +1,22 @@
import pytest
from django.contrib.auth.models import User
from django.test import Client, TestCase
from rest_framework.reverse import reverse
from access.models.organization import Organization
from api.tests.abstract.viewsets import ViewSetModel
from api.tests.unit.test_unit_common_viewset import ModelViewSetInheritedCases
from assistance.viewsets.request import ViewSet
class ViewsetCommon(
ViewSetModel,
class RequestViewsetList(
ModelViewSetInheritedCases,
TestCase,
):
viewset = ViewSet
route_name = 'v2:_api_v2_ticket_request'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization
3. create super user
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.view_user = User.objects.create_user(username="test_view_user", password="password", is_superuser=True)
self.kwargs = {}
class RequestViewsetList(
ViewsetCommon,
TestCase,
):
@classmethod
def setUpTestData(self):

View File

@ -14,6 +14,7 @@ feature_flag = {
'disable_downloading': False # Prevent downloading feature flags
'unique_id': 'unique ID for application', # Unique ID for this instance of your Django application
'version': '1.0.0', # The Version of Your Django Application
'over_rides': [] # list(dict). Feature Flag over rides. use same format as API endpoint.
} # Note: All key values are strings
```

View File

@ -76,6 +76,9 @@ class CenturionFeatureFlagging:
_last_modified: datetime = None
""" Last modified date/time of the feature flags"""
_over_rides: dict = None
"""Feature Flag Over rides."""
_response: requests.Response = None
"""Cached response from fetched feature flags"""
@ -95,6 +98,7 @@ class CenturionFeatureFlagging:
disable_downloading: bool = False,
unique_id: str = None,
version: str = None,
over_rides: dict = None,
):
if not str(cache_dir).endswith('/'):
@ -108,6 +112,25 @@ class CenturionFeatureFlagging:
self._disable_downloading = disable_downloading
if self._disable_downloading:
self._feature_flags = {}
_over_rides: dict = {}
if over_rides:
for entry in over_rides:
[*key], [*flag] = zip(*entry.items())
_over_rides.update({
key[0]: FeatureFlag(key[0], flag[0])
})
self._over_rides = _over_rides
if version is None:
@ -142,6 +165,9 @@ class CenturionFeatureFlagging:
self._feature_flags is not None
and self._last_modified is not None
)
or (
self._over_rides is not None
)
):
return True
@ -164,7 +190,10 @@ class CenturionFeatureFlagging:
Returns:
dict: A complete Feature Flag.
"""
if self._feature_flags is None:
if(
self._feature_flags is None
and self._over_rides.get(key, None) is None
):
print('Feature Flagging has not been completly initialized.')
print(' please ensure that the feature flags have been downloaded.')
@ -174,6 +203,7 @@ class CenturionFeatureFlagging:
if(
self._feature_flags.get(key, None) is None
and self._over_rides.get(key, None) is None
and raise_exceptions
):
@ -182,10 +212,19 @@ class CenturionFeatureFlagging:
elif(
not raise_exceptions
and self._feature_flags.get(key, None) is None
and self._over_rides.get(key, None) is None
):
return False
elif(
not raise_exceptions
and self._over_rides.get(key, None) is not None
):
return self._over_rides[key]
return self._feature_flags[key]
@ -258,18 +297,31 @@ class CenturionFeatureFlagging:
self._response = response
fetched_flags += resp.json()['results']
if resp.status_code == 304: # Nothing has changed, exit the loop
url = None
else: # Fetch next page of results
elif resp.ok: # Fetch next page of results
fetched_flags += resp.json()['results']
url = resp.json()['next']
else:
url = None
except requests.exceptions.ConnectionError as err:
print(f'Error Connecting to {url}')
url = None
except requests.exceptions.ReadTimeout as err:
print(f'Connection Timed Out connecting to {url}')
url = None

View File

@ -27,6 +27,7 @@ class Command(BaseCommand):
disable_downloading = settings.feature_flag.get('disable_downloading', False),
unique_id = settings.feature_flag.get('unique_id', None),
version = settings.feature_flag.get('version', None),
over_rides = settings.feature_flag.get('over_rides', None),
)
self.stdout.write('Fetching Feature Flags.....')
@ -39,7 +40,7 @@ class Command(BaseCommand):
else:
self.stdout.stderr('Error. Something went wrong.')
self.stderr.write('Error. Something went wrong.')
if kwargs['reload']:

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