Compare commits

...

179 Commits

Author SHA1 Message Date
c496d10c1a bump: version 1.0.0-b4 → 1.0.0-b5 2024-07-31 17:02:31 +00:00
Jon
3993cc96a5 Merge branch '160-api-config' into 'development'
feat: add Config groups to API

Closes #161 and #160

See merge request nofusscomputing/projects/centurion_erp!45
2024-07-31 16:44:55 +00:00
Jon
a4b37b34a9 docs: Add dets of collection
!45 closes #161
2024-08-01 02:12:42 +09:30
Jon
2f55024f0b fix(api): Ensure device groups is read only
checks for required fields

!45 #160 #162
2024-08-01 01:54:24 +09:30
Jon
213644a51a test(api): Field existence and type checks for device
checks for required fields

!45 #160 #162
2024-08-01 01:12:33 +09:30
Jon
281d839801 feat(api): Add device config groups to devices
!45 #160 nofusscomputing/projects/ansible/collections/centurion_erp_collection!7 nofusscomputing/projects/ansible/collections/centurion_erp_collection#4
2024-08-01 00:32:16 +09:30
Jon
4fd157a785 test(api): test configgroups API fields
tests for type and existence

!45 closes #160
2024-07-31 22:27:45 +09:30
Jon
968b3a0f92 feat(api): Ability to fetch configgroups from api along with config
!45 #160 nofusscomputing/projects/ansible/collections/centurion_erp_collection#4
2024-07-31 22:27:32 +09:30
Jon
f5ba608ed1 ci: var to export for use in script
!45
2024-07-31 21:19:55 +09:30
289668bb7f bump: version 1.0.0-b3 → 1.0.0-b4 2024-07-29 07:54:33 +00:00
Jon
9e28722dba Merge branch 'b3-fixes' into 'development'
fix: release-b3 fixes

Closes #155

See merge request nofusscomputing/projects/centurion_erp!44
2024-07-29 07:38:00 +00:00
Jon
9b673f4a07 fix(api): cleanup team post/get
!44 #159
2024-07-29 17:03:25 +09:30
Jon
3a9e4b29b3 fix(api): confirm HTTP method is allowed before permission check
return HTTP/405 for logged in user ONLY!!

!44 #159
2024-07-29 17:02:52 +09:30
Jon
8d59462561 fix(api): Ensure that organizations can't be created via the API
!44 fixes #155
2024-07-29 17:02:37 +09:30
Jon
098e41e6a1 feat(swagger): remove {format} suffixed doc entries
!44
2024-07-29 16:49:51 +09:30
Jon
fc3f0b39e2 ci: add debug out to extra command
!44
2024-07-29 16:49:24 +09:30
Jon
de53948cea test: confirm that the tenancymanager is called
!43
2024-07-21 13:27:45 +09:30
Jon
823ebc0eb5 fix(access): Team model class inheritance order corrected
!42
2024-07-21 13:27:36 +09:30
41414438d1 bump: version 1.0.0-b2 → 1.0.0-b3 2024-07-21 01:47:05 +00:00
Jon
5704560beb fix(itam): Limit os version count to devices user has access to
!42
2024-07-21 10:07:36 +09:30
Jon
8a48902b64 ci: return command to release
!42
2024-07-20 13:02:21 +09:30
61fe059513 bump: version 1.0.0-b1 → 1.0.0-b2 2024-07-19 10:41:41 +00:00
Jon
94576cc733 ci: fix additional command as part of release
!42
2024-07-19 19:45:50 +09:30
Jon
3a32c62119 fix(itam): only show os version once
!42 fixes #139
2024-07-19 18:01:10 +09:30
Jon
9ea4fe1adc ci: Create Version labels within repo on release
!42
2024-07-19 18:01:10 +09:30
Jon
0798a672c2 docs(administration): spread the love out
!42
2024-07-19 18:01:10 +09:30
f4e68529ba bump: version 1.0.0-a4 → 1.0.0-b1 2024-07-19 07:15:09 +00:00
Jon
92a411baec docs(administration): explain the magic
!42 #74
2024-07-19 16:27:33 +09:30
Jon
034857d088 ci: dev branch releases now beta
preperation for RC. all dev releases are now beta.

!42 #74
2024-07-19 15:35:32 +09:30
Jon
e5ce86a9bb fix(itam): ensure installed operating system count is limited to users organizations
!42
2024-07-19 11:02:56 +09:30
Jon
5188b3d52e fix(itam): ensure installed software count is limited to users organizations
!42
2024-07-19 00:39:05 +09:30
61b9435d1f bump: version 1.0.0-a3 → 1.0.0-a4 2024-07-18 12:59:03 +00:00
Jon
8244676530 test: ensure inventory upload matches by both serial number and uuid if device name different
!42 #15
2024-07-18 22:05:12 +09:30
Jon
ec1e7cca85 test: placeholder for moving organization
!42 #15
2024-07-18 22:04:27 +09:30
Jon
72ab9253d7 feat(api): When processing uploaded inventory and name does not match, update name to one within inventory file
!43
2024-07-18 17:08:52 +09:30
Jon
4f89255c4f feat(config_management): Group name to be entire breadcrumb
!43
2024-07-18 16:51:22 +09:30
8d6d1d0d56 bump: version 1.0.0-a2 → 1.0.0-a3 2024-07-18 06:25:34 +00:00
Jon
2d0c3a660a fix(config_management): dont attempt to do action during save if group being created
!42
2024-07-18 15:34:25 +09:30
Jon
974a208869 chore(config_management): remove org filter as its not required
not required as org filtering is done as part of the initial queryset within the model.

!42
2024-07-18 15:34:25 +09:30
Jon
7f225784c2 chore(settings): remove org filter as its not required
not required as org filtering is done as part of the initial queryset within the model.

!42
2024-07-18 15:34:25 +09:30
Jon
a3be95013c fix(itam): remove org filter for device so that user can see installations
not required as org filtering is done as part of the initial queryset within the model.

!42
2024-07-18 15:34:25 +09:30
Jon
adefbf3960 fix(itam): remove org filter for operating systems so that user can see installations
not required as org filtering is done as part of the initial queryset within the model.

!42
2024-07-18 15:34:25 +09:30
Jon
9a1ca7a104 fix(itam): remove org filter for software so that user can see installations
not required as org filtering is done as part of the initial queryset within the model.

!42
2024-07-18 15:34:25 +09:30
Jon
e84e80cd8f feat(config_management): Prevent a config group from being able to change organization
!42
2024-07-18 15:34:25 +09:30
Jon
ebc266010a feat(itam): On device organization change remove config groups
!42
2024-07-18 15:34:25 +09:30
Jon
519277e18b fix(itam): Device related items should not be global.
!42
2024-07-18 15:34:25 +09:30
Jon
a5a5874211 fix(itam): When changing device organization move related items too.
!42 fixes #137
2024-07-18 15:34:25 +09:30
fa2b90ee7b bump: version 1.0.0-a1 → 1.0.0-a2 2024-07-17 16:53:14 +00:00
Jon
5c74360842 fix(base): dont show user warning bar for non-authenticated user
!42
2024-07-18 01:48:57 +09:30
Jon
8457f15eca fix(api): correct inventory operating system selection by name
!42 #134
2024-07-18 01:48:21 +09:30
Jon
5bc5a4b065 docs(worker): add worker and task logs
!42 fixes #135
2024-07-18 01:28:22 +09:30
Jon
40350d166e feat(api): Inventory matching of device second by uuid
!42 #134
2024-07-18 01:17:51 +09:30
Jon
9a94ba31e4 feat(api): Inventory matching of device first by serial number
!42 #134
2024-07-18 01:17:29 +09:30
Jon
55197e7dcc fix(api): correct inventory operating system and it's linking to device
wasn't updating existing device os

!42 #134
2024-07-18 01:16:34 +09:30
Jon
a67bc70503 fix(api): correct inventory device search to be case insensitive
!42 fixes #134
2024-07-18 01:14:33 +09:30
Jon
60538e1cec feat(base): show warning bar if the user has not set a default organization
!42 fixes #133
2024-07-17 23:51:25 +09:30
Jon
416e029c23 revert: return ci build settings to not include branch alpha
partial revert of a1759ecaaf

!42
2024-07-17 11:00:41 +09:30
fe64c11927 bump: version 0.7.0 → 1.0.0-a1 2024-07-16 05:56:44 +00:00
Jon
1f8244ae40 ci: use updated commitizen
!42 !40 #74
2024-07-16 15:11:39 +09:30
Jon
9871cf248b Merge branch 'v-1-0-0-alpha' into 'development'
refactor: repo preperation for v1.0.0-Alpha-1

See merge request nofusscomputing/projects/centurion_erp!40
2024-07-16 04:24:44 +00:00
Jon
a1759ecaaf ci: add alpha branch to docker builds and publish
!40 !42 #74
2024-07-16 13:39:52 +09:30
Jon
30e0342f52 ci: temp change to release, on dev to be alpha release
!40 !35 #74
2024-07-16 13:31:18 +09:30
Jon
5a201ef548 refactor!: Squash database migrations
BREAKING CHANGE: squashed DB migrations in preparation for v1.0 release.

!40 !35 #74
2024-07-16 13:31:18 +09:30
Jon
7b26fac73d feat: Administratively set global items org/is_global field now read-only
!42 fixes #126
2024-07-16 13:19:30 +09:30
Jon
7c62309a31 fix(core): migrate manufacturer to use new form/view logic
!42 fixes #127
2024-07-16 11:55:25 +09:30
Jon
621cbd2d71 revert: return organization filtering back to forms
!42 #124
2024-07-16 00:02:45 +09:30
Jon
d8e89bee10 test: tenancy objects
!42 #15 closes #124
2024-07-15 23:22:15 +09:30
Jon
4ee62aa399 test: refactor to single abstract model for inclusion.
!42 #15
2024-07-15 23:01:49 +09:30
Jon
f1201e8933 feat(access): Add multi-tennant manager
manager filters results to that of data from the organizations the users is part of.

!42 #124
2024-07-15 16:17:08 +09:30
Jon
9acc4fdfcb docs(gitlab): update MR template
!42 #74
2024-07-14 17:16:16 +09:30
Jon
6776612b66 chore: move docker-compose to deploy directory
intent of dir is that is the location for all avail deploy methods

!42 #74
2024-07-14 17:11:49 +09:30
Jon
af3e770760 fix(settings): correct the permission to view manufacturers
!42 #74
2024-07-14 17:09:18 +09:30
Jon
fbe7e63cc9 fix(access): Correct team form fields
fixes missing name for team

!42 #74
2024-07-14 16:57:25 +09:30
Jon
aec460306b fix(config_management): don't exclude parent from field, only self
!42 #74
2024-07-14 16:48:54 +09:30
46c4419350 bump: version 0.6.0 → 0.7.0 2024-07-14 06:25:37 +00:00
Jon
1f35893db9 chore: footer text to be grey
does not need to be prominent

!35
2024-07-14 15:27:03 +09:30
Jon
935e10dc24 docs(development): add initial forms
!35
2024-07-14 04:56:11 +09:30
Jon
a4617c28f8 chore: complete footer layout
git icon from https://gitlab-org.gitlab.io/gitlab-svgs

!35 closes #25
2024-07-14 04:22:41 +09:30
Jon
d4aaea4dbb docs(development): update views, models and index
!35
2024-07-14 03:10:42 +09:30
Jon
a7168834ba chore: add new exception MissingAttribute
used for development

!35
2024-07-14 02:52:28 +09:30
Jon
329049e81d docs: roadmap update
!35
2024-07-14 01:51:53 +09:30
Jon
e25ec12cb0 ci: correct test report path
!35
2024-07-14 01:15:54 +09:30
Jon
5ae487cd3e fix(config_management): Don't allow a config group to assign itself as its parent
!35 fixes #122
2024-07-14 01:09:19 +09:30
Jon
4c42f77692 feat(core): Filter every form field if associated with an organization to users organizations only
!35 fixes #119
2024-07-14 01:08:00 +09:30
Jon
3aab7b57e8 fix(config_management): correct permission for deleting a host from config group
!35
2024-07-13 23:12:31 +09:30
Jon
931c9864db fix(config_management): use parent group details to work out permissions when adding a host
!35 fixes #120
2024-07-13 23:11:59 +09:30
Jon
65bf994619 fix(config_management): use parent group details to work out permissions
!35 fixes #121
2024-07-13 22:44:54 +09:30
Jon
367c4bebb6 refactor: adjust views missing add/change form to now use forms
!35 #15 #46 #74 #120 #121 fixes #118
2024-07-13 17:32:45 +09:30
Jon
8c1be67974 test: add test test_view_*_attribute_not_exists_fields for add and change views
!25 #15 #46
2024-07-13 17:25:20 +09:30
Jon
789c035a03 test: fix test_view_change_attribute_type_form_class to test if type class
!25 #15 #46
2024-07-13 17:23:56 +09:30
Jon
1cf15f7339 feat(core): add var template_name to common view template for all views that require it
!35
2024-07-13 16:01:12 +09:30
Jon
77ff580f19 fix(itam): Add missing permissions to software categories index view
!35 #74
2024-07-13 16:01:12 +09:30
Jon
423ff11d4c fix(itam): Add missing permissions to device types index view
!35 #74
2024-07-13 16:01:12 +09:30
Jon
9e4b5185b1 fix(itam): Add missing permissions to device model index view
!35 #74
2024-07-13 16:01:12 +09:30
Jon
020441c41a fix(settings): Add missing permissions to app settings view
!35 #74
2024-07-13 16:01:12 +09:30
Jon
d0a3b7b49d fix(itam): Add missing permissions to software index view
!35 #74
2024-07-13 16:01:12 +09:30
Jon
960fa5485d fix(itam): Add missing permissions to operating system index view
!35 #74
2024-07-13 16:01:12 +09:30
Jon
26db463044 fix(itam): Add missing permissions to device index view
!35 #74
2024-07-13 16:01:12 +09:30
Jon
1193f1d86d fix(config_management): Add missing permissions to group views
!35 #74
2024-07-13 16:01:12 +09:30
Jon
9bece0a811 test(views): add test cases for model views
!35  #46 #15 #118 #120 #121
2024-07-13 16:01:12 +09:30
Jon
f29ec63f46 test: Add Test case abstract classes to models
!35 #46 #15
2024-07-13 16:01:12 +09:30
Jon
e48278e6e9 Merge branch 'docs' into 'development'
docs(centurion_docs): update docs

See merge request nofusscomputing/projects/centurion_erp!41
2024-07-13 04:19:40 +00:00
c41c7ed1f0 docs: update mkdocs
Change Repo name
Update URL and URI

!41
2024-07-13 12:04:59 +09:30
c9190e9a7d docs: Update index
Slight re-word

nofusscomputing/projects/centurion_erp!41
2024-07-13 11:58:15 +09:30
0294f5ed65 docs(centurion): replace Django ITSM -> Centurion ERP
nofusscomputing/projects/centurion_erp!41
2024-07-13 11:57:34 +09:30
Jon
ae4fdcfc58 chore: clean-up readme
!35
2024-07-12 13:50:34 +09:30
Jon
a395f30bd4 fix(security) update djangorestframework 3.15.1 -> 3.15.2
[CVE-2024-21520](https://cwe.mitre.org/data/definitions/79.html)

!35
2024-07-12 13:48:06 +09:30
Jon
c057ffdc9c feat(core): add Display view to common forms abstract class
intended to display generic data

!35
2024-07-12 12:47:17 +09:30
Jon
6837c38303 feat(navigation): always show every menu for super admin
!35
2024-07-12 12:44:41 +09:30
Jon
ee8920a464 fix(navigation): always show settings menu entry
!35
2024-07-12 09:27:29 +09:30
Jon
ccfdf005f7 chore: remove issue#13 links for model notes
!35 closes #13
2024-07-12 09:23:12 +09:30
Jon
0276f9454b refactor: add navigation menu expand arrows
!35 closes #21
2024-07-12 09:19:13 +09:30
Jon
45cc34284a feat(core): only display navigation menu item if use can view model
!35 fixes #114
2024-07-12 07:16:05 +09:30
Jon
7329a65ae7 docs: update roadmap
!35
2024-07-12 05:56:22 +09:30
Jon
9a529a64e2 docs: add bug count badge
!35
2024-07-12 05:53:21 +09:30
Jon
f2640df0d3 feat(django): update 5.0.6 -> 5.0.7
!35
2024-07-12 05:51:21 +09:30
Jon
7d172fb4af refactor: migrate views to use new abstract model view classes
!35 fixes #111
2024-07-12 05:50:36 +09:30
Jon
f848d01b34 refactor: migrate forms to use new abstract model form class
!35
2024-07-12 05:47:49 +09:30
Jon
44f20b28be feat(core): add common forms abstract class
form class for inclusion in our forms

!35
2024-07-12 05:45:39 +09:30
Jon
2e22a484a0 feat(core): add common views abstract class
class for our views

!35
2024-07-12 05:03:11 +09:30
Jon
a62a36ba82 fix(itam): cater for fields that are prefixed
!35 fixes #112
2024-07-11 18:27:07 +09:30
Jon
c00cf16bc8 fix(itam): Ability to view software category
Ensure organization filters to list of orgs

!35
2024-07-11 18:12:07 +09:30
Jon
7784dfede9 fix(itam): correct view permission
!35
2024-07-11 18:01:10 +09:30
Jon
03d350e302 fix(access): When adding a new team to org ensure parent model is fetched
!35
2024-07-11 17:56:12 +09:30
Jon
9b79c9d7ff docs: update readme
!35
2024-07-11 16:57:21 +09:30
Jon
1d5c86f13b fix(access): enable org manager to view orgs
corrects http/500

!35 fixes #105
2024-07-11 16:34:19 +09:30
Jon
9e336d368d fix(settings): restrict user visible organizations to ones they are part of
this includes if they are the org manager

!35 fixes #99
2024-07-11 15:53:50 +09:30
Jon
937e935949 fix(access): enable org manager to view orgs
!35 fixes #105
2024-07-11 15:37:16 +09:30
Jon
860eaa6749 fix(access): fetch object if method exists
enables the setting og self.model with get_object method

!35 fixes #105
2024-07-11 14:25:53 +09:30
Jon
aab94431a9 fix(docs): update docs link to new path
!35 fixes #103
2024-07-11 14:08:18 +09:30
Jon
7cfede45b8 refactor(access): Rename Team Button "new user" -> "Assign User"
!35 fixes #110
2024-07-11 14:00:55 +09:30
Jon
65de93715d refactor(access): model pk and name not required context for adding a device
!35
2024-07-11 13:59:58 +09:30
Jon
fea7ea3119 refactor: rename field "model notes" -> "Notes"
!35 fixes #102 #104
2024-07-11 13:58:14 +09:30
Jon
524a70ba18 fix(access): correctly set team user parent model to team
!35 fixes #109
2024-07-11 13:43:27 +09:30
Jon
29c4b4a0ca fix(access): fallback to django permissions if org permissions check is false
!35 #109 fixes #101
2024-07-11 13:41:36 +09:30
Jon
f5ae01b08d fix(access): Correct logic so that org managers can see orgs they manage
!35 fixes #100
2024-07-10 17:23:24 +09:30
Jon
ee3dd68cfe fix(base): add missing content_title to context
!35 #74
2024-07-10 15:58:18 +09:30
Jon
25efa31493 fix(access): Enable Organization Manager to view organisations they are assigned to
!35 fixes #100
2024-07-10 15:14:53 +09:30
Jon
4a6ce35332 fix(api): correct logic for adding inventory UUID and serial number to device
!35
2024-07-10 14:45:46 +09:30
Jon
332810ffd6 feat: add postgreSQL database support
!35
2024-07-10 14:05:34 +09:30
Jon
f0bbd22cf4 refactor: remove settings model
wasnt required, so removed

!35
2024-07-10 12:02:06 +09:30
Jon
6bf681530d chore: fix docs indent
!35
2024-07-10 03:04:04 +09:30
Jon
9dd2f6a341 docs: fix mkdocs navigation
!35
2024-07-10 02:13:32 +09:30
Jon
23c640a460 docs: add roadmap
!35
2024-07-10 02:13:12 +09:30
Jon
3c6092f776 chore: rename to Centurion ERP
!35
2024-07-10 01:58:19 +09:30
Jon
cb66b9303a feat(ui): add config groups navigation icon
!35
2024-07-09 22:50:38 +09:30
Jon
2d80f02634 fix(ui): navigation alignment and software icon
!35
2024-07-09 22:34:35 +09:30
Jon
abe1ce6948 fix(ui): display organization manager name instead of ID
!35
2024-07-09 22:31:55 +09:30
Jon
fb907283b0 refactor(ui): increase indentation to sub-menu items
!35
2024-07-09 17:32:29 +09:30
Jon
86ed7318ec fix(access): ensure name param exists before attempting to access
!35
2024-07-09 17:29:48 +09:30
Jon
a2a8e12046 feat(ui): add some navigation icons
!35 #21 #22 #23
2024-07-09 17:21:46 +09:30
Jon
c1a8ee65f2 refactor(itam): rename old inventory status icon for use with security
!35
2024-07-09 16:24:52 +09:30
Jon
6a14f78bf7 feat(itam): update inventory status icon
!35
2024-07-09 16:24:10 +09:30
Jon
90a01911da fix(itam): dont show none/nil for device fields containing no value
!35
2024-07-09 15:47:27 +09:30
Jon
de3ed3a881 fix(itam): show device model name instead of ID
!35
2024-07-09 15:42:46 +09:30
Jon
656807e410 feat(itam): ensure device software pagination links keep interface on software tab
!35 closes #81
2024-07-09 14:33:22 +09:30
Jon
f64be2ea33 fix(api): Ensure if serial number from inventory is null that it's not used
!35 fixes #78
2024-07-09 13:21:19 +09:30
Jon
ef9c596ec7 fix(api): ensure checked uuid and serial number is used for updating
!35
2024-07-09 05:28:48 +09:30
Jon
f22e886d92 Merge branch '76-background-worker' into 'development'
feat: "Migrate inventory processing to background worker"

See merge request nofusscomputing/projects/django_template!39
2024-07-08 18:34:33 +00:00
Jon
a2c67541ec test(inventory): add mocks?? for calling background worker
!39 #76
2024-07-09 03:51:21 +09:30
Jon
5f4231ab04 test(view): view permission checks
!39 #76
2024-07-09 02:29:35 +09:30
Jon
b0405c8fd0 test(inventory): update tests for background worker changes
!39 #76
2024-07-09 02:27:24 +09:30
Jon
b42bb3a30e feat(access): enable non-organization django permission checks
!39 #76
2024-07-09 02:26:21 +09:30
Jon
27eb54cc37 docs(api): update swagger docs with inventory changes
!39 #76
2024-07-08 23:10:34 +09:30
Jon
a8e2c687b1 docs(administration): notate rabbitMQ setup
!39 #76
2024-07-08 23:06:51 +09:30
Jon
7aeba34787 refactor(api): migrate inventory processing to background worker
!39 #76
2024-07-08 22:54:34 +09:30
Jon
090c4a5425 feat(settings): Add celery task results index and view page
!39 #76
2024-07-08 22:52:34 +09:30
Jon
87a1f2aa20 feat(base): Add background worker
!39 #76
2024-07-08 22:38:06 +09:30
Jon
70135eaa91 Merge branch 'fixes-inventory' into 'development'
fix: inventory

See merge request nofusscomputing/projects/django_template!38
2024-07-06 06:17:54 +00:00
Jon
f47b97e2a0 refactor(itam): only perform actions on device inventory if DB matches inventory item
!38 #75
2024-07-06 15:45:48 +09:30
Jon
67f20ecb66 fix(itam): only remove device software when not found during inventory upload
!38 #75
2024-07-06 15:45:48 +09:30
Jon
3bceb66600 fix(itam): only update software version if different
!38 #75
2024-07-06 15:45:48 +09:30
Jon
fe34b8274d chore: update submodules to head
!35
2024-07-01 04:03:05 +09:30
Jon
a235aa7ec3 ci: add submodule update job
!35
2024-07-01 04:02:51 +09:30
Jon
f69f883439 Merge branch '66-fix-inventory-uuid-update' into 'development'
fix: existing device without uuid not updated when uploading an inventory

Closes #66

See merge request nofusscomputing/projects/django_template!37
2024-06-30 15:06:02 +00:00
Jon
7b4ed7b135 feat(itam): Update Serial Number from inventory if present and Serial Number not set
!37
2024-07-01 00:27:46 +09:30
Jon
b801c9a49e feat(itam): Update UUID from inventory if present and UUID not set
!37 fixes #66
2024-07-01 00:27:28 +09:30
Jon
583e1767a1 Merge branch '67-fix-device-software-pagination' into 'development'
fix: Device Software tab pagination does not work

Closes #67

See merge request nofusscomputing/projects/django_template!36
2024-06-30 14:33:19 +00:00
Jon
241ba47c80 fix(itam): correct device software pagination
!36 fixes #67
2024-07-01 00:01:28 +09:30
299 changed files with 7272 additions and 2656 deletions

View File

@ -1,7 +1,8 @@
---
commitizen:
name: cz_conventional_commits
prerelease_offset: 1
tag_format: $version
update_changelog_on_bump: false
version: 0.6.0
version: 1.0.0-b5
version_scheme: semver

5
.gitignore vendored
View File

@ -1,10 +1,11 @@
venv/**
*/static/**
__pycache__
**db.sqlite3
**.sqlite3
**.sqlite
**.coverage
artifacts/
**.tmp.*
volumes/
build/
pages/
pages/

View File

@ -2,21 +2,24 @@
variables:
MY_PROJECT_ID: "57560288"
GIT_SYNC_URL: "https://$GITHUB_USERNAME_ROBOT:$GITHUB_TOKEN_ROBOT@github.com/NoFussComputing/django_template.git"
GIT_SYNC_URL: "https://$GITHUB_USERNAME_ROBOT:$GITHUB_TOKEN_ROBOT@github.com/NoFussComputing/centurion_erp.git"
# Docker Build / Publish
DOCKER_IMAGE_BUILD_TARGET_PLATFORMS: "linux/amd64,linux/arm64"
DOCKER_IMAGE_BUILD_NAME: django-template
DOCKER_IMAGE_BUILD_NAME: centurion-erp
DOCKER_IMAGE_BUILD_REGISTRY: $CI_REGISTRY_IMAGE
DOCKER_IMAGE_BUILD_TAG: $CI_COMMIT_SHA
# Docker Publish
DOCKER_IMAGE_PUBLISH_NAME: django-template
DOCKER_IMAGE_PUBLISH_NAME: centurion-erp
DOCKER_IMAGE_PUBLISH_REGISTRY: docker.io/nofusscomputing
DOCKER_IMAGE_PUBLISH_URL: https://hub.docker.com/r/nofusscomputing/$DOCKER_IMAGE_PUBLISH_NAME
# Extra release commands
MY_COMMAND: ./.gitlab/additional_actions_bump.sh
# Docs NFC
PAGES_ENVIRONMENT_PATH: projects/django-template/
PAGES_ENVIRONMENT_PATH: projects/centurion_erp/
# RELEASE_ADDITIONAL_ACTIONS_BUMP: ./.gitlab/additional_actions_bump.sh
@ -31,6 +34,9 @@ include:
- template/automagic.gitlab-ci.yaml
Update Git Submodules:
extends: .ansible_playbook_git_submodule
Docker Container:
extends: .build_docker_container
@ -112,6 +118,120 @@ Docker Container:
- when: never
.gitlab_release:
stage: release
image: registry.gitlab.com/gitlab-org/release-cli:latest
before_script:
- if [ "0$JOB_ROOT_DIR" == "0" ]; then ROOT_DIR=gitlab-ci; else ROOT_DIR=$JOB_ROOT_DIR ; fi
- echo "[DEBUG] ROOT_DIR[$ROOT_DIR]"
- mkdir -p "$CI_PROJECT_DIR/artifacts/$CI_JOB_STAGE/$CI_JOB_NAME"
- mkdir -p "$CI_PROJECT_DIR/artifacts/$CI_JOB_STAGE/tests"
- apk update
- apk add git curl
- apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python
- python -m ensurepip && ln -sf pip3 /usr/bin/pip
- pip install --upgrade pip
- pip install -r $ROOT_DIR/gitlab_release/requirements.txt
# - pip install $ROOT_DIR/gitlab_release/python-module/cz_nfc/.
- pip install commitizen --force
- 'CLONE_URL="https://gitlab-ci-token:$GIT_COMMIT_TOKEN@gitlab.com/$CI_PROJECT_PATH.git"'
- echo "[DEBUG] CLONE_URL[$CLONE_URL]"
- git clone -b development $CLONE_URL repo
- cd repo
- git branch
- git config --global user.email "helpdesk@nofusscomputing.com"
- git config --global user.name "nfc_bot"
- git push --set-upstream origin development
- RELEASE_VERSION_CURRENT=$(cz version --project)
script:
- if [ "$CI_COMMIT_BRANCH" == "development" ] ; then RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout --prerelease beta); else RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout); fi
- RELEASE_VERSION_NEW=$(cz version --project)
- export RELEASE_TAG=$RELEASE_VERSION_NEW
- echo "[DEBUG] RELEASE_VERSION_CURRENT[$RELEASE_VERSION_CURRENT]"
- echo "[DEBUG] RELEASE_CHANGELOG[$RELEASE_CHANGELOG]"
- echo "[DEBUG] RELEASE_VERSION_NEW[$RELEASE_VERSION_NEW]"
- echo "[DEBUG] RELEASE_TAG[$RELEASE_TAG]"
- RELEASE_TAG_SHA1=$(git log -n1 --format=format:"%H")
- echo "[DEBUG] RELEASE_TAG_SHA1[$RELEASE_TAG_SHA1]"
- |
if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then
echo "[DEBUG] not running extra actions, no new version";
else
echo "[DEBUG] Creating new Version Label";
echo "----------------------------";
echo ${MY_COMMAND};
echo "----------------------------";
cat ${MY_COMMAND};
echo "----------------------------";
${MY_COMMAND};
echo "----------------------------";
fi
- if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No tag to delete, version was not bumped"; else git tag -d $RELEASE_TAG; fi
- if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No push will be conducted, version was not bumped"; else git push; fi
- if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No release will be created, version was not bumped"; else release-cli create --name "Release $RELEASE_TAG" --tag-name "$RELEASE_TAG" --tag-message "$RELEASE_CHANGELOG" --ref "$RELEASE_TAG_SHA1" --description "$RELEASE_CHANGELOG"; fi
- if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git checkout master; fi
- if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git push --set-upstream origin master; fi
- if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git merge --no-ff development; fi
- if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git push origin master; fi
after_script:
- rm -Rf repo
rules:
- if: '$JOB_STOP_GITLAB_RELEASE'
when: never
- if: "$CI_COMMIT_AUTHOR =='nfc_bot <helpdesk@nofusscomputing.com>'"
when: never
- if: # condition_master_branch_push
$CI_COMMIT_BRANCH == "master" &&
$CI_PIPELINE_SOURCE == "push"
allow_failure: false
when: on_success
- if: # condition_dev_branch_push
$CI_COMMIT_BRANCH == "development" &&
$CI_PIPELINE_SOURCE == "push"
when: manual
allow_failure: true
# for testing
# - if: '$CI_COMMIT_BRANCH != "master"'
# when: always
# allow_failure: true
- when: never
#
# Release
#
Gitlab Release:
extends:
- .gitlab_release
Docker.Hub.Branch.Publish:
extends: .publish-docker-hub
needs: [ "Docker Container" ]
@ -159,13 +279,8 @@ Github (Push --mirror):
when: never
- if: # condition_master_or_dev_push
(
$CI_COMMIT_BRANCH == "master"
||
$CI_COMMIT_BRANCH == "development"
||
$CI_COMMIT_BRANCH == "14-feat-project-management"
) &&
$CI_COMMIT_BRANCH
&&
$CI_PIPELINE_SOURCE == "push"
when: always

View File

@ -0,0 +1,7 @@
#!/bin/sh
# Create Version label wtihn repo
curl \
--data "name=v${RELEASE_TAG}&color=#eee600&description=Version%20that%20is%20affected" \
--header "PRIVATE-TOKEN: $GIT_COMMIT_TOKEN" \
"https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/labels"

View File

@ -20,16 +20,18 @@
<!-- dont remove tasks below strike through including the checkbox by enclosing in double tidle '~~' -->
- [ ] ~"breaking-change" Any Breaking change(s)
- [ ] Contains ~"breaking-change" Any Breaking change(s)?
_Breaking Change must also be notated in the commit that introduces it and in [Conventional Commit Format](https://www.conventionalcommits.org/en/v1.0.0/)._
- [ ] Release notes updated
- [ ] ~Documentation Documentation written
_All features to be documented within the correct section(s). Administration, Development and/or User_
- [ ] Milestone assigned
- [ ] [Unit Test(s) Written](https://nofusscomputing.com/projects/django-template/development/testing/)
- [ ] [Unit Test(s) Written](https://nofusscomputing.com/projects/centurion_erp/development/testing/)
_ensure test coverage delta is not less than zero_

View File

@ -49,7 +49,7 @@ Unit:
- artifacts/
environment:
name: Unit Test Coverage Report
url: https://nofusscomputing.gitlab.io/-/projects/django_template/-/jobs/${CI_JOB_ID}/artifacts/artifacts/coverage/index.html
url: https://nofusscomputing.gitlab.io/-/projects/centurion_erp/-/jobs/${CI_JOB_ID}/artifacts/artifacts/coverage/index.html
UI:

17
.vscode/launch.json vendored
View File

@ -15,6 +15,23 @@
"django": true,
"autoStartBrowser": false,
"program": "${workspaceFolder}/app/manage.py"
},
{
"name": "Debug: Celery",
"type": "python",
"request": "launch",
"module": "celery",
"console": "integratedTerminal",
"args": [
"-A",
"app",
"worker",
"-l",
"INFO",
"-n",
"debug-itsm@%h"
],
"cwd": "${workspaceFolder}/app"
}
]
}

View File

@ -13,4 +13,8 @@
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"testing.coverageToolbarEnabled": true,
"cSpell.words": [
"ITSM"
],
"cSpell.language": "en-AU",
}

View File

@ -1,3 +1,206 @@
## 1.0.0-b5 (2024-07-31)
### Feat
- **api**: Add device config groups to devices
- **api**: Ability to fetch configgroups from api along with config
### Fix
- **api**: Ensure device groups is read only
## 1.0.0-b4 (2024-07-29)
### Feat
- **swagger**: remove `{format}` suffixed doc entries
### Fix
- **api**: cleanup team post/get
- **api**: confirm HTTP method is allowed before permission check
- **api**: Ensure that organizations can't be created via the API
- **access**: Team model class inheritance order corrected
## 1.0.0-b3 (2024-07-21)
### Fix
- **itam**: Limit os version count to devices user has access to
## 1.0.0-b2 (2024-07-19)
### Fix
- **itam**: only show os version once
## 1.0.0-b1 (2024-07-19)
### Fix
- **itam**: ensure installed operating system count is limited to users organizations
- **itam**: ensure installed software count is limited to users organizations
## 1.0.0-a4 (2024-07-18)
### Feat
- **api**: When processing uploaded inventory and name does not match, update name to one within inventory file
- **config_management**: Group name to be entire breadcrumb
## 1.0.0-a3 (2024-07-18)
### Feat
- **config_management**: Prevent a config group from being able to change organization
- **itam**: On device organization change remove config groups
### Fix
- **config_management**: dont attempt to do action during save if group being created
- **itam**: remove org filter for device so that user can see installations
- **itam**: remove org filter for operating systems so that user can see installations
- **itam**: remove org filter for software so that user can see installations
- **itam**: Device related items should not be global.
- **itam**: When changing device organization move related items too.
## 1.0.0-a2 (2024-07-17)
### Feat
- **api**: Inventory matching of device second by uuid
- **api**: Inventory matching of device first by serial number
- **base**: show warning bar if the user has not set a default organization
### Fix
- **base**: dont show user warning bar for non-authenticated user
- **api**: correct inventory operating system selection by name
- **api**: correct inventory operating system and it's linking to device
- **api**: correct inventory device search to be case insensitive
## 1.0.0-a1 (2024-07-16)
### BREAKING CHANGE
- squashed DB migrations in preparation for v1.0 release.
### Feat
- Administratively set global items org/is_global field now read-only
- **access**: Add multi-tennant manager
### Fix
- **core**: migrate manufacturer to use new form/view logic
- **settings**: correct the permission to view manufacturers
- **access**: Correct team form fields
- **config_management**: don't exclude parent from field, only self
### Refactor
- Squash database migrations
## 0.7.0 (2024-07-14)
### Bug Fixes
- **config_management**: [5ae487cd](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/5ae487cd3eac2f5273d3b2a9e7642e714bdbde68) - Don't allow a config group to assign itself as its parent [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#122](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/122) ]
- **config_management**: [3aab7b57](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/3aab7b57e80b48e1f4671413034c1c71dfad4c66) - correct permission for deleting a host from config group [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **config_management**: [931c9864](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/931c9864db8e14072a0a7e331d525aeedd19eb2a) - use parent group details to work out permissions when adding a host [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#120](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/120) ]
- **config_management**: [65bf9946](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/65bf994619949c4d42bfa92e06e8c63f67acabca) - use parent group details to work out permissions [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#121](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/121) ]
- **itam**: [77ff580f](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/77ff580f19e41a430cfa7d3bf1ba3870d7993cf9) - Add missing permissions to software categories index view [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#74](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/74) ]
- **itam**: [423ff11d](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/423ff11d4c30670cb8c7832b452af78cf54a5fd3) - Add missing permissions to device types index view [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#74](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/74) ]
- **itam**: [9e4b5185](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/9e4b5185b144ade67b98aeb6e909e1af39b62545) - Add missing permissions to device model index view [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#74](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/74) ]
- **settings**: [020441c4](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/020441c41aae4ccc3453becdf451918bc3a432f7) - Add missing permissions to app settings view [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#74](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/74) ]
- **itam**: [d0a3b7b4](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/d0a3b7b49dfa8c0468106652af723b824c7ecf89) - Add missing permissions to software index view [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#74](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/74) ]
- **itam**: [960fa548](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/960fa5485d3198c1e4868957e6d57f3c8bd65cf8) - Add missing permissions to operating system index view [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#74](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/74) ]
- **itam**: [26db4630](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/26db4630445ceff412a6d12f97f661e95c165a3b) - Add missing permissions to device index view [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#74](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/74) ]
- **config_management**: [1193f1d8](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/1193f1d86d247e5df77dc44bae36a5534c0f0c88) - Add missing permissions to group views [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#74](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/74) ]
- **navigation**: [ee8920a4](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/ee8920a464017e2ec7ef714530a105197eaae75b) - always show settings menu entry [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **itam**: [a62a36ba](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/a62a36ba82252257646325679180c68632971c52) - cater for fields that are prefixed [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#112](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/112) ]
- **itam**: [c00cf16b](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/c00cf16bc8ac85f5c5bf19d30cf78ae9a838d00f) - Ability to view software category [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **itam**: [7784dfed](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/7784dfede98f94bd1a5e4df5155130e91876fe67) - correct view permission [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **access**: [03d350e3](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/03d350e302c443ff53fc71e32bc32f489af1409a) - When adding a new team to org ensure parent model is fetched [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **access**: [1d5c86f1](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/1d5c86f13b2df36e0562e1fe3b6aca7dfa04a7ec) - enable org manager to view orgs [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#105](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/105) ]
- **settings**: [9e336d36](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/9e336d368d51381701b358287853f9ceab61b49f) - restrict user visible organizations to ones they are part of [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#99](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/99) ]
- **access**: [937e9359](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/937e9359498da9388fd730c69e591679458c331e) - enable org manager to view orgs [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#105](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/105) ]
- **access**: [860eaa67](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/860eaa674937dc9ff1690615bafcddaab38d890d) - fetch object if method exists [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#105](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/105) ]
- **docs**: [aab94431](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/aab94431a9d3864ace91af70e29b5dc59b61fd6f) - update docs link to new path [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#103](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/103) ]
- **access**: [524a70ba](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/524a70ba184c0af1125636f5923492ba65765f1e) - correctly set team user parent model to team [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#109](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/109) ]
- **access**: [29c4b4a0](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/29c4b4a0caaa37866774235d56312f2b9ede8148) - fallback to django permissions if org permissions check is false [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#109](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/109) [#101](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/101) ]
- **access**: [f5ae01b0](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/f5ae01b08d0ce91e4af86a042effff79db489d6a) - Correct logic so that org managers can see orgs they manage [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#100](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/100) ]
- **base**: [ee3dd68c](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/ee3dd68cfe78c71a4431e88f6d1f6429dd2af8c0) - add missing content_title to context [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#74](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/74) ]
- **access**: [25efa314](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/25efa31493f71eca5d553d46136d3a261a9d3612) - Enable Organization Manager to view organisations they are assigned to [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#100](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/100) ]
- **api**: [4a6ce353](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/4a6ce353325148f66e3216e61feee7ad96b45cbc) - correct logic for adding inventory UUID and serial number to device [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **ui**: [2d80f026](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/2d80f026341910eecd23560ef970323ac275b112) - navigation alignment and software icon [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **ui**: [abe1ce69](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/abe1ce69480de5c831d5183845987bc9a3264fc3) - display organization manager name instead of ID [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **access**: [86ed7318](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/86ed7318ecd43c15cfccdb344af4ea2ac05c8a87) - ensure name param exists before attempting to access [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **itam**: [90a01911](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/90a01911dacd698d0b7832a05e24eec6fe8310eb) - dont show none/nil for device fields containing no value [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **itam**: [de3ed3a8](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/de3ed3a881bd53e533b9b35f37ce17419c6d75f2) - show device model name instead of ID [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **api**: [f64be2ea](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/f64be2ea33ebf0ae1ba735a7259caf880bcddad5) - Ensure if serial number from inventory is `null` that it's not used [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#78](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/78) ]
- **api**: [ef9c596e](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/ef9c596ec79decec268ffb700e62d5bc35e49019) - ensure checked uuid and serial number is used for updating [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **itam**: [67f20ecb](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/67f20ecb661b039092ad34b491c3a8a7296534db) - only remove device software when not found during inventory upload [ [!38](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/38) [#75](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/75) ]
- **itam**: [3bceb666](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/3bceb66600404919ac32498ffa5cbb4f24fcced4) - only update software version if different [ [!38](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/38) [#75](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/75) ]
- **itam**: [241ba47c](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/241ba47c80805dbd648392ef0b6b26793d3f55ff) - correct device software pagination [ [!36](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/36) [#67](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/67) ]
### Code Refactor
- [367c4beb](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/367c4bebb67c24ffcc2ade19abfeac6089a1e702) - adjust views missing add/change form to now use forms [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#15](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/15) [#46](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/46) [#74](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/74) [#120](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/120) [#121](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/121) [#118](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/118) ]
- [0276f945](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/0276f9454b3999ec147314327b25945a69213250) - add navigation menu expand arrows [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#21](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/21) ]
- [7d172fb4](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/7d172fb4afa284e67231d3d24c7f1bdc533f922a) - migrate views to use new abstract model view classes [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#111](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/111) ]
- [f848d01b](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/f848d01b347af5615613e8bfdb0d9d7324e1daec) - migrate forms to use new abstract model form class [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **access**: [7cfede45](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/7cfede45b89dd5d5ce5b2d2fa8e4ef0c64d31ab3) - Rename Team Button "new user" -> "Assign User" [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#110](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/110) ]
- **access**: [65de9371](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/65de93715d505d5c71b150175e31471b5a07bb8f) - model pk and name not required context for adding a device [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- [fea7ea31](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/fea7ea31198190bf115dc89595b00d7e034aa991) - rename field "model notes" -> "Notes" [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#102](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/102) [#104](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/104) ]
- [f0bbd22c](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/f0bbd22cf441cb3c3f5b01c8108a7a0fd8357938) - remove settings model [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **ui**: [fb907283](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/fb907283b036454aa2afd41bbc658c8feb1a44d8) - increase indentation to sub-menu items [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **itam**: [c1a8ee65](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/c1a8ee65f2bc892c2ca8bfacc5f95094a0130b24) - rename old inventory status icon for use with security [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **api**: [7aeba347](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/7aeba347875cbefb6d5eae112804ae6f0097d264) - migrate inventory processing to background worker [ [!39](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/39) [#76](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/76) ]
- **itam**: [f47b97e2](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/f47b97e2a084e1acbfba733c910f3b8f4f764a36) - only perform actions on device inventory if DB matches inventory item [ [!38](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/38) [#75](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/75) ]
### Continious Integration
- [e25ec12c](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/e25ec12cb02f8f48853806d9cc97959d3e757115) - correct test report path [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- [a235aa7e](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/a235aa7ec37a6dda69bc96ab854df41eacba255b) - add submodule update job [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
### Documentaton / Guides
- **development**: [935e10dc](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/935e10dc24d10c2d99b52a185730d676b4978908) - add initial forms [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **development**: [d4aaea4d](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/d4aaea4dbb250f23850d02d8c5cb9ecbc147a9da) - update views, models and index [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- [329049e8](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/329049e81dd50ca01512f75323669c7953be2199) - roadmap update [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- [c41c7ed1](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/c41c7ed1f09a7a13ac93043bee4e3f3ce0613245) - update mkdocs [ [!41](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/41) ]
- [c9190e9a](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/c9190e9a7dd12725df323811816ea603315331e9) - Update index [ [!41](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/41) ]
- **centurion**: [0294f5ed](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/0294f5ed65868fadd9f27a8a9c22ca861418061b) - replace Django ITSM -> Centurion ERP [ [!41](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/41) ]
- [7329a65a](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/7329a65ae7f30b6e89d0c284609aac2063c76ea6) - update roadmap [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- [9a529a64](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/9a529a64e2e5deaeebec872e3cbe91a6eccebcbe) - add bug count badge [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- [9b79c9d7](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/9b79c9d7ffaacc6ecd9a8d379bc26a303d2951c4) - update readme [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- [9dd2f6a3](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/9dd2f6a341c4e4c78de4e80769552914a4b23bc9) - fix mkdocs navigation [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- [23c640a4](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/23c640a4602948d1b6ec0d3a88473aa26d359997) - add roadmap [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **api**: [27eb54cc](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/27eb54cc3729bcf3ac4b74c84b2def8086685b34) - update swagger docs with inventory changes [ [!39](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/39) [#76](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/76) ]
- **administration**: [a8e2c687](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/a8e2c687b11186f922abd57bb46e98de9f9bf985) - notate rabbitMQ setup [ [!39](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/39) [#76](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/76) ]
### Features
- **core**: [4c42f776](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/4c42f776924dc2f8c52c237b9facd35f10e85e28) - Filter every form field if associated with an organization to users organizations only [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#119](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/119) ]
- **core**: [1cf15f73](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/1cf15f7339a50d9aa1a7940feb84a3128a573ea6) - add var `template_name` to common view template for all views that require it [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **core**: [c057ffdc](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/c057ffdc9c57b399206579f13c51bf4d28120e88) - add Display view to common forms abstract class [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **navigation**: [6837c383](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/6837c383034b962973248cbdd6ae6a5a8a758a41) - always show every menu for super admin [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **core**: [45cc3428](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/45cc34284a7b89aa3d61e61cfa3a12c73165127b) - only display navigation menu item if use can view model [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#114](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/114) ]
- **django**: [f2640df0](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/f2640df0d3737a5fc7a416cd692c37690786e7d1) - update 5.0.6 -> 5.0.7 [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **core**: [44f20b28](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/44f20b28be8c54978b53f00fac9664a5c403ed50) - add common forms abstract class [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **core**: [2e22a484](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/2e22a484a0f4416f41dffb49c68db703c177fe0d) - add common views abstract class [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- [332810ff](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/332810ffd6cf6024a0a917024eafed21ec8d2139) - add postgreSQL database support [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **ui**: [cb66b930](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/cb66b9303aa752ee131e3fd6edb9d670e37c3b0e) - add config groups navigation icon [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **ui**: [a2a8e120](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/a2a8e1204649a27481862e85f8292a229e85fa97) - add some navigation icons [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#21](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/21) [#22](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/22) [#23](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/23) ]
- **itam**: [6a14f78b](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/6a14f78bf7fcda8509d1b0c6b477711b4da59180) - update inventory status icon [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ]
- **itam**: [656807e4](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/656807e410c4dcc0d049cc61ce67c07114313925) - ensure device software pagination links keep interface on software tab [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#81](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/81) ]
- **access**: [b42bb3a3](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/b42bb3a30e613ed9701c95c6ad10fa1890d17dac) - enable non-organization django permission checks [ [!39](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/39) [#76](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/76) ]
- **settings**: [090c4a54](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/090c4a542544cb61356bc00ce2258463d5647f67) - Add celery task results index and view page [ [!39](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/39) [#76](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/76) ]
- **base**: [87a1f2aa](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/87a1f2aa20fdcbbff1863a751a0c5e7d91b269bb) - Add background worker [ [!39](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/39) [#76](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/76) ]
- **itam**: [7b4ed7b1](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/7b4ed7b13537de064680f3c19a703b37c9d2bb83) - Update Serial Number from inventory if present and Serial Number not set [ [!37](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/37) ]
- **itam**: [b801c9a4](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/b801c9a49e70ca5640c65bafdd05c29daed40798) - Update UUID from inventory if present and UUID not set [ [!37](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/37) [#66](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/66) ]
## 0.6.0 (2024-06-30)
### Bug Fixes

View File

@ -15,7 +15,7 @@ pip install -r requirements.txt
```
To setup the django test server run the following
To setup the centurion erp test server run the following
``` bash
@ -50,9 +50,9 @@ See [Documentation](https://nofusscomputing.com/projects/django-template/develop
cd app
docker build . --tag django-app:dev
docker build . --tag centurion-erp:dev
docker run -d --rm -v ${PWD}/db.sqlite3:/app/db.sqlite3 -p 8002:8000 --name app django-app:dev
docker run -d --rm -v ${PWD}/db.sqlite3:/app/db.sqlite3 -p 8002:8000 --name app centurion-erp:dev
```

View File

@ -1,21 +1,65 @@
<span style="text-align: center;">
![GitLab Bugs](https://img.shields.io/gitlab/issues/open/nofusscomputing%2Fprojects%2Fdjango_template?labels=type%3A%3Abug&style=plastic&logo=gitlab&label=Bug%20Fixes%20Required&color=fc6d26)
# No Fuss Computing - Centurion ERP
<br>
![Project Status - Active](https://img.shields.io/badge/Project%20Status-Active-green?logo=gitlab&style=plastic)
![GitLab Issues](https://img.shields.io/gitlab/issues/open/nofusscomputing%2Fprojects%2Fdjango_template?style=plastic&logo=gitlab&label=Issues&color=fc6d26)
![Docker Pulls](https://img.shields.io/docker/pulls/nofusscomputing/django-template?style=plastic&logo=docker&color=0db7ed)
![Gitlab Code Coverage](https://img.shields.io/gitlab/pipeline-coverage/nofusscomputing%2Fprojects%2Fdjango_template?branch=master&style=plastic&logo=gitlab&label=Test%20Coverage)
[![Docker Pulls](https://img.shields.io/docker/pulls/nofusscomputing/centurion-erp?style=plastic&logo=docker&color=0db7ed)](https://hub.docker.com/r/nofusscomputing/centurion-erp)
artifacts
----
<br>
![Gitlab forks count](https://img.shields.io/badge/dynamic/json?label=Forks&query=%24.forks_count&url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F57560288%2F&color=ff782e&logo=gitlab&style=plastic) ![Gitlab stars](https://img.shields.io/badge/dynamic/json?label=Stars&query=%24.star_count&url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F57560288%2F&color=ff782e&logo=gitlab&style=plastic) [![Open Issues](https://img.shields.io/badge/dynamic/json?color=ff782e&logo=gitlab&style=plastic&label=Open%20Issues&query=%24.statistics.counts.opened&url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F57560288%2Fissues_statistics)](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues) [![GitLab Bugs](https://img.shields.io/gitlab/issues/open/nofusscomputing%2Fprojects%2Fcenturion_erp?labels=type%3A%3Abug&style=plastic&logo=gitlab&label=Bug%20Fixes%20Required&color=fc6d26)](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/?sort=created_date&state=opened&label_name%5B%5D=type%3A%3Abug)
dont work to file
https://gitlab.com/nofusscomputing/projects/django_template/-/jobs/artifacts/master/browse/artifacts/coverage/index.html?job=Unit
works to dir
https://gitlab.com/nofusscomputing/projects/django_template/-/jobs/artifacts/master/browse/artifacts/coverage/?job=Unit
![GitHub forks](https://img.shields.io/github/forks/NofussComputing/centurion_erp?logo=github&style=plastic&color=000000&labell=Forks) ![GitHub stars](https://img.shields.io/github/stars/NofussComputing/centurion_erp?color=000000&logo=github&style=plastic) ![Github Watchers](https://img.shields.io/github/watchers/NofussComputing/centurion_erp?color=000000&label=Watchers&logo=github&style=plastic)
<br>
This project is hosted on [gitlab](https://gitlab.com/nofusscomputing/projects/centurion_erp) and has a read-only copy hosted on [Github](https://github.com/NofussComputing/centurion_erp).
----
**Stable Branch**
![Gitlab build status - stable](https://img.shields.io/badge/dynamic/json?color=ff782e&label=Build&query=0.status&url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F57560288%2Fpipelines%3Fref%3Dmaster&logo=gitlab&style=plastic) ![branch release version](https://img.shields.io/badge/dynamic/yaml?color=ff782e&logo=gitlab&style=plastic&label=Release&query=%24.commitizen.version&url=https%3A//gitlab.com/nofusscomputing/projects/centurion_erp%2F-%2Fraw%2Fmaster%2F.cz.yaml) [![Gitlab Code Coverage](https://img.shields.io/gitlab/pipeline-coverage/nofusscomputing%2Fprojects%2Fcenturion_erp?branch=master&style=plastic&logo=gitlab&label=Test%20Coverage)](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/jobs/artifacts/master/browse/artifacts/coverage/?job=Unit)
----
**Development Branch**
![Gitlab build status - development](https://img.shields.io/badge/dynamic/json?color=ff782e&label=Build&query=0.status&url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F57560288%2Fpipelines%3Fref%3Ddevelopment&logo=gitlab&style=plastic) ![branch release version](https://img.shields.io/badge/dynamic/yaml?color=ff782e&logo=gitlab&style=plastic&label=Release&query=%24.commitizen.version&url=https%3A//gitlab.com/nofusscomputing/projects/centurion_erp%2F-%2Fraw%2Fdevelopment%2F.cz.yaml) [![Gitlab Code Coverage](https://img.shields.io/gitlab/pipeline-coverage/nofusscomputing%2Fprojects%2Fcenturion_erp?branch=development&style=plastic&logo=gitlab&label=Test%20Coverage)](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/jobs/artifacts/development/browse/artifacts/coverage/?job=Unit)
----
<br>
</div>
links:
- [Issues](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues)
- [Merge Requests (Pull Requests)](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests)
An ERP with a large emphasis on the IT Service Management (ITSM) and Automation.
## Contributing
All contributions for this project must conducted from [Gitlab](https://gitlab.com/nofusscomputing/projects/centurion_erp).
For further details on contributing please refer to the [contribution guide](CONTRIBUTING.md).
## Other
This repo is release under this [license](LICENSE)

7
Release-Notes.md Normal file
View File

@ -0,0 +1,7 @@
# Version 1.0.0
Initial Release of Centurion ERP.
## Breaking changes
- Nil

View File

@ -5,8 +5,9 @@ from app import settings
from access.models import Organization
from core.forms.common import CommonModelForm
class OrganizationForm(forms.ModelForm):
class OrganizationForm(CommonModelForm):
class Meta:
model = Organization

View File

@ -6,8 +6,10 @@ from django.forms import inlineformset_factory
from app import settings
from .team_users import TeamUsersForm, TeamUsers
from access.models import Team
from core.forms.common import CommonModelForm
TeamUserFormSet = inlineformset_factory(
model=TeamUsers,
@ -19,12 +21,25 @@ TeamUserFormSet = inlineformset_factory(
]
)
class TeamForm(forms.ModelForm):
class TeamFormAdd(CommonModelForm):
class Meta:
model = Team
fields = [
'name',
'team_name',
'model_notes',
]
class TeamForm(CommonModelForm):
class Meta:
model = Team
fields = [
'team_name',
'permissions',
'model_notes',
]
@ -55,12 +70,15 @@ class TeamForm(forms.ModelForm):
'access',
'config_management',
'core',
'django_celery_results',
'itam',
'settings',
]
exclude_models = [
'appsettings',
'chordcounter',
'groupresult',
'organization'
'settings',
'usersettings',
@ -68,8 +86,11 @@ class TeamForm(forms.ModelForm):
exclude_permissions = [
'add_organization',
'add_taskresult',
'change_organization',
'change_taskresult',
'delete_organization',
'delete_taskresult',
]
self.fields['permissions'].queryset = Permission.objects.filter(

View File

@ -1,12 +1,12 @@
from django import forms
from django.db.models import Q
from app import settings
from access.models import TeamUsers
from core.forms.common import CommonModelForm
class TeamUsersForm(forms.ModelForm):
class TeamUsersForm(CommonModelForm):
class Meta:
model = TeamUsers

View File

@ -1,6 +1,7 @@
# Generated by Django 5.0.4 on 2024-05-13 16:08
# Generated by Django 5.0.7 on 2024-07-12 03:54
import access.fields
import access.models
import django.contrib.auth.models
import django.db.models.deletion
import django.utils.timezone
@ -23,9 +24,11 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=50, unique=True)),
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
('slug', access.fields.AutoSlugField()),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('manager', models.ForeignKey(help_text='Organization Manager', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name_plural': 'Organizations',
@ -37,10 +40,11 @@ class Migration(migrations.Migration):
fields=[
('group_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='auth.group')),
('is_global', models.BooleanField(default=False)),
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
('team_name', models.CharField(default='', max_length=50, verbose_name='Name')),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='access.organization')),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
],
options={
'verbose_name_plural': 'Teams',

View File

@ -1,19 +0,0 @@
# Generated by Django 5.0.6 on 2024-05-23 10:37
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='team',
name='organization',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization'),
),
]

View File

@ -1,20 +0,0 @@
# Generated by Django 5.0.6 on 2024-06-05 09:16
import access.models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0002_alter_team_organization'),
]
operations = [
migrations.AlterField(
model_name='team',
name='organization',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.0.6 on 2024-06-11 20:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0003_alter_team_organization'),
]
operations = [
migrations.AddField(
model_name='team',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True),
),
]

View File

@ -1,26 +0,0 @@
# Generated by Django 5.0.6 on 2024-06-17 10:03
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0004_team_model_notes'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='organization',
name='manager',
field=models.ForeignKey(help_text='Organization Manager', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='organization',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True),
),
]

View File

@ -4,8 +4,6 @@ from django.contrib.auth.models import Group
from django.core.exceptions import PermissionDenied
from django.utils.functional import cached_property
from .models import Organization, Team
@ -57,9 +55,11 @@ class OrganizationMixin():
id = obj.get_organization().id
if obj.is_global:
if hasattr(obj, 'is_global'):
id = 0
if obj.is_global:
id = 0
except AttributeError:
@ -70,6 +70,20 @@ class OrganizationMixin():
id = int(self.request.POST.get("organization", ""))
for field in self.request.POST.dict(): # cater for fields prefixed '<prefix>-<field name>'
a_field = str(field).split('-')
if len(a_field) == 2:
if a_field[1] == 'organization':
id = int(self.request.POST.get(field))
except:
pass
return id
@ -191,8 +205,26 @@ class OrganizationMixin():
is_organization_manager = False
queryset = None
if hasattr(self, 'get_queryset'):
queryset = self.get_queryset()
obj = None
if hasattr(self, 'get_object'):
try:
obj = self.get_object()
except:
pass
if hasattr(self, 'model'):
if self.model._meta.label_lower in organization_manager_models:
@ -203,10 +235,32 @@ class OrganizationMixin():
is_organization_manager = True
if not self.has_organization_permission() and not request.user.is_superuser and not is_organization_manager:
return False
return True
return True
if request.user.is_superuser:
return True
perms = self.get_permission_required()
if self.has_organization_permission():
return True
if self.request.user.has_perms(perms) and len(self.kwargs) == 0 and str(self.request.method).lower() == 'get':
return True
for required_permission in self.permission_required:
if required_permission.replace(
'view_', ''
) == 'access.organization' and len(self.kwargs) == 0:
return True
return False
@ -276,7 +330,33 @@ class OrganizationPermission(AccessMixin, OrganizationMixin):
if len(self.permission_required) > 0:
non_organization_models = [
'TaskResult'
]
if hasattr(self, 'model'):
if hasattr(self.model, '__name__'):
if self.model.__name__ in non_organization_models:
if hasattr(self, 'get_object'):
self.get_object()
perms = self.get_permission_required()
if not self.request.user.has_perms(perms):
return self.handle_no_permission()
return super().dispatch(self.request, *args, **kwargs)
if not self.permission_check(request):
raise PermissionDenied('You are not part of this organization')
raise PermissionDenied('You are not part of this organization')
return super().dispatch(self.request, *args, **kwargs)

View File

@ -49,6 +49,7 @@ class Organization(SaveHistory):
blank = True,
default = None,
null= True,
verbose_name = 'Notes',
)
slug = AutoSlugField()
@ -62,13 +63,103 @@ class Organization(SaveHistory):
return self
class TenancyObject(models.Model):
class TenancyManager(models.Manager):
"""Multi-Tennant Object Manager
This manager specifically caters for the multi-tenancy features of Centurion ERP.
"""
def get_queryset(self):
""" Fetch the data
This function filters the data fetched from the database to that which is from the organizations
the user is a part of.
!!! danger "Requirement"
This method may be overridden however must still be called from the overriding function. i.e. `super().get_queryset()`
## Workflow
This functions workflow is as follows:
- Fetch the user from the request
- Check if the user is authenticated
- Iterate over the users teams
- Store unique organizations from users teams
- return results
Returns:
(queryset): **super user**: return unfiltered data.
(queryset): **not super user**: return data from the stored unique organizations.
"""
request = get_request()
user_organizations: list(str()) = []
if request:
user = request.user._wrapped if hasattr(request.user,'_wrapped') else request.user
if user.is_authenticated:
for team_user in TeamUsers.objects.filter(user=user):
if team_user.team.organization.name not in user_organizations:
if not user_organizations:
self.user_organizations = []
user_organizations += [ team_user.team.organization.id ]
if len(user_organizations) > 0 and not user.is_superuser:
return super().get_queryset().filter(
models.Q(organization__in=user_organizations)
|
models.Q(is_global = True)
)
return super().get_queryset()
class TenancyObject(SaveHistory):
""" Tenancy Model Abstrct class.
This class is for inclusion wihtin **every** model within Centurion ERP.
Provides the required fields, functions and methods for multi tennant objects.
Unless otherwise stated, **no** object within this class may be overridden.
Raises:
ValidationError: User failed to supply organization
"""
objects = TenancyManager()
""" Multi-Tenanant Objects """
class Meta:
abstract = True
def validatate_organization_exists(self):
"""Ensure that the user did provide an organization
Raises:
ValidationError: User failed to supply organization.
"""
if not self:
raise ValidationError('You must provide an organization')
@ -91,13 +182,15 @@ class TenancyObject(models.Model):
blank = True,
default = None,
null= True,
verbose_name = 'Notes',
)
def get_organization(self) -> Organization:
return self.organization
class Team(Group, TenancyObject, SaveHistory):
class Team(Group, TenancyObject):
class Meta:
# proxy = True
verbose_name_plural = "Teams"

View File

@ -1,6 +1,5 @@
{% extends 'base.html.j2' %}
{% block title %}Organizations{% endblock %}
{% block content_header_icon %}{% endblock %}
{% block content %}

View File

@ -57,7 +57,7 @@ form div .helptext {
<div class="detail-view-field">
<label>{{ form.manager.label }}</label>
<span>{{ form.manager.value }}</span>
<span>{{ organization.manager }}</span>
</div>
<div class="detail-view-field">

View File

@ -8,7 +8,6 @@
{{ form.as_div }}
{% include 'icons/issue_link.html.j2' with issue=13 %}<br>
<input style="display:unset;" type="submit" value="Submit">
</form>
@ -18,7 +17,7 @@
<input type="button" value="<< Back" onclick="window.location='{% url 'Access:_organization_view' pk=organization.id %}';">
<input type="button" value="Delete Team"
onclick="window.location='{% url 'Access:_team_delete' organization_id=organization.id pk=team.id %}';">
<input type="button" value="New User"
<input type="button" value="Assign User"
onclick="window.location='{% url 'Access:_team_user_add' organization_id=organization.id pk=team.id %}';">
{{ formset.management_form }}

View File

@ -1,6 +1,8 @@
import pytest
import unittest
from access.models import TenancyManager
class TenancyObject:
@ -66,3 +68,21 @@ class TenancyObject:
Must not be able to edit an item without an organization
"""
pass
def test_has_attr_organization(self):
""" TenancyObject attribute check
TenancyObject has function objects
"""
assert hasattr(self.model, 'objects')
def test_attribute_is_type_objects(self):
""" Attribute Check
attribute `objects` must be set to `access.models.TenancyManager()`
"""
assert type(self.model.objects) is TenancyManager

View File

@ -5,7 +5,7 @@ from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import TestCase
from django.test import Client, TestCase
from access.models import Organization, Team, TeamUsers, Permission
@ -24,7 +24,7 @@ class OrganizationPermissionsAPI(TestCase, APIPermissionChange, APIPermissionVie
url_name = '_api_organization'
url_list = 'device-list'
url_list = '_api_orgs'
change_data = {'name': 'device'}
@ -124,6 +124,8 @@ class OrganizationPermissionsAPI(TestCase, APIPermissionChange, APIPermissionVie
delete_team.permissions.set([delete_permissions])
self.super_user = User.objects.create_user(username="super_user", password="password", is_superuser=True)
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
@ -171,3 +173,67 @@ class OrganizationPermissionsAPI(TestCase, APIPermissionChange, APIPermissionVie
team = different_organization_team,
user = self.different_organization_user
)
def test_add_is_prohibited_anon_user(self):
""" Ensure Organization cant be created
Attempt to create organization as anon user
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_list)
# client.force_login(self.add_user)
response = client.post(url, data={'name': 'should not create'}, content_type='application/json')
assert response.status_code == 401
def test_add_is_prohibited_diff_org_user(self):
""" Ensure Organization cant be created
Attempt to create organization as user with different org permissions.
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_list)
client.force_login(self.different_organization_user)
response = client.post(url, data={'name': 'should not create'}, content_type='application/json')
assert response.status_code == 405
def test_add_is_prohibited_super_user(self):
""" Ensure Organization cant be created
Attempt to create organization as user who is super user
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_list)
client.force_login(self.super_user)
response = client.post(url, data={'name': 'should not create'}, content_type='application/json')
assert response.status_code == 405
def test_add_is_prohibited_user_same_org(self):
""" Ensure Organization cant be created
Attempt to create organization as user with permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_list)
client.force_login(self.add_user)
response = client.post(url, data={'name': 'should not create'}, content_type='application/json')
assert response.status_code == 405

View File

@ -0,0 +1,21 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import ModelDisplay, ModelIndex
class OrganizationViews(
TestCase,
ModelDisplay,
ModelIndex
):
display_module = 'access.views.organization'
display_view = 'View'
index_module = display_module
index_view = 'IndexView'

View File

@ -5,9 +5,14 @@ from django.test import TestCase, Client
from access.models import Organization, Team, TeamUsers, Permission
from app.tests.abstract.models import TenancyModel
class TeamModel(TestCase):
class TeamModel(
TestCase,
TenancyModel
):
model = Team
@ -53,4 +58,13 @@ class TeamModel(TestCase):
the save method is overridden. the function attributes must match default django method
"""
pass
@pytest.mark.skip(reason="uses Django group manager")
def test_attribute_is_type_objects(self):
pass
@pytest.mark.skip(reason="uses Django group manager")
def test_model_class_tenancy_manager_function_get_queryset_called(self):
pass

View File

@ -1,17 +0,0 @@
import pytest
import unittest
import requests
from django.test import TestCase, Client
from access.models import Team
from access.tests.abstract.tenancy_object import TenancyObject
class TeamTenancyObject(
TestCase,
TenancyObject
):
model = Team

View File

@ -0,0 +1,29 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import ModelAdd, ModelDelete, ModelDisplay
class TeamViews(
TestCase,
ModelAdd,
ModelDelete,
ModelDisplay,
):
add_module = 'access.views.team'
add_view = 'Add'
# change_module = add_module
# change_view = 'Change'
delete_module = add_module
delete_view = 'Delete'
display_module = add_module
display_view = 'View'

View File

@ -0,0 +1,30 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import AddView, DeleteView
class TeamUserViews(
TestCase,
AddView,
DeleteView
):
add_module = 'access.views.user'
add_view = 'Add'
# change_module = add_module
# change_view = 'GroupView'
delete_module = add_module
delete_view = 'Delete'
# display_module = add_module
# display_view = 'GroupView'
# index_module = add_module
# index_view = 'GroupIndexView'

View File

@ -3,17 +3,45 @@ import unittest
from django.test import TestCase
from access.models import TenancyObject
from access.models import TenancyObject, TenancyManager
from core.mixin.history_save import SaveHistory
from unittest.mock import patch
class TenancyObject(TestCase):
class TenancyManagerTests(TestCase):
item = TenancyManager
def test_has_attribute_get_queryset(self):
""" Field organization exists """
assert hasattr(self.item, 'get_queryset')
def test_is_function_get_queryset(self):
""" Attribute 'get_organization' is a function """
assert callable(self.item.get_queryset)
class TenancyObjectTests(TestCase):
item = TenancyObject
def test_class_inherits_save_history(self):
""" Confirm class inheritence
TenancyObject must inherit SaveHistory
"""
assert issubclass(TenancyObject, SaveHistory)
def test_has_attribute_organization(self):
""" Field organization exists """
@ -43,3 +71,23 @@ class TenancyObject(TestCase):
""" Attribute 'get_organization' is a function """
assert callable(self.item.get_organization)
@pytest.mark.skip(reason="figure out how to test abstract class")
def test_has_attribute_objects(self):
""" Attribute Check
attribute `objects` must be set to `access.models.TenancyManager()`
"""
assert 'objects' in self.item
@pytest.mark.skip(reason="figure out how to test abstract class")
def test_attribute_not_none_objects(self):
""" Attribute Check
attribute `objects` must be set to `access.models.TenancyManager()`
"""
assert self.item.objects is not None

View File

@ -1,4 +1,5 @@
from django.contrib.auth import decorators as auth_decorator
from django.db.models import Q
from django.utils.decorators import method_decorator
from django.views import generic
@ -7,9 +8,12 @@ from access.models import *
from access.forms.organization import OrganizationForm
from core.views.common import ChangeView, IndexView
class IndexView(OrganizationPermission, generic.ListView):
class IndexView(IndexView):
model = Organization
permission_required = [
'access.view_organization'
]
@ -17,6 +21,14 @@ class IndexView(OrganizationPermission, generic.ListView):
context_object_name = "organization_list"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Organizations'
return context
def get_queryset(self):
if self.request.user.is_superuser:
@ -25,11 +37,15 @@ class IndexView(OrganizationPermission, generic.ListView):
else:
return Organization.objects.filter(pk__in=self.user_organizations())
return Organization.objects.filter(
Q(pk__in=self.user_organizations())
|
Q(manager=self.request.user.id)
)
class View(OrganizationPermission, generic.UpdateView):
class View(ChangeView):
context_object_name = "organization"
@ -70,6 +86,8 @@ class View(OrganizationPermission, generic.UpdateView):
context['model_pk'] = self.kwargs['pk']
context['model_name'] = self.model._meta.verbose_name.replace(' ', '')
context['content_title'] = 'Organization - ' + context[self.context_object_name].name
return context

View File

@ -2,16 +2,15 @@ from django.contrib.auth import decorators as auth_decorator
from django.contrib.auth.models import Permission
from django.utils.decorators import method_decorator
from django.urls import reverse
from django.views import generic
from access.forms.team import TeamForm
# from access.forms.team_users import TeamUsersForm
from access.forms.team import TeamForm, TeamFormAdd
from access.models import Team, TeamUsers, Organization
from access.mixin import *
from core.views.common import AddView, ChangeView, DeleteView
class View(OrganizationPermission, generic.UpdateView):
class View(ChangeView):
context_object_name = "team"
@ -79,15 +78,19 @@ class View(OrganizationPermission, generic.UpdateView):
class Add(OrganizationPermission, generic.CreateView):
class Add(AddView):
form_class = TeamFormAdd
model = Team
parent_model = Organization
permission_required = [
'access.add_team',
]
template_name = 'form.html.j2'
fields = [
'team_name',
]
def form_valid(self, form):
form.instance.organization = Organization.objects.get(pk=self.kwargs['pk'])
@ -101,8 +104,6 @@ class Add(OrganizationPermission, generic.CreateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['model_pk'] = self.kwargs['pk']
context['model_name'] = self.model._meta.verbose_name.replace(' ', '')
context['content_title'] = 'Add Team'
@ -110,7 +111,7 @@ class Add(OrganizationPermission, generic.CreateView):
class Delete(OrganizationPermission, generic.DeleteView):
class Delete(DeleteView):
model = Team
permission_required = [
'access.delete_team'

View File

@ -1,15 +1,13 @@
from django.contrib.auth import decorators as auth_decorator
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import generic
from access.forms.team_users import TeamUsersForm
from access.mixin import OrganizationPermission
from access.models import Team, TeamUsers
from core.views.common import AddView, DeleteView
class Add(OrganizationPermission, generic.CreateView):
class Add(AddView):
context_object_name = "teamuser"
@ -17,10 +15,9 @@ class Add(OrganizationPermission, generic.CreateView):
model = TeamUsers
parent_model = TeamUsers
parent_model = Team
permission_required = [
'access.view_team',
'access.add_teamusers'
]
@ -52,7 +49,7 @@ class Add(OrganizationPermission, generic.CreateView):
return context
class Delete(OrganizationPermission, generic.DeleteView):
class Delete(DeleteView):
model = TeamUsers
permission_required = [
'access.delete_teamusers'

View File

@ -5,9 +5,10 @@ from api.models.tokens import AuthToken
from app import settings
from core.forms.common import CommonModelForm
class AuthTokenForm(forms.ModelForm):
class AuthTokenForm(CommonModelForm):
prefix = 'user_token'

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.6 on 2024-06-27 18:25
# Generated by Django 5.0.7 on 2024-07-12 03:54
import access.fields
import django.db.models.deletion

View File

@ -14,9 +14,8 @@ class TeamSerializerBase(serializers.ModelSerializer):
class Meta:
model = Team
fields = (
"id",
"team_name",
'organization',
'team_name',
'permissions',
'url',
)
@ -29,9 +28,18 @@ class TeamSerializerBase(serializers.ModelSerializer):
class TeamPermissionSerializer(serializers.ModelSerializer):
class Meta:
model = Permission
depth = 1
fields = '__all__'
class TeamSerializer(TeamSerializerBase):
permissions = serializers.SerializerMethodField('get_url')
permissions_url = serializers.SerializerMethodField('get_url')
def get_url(self, obj):
@ -63,16 +71,18 @@ class TeamSerializer(TeamSerializerBase):
class Meta:
model = Team
depth = 1
depth = 2
fields = (
"id",
"team_name",
'organization',
'permissions',
'permissions_url',
'url',
)
read_only_fields = [
'permissions',
'id',
'organization',
'permissions_url',
'url'
]
@ -111,7 +121,7 @@ class OrganizationSerializer(serializers.ModelSerializer):
return request.build_absolute_uri(reverse('API:_api_organization_teams', args=[obj.id]))
teams = TeamSerializerBase(source='team_set', many=True, read_only=False)
teams = TeamSerializer(source='team_set', many=True, read_only=False)
view_name="API:_api_organization"

View File

@ -0,0 +1,86 @@
from rest_framework import serializers
from rest_framework.reverse import reverse
from config_management.models.groups import ConfigGroups
class ParentGroupSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField('get_url')
class Meta:
model = ConfigGroups
fields = [
'id',
'name',
'url',
]
read_only_fields = [
'id',
'name',
'url',
]
def get_url(self, obj):
request = self.context.get('request')
return request.build_absolute_uri(reverse("API:_api_config_group", args=[obj.pk]))
class ConfigGroupsSerializerBase(serializers.ModelSerializer):
parent = ParentGroupSerializer(read_only=True)
url = serializers.SerializerMethodField('get_url')
class Meta:
model = ConfigGroups
fields = [
'id',
'parent',
'name',
'config',
'url',
]
read_only_fields = [
'id',
'name',
'config',
'url',
]
def get_url(self, obj):
request = self.context.get('request')
return request.build_absolute_uri(reverse("API:_api_config_group", args=[obj.pk]))
class ConfigGroupsSerializer(ConfigGroupsSerializerBase):
class Meta:
model = ConfigGroups
depth = 1
fields = [
'id',
'parent',
'name',
'config',
'url',
]
read_only_fields = [
'id',
'parent',
'name',
'config',
'url',
]

View File

@ -1,9 +1,38 @@
from django.urls import reverse
from itam.models.device import Device
from rest_framework import serializers
from api.serializers.config import ParentGroupSerializer
from config_management.models.groups import ConfigGroupHosts
from itam.models.device import Device
class DeviceConfigGroupsSerializer(serializers.ModelSerializer):
name = serializers.CharField(source='group.name', read_only=True)
url = serializers.HyperlinkedIdentityField(
view_name="API:_api_config_group", format="html"
)
class Meta:
model = ConfigGroupHosts
fields = [
'id',
'name',
'url',
]
read_only_fields = [
'id',
'name',
'url',
]
class DeviceSerializer(serializers.ModelSerializer):
@ -13,7 +42,9 @@ class DeviceSerializer(serializers.ModelSerializer):
)
config = serializers.SerializerMethodField('get_device_config')
groups = DeviceConfigGroupsSerializer(source='configgrouphosts_set', many=True, read_only=True)
def get_device_config(self, device):
request = self.context.get('request')
@ -22,11 +53,29 @@ class DeviceSerializer(serializers.ModelSerializer):
class Meta:
model = Device
fields = '__all__'
read_only_fields = [
'inventorydate',
depth = 1
fields = [
'id',
'is_global',
'slug',
'name',
'config',
'serial_number',
'uuid',
'inventorydate',
'created',
'modified',
'groups',
'organization',
'url',
]
read_only_fields = [
'id',
'config',
'inventorydate',
'created',
'modified',
'groups',
'url',
]

446
app/api/tasks.py Normal file
View File

@ -0,0 +1,446 @@
import json
import re
from django.utils import timezone
from celery import shared_task, current_task
from celery.utils.log import get_task_logger
from celery import states
from access.models import Organization
from api.serializers.inventory import Inventory
from itam.models.device import Device, DeviceType, DeviceOperatingSystem, DeviceSoftware
from itam.models.operating_system import OperatingSystem, OperatingSystemVersion
from itam.models.software import Software, SoftwareCategory, SoftwareVersion
from settings.models.app_settings import AppSettings
logger = get_task_logger(__name__)
@shared_task(bind=True)
def process_inventory(self, data, organization: int):
device = None
device_operating_system = None
operating_system = None
operating_system_version = None
try:
logger.info('Begin Processing Inventory')
data = json.loads(data)
data = Inventory(data)
organization = Organization.objects.get(id=organization)
app_settings = AppSettings.objects.get(owner_organization = None)
device_serial_number = None
device_uuid = None
if data.details.serial_number and str(data.details.serial_number).lower() != 'na':
device_serial_number = str(data.details.serial_number)
if data.details.uuid and str(data.details.uuid).lower() != 'na':
device_uuid = str(data.details.uuid)
if device_serial_number: # Search for device by serial number.
device = Device.objects.filter(
serial_number__iexact=device_serial_number
)
if device.exists():
device = Device.objects.get(
serial_number__iexact=device_serial_number
)
else:
device = None
if device_uuid and not device: # Search for device by UUID.
device = Device.objects.filter(
uuid__iexact=device_uuid
)
if device.exists():
device = Device.objects.get(
uuid__iexact=device_uuid
)
else:
device = None
if not device: # Search for device by Name.
device = Device.objects.filter(
name__iexact=str(data.details.name).lower()
)
if device.exists():
device = Device.objects.get(
name__iexact=str(data.details.name).lower()
)
else:
device = None
if not device: # Create the device
device = Device.objects.create(
name = data.details.name,
device_type = None,
serial_number = device_serial_number,
uuid = device_uuid,
organization = organization,
)
if device:
logger.info(f"Device: {device.name}, Serial: {device.serial_number}, UUID: {device.uuid}")
device_edited = False
if not device.uuid and device_uuid:
device.uuid = device_uuid
device_edited = True
if not device.serial_number and device_serial_number:
device.serial_number = data.details.serial_number
device_edited = True
if str(device.name).lower() != str(data.details.name).lower(): # Update device Name
device.name = data.details.name
device_edited = True
if device_edited:
device.save()
operating_system = OperatingSystem.objects.filter(
name=data.operating_system.name,
is_global = True
)
if operating_system.exists():
operating_system = OperatingSystem.objects.get(
name=data.operating_system.name,
is_global = True
)
else:
operating_system = None
if not operating_system:
operating_system = OperatingSystem.objects.filter(
name=data.operating_system.name,
organization = organization
)
if operating_system.exists():
operating_system = OperatingSystem.objects.get(
name=data.operating_system.name,
organization = organization
)
else:
operating_system = None
if not operating_system:
operating_system = OperatingSystem.objects.create(
name = data.operating_system.name,
organization = organization,
is_global = True
)
operating_system_version = OperatingSystemVersion.objects.filter(
name=data.operating_system.version_major,
operating_system=operating_system
)
if operating_system_version.exists():
operating_system_version = OperatingSystemVersion.objects.get(
name=data.operating_system.version_major,
operating_system=operating_system
)
else:
operating_system_version = None
if not operating_system_version:
operating_system_version = OperatingSystemVersion.objects.create(
organization = organization,
is_global = True,
name = data.operating_system.version_major,
operating_system = operating_system,
)
device_operating_system = DeviceOperatingSystem.objects.filter(
device=device,
)
if device_operating_system.exists():
device_operating_system = DeviceOperatingSystem.objects.get(
device=device,
)
else:
device_operating_system = None
if not device_operating_system:
device_operating_system = DeviceOperatingSystem.objects.create(
organization = organization,
device=device,
version = data.operating_system.version,
operating_system_version = operating_system_version,
installdate = timezone.now()
)
if not device_operating_system.installdate: # Only update install date if empty
device_operating_system.installdate = timezone.now()
device_operating_system.save()
if device_operating_system.operating_system_version != operating_system_version:
device_operating_system.operating_system_version = operating_system_version
device_operating_system.save()
if device_operating_system.version != data.operating_system.version:
device_operating_system.version = data.operating_system.version
device_operating_system.save()
if app_settings.software_is_global:
software_organization = app_settings.global_organization
else:
software_organization = device.organization
if app_settings.software_categories_is_global:
software_category_organization = app_settings.global_organization
else:
software_category_organization = device.organization
inventoried_software: list = []
for inventory in list(data.software):
software = None
software_category = None
software_version = None
device_software = None
software_category = SoftwareCategory.objects.filter( name = inventory.category )
if software_category.exists():
software_category = SoftwareCategory.objects.get(
name = inventory.category
)
else: # Create Software Category
software_category = SoftwareCategory.objects.create(
organization = software_category_organization,
is_global = True,
name = inventory.category,
)
if software_category.name == inventory.category:
if Software.objects.filter( name = inventory.name ).exists():
software = Software.objects.get(
name = inventory.name
)
if not software.category:
software.category = software_category
software.save()
else: # Create Software
software = Software.objects.create(
organization = software_organization,
is_global = True,
name = inventory.name,
category = software_category,
)
if software.name == inventory.name:
pattern = r"^(\d+:)?(?P<semver>\d+\.\d+(\.\d+)?)"
semver = re.search(pattern, str(inventory.version), re.DOTALL)
if semver:
semver = semver['semver']
else:
semver = inventory.version
if SoftwareVersion.objects.filter( name = semver, software = software ).exists():
software_version = SoftwareVersion.objects.get(
name = semver,
software = software,
)
else: # Create Software Category
software_version = SoftwareVersion.objects.create(
organization = organization,
is_global = True,
name = semver,
software = software,
)
if software_version.name == semver:
if DeviceSoftware.objects.filter( software = software, device=device ).exists():
device_software = DeviceSoftware.objects.get(
device = device,
software = software
)
logger.debug(f"Select Existing Device Software: {device_software.software.name}")
else: # Create Software
device_software = DeviceSoftware.objects.create(
organization = organization,
is_global = True,
installedversion = software_version,
software = software,
device = device,
action=None
)
logger.debug(f"Create Device Software: {device_software.software.name}")
if device_software: # Update the Inventoried software
inventoried_software += [ device_software.id ]
if not device_software.installed: # Only update install date if blank
device_software.installed = timezone.now()
device_software.save()
logger.debug(f"Update Device Software (installed): {device_software.software.name}")
if device_software.installedversion.name != software_version.name:
device_software.installedversion = software_version
device_software.save()
logger.debug(f"Update Device Software (installedversion): {device_software.software.name}")
for not_installed in DeviceSoftware.objects.filter( device=device ):
if not_installed.id not in inventoried_software:
not_installed.delete()
logger.debug(f"Remove Device Software: {not_installed.software.name}")
if device and operating_system and operating_system_version and device_operating_system:
device.inventorydate = timezone.now()
device.save()
logger.info('Finish Processing Inventory')
return str('finished...')
except Exception as e:
logger.critical('Exception')
raise Exception(e)
return str(f'Exception Occured: {e}')

View File

@ -1,4 +1,5 @@
import datetime
import json
import pytest
import unittest
@ -6,6 +7,7 @@ from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import TestCase, Client
from django.test.utils import override_settings
from unittest.mock import patch
@ -14,6 +16,8 @@ from access.models import Organization, Team, TeamUsers, Permission
from api.views.mixin import OrganizationPermissionAPI
from api.serializers.inventory import Inventory
from api.tasks import process_inventory
from itam.models.device import Device, DeviceOperatingSystem, DeviceSoftware
from itam.models.operating_system import OperatingSystem, OperatingSystemVersion
from itam.models.software import Software, SoftwareCategory, SoftwareVersion
@ -105,11 +109,7 @@ class InventoryAPI(TestCase):
)
# upload the inventory
client = Client()
url = reverse('API:_api_device_inventory')
client.force_login(self.add_user)
self.response = client.post(url, data=self.inventory, content_type='application/json')
process_inventory(json.dumps(self.inventory), organization.id)
self.device = Device.objects.get(name=self.inventory['details']['name'])
@ -147,6 +147,8 @@ class InventoryAPI(TestCase):
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
@patch.object(OrganizationPermissionAPI, 'permission_check')
def test_inventory_function_called_permission_check(self, permission_check):
""" Inventory Upload checks permissions
@ -167,6 +169,8 @@ class InventoryAPI(TestCase):
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
@patch.object(Inventory, '__init__')
def test_inventory_serializer_inventory_called(self, serializer):
""" Inventory Upload checks permissions
@ -187,6 +191,8 @@ class InventoryAPI(TestCase):
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
@patch.object(Inventory.Details, '__init__')
def test_inventory_serializer_inventory_details_called(self, serializer):
""" Inventory Upload uses Inventory serializer
@ -204,6 +210,8 @@ class InventoryAPI(TestCase):
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
@patch.object(Inventory.OperatingSystem, '__init__')
def test_inventory_serializer_inventory_operating_system_called(self, serializer):
""" Inventory Upload uses Inventory serializer
@ -221,6 +229,8 @@ class InventoryAPI(TestCase):
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
@patch.object(Inventory.Software, '__init__')
def test_inventory_serializer_inventory_software_called(self, serializer):
""" Inventory Upload uses Inventory serializer
@ -245,6 +255,343 @@ class InventoryAPI(TestCase):
def test_api_inventory_device_uuid_match(self):
""" Device uuid match """
assert self.device.uuid == self.inventory['details']['uuid']
def test_api_inventory_device_serial_number_match(self):
""" Device SN match """
assert self.device.serial_number == self.inventory['details']['serial_number']
def test_api_inventory_operating_system_added(self):
""" Operating System is created """
assert self.operating_system.name == self.inventory['os']['name']
def test_api_inventory_operating_system_version_added(self):
""" Operating System version is created """
assert self.operating_system_version.name == self.inventory['os']['version_major']
def test_api_inventory_device_has_operating_system_added(self):
""" Operating System version linked to device """
assert self.device_operating_system.version == self.inventory['os']['version']
@pytest.mark.skip(reason="to be written")
def test_api_inventory_device_operating_system_version_is_semver(self):
""" Operating System version is full semver
Operating system versions name is the major version number of semver.
The device version is to be full semver
"""
pass
@pytest.mark.skip(reason="to be written")
def test_api_inventory_software_no_version_cleaned(self):
""" Check softare cleaned up
As part of the inventory upload the software versions of software found on the device is set to null
and before the processing is completed, the version=null software is supposed to be cleaned up.
"""
pass
def test_api_inventory_software_category_added(self):
""" Software category exists """
assert self.software_category.name == self.inventory['software'][0]['category']
def test_api_inventory_software_added(self):
""" Test software exists """
assert self.software.name == self.inventory['software'][0]['name']
def test_api_inventory_software_category_linked_to_software(self):
""" Software category linked to software """
assert self.software.category == self.software_category
def test_api_inventory_software_version_added(self):
""" Test software version exists """
assert self.software_version.name == self.inventory['software'][0]['version']
def test_api_inventory_software_version_returns_semver(self):
""" Software Version from inventory returns semver if within version string """
assert self.software_version_is_semver.name == str(self.inventory['software'][2]['version']).split('-')[0]
def test_api_inventory_software_version_returns_original_version(self):
""" Software Version from inventory returns inventoried version if no semver found """
assert self.software_version_not_semver.name == self.inventory['software'][1]['version']
def test_api_inventory_software_version_linked_to_software(self):
""" Test software version linked to software it belongs too """
assert self.software_version.software == self.software
def test_api_inventory_device_has_software_version(self):
""" Inventoried software is linked to device and it's the corret one"""
assert self.software_version.name == self.inventory['software'][0]['version']
def test_api_inventory_device_software_has_installed_date(self):
""" Inventoried software version has install date """
assert self.device_software.installed is not None
def test_api_inventory_device_software_installed_date_type(self):
""" Inventoried software version has install date """
assert type(self.device_software.installed) is datetime.datetime
@pytest.mark.skip(reason="to be written")
def test_api_inventory_device_software_blank_installed_date_is_updated(self):
""" A blank installed date of software is updated if the software was already attached to the device """
pass
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
def test_api_inventory_valid_status_ok_existing_device(self):
""" Successful inventory upload returns 200 for existing device"""
client = Client()
url = reverse('API:_api_device_inventory')
client.force_login(self.add_user)
response = client.post(url, data=self.inventory, content_type='application/json')
assert response.status_code == 200
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
def test_api_inventory_invalid_status_bad_request(self):
""" Incorrectly formated inventory upload returns 400 """
client = Client()
url = reverse('API:_api_device_inventory')
mod_inventory = self.inventory.copy()
mod_inventory.update({
'details': {
'name': 'test_api_inventory_invalid_status_bad_request'
},
'software': {
'not_within_a': 'list'
}
})
client.force_login(self.add_user)
response = client.post(url, data=mod_inventory, content_type='application/json')
assert response.status_code == 400
@pytest.mark.skip(reason="to be written")
def test_api_inventory_exeception_status_sever_error(self):
""" if the method throws an exception 500 must be returned.
idea to test: add a random key to the report that is not documented
and perform some action against it that will cause a python exception.
"""
pass
class InventoryAPIDifferentNameSerialNumberMatch(TestCase):
""" Test inventory upload with different name
should match by serial number
"""
model = Device
model_name = 'device'
app_label = 'itam'
inventory = {
"details": {
"name": "device_name",
"serial_number": "serial_number_123",
"uuid": "string"
},
"os": {
"name": "os_name",
"version_major": "12",
"version": "12.1"
},
"software": [
{
"name": "software_name",
"category": "category_name",
"version": "1.2.3"
},
{
"name": "software_name_not_semver",
"category": "category_name",
"version": "2024.4"
},
{
"name": "software_name_semver_contained",
"category": "category_name",
"version": "1.2.3-rc1"
},
]
}
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user
2. Create a team for user with correct permissions
3. add user to the teeam
4. upload the inventory
5. conduct queries for tests
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
Device.objects.create(
name='random device name',
serial_number='serial_number_123'
)
add_permissions = Permission.objects.get(
codename = 'add_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
add_team = Team.objects.create(
team_name = 'add_team',
organization = organization,
)
add_team.permissions.set([add_permissions])
self.add_user = User.objects.create_user(username="test_user_add", password="password")
add_user_settings = UserSettings.objects.get(user=self.add_user)
add_user_settings.default_organization = organization
add_user_settings.save()
teamuser = TeamUsers.objects.create(
team = add_team,
user = self.add_user
)
# upload the inventory
process_inventory(json.dumps(self.inventory), organization.id)
self.device = Device.objects.get(name=self.inventory['details']['name'])
self.operating_system = OperatingSystem.objects.get(name=self.inventory['os']['name'])
self.operating_system_version = OperatingSystemVersion.objects.get(name=self.inventory['os']['version_major'])
self.device_operating_system = DeviceOperatingSystem.objects.get(version=self.inventory['os']['version'])
self.software = Software.objects.get(name=self.inventory['software'][0]['name'])
self.software_category = SoftwareCategory.objects.get(name=self.inventory['software'][0]['category'])
self.software_version = SoftwareVersion.objects.get(
name = self.inventory['software'][0]['version'],
software = self.software,
)
self.software_not_semver = Software.objects.get(name=self.inventory['software'][1]['name'])
self.software_version_not_semver = SoftwareVersion.objects.get(
name = self.inventory['software'][1]['version'],
software = self.software_not_semver
)
self.software_is_semver = Software.objects.get(name=self.inventory['software'][2]['name'])
self.software_version_is_semver = SoftwareVersion.objects.get(
software = self.software_is_semver
)
self.device_software = DeviceSoftware.objects.get(device=self.device,software=self.software)
def test_api_inventory_device_added(self):
""" Device is created """
assert self.device.name == self.inventory['details']['name']
def test_api_inventory_device_uuid_match(self):
""" Device uuid match """
assert self.device.uuid == self.inventory['details']['uuid']
def test_api_inventory_device_serial_number_match(self):
""" Device SN match """
assert self.device.serial_number == self.inventory['details']['serial_number']
def test_api_inventory_operating_system_added(self):
""" Operating System is created """
@ -366,56 +713,277 @@ class InventoryAPI(TestCase):
def test_api_inventory_valid_status_ok_existing_device(self):
""" Successful inventory upload returns 200 for existing device"""
client = Client()
url = reverse('API:_api_device_inventory')
client.force_login(self.add_user)
response = client.post(url, data=self.inventory, content_type='application/json')
assert response.status_code == 200
def test_api_inventory_valid_status_created(self):
""" Successful inventory upload returns 201 """
assert self.response.status_code == 201
class InventoryAPIDifferentNameUUIDMatch(TestCase):
""" Test inventory upload with different name
def test_api_inventory_invalid_status_bad_request(self):
""" Incorrectly formated inventory upload returns 400 """
should match by uuid
"""
client = Client()
url = reverse('API:_api_device_inventory')
model = Device
mod_inventory = self.inventory.copy()
model_name = 'device'
app_label = 'itam'
mod_inventory.update({
'details': {
'name': 'test_api_inventory_invalid_status_bad_request'
inventory = {
"details": {
"name": "device_name",
"serial_number": "serial_number_123",
"uuid": "123-456-789"
},
"os": {
"name": "os_name",
"version_major": "12",
"version": "12.1"
},
"software": [
{
"name": "software_name",
"category": "category_name",
"version": "1.2.3"
},
'software': {
'not_within_a': 'list'
}
})
{
"name": "software_name_not_semver",
"category": "category_name",
"version": "2024.4"
},
{
"name": "software_name_semver_contained",
"category": "category_name",
"version": "1.2.3-rc1"
},
]
}
client.force_login(self.add_user)
response = client.post(url, data=mod_inventory, content_type='application/json')
assert response.status_code == 400
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user
2. Create a team for user with correct permissions
3. add user to the teeam
4. upload the inventory
5. conduct queries for tests
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
Device.objects.create(
name='random device name',
uuid='123-456-789'
)
add_permissions = Permission.objects.get(
codename = 'add_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
add_team = Team.objects.create(
team_name = 'add_team',
organization = organization,
)
add_team.permissions.set([add_permissions])
self.add_user = User.objects.create_user(username="test_user_add", password="password")
add_user_settings = UserSettings.objects.get(user=self.add_user)
add_user_settings.default_organization = organization
add_user_settings.save()
teamuser = TeamUsers.objects.create(
team = add_team,
user = self.add_user
)
# upload the inventory
process_inventory(json.dumps(self.inventory), organization.id)
self.device = Device.objects.get(name=self.inventory['details']['name'])
self.operating_system = OperatingSystem.objects.get(name=self.inventory['os']['name'])
self.operating_system_version = OperatingSystemVersion.objects.get(name=self.inventory['os']['version_major'])
self.device_operating_system = DeviceOperatingSystem.objects.get(version=self.inventory['os']['version'])
self.software = Software.objects.get(name=self.inventory['software'][0]['name'])
self.software_category = SoftwareCategory.objects.get(name=self.inventory['software'][0]['category'])
self.software_version = SoftwareVersion.objects.get(
name = self.inventory['software'][0]['version'],
software = self.software,
)
self.software_not_semver = Software.objects.get(name=self.inventory['software'][1]['name'])
self.software_version_not_semver = SoftwareVersion.objects.get(
name = self.inventory['software'][1]['version'],
software = self.software_not_semver
)
self.software_is_semver = Software.objects.get(name=self.inventory['software'][2]['name'])
self.software_version_is_semver = SoftwareVersion.objects.get(
software = self.software_is_semver
)
self.device_software = DeviceSoftware.objects.get(device=self.device,software=self.software)
def test_api_inventory_device_added(self):
""" Device is created """
assert self.device.name == self.inventory['details']['name']
def test_api_inventory_device_uuid_match(self):
""" Device uuid match """
assert self.device.uuid == self.inventory['details']['uuid']
def test_api_inventory_device_serial_number_match(self):
""" Device SN match """
assert self.device.serial_number == self.inventory['details']['serial_number']
def test_api_inventory_operating_system_added(self):
""" Operating System is created """
assert self.operating_system.name == self.inventory['os']['name']
def test_api_inventory_operating_system_version_added(self):
""" Operating System version is created """
assert self.operating_system_version.name == self.inventory['os']['version_major']
def test_api_inventory_device_has_operating_system_added(self):
""" Operating System version linked to device """
assert self.device_operating_system.version == self.inventory['os']['version']
@pytest.mark.skip(reason="to be written")
def test_api_inventory_exeception_status_sever_error(self):
""" if the method throws an exception 500 must be returned.
def test_api_inventory_device_operating_system_version_is_semver(self):
""" Operating System version is full semver
idea to test: add a random key to the report that is not documented
and perform some action against it that will cause a python exception.
Operating system versions name is the major version number of semver.
The device version is to be full semver
"""
pass
@pytest.mark.skip(reason="to be written")
def test_api_inventory_software_no_version_cleaned(self):
""" Check softare cleaned up
As part of the inventory upload the software versions of software found on the device is set to null
and before the processing is completed, the version=null software is supposed to be cleaned up.
"""
pass
def test_api_inventory_software_category_added(self):
""" Software category exists """
assert self.software_category.name == self.inventory['software'][0]['category']
def test_api_inventory_software_added(self):
""" Test software exists """
assert self.software.name == self.inventory['software'][0]['name']
def test_api_inventory_software_category_linked_to_software(self):
""" Software category linked to software """
assert self.software.category == self.software_category
def test_api_inventory_software_version_added(self):
""" Test software version exists """
assert self.software_version.name == self.inventory['software'][0]['version']
def test_api_inventory_software_version_returns_semver(self):
""" Software Version from inventory returns semver if within version string """
assert self.software_version_is_semver.name == str(self.inventory['software'][2]['version']).split('-')[0]
def test_api_inventory_software_version_returns_original_version(self):
""" Software Version from inventory returns inventoried version if no semver found """
assert self.software_version_not_semver.name == self.inventory['software'][1]['version']
def test_api_inventory_software_version_linked_to_software(self):
""" Test software version linked to software it belongs too """
assert self.software_version.software == self.software
def test_api_inventory_device_has_software_version(self):
""" Inventoried software is linked to device and it's the corret one"""
assert self.software_version.name == self.inventory['software'][0]['version']
def test_api_inventory_device_software_has_installed_date(self):
""" Inventoried software version has install date """
assert self.device_software.installed is not None
def test_api_inventory_device_software_installed_date_type(self):
""" Inventoried software version has install date """
assert type(self.device_software.installed) is datetime.datetime
@pytest.mark.skip(reason="to be written")
def test_api_inventory_device_software_blank_installed_date_is_updated(self):
""" A blank installed date of software is updated if the software was already attached to the device """
pass

View File

@ -1,3 +1,4 @@
import celery
import pytest
import unittest
import requests
@ -7,6 +8,9 @@ from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import TestCase, Client
from django.test.utils import override_settings
from unittest.mock import patch
from access.models import Organization, Team, TeamUsers, Permission
@ -188,6 +192,8 @@ class InventoryPermissionsAPI(TestCase):
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
def test_device_auth_add_user_anon_denied(self):
""" Check correct permission for add
@ -203,6 +209,8 @@ class InventoryPermissionsAPI(TestCase):
assert response.status_code == 401
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
def test_device_auth_add_no_permission_denied(self):
""" Check correct permission for add
@ -219,6 +227,8 @@ class InventoryPermissionsAPI(TestCase):
assert response.status_code == 403
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
def test_device_auth_add_different_organization_denied(self):
""" Check correct permission for add
@ -235,6 +245,8 @@ class InventoryPermissionsAPI(TestCase):
assert response.status_code == 403
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
def test_device_auth_add_permission_view_denied(self):
""" Check correct permission for add
@ -251,6 +263,8 @@ class InventoryPermissionsAPI(TestCase):
assert response.status_code == 403
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
def test_device_auth_add_has_permission(self):
""" Check correct permission for add
@ -264,6 +278,6 @@ class InventoryPermissionsAPI(TestCase):
client.force_login(self.add_user)
response = client.post(url, data=self.inventory, content_type='application/json')
assert response.status_code == 201
assert response.status_code == 200

View File

@ -3,7 +3,7 @@ from django.urls import path
from rest_framework.routers import DefaultRouter
from rest_framework.urlpatterns import format_suffix_patterns
from .views import access, index
from .views import access, config, index
from .views.itam import software, config as itam_config
from .views.itam.device import DeviceViewSet
@ -24,6 +24,9 @@ router.register('software', software.SoftwareViewSet, basename='software')
urlpatterns = [
path("config/<slug:slug>/", itam_config.View.as_view(), name="_api_device_config"),
path("configuration/", config.ConfigGroupsList.as_view(), name='_api_config_groups'),
path("configuration/<int:pk>", config.ConfigGroupsDetail.as_view(), name='_api_config_group'),
path("device/inventory", inventory.Collect.as_view(), name="_api_device_inventory"),
path("organization/", access.OrganizationList.as_view(), name='_api_orgs'),

View File

@ -1,5 +1,7 @@
from django.contrib.auth.models import Permission
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse
from rest_framework import generics, routers, serializers, views
from rest_framework.permissions import DjangoObjectPermissions
from rest_framework.response import Response
@ -7,12 +9,17 @@ from rest_framework.response import Response
from access.mixin import OrganizationMixin
from access.models import Organization, Team
from api.serializers.access import OrganizationSerializer, OrganizationListSerializer, TeamSerializer
from api.serializers.access import OrganizationSerializer, OrganizationListSerializer, TeamSerializer, TeamPermissionSerializer
from api.views.mixin import OrganizationPermissionAPI
class OrganizationList(generics.ListCreateAPIView):
@extend_schema_view(
get=extend_schema(
summary = "Fetch Organizations",
description="Returns a list of organizations."
),
)
class OrganizationList(generics.ListAPIView):
permission_classes = [
OrganizationPermissionAPI
@ -28,7 +35,18 @@ class OrganizationList(generics.ListCreateAPIView):
class OrganizationDetail(generics.RetrieveUpdateDestroyAPIView):
@extend_schema_view(
get=extend_schema(
summary = "Get An Organization",
),
patch=extend_schema(
summary = "Update an organization",
),
put=extend_schema(
summary = "Update an organization",
),
)
class OrganizationDetail(generics.RetrieveUpdateAPIView):
permission_classes = [
OrganizationPermissionAPI
@ -44,6 +62,20 @@ class OrganizationDetail(generics.RetrieveUpdateDestroyAPIView):
@extend_schema_view(
post=extend_schema(
summary = "Create a Team",
description = """Create a team within the defined organization.""",
tags = ['team',],
request = TeamSerializer,
responses = {
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
401: OpenApiResponse(description='User Not logged in'),
403: OpenApiResponse(description='User is missing permission or in different organization'),
}
),
create=extend_schema(exclude=True),
)
class TeamList(generics.ListCreateAPIView):
permission_classes = [
@ -66,6 +98,45 @@ class TeamList(generics.ListCreateAPIView):
@extend_schema_view(
get=extend_schema(
summary = "Fetch a Team",
description = """Fetch a team within the defined organization.
""",
methods=["GET"],
tags = ['team',],
request = TeamSerializer,
responses = {
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
401: OpenApiResponse(description='User Not logged in'),
403: OpenApiResponse(description='User is missing permission or in different organization'),
}
),
patch=extend_schema(
summary = "Update a Team",
description = """Update a team within the defined organization.
""",
methods=["Patch"],
tags = ['team',],
request = TeamSerializer,
responses = {
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
401: OpenApiResponse(description='User Not logged in'),
403: OpenApiResponse(description='User is missing permission or in different organization'),
}
),
put = extend_schema(
summary = "Amend a team",
tags = ['team',],
),
delete=extend_schema(
summary = "Delete a Team",
tags = ['team',],
),
post = extend_schema(
exclude = True,
)
)
class TeamDetail(generics.RetrieveUpdateDestroyAPIView):
permission_classes = [
@ -79,12 +150,66 @@ class TeamDetail(generics.RetrieveUpdateDestroyAPIView):
class TeamPermissionDetail(routers.APIRootView):
@extend_schema_view(
get=extend_schema(
summary = "Fetch a teams permissions",
tags = ['team',],
),
post=extend_schema(
summary = "Replace team Permissions",
description = """Replace the teams permissions with the permissions supplied.
# temp disabled until permission checker updated
# permission_classes = [
# OrganizationPermissionAPI
# ]
Teams Permissions will be replaced with the permissions supplied. **ALL** existing permissions will be
removed.
permissions are required to be in format `<module name>_<permission>_<table name>`
""",
methods=["POST"],
tags = ['team',],
request = TeamPermissionSerializer,
responses = {
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
401: OpenApiResponse(description='User Not logged in'),
403: OpenApiResponse(description='User is missing permission or in different organization'),
}
),
delete=extend_schema(
summary = "Delete permissions",
tags = ['team',],
),
patch = extend_schema(
summary = "Amend team Permissions",
description = """Amend the teams permissions with the permissions supplied.
Teams permissions will include the existing permissions along with the ones supplied.
permissions are required to be in format `<module name>_<permission>_<table name>`
""",
methods=["PATCH"],
parameters = None,
tags = ['team',],
request = TeamPermissionSerializer,
responses = {
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
401: OpenApiResponse(description='User Not logged in'),
403: OpenApiResponse(description='User is missing permission or in different organization'),
}
),
put = extend_schema(
summary = "Amend team Permissions",
tags = ['team',],
)
)
class TeamPermissionDetail(views.APIView):
permission_classes = [
OrganizationPermissionAPI
]
queryset = Team.objects.all()
serializer_class = TeamPermissionSerializer
def get(self, request, *args, **kwargs):

54
app/api/views/config.py Normal file
View File

@ -0,0 +1,54 @@
from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import generics
from api.serializers.config import ConfigGroupsSerializer
from api.views.mixin import OrganizationPermissionAPI
from config_management.models.groups import ConfigGroups
@extend_schema_view(
get=extend_schema(
summary = "Fetch Config groups",
description="Returns a list of Config Groups."
),
)
class ConfigGroupsList(generics.ListAPIView):
permission_classes = [
OrganizationPermissionAPI
]
queryset = ConfigGroups.objects.all()
lookup_field = 'pk'
serializer_class = ConfigGroupsSerializer
def get_view_name(self):
return "Config Groups"
@extend_schema_view(
get=extend_schema(
summary = "Get A Config Group",
# responses = {}
),
)
class ConfigGroupsDetail(generics.RetrieveAPIView):
permission_classes = [
OrganizationPermissionAPI
]
queryset = ConfigGroups.objects.all()
lookup_field = 'pk'
serializer_class = ConfigGroupsSerializer
def get_view_name(self):
return "Config Group"

View File

@ -27,6 +27,7 @@ class Index(viewsets.ViewSet):
{
# "teams": reverse("_api_teams", request=request),
"devices": reverse("API:device-list", request=request),
"config_groups": reverse("API:_api_config_groups", request=request),
"organizations": reverse("API:_api_orgs", request=request),
"software": reverse("API:software-list", request=request),
}

View File

@ -1,32 +1,25 @@
# from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
import json
import re
from django.core.exceptions import ValidationError, PermissionDenied
from django.http import Http404, JsonResponse
from django.utils import timezone
from drf_spectacular.utils import extend_schema, OpenApiExample, OpenApiTypes, OpenApiResponse, OpenApiParameter
from drf_spectacular.utils import extend_schema, OpenApiResponse
from rest_framework import generics, views
from rest_framework.response import Response
from access.mixin import OrganizationMixin
from access.models import Organization
from api.views.mixin import OrganizationPermissionAPI
from api.serializers.itam.inventory import InventorySerializer
from api.serializers.inventory import Inventory
from core.http.common import Http
from itam.models.device import Device, DeviceType, DeviceOperatingSystem, DeviceSoftware
from itam.models.operating_system import OperatingSystem, OperatingSystemVersion
from itam.models.software import Software, SoftwareCategory, SoftwareVersion
from itam.models.device import Device
from settings.models.app_settings import AppSettings
from settings.models.user_settings import UserSettings
from api.tasks import process_inventory
class InventoryPermissions(OrganizationPermissionAPI):
@ -68,9 +61,7 @@ this setting populated, no device will be created and the endpoint will return H
tags = ['device', 'inventory',],
request = InventorySerializer,
responses = {
200: OpenApiResponse(description='Inventory updated an existing device'),
201: OpenApiResponse(description='Inventory created a new device'),
400: OpenApiResponse(description='Inventory is invalid'),
200: OpenApiResponse(description='Inventory upload successful'),
401: OpenApiResponse(description='User Not logged in'),
403: OpenApiResponse(description='User is missing permission or in different organization'),
500: OpenApiResponse(description='Exception occured. View server logs for the Stack Trace'),
@ -102,234 +93,9 @@ this setting populated, no device will be created and the endpoint will return H
raise Http404
device_operating_system = None
operating_system = None
operating_system_version = None
task = process_inventory.delay(request.body, self.default_organization.id)
app_settings = AppSettings.objects.get(owner_organization = None)
if not device: # Create the device
device = Device.objects.create(
name = data.details.name,
device_type = None,
serial_number = data.details.serial_number,
uuid = data.details.uuid,
organization = self.default_organization,
)
status = Http.Status.CREATED
if OperatingSystem.objects.filter( slug=data.operating_system.name ).exists():
operating_system = OperatingSystem.objects.get( slug=data.operating_system.name )
else: # Create Operating System
operating_system = OperatingSystem.objects.create(
name = data.operating_system.name,
organization = self.default_organization,
is_global = True
)
if OperatingSystemVersion.objects.filter( name=data.operating_system.version_major, operating_system=operating_system ).exists():
operating_system_version = OperatingSystemVersion.objects.get(
organization = self.default_organization,
is_global = True,
name = data.operating_system.version_major,
operating_system = operating_system
)
else: # Create Operating System Version
operating_system_version = OperatingSystemVersion.objects.create(
organization = self.default_organization,
is_global = True,
name = data.operating_system.version_major,
operating_system = operating_system,
)
if DeviceOperatingSystem.objects.filter( version=data.operating_system.version, device=device, operating_system_version=operating_system_version ).exists():
device_operating_system = DeviceOperatingSystem.objects.get(
device=device,
version = data.operating_system.version,
operating_system_version = operating_system_version,
)
if not device_operating_system.installdate: # Only update install date if empty
device_operating_system.installdate = timezone.now()
device_operating_system.save()
else: # Create Operating System Version
device_operating_system = DeviceOperatingSystem.objects.create(
organization = self.default_organization,
device=device,
version = data.operating_system.version,
operating_system_version = operating_system_version,
installdate = timezone.now()
)
if app_settings.software_is_global:
software_organization = app_settings.global_organization
else:
software_organization = device.organization
if app_settings.software_categories_is_global:
software_category_organization = app_settings.global_organization
else:
software_category_organization = device.organization
for inventory in list(data.software):
software = None
software_category = None
software_version = None
device_software = None
if SoftwareCategory.objects.filter( name = inventory.category ).exists():
software_category = SoftwareCategory.objects.get(
name = inventory.category
)
else: # Create Software Category
software_category = SoftwareCategory.objects.create(
organization = software_category_organization,
is_global = True,
name = inventory.category,
)
if Software.objects.filter( name = inventory.name ).exists():
software = Software.objects.get(
name = inventory.name
)
if not software.category:
software.category = software_category
software.save()
else: # Create Software
software = Software.objects.create(
organization = software_organization,
is_global = True,
name = inventory.name,
category = software_category,
)
pattern = r"^(\d+:)?(?P<semver>\d+\.\d+(\.\d+)?)"
semver = re.search(pattern, str(inventory.version), re.DOTALL)
if semver:
semver = semver['semver']
else:
semver = inventory.version
if SoftwareVersion.objects.filter( name = semver, software = software ).exists():
software_version = SoftwareVersion.objects.get(
name = semver,
software = software,
)
else: # Create Software Category
software_version = SoftwareVersion.objects.create(
organization = self.default_organization,
is_global = True,
name = semver,
software = software,
)
if DeviceSoftware.objects.filter( software = software, device=device ).exists():
device_software = DeviceSoftware.objects.get(
device = device,
software = software
)
else: # Create Software
device_software = DeviceSoftware.objects.create(
organization = self.default_organization,
is_global = True,
installedversion = software_version,
software = software,
device = device,
action=None
)
if device_software: # Update the Inventoried software
clear_installed_software = DeviceSoftware.objects.filter(
device = device,
software = software
)
# Clear installed version of all installed software
# any found later with no version to be removed
clear_installed_software.update(installedversion=None)
if not device_software.installed: # Only update install date if blank
device_software.installed = timezone.now()
device_software.save()
device_software.installedversion = software_version
device_software.save()
if device and operating_system and operating_system_version and device_operating_system:
# Remove software no longer installed
DeviceSoftware.objects.filter(
device = device,
software = software,
).delete()
device.inventorydate = timezone.now()
device.save()
if status != Http.Status.CREATED:
status = Http.Status.OK
response_data: dict = {"task_id": f"{task.id}"}
except PermissionDenied as e:

View File

@ -1,6 +1,7 @@
from django.core.exceptions import PermissionDenied
from django.forms import ValidationError
from rest_framework import exceptions
from rest_framework.permissions import DjangoObjectPermissions
from access.mixin import OrganizationMixin
@ -28,12 +29,16 @@ class OrganizationPermissionAPI(DjangoObjectPermissions, OrganizationMixin):
self.request = request
method = self.request._request.method.lower()
if method.upper() not in view.allowed_methods:
view.http_method_not_allowed(request._request)
if hasattr(view, 'queryset'):
if view.queryset.model._meta:
self.obj = view.queryset.model
method = self.request._request.method.lower()
object_organization = None
if method == 'get':

View File

@ -0,0 +1,3 @@
from .celery import worker as celery_app
__all__ = ('celery_app',)

18
app/app/celery.py Normal file
View File

@ -0,0 +1,18 @@
import os
from django.conf import settings
from celery import Celery
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')
worker = Celery('app')
worker.config_from_object(f'django.conf:settings', namespace='CELERY')
worker.autodiscover_tasks()
@worker.task(bind=True, ignore_result=True)
def debug_task(self):
print(f'Request: {self!r}')

View File

@ -5,6 +5,8 @@ from app.urls import urlpatterns
from django.conf import settings
from django.urls import URLPattern, URLResolver
from access.models import Organization
from settings.models.user_settings import UserSettings
@ -72,6 +74,23 @@ def user_settings(context) -> int:
return None
def user_default_organization(context) -> int:
""" Provides the users default organization.
Returns:
int: Users Default Organization
"""
if context.user.is_authenticated:
settings = UserSettings.objects.filter(user=context.user)
if settings[0].default_organization:
return settings[0].default_organization.id
return None
def nav_items(context) -> list(dict()):
""" Fetch All Project URLs
@ -88,7 +107,7 @@ def nav_items(context) -> list(dict()):
is_active: {bool} if this link is the active URL
Returns:
_type_: _description_
list: Items user has view access to
"""
dnav = []
@ -142,11 +161,45 @@ def nav_items(context) -> list(dict()):
name = str(pattern.name)
nav_items = nav_items + [ {
'name': name,
'url': url,
'is_active': is_active
} ]
if hasattr(pattern.callback.view_class, 'permission_required'):
permissions_required = pattern.callback.view_class.permission_required
user_has_perm = False
if type(permissions_required) is list:
user_has_perm = context.user.has_perms(permissions_required)
else:
user_has_perm = context.user.has_perm(permissions_required)
if hasattr(pattern.callback.view_class, 'model'):
if pattern.callback.view_class.model is Organization and context.user.is_authenticated:
organizations = Organization.objects.filter(manager = context.user)
if len(organizations) > 0:
user_has_perm = True
if str(nav_group.app_name).lower() == 'settings':
user_has_perm = True
if context.user.is_superuser:
user_has_perm = True
if user_has_perm:
nav_items = nav_items + [ {
'name': name,
'url': url,
'is_active': is_active
} ]
if len(nav_items) > 0:
@ -167,4 +220,5 @@ def common(context):
'nav_items': nav_items(context),
'social_backends': social_backends(context),
'user_settings': user_settings(context),
'user_default_organization': user_default_organization(context)
}

View File

@ -24,14 +24,61 @@ SETTINGS_DIR = '/etc/itsm' # Primary Settings Directory
BUILD_REPO = os.getenv('CI_PROJECT_URL')
BUILD_SHA = os.getenv('CI_COMMIT_SHA')
BUILD_VERSION = os.getenv('CI_COMMIT_TAG')
DOCS_ROOT = 'https://nofusscomputing.com/projects/django-template/user/'
DOCS_ROOT = 'https://nofusscomputing.com/projects/centurion_erp/user/'
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
# Celery settings
CELERY_ACCEPT_CONTENT = ['json']
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True # broker_connection_retry_on_startup
CELERY_BROKER_URL = 'amqp://guest:guest@172.16.10.102:30712/itsm'
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#broker-use-ssl
# import ssl
# broker_use_ssl = {
# 'keyfile': '/var/ssl/private/worker-key.pem',
# 'certfile': '/var/ssl/amqp-server-cert.pem',
# 'ca_certs': '/var/ssl/myca.pem',
# 'cert_reqs': ssl.CERT_REQUIRED
# }
CELERY_BROKER_POOL_LIMIT = 3 # broker_pool_limit
CELERY_CACHE_BACKEND = 'django-cache'
CELERY_ENABLE_UTC = True
CELERY_RESULT_BACKEND = 'django-db'
CELERY_RESULT_EXTENDED = True
CELERY_TASK_SERIALIZER = 'json'
CELERY_TIMEZONE = 'UTC'
CELERY_TASK_DEFAULT_EXCHANGE = 'ITSM' # task_default_exchange
CELERY_TASK_DEFAULT_PRIORITY = 10 # 1-10=LOW-HIGH task_default_priority
# CELERY_TASK_DEFAULT_QUEUE = 'background'
CELERY_TASK_TIME_LIMIT = 3600 # task_time_limit
CELERY_TASK_TRACK_STARTED = True # task_track_started
# dont set concurrency for docer as it defaults to CPU count
CELERY_WORKER_CONCURRENCY = 2 # worker_concurrency - Default: Number of CPU cores
CELERY_WORKER_DEDUPLICATE_SUCCESSFUL_TASKS = True # worker_deduplicate_successful_tasks
CELERY_WORKER_MAX_TASKS_PER_CHILD = 1 # worker_max_tasks_per_child
# CELERY_WORKER_MAX_MEMORY_PER_CHILD = 10000 # 10000=10mb worker_max_memory_per_child - Default: No limit. Type: int (kilobytes)
# CELERY_TASK_SEND_SENT_EVENT = True
CELERY_WORKER_SEND_TASK_EVENTS = True # worker_send_task_events
# django setting.
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
'LOCATION': 'my_cache_table',
}
}
#
# Defaults
#
ALLOWED_HOSTS = [ '*' ] # Site host to serve
DEBUG = False # SECURITY WARNING: don't run with debug turned on in production!
SITE_URL = 'http://127.0.0.1' # domain with HTTP method for the sites URL
@ -62,6 +109,7 @@ INSTALLED_APPS = [
'rest_framework',
'rest_framework_json_api',
'social_django',
'django_celery_results',
'core.apps.CoreConfig',
'access.apps.AccessConfig',
'itam.apps.ItamConfig',
@ -169,7 +217,7 @@ STATICFILES_DIRS = [
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
SITE_TITLE = "Site Title"
SITE_TITLE = "Centurion ERP"
API_ENABLED = True
@ -259,6 +307,9 @@ curl:
'SWAGGER_UI_DIST': 'SIDECAR',
'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
'REDOC_DIST': 'SIDECAR',
'PREPROCESSING_HOOKS': [
'drf_spectacular.hooks.preprocess_exclude_path_format'
],
}
DATETIME_FORMAT = 'j N Y H:i:s'

View File

@ -118,6 +118,16 @@ class ModelPermissionsAdd:
add_data: dict = None
@pytest.mark.skip(reason="ToDO: write test")
def test_model_requires_attribute_parent_model(self):
""" Child model requires 'django view' attribute 'parent_model'
When a child-model is added the parent model is required so that the organization can be detrmined.
"""
pass
def test_model_add_user_anon_denied(self):
""" Check correct permission for add

View File

@ -0,0 +1,101 @@
import pytest
import unittest
from access.models import TenancyObject
from access.tests.abstract.tenancy_object import TenancyObject as TenancyObjectTestCases
from app.tests.abstract.views import AddView, ChangeView, DeleteView, DisplayView, IndexView
from core.mixin.history_save import SaveHistory
from core.tests.abstract.models import Models
class BaseModel:
""" Test cases for all models """
model = None
""" Model to test """
@pytest.mark.skip(reason="figure out how to test sub-sub-class")
def test_class_inherits_save_history(self):
""" Confirm class inheritence
TenancyObject must inherit SaveHistory
"""
assert issubclass(self.model, TenancyObject)
class TenancyModel(
BaseModel,
TenancyObjectTestCases,
Models
):
""" Test cases for tenancy models"""
model = None
""" Model to test """
class ModelAdd(
AddView
):
""" Unit Tests for Model Add """
class ModelChange(
ChangeView
):
""" Unit Tests for Model Change """
class ModelDelete(
DeleteView
):
""" Unit Tests for Model delete """
class ModelDisplay(
DisplayView
):
""" Unit Tests for Model display """
class ModelIndex(
IndexView
):
""" Unit Tests for Model index """
class ModelCommon(
ModelAdd,
ModelChange,
ModelDelete,
ModelDisplay
):
""" Unit Tests for all models """
class PrimaryModel(
ModelCommon,
ModelIndex
):
""" Tests for Primary Models
A Primary model is a model that is deemed a model that has the following views:
- Add
- Change
- Delete
- Display
- Index
"""

View File

@ -0,0 +1,565 @@
import inspect
import pytest
import unittest
class AddView:
""" Testing of Display view """
add_module: str = None
""" Full module path to test """
add_view: str = None
""" View Class name to test """
def test_view_add_attribute_not_exists_fields(self):
""" Attribute does not exists test
Ensure that `fields` attribute is not defined as the expectation is that a form will be used.
"""
module = __import__(self.add_module, fromlist=[self.add_view])
assert hasattr(module, self.add_view)
viewclass = getattr(module, self.add_view)
assert viewclass.fields is None
def test_view_add_attribute_exists_form_class(self):
""" Attribute exists test
Ensure that `form_class` attribute is defined as it's required.
"""
module = __import__(self.add_module, fromlist=[self.add_view])
assert hasattr(module, self.add_view)
viewclass = getattr(module, self.add_view)
assert hasattr(viewclass, 'form_class')
def test_view_add_attribute_type_form_class(self):
""" Attribute Type Test
Ensure that `form_class` attribute is a class.
"""
module = __import__(self.add_module, fromlist=[self.add_view])
assert hasattr(module, self.add_view)
viewclass = getattr(module, self.add_view)
assert inspect.isclass(viewclass.form_class)
def test_view_add_attribute_exists_model(self):
""" Attribute exists test
Ensure that `model` attribute is defined as it's required .
"""
module = __import__(self.add_module, fromlist=[self.add_view])
assert hasattr(module, self.add_view)
viewclass = getattr(module, self.add_view)
assert hasattr(viewclass, 'model')
def test_view_add_attribute_exists_permission_required(self):
""" Attribute exists test
Ensure that `permission_required` attribute is defined as it's required.
"""
module = __import__(self.add_module, fromlist=[self.add_view])
assert hasattr(module, self.add_view)
viewclass = getattr(module, self.add_view)
assert hasattr(viewclass, 'permission_required')
def test_view_add_attribute_type_permission_required(self):
""" Attribute Type Test
Ensure that `permission_required` attribute is a list
"""
module = __import__(self.add_module, fromlist=[self.add_view])
assert hasattr(module, self.add_view)
viewclass = getattr(module, self.add_view)
assert type(viewclass.permission_required) is list
def test_view_add_attribute_exists_template_name(self):
""" Attribute exists test
Ensure that `template_name` attribute is defined as it's required.
"""
module = __import__(self.add_module, fromlist=[self.add_view])
assert hasattr(module, self.add_view)
viewclass = getattr(module, self.add_view)
assert hasattr(viewclass, 'template_name')
def test_view_add_attribute_type_template_name(self):
""" Attribute Type Test
Ensure that `template_name` attribute is a string.
"""
module = __import__(self.add_module, fromlist=[self.add_view])
assert hasattr(module, self.add_view)
viewclass = getattr(module, self.add_view)
assert type(viewclass.template_name) is str
class ChangeView:
""" Testing of Display view """
change_module: str = None
""" Full module path to test """
change_view: str = None
""" Change Class name to test """
def test_view_change_attribute_not_exists_fields(self):
""" Attribute does not exists test
Ensure that `fields` attribute is not defined as the expectation is that a form will be used.
"""
module = __import__(self.change_module, fromlist=[self.change_view])
assert hasattr(module, self.change_view)
viewclass = getattr(module, self.change_view)
assert viewclass.fields is None
def test_view_change_attribute_exists_form_class(self):
""" Attribute exists test
Ensure that `form_class` attribute is defined as it's required.
"""
module = __import__(self.change_module, fromlist=[self.change_view])
assert hasattr(module, self.change_view)
viewclass = getattr(module, self.change_view)
assert hasattr(viewclass, 'form_class')
def test_view_change_attribute_type_form_class(self):
""" Attribute Type Test
Ensure that `form_class` attribute is a string.
"""
module = __import__(self.change_module, fromlist=[self.change_view])
assert hasattr(module, self.change_view)
viewclass = getattr(module, self.change_view)
assert inspect.isclass(viewclass.form_class)
def test_view_change_attribute_exists_model(self):
""" Attribute exists test
Ensure that `model` attribute is defined as it's required .
"""
module = __import__(self.change_module, fromlist=[self.change_view])
assert hasattr(module, self.change_view)
viewclass = getattr(module, self.change_view)
assert hasattr(viewclass, 'model')
def test_view_change_attribute_exists_permission_required(self):
""" Attribute exists test
Ensure that `permission_required` attribute is defined as it's required.
"""
module = __import__(self.change_module, fromlist=[self.change_view])
assert hasattr(module, self.change_view)
viewclass = getattr(module, self.change_view)
assert hasattr(viewclass, 'permission_required')
def test_view_change_attribute_type_permission_required(self):
""" Attribute Type Test
Ensure that `permission_required` attribute is a list
"""
module = __import__(self.change_module, fromlist=[self.change_view])
assert hasattr(module, self.change_view)
viewclass = getattr(module, self.change_view)
assert type(viewclass.permission_required) is list
def test_view_change_attribute_exists_template_name(self):
""" Attribute exists test
Ensure that `template_name` attribute is defined as it's required.
"""
module = __import__(self.change_module, fromlist=[self.change_view])
assert hasattr(module, self.change_view)
viewclass = getattr(module, self.change_view)
assert hasattr(viewclass, 'template_name')
def test_view_change_attribute_type_template_name(self):
""" Attribute Type Test
Ensure that `template_name` attribute is a string.
"""
module = __import__(self.change_module, fromlist=[self.change_view])
assert hasattr(module, self.change_view)
viewclass = getattr(module, self.change_view)
assert type(viewclass.template_name) is str
class DeleteView:
""" Testing of Display view """
delete_module: str = None
""" Full module path to test """
delete_view: str = None
""" Delete Class name to test """
def test_view_delete_attribute_exists_model(self):
""" Attribute exists test
Ensure that `model` attribute is defined as it's required .
"""
module = __import__(self.delete_module, fromlist=[self.delete_view])
assert hasattr(module, self.delete_view)
viewclass = getattr(module, self.delete_view)
assert hasattr(viewclass, 'model')
def test_view_delete_attribute_exists_permission_required(self):
""" Attribute exists test
Ensure that `model` attribute is defined as it's required .
"""
module = __import__(self.delete_module, fromlist=[self.delete_view])
assert hasattr(module, self.delete_view)
viewclass = getattr(module, self.delete_view)
assert hasattr(viewclass, 'permission_required')
def test_view_delete_attribute_type_permission_required(self):
""" Attribute Type Test
Ensure that `permission_required` attribute is a list
"""
module = __import__(self.delete_module, fromlist=[self.delete_view])
assert hasattr(module, self.delete_view)
viewclass = getattr(module, self.delete_view)
assert type(viewclass.permission_required) is list
def test_view_delete_attribute_exists_template_name(self):
""" Attribute exists test
Ensure that `template_name` attribute is defined as it's required.
"""
module = __import__(self.delete_module, fromlist=[self.delete_view])
assert hasattr(module, self.delete_view)
viewclass = getattr(module, self.delete_view)
assert hasattr(viewclass, 'template_name')
def test_view_delete_attribute_type_template_name(self):
""" Attribute Type Test
Ensure that `template_name` attribute is a string.
"""
module = __import__(self.delete_module, fromlist=[self.delete_view])
assert hasattr(module, self.delete_view)
viewclass = getattr(module, self.delete_view)
assert type(viewclass.template_name) is str
class DisplayView:
""" Testing of Display view """
display_module: str = None
""" Full module path to test """
display_view: str = None
""" Change Class name to test """
def test_view_display_attribute_exists_model(self):
""" Attribute exists test
Ensure that `model` attribute is defined as it's required .
"""
module = __import__(self.display_module, fromlist=[self.display_view])
assert hasattr(module, self.display_view)
viewclass = getattr(module, self.display_view)
assert hasattr(viewclass, 'model')
def test_view_display_attribute_exists_permission_required(self):
""" Attribute exists test
Ensure that `permission_required` attribute is defined as it's required.
"""
module = __import__(self.display_module, fromlist=[self.display_view])
assert hasattr(module, self.display_view)
viewclass = getattr(module, self.display_view)
assert hasattr(viewclass, 'permission_required')
def test_view_display_attribute_type_permission_required(self):
""" Attribute Type Test
Ensure that `permission_required` attribute is a list
"""
module = __import__(self.display_module, fromlist=[self.display_view])
assert hasattr(module, self.display_view)
viewclass = getattr(module, self.display_view)
assert type(viewclass.permission_required) is list
def test_view_display_attribute_exists_template_name(self):
""" Attribute exists test
Ensure that `template_name` attribute is defined as it's required.
"""
module = __import__(self.display_module, fromlist=[self.display_view])
assert hasattr(module, self.display_view)
viewclass = getattr(module, self.display_view)
assert hasattr(viewclass, 'template_name')
def test_view_display_attribute_type_template_name(self):
""" Attribute Type Test
Ensure that `template_name` attribute is a string.
"""
module = __import__(self.display_module, fromlist=[self.display_view])
assert hasattr(module, self.display_view)
viewclass = getattr(module, self.display_view)
assert type(viewclass.template_name) is str
class IndexView:
""" Testing of Display view """
index_module: str = None
""" Full module path to test """
index_view: str = None
""" Index Class name to test """
def test_view_index_attribute_exists_model(self):
""" Attribute exists test
Ensure that `model` attribute is defined as it's required .
"""
module = __import__(self.index_module, fromlist=[self.index_view])
assert hasattr(module, self.index_view)
viewclass = getattr(module, self.index_view)
assert hasattr(viewclass, 'model')
def test_view_index_attribute_exists_permission_required(self):
""" Attribute exists test
Ensure that `model` attribute is defined as it's required .
"""
module = __import__(self.index_module, fromlist=[self.index_view])
assert hasattr(module, self.index_view)
viewclass = getattr(module, self.index_view)
assert hasattr(viewclass, 'permission_required')
def test_view_index_attribute_type_permission_required(self):
""" Attribute Type Test
Ensure that `permission_required` attribute is a list
"""
module = __import__(self.index_module, fromlist=[self.index_view])
assert hasattr(module, self.index_view)
viewclass = getattr(module, self.index_view)
assert type(viewclass.permission_required) is list
def test_view_index_attribute_exists_template_name(self):
""" Attribute exists test
Ensure that `template_name` attribute is defined as it's required.
"""
module = __import__(self.index_module, fromlist=[self.index_view])
assert hasattr(module, self.index_view)
viewclass = getattr(module, self.index_view)
assert hasattr(viewclass, 'template_name')
def test_view_index_attribute_type_template_name(self):
""" Attribute Type Test
Ensure that `template_name` attribute is a string.
"""
module = __import__(self.index_module, fromlist=[self.index_view])
assert hasattr(module, self.index_view)
viewclass = getattr(module, self.index_view)
assert type(viewclass.template_name) is str
class AllViews(
AddView,
ChangeView,
DeleteView,
DisplayView,
IndexView
):
""" Abstract test class containing ALL view tests """
add_module: str = None
""" Full module path to test """
add_view: str = None
""" View Class name to test """
change_module: str = None
""" Full module path to test """
change_view: str = None
""" Change Class name to test """
delete_module: str = None
""" Full module path to test """
delete_view: str = None
""" Delete Class name to test """
display_module: str = None
""" Full module path to test """
display_view: str = None
""" Change Class name to test """
index_module: str = None
""" Full module path to test """
index_view: str = None
""" Index Class name to test """

View File

@ -1,11 +1,13 @@
from django import forms
from django.db.models import Q
from config_management.models.groups import ConfigGroupSoftware
from core.forms.common import CommonModelForm
from itam.models.software import Software
class SoftwareAdd(forms.ModelForm):
class SoftwareAdd(CommonModelForm):
class Meta:
model = ConfigGroupSoftware
@ -13,9 +15,3 @@ class SoftwareAdd(forms.ModelForm):
'software',
'action'
]
def __init__(self, *args, **kwargs):
organizations = kwargs.pop('organizations')
super().__init__(*args, **kwargs)
self.fields['software'].queryset = Software.objects.filter(Q(organization_id__in=organizations) | Q(is_global = True))

View File

@ -1,11 +1,13 @@
from django import forms
from django.db.models import Q
from config_management.models.groups import ConfigGroupSoftware
from core.forms.common import CommonModelForm
from itam.models.software import Software, SoftwareVersion
class SoftwareUpdate(forms.ModelForm):
class SoftwareUpdate(CommonModelForm):
class Meta:
model = ConfigGroupSoftware

View File

@ -1,11 +1,13 @@
from django import forms
from django.db.models import Q
from config_management.models.groups import ConfigGroups
from core.forms.common import CommonModelForm
from itam.models.software import Software, SoftwareVersion
class ConfigGroupForm(forms.ModelForm):
class ConfigGroupForm(CommonModelForm):
class Meta:
model = ConfigGroups
@ -13,5 +15,20 @@ class ConfigGroupForm(forms.ModelForm):
'name',
'parent',
'is_global',
'organization',
'model_notes',
'config',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if hasattr(kwargs['instance'], 'id'):
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(
).exclude(
id=int(kwargs['instance'].id)
)

View File

@ -1,11 +1,12 @@
from django import forms
from itam.models.device import Device
from config_management.models.groups import ConfigGroups, ConfigGroupHosts
from core.forms.common import CommonModelForm
class ConfigGroupHostsForm(forms.ModelForm):
class ConfigGroupHostsForm(CommonModelForm):
__name__ = 'asdsa'

View File

@ -1,6 +1,8 @@
# Generated by Django 5.0.6 on 2024-06-02 14:48
# Generated by Django 5.0.7 on 2024-07-12 03:54
import access.fields
import access.models
import config_management.models.groups
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
@ -11,7 +13,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('access', '0002_alter_team_organization'),
('access', '0001_initial'),
]
operations = [
@ -19,16 +21,17 @@ class Migration(migrations.Migration):
name='ConfigGroups',
fields=[
('is_global', models.BooleanField(default=False)),
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('name', models.CharField(max_length=50)),
('config', models.JSONField(blank=True, default=None, null=True)),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization')),
('config', models.JSONField(blank=True, default=None, null=True, validators=[config_management.models.groups.ConfigGroups.validate_config_keys_not_reserved])),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
('parent', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='config_management.configgroups')),
],
options={
'verbose_name': 'Config Groups',
'abstract': False,
},
),
]

View File

@ -1,43 +0,0 @@
# Generated by Django 5.0.6 on 2024-06-02 20:51
import access.fields
import config_management.models.groups
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0002_alter_team_organization'),
('config_management', '0001_initial'),
('itam', '0012_alter_device_serial_number_alter_device_uuid'),
]
operations = [
migrations.AlterModelOptions(
name='configgroups',
options={},
),
migrations.AlterField(
model_name='configgroups',
name='config',
field=models.JSONField(blank=True, default=None, null=True, validators=[config_management.models.groups.ConfigGroups.validate_config_keys_not_reserved]),
),
migrations.CreateModel(
name='ConfigGroupHosts',
fields=[
('is_global', models.BooleanField(default=False)),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='config_management.configgroups')),
('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='itam.device', validators=[config_management.models.groups.ConfigGroupHosts.validate_host_no_parent_group])),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization')),
],
options={
'abstract': False,
},
),
]

View File

@ -1,7 +1,8 @@
# Generated by Django 5.0.6 on 2024-06-07 21:43
# Generated by Django 5.0.7 on 2024-07-12 03:58
import access.fields
import access.models
import config_management.models.groups
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
@ -10,16 +11,33 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0003_alter_team_organization'),
('config_management', '0003_alter_configgrouphosts_organization_and_more'),
('itam', '0013_alter_device_organization_and_more'),
('access', '0001_initial'),
('config_management', '0001_initial'),
('itam', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='ConfigGroupHosts',
fields=[
('is_global', models.BooleanField(default=False)),
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='config_management.configgroups')),
('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='itam.device', validators=[config_management.models.groups.ConfigGroupHosts.validate_host_no_parent_group])),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='ConfigGroupSoftware',
fields=[
('is_global', models.BooleanField(default=False)),
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),

View File

@ -1,26 +0,0 @@
# Generated by Django 5.0.6 on 2024-06-05 09:16
import access.models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0003_alter_team_organization'),
('config_management', '0002_alter_configgroups_options_alter_configgroups_config_and_more'),
]
operations = [
migrations.AlterField(
model_name='configgrouphosts',
name='organization',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]),
),
migrations.AlterField(
model_name='configgroups',
name='organization',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]),
),
]

View File

@ -1,28 +0,0 @@
# Generated by Django 5.0.6 on 2024-06-11 20:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config_management', '0004_configgroupsoftware'),
]
operations = [
migrations.AddField(
model_name='configgrouphosts',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True),
),
migrations.AddField(
model_name='configgroups',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True),
),
migrations.AddField(
model_name='configgroupsoftware',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True),
),
]

View File

@ -186,6 +186,15 @@ class ConfigGroups(GroupsCommonFields, SaveHistory):
if self.parent:
self.organization = ConfigGroups.objects.get(id=self.parent.id).organization
if self.pk:
obj = ConfigGroups.objects.get(
id = self.id,
)
# Prevent organization change. ToDo: add feature so that config can change organizations
self.organization = obj.organization
super().save(*args, **kwargs)
@ -193,7 +202,7 @@ class ConfigGroups(GroupsCommonFields, SaveHistory):
if self.parent:
return f'{self.parent.name} > {self.name}'
return f'{self.parent} > {self.name}'
return self.name

View File

@ -48,7 +48,7 @@
{% csrf_token %}
{{ form }}
{% include 'icons/issue_link.html.j2' with issue=13 %}<br>
<br>
<input type="submit" value="Submit">
<script>

View File

@ -5,12 +5,17 @@ from django.test import TestCase
from access.models import Organization
from app.tests.abstract.models import TenancyModel
from config_management.models.groups import ConfigGroups
@pytest.mark.django_db
class ConfigGroupsModel(TestCase):
class ConfigGroupsModel(
TestCase,
TenancyModel
):
model = ConfigGroups

View File

@ -1,18 +0,0 @@
import pytest
import unittest
import requests
from django.test import TestCase, Client
from access.tests.abstract.tenancy_object import TenancyObject
from config_management.models.groups import ConfigGroups
class ConfigGroupsTenancyObject(
TestCase,
TenancyObject
):
model = ConfigGroups

View File

@ -0,0 +1,224 @@
import pytest
import unittest
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import Client, TestCase
from access.models import Organization, Team, TeamUsers, Permission
from app.tests.abstract.models import TenancyModel
from config_management.models.groups import ConfigGroups
@pytest.mark.django_db
class ConfigGroupsAPI(
TestCase,
):
model = ConfigGroups
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
2. Create an item
"""
self.organization = Organization.objects.create(name='test_org')
self.item = self.model.objects.create(
organization = self.organization,
name = 'one',
config = dict({"key": "one", "existing": "dont_over_write"})
)
self.second_item = self.model.objects.create(
organization = self.organization,
name = 'one_two',
config = dict({"key": "two"}),
parent = self.item
)
self.url_view_kwargs = {'pk': self.second_item.id}
view_permissions = Permission.objects.get(
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = self.organization,
)
view_team.permissions.set([view_permissions])
self.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
client = Client()
url = reverse('API:_api_config_group', kwargs=self.url_view_kwargs)
client.force_login(self.view_user)
response = client.get(url)
self.api_data = response.data
def test_api_field_exists_id(self):
""" Test for existance of API Field
id field must exist
"""
assert 'id' in self.api_data
def test_api_field_type_id(self):
""" Test for type for API Field
id field must be int
"""
assert type(self.api_data['id']) is int
def test_api_field_exists_parent(self):
""" Test for existance of API Field
parent field must exist
"""
assert 'parent' in self.api_data
def test_api_field_type_parent(self):
""" Test for type for API Field
parent field must be dict
"""
assert type(self.api_data['parent']) is dict
def test_api_field_exists_name(self):
""" Test for existance of API Field
name field must exist
"""
assert 'name' in self.api_data
def test_api_field_type_name(self):
""" Test for type for API Field
name field must be str
"""
assert type(self.api_data['name']) is str
def test_api_field_exists_config(self):
""" Test for existance of API Field
config field must exist
"""
assert 'config' in self.api_data
def test_api_field_type_config(self):
""" Test for type for API Field
config field must be dict
"""
assert type(self.api_data['config']) is dict
def test_api_field_exists_url(self):
""" Test for existance of API Field
url field must exist
"""
assert 'url' in self.api_data
def test_api_field_type_url(self):
""" Test for type for API Field
url field must be str
"""
assert type(self.api_data['url']) is str
def test_api_field_exists_parent_id(self):
""" Test for existance of API Field
parent.id field must exist
"""
assert 'id' in self.api_data['parent']
def test_api_field_type_parent_id(self):
""" Test for type for API Field
parent.id field must be int
"""
assert type(self.api_data['parent']['id']) is int
def test_api_field_exists_parent_name(self):
""" Test for existance of API Field
parent.name field must exist
"""
assert 'name' in self.api_data['parent']
def test_api_field_type_parent_name(self):
""" Test for type for API Field
parent.name field must be str
"""
assert type(self.api_data['parent']['name']) is str
def test_api_field_exists_parent_url(self):
""" Test for existance of API Field
parent.url field must exist
"""
assert 'url' in self.api_data['parent']
def test_api_field_type_parent_url(self):
""" Test for type for API Field
parent.url field must be str
"""
assert type(self.api_data['parent']['url']) is str

View File

@ -0,0 +1,29 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import PrimaryModel
class ConfigManagementViews(
TestCase,
PrimaryModel
):
add_module = 'config_management.views.groups.groups'
add_view = 'GroupAdd'
change_module = add_module
change_view = 'GroupView'
delete_module = add_module
delete_view = 'GroupDelete'
display_module = add_module
display_view = 'GroupView'
index_module = add_module
index_view = 'GroupIndexView'

View File

@ -5,6 +5,8 @@ from django.test import TestCase
from access.models import Organization
from app.tests.abstract.models import TenancyModel
from config_management.models.groups import ConfigGroups, ConfigGroupSoftware
from itam.models.device import DeviceSoftware
@ -12,7 +14,10 @@ from itam.models.software import Software
class ConfigGroupSoftwareModel(TestCase):
class ConfigGroupSoftwareModel(
TestCase,
TenancyModel
):
model = ConfigGroupSoftware

View File

@ -1,18 +0,0 @@
import pytest
import unittest
import requests
from django.test import TestCase, Client
from access.tests.abstract.tenancy_object import TenancyObject
from config_management.models.groups import ConfigGroupSoftware
class ConfigGroupSoftwareTenancyObject(
TestCase,
TenancyObject
):
model = ConfigGroupSoftware

View File

@ -0,0 +1,31 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import AddView, ChangeView, DeleteView
class ConfigGroupsSoftwareViews(
TestCase,
AddView,
ChangeView,
DeleteView
):
add_module = 'config_management.views.groups.software'
add_view = 'GroupSoftwareAdd'
change_module = add_module
change_view = 'GroupSoftwareChange'
delete_module = add_module
delete_view = 'GroupSoftwareDelete'
# display_module = add_module
# display_view = 'GroupView'
# index_module = add_module
# index_view = 'GroupIndexView'

View File

@ -10,14 +10,14 @@ urlpatterns = [
path('group/add', GroupAdd.as_view(), name='_group_add'),
path('group/<int:pk>', GroupView.as_view(), name='_group_view'),
path('group/<int:group_id>/child', GroupAdd.as_view(), name='_group_add_child'),
path('group/<int:pk>/child', GroupAdd.as_view(), name='_group_add_child'),
path('group/<int:pk>/delete', GroupDelete.as_view(), name='_group_delete'),
path("group/<int:pk>/software/add", GroupSoftwareAdd.as_view(), name="_group_software_add"),
path("group/<int:group_id>/software/<int:pk>", GroupSoftwareChange.as_view(), name="_group_software_change"),
path("group/<int:group_id>/software/<int:pk>/delete", GroupSoftwareDelete.as_view(), name="_group_software_delete"),
path('group/<int:group_id>/host', GroupHostAdd.as_view(), name='_group_add_host'),
path('group/<int:pk>/host', GroupHostAdd.as_view(), name='_group_add_host'),
path('group/<int:group_id>/host/<int:pk>/delete', GroupHostDelete.as_view(), name='_group_delete_host'),
]

View File

@ -1,15 +1,12 @@
import json
from django.contrib.auth import decorators as auth_decorator
from django.db.models import Count, Q
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import generic
from access.mixin import OrganizationPermission
from core.forms.comment import AddNoteForm
from core.models.notes import Notes
from core.views.common import AddView, ChangeView, DeleteView, IndexView
from itam.models.device import Device
@ -21,7 +18,7 @@ from config_management.models.groups import ConfigGroups, ConfigGroupHosts, Conf
class GroupIndexView(OrganizationPermission, generic.ListView):
class GroupIndexView(IndexView):
context_object_name = "groups"
@ -29,7 +26,9 @@ class GroupIndexView(OrganizationPermission, generic.ListView):
paginate_by = 10
permission_required = 'config_management.view_configgroups'
permission_required = [
'config_management.view_configgroups'
]
template_name = 'config_management/group_index.html.j2'
@ -47,23 +46,15 @@ class GroupIndexView(OrganizationPermission, generic.ListView):
def get_queryset(self):
if self.request.user.is_superuser:
return self.model.objects.filter(parent=None).order_by('name')
else:
return self.model.objects.filter(Q(parent=None, organization__in=self.user_organizations()) | Q(parent=None, is_global = True)).order_by('name')
return self.model.objects.filter(parent=None).order_by('name')
class GroupAdd(OrganizationPermission, generic.CreateView):
class GroupAdd(AddView):
fields = [
'name',
'parent',
'organization',
]
organization_field = 'organization'
form_class = ConfigGroupForm
model = ConfigGroups
@ -80,11 +71,11 @@ class GroupAdd(OrganizationPermission, generic.CreateView):
'organization': UserSettings.objects.get(user = self.request.user).default_organization
}
if 'group_id' in self.kwargs:
if 'pk' in self.kwargs:
if self.kwargs['group_id']:
if self.kwargs['pk']:
initial.update({'parent': self.kwargs['group_id']})
initial.update({'parent': self.kwargs['pk']})
self.model.parent.field.hidden = True
@ -111,7 +102,7 @@ class GroupAdd(OrganizationPermission, generic.CreateView):
class GroupView(OrganizationPermission, generic.UpdateView):
class GroupView(ChangeView):
context_object_name = "group"
@ -195,7 +186,7 @@ class GroupView(OrganizationPermission, generic.UpdateView):
class GroupDelete(OrganizationPermission, generic.DeleteView):
class GroupDelete(DeleteView):
model = ConfigGroups
@ -220,12 +211,14 @@ class GroupDelete(OrganizationPermission, generic.DeleteView):
class GroupHostAdd(OrganizationPermission, generic.CreateView):
class GroupHostAdd(AddView):
model = ConfigGroupHosts
parent_model = ConfigGroups
permission_required = [
'config_management.add_hosts',
'config_management.add_configgrouphosts',
]
template_name = 'form.html.j2'
@ -235,7 +228,9 @@ class GroupHostAdd(OrganizationPermission, generic.CreateView):
def form_valid(self, form):
form.instance.group_id = self.kwargs['group_id']
form.instance.group_id = self.kwargs['pk']
form.instance.organization = self.parent_model.objects.get(pk=form.instance.group_id).organization
return super().form_valid(form)
@ -252,40 +247,31 @@ class GroupHostAdd(OrganizationPermission, generic.CreateView):
form_class = super().get_form(form_class=None)
group = ConfigGroups.objects.get(pk=self.kwargs['group_id'])
group = ConfigGroups.objects.get(pk=self.kwargs['pk'])
exsting_group_hosts = ConfigGroupHosts.objects.filter(group=group)
form_class.fields["host"].queryset = None
form_class.fields["host"].queryset = form_class.fields["host"].queryset.filter(
).exclude(
id__in=exsting_group_hosts.values_list('host', flat=True)
)
if group.is_global:
form_class.fields["host"].queryset = Device.objects.filter(
).exclude(
id__in=exsting_group_hosts.values_list('host', flat=True)
)
if form_class.fields["host"].queryset is None:
form_class.fields["host"].queryset = Device.objects.filter(
organization=group.organization.id,
).exclude(id__in=exsting_group_hosts.values_list('host', flat=True))
return form_class
def get_success_url(self, **kwargs):
return reverse('Config Management:_group_view', args=[self.kwargs['group_id'],])
return reverse('Config Management:_group_view', args=[self.kwargs['pk'],])
class GroupHostDelete(OrganizationPermission, generic.DeleteView):
class GroupHostDelete(DeleteView):
model = ConfigGroupHosts
permission_required = [
'config_management.delete_hosts',
'config_management.delete_configgrouphosts',
]
template_name = 'form.html.j2'

View File

@ -1,7 +1,4 @@
from django.urls import reverse
from django.views import generic
from access.mixin import OrganizationPermission
from itam.models.software import Software
@ -9,9 +6,10 @@ from config_management.forms.group.add_software import SoftwareAdd
from config_management.forms.group.change_software import SoftwareUpdate
from config_management.models.groups import ConfigGroups, ConfigGroupSoftware
from core.views.common import AddView, ChangeView, DeleteView
class GroupSoftwareAdd(OrganizationPermission, generic.CreateView):
class GroupSoftwareAdd(AddView):
form_class = SoftwareAdd
@ -53,13 +51,6 @@ class GroupSoftwareAdd(OrganizationPermission, generic.CreateView):
return super().form_valid(form)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
obj = ConfigGroups.objects.get(pk=self.kwargs['pk'])
kwargs['organizations'] = [ obj.organization.id ]
return kwargs
def get_success_url(self, **kwargs):
return reverse('Config Management:_group_view', args=(self.kwargs['pk'],))
@ -74,7 +65,7 @@ class GroupSoftwareAdd(OrganizationPermission, generic.CreateView):
class GroupSoftwareChange(OrganizationPermission, generic.UpdateView):
class GroupSoftwareChange(ChangeView):
form_class = SoftwareUpdate
@ -113,7 +104,7 @@ class GroupSoftwareChange(OrganizationPermission, generic.UpdateView):
class GroupSoftwareDelete(OrganizationPermission, generic.DeleteView):
class GroupSoftwareDelete(DeleteView):
model = ConfigGroupSoftware

5
app/core/exceptions.py Normal file
View File

@ -0,0 +1,5 @@
class MissingAttribute(Exception):
""" An attribute is missing"""
pass

View File

View File

@ -1,10 +1,10 @@
from django import forms
from app import settings
from core.forms.common import CommonModelForm
from core.models.notes import Notes
class AddNoteForm(forms.ModelForm):
class AddNoteForm(CommonModelForm):
prefix = 'note'

100
app/core/forms/common.py Normal file
View File

@ -0,0 +1,100 @@
from django import forms
from django.db.models import Q
from access.models import Organization, TeamUsers
class CommonModelForm(forms.ModelForm):
""" Abstract Form class for form inclusion
This class exists so that common functions can be conducted against forms as they are loaded.
"""
organization_field: str = 'organization'
""" Organization Field
Name of the field that contains Organizations.
This field will be filtered to those that the user is part of.
"""
def __init__(self, *args, **kwargs):
"""Form initialization.
Initialize the form using the super classes first then continue to initialize the form using logic
contained within this method.
## Tenancy Objects
Fields that contain an attribute called `organization` will have the objects filtered to
the organizations the user is part of. If the object has `is_global=True`, that object will not be
filtered out.
!!! danger "Requirement"
This method may be overridden however must still be called from the overriding function. i.e. `super().__init__(*args, **kwargs)`
"""
user = kwargs.pop('user', None)
user_organizations: list([str]) = []
user_organizations_id: list(int()) = []
for team_user in TeamUsers.objects.filter(user=user):
if team_user.team.organization.name not in user_organizations:
if not user_organizations:
self.user_organizations = []
user_organizations += [ team_user.team.organization.name ]
user_organizations_id += [ team_user.team.organization.id ]
new_kwargs: dict = {}
for key, value in kwargs.items():
if key != 'user':
new_kwargs.update({key: value})
super().__init__(*args, **new_kwargs)
if len(user_organizations_id) > 0:
for field_name in self.fields:
field = self.fields[field_name]
if hasattr(field, 'queryset'):
if hasattr(field.queryset.model, 'organization'):
if hasattr(field.queryset.model, 'is_global'):
self.fields[field_name].queryset = field.queryset.filter(
Q(organization__in=user_organizations_id)
|
Q(is_global = True)
)
else:
self.fields[field_name].queryset = field.queryset.filter(
Q(organization__in=user_organizations_id)
)
if self.Meta.fields:
if self.organization_field in self.Meta.fields:
self.fields[self.organization_field].queryset = self.fields[self.organization_field].queryset.filter(
Q(id__in=user_organizations_id)
|
Q(manager=user)
)

View File

@ -0,0 +1,26 @@
from django import forms
from core.forms.common import CommonModelForm
from core.models.manufacturer import Manufacturer
from settings.forms.admin_settings_global import AdminGlobalModels
class ManufacturerForm(
AdminGlobalModels,
CommonModelForm
):
class Meta:
fields = [
'name',
'slug',
'id',
'organization',
'is_global',
'model_notes',
]
model = Manufacturer

View File

@ -1,6 +1,7 @@
# Generated by Django 5.0.6 on 2024-05-20 15:44
# Generated by Django 5.0.7 on 2024-07-12 03:54
import access.fields
import access.models
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
@ -13,29 +14,43 @@ class Migration(migrations.Migration):
dependencies = [
('access', '0001_initial'),
('itam', '0007_device_inventorydate'),
('config_management', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Notes',
name='History',
fields=[
('is_global', models.BooleanField(default=False)),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('serial_number', models.CharField(blank=True, default=None, max_length=50, null=True, verbose_name='Serial Number')),
('note', models.TextField(default=None, null=True, verbose_name='Note')),
('device', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='itam.device')),
('operatingsystem', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='itam.operatingsystem')),
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='access.organization')),
('software', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='itam.software')),
('usercreated', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='usercreated', to=settings.AUTH_USER_MODEL, verbose_name='Added By')),
('usermodified', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='usermodified', to=settings.AUTH_USER_MODEL, verbose_name='Edited By')),
('before', models.TextField(blank=True, default=None, help_text='JSON Object before Change', null=True)),
('after', models.TextField(blank=True, default=None, help_text='JSON Object After Change', null=True)),
('action', models.IntegerField(choices=[('1', 'Create'), ('2', 'Update'), ('3', 'Delete')], default=None, null=True)),
('item_pk', models.IntegerField(default=None, null=True)),
('item_class', models.CharField(default=None, max_length=50, null=True)),
('item_parent_pk', models.IntegerField(default=None, null=True)),
('item_parent_class', models.CharField(default=None, max_length=50, null=True)),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created'],
},
),
migrations.CreateModel(
name='Manufacturer',
fields=[
('is_global', models.BooleanField(default=False)),
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('name', models.CharField(max_length=50, unique=True)),
('slug', access.fields.AutoSlugField()),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
],
options={
'ordering': ['name'],
},
),
]

View File

@ -0,0 +1,43 @@
# Generated by Django 5.0.7 on 2024-07-12 03:58
import access.fields
import access.models
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0001_initial'),
('config_management', '0002_configgrouphosts_configgroupsoftware'),
('core', '0001_initial'),
('itam', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Notes',
fields=[
('is_global', models.BooleanField(default=False)),
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('note', models.TextField(blank=True, default=None, null=True, verbose_name='Note')),
('config_group', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='config_management.configgroups')),
('device', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.device')),
('operatingsystem', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.operatingsystem')),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
('software', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.software')),
('usercreated', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='usercreated', to=settings.AUTH_USER_MODEL, verbose_name='Added By')),
('usermodified', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='usermodified', to=settings.AUTH_USER_MODEL, verbose_name='Edited By')),
],
options={
'ordering': ['-created'],
},
),
]

View File

@ -1,34 +0,0 @@
# Generated by Django 5.0.6 on 2024-05-20 16:06
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
('itam', '0007_device_inventorydate'),
]
operations = [
migrations.RemoveField(
model_name='notes',
name='serial_number',
),
migrations.AlterField(
model_name='notes',
name='device',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.device'),
),
migrations.AlterField(
model_name='notes',
name='operatingsystem',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.operatingsystem'),
),
migrations.AlterField(
model_name='notes',
name='software',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.software'),
),
]

View File

@ -1,41 +0,0 @@
# Generated by Django 5.0.6 on 2024-05-23 03:59
import access.fields
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0002_remove_notes_serial_number_alter_notes_device_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='notes',
name='note',
field=models.TextField(blank=True, default=None, null=True, verbose_name='Note'),
),
migrations.CreateModel(
name='History',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('before', models.TextField(blank=True, default=None, help_text='JSON Object before Change', null=True)),
('after', models.TextField(blank=True, default=None, help_text='JSON Object After Change', null=True)),
('action', models.IntegerField(choices=[('1', 'Create'), ('2', 'Update'), ('3', 'Delete')], default=None, null=True)),
('item_pk', models.IntegerField(default=None, null=True)),
('item_class', models.CharField(default=None, max_length=50, null=True)),
('item_parent_pk', models.IntegerField(default=None, null=True)),
('item_parent_class', models.CharField(default=None, max_length=50, null=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created'],
},
),
]

View File

@ -1,20 +0,0 @@
# Generated by Django 5.0.6 on 2024-05-23 10:37
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0002_alter_team_organization'),
('core', '0003_alter_notes_note_history'),
]
operations = [
migrations.AlterField(
model_name='notes',
name='organization',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization'),
),
]

View File

@ -1,32 +0,0 @@
# Generated by Django 5.0.6 on 2024-05-23 10:58
import access.fields
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0002_alter_team_organization'),
('core', '0004_notes_is_null'),
]
operations = [
migrations.CreateModel(
name='Manufacturer',
fields=[
('is_global', models.BooleanField(default=False)),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('name', models.CharField(max_length=50, unique=True)),
('slug', access.fields.AutoSlugField()),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization')),
],
options={
'ordering': ['name'],
},
),
]

View File

@ -1,21 +0,0 @@
# Generated by Django 5.0.6 on 2024-05-25 05:21
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0005_manufacturer'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='history',
name='user',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -1,20 +0,0 @@
# Generated by Django 5.0.6 on 2024-06-02 14:48
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config_management', '0001_initial'),
('core', '0006_alter_history_user'),
]
operations = [
migrations.AddField(
model_name='notes',
name='config_group',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='config_management.configgroups'),
),
]

View File

@ -1,26 +0,0 @@
# Generated by Django 5.0.6 on 2024-06-05 09:16
import access.models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0003_alter_team_organization'),
('core', '0007_notes_config_group'),
]
operations = [
migrations.AlterField(
model_name='manufacturer',
name='organization',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]),
),
migrations.AlterField(
model_name='notes',
name='organization',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]),
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 5.0.6 on 2024-06-11 20:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0008_alter_manufacturer_organization_and_more'),
]
operations = [
migrations.AddField(
model_name='manufacturer',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True),
),
migrations.AddField(
model_name='notes',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True),
),
]

View File

@ -0,0 +1,138 @@
{% extends 'base.html.j2' %}
{% load json %}
{% load markdown %}
{% block content %}
<script>
function openCity(evt, cityName) {
// Declare all variables
var i, tabcontent, tablinks;
// Get all elements with class="tabcontent" and hide them
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
// Get all elements with class="tablinks" and remove the class "active"
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
// Show the current tab, and add an "active" class to the button that opened the tab
document.getElementById(cityName).style.display = "block";
evt.currentTarget.className += " active";
}
</script>
<div class="tab">
<button onclick="window.location='{% url 'Settings:_task_results' %}';"
style="vertical-align: middle; padding: auto; margin: 0px">
<svg xmlns="http://www.w3.org/2000/svg" height="25px" viewBox="0 -960 960 960" width="25px"
style="vertical-align: middle; margin: 0px; padding: 0px border: none; " fill="#6a6e73">
<path d="m313-480 155 156q11 11 11.5 27.5T468-268q-11 11-28 11t-28-11L228-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T468-692q11 11 11 28t-11 28L313-480Zm264 0 155 156q11 11 11.5 27.5T732-268q-11 11-28 11t-28-11L492-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T732-692q11 11 11 28t-11 28L577-480Z" />
</svg> Back to Task Results</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
<!-- <button class="tablinks" onclick="openCity(event, 'Installations')">Installations</button> -->
</div>
<style>
.detail-view-field {
display:unset;
height: 30px;
line-height: 30px;
padding: 0px 20px 40px 20px;
}
.detail-view-field label {
display: inline-block;
font-weight: bold;
width: 200px;
margin: 10px;
/*padding: 10px;*/
height: 30px;
line-height: 30px;
}
.detail-view-field span {
display: inline-block;
width: 340px;
margin: 10px;
/*padding: 10px;*/
border-bottom: 1px solid #ccc;
height: 30px;
line-height: 30px;
}
</style>
<div id="Details" class="tabcontent">
<h3>Details </h3>
<div style="align-items:flex-start; align-content: center; display: flexbox; width: 100%">
<div style="display: inline; width: 40%; margin: 30px;">
<div class="detail-view-field">
<label>{{ form.task_id.label }}</label>
<span>{{ form.task_id.value }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.task_name.label }}</label>
<span>{{ form.task_name.value }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.status.label }}</label>
<span>{{ form.status.value }}</span>
</div>
<div class="detail-view-field">
<label>Created</label>
<span>{{ task_result.date_created }}</span>
</div>
<div class="detail-view-field">
<label>Finished</label>
<span>{{ task_result.date_done }}</span>
</div>
</div>
<div style="display: inline; width: 40%; margin: 30px; text-align: left;">
<div>
<label style="font-weight: bold; width: 100%; border-bottom: 1px solid #ccc; display: block; text-align: inherit;">{{ form.task_args.label }}</label>
<div style="display: inline-block; text-align: left;">{{ form.task_args.value }}</div>
</div>
<br />
<div>
<label style="font-weight: bold; width: 100%; border-bottom: 1px solid #ccc; display: block; text-align: inherit;">Result</label>
<div style="display: inline-block; text-align: left;"><pre style="text-align: left; max-width: 300px;">{{ task_result.result | json_pretty }}</pre></div>
</div>
</div>
</div>
<script>
// Get the element with id="defaultOpen" and click on it
document.getElementById("defaultOpen").click();
</script>
</div>
{% endblock %}

View File

@ -0,0 +1,25 @@
{% extends 'base.html.j2' %}
{% block content %}
<input type="button" value="<< Back to settings" onclick="window.location='{% url 'Settings:Settings' %}';">
<table style="max-width: 100%;">
<thead>
<th>ID</th>
<th>Name</th>
<th>Status</th>
<th>Created</th>
<th>Completed</th>
</thead>
{% for entry in task_results %}
<tr class="clicker">
<td><a href="{% url 'Settings:_task_result_view' pk=entry.id %}">{{ entry.task_id }}</a></td>
<td>{{ entry.task_name }}</td>
<td>{{ entry.status }}</td>
<td>{{ entry.date_created }}</td>
<td>{{ entry.date_done }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@ -0,0 +1,60 @@
import pytest
import unittest
from django.test import TestCase
from unittest.mock import patch
from access.models import TenancyManager
class Models:
""" Test cases for Model Abstract Classes """
def test_model_class_tenancy_manager_function_get_queryset(self):
""" Function Check
function `get_queryset()` must exist
"""
assert hasattr(self.model.objects, 'get_queryset')
assert callable(self.model.objects.get_queryset)
@patch.object(TenancyManager, 'get_queryset')
def test_model_class_tenancy_manager_function_get_queryset_called(self, get_queryset):
""" Function Check
function `access.models.TenancyManager.get_queryset()` within the Tenancy manager must
be called as this function limits queries to the current users organizations.
"""
self.model.objects.filter()
assert get_queryset.called
@pytest.mark.skip(reason="write test")
def test_model_class_tenancy_manager_results_get_queryset(self):
""" Function Results Check
function `get_queryset()` must not return data from any organization the user is
not part of.
"""
pass
@pytest.mark.skip(reason="write test")
def test_model_class_tenancy_manager_results_get_queryset_super_user(self):
""" Function Results Check
function `get_queryset()` must return un-filtered data for super-user.
"""
pass

View File

@ -0,0 +1,17 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import TenancyModel
from core.models.manufacturer import Manufacturer
class ManufacturerModel(
TestCase,
TenancyModel
):
model = Manufacturer

View File

@ -0,0 +1,29 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import PrimaryModel
class ManufacturerViews(
TestCase,
PrimaryModel
):
add_module = 'settings.views.manufacturer'
add_view = 'Add'
change_module = add_module
change_view = 'View'
delete_module = add_module
delete_view = 'Delete'
display_module = add_module
display_view = 'View'
index_module = add_module
index_view = 'Index'

View File

@ -0,0 +1,35 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import ModelDisplay
class HistoryViews(
TestCase,
ModelDisplay
):
# add_module = 'config_management.views.groups.groups'
# add_view = 'GroupAdd'
# change_module = add_module
# change_view = 'GroupView'
# delete_module = add_module
# delete_view = 'GroupDelete'
display_module = 'core.views.history'
display_view = 'View'
# index_module = add_module
# index_view = 'GroupIndexView'
@pytest.mark.skip(reason="test this models dynamic build of self.model")
def test_view_display_attribute_exists_model(self):
""" As part of display init this view dynamically builds self.model """
pass

View File

@ -1,21 +1,31 @@
from django.test import TestCase, Client
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import TenancyModel
from core.models.notes import Notes
@pytest.mark.skip(reason="to be written")
def test_note_new_correct_usercreated():
""" The user who added the note must be added to the note """
pass
class NotesModel(
TestCase,
TenancyModel
):
model = Notes
@pytest.mark.skip(reason="to be written")
def test_note_new_correct_usermodified():
""" The user who edited the note must be added to the note """
pass
@pytest.mark.skip(reason="to be written")
def test_note_new_correct_usercreated():
""" The user who added the note must be added to the note """
pass
@pytest.mark.skip(reason="to be written")
def test_note_new_correct_usermodified():
""" The user who edited the note must be added to the note """
pass

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