Compare commits

...

456 Commits

Author SHA1 Message Date
4b8353350b build: bump version 1.2.2 -> 1.3.0 2024-10-31 06:44:57 +00:00
Jon
464af55612 Merge pull request #371 from nofusscomputing/feature-production-webserver 2024-10-31 16:01:13 +09:30
Jon
9f826d7142 docs: Update release notes
ref: #371 closes #363
2024-10-31 15:50:57 +09:30
Jon
ea8a054005 fix(docker): Ensure SupervisorD daemon config directory exists.
ref: #363 #371
2024-10-31 15:19:50 +09:30
Jon
8479130ef1 feat(docker): Add worker service config for SupervisorD
ref: #363 #371
2024-10-31 15:00:55 +09:30
Jon
4303232543 refactor(docker): Switch to entrypoint
ref: #363 #371
2024-10-31 14:58:18 +09:30
Jon
0cd4a2bab4 fix(docker): use alias for static
ref: #363 #371
2024-10-30 04:07:58 +09:30
Jon
0b4fc25462 fix(access): testing of param causing gunicorn to fail
ref: #363 #371
2024-10-30 02:39:42 +09:30
Jon
f17d74f8dc fix(docker): place nginx conf in correct path
ref: #363 #371
2024-10-30 02:39:03 +09:30
Jon
07be745bbe fix(docker): gunicorn must call method
ref: #363 #371
2024-10-30 02:37:49 +09:30
Jon
510ab69af8 fix(docker): Ensure NginX config applied after it's installed
ref: #363 #371
2024-10-30 01:50:41 +09:30
Jon
b9349e6590 fix(docker): Add proxy params for NginX
ref: #363 #371
2024-10-30 01:24:35 +09:30
Jon
4fd3abb9d6 fix(docker): Make centurion the default nginx conf
ref: #363 #371
2024-10-30 01:24:13 +09:30
Jon
ac562e7490 feat(docker): ensure supervisor starts
ref: #363 #371
2024-10-30 00:57:02 +09:30
Jon
4fe5916a76 feat(docker): use correct file location for nginx config
ref: #363 #370 #371
2024-10-30 00:39:03 +09:30
Jon
b858825838 feat(docker): Fail the build if django is not found
ref: #363 #370 #371
2024-10-30 00:31:10 +09:30
Jon
5d92a3315f fix(docker): Correct NginX start command
ref: #363 #371
2024-10-30 00:30:34 +09:30
Jon
7a0f85c556 feat(docker): Install NginX to serve site
ref: #363 #371
2024-10-30 00:12:20 +09:30
Jon
cfa284d4ad feat(docker): Add supervisord for install
ref: #363 #371
2024-10-30 00:10:10 +09:30
Jon
447e985740 feat(docker): Add gunicorn for install
ref: #363 #371
2024-10-30 00:08:54 +09:30
Jon
7d872b97f2 feat: update docker image alpine 3.19 ->3.20
ref: #363 #371
2024-10-30 00:06:57 +09:30
ca3b99cb3a build: bump version 1.2.1 -> 1.2.2 2024-10-29 13:00:55 +00:00
Jon
8d4f686f6c Merge pull request #369 from nofusscomputing/fix-docker-container 2024-10-29 22:23:56 +09:30
Jon
6f57bd84e7 fix(docker): adjust pyyaml to >-6.0.1
no python modules are being installed

ref: #369
2024-10-29 19:05:39 +09:30
6351acabe5 build: bump version 1.2.0 -> 1.2.1 2024-10-22 09:16:31 +00:00
Jon
9d6ea1d7c3 refactor(project_management): dont order queryset for project
this is done at modelMeta

ref: #358
2024-10-22 18:36:21 +09:30
Jon
035c6ed60c fix(project_management): Ensure user cant see projects for organizations they are apart of
ref: #358
2024-10-22 18:31:21 +09:30
31e88b2f96 build: bump version 1.1.0 -> 1.2.0 2024-10-11 14:59:15 +00:00
Jon
28f51d3bb6 Merge pull request #259 from nofusscomputing/feature-v1-2 2024-10-12 00:11:22 +09:30
Jon
c634695e4e feat: update django 5.0.8 -> 5.1.2
ref: #259
2024-10-11 23:56:10 +09:30
Jon
983921fc22 chore: squash migrations
squash all to limit number of migrations required

ref: #259
2024-10-11 23:41:03 +09:30
Jon
9fc5b0eb09 Merge pull request #341 from nofusscomputing/340-project-name-length 2024-10-05 12:23:15 +09:30
Jon
b1fc8e0f98 feat(settings): Add API filter and search
ref: #341
2024-10-05 11:42:39 +09:30
Jon
551473feb7 feat(core): Add API filter of fields external_system and external_ref for projects
ref: #341
2024-10-05 11:42:39 +09:30
Jon
105cb63d71 feat(core): Add API filter of fields external_system and external_ref to tickets
ref: #341
2024-10-05 11:42:39 +09:30
Jon
1dda4a9fb5 feat(project_management): increase project field length 50 -> 100 chars
ref: #341 closes #340
2024-10-05 11:42:39 +09:30
Jon
c53ec9ec5d Merge pull request #339 from nofusscomputing/321-ticket-field-length 2024-09-30 13:20:05 +09:30
Jon
a44b2479e3 feat(core): increase ticket title field length 50 -> 100 chars
ref: #339  closes #321
2024-09-30 13:09:02 +09:30
Jon
ec26e16132 Merge pull request #309 from nofusscomputing/ticket-work 2024-09-22 12:28:09 +09:30
Jon
59a930f934 feat(core): Add ability track ticket estimation time for completion
ref: #296 #309 #312
2024-09-21 15:48:06 +09:30
Jon
c7701bb2df feat(core): Add ability to delete a ticket
ref: #296 #309
2024-09-21 14:31:07 +09:30
Jon
4ac0da6ba2 fix: ensure model mandatory fields don't specify a default value
ref: #309 fixes #306
2024-09-21 14:03:47 +09:30
Jon
19ad262617 feat(core): [Templating Engine] Add template tag concat_strings
ref: #296 #309
2024-09-21 13:13:07 +09:30
Jon
0e987088a3 feat(itim): Add ticket tab to services
shows related tickets

ref: #296 #309
2024-09-21 13:12:28 +09:30
Jon
c74b89e0d6 feat(itim): Add ticket tab to clusters
shows related tickets

ref: #296 #309
2024-09-21 13:12:16 +09:30
Jon
e762713416 feat(itam): Add ticket tab to software
shows related tickets

ref: #296 #309
2024-09-21 13:12:01 +09:30
Jon
facdd0111b feat(itam): Add ticket tab to operating systems
shows related tickets

ref: #296 #309
2024-09-21 13:11:48 +09:30
Jon
280abb8841 feat(itam): Add ticket tab to devices
shows related tickets

ref: #296 #309
2024-09-21 13:11:24 +09:30
Jon
064f74736f feat(config_management): Add ticket tab to conf groups
ref: #296 #309
2024-09-21 13:10:36 +09:30
Jon
d26868eced refactor(core): Ticket Linked ref render as template
ref: #296 #309
2024-09-21 12:28:00 +09:30
Jon
b5ec42fc56 fix(api): Ensure user is set to current user for ticket comment
ref: #296 #309
2024-09-21 12:01:58 +09:30
Jon
104575780a test(core): Ticket Linked item view checks
ref: #296 #309
2024-09-21 11:45:24 +09:30
Jon
1c1f4ecdfa test(core): Ticket Linked item permission checks
ref: #296 #309
2024-09-21 11:45:13 +09:30
Jon
582ee4031d docs(core): correct typos
ref: #309
2024-09-21 10:57:24 +09:30
Jon
fca8ad5a78 Merge pull request #308 from nofusscomputing/linked-item 2024-09-20 17:22:46 +09:30
Jon
fff3a96889 fix(core): remove org field when editing a ticket
ref: #308
2024-09-20 16:57:53 +09:30
Jon
dfdc5bac9d feat(core): Add slash command link for linking items to tickets
ref: #296 #308
2024-09-20 16:57:34 +09:30
Jon
c022551427 feat(core): Add to markdown rendering model references
ref: #296 #308
2024-09-20 15:26:38 +09:30
Jon
76954c019b feat(core): Ability to link items to all ticket types
ref: #296 #308
2024-09-20 15:25:42 +09:30
Jon
c3de79050e feat(core): add model ticket linked items
ref: #296 #308
2024-09-20 12:39:54 +09:30
Jon
3cce436938 Merge pull request #301 from nofusscomputing/293-missing-project-fields 2024-09-18 17:03:21 +09:30
Jon
ed2bf96626 chore(core): during validation, Catch random errors and show user stack trace
ref: #301 #305
2024-09-18 16:59:17 +09:30
Jon
3693ddadad fix(core): during validation, if subscribed users not specified, use empty list
ref: #301 closes #305
2024-09-18 16:58:42 +09:30
Jon
8866f94fab docs(project_management): interim project pages
ref: #295 #300
2024-09-18 16:43:25 +09:30
Jon
0ae1395d92 test(project_management): Project Milestone api permission checks
ref: #301 closes #285 closes #295
2024-09-18 15:19:08 +09:30
Jon
cf73323dd3 feat(project_management): Add project milestones api endpoint
ref: #285 #301
2024-09-18 14:53:33 +09:30
Jon
f019c50e44 feat(project_management): Add import_project permission and add api serializer
ref: #295 #301
2024-09-18 13:20:44 +09:30
Jon
3e56c9861c feat(core): great odins beard, remove the checkbox formatting
ref: #301
2024-09-18 12:36:58 +09:30
Jon
d6f475a009 feat(project_management): Add field is_deleted to projects
preparation for the purge permission.

ref: #301 closes #293
2024-09-18 12:30:18 +09:30
Jon
b2f20766de feat(project_management): Calculate project completion percentage and display
ref: #293 #301
2024-09-18 12:16:22 +09:30
Jon
81e98cfd6f Merge pull request #300 from nofusscomputing/295-project-work 2024-09-18 00:57:59 +09:30
Jon
8cae85badc fix(core): add missing pagination to ticket comment categories index
ref: #300 fixes #291
2024-09-18 00:52:11 +09:30
Jon
d6e0fd0b46 fix(core): add missing pagination to ticket categories index
ref: #300 fixes #289
2024-09-18 00:51:48 +09:30
Jon
8d7d79c29b feat(core): order project categories with parent name if applicable
ref: #300 closes #290
2024-09-18 00:46:53 +09:30
Jon
92d3692f85 fix(project_management): Ensure project type and state show on index page
ref: #295 #300
2024-09-18 00:20:07 +09:30
Jon
7f094a9f96 test(project_management): Project TYpe tenancy model checks
ref: #295 #300 closes #294
2024-09-18 00:11:17 +09:30
Jon
7c9d6ced6a test(project_management): Project Type view checks
ref: #294 #295 #300
2024-09-18 00:10:36 +09:30
Jon
9b747b08da test(project_management): Project Type permission checks
ref: #294 #295 #300
2024-09-18 00:10:18 +09:30
Jon
f4695bad1e test(project_management): Project Type core history checks
ref: #294 #295 #300
2024-09-18 00:10:01 +09:30
Jon
744f2f380f test(project_management): Project Type tenancy object checks
ref: #294 #295 #300
2024-09-18 00:09:41 +09:30
Jon
22c6b9d3fe feat(project_management): Add Project Type to the UI
ref: #294 #295 #300
2024-09-18 00:05:15 +09:30
Jon
c1aeb3a258 chore(project_management): cleanup Project State
ref: #294 #295 #300
2024-09-17 23:56:47 +09:30
Jon
0eb6a8bde9 test(project_management): Project State permission checks
ref: #294 #295 #300
2024-09-17 17:21:58 +09:30
Jon
477f089de3 test(project_management): Project State tenancy model checks
ref: #294 #295 #300
2024-09-17 17:21:26 +09:30
Jon
64faffa7b6 test(project_management): Project State view checks
ref: #294 #295 #300
2024-09-17 17:21:16 +09:30
Jon
2196db9479 test(project_management): Project State core history checks
ref: #294 #295 #300
2024-09-17 17:20:59 +09:30
Jon
14d949228d test(project_management): Project State tenancy object checks
ref: #294 #295 #300
2024-09-17 17:20:44 +09:30
Jon
f8e96a556d feat(project_management): Add Project State to the UI
ref: #294 #295 #300
2024-09-17 17:20:00 +09:30
Jon
5ad974f947 revert(core): revert ticket class defined in dir init
reef: #300
2024-09-17 17:14:11 +09:30
Jon
9c9009ff52 test(project_management): Project type API permission checks
ref: #294 #295 #300
2024-09-17 14:09:12 +09:30
Jon
bd09412f6f test(project_management): Project state API permission checks
ref: #294 #295 #300
2024-09-17 14:05:15 +09:30
Jon
82d48fe27b Merge pull request #299 from nofusscomputing/294-feat-project-state-type 2024-09-17 13:54:57 +09:30
Jon
1315cc584b feat(project_management): add priority field to project model, form and api endpoint
ref: #293 #299
2024-09-17 13:50:06 +09:30
Jon
17bed1ef7a Merge pull request #298 from nofusscomputing/294-feat-project-state-type 2024-09-17 13:26:34 +09:30
Jon
f0dd7bc256 feat(project_management): add organization field to project form and api endpoint
ref: #293 #298
2024-09-17 13:21:08 +09:30
Jon
5b356c3c11 feat(project_management): add project_type field to project form
ref: #294 #298
2024-09-17 13:14:53 +09:30
Jon
fbbae64ff2 feat(project_management): add external_ref and external_system field to project model
ref: #294 #298
2024-09-17 13:12:54 +09:30
Jon
34b85441e2 feat(project_management): add project type field to project model
ref: #294 #298
2024-09-17 13:12:26 +09:30
Jon
e9b122cf8c feat(project_management): add project type api endpoint
ref: #294 #298
2024-09-17 13:07:50 +09:30
Jon
3f0853654e feat(project_management): new model project type
ref: #294 #298
2024-09-17 13:06:15 +09:30
Jon
0de451af70 feat(project_management): add project state api endpoint
ref: #294 #298
2024-09-17 12:50:43 +09:30
Jon
a4926f8a0d feat(project_management): add project state field to project model
ref: #294 #298
2024-09-17 12:49:53 +09:30
Jon
1314f9a1ff feat(project_managemenet): new model project state
ref: #294 #298
2024-09-17 12:46:48 +09:30
Jon
d5e344f67c Merge pull request #292 from nofusscomputing/project-milestone 2024-09-16 16:27:54 +09:30
Jon
d8654fae6d test(project_management): Project miletone skipped api checks
ref: #285 #292
2024-09-16 16:22:14 +09:30
Jon
033f47b6b9 feat(project_management): add field external system to projects
ref: #292
2024-09-16 16:05:37 +09:30
Jon
383bca4ff9 refactor(core): migrate ticket enums to own class
ref: #292
2024-09-16 15:59:54 +09:30
Jon
22f7b1e7c5 fix(core): Add replacement function within ticket validation as cleaned_data attribute replacement
ref: #292
2024-09-16 13:57:57 +09:30
Jon
d5ad03546c fix(core): Ensure the ticket clears project field on project removal
ref: #292
2024-09-16 12:48:32 +09:30
Jon
f79076ddef fix(core): Remove ticket fields user has no access to
ref: #292
2024-09-16 12:42:37 +09:30
Jon
7a31498e91 refactor(core): Ticket validation errors setup for both api and ui
ref: #285 #292
2024-09-16 12:42:00 +09:30
Jon
95f9d90877 feat(core): validate field milestone for all ticket types
ref: #285 #292
2024-09-16 12:41:04 +09:30
Jon
3bac0c19ac feat(core): Add field milestone to all ticket types
ref: #292
2024-09-16 03:50:18 +09:30
Jon
b8f4123185 refactor(core): for tickets use validation for organization field
ref: #292
2024-09-16 03:49:05 +09:30
Jon
d2e9c838de refactor(core): refine ticket field permission and validation
ref: #292
2024-09-16 03:45:38 +09:30
Jon
5cd51ba00e test(project_management): Project Milestone tenancy model checks
ref: #285 #292
2024-09-14 16:12:56 +09:30
Jon
a3fafdafbd test(project_management): Project Milestone view checks
ref: #285 #292
2024-09-14 16:12:31 +09:30
Jon
df9ad069c4 test(project_management): Project Milestone ui permission checks
ref: #285 #292
2024-09-14 16:12:04 +09:30
Jon
2ff3dab014 test(project_management): Project Milestone core history checks
ref: #285 #292
2024-09-14 16:11:39 +09:30
Jon
ae9526ef57 test(project_management): Project Milestone Tenancy object checks
ref: #285 #292
2024-09-14 16:11:28 +09:30
Jon
5e235617e0 feat(project_management): Add project milestones
ref: #285 #292
2024-09-14 16:09:07 +09:30
Jon
a373247cda Merge pull request #284 from nofusscomputing/ticket-categories 2024-09-14 13:26:40 +09:30
Jon
c81e319aac feat(core): Add slash command "related ticket" for ticket and ticket comments
ref: #284 closes #287
2024-09-14 13:19:39 +09:30
Jon
d05537a619 fix(core): correct logic for slash command /spend
ref: #284 c#286
2024-09-14 11:20:47 +09:30
Jon
7894ac5522 docs(core): Add slash command /spend for ticket and ticket comments
ref: #284 closes #286
2024-09-13 22:27:48 +09:30
Jon
64577cf806 feat(core): Suffix username to action comments
ref: #250 #284
2024-09-13 21:59:04 +09:30
Jon
c3307152e8 feat(core): Add slash command /spend for ticket and ticket comments
ref: #284 closes #286
2024-09-13 21:58:19 +09:30
Jon
a7e99eb5b4 refactor: reduce action comment spacing
ref: #24 #284
2024-09-13 21:07:35 +09:30
Jon
c45aae7048 chore: docs linting errors
ref: #284
2024-09-13 16:22:43 +09:30
Jon
9cb3afeb30 feat(core): Disable HTML tag rendering for markdown
ref: #284 closes #271
2024-09-13 16:19:42 +09:30
Jon
6e7e6587c2 docs: update roadmap
ref: #284
2024-09-13 15:01:41 +09:30
Jon
5f3c7296b7 feat(project_management): remove requirement for code field to be populated
ref: #14 #284
2024-09-13 14:39:50 +09:30
Jon
2e15e61059 fix(project_management): correct project view permissions
ref: #14 #284
2024-09-13 14:35:37 +09:30
Jon
574357b60a test(core): Project tenancy model checks
ref: #14 #284
2024-09-13 14:35:09 +09:30
Jon
1576605acb test(core): Project view checks
ref: #14 #284
2024-09-13 14:34:57 +09:30
Jon
9d564ffbb2 test(core): Project UI permission checks
ref: #14 #284
2024-09-13 14:34:47 +09:30
Jon
b56f3236fd test(core): Project API permission checks
ref: #14 #284
2024-09-13 14:34:38 +09:30
Jon
6e566b8840 test(core): Project history checks
ref: #14 #284
2024-09-13 14:34:28 +09:30
Jon
34a1a19089 test(core): Project Tenancy object checks
ref: #14 #284
2024-09-13 14:34:19 +09:30
Jon
80dc797651 test(core): Ticket comment category API permission checks
ref: #284 closes #283
2024-09-13 13:59:11 +09:30
Jon
f2a4223d25 feat(core): Add ticket comment category API endpoint
ref: #283 #284
2024-09-13 13:54:42 +09:30
Jon
902aaf31dd fix(core): Correct view permissions for ticket comment category
ref: #283 #284
2024-09-13 13:41:20 +09:30
Jon
1be23148d7 test(core): add missing ticket category view checks
ref: #283 #284
2024-09-13 13:40:57 +09:30
Jon
88d6a73454 test(core): ticket comment category tenancy model checks
ref: #283 #284
2024-09-13 13:40:26 +09:30
Jon
01c57b37ad test(core): ticket comment category view checks
ref: #283 #284
2024-09-13 13:40:03 +09:30
Jon
9fbb88fa5f test(core): ticket comment category ui permission checks
ref: #283 #284
2024-09-13 13:38:15 +09:30
Jon
a0b0d79777 test(core): ticket comment category history checks
ref: #283 #284
2024-09-13 13:38:03 +09:30
Jon
f3dccd3b84 test(core): ticket comment category tenancy model checks
ref: #283 #284
2024-09-13 13:37:49 +09:30
Jon
56b715797e feat(core): Ability to assign categories to ticket comments
ref: #14 #96 #93 #95 #90 #283 #283 #284
2024-09-13 13:30:36 +09:30
Jon
11948c9500 feat(core): Add ticket comment categories
ref: #14 #96 #93 #95 #90 #283 #284
2024-09-13 13:14:25 +09:30
Jon
1161bf79aa fix(core): correct url typo for ticket category API endpoint
ref: #283 #284
2024-09-13 12:45:33 +09:30
Jon
40f564b32a fix(core): dont attempt to modify field for ticket category API list
ref: #283 #284
2024-09-13 12:45:13 +09:30
Jon
4fdabc16ba test(core): ticket category API permission checks
ref: #283 #284
2024-09-13 12:38:26 +09:30
Jon
e68dbdfb4c test(core): ticket category history checks
ref: #283 #284
2024-09-13 12:38:14 +09:30
Jon
f2898037b0 test(core): ticket category tenancy model checks
ref: #283 #284
2024-09-13 12:37:48 +09:30
Jon
5d116c7224 feat(core): Extend all ticket endpoints to contain ticket categories
ref: #14 #96 #93 #95 #90 #283 #284
2024-09-13 12:25:30 +09:30
Jon
2a31815267 feat(core): Add ticket category API endpoint
ref: #14 #96 #93 #95 #90 #283 #284
2024-09-13 12:24:36 +09:30
Jon
ded6a72072 test(core): ticket category model checks
ref: #14 #96 #93 #95 #90 #283 #284
2024-09-13 11:26:50 +09:30
Jon
7d80857d8d test(core): view checks
ref: #14 #96 #93 #95 #90 #283 #284
2024-09-13 11:26:27 +09:30
Jon
297e318243 test(core): ui permissions
ref: #14 #96 #93 #95 #90 #283 #284
2024-09-13 11:26:14 +09:30
Jon
6cc992f6d6 fix(core): Dont attempt to render ticket category if none
ref: #14 #96 #93 #95 #90 #283 #284
2024-09-13 11:25:37 +09:30
Jon
6402897329 fix(core): Correct the delete permission
ref: #14 #96 #93 #95 #90 #283 #284
2024-09-13 11:25:14 +09:30
Jon
5f7d0e474e feat(core): Ability to assign categories to tickets
ref: #14 #96 #93 #95 #90 #283 #284
2024-09-13 11:03:40 +09:30
Jon
09bb2d8e27 feat(core): Addpage titles to view abstract classes
ref: #283
2024-09-13 10:43:04 +09:30
Jon
e28dbea05b feat(core): Add ticket categories
ref: #283 #284
2024-09-13 10:42:16 +09:30
Jon
9d8c894cff Merge pull request #270 from nofusscomputing/feat-2024-09-11 2024-09-12 18:41:47 +09:30
Jon
9942348ba3 docs(development): add markdown refs
ref: #14 #96 #93 #95 #90 #270 closes #250
2024-09-12 17:58:55 +09:30
Jon
2e98eda8a4 fix(core): correct project task reply link for comments
ref: #14 #96 #93 #95 #90 #250 #270
2024-09-12 17:21:43 +09:30
Jon
2ab2b65fc2 refactor(core): update markdown styles
ref: #270 #271
2024-09-12 17:09:39 +09:30
Jon
3e684b117f refactor(core): migrate ticket number rendering as markdown_it plugin
ref: #270 #271
2024-09-12 16:28:27 +09:30
Jon
51f28a6cf8 refactor(core): move markdown functions out of ticket model
ref: #270 ##271
2024-09-12 16:16:29 +09:30
Jon
212e864db1 feat(core): during markdown render, if ticket ID not found return the tag
ref: #270
2024-09-12 12:40:46 +09:30
Jon
0adfd95ced fea(core): Add opened_by user as subscribed to ticket when creating
ref: #14 #96 #93 #95 #90 #250 #270
2024-09-12 12:40:35 +09:30
Jon
c9d05152c9 feat(core): Add heading anchor plugin to markdown
ref: #270
2024-09-12 00:49:26 +09:30
Jon
a8b21d7c74 feat(core): correct markdown formatting for KB articles
ref: #270
2024-09-12 00:04:51 +09:30
Jon
97874b73f6 docs: correct date
ref: #270
2024-09-11 22:51:58 +09:30
Jon
948713d13d test(core): correct project tests for triage user
ref: #14 #96 #93 #95 #90 #250 #270
2024-09-11 22:45:58 +09:30
Jon
63146aa41c feat(core): remove project field from being editable when creating project task
ref: #14 #270
2024-09-11 22:31:55 +09:30
Jon
c7f69ad7c1 feat(core): Add admonition style
ref: #14 #96 #93 #95 #90 #250 #270 closes #272
2024-09-11 22:19:23 +09:30
Jon
008f8c1554 feat(project_management): Validate project task has project set
ref: #14 #96 #93 #95 #90 #250 #270
2024-09-11 21:20:59 +09:30
Jon
122216dbe4 chore(core): remove unused markdown import
ref: #14 #96 #93 #95 #90 #250 #270
2024-09-11 21:15:21 +09:30
Jon
c0ac09b928 fix(core): correct project task comment buttons
ref: #14 #96 #93 #95 #90 #250 #270
2024-09-11 20:16:05 +09:30
Jon
cfda7e5e1e feat(core): set project ID to match url kwarg
ref: #14 #96 #93 #95 #90 #250 #270
2024-09-11 20:15:37 +09:30
Jon
91af43adba feat(core): Add action comment on title change
ref: #14 #96 #93 #95 #90 #250 #270
2024-09-11 19:14:44 +09:30
Jon
bfb7176db3 feat(core): Add task listts plugin to markdowm
ref: #14 #96 #93 #95 #90 #250 #270
2024-09-11 19:14:22 +09:30
Jon
411cd5d4a3 feat(core): Add footnote plugin to markdowm
ref: #14 #96 #93 #95 #90 #250 #270
2024-09-11 19:14:07 +09:30
Jon
91aa87d122 feat(core): Add admonition plugin to markdowm
ref: #14 #96 #93 #95 #90 #250 #270
2024-09-11 19:13:58 +09:30
Jon
14bdc67a4a feat(core): Add table extension to markdowm
ref: #14 #96 #93 #95 #90 #250 #270
2024-09-11 19:13:35 +09:30
Jon
b86b1fd1ad feat(core): Add strikethrough extension to markdowm
ref: #14 #96 #93 #95 #90 #250 #270
2024-09-11 19:13:25 +09:30
Jon
00ec5179f9 feat(core): Add linkify extension to markdowm
ref: #14 #96 #93 #95 #90 #250 #270
2024-09-11 19:13:02 +09:30
Jon
200c9d8d8d feat(core): move markdown parser py-markdown -> markdown-it
py-markdown was missing a lot of the common/gfm items.

ref: #14 #96 #93 #95 #90 #250 #270
2024-09-11 19:12:05 +09:30
Jon
b69d210759 chore(core): remove superuser clause
ref: #250 #96 #93 #95 #90 #270
2024-09-11 15:26:06 +09:30
Jon
eb4a58ed01 fix(project_management): correct comment reply url name
ref: #14 #270
2024-09-11 15:21:55 +09:30
Jon
bc2f30ac9b feat(core): Add organization column to ticket pages
ref: #250 #96 #93 #95 #90 #270
2024-09-11 15:21:19 +09:30
Jon
e87bbe9ed8 fix(core): Generate the correct edit url for tickets
ref: #250 #96 #93 #95 #90 #270
2024-09-11 15:20:54 +09:30
Jon
68785ef6c0 fix(core): Generate the correct comment urls for tickets
ref: #250 #96 #93 #95 #90 #270
2024-09-11 15:20:21 +09:30
Jon
b78e2adb09 fix(core): Redirect to correct url for itim tickets after adding comment
ref: #250 #96 #93 #95 #90 #270
2024-09-11 15:19:03 +09:30
Jon
26c985e683 feat(core): Allow super-user to edit ticket comment source
ref: #250 #96 #93 #95 #90 #270
2024-09-11 14:07:59 +09:30
Jon
58e2b9f7f5 feat(core): Render linked tickets the same as the rendered markdown link
ref: #250 #96 #93 #95 #90 #270
2024-09-11 13:50:17 +09:30
Jon
34f2d4c4d4 chore(core): remove model history link
not required as the history is saved as action comments

ref: #250 #96 #93 #95 #90 #270
2024-09-11 13:18:27 +09:30
Jon
56c3b9d7de feat(core): Add project task link for related project task
ref: #250 #96 #93 #95 #90 #270
2024-09-11 13:14:07 +09:30
Jon
c83ffe542e fix(core): correct linked tickets hyperlink address
ref: #250 #96 #93 #95 #90 #270
2024-09-11 13:09:24 +09:30
Jon
b07872c8c2 feat(project_management): Add project duration field
ref: #14 #270
2024-09-11 12:50:31 +09:30
Jon
1cc196fd06 feat(core): Add external ref to tickets if populated
ref: #250 #96 #93 #95 #90 #270
2024-09-11 12:36:47 +09:30
Jon
dd68bfbea8 refactor(core): Adjust test layout for itsm and project field based permissions
ref: #250 #96 #93 #95 #90 #264 #268
2024-09-11 12:18:24 +09:30
Jon
ea5888f39f Merge pull request #267 from nofusscomputing/feat-ticket-comments 2024-09-10 17:11:33 +09:30
Jon
78607a0bf9 test(core): Project task permission checks
ref: #250 #96 #93 #95 #90 #264 #267
2024-09-10 17:01:57 +09:30
Jon
fa9cff390a feat(core): Add project task permissions
ref: #250 #96 #93 #95 #90 #264 #267 fixes #269
2024-09-10 17:01:24 +09:30
Jon
2613a132a6 fix(core): order ticket comments by creation date
oldest first..

ref: #250 #96 #93 #95 #90 #264 #267 fixes #269
2024-09-10 16:26:04 +09:30
Jon
7d8b54a980 fix(core): Ensure for both ticket and comment, external details are unique.
ref: #250 #96 #93 #95 #90 #264 #267 fixes #268
2024-09-10 16:09:44 +09:30
Jon
6371fa03a1 chore(project_management): remove non-ticket based project tasks
project tasks scope was moved to a type of ticket.

ref: #14 #250 #267
2024-09-10 15:52:34 +09:30
Jon
daa872d2e7 feat(project_management): Add project tasks
ref: #14 #250 #267
2024-09-10 15:38:30 +09:30
Jon
4d1600e396 refactor(project_management): migrate projects to new style for views
ref: #14 #267
2024-09-10 15:27:05 +09:30
Jon
63d33c287c feat(api): Add project tasks endpoint
ref: #14 #267
2024-09-10 13:42:03 +09:30
Jon
ae72d4ab6a feat(api): Add projects endpoint
ref: #14 #267
2024-09-10 13:37:13 +09:30
Jon
8a747d1d1f feat(api): Add project management endpoint
ref: #14 #267
2024-09-10 13:33:21 +09:30
Jon
55e512efb8 test(core): Ticket comment API permission checks
ref: #250 #96 #93 #95 #90 #264 #267
2024-09-10 12:16:16 +09:30
Jon
f09e7b77db fix(core): Ensure on ticket comment create and update a response is returned
ref: #250 #96 #93 #95 #90 #264 #267
2024-09-10 12:16:03 +09:30
Jon
4177f71972 test(core): Ticket comment permission checks
ref: #250 #96 #93 #95 #90 #264 #267
2024-09-10 11:42:10 +09:30
Jon
0c3e38c543 test(core): Ticket comment Views
ref: #250 #96 #93 #95 #90 #264 #267
2024-09-10 10:58:54 +09:30
Jon
94dd555e9b test(core): Tenancy model tests for ticket comment
ref: #250 #96 #93 #95 #90 #264 #267
2024-09-10 10:53:31 +09:30
Jon
10bffe0f0f Merge pull request #266 from nofusscomputing/feat-tickets 2024-09-09 18:00:35 +09:30
Jon
aa6baf94a6 feat(core): support negative numbers when Calculating ticket duration for ticket meta and comments
enables time to be subtracted when negative value added to duration field.

ref: #250 #96 #93 #95 #90 #264 #266
2024-09-09 17:59:22 +09:30
Jon
44604d98ab feat(core): Caclulate ticket duration for ticket meta and comments
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-09 17:36:17 +09:30
Jon
63077dfa26 feat(core): Add edit details to ticket and comments
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-09 16:39:11 +09:30
Jon
0794e5b58f test(core): ensure history for ticket models is not saved
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-09 16:13:43 +09:30
Jon
c67e1430bd feat(core): Don't save model history for ticket models
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-09 16:13:22 +09:30
Jon
f3b249d18f test: Ensure tenancy models save model history
ref: #266 #250
2024-09-09 16:11:40 +09:30
Jon
857b8781cb feat(core): add option to allow the prevention of history saving for tenancy models
ref: #266 #250
2024-09-09 16:10:55 +09:30
Jon
118d41a53b test(core): remove duplicated tenancy object tests for ticket model
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-09 16:08:16 +09:30
Jon
2cb21ae4a7 test(core): correct triage user test names for allowed field permissions
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-09 16:07:39 +09:30
Jon
7e0bd630b5 test(core): project field permission check for triage user
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-09 15:39:37 +09:30
Jon
a57e977131 feat(core): Add project field to tickets allowed fields
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-09 15:38:41 +09:30
Jon
082a351c17 test(core): Ticket Action comment checks for related tickets
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-09 14:54:30 +09:30
Jon
a47e1977f0 fix(core): Ensure related tricket action comment is trimmed
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-09 14:52:39 +09:30
Jon
b0a4d2ca84 test(core): Ticket Action comment checks for subscribing team
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-09 14:18:48 +09:30
Jon
afceaca736 test(core): Ticket Action comment checks for subscribing user
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-09 14:18:22 +09:30
Jon
c59dc7d2bf test(core): Ticket Action comment checks for unassigning team
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-09 14:06:45 +09:30
Jon
e7015570d5 test(core): Ticket Action comment checks for assigning team
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-09 14:06:17 +09:30
Jon
a68a9e7ef3 fix(core): Team assigned to ticket status update
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-09 14:05:39 +09:30
Jon
3ea84f008b test(core): Ticket Action comment checks for un-assigning user
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-09 13:48:09 +09:30
Jon
47aeac846b test(core): Ticket Action comment checks for assigning user
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-09 13:47:58 +09:30
Jon
69124cff08 chore(core): Remove field '_django_version' from history save
must have been introduced in django 5.0.8

ref: #250 #96 #93 #95 #90 #264 #266
2024-09-09 13:44:59 +09:30
Jon
a99c1bb418 refactor(core): REmove constraint on setting user for ticket comment
required so that tests can run. ToDo: add tests to ensure that user is set.

ref: #250 #96 #93 #95 #90 #264 #266
2024-09-09 13:44:04 +09:30
Jon
b80ca93ced test(core): Add ticket project field permission check
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-09 12:13:05 +09:30
Jon
8998292a0f fix(api): ensure ticket_type is set from view var
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-09 12:02:36 +09:30
Jon
bc39b1b8b5 test(core): ensure ticket_type tests dont have change value that matches ticket type
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-08 18:17:24 +09:30
Jon
b93d3d2175 chore: add test to makefile
ref: #266
2024-09-08 18:10:54 +09:30
Jon
b1277c98ab fix(core): Add ticket fields to ticket types
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-08 18:10:31 +09:30
Jon
c2eaf120b6 fix(core): During ticket form validation confirm if value specified/different then default
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-08 18:09:35 +09:30
Jon
41158e495f fix(core): Correctly set the ticket type initial value
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-08 18:05:17 +09:30
Jon
3261342c4f test(core): field based permission tests for add, change, import and triage user
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-08 16:46:17 +09:30
Jon
8b4068ac7e fix(core): prevent import user from having permssions within UI
only allow import user to have API permissions.

ref: #250 #96 #93 #95 #90 #264 #266
2024-09-08 16:41:43 +09:30
Jon
27958f5e7a refactor(core): cache fields allowed during ticket validation
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-08 16:41:31 +09:30
Jon
819dc01451 refactor(core): dont require specifying ticket status
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-08 13:13:36 +09:30
Jon
8277e05205 feat(core): Update ticket status when assigned/unassigned users/teams
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-07 14:33:01 +09:30
Jon
f4d96c78e7 feat(core): Create action comment for subscribed users/teams
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-07 14:32:24 +09:30
Jon
a08d74cd3c feat(core): Create action comment for assigned users/teams
ref: #250 #96 #93 #95 #90 #264 #266
2024-09-07 14:30:22 +09:30
Jon
878d2509cd Merge pull request #265 from nofusscomputing/264-api-refactor-tickets 2024-09-06 18:12:00 +09:30
Jon
09247246bb fix(api): correct ticket view links
ref: #250 #96 #93 #95 #90 #264 #265
2024-09-06 18:01:22 +09:30
Jon
685b8266e4 feat(core): adding of more ticket status icons
ref: #250 #96 #93 #95 #90 #264 #265
2024-09-06 17:58:14 +09:30
Jon
7a2f7fdf3d refactor(core): move id to end for rendered ticket link.
ref: #250 #96 #93 #95 #90 #264 #265
2024-09-06 17:20:32 +09:30
Jon
ecaa24192f fix(core): Correct display of ticket status within ticket interface
ref: #250 #96 #93 #95 #90 #264 #265
2024-09-06 17:10:06 +09:30
Jon
f49cc9c286 refactor(api): Ticket (change, incident, problem and request) to static api endpoints
ref: #250 #96 #93 #95 #90 #264 #265
2024-09-06 16:45:26 +09:30
Jon
53ae19eda8 test(api): Ticket (change, incident, problem and request) api permission checks
ref: #250 #96 #93 #95 #90 #264 #265
2024-09-06 10:26:55 +09:30
Jon
d8361bf741 feat(api): Ticket endpoint dynamic permissions
ref: #250 #96 #93 #95 #90 #264 #265
2024-09-06 10:26:16 +09:30
Jon
d70f04c63d refactor(api): make ticket status field mandatory
ref: #250 #96 #93 #95 #90 #264 #265
2024-09-06 10:25:47 +09:30
Jon
0f4b9fef9e refactor(api): Move core tickets to own ticket endpoints
require so that permissions can be dynamic

ref: #250 #96 #93 #95 #90 #265 closes #264
2024-09-04 11:59:38 +09:30
Jon
05c18702ff Merge pull request #263 from nofusscomputing/250-ticket-tests 2024-09-03 17:51:15 +09:30
Jon
754c311580 feat(core): add ticket status badge
ref: #250 #96 #93 #95 #90 #263
2024-09-03 17:50:41 +09:30
Jon
f6dd5a3156 chore: remove empty settings model
ref: #263
2024-09-03 17:00:46 +09:30
Jon
53489ec43b feat(access): add ability to fetch dynamic permissions
ref: #250 #96 #93 #95 #90 #263
2024-09-03 17:00:22 +09:30
Jon
da8d97a274 test(core): interim ticket unit tests
ref: #250 #96 #93 #95 #90 #263
2024-09-03 16:59:24 +09:30
Jon
8161d67a1f test(itam): Ensure if an attempt to add an existing device via API, it's not recreated and is returned.
ref: #262 #263
2024-09-03 15:22:35 +09:30
Jon
381d59c18f refactor(core): During form validation for a ticket, use defaults if not defined for mandatory fields
ref: #250 #96 #93 #95 #90 #263
2024-09-03 14:56:36 +09:30
Jon
55a40fcf4d refactor(core): Ticket form ticket_type to use class var
ref: #250 #96 #93 #95 #90 #263
2024-09-03 14:56:01 +09:30
Jon
cfc690f1c2 feat(core): Add delete view for ticket types: request, incident, change and problem
ref: #250 #96 #93 #95 #90 #263
2024-09-03 14:54:58 +09:30
Jon
a3bfa921e8 test: correct typo in test description for test_model_add_has_permission
ref: #263
2024-09-03 14:53:23 +09:30
Jon
c670f017a0 fix(api): Ensure if device found it is returned
ref: #262
2024-09-03 13:51:12 +09:30
Jon
f70c5a28af Merge pull request #260 from nofusscomputing/257-ticket-comment-validation 2024-09-03 12:12:47 +09:30
Jon
c3d64a031d feat(api): when attempting to create a device and it's found within DB, dont recreate, return it.
DB matches: name and uuid then name and serial number. first found is returned.

ref: #260 closes #262
2024-09-03 12:07:35 +09:30
Jon
eb94729277 feat(core): When solution comment posted to ticket update status to solved
ref: #250 #96 #93 #95 #90 #257 #260
2024-09-02 16:06:43 +09:30
Jon
c339f17c5c feat(core): Add opened by column to ticket indexes
ref: #250 #96 #93 #95 #90
2024-09-02 15:39:18 +09:30
Jon
d7dd2d6d8b feat(core): permit user to add comment to own ticket
ref: #250 #96 #93 #95 #90 #257
2024-09-02 15:38:49 +09:30
Jon
342fe7da9e fix(core): Ensure status field remains as part of ticket
ref: #250 #96 #93 #95 #90
2024-09-02 15:37:52 +09:30
Jon
910a002201 feat(core): Allow OP to edit own Ticket Comment
ref: #250 #96 #93 #95 #90 closes #257
2024-09-02 15:16:23 +09:30
Jon
5f6c36e823 feat(core): Ticket Comment form submission validation
ref: #250 #96 #93 #95 #90 #257
2024-09-02 14:54:54 +09:30
Jon
cf577bbb4f feat(core): Ticket Comment can be edited by owner
ref: #250 #96 #93 #95 #90 #257
2024-09-02 14:39:38 +09:30
Jon
b8253ae9ba feat(core): Ticket Comment source hidden for non-triage users
ref: #250 #96 #93 #95 #90 #257
2024-09-02 14:39:01 +09:30
Jon
978bcf3b45 refactor(core): cache permission check for ticket types
ref: #250 #96 #93 #95 #90 #257
2024-09-02 14:38:26 +09:30
Jon
f76f81a312 feat(core): When fetching allowed ticket comment fields, check against permissions
ref: #250 #96 #93 #95 #90 #257
2024-09-02 13:39:58 +09:30
Jon
5793295e1a feat(core): pass request to ticket comment form
ref: #250 #96 #93 #95 #90 #257
2024-09-02 13:36:58 +09:30
Jon
6df22314c9 feat(itam): Accept device UUID in any case.
when saving, normalise to lowercase

ref: #260 closes #261
2024-09-02 13:29:29 +09:30
Jon
d7c3e051de refactor(core): Move allowed fields logic to own function
ref: #250 #96 #93 #95 #90 #257
2024-09-02 12:33:54 +09:30
Jon
057a39091d Merge pull request #252 from nofusscomputing/250-ticket-model 2024-09-01 17:12:27 +09:30
Jon
ba8b618b7d chore(core): update validate field permission docstring
ref: #250 #252 #96 #93 #95 #90 #115
2024-09-01 17:01:29 +09:30
Jon
0b86ded4f5 chore(core): Add Ticket Comment validation class
ref: #250 #252 #96 #93 #95 #90 #115 #257
2024-09-01 16:58:24 +09:30
Jon
523341cf4a docs(core): Add some ticketing docs
ref: #250 #252 #96 #93 #95 #90 #115
2024-09-01 16:44:52 +09:30
Jon
ee17095a14 chore(core): squash ticket migrations
ref: #250 #252 #96 #93 #95 #90 #115
2024-09-01 14:40:09 +09:30
Jon
7829f4b7d8 feat(core): Add ticket status icon
ref: #250 #252 #96 #93 #95 #90 #115
2024-09-01 13:52:24 +09:30
Jon
058e057088 feat(core): Enable ticket comment created date can be set when an import user
ref: #250 #252 #96 #93 #95 #90 #115
2024-09-01 13:52:24 +09:30
Jon
c8d7b52fbf fix(core): Correct modified field to correct type for ticket comment
ref: #250 #252 #96 #93 #95 #90 #115
2024-09-01 13:52:24 +09:30
Jon
9732656556 fix(api): Filter ticket comments to match ticket
ref: #250 #252 #96 #93 #95 #90 #115
2024-09-01 13:52:24 +09:30
Jon
967b9251e2 feat(api): Set default values for ticket comment form to match ticket
ref: #250 #252 #96 #93 #95 #90 #115
2024-09-01 13:52:24 +09:30
Jon
3ba89a926b fix(core): Correct modified field to correct type
ref: #250 #252 #96 #93 #95 #90 #115
2024-09-01 13:52:24 +09:30
Jon
b04b6fe645 fix(core): Ensure new ticket can be created
ref: #250 #252 #96 #93 #95 #90 #115
2024-09-01 13:52:24 +09:30
Jon
1829395a8a fix(core): Add ticket_type field to import_permissions
ref: #250 #252 #96 #93 #95 #90 #115
2024-09-01 13:52:24 +09:30
Jon
6f2d431ae1 fix(core): Ensure that the organization field is available
ref: #250 #252 #96 #93 #95 #90 #115
2024-09-01 13:52:24 +09:30
Jon
9132608aaf feat(core): render ticket number #\d+ links within markdown
ref: #250 #252 #96 #93 #95 #90 #115
2024-08-31 17:51:25 +09:30
Jon
f0b604b5dc feat(core): Use common function for markdown rendering for ticket objects
ref: #250 #252 #96 #93 #95 #90 #115
2024-08-31 16:58:56 +09:30
Jon
d1b9283a9a test: Add view must have function get_initial
organization is set here for tenancy objects

ref: #252
2024-08-31 16:03:28 +09:30
Jon
fe353904d8 test(itam): Refactor Device tests organization field to be editable.
ref: #252
2024-08-31 15:29:40 +09:30
Jon
011a6c156e test: Ensure tests add organization to tenancy objects on creation
ref: #252
2024-08-31 15:28:42 +09:30
Jon
8662feb1c7 feat(api): Ensure device can add/edit organization
ref: #252
2024-08-31 15:27:08 +09:30
Jon
097b3fe8b6 feat(core): Add api validation for ticket
ref: #250 #252 #96 #93 #95 #90 #115
2024-08-31 14:34:20 +09:30
Jon
7f138d4b68 feat(core): Ensure for tenancy objects that the organization is set
ref: #252
2024-08-31 13:19:49 +09:30
Jon
6532d0e0d7 fix(core): dont remove hidden fields on ticket comment form
ref: #250 #252 #96 #93 #95 #90 #115
2024-08-31 13:17:32 +09:30
Jon
8242d9f269 fix(core): Correct ticket comment permissions
ref: #250 #252 #96 #93 #95 #90 #115
2024-08-31 13:17:04 +09:30
Jon
28fe89e048 feat(core): Ticket comment orgaanization set to ticket organization
ref: #250 #252 #96 #93 #95 #90 #115
2024-08-31 13:16:02 +09:30
Jon
b709839c38 refactor(access): Add definable parameters to organization mixin
ref: #252
2024-08-31 12:25:09 +09:30
Jon
638ea466f0 docs(core): initial docs pages for v1.2
ref: #250 #252 #96 #93 #95 #90 #115
2024-08-31 11:53:45 +09:30
Jon
31bc1e4e76 fix(access): correct permission check to cater for is_global=None
ref: #250 #252 #96 #93 #95 #90 #115
2024-08-31 11:25:08 +09:30
Jon
0535674a96 docs(core): document get_dynamic_permissions function
ref: #252
2024-08-31 11:22:13 +09:30
Jon
5f3b12a472 chore(core): clean up ticket css
ref: #250 #252 #96 #93 #95 #90 #115
2024-08-31 11:16:10 +09:30
Jon
6ec16cbeb0 feat(core): colour code related ticket background to ticket type
ref: #250 #252 #96 #93 #95 #90 #115
2024-08-30 18:20:47 +09:30
Jon
1665e519a4 fix(core): return correct redirect path for related ticket form
ref: #250 #252 #96 #93 #95 #90 #115
2024-08-30 15:50:19 +09:30
Jon
95979c6095 feat(core): Validate ticket related and prevent duel related entries
ref: #250 #252 #96 #93 #95 #90 #115
2024-08-30 15:49:42 +09:30
Jon
8b004466d1 feat(core): Validate ticket status field for all ticket types
ref: #250 #252 #96 #93 #95 #90 #115
2024-08-30 15:45:15 +09:30
Jon
3f1f2fd8d4 feat(core): Add ticket action comments on ticket update
ref: #250 #252 #96 #93 #95 #90 #115
2024-08-30 15:00:36 +09:30
Jon
96ed198efc fix(core): use from ticket title for "blocked by"
ref: #250 #252 #96 #93 #95 #90 #115
2024-08-30 12:30:14 +09:30
Jon
6a52730b49 feat(core): Add Title bar to ticket form
ref: #250 #252 #96 #93 #95 #90 #115
2024-08-30 12:23:05 +09:30
Jon
5c4a802017 feat(core): Add field level permission and validation checks
ref: #250 #252 #96 #93 #95 #90 #11
2024-08-28 17:44:41 +09:30
Jon
e59a08b351 refactor(access): cache user_organizations on lookup
ref: #252
2024-08-28 17:39:16 +09:30
Jon
2a7857b60d refactor(access): cache object_organization on lookup
ref: #252
2024-08-28 17:38:29 +09:30
Jon
09afd7f165 feat(core): Add permission checking to Tickets form
ref: #250 #252 #96 #93 #95 #90 #115
2024-08-27 17:18:52 +09:30
Jon
e63bec83e8 feat(access): add dynamic permissions to Tenancy Permissions
ref: #252 #250
2024-08-27 17:09:57 +09:30
Jon
5d74ddfee5 fix(access): Don't query for is_global=None within TenancyManager
ref: #252
2024-08-27 17:05:50 +09:30
Jon
3c44561b19 chore(development): Add makefile
ref: #252 #248
2024-08-26 15:14:04 +09:30
Jon
8edb209d16 feat(api): Add Tickets endpoint
ref: #252 #248
2024-08-26 15:13:12 +09:30
Jon
81bd635ca4 feat(itim): Add Problem ticket to navigation
ref: #250 #252 #93
2024-08-25 17:52:41 +09:30
Jon
6ff3fe5949 feat(itim): Add Incident ticket to navigation
ref: #250 #252 #93
2024-08-25 17:52:20 +09:30
Jon
31067aab95 feat(itim): Add Change ticket to navigation
ref: #250 #252 #90
2024-08-25 17:51:53 +09:30
Jon
7b3a007862 feat(assistance): Add Request ticket to navigation
ref: #250 #252 #96
2024-08-25 17:48:17 +09:30
Jon
c5a5c393a8 feat(core): add basic ticketing system
ref: #250 #252 #96 #93 #95 #90 #115
2024-08-25 17:45:55 +09:30
Jon
52db44eac7 feat(development): add option for including additional stylesheets
ref: #252
2024-08-25 11:31:43 +09:30
Jon
cb9c782d0c chore(base): remove dev apps from debug added urls
ref: #252
2024-08-24 17:03:01 +09:30
Jon
b8c4a540fa fix(core): ensure is_global check does not process null value
ref: #252
2024-08-24 17:00:59 +09:30
Jon
91d85d93c7 chore(core): Add initial comment model file
ref: #252 #250
2024-08-23 18:06:32 +09:30
Jon
73ca0feb55 chore(core): Add initial ticket model file
ref: #252 #250
2024-08-23 18:06:32 +09:30
Jon
64fd8b5686 feat(ui): add project management icon
!31
2024-08-23 18:03:52 +09:30
Jon
58a9532c70 chore: update to cater for recent dev changes and class inheritance
!31 !40 !42
2024-08-23 18:03:52 +09:30
Jon
3c206f5aef feat(project_management): Add manager and users for projects and tasks
!30 #14
2024-08-23 18:03:52 +09:30
Jon
9bc4f186d5 feat(project_management): Project task view "view"
!30 #14
2024-08-23 18:03:52 +09:30
Jon
f883d4190a feat(project_management): Project task edit view
!30 #14
2024-08-23 18:03:52 +09:30
Jon
c6fc2d3e7c feat(project_management): Project task delete view
!30 #14
2024-08-23 18:03:52 +09:30
Jon
e4d1bb4d3c feat(project_management): Project task add view
!30 #14
2024-08-23 18:03:52 +09:30
Jon
376faf3d5a feat(project_management): Add project task model
!30 #14
2024-08-23 18:03:52 +09:30
Jon
6c0ca9cb86 chore(project_management): add urls to include
!30
2024-08-23 18:03:52 +09:30
Jon
326753d0ff chore(project_management): Add doc link to project view page
!30 #14
2024-08-23 18:02:53 +09:30
Jon
47f95ddae2 chore(project_management): add placeholder code for project notes
!30 #14
2024-08-23 18:02:53 +09:30
Jon
86fc1448a6 feat(project_management): save project history
!30 #14
2024-08-23 18:02:53 +09:30
Jon
a91ae337c4 feat(project_management): add project delete page
!30 #14
2024-08-23 18:00:45 +09:30
Jon
58466fa490 feat(project_management): add project edit page
!30 #14
2024-08-23 18:00:45 +09:30
Jon
de78a30a5d feat(project_management): add project view page
!30 #14
2024-08-23 18:00:45 +09:30
Jon
f7d61696d1 feat(project_management): add project add page
!30 #14
2024-08-23 18:00:45 +09:30
Jon
e35ccd360b feat(project_management): add project index page
!30
2024-08-23 18:00:45 +09:30
Jon
5f3a778002 feat(project_management): add interim project model
!30
2024-08-23 18:00:45 +09:30
dcba456af3 build: bump version 1.0.0 -> 1.1.0 2024-08-23 08:26:37 +00:00
Jon
6d98006a37 Merge pull request #210 from nofusscomputing/v1-1 2024-08-23 17:47:59 +09:30
Jon
4ac0c157bc feat(itim): Dont attempt to apply cluster type config if no type specified.
ref: #247 #71
2024-08-23 17:45:44 +09:30
Jon
e696129f0b feat(itim): Service config rendered as part of cluster config
ref: #247 #125 closes #69
2024-08-23 17:45:44 +09:30
Jon
cf5c512a64 feat(itim): dont force config key, validate when it's required
ref: #247 #69
2024-08-23 17:45:44 +09:30
Jon
32f45f2d5f feat(itim): Services assignable to cluster
ref: #247 #125
2024-08-23 17:45:44 +09:30
Jon
66b8bd5a74 feat(itim): Ability to add configuration to cluster type
ref: #247 closes #71
2024-08-23 17:45:44 +09:30
Jon
bfb20dab0f feat(itim): Ability to add external link to cluster
ref: #244 #71 #6
2024-08-23 17:45:44 +09:30
Jon
6b28569bca fix(settings): return the rendering of external links to models
ref: #244
2024-08-23 17:45:44 +09:30
Jon
79b2c668fa docs(itim): cluster user docs
ref: #244 #71
2024-08-23 17:45:44 +09:30
Jon
5d660694c3 refactor(itim): Add Cluster type to index page
ref: #244 #71
2024-08-23 17:45:44 +09:30
Jon
e70d0392c0 chore(itim): Add Cluster icon to navigation
ref: #244 #71
2024-08-23 17:45:44 +09:30
Jon
caa47a3bb6 test(itim): Cluster Types unit tests
ref: #244 #71
2024-08-23 17:45:44 +09:30
Jon
75203c022a feat(itim): Ability to add and configure Cluster Types
ref: #244 #71
2024-08-23 17:45:44 +09:30
Jon
b65e577017 chore(itim): add placeholder for assigning service to a cluster
ref: #244 #71 #125
2024-08-23 17:45:44 +09:30
Jon
45ef81481f test(itim): Cluster unit tests
ref: #244 #71
2024-08-23 17:45:44 +09:30
Jon
f9dee4465b feat(itim): Add cluster to history save
ref: #244 #71
2024-08-23 17:45:44 +09:30
Jon
8ec1ea2a4c feat(itim): prevent cluster from setting itself as parent
ref: #244 #71
2024-08-23 17:45:44 +09:30
Jon
17df9d1fa3 fix(core): Ensure when saving history json is correctly formatted
ref: #244
2024-08-23 17:45:44 +09:30
Jon
24967ae3a6 fix(itim): Fix name typo in Add Service button
ref: #244
2024-08-23 17:45:44 +09:30
Jon
30bd8aa483 feat(itim): Ability to add and configure cluster
ref: #244 #71
2024-08-23 17:45:44 +09:30
Jon
efce9c0219 chore: update migrations from previous days work
ref: #244 #71 #245
2024-08-23 17:45:44 +09:30
Jon
0020550dde feat(itam): Track if device is virtual
ref: #244 closes #245
2024-08-23 17:45:44 +09:30
Jon
04a9cde47e feat(api): Endpoint to fetch user permissions
ref: #244 closes #164
2024-08-23 17:45:44 +09:30
Jon
e472022c91 feat(development): Add function to filter permissions to those used by centurion
ref: #244 #164
2024-08-23 17:45:44 +09:30
Jon
d778cd0e83 feat(development): Add new template tag choice_ids for string list casting
Ref: #244 closes #243
2024-08-23 17:45:44 +09:30
Jon
1f76da8709 refactor(itam): Knowledge Base now uses details template
#242 closes #231
2024-08-23 17:45:44 +09:30
Jon
7ddc0abce6 test(itam): Correct Device Type Model permissions test to use "change" view
#242 #234
2024-08-23 17:45:44 +09:30
Jon
a2af58ae09 refactor(itam): Device Type now uses details template
#242 closes #234
2024-08-23 17:45:44 +09:30
Jon
8e71bb932e test(itam): Correct Operating System Model permissions test to use "change" view
#242 #229
2024-08-23 17:45:44 +09:30
Jon
8c1f033b1c refactor(itam): Operating System now uses details template
#242 closes #229
2024-08-23 17:45:44 +09:30
Jon
eb4df77614 refactor(itim): Service Port now uses details template
#242 closes #238
2024-08-23 17:45:44 +09:30
Jon
fed6eee951 test(config_management): Correct Device Model permissions test to use "change" view
#242 #235
2024-08-23 17:45:44 +09:30
Jon
6a0b507c3b refactor(itam): Device Model now uses details template
#242 closes #235
2024-08-23 17:45:44 +09:30
Jon
47b2e61987 test(config_management): Correct Config Group permissions test to use "change" view
#242 #230
2024-08-23 17:45:44 +09:30
Jon
28259b329e refactor(config_management): Config Groups now uses details template
#242 closes #230
2024-08-23 17:45:44 +09:30
Jon
4391aa3ea8 test(itam): Correct Software Category permissions test to use "change" view
#242 #236
2024-08-23 17:45:44 +09:30
Jon
4a4c8e94e4 refactor(itam): Software Categories now uses details template
#242 closes #236
2024-08-23 17:45:44 +09:30
Jon
d41cc312bb test(core): Correct manufacturer permissions test to use "change" view
.#242 #232
2024-08-23 17:45:44 +09:30
Jon
12abc741d2 refactor(itam): manufacturer now uses details template
#242 closes #232
2024-08-23 17:45:44 +09:30
Jon
a8262e0a54 test(itam): Correct software permissions test to use "change" view
#240 #233
2024-08-23 17:45:44 +09:30
Jon
2011c212ba refactor(itam): software now uses details template
#240 closes #233
2024-08-23 17:45:44 +09:30
Jon
564871ca3c chore(itam): remove commented code from device form
#240 closes #227
2024-08-23 17:45:44 +09:30
Jon
95bb15238a fix: Ensure tenancy models have Meta.verbose_name_plural attribute
.#240 closes #239
2024-08-23 17:45:44 +09:30
Jon
cafc5ce6e2 test(model): test for checking if Meta sub-class has variable verbose_name_plural
#239 #240
2024-08-23 17:45:44 +09:30
Jon
68c3b64424 fix(base): Use correct url for back button
.#227 #240
2024-08-23 17:45:44 +09:30
Jon
300fe283d6 refactor(itam): device now use details template
#227 #240
2024-08-23 17:45:44 +09:30
Jon
ac6408c3bb feat(development): Render model_name_plural as part of back button
#227 #239
2024-08-23 17:45:44 +09:30
Jon
750e323947 feat(development): add to form field model_name_plural
#227 #239
2024-08-23 17:45:44 +09:30
Jon
4cca9d9904 feat(development): render heading if section included
#227
2024-08-23 17:45:44 +09:30
Jon
955081f155 chore: add Merge/Pull request template
#226
2024-08-23 17:45:44 +09:30
Jon
01e47c889b docs(roadmap): update completed features
#226
2024-08-23 17:45:43 +09:30
Jon
2cd4d387a7 docs(base): detail view template
. #24 #226 closes #22
2024-08-23 17:45:43 +09:30
Jon
ea8c60ccc5 refactor(itim): services now use details template
. #22 #226
2024-08-23 17:45:43 +09:30
Jon
4ecf5236c1 feat(base): create detail view templates
purpose is to aid in the development of a detail form

#22 #24 #226
2024-08-23 17:45:43 +09:30
Jon
eb919f2d5e docs: initial adding of template page
#22
2024-08-23 17:45:43 +09:30
Jon
485dd43b58 chore: add services navigation icon
!43 #69
2024-08-23 17:45:43 +09:30
Jon
fd4da657fb fix(itim): ensure that the service template config is also rendered as part of device config
!43 #69
2024-08-23 17:45:43 +09:30
Jon
acc6879fb1 docs: fluff the port and services
!43 closes #69
2024-08-23 17:45:43 +09:30
Jon
d339fdb645 fix(itim): dont render link if no device
!43 #69
2024-08-23 17:45:43 +09:30
Jon
53a720a802 feat(itam): Render Service Config with device config
!43 #69
2024-08-23 17:45:43 +09:30
Jon
0b04cdcfbf feat(itam): Display deployed services for devices
!43 #69
2024-08-23 17:45:43 +09:30
Jon
b5d2fe70ff feat(itim): Prevent circular service dependencies
!43 #69
2024-08-23 17:45:43 +09:30
Jon
6d6f1c5401 feat(itim): Port number validation to check for valid port numbers
!43 #69
2024-08-23 17:45:43 +09:30
Jon
7b8b8a6394 feat(itim): Prevent Service template from being assigned as dependent service
!43 #69
2024-08-23 17:45:43 +09:30
Jon
2a3373a19b feat(itim): Add service template support
!43 #69
2024-08-23 17:45:43 +09:30
Jon
eb320c4e95 fix(itim): Dont show self within service dependencies
!43 #69
2024-08-23 17:45:43 +09:30
Jon
0b220424bb feat(itim): Ports for service management
!43 #69
2024-08-23 17:45:43 +09:30
Jon
a948ec7bd7 feat(itim): Service Management
!43 #69
2024-08-23 17:45:43 +09:30
Jon
56196f721d fix(assistance): Only return distinct values when limiting KB articles
!43 #10
2024-08-23 17:45:43 +09:30
Jon
3d06112860 docs(assistance): document kb categories for user
!43 closes #10
2024-08-23 17:45:43 +09:30
Jon
05484d9e02 feat(assistance): Filter KB articles to target user
only intended to filter for users whom dont have change perm.

!43 #10
2024-08-23 17:45:43 +09:30
Jon
b73807a140 feat(assistance): Add date picker to date fields for KB articles
!43 #10
2024-08-23 17:45:43 +09:30
Jon
215c5e464c feat(assistance): Dont display expired articles for "view" users
!43 #10
2024-08-23 17:45:43 +09:30
Jon
cf2dce320c docs(assistance): document kb for user
!43 #10
2024-08-23 17:45:43 +09:30
Jon
32cdcc38b5 feat(base): add code highlighting to markdown
!43 #10
2024-08-23 17:45:43 +09:30
Jon
4b3ea06f70 feat(assistance): Categorised Knowledge base articles
!43 #10
2024-08-23 17:45:43 +09:30
Jon
2e7a6a42b4 docs(assistance): added pages for knowledgebase
!43 #10
2024-08-23 17:45:43 +09:30
Jon
be0ec86c48 chore(base): rename information -> assistance
!43 #10
2024-08-23 17:45:43 +09:30
Jon
8c493e8fa3 feat(itim): Add menu entry
!43 #69 #71
2024-08-23 17:45:43 +09:30
Jon
9668e811c5 feat(itam): Ability to add device configuration
!43 fixes #44
2024-08-23 17:45:43 +09:30
Jon
28ce99f46a test(external_link): add tests
!43 fixes #6
2024-08-23 17:45:43 +09:30
Jon
9b4dbc58f3 feat(settings): New model to allow adding templated links to devices and software
!43 #6
2024-08-23 17:45:43 +09:30
Jon
f295f15034 docs: move settings pages into sub-directory
!43 #6
2024-08-23 17:45:43 +09:30
4c41994068 build: bump version 1.0.0-b14 -> 1.0.0 2024-08-23 08:03:31 +00:00
Jon
6f7b3ffad6 chore: add github pr template
ref: #213
2024-08-20 14:21:00 +09:30
Jon
cc97128e25 ci: add mkdocs workflow
ref: #213
2024-08-20 12:28:43 +09:30
Jon
b6ba3d38dc docs: migrate project links to github
ref: #213
2024-08-20 12:08:59 +09:30
Jon
18b788844a ci: update gitlab-ci to current head
ref: #209
2024-08-19 16:53:31 +09:30
424 changed files with 33700 additions and 1339 deletions

View File

@ -17,5 +17,5 @@ commitizen:
prerelease_offset: 1
tag_format: $version
update_changelog_on_bump: false
version: 1.0.0-b14
version: 1.3.0
version_scheme: semver

39
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,39 @@
### :books: Summary
<!-- your summary here emojis ref: https://github.com/yodamad/gitlab-emoji -->
### :link: Links / References
<!--
using a list as any links to other references or links as required. if relevant, describe the link/reference
Include any issues or related merge requests. Note: dependent MR's also to be added to "Merge request dependencies"
-->
### :construction_worker: Tasks
- [ ] Add your tasks here if required (delete)
<!-- dont remove tasks below strike through including the checkbox by enclosing in double tidle '~~' -->
- [ ] :firecracker: Contains breaking-change Any Breaking change(s)?
_Breaking Change must also be notated in the commit that introduces it and in [Conventional Commit Format](https://www.conventionalcommits.org/en/v1.0.0/)._
- [ ] :notebook: Release notes updated
- [ ] :blue_book: Documentation written
_All features to be documented within the correct section(s). Administration, Development and/or User_
- [ ] :checkered_flag: Milestone assigned
- [ ] :test_tube: [Unit Test(s) Written](https://nofusscomputing.com/projects/centurion_erp/development/testing/)
_ensure test coverage delta is not less than zero_
- [ ] :page_facing_up: Roadmap updated

View File

@ -16,6 +16,17 @@ env:
jobs:
mkdocs:
name: 'MKDocs'
permissions:
pull-requests: write
contents: write
statuses: write
checks: write
actions: write
uses: nofusscomputing/action_mkdocs/.github/workflows/reusable_mkdocs.yaml@development
docker:
name: 'Docker'
uses: nofusscomputing/action_docker/.github/workflows/docker.yaml@development

6
.gitignore vendored
View File

@ -9,3 +9,9 @@ artifacts/
volumes/
build/
pages/
node_modules/
.markdownlint-cli2.jsonc
.markdownlint.json
package-lock.json
package.json
**.junit.xml

View File

@ -35,3 +35,5 @@
- [ ] [Unit Test(s) Written](https://nofusscomputing.com/projects/centurion_erp/development/testing/)
_ensure test coverage delta is not less than zero_
- [ ] :page_facing_up: Roadmap updated

View File

@ -7,5 +7,6 @@
"streetsidesoftware.code-spell-checker",
"qwtel.sqlite-viewer",
"jebbs.markdown-extended",
"william-voyek.vscode-nginx",
]
}

19
.vscode/launch.json vendored
View File

@ -16,6 +16,25 @@
"autoStartBrowser": false,
"program": "${workspaceFolder}/app/manage.py"
},
{
"name": "Debug: Gunicorn",
"type": "debugpy",
"request": "launch",
"module": "gunicorn",
"args": [
"--access-logfile",
"-",
"--workers",
"3",
"--bind",
"0.0.0.0:8002",
"app.wsgi:application",
],
"django": true,
"autoStartBrowser": false,
"cwd": "${workspaceFolder}/app"
},
{
"name": "Debug: Celery",
"type": "python",

View File

@ -1,3 +1,431 @@
## 1.3.0 (2024-10-31)
### feat
- **docker**: Add worker service config for SupervisorD
- **docker**: ensure supervisor starts
- **docker**: use correct file location for nginx config
- **docker**: Fail the build if django is not found
- **docker**: Install NginX to serve site
- **docker**: Add supervisord for install
- **docker**: Add gunicorn for install
- update docker image alpine 3.19 ->3.20
### Fixes
- **docker**: Ensure SupervisorD daemon config directory exists.
- **docker**: use alias for static
- **access**: testing of param causing gunicorn to fail
- **docker**: place nginx conf in correct path
- **docker**: gunicorn must call method
- **docker**: Ensure NginX config applied after it's installed
- **docker**: Add proxy params for NginX
- **docker**: Make centurion the default nginx conf
- **docker**: Correct NginX start command
### Refactoring
- **docker**: Switch to entrypoint
## 1.2.2 (2024-10-29)
### Fixes
- **docker**: adjust pyyaml to >-6.0.1
## 1.2.1 (2024-10-22)
### Fixes
- **project_management**: Ensure user cant see projects for organizations they are apart of
### Refactoring
- **project_management**: dont order queryset for project
## 1.2.0 (2024-10-11)
### feat
- update django 5.0.8 -> 5.1.2
- **settings**: Add API filter and search
- **core**: Add API filter of fields external_system and external_ref for projects
- **core**: Add API filter of fields external_system and external_ref to tickets
- **project_management**: increase project field length 50 -> 100 chars
- **core**: increase ticket title field length 50 -> 100 chars
- **core**: Add ability track ticket estimation time for completion
- **core**: Add ability to delete a ticket
- **core**: [Templating Engine] Add template tag concat_strings
- **itim**: Add ticket tab to services
- **itim**: Add ticket tab to clusters
- **itam**: Add ticket tab to software
- **itam**: Add ticket tab to operating systems
- **itam**: Add ticket tab to devices
- **config_management**: Add ticket tab to conf groups
- **core**: Add slash command `link` for linking items to tickets
- **core**: Add to markdown rendering model references
- **core**: Ability to link items to all ticket types
- **core**: add model ticket linked items
- **project_management**: Add project milestones api endpoint
- **project_management**: Add import_project permission and add api serializer
- **core**: great odins beard, remove the checkbox formatting
- **project_management**: Add field is_deleted to projects
- **project_management**: Calculate project completion percentage and display
- **core**: order project categories with parent name if applicable
- **project_management**: Add Project Type to the UI
- **project_management**: Add Project State to the UI
- **project_management**: add priority field to project model, form and api endpoint
- **project_management**: add organization field to project form and api endpoint
- **project_management**: add project_type field to project form
- **project_management**: add external_ref and external_system field to project model
- **project_management**: add project type field to project model
- **project_management**: add project type api endpoint
- **project_management**: new model project type
- **project_management**: add project state api endpoint
- **project_management**: add project state field to project model
- **project_managemenet**: new model project state
- **project_management**: add field external system to projects
- **core**: validate field milestone for all ticket types
- **core**: Add field milestone to all ticket types
- **project_management**: Add project milestones
- **core**: Add slash command "related ticket" for ticket and ticket comments
- **core**: Suffix username to action comments
- **core**: Add slash command `/spend` for ticket and ticket comments
- **core**: Disable HTML tag rendering for markdown
- **project_management**: remove requirement for code field to be populated
- **core**: Add ticket comment category API endpoint
- **core**: Ability to assign categories to ticket comments
- **core**: Add ticket comment categories
- **core**: Extend all ticket endpoints to contain ticket categories
- **core**: Add ticket category API endpoint
- **core**: Ability to assign categories to tickets
- **core**: Addpage titles to view abstract classes
- **core**: Add ticket categories
- **core**: during markdown render, if ticket ID not found return the tag
- **core**: Add heading anchor plugin to markdown
- **core**: correct markdown formatting for KB articles
- **core**: remove project field from being editable when creating project task
- **core**: Add admonition style
- **project_management**: Validate project task has project set
- **core**: set project ID to match url kwarg
- **core**: Add action comment on title change
- **core**: Add task listts plugin to markdowm
- **core**: Add footnote plugin to markdowm
- **core**: Add admonition plugin to markdowm
- **core**: Add table extension to markdowm
- **core**: Add strikethrough extension to markdowm
- **core**: Add linkify extension to markdowm
- **core**: move markdown parser py-markdown -> markdown-it
- **core**: Add organization column to ticket pages
- **core**: Allow super-user to edit ticket comment source
- **core**: Render linked tickets the same as the rendered markdown link
- **core**: Add project task link for related project task
- **project_management**: Add project duration field
- **core**: Add external ref to tickets if populated
- **core**: Add project task permissions
- **project_management**: Add project tasks
- **api**: Add project tasks endpoint
- **api**: Add projects endpoint
- **api**: Add project management endpoint
- **core**: support negative numbers when Calculating ticket duration for ticket meta and comments
- **core**: Caclulate ticket duration for ticket meta and comments
- **core**: Add edit details to ticket and comments
- **core**: Don't save model history for ticket models
- **core**: add option to allow the prevention of history saving for tenancy models
- **core**: Add project field to tickets allowed fields
- **core**: Update ticket status when assigned/unassigned users/teams
- **core**: Create action comment for subscribed users/teams
- **core**: Create action comment for assigned users/teams
- **core**: adding of more ticket status icons
- **api**: Ticket endpoint dynamic permissions
- **core**: add ticket status badge
- **access**: add ability to fetch dynamic permissions
- **core**: Add delete view for ticket types: request, incident, change and problem
- **api**: when attempting to create a device and it's found within DB, dont recreate, return it.
- **core**: When solution comment posted to ticket update status to solved
- **core**: Add opened by column to ticket indexes
- **core**: permit user to add comment to own ticket
- **core**: Allow OP to edit own Ticket Comment
- **core**: Ticket Comment form submission validation
- **core**: Ticket Comment can be edited by owner
- **core**: Ticket Comment source hidden for non-triage users
- **core**: When fetching allowed ticket comment fields, check against permissions
- **core**: pass request to ticket comment form
- **itam**: Accept device UUID in any case.
- **core**: Add ticket status icon
- **core**: Enable ticket comment created date can be set when an import user
- **api**: Set default values for ticket comment form to match ticket
- **core**: render ticket number `#\d+` links within markdown
- **core**: Use common function for markdown rendering for ticket objects
- **api**: Ensure device can add/edit organization
- **core**: Add api validation for ticket
- **core**: Ensure for tenancy objects that the organization is set
- **core**: Ticket comment orgaanization set to ticket organization
- **core**: colour code related ticket background to ticket type
- **core**: Validate ticket related and prevent duel related entries
- **core**: Validate ticket status field for all ticket types
- **core**: Add ticket action comments on ticket update
- **core**: Add Title bar to ticket form
- **core**: Add field level permission and validation checks
- **core**: Add permission checking to Tickets form
- **access**: add dynamic permissions to Tenancy Permissions
- **api**: Add Tickets endpoint
- **itim**: Add Problem ticket to navigation
- **itim**: Add Incident ticket to navigation
- **itim**: Add Change ticket to navigation
- **assistance**: Add Request ticket to navigation
- **core**: add basic ticketing system
- **development**: add option for including additional stylesheets
- **ui**: add project management icon
- **project_management**: Add manager and users for projects and tasks
- **project_management**: Project task view "view"
- **project_management**: Project task edit view
- **project_management**: Project task delete view
- **project_management**: Project task add view
- **project_management**: Add project task model
- **project_management**: save project history
- **project_management**: add project delete page
- **project_management**: add project edit page
- **project_management**: add project view page
- **project_management**: add project add page
- **project_management**: add project index page
- **project_management**: add interim project model
### Fixes
- ensure model mandatory fields don't specify a default value
- **api**: Ensure user is set to current user for ticket comment
- **core**: remove org field when editing a ticket
- **core**: during validation, if subscribed users not specified, use empty list
- **core**: add missing pagination to ticket comment categories index
- **core**: add missing pagination to ticket categories index
- **project_management**: Ensure project type and state show on index page
- **core**: Add replacement function within ticket validation as `cleaned_data` attribute replacement
- **core**: Ensure the ticket clears project field on project removal
- **core**: Remove ticket fields user has no access to
- **core**: correct logic for slash command `/spend`
- **project_management**: correct project view permissions
- **core**: Correct view permissions for ticket comment category
- **core**: correct url typo for ticket category API endpoint
- **core**: dont attempt to modify field for ticket category API list
- **core**: Dont attempt to render ticket category if none
- **core**: Correct the delete permission
- **core**: correct project task reply link for comments
- **core**: correct project task comment buttons
- **project_management**: correct comment reply url name
- **core**: Generate the correct edit url for tickets
- **core**: Generate the correct comment urls for tickets
- **core**: Redirect to correct url for itim tickets after adding comment
- **core**: correct linked tickets hyperlink address
- **core**: order ticket comments by creation date
- **core**: Ensure for both ticket and comment, external details are unique.
- **core**: Ensure on ticket comment create and update a response is returned
- **core**: Ensure related tricket action comment is trimmed
- **core**: Team assigned to ticket status update
- **api**: ensure ticket_type is set from view var
- **core**: Add ticket fields to ticket types
- **core**: During ticket form validation confirm if value specified/different then default
- **core**: Correctly set the ticket type initial value
- **core**: prevent import user from having permssions within UI
- **api**: correct ticket view links
- **core**: Correct display of ticket status within ticket interface
- **api**: Ensure if device found it is returned
- **core**: Ensure status field remains as part of ticket
- **core**: Correct modified field to correct type for ticket comment
- **api**: Filter ticket comments to match ticket
- **core**: Correct modified field to correct type
- **core**: Ensure new ticket can be created
- **core**: Add `ticket_type` field to import_permissions
- **core**: Ensure that the organization field is available
- **core**: dont remove hidden fields on ticket comment form
- **core**: Correct ticket comment permissions
- **access**: correct permission check to cater for is_global=None
- **core**: return correct redirect path for related ticket form
- **core**: use from ticket title for "blocked by"
- **access**: Don't query for `is_global=None` within `TenancyManager`
- **core**: ensure is_global check does not process null value
### Refactoring
- **core**: Ticket Linked ref render as template
- **core**: migrate ticket enums to own class
- **core**: Ticket validation errors setup for both api and ui
- **core**: for tickets use validation for organization field
- **core**: refine ticket field permission and validation
- reduce action comment spacing
- **core**: update markdown styles
- **core**: migrate ticket number rendering as markdown_it plugin
- **core**: move markdown functions out of ticket model
- **core**: Adjust test layout for itsm and project field based permissions
- **project_management**: migrate projects to new style for views
- **core**: REmove constraint on setting user for ticket comment
- **core**: cache fields allowed during ticket validation
- **core**: dont require specifying ticket status
- **core**: move id to end for rendered ticket link.
- **api**: Ticket (change, incident, problem and request) to static api endpoints
- **api**: make ticket status field mandatory
- **api**: Move core tickets to own ticket endpoints
- **core**: During form validation for a ticket, use defaults if not defined for mandatory fields
- **core**: Ticket form ticket_type to use class var
- **core**: cache permission check for ticket types
- **core**: Move allowed fields logic to own function
- **access**: Add definable parameters to organization mixin
- **access**: cache user_organizations on lookup
- **access**: cache object_organization on lookup
### Tests
- **core**: Ticket Linked item view checks
- **core**: Ticket Linked item permission checks
- **project_management**: Project Milestone api permission checks
- **project_management**: Project TYpe tenancy model checks
- **project_management**: Project Type view checks
- **project_management**: Project Type permission checks
- **project_management**: Project Type core history checks
- **project_management**: Project Type tenancy object checks
- **project_management**: Project State permission checks
- **project_management**: Project State tenancy model checks
- **project_management**: Project State view checks
- **project_management**: Project State core history checks
- **project_management**: Project State tenancy object checks
- **project_management**: Project type API permission checks
- **project_management**: Project state API permission checks
- **project_management**: Project miletone skipped api checks
- **project_management**: Project Milestone tenancy model checks
- **project_management**: Project Milestone view checks
- **project_management**: Project Milestone ui permission checks
- **project_management**: Project Milestone core history checks
- **project_management**: Project Milestone Tenancy object checks
- **core**: Project tenancy model checks
- **core**: Project view checks
- **core**: Project UI permission checks
- **core**: Project API permission checks
- **core**: Project history checks
- **core**: Project Tenancy object checks
- **core**: Ticket comment category API permission checks
- **core**: add missing ticket category view checks
- **core**: ticket comment category tenancy model checks
- **core**: ticket comment category view checks
- **core**: ticket comment category ui permission checks
- **core**: ticket comment category history checks
- **core**: ticket comment category tenancy model checks
- **core**: ticket category API permission checks
- **core**: ticket category history checks
- **core**: ticket category tenancy model checks
- **core**: ticket category model checks
- **core**: view checks
- **core**: ui permissions
- **core**: correct project tests for triage user
- **core**: Project task permission checks
- **core**: Ticket comment API permission checks
- **core**: Ticket comment permission checks
- **core**: Ticket comment Views
- **core**: Tenancy model tests for ticket comment
- **core**: ensure history for ticket models is not saved
- Ensure tenancy models save model history
- **core**: remove duplicated tenancy object tests for ticket model
- **core**: correct triage user test names for allowed field permissions
- **core**: project field permission check for triage user
- **core**: Ticket Action comment checks for related tickets
- **core**: Ticket Action comment checks for subscribing team
- **core**: Ticket Action comment checks for subscribing user
- **core**: Ticket Action comment checks for unassigning team
- **core**: Ticket Action comment checks for assigning team
- **core**: Ticket Action comment checks for un-assigning user
- **core**: Ticket Action comment checks for assigning user
- **core**: Add ticket project field permission check
- **core**: ensure ticket_type tests dont have change value that matches ticket type
- **core**: field based permission tests for add, change, import and triage user
- **api**: Ticket (change, incident, problem and request) api permission checks
- **core**: interim ticket unit tests
- **itam**: Ensure if an attempt to add an existing device via API, it's not recreated and is returned.
- correct typo in test description for `test_model_add_has_permission`
- Add view must have function `get_initial`
- **itam**: Refactor Device tests organization field to be editable.
- Ensure tests add organization to tenancy objects on creation
## 1.1.0 (2024-08-23)
### feat
- **itim**: Dont attempt to apply cluster type config if no type specified.
- **itim**: Service config rendered as part of cluster config
- **itim**: dont force config key, validate when it's required
- **itim**: Services assignable to cluster
- **itim**: Ability to add configuration to cluster type
- **itim**: Ability to add external link to cluster
- **itim**: Ability to add and configure Cluster Types
- **itim**: Add cluster to history save
- **itim**: prevent cluster from setting itself as parent
- **itim**: Ability to add and configure cluster
- **itam**: Track if device is virtual
- **api**: Endpoint to fetch user permissions
- **development**: Add function to filter permissions to those used by centurion
- **development**: Add new template tag `choice_ids` for string list casting
- **development**: Render `model_name_plural` as part of back button
- **development**: add to form field `model_name_plural`
- **development**: render heading if section included
- **base**: create detail view templates
- **itam**: Render Service Config with device config
- **itam**: Display deployed services for devices
- **itim**: Prevent circular service dependencies
- **itim**: Port number validation to check for valid port numbers
- **itim**: Prevent Service template from being assigned as dependent service
- **itim**: Add service template support
- **itim**: Ports for service management
- **itim**: Service Management
- **assistance**: Filter KB articles to target user
- **assistance**: Add date picker to date fields for KB articles
- **assistance**: Dont display expired articles for "view" users
- **base**: add code highlighting to markdown
- **assistance**: Categorised Knowledge base articles
- **itim**: Add menu entry
- **itam**: Ability to add device configuration
- **settings**: New model to allow adding templated links to devices and software
### Fixes
- **settings**: return the rendering of external links to models
- **core**: Ensure when saving history json is correctly formatted
- **itim**: Fix name typo in Add Service button
- Ensure tenancy models have `Meta.verbose_name_plural` attribute
- **base**: Use correct url for back button
- **itim**: ensure that the service template config is also rendered as part of device config
- **itim**: dont render link if no device
- **itim**: Dont show self within service dependencies
- **assistance**: Only return distinct values when limiting KB articles
### Refactoring
- **itim**: Add Cluster type to index page
- **itam**: Knowledge Base now uses details template
- **itam**: Device Type now uses details template
- **itam**: Operating System now uses details template
- **itim**: Service Port now uses details template
- **itam**: Device Model now uses details template
- **config_management**: Config Groups now uses details template
- **itam**: Software Categories now uses details template
- **itam**: manufacturer now uses details template
- **itam**: software now uses details template
- **itam**: device now use details template
- **itim**: services now use details template
### Tests
- **itim**: Cluster Types unit tests
- **itim**: Cluster unit tests
- **itam**: Correct Device Type Model permissions test to use "change" view
- **itam**: Correct Operating System Model permissions test to use "change" view
- **config_management**: Correct Device Model permissions test to use "change" view
- **config_management**: Correct Config Group permissions test to use "change" view
- **itam**: Correct Software Category permissions test to use "change" view
- **core**: Correct manufacturer permissions test to use "change" view
- **itam**: Correct software permissions test to use "change" view
- **model**: test for checking if Meta sub-class has variable verbose_name_plural
- **external_link**: add tests
## 1.0.0 (2024-08-23)
## 1.0.0-b14 (2024-08-12)
### Fixes

View File

@ -1,6 +1,76 @@
# Contribution Guide
Development of this project has been setup to be done from VSCodium. The following additional requirements need to be met:
- npm has been installed. _required for `markdown` linting_
`sudo apt install -y --no-install-recommends npm`
- setup of other requirements can be done with `make prepare`
- **ALL** Linting must pass for Merge to be conducted.
_`make lint`_
## TL;DR
from the root of the project to start a test server use:
``` bash
# activate python venv
source /tmp/centurion_erp/bin/activate
# enter app dir
cd app
# Start dev server can be viewed at http://127.0.0.1:8002
python manage.py runserver 8002
# Run any migrations, if required
python manage.py migrate
# Create a super suer if required
python manage.py createsuperuser
```
## Makefile
!!! tip "TL;DR"
Common make commands are `make prepare` then `make docs` and `make lint`
Included within the root of the repository is a makefile that can be used during development to check/run different items as is required during development. The following make targets are available:
- `prepare`
_prepare the repository. init's all git submodules and sets up a python virtual env and other make targets_
- `docs`
_builds the docs and places them within a directory called build, which can be viewed within a web browser_
- `lint`
_conducts all required linting_
- `docs-lint`
_lints the markdown documents within the docs directory for formatting errors that MKDocs may/will have an issue with._
- `clean`
_cleans up build artifacts and removes the python virtual environment_
> this doc is yet to receive a re-write
# Old working docs
## Dev Environment
It's advised to setup a python virtual env for development. this can be done with the following commands.
@ -30,6 +100,9 @@ python3 manage.py createsuperuser
# If model changes
python3 manage.py makemigrations --noinput
# To update code highlight run
pygmentize -S default -f html -a .codehilite > project-static/code.css
```
Updates to python modules will need to be captured with SCM. This can be done by running `pip freeze > requirements.txt` from the running virtual environment.

View File

@ -1,3 +1,19 @@
# Version 1.3.0
!!! danger "Security"
As is currently the recommended method of deployment, the Centurion Container must be deployed behind a reverse proxy the conducts the SSL termination.
This release updates the docker container to be a production setup for deployment of Centurion. Prior to this version Centurion ERP was using a development setup for the webserver.
- Docker now uses SupervisorD for container
- Gunicorn WSGI setup for Centurion with NginX as the webserver.
- Container now has a health check.
- To setup container as "Worker", set `IS_WORKER='True'` environmental variable within container. _**Note:** You can still use command `celery -A app worker -l INFO`, although **not** recommended as the container health check will not be functioning_
# Version 1.0.0
Initial Release of Centurion ERP.

View File

@ -1,13 +1,13 @@
from django import forms
from django.contrib.auth.models import Permission
from django.db.models import Q
from django.forms import inlineformset_factory
from app import settings
from .team_users import TeamUsersForm, TeamUsers
from access.models import Team
from access.functions import permissions
from app import settings
from core.forms.common import CommonModelForm
@ -66,37 +66,4 @@ class TeamForm(CommonModelForm):
self.fields['permissions'].widget.attrs = {'style': "height: 200px;"}
apps = [
'access',
'config_management',
'core',
'django_celery_results',
'itam',
'settings',
]
exclude_models = [
'appsettings',
'chordcounter',
'groupresult',
'organization'
'settings',
'usersettings',
]
exclude_permissions = [
'add_organization',
'add_taskresult',
'change_organization',
'change_taskresult',
'delete_organization',
'delete_taskresult',
]
self.fields['permissions'].queryset = Permission.objects.filter(
content_type__app_label__in=apps,
).exclude(
content_type__model__in=exclude_models
).exclude(
codename__in = exclude_permissions
)
self.fields['permissions'].queryset = permissions.permission_queryset()

View File

@ -0,0 +1,46 @@
from django.contrib.auth.models import Permission
def permission_queryset():
"""Filter Permissions to those used within the application
Returns:
list: Filtered queryset that only contains the used permissions
"""
apps = [
'access',
'assistance',
'config_management',
'core',
'django_celery_results',
'itam',
'itim',
'settings',
]
exclude_models = [
'appsettings',
'chordcounter',
'comment',
'groupresult',
'organization'
'settings',
'usersettings',
]
exclude_permissions = [
'add_organization',
'add_taskresult',
'change_organization',
'change_taskresult',
'delete_organization',
'delete_taskresult',
]
return Permission.objects.filter(
content_type__app_label__in=apps,
).exclude(
content_type__model__in=exclude_models
).exclude(
codename__in = exclude_permissions
)

View File

@ -10,6 +10,19 @@ from .models import Organization, Team
class OrganizationMixin():
"""Base Organization class"""
parent_model: str = None
""" Parent Model
This attribute defines the parent model for the model in question. The parent model when defined
will be used as the object to obtain the permissions from.
"""
parent_model_pk_kwarg: str = 'pk'
"""Parent Model kwarg
This value is used to define the kwarg that is used as the parent objects primary key (pk).
"""
request = None
user_groups = []
@ -26,20 +39,24 @@ class OrganizationMixin():
parent_model (Model): with PK from kwargs['pk']
"""
return self.parent_model.objects.get(pk=self.kwargs['pk'])
return self.parent_model.objects.get(pk=self.kwargs[self.parent_model_pk_kwarg])
def object_organization(self) -> int:
id = None
if hasattr(self, '_object_organization'):
return self._object_organization
try:
if hasattr(self, 'get_queryset'):
self.get_queryset()
if hasattr(self, 'parent_model'):
if self.parent_model:
obj = self.get_parent_obj()
id = obj.get_organization().id
@ -61,6 +78,10 @@ class OrganizationMixin():
id = 0
if hasattr(self, 'instance') and id is None: # Form Instance
id = self.instance.get_organization()
except AttributeError:
@ -84,6 +105,10 @@ class OrganizationMixin():
pass
if id is not None:
self._object_organization = id
return id
@ -147,6 +172,10 @@ class OrganizationMixin():
user_organizations = []
if hasattr(self, '_user_organizations'):
return self._user_organizations
teams = Team.objects
for group in self.request.user.groups.all():
@ -157,14 +186,32 @@ class OrganizationMixin():
user_organizations = user_organizations + [team.organization.id]
if len(user_organizations) > 0:
self._user_organizations = user_organizations
return user_organizations
# ToDo: Ensure that the group has access to item
def has_organization_permission(self, organization: int=None) -> bool:
def has_organization_permission(self, organization: int = None, permissions_required: list = None) -> bool:
""" Check if user has permission within organization.
Args:
organization (int, optional): Organization to check. Defaults to None.
permissions_required (list, optional): if doing object level permissions, pass in required permission. Defaults to None.
Returns:
bool: True for yes.
"""
has_permission = False
if permissions_required is None:
permissions_required = self.get_permission_required()
if not organization:
organization = self.object_organization()
@ -182,7 +229,7 @@ class OrganizationMixin():
assembled_permission = str(permission["content_type__app_label"]) + '.' + str(permission["codename"])
if assembled_permission in self.get_permission_required() and (team['organization_id'] == organization or organization == 0):
if assembled_permission in permissions_required and (team['organization_id'] == organization or organization == 0):
return True
@ -242,15 +289,23 @@ class OrganizationMixin():
return True
perms = self.get_permission_required()
if permissions_required:
if self.has_organization_permission():
perms = permissions_required
else:
perms = self.get_permission_required()
if self.has_organization_permission(permissions_required = perms):
return True
if self.request.user.has_perms(perms) and len(self.kwargs) == 0 and str(self.request.method).lower() == 'get':
if self.request.user.has_perms(perms) and str(self.request.method).lower() == 'get':
return True
if len(self.kwargs) == 0 or (len(self.kwargs) == 1 and 'ticket_type' in self.kwargs):
return True
for required_permission in self.permission_required:
@ -327,6 +382,12 @@ class OrganizationPermission(AccessMixin, OrganizationMixin):
if not request.user.is_authenticated:
return self.handle_no_permission()
if len(self.permission_required) == 0:
if hasattr(self, 'get_dynamic_permissions'):
self.permission_required = self.get_dynamic_permissions()
if len(self.permission_required) > 0:

View File

@ -15,9 +15,6 @@ class Organization(SaveHistory):
verbose_name_plural = "Organizations"
ordering = ['name']
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if self.slug == '_':
@ -62,6 +59,9 @@ class Organization(SaveHistory):
def get_organization(self):
return self
def __str__(self):
return self.name
class TenancyManager(models.Manager):
@ -106,7 +106,9 @@ class TenancyManager(models.Manager):
if request:
user = request.user._wrapped if hasattr(request.user,'_wrapped') else request.user
# user = request.user._wrapped if hasattr(request.user,'_wrapped') else request.user
user = request.user
if user.is_authenticated:
@ -124,13 +126,21 @@ class TenancyManager(models.Manager):
user_organizations += [ team_user.team.organization.id ]
if len(user_organizations) > 0 and not user.is_superuser:
if len(user_organizations) > 0 and not user.is_superuser and self.model.is_global is not None:
return super().get_queryset().filter(
models.Q(organization__in=user_organizations)
|
models.Q(is_global = True)
)
if self.model.is_global:
return super().get_queryset().filter(
models.Q(organization__in=user_organizations)
|
models.Q(is_global = True)
)
else:
return super().get_queryset().filter(
models.Q(organization__in=user_organizations)
)
return super().get_queryset()
@ -188,6 +198,15 @@ class TenancyObject(SaveHistory):
def get_organization(self) -> Organization:
return self.organization
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
if self.organization is None:
raise ValidationError('Organization not defined')
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
class Team(Group, TenancyObject):
@ -196,9 +215,6 @@ class Team(Group, TenancyObject):
verbose_name_plural = "Teams"
ordering = ['team_name']
def __str__(self):
return self.name
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
@ -241,6 +257,10 @@ class Team(Group, TenancyObject):
return [permission_list, self.permissions.all()]
def __str__(self):
return self.team_name
class TeamUsers(SaveHistory):
@ -318,3 +338,6 @@ class TeamUsers(SaveHistory):
return self.team
def __str__(self):
return self.user.username

View File

@ -11,6 +11,19 @@ class TenancyObject:
model = None
""" Model to be tested """
should_model_history_be_saved: bool = True
""" Should model history be saved.
By default this should always be 'True', however in special
circumstances, this may not be desired.
"""
def test_history_save(self):
"""Confirm the desired intent for saving model history."""
assert self.model.save_model_history == self.should_model_history_be_saved
def test_has_attr_get_organization(self):
""" TenancyObject attribute check

View File

@ -91,3 +91,13 @@ class TenancyObjectTests(TestCase):
"""
assert self.item.objects is not None
@pytest.mark.skip(reason="write test")
def test_field_not_none_organzation(self):
""" Ensure field is set
Field organization must be defined for all tenancy objects
"""
assert self.item.objects is not None

View File

@ -0,0 +1,63 @@
from rest_framework.fields import empty
from api.serializers.core.ticket import TicketSerializer
from core.models.ticket.ticket import Ticket
class RequestTicketSerializer(
TicketSerializer,
):
class Meta:
model = Ticket
fields = [
'id',
'assigned_teams',
'assigned_users',
'category',
'created',
'modified',
'status',
'title',
'description',
'estimate',
'urgency',
'impact',
'priority',
'external_ref',
'external_system',
'ticket_type',
'is_deleted',
'date_closed',
# 'planned_start_date',
# 'planned_finish_date',
# 'real_start_date',
# 'real_finish_date',
'opened_by',
'organization',
'project',
'milestone',
'subscribed_teams',
'subscribed_users',
'ticket_comments',
'url',
]
read_only_fields = [
'id',
'ticket_type',
'url',
]
def __init__(self, instance=None, data=empty, **kwargs):
super().__init__(instance=instance, data=data, **kwargs)
self.fields.fields['category'].queryset = self.fields.fields['category'].queryset.filter(
request = True
)

View File

@ -0,0 +1,195 @@
from django.urls import reverse
from rest_framework import serializers
from rest_framework.fields import empty
from api.serializers.core.ticket_comment import TicketCommentSerializer
from core.forms.validate_ticket import TicketValidation
from core.models.ticket.ticket import Ticket
class TicketSerializer(
serializers.ModelSerializer,
TicketValidation,
):
url = serializers.SerializerMethodField('get_url_ticket')
def get_url_ticket(self, item):
request = self.context.get('request')
kwargs: dict = {
'pk': item.id
}
if item.ticket_type == self.Meta.model.TicketType.CHANGE.value:
view_name = '_api_itim_change'
elif item.ticket_type == self.Meta.model.TicketType.INCIDENT.value:
view_name = '_api_itim_incident'
elif item.ticket_type == self.Meta.model.TicketType.PROBLEM.value:
view_name = '_api_itim_problem'
elif item.ticket_type == self.Meta.model.TicketType.REQUEST.value:
view_name = '_api_assistance_request'
elif item.ticket_type == self.Meta.model.TicketType.PROJECT_TASK.value:
view_name = '_api_project_tasks'
kwargs.update({'project_id': item.project.id})
else:
raise ValueError('Serializer unable to obtain ticket type')
return request.build_absolute_uri(
reverse(
'API:' + view_name + '-detail',
kwargs = kwargs
)
)
ticket_comments = serializers.SerializerMethodField('get_url_ticket_comments')
def get_url_ticket_comments(self, item):
request = self.context.get('request')
kwargs: dict = {
'ticket_id': item.id
}
if item.ticket_type == self.Meta.model.TicketType.CHANGE.value:
view_name = '_api_itim_change_ticket_comments'
elif item.ticket_type == self.Meta.model.TicketType.INCIDENT.value:
view_name = '_api_itim_incident_ticket_comments'
elif item.ticket_type == self.Meta.model.TicketType.PROBLEM.value:
view_name = '_api_itim_problem_ticket_comments'
elif item.ticket_type == self.Meta.model.TicketType.REQUEST.value:
view_name = '_api_assistance_request_ticket_comments'
elif item.ticket_type == self.Meta.model.TicketType.PROJECT_TASK.value:
view_name = '_api_project_tasks_comments'
kwargs.update({'project_id': item.project.id})
else:
raise ValueError('Serializer unable to obtain ticket type')
return request.build_absolute_uri(
reverse(
'API:' + view_name + '-list',
kwargs = kwargs
)
)
class Meta:
model = Ticket
fields = [
'id',
'assigned_teams',
'assigned_users',
'category',
'created',
'modified',
'status',
'title',
'description',
'urgency',
'impact',
'priority',
'external_ref',
'external_system',
'ticket_type',
'is_deleted',
'date_closed',
'planned_start_date',
'planned_finish_date',
'real_start_date',
'real_finish_date',
'opened_by',
'organization',
'project',
'subscribed_teams',
'subscribed_users',
'ticket_comments',
'url',
]
read_only_fields = [
'id',
'url',
]
def __init__(self, instance=None, data=empty, **kwargs):
self.fields.fields['status'].initial = Ticket.TicketStatus.All.NEW
self.fields.fields['status'].default = Ticket.TicketStatus.All.NEW
self.ticket_type_fields = self.Meta.fields
super().__init__(instance=instance, data=data, **kwargs)
self.fields['organization'].required = True
def is_valid(self, *, raise_exception=True) -> bool:
is_valid = False
try:
self.request = self._context['request']
is_valid = super().is_valid(raise_exception=raise_exception)
self._ticket_type = str(self.fields['ticket_type'].choices[self._context['view']._ticket_type_value]).lower().replace(' ', '_')
is_valid = self.validate_ticket()
self.validated_data['ticket_type'] = int(self._context['view']._ticket_type_value)
if self.instance is None:
subscribed_users: list = []
if 'subscribed_users' in self.validated_data:
subscribed_users = self.validated_data['subscribed_users']
self.validated_data['subscribed_users'] = subscribed_users + [ self.validated_data['opened_by'] ]
except Exception as unhandled_exception:
serializers.ParseError(
detail=f"Server encountered an error during validation, Traceback: {unhandled_exception.with_traceback}"
)
return is_valid

View File

@ -0,0 +1,44 @@
from django.urls import reverse
from rest_framework import serializers
from rest_framework.fields import empty
from api.serializers.core.ticket_comment import TicketCommentSerializer
from core.forms.validate_ticket import TicketValidation
from core.models.ticket.ticket_category import TicketCategory
class TicketCategorySerializer(
serializers.ModelSerializer,
):
url = serializers.HyperlinkedIdentityField(
view_name="API:_api_ticket_category-detail", format="html"
)
class Meta:
model = TicketCategory
fields = '__all__'
read_only_fields = [
'id',
'url',
]
def __init__(self, instance=None, data=empty, **kwargs):
if instance is not None:
if hasattr(instance, 'id'):
self.fields.fields['parent'].queryset = self.fields.fields['parent'].queryset.exclude(
id=instance.id
)
super().__init__(instance=instance, data=data, **kwargs)

View File

@ -0,0 +1,74 @@
from django.urls import reverse
from rest_framework import serializers
from rest_framework.fields import empty
from core.models.ticket.ticket_comment import Ticket, TicketComment
class TicketCommentSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField('get_url_ticket_comment')
def get_url_ticket_comment(self, item):
request = self.context.get('request')
if item.ticket.ticket_type == item.ticket.__class__.TicketType.CHANGE:
view_name = '_api_itim_change_ticket_comments'
elif item.ticket.ticket_type == item.ticket.__class__.TicketType.INCIDENT:
view_name = '_api_itim_incident_ticket_comments'
elif item.ticket.ticket_type == item.ticket.__class__.TicketType.PROBLEM:
view_name = '_api_itim_problem_ticket_comments'
elif item.ticket.ticket_type == item.ticket.__class__.TicketType.REQUEST:
view_name = '_api_assistance_request_ticket_comments'
else:
raise ValueError('Serializer unable to obtain ticket type')
return request.build_absolute_uri(
reverse('API:' + view_name + '-detail',
kwargs={
'ticket_id': item.ticket.id,
'pk': item.id
}
)
)
class Meta:
model = TicketComment
fields = '__all__'
def __init__(self, instance=None, data=empty, **kwargs):
if 'context' in self._kwargs:
if 'view' in self._kwargs['context']:
if 'ticket_id' in self._kwargs['context']['view'].kwargs:
ticket = Ticket.objects.get(pk=int(self._kwargs['context']['view'].kwargs['ticket_id']))
self.fields.fields['organization'].initial = ticket.organization.id
self.fields.fields['ticket'].initial = int(self._kwargs['context']['view'].kwargs['ticket_id'])
self.fields.fields['comment_type'].initial = TicketComment.CommentType.COMMENT
self.fields.fields['user'].initial = kwargs['context']['request']._user.id
super().__init__(instance=instance, data=data, **kwargs)

View File

@ -0,0 +1,42 @@
from django.urls import reverse
from rest_framework import serializers
from rest_framework.fields import empty
from core.models.ticket.ticket_comment_category import TicketCommentCategory
class TicketCommentCategorySerializer(
serializers.ModelSerializer,
):
url = serializers.HyperlinkedIdentityField(
view_name="API:_api_ticket_comment_category-detail", format="html"
)
class Meta:
model = TicketCommentCategory
fields = '__all__'
read_only_fields = [
'id',
'url',
]
def __init__(self, instance=None, data=empty, **kwargs):
if instance is not None:
if hasattr(instance, 'id'):
self.fields.fields['parent'].queryset = self.fields.fields['parent'].queryset.exclude(
id=instance.id
)
super().__init__(instance=instance, data=data, **kwargs)

View File

@ -53,7 +53,6 @@ class DeviceSerializer(serializers.ModelSerializer):
class Meta:
model = Device
depth = 1
fields = [
'id',
'is_global',

View File

@ -0,0 +1,63 @@
from rest_framework.fields import empty
from api.serializers.core.ticket import TicketSerializer
from core.models.ticket.ticket import Ticket
class ChangeTicketSerializer(
TicketSerializer,
):
class Meta:
model = Ticket
fields = [
'id',
'assigned_teams',
'assigned_users',
'category',
'created',
'modified',
'status',
'title',
'description',
'estimate',
'urgency',
'impact',
'priority',
'external_ref',
'external_system',
'ticket_type',
'is_deleted',
'date_closed',
# 'planned_start_date',
# 'planned_finish_date',
# 'real_start_date',
# 'real_finish_date',
'opened_by',
'organization',
'project',
'milestone',
'subscribed_teams',
'subscribed_users',
'ticket_comments',
'url',
]
read_only_fields = [
'id',
'ticket_type',
'url',
]
def __init__(self, instance=None, data=empty, **kwargs):
super().__init__(instance=instance, data=data, **kwargs)
self.fields.fields['category'].queryset = self.fields.fields['category'].queryset.filter(
project_task = True
)

View File

@ -0,0 +1,63 @@
from rest_framework.fields import empty
from api.serializers.core.ticket import TicketSerializer
from core.models.ticket.ticket import Ticket
class IncidentTicketSerializer(
TicketSerializer,
):
class Meta:
model = Ticket
fields = [
'id',
'assigned_teams',
'assigned_users',
'category',
'created',
'modified',
'status',
'title',
'description',
'estimate',
'urgency',
'impact',
'priority',
'external_ref',
'external_system',
'ticket_type',
'is_deleted',
'date_closed',
# 'planned_start_date',
# 'planned_finish_date',
# 'real_start_date',
# 'real_finish_date',
'opened_by',
'organization',
'project',
'milestone',
'subscribed_teams',
'subscribed_users',
'ticket_comments',
'url',
]
read_only_fields = [
'id',
'ticket_type',
'url',
]
def __init__(self, instance=None, data=empty, **kwargs):
super().__init__(instance=instance, data=data, **kwargs)
self.fields.fields['category'].queryset = self.fields.fields['category'].queryset.filter(
incident = True
)

View File

@ -0,0 +1,63 @@
from rest_framework.fields import empty
from api.serializers.core.ticket import TicketSerializer
from core.models.ticket.ticket import Ticket
class ProblemTicketSerializer(
TicketSerializer,
):
class Meta:
model = Ticket
fields = [
'id',
'assigned_teams',
'assigned_users',
'category',
'created',
'modified',
'status',
'title',
'description',
'estimate',
'urgency',
'impact',
'priority',
'external_ref',
'external_system',
'ticket_type',
'is_deleted',
'date_closed',
# 'planned_start_date',
# 'planned_finish_date',
# 'real_start_date',
# 'real_finish_date',
'opened_by',
'organization',
'project',
'milestone',
'subscribed_teams',
'subscribed_users',
'ticket_comments',
'url',
]
read_only_fields = [
'id',
'ticket_type',
'url',
]
def __init__(self, instance=None, data=empty, **kwargs):
super().__init__(instance=instance, data=data, **kwargs)
self.fields.fields['category'].queryset = self.fields.fields['category'].queryset.filter(
problem = True
)

View File

@ -0,0 +1,74 @@
from django.urls import reverse
from rest_framework import serializers
from rest_framework.fields import empty
from project_management.models.projects import Project
from project_management.models.project_milestone import ProjectMilestone
class ProjectMilestoneSerializer(
serializers.ModelSerializer,
):
url = serializers.SerializerMethodField('get_url_project_milestone')
def get_url_project_milestone(self, item):
request = self.context.get('request')
return request.build_absolute_uri(
reverse('API:_api_project_milestone-detail',
kwargs={
'project_id': item.project.id,
'pk': item.id
}
)
)
class Meta:
model = ProjectMilestone
fields = [
'name',
'description',
'organization',
'project',
'start_date',
'finish_date',
'created',
'modified',
'url',
]
read_only_fields = [
'id',
'url',
]
def __init__(self, instance=None, data=empty, **kwargs):
self.fields.fields['organization'].read_only = True
self.fields.fields['project'].read_only = True
super().__init__(instance=instance, data=data, **kwargs)
def is_valid(self, *, raise_exception=False):
is_valid = super().is_valid(raise_exception=raise_exception)
project = Project.objects.get(
pk = int(self._kwargs['context']['view'].kwargs['project_id'])
)
self._validated_data.update({
'organization': project.organization,
'project': project
})
return is_valid

View File

@ -0,0 +1,33 @@
from django.urls import reverse
from rest_framework import serializers
from rest_framework.fields import empty
from project_management.models.project_states import ProjectState
class ProjectStateSerializer(
serializers.ModelSerializer,
):
url = serializers.HyperlinkedIdentityField(
view_name="API:_api_project_state-detail", format="html"
)
class Meta:
model = ProjectState
fields = '__all__'
read_only_fields = [
'id',
'url',
]
def __init__(self, instance=None, data=empty, **kwargs):
super().__init__(instance=instance, data=data, **kwargs)

View File

@ -0,0 +1,63 @@
from rest_framework.fields import empty
from api.serializers.core.ticket import TicketSerializer
from core.models.ticket.ticket import Ticket
class ProjectTaskSerializer(
TicketSerializer,
):
class Meta:
model = Ticket
fields = [
'id',
'assigned_teams',
'assigned_users',
'category',
'created',
'modified',
'status',
'title',
'description',
'estimate',
'urgency',
'impact',
'priority',
'external_ref',
'external_system',
'ticket_type',
'is_deleted',
'date_closed',
'planned_start_date',
'planned_finish_date',
'real_start_date',
'real_finish_date',
'opened_by',
'organization',
'project',
'milestone',
'subscribed_teams',
'subscribed_users',
'ticket_comments',
'url',
]
read_only_fields = [
'id',
'ticket_type',
'url',
]
def __init__(self, instance=None, data=empty, **kwargs):
super().__init__(instance=instance, data=data, **kwargs)
self.fields.fields['category'].queryset = self.fields.fields['category'].queryset.filter(
project_task = True
)

View File

@ -0,0 +1,33 @@
from django.urls import reverse
from rest_framework import serializers
from rest_framework.fields import empty
from project_management.models.project_types import ProjectType
class ProjectTypeSerializer(
serializers.ModelSerializer,
):
url = serializers.HyperlinkedIdentityField(
view_name="API:_api_project_state-detail", format="html"
)
class Meta:
model = ProjectType
fields = '__all__'
read_only_fields = [
'id',
'url',
]
def __init__(self, instance=None, data=empty, **kwargs):
super().__init__(instance=instance, data=data, **kwargs)

View File

@ -0,0 +1,134 @@
from django.urls import reverse
from rest_framework import serializers
from rest_framework.fields import empty
from project_management.models.projects import Project
class ProjectSerializer(
serializers.ModelSerializer,
):
percent_completed = serializers.CharField(
read_only = True,
)
url = serializers.SerializerMethodField('get_url')
def get_url(self, item):
request = self.context.get('request')
return request.build_absolute_uri(reverse("API:_api_projects-detail", args=[item.pk]))
project_tasks_url = serializers.SerializerMethodField('get_url_project_tasks')
def get_url_project_tasks(self, item):
request = self.context.get('request')
return request.build_absolute_uri(
reverse(
'API:_api_project_tasks-list',
kwargs={
'project_id': item.id
}
)
)
project_milestone_url = serializers.SerializerMethodField('get_url_project_milestone')
def get_url_project_milestone(self, item):
request = self.context.get('request')
return request.build_absolute_uri(
reverse(
'API:_api_project_milestone-list',
kwargs={
'project_id': item.id
}
)
)
class Meta:
model = Project
fields = [
'id',
'organization',
'state',
'project_type',
'priority',
'name',
'description',
'code',
'planned_start_date',
'planned_finish_date',
'real_start_date',
'real_finish_date',
'manager_user',
'manager_team',
'team_members',
'project_tasks_url',
'project_milestone_url',
'percent_completed',
'created',
'modified',
'url',
]
read_only_fields = [
'id',
'url',
'created',
'modified',
]
class ProjectImportSerializer(ProjectSerializer):
class Meta:
model = Project
fields = [
'id',
'organization',
'state',
'project_type',
'priority',
'name',
'description',
'code',
'planned_start_date',
'planned_finish_date',
'real_start_date',
'real_finish_date',
'manager_user',
'manager_team',
'team_members',
'project_tasks_url',
'project_milestone_url',
'percent_completed',
'created',
'modified',
'external_ref',
'external_system',
'is_deleted',
'url',
]
read_only_fields = [
'id',
'url',
]

View File

@ -194,7 +194,7 @@ class APIPermissionAdd:
def test_add_has_permission(self):
""" Check correct permission for add
Attempt to add as user with no permission
Attempt to add as user with permission
"""
client = Client()

View File

@ -502,7 +502,8 @@ class InventoryAPIDifferentNameSerialNumberMatch(TestCase):
Device.objects.create(
name='random device name',
serial_number='serial_number_123'
serial_number='serial_number_123',
organization = organization,
)
add_permissions = Permission.objects.get(
@ -537,7 +538,7 @@ class InventoryAPIDifferentNameSerialNumberMatch(TestCase):
process_inventory(json.dumps(self.inventory), organization.id)
self.device = Device.objects.get(name=self.inventory['details']['name'])
self.device = Device.objects.get(name=self.inventory['details']['name'], organization = organization)
self.operating_system = OperatingSystem.objects.get(name=self.inventory['os']['name'])
@ -778,7 +779,8 @@ class InventoryAPIDifferentNameUUIDMatch(TestCase):
Device.objects.create(
name='random device name',
uuid='123-456-789'
uuid='123-456-789',
organization = organization,
)
add_permissions = Permission.objects.get(

View File

@ -5,6 +5,25 @@ from rest_framework.urlpatterns import format_suffix_patterns
from .views import access, config, index
from api.views.settings import permissions
from api.views.settings import index as settings
from api.views import assistance, itim, project_management
from api.views.assistance import request_ticket
from api.views.core import (
ticket_categories,
ticket_comment_categories,
ticket_comments as core_ticket_comments
)
from api.views.itim import change_ticket, incident_ticket, problem_ticket
from api.views.project_management import (
projects,
project_milestone,
project_state,
project_type,
project_task
)
from .views.itam import software, config as itam_config
from .views.itam.device import DeviceViewSet
from .views.itam import inventory
@ -13,15 +32,47 @@ from .views.itam import inventory
app_name = "API"
router = DefaultRouter()
router = DefaultRouter(trailing_slash=False)
router.register('', index.Index, basename='_api_home')
router.register('assistance/request', request_ticket.View, basename='_api_assistance_request')
router.register('assistance/request/(?P<ticket_id>[0-9]+)/comments', core_ticket_comments.View, basename='_api_assistance_request_ticket_comments')
router.register('device', DeviceViewSet, basename='device')
router.register('itim/change', change_ticket.View, basename='_api_itim_change')
router.register('itim/change/(?P<ticket_id>[0-9]+)/comments', core_ticket_comments.View, basename='_api_itim_change_ticket_comments')
router.register('itim/incident', incident_ticket.View, basename='_api_itim_incident')
router.register('itim/incident/(?P<ticket_id>[0-9]+)/comments', core_ticket_comments.View, basename='_api_itim_incident_ticket_comments')
router.register('itim/problem', problem_ticket.View, basename='_api_itim_problem')
router.register('itim/problem/(?P<ticket_id>[0-9]+)/comments', core_ticket_comments.View, basename='_api_itim_problem_ticket_comments')
router.register('project_management/projects', projects.View, basename='_api_projects')
router.register('project_management/projects/(?P<project_id>[0-9]+)/milestones', project_milestone.View, basename='_api_project_milestone')
router.register('project_management/projects/(?P<project_id>[0-9]+)/tasks', project_task.View, basename='_api_project_tasks')
router.register('project_management/projects/(?P<project_id>[0-9]+)/tasks/(?P<ticket_id>[0-9]+)/comments', core_ticket_comments.View, basename='_api_project_tasks_comments')
router.register('settings/ticket_categories', ticket_categories.View, basename='_api_ticket_category')
router.register('settings/project_state', project_state.View, basename='_api_project_state')
router.register('settings/project_type', project_type.View, basename='_api_project_type')
router.register('settings/ticket_comment_categories', ticket_comment_categories.View, basename='_api_ticket_comment_category')
router.register('software', software.SoftwareViewSet, basename='software')
urlpatterns = [
path("assistance", assistance.index.Index.as_view(), name="_api_assistance"),
#
# Sof Old Paths to be refactored
#
path("config/<slug:slug>/", itam_config.View.as_view(), name="_api_device_config"),
path("configuration/", config.ConfigGroupsList.as_view(), name='_api_config_groups'),
@ -29,6 +80,8 @@ urlpatterns = [
path("device/inventory", inventory.Collect.as_view(), name="_api_device_inventory"),
path("itim", itim.index.Index.as_view(), name="_api_itim"),
path("organization/", access.OrganizationList.as_view(), name='_api_orgs'),
path("organization/<int:pk>/", access.OrganizationDetail.as_view(), name='_api_organization'),
path("organization/<int:organization_id>/team", access.TeamList.as_view(), name='_api_organization_teams'),
@ -36,6 +89,11 @@ urlpatterns = [
path("organization/<int:organization_id>/team/<int:group_ptr_id>/permissions", access.TeamPermissionDetail.as_view(), name='_api_team_permission'),
path("organization/team/", access.TeamList.as_view(), name='_api_teams'),
path("project_management", project_management.index.Index.as_view(), name="_api_project_management"),
path("settings", settings.View.as_view(), name='_settings'),
path("settings/permissions", permissions.View.as_view(), name='_settings_permissions'),
]
urlpatterns = format_suffix_patterns(urlpatterns)

View File

@ -0,0 +1 @@
from .index import *

View File

@ -0,0 +1,35 @@
from django.utils.safestring import mark_safe
from rest_framework import generics, permissions, routers, views
# from rest_framework.decorators import api_view
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.reverse import reverse
class Index(views.APIView):
permission_classes = [
IsAuthenticated,
]
def get_view_name(self):
return "Assistance"
def get_view_description(self, html=False) -> str:
text = "Assistance Module"
if html:
return mark_safe(f"<p>{text}</p>")
else:
return text
def get(self, request, *args, **kwargs):
body: dict = {
'requests': reverse('API:_api_assistance_request-list', request=request)
}
return Response(body)

View File

@ -0,0 +1,77 @@
from drf_spectacular.utils import extend_schema, OpenApiResponse
from api.serializers.assistance.request import RequestTicketSerializer
from api.views.core.tickets import View
class View(View):
_ticket_type:str = 'request'
@extend_schema(
summary='Create a ticket',
description = """This model includes all of the ticket types.
Due to this not all fields will be available and what fields are available
depends upon the comment type. see
[administration docs](https://nofusscomputing.com/projects/centurion_erp/administration/core/ticketing/index.html) for more info.
""",
request = RequestTicketSerializer,
responses = {
201: OpenApiResponse(
response = RequestTicketSerializer,
),
}
)
def create(self, request, *args, **kwargs):
return super().create(request, *args, **kwargs)
@extend_schema(
summary='Fetch all tickets',
description = """This model includes all of the ticket comment types.
Due to this not all fields will be available and what fields are available
depends upon the comment type. see
[administration docs](https://nofusscomputing.com/projects/centurion_erp/administration/core/ticketing/index.html) for more info.
""",
methods=["GET"],
responses = {
200: OpenApiResponse(
description='Success',
response = RequestTicketSerializer
)
}
)
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
@extend_schema(
summary='Fetch the selected ticket',
description = """This model includes all of the ticket comment types.
Due to this not all fields will be available and what fields are available
depends upon the comment type. see
[administration docs](https://nofusscomputing.com/projects/centurion_erp/administration/core/ticketing/index.html) for more info.
""",
methods=["GET"],
responses = {
200: OpenApiResponse(
description='Success',
response = RequestTicketSerializer
)
}
)
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
def get_view_name(self):
if self.detail:
return "Request Ticket"
return 'Request Tickets'

View File

@ -0,0 +1,79 @@
from django.shortcuts import get_object_or_404
from drf_spectacular.utils import extend_schema, OpenApiResponse
from rest_framework import generics, viewsets
from access.mixin import OrganizationMixin
from api.serializers.core.ticket_category import TicketCategory, TicketCategorySerializer
from api.views.mixin import OrganizationPermissionAPI
class View(OrganizationMixin, viewsets.ModelViewSet):
permission_classes = [
OrganizationPermissionAPI
]
queryset = TicketCategory.objects.all()
serializer_class = TicketCategorySerializer
@extend_schema(
summary='Create a ticket category',
request = TicketCategorySerializer,
responses = {
201: OpenApiResponse(description='Ticket category created', response=TicketCategorySerializer),
403: OpenApiResponse(description='User tried to edit field they dont have access to'),
}
)
def create(self, request, *args, **kwargs):
return super().create(request, *args, **kwargs)
@extend_schema(
summary='Fetch all of a tickets category',
methods=["GET"],
responses = {
200: OpenApiResponse(description='Success', response=TicketCategorySerializer),
}
)
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
@extend_schema(
summary='Fetch the selected ticket category',
methods=["GET"],
responses = {
200: OpenApiResponse(description='Success', response=TicketCategorySerializer),
}
)
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
@extend_schema(
summary='Update a ticket category',
methods=["PUT"],
responses = {
200: OpenApiResponse(description='Ticket comment updated', response=TicketCategorySerializer),
403: OpenApiResponse(description='User tried to edit field they dont have access to'),
}
)
def update(self, request, *args, **kwargs):
return super().update(request, *args, **kwargs)
def get_view_name(self):
if self.detail:
return "Ticket Category"
return 'Ticket Categories'

View File

@ -0,0 +1,79 @@
from django.shortcuts import get_object_or_404
from drf_spectacular.utils import extend_schema, OpenApiResponse
from rest_framework import generics, viewsets
from access.mixin import OrganizationMixin
from api.serializers.core.ticket_comment_category import TicketCommentCategory, TicketCommentCategorySerializer
from api.views.mixin import OrganizationPermissionAPI
class View(OrganizationMixin, viewsets.ModelViewSet):
permission_classes = [
OrganizationPermissionAPI
]
queryset = TicketCommentCategory.objects.all()
serializer_class = TicketCommentCategorySerializer
@extend_schema(
summary='Create a ticket comment category',
request = TicketCommentCategorySerializer,
responses = {
201: OpenApiResponse(description='Ticket category created', response=TicketCommentCategorySerializer),
403: OpenApiResponse(description='User tried to edit field they dont have access to'),
}
)
def create(self, request, *args, **kwargs):
return super().create(request, *args, **kwargs)
@extend_schema(
summary='Fetch all of the ticket comment categories',
methods=["GET"],
responses = {
200: OpenApiResponse(description='Success', response=TicketCommentCategorySerializer),
}
)
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
@extend_schema(
summary='Fetch the selected ticket comment category',
methods=["GET"],
responses = {
200: OpenApiResponse(description='Success', response=TicketCommentCategorySerializer),
}
)
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
@extend_schema(
summary='Update a ticket comment category',
methods=["PUT"],
responses = {
200: OpenApiResponse(description='Ticket comment updated', response=TicketCommentCategorySerializer),
403: OpenApiResponse(description='User tried to edit field they dont have access to'),
}
)
def update(self, request, *args, **kwargs):
return super().update(request, *args, **kwargs)
def get_view_name(self):
if self.detail:
return "Ticket Comment Category"
return 'Ticket Comment Categories'

View File

@ -0,0 +1,102 @@
from django.shortcuts import get_object_or_404
from drf_spectacular.utils import extend_schema, OpenApiResponse
from rest_framework import generics, viewsets
from access.mixin import OrganizationMixin
from api.serializers.core.ticket_comment import TicketCommentSerializer
from api.views.mixin import OrganizationPermissionAPI
from core.models.ticket.ticket_comment import TicketComment
class View(OrganizationMixin, viewsets.ModelViewSet):
permission_classes = [
OrganizationPermissionAPI
]
queryset = TicketComment.objects.all()
serializer_class = TicketCommentSerializer
@extend_schema(
summary='Create a ticket comment',
description = """This model includes all of the ticket comment types.
Due to this not all fields will be available and what fields are available
depends upon the comment type.
""",
request = TicketCommentSerializer,
responses = {
201: OpenApiResponse(description='Ticket comment created', response=TicketCommentSerializer),
403: OpenApiResponse(description='User tried to edit field they dont have access to'),
}
)
def create(self, request, *args, **kwargs):
return super().create(request, *args, **kwargs)
@extend_schema(
summary='Fetch all of a tickets comments',
methods=["GET"],
responses = {
200: OpenApiResponse(description='Success', response=TicketCommentSerializer),
}
)
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
@extend_schema(
summary='Fetch the selected ticket Comment',
methods=["GET"],
responses = {
200: OpenApiResponse(description='Success', response=TicketCommentSerializer),
}
)
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
@extend_schema(
summary='Update a ticket Comment',
description = """This model includes all of the ticket comment types.
Due to this not all fields will be available and what fields are available
depends upon the comment type.
""",
methods=["PUT"],
responses = {
200: OpenApiResponse(description='Ticket comment updated', response=TicketCommentSerializer),
403: OpenApiResponse(description='User tried to edit field they dont have access to'),
}
)
def update(self, request, *args, **kwargs):
return super().update(request, *args, **kwargs)
def get_queryset(self):
if 'ticket_id' in self.kwargs:
self.queryset = self.queryset.filter(ticket=self.kwargs['ticket_id']).order_by('created')
if 'pk' in self.kwargs:
self.queryset = self.queryset.filter(pk = self.kwargs['pk'])
return self.queryset
def get_view_name(self):
if self.detail:
return "Ticket Comment"
return 'Ticket Comments'

View File

@ -0,0 +1,145 @@
from django.db.models import Q
from rest_framework import generics, viewsets
from access.mixin import OrganizationMixin
from api.serializers.assistance.request import RequestTicketSerializer
from api.serializers.itim.change import ChangeTicketSerializer
from api.serializers.itim.incident import IncidentTicketSerializer
from api.serializers.itim.problem import ProblemTicketSerializer
from api.serializers.project_management.project_task import ProjectTaskSerializer
from api.views.mixin import OrganizationPermissionAPI
from core.models.ticket.ticket import Ticket
class View(OrganizationMixin, viewsets.ModelViewSet):
filterset_fields = [
'external_system',
'external_ref',
]
search_fields = [
'title',
'description',
]
permission_classes = [
OrganizationPermissionAPI
]
def get_dynamic_permissions(self):
if self.action == 'create':
action_keyword = 'add'
elif self.action == 'destroy':
action_keyword = 'delete'
elif self.action == 'list':
action_keyword = 'view'
elif self.action == 'partial_update':
action_keyword = 'change'
elif self.action == 'retrieve':
action_keyword = 'view'
elif self.action == 'update':
action_keyword = 'change'
elif self.action is None:
action_keyword = 'view'
else:
raise ValueError('unable to determin the action_keyword')
self.permission_required = [
'core.' + action_keyword + '_ticket_' + self._ticket_type,
]
return super().get_permission_required()
queryset = Ticket.objects.all()
def get_serializer(self, *args, **kwargs):
if self._ticket_type == 'change':
self.serializer_class = ChangeTicketSerializer
self._ticket_type_value = Ticket.TicketType.CHANGE.value
elif self._ticket_type == 'incident':
self.serializer_class = IncidentTicketSerializer
self._ticket_type_value = Ticket.TicketType.INCIDENT.value
elif self._ticket_type == 'problem':
self.serializer_class = ProblemTicketSerializer
self._ticket_type_value = Ticket.TicketType.PROBLEM.value
elif self._ticket_type == 'request':
self.serializer_class = RequestTicketSerializer
self._ticket_type_value = Ticket.TicketType.REQUEST.value
elif self._ticket_type == 'project_task':
self.serializer_class = ProjectTaskSerializer
self._ticket_type_value = Ticket.TicketType.PROJECT_TASK.value
else:
raise ValueError('unable to determin the serializer_class')
return super().get_serializer(*args, **kwargs)
def get_queryset(self):
if self._ticket_type == 'change':
ticket_type = self.queryset.model.TicketType.CHANGE.value
elif self._ticket_type == 'incident':
ticket_type = self.queryset.model.TicketType.INCIDENT.value
elif self._ticket_type == 'problem':
ticket_type = self.queryset.model.TicketType.PROBLEM.value
elif self._ticket_type == 'request':
ticket_type = self.queryset.model.TicketType.REQUEST.value
elif self._ticket_type == 'project_task':
ticket_type = self.queryset.model.TicketType.REQUEST.value
return self.queryset.filter(
project = self.kwargs['project_id']
)
else:
raise ValueError('Unknown ticket type. kwarg `ticket_type` must be set')
return self.queryset.filter(
ticket_type = ticket_type
)

View File

@ -1,15 +1,19 @@
# from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
from django.contrib.auth.models import User
from django.utils.safestring import mark_safe
from rest_framework import generics, permissions, routers, viewsets
from rest_framework.decorators import api_view
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.reverse import reverse
class Index(viewsets.ViewSet):
# permission_required = 'access.view_organization'
permission_classes = [
IsAuthenticated,
]
def get_view_name(self):
return "API Index"
@ -26,9 +30,13 @@ class Index(viewsets.ViewSet):
return Response(
{
# "teams": reverse("_api_teams", request=request),
'assistance': reverse("API:_api_assistance", request=request),
"devices": reverse("API:device-list", request=request),
"config_groups": reverse("API:_api_config_groups", request=request),
'itim': reverse("API:_api_itim", request=request),
"organizations": reverse("API:_api_orgs", request=request),
'project_management': reverse("API:_api_project_management", request=request),
"settings": reverse('API:_settings', request=request),
"software": reverse("API:software-list", request=request),
}
)

View File

@ -1,9 +1,10 @@
from django.db.models import Q
from django.shortcuts import get_object_or_404
from drf_spectacular.utils import extend_schema
from drf_spectacular.utils import extend_schema, OpenApiResponse
from rest_framework import generics, viewsets
from rest_framework.response import Response
from access.mixin import OrganizationMixin
@ -24,6 +25,46 @@ class DeviceViewSet(OrganizationMixin, viewsets.ModelViewSet):
serializer_class = DeviceSerializer
@extend_schema(
summary = 'Create a device',
description="""Add a new device to the ITAM database.
If you attempt to create a device and a device with a matching name and uuid or name and serial number
is found within the database, it will not re-create it. The device will be returned within the message body.
""",
methods=["POST"],
responses = {
200: OpenApiResponse(description='Device allready exists', response=DeviceSerializer),
201: OpenApiResponse(description='Device created', response=DeviceSerializer),
400: OpenApiResponse(description='Validation failed.'),
403: OpenApiResponse(description='User is missing create permissions'),
}
)
def create(self, request, *args, **kwargs):
current_device = []
if 'uuid' in self.request.POST:
current_device = self.serializer_class.Meta.model.objects.filter(
organization = int(self.request.POST['organization']),
uuid = str(self.request.POST['uuid'])
)
if 'serial_number' in self.request.POST and len(current_device) == 0:
current_device = self.serializer_class.Meta.model.objects.filter(
organization = int(self.request.POST['organization']),
serial_number = str(self.request.POST['serial_number'])
)
if len(current_device) == 1:
instance = current_device.get()
serializer = self.get_serializer(instance)
return Response(serializer.data)
return super().create(request, *args, **kwargs)
@extend_schema( description='Fetch devices that are from the users assigned organization(s)', methods=["GET"])
def list(self, request):

View File

@ -0,0 +1 @@
from .index import *

View File

@ -0,0 +1,82 @@
from drf_spectacular.utils import extend_schema, OpenApiResponse
from api.serializers.itim.change import ChangeTicketSerializer
from api.views.core.tickets import View
class View(View):
_ticket_type:str = 'change'
@extend_schema(
summary='Create a ticket',
description = """This model includes all of the ticket types.
Due to this not all fields will be available and what fields are available
depends upon the comment type. see
[administration docs](https://nofusscomputing.com/projects/centurion_erp/administration/core/ticketing/index.html) for more info.
""",
request = ChangeTicketSerializer,
responses = {
201: OpenApiResponse(
response = ChangeTicketSerializer,
),
}
)
def create(self, request, *args, **kwargs):
return super().create(request, *args, **kwargs)
@extend_schema(
summary='Fetch all tickets',
description = """This model includes all of the ticket comment types.
Due to this not all fields will be available and what fields are available
depends upon the comment type. see
[administration docs](https://nofusscomputing.com/projects/centurion_erp/administration/core/ticketing/index.html) for more info.
""",
methods=["GET"],
responses = {
200: OpenApiResponse(
description='Success',
response = ChangeTicketSerializer
)
}
)
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
@extend_schema(
summary='Fetch the selected ticket',
description = """This model includes all of the ticket comment types.
Due to this not all fields will be available and what fields are available
depends upon the comment type. see
[administration docs](https://nofusscomputing.com/projects/centurion_erp/administration/core/ticketing/index.html) for more info.
""",
methods=["GET"],
responses = {
200: OpenApiResponse(
description='Success',
response = ChangeTicketSerializer
)
}
)
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
def get_view_name(self):
if self.detail:
return "Change Ticket"
return 'Change Tickets'

View File

@ -0,0 +1,81 @@
from drf_spectacular.utils import extend_schema, OpenApiResponse
from api.serializers.itim.incident import IncidentTicketSerializer
from api.views.core.tickets import View
class View(View):
_ticket_type:str = 'incident'
@extend_schema(
summary='Create a ticket',
description = """This model includes all of the ticket types.
Due to this not all fields will be available and what fields are available
depends upon the comment type. see
[administration docs](https://nofusscomputing.com/projects/centurion_erp/administration/core/ticketing/index.html) for more info.
""",
request = IncidentTicketSerializer,
responses = {
201: OpenApiResponse(
response = IncidentTicketSerializer,
),
}
)
def create(self, request, *args, **kwargs):
return super().create(request, *args, **kwargs)
@extend_schema(
summary='Fetch all tickets',
description = """This model includes all of the ticket comment types.
Due to this not all fields will be available and what fields are available
depends upon the comment type. see
[administration docs](https://nofusscomputing.com/projects/centurion_erp/administration/core/ticketing/index.html) for more info.
""",
methods=["GET"],
responses = {
200: OpenApiResponse(
description='Success',
response = IncidentTicketSerializer
)
}
)
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
@extend_schema(
summary='Fetch the selected ticket',
description = """This model includes all of the ticket comment types.
Due to this not all fields will be available and what fields are available
depends upon the comment type. see
[administration docs](https://nofusscomputing.com/projects/centurion_erp/administration/core/ticketing/index.html) for more info.
""",
methods=["GET"],
responses = {
200: OpenApiResponse(
description='Success',
response = IncidentTicketSerializer
)
}
)
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
def get_view_name(self):
if self.detail:
return "Incident Ticket"
return 'Incident Tickets'

View File

@ -0,0 +1,36 @@
from django.utils.safestring import mark_safe
from rest_framework import views
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.reverse import reverse
class Index(views.APIView):
permission_classes = [
IsAuthenticated,
]
def get_view_name(self):
return "ITIM"
def get_view_description(self, html=False) -> str:
text = "ITIM Module"
if html:
return mark_safe(f"<p>{text}</p>")
else:
return text
def get(self, request, *args, **kwargs):
body: dict = {
'changes': reverse('API:_api_itim_change-list', request=request),
'incidents': reverse('API:_api_itim_incident-list', request=request),
'problems': reverse('API:_api_itim_problem-list', request=request),
}
return Response(body)

View File

@ -0,0 +1,81 @@
from drf_spectacular.utils import extend_schema, OpenApiResponse
from api.serializers.itim.problem import ProblemTicketSerializer
from api.views.core.tickets import View
class View(View):
_ticket_type:str = 'problem'
@extend_schema(
summary='Create a ticket',
description = """This model includes all of the ticket types.
Due to this not all fields will be available and what fields are available
depends upon the comment type. see
[administration docs](https://nofusscomputing.com/projects/centurion_erp/administration/core/ticketing/index.html) for more info.
""",
request = ProblemTicketSerializer,
responses = {
201: OpenApiResponse(
response = ProblemTicketSerializer,
),
}
)
def create(self, request, *args, **kwargs):
return super().create(request, *args, **kwargs)
@extend_schema(
summary='Fetch all tickets',
description = """This model includes all of the ticket comment types.
Due to this not all fields will be available and what fields are available
depends upon the comment type. see
[administration docs](https://nofusscomputing.com/projects/centurion_erp/administration/core/ticketing/index.html) for more info.
""",
methods=["GET"],
responses = {
200: OpenApiResponse(
description='Success',
response = ProblemTicketSerializer
)
}
)
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
@extend_schema(
summary='Fetch the selected ticket',
description = """This model includes all of the ticket comment types.
Due to this not all fields will be available and what fields are available
depends upon the comment type. see
[administration docs](https://nofusscomputing.com/projects/centurion_erp/administration/core/ticketing/index.html) for more info.
""",
methods=["GET"],
responses = {
200: OpenApiResponse(
description='Success',
response = ProblemTicketSerializer
)
}
)
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
def get_view_name(self):
if self.detail:
return "Problem Ticket"
return 'Problem Tickets'

View File

@ -75,6 +75,12 @@ class OrganizationPermissionAPI(DjangoObjectPermissions, OrganizationMixin):
self.permission_required = [ permission ]
if hasattr(view, 'get_dynamic_permissions'):
self.permission_required = view.get_dynamic_permissions()
if view:
if 'organization_id' in view.kwargs:

View File

@ -0,0 +1 @@
from .index import *

View File

@ -0,0 +1,34 @@
from django.utils.safestring import mark_safe
from rest_framework import generics, permissions, routers, views
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.reverse import reverse
class Index(views.APIView):
permission_classes = [
IsAuthenticated,
]
def get_view_name(self):
return "Projects"
def get_view_description(self, html=False) -> str:
text = "Projects Managementn Module"
if html:
return mark_safe(f"<p>{text}</p>")
else:
return text
def get(self, request, *args, **kwargs):
body: dict = {
'projects': reverse('API:_api_projects-list', request=request)
}
return Response(body)

View File

@ -0,0 +1,90 @@
from drf_spectacular.utils import extend_schema, OpenApiResponse
from rest_framework import viewsets
from access.mixin import OrganizationMixin
from api.serializers.project_management.project_milestone import ProjectMilestone, ProjectMilestoneSerializer
# from api.views.core.tickets import View
from api.views.mixin import OrganizationPermissionAPI
class View(OrganizationMixin, viewsets.ModelViewSet):
permission_classes = [
OrganizationPermissionAPI
]
queryset = ProjectMilestone.objects.all()
serializer_class = ProjectMilestoneSerializer
@extend_schema(
summary='Create a project milestone',
request = ProjectMilestoneSerializer,
responses = {
201: OpenApiResponse(
response = ProjectMilestoneSerializer,
),
}
)
def create(self, request, *args, **kwargs):
return super().create(request, *args, **kwargs)
@extend_schema(
summary='Fetch all project milestones',
methods=["GET"],
responses = {
200: OpenApiResponse(
description='Success',
response = ProjectMilestoneSerializer
)
}
)
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
@extend_schema(
summary='Fetch the selected project milestone',
methods=["GET"],
responses = {
200: OpenApiResponse(
description='Success',
response = ProjectMilestoneSerializer
)
}
)
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
def get_view_name(self):
if self.detail:
return ProjectMilestone._meta.verbose_name
return ProjectMilestone._meta.verbose_name_plural
def get_queryset(self):
if 'project_id' in self.kwargs:
self.queryset = self.queryset.filter(
project=self.kwargs['project_id']
)
if 'pk' in self.kwargs:
self.queryset = self.queryset.filter(
pk = self.kwargs['pk']
)
return self.queryset

View File

@ -0,0 +1,73 @@
from drf_spectacular.utils import extend_schema, OpenApiResponse
from rest_framework import viewsets
from access.mixin import OrganizationMixin
from api.serializers.project_management.project_state import ProjectState, ProjectStateSerializer
from api.views.core.tickets import View
from api.views.mixin import OrganizationPermissionAPI
class View(OrganizationMixin, viewsets.ModelViewSet):
permission_classes = [
OrganizationPermissionAPI
]
queryset = ProjectState.objects.all()
serializer_class = ProjectStateSerializer
@extend_schema(
summary='Create a project state',
request = ProjectStateSerializer,
responses = {
201: OpenApiResponse(
response = ProjectStateSerializer,
),
}
)
def create(self, request, *args, **kwargs):
return super().create(request, *args, **kwargs)
@extend_schema(
summary='Fetch all project states',
methods=["GET"],
responses = {
200: OpenApiResponse(
description='Success',
response = ProjectStateSerializer
)
}
)
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
@extend_schema(
summary='Fetch the selected project state',
methods=["GET"],
responses = {
200: OpenApiResponse(
description='Success',
response = ProjectStateSerializer
)
}
)
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
def get_view_name(self):
if self.detail:
return ProjectState._meta.verbose_name
return ProjectState._meta.verbose_name_plural

View File

@ -0,0 +1,64 @@
from drf_spectacular.utils import extend_schema, OpenApiResponse
from api.serializers.project_management.project_task import ProjectTaskSerializer
from api.views.core.tickets import View
class View(View):
_ticket_type:str = 'project_task'
@extend_schema(
summary='Create a Project Task',
request = ProjectTaskSerializer,
responses = {
201: OpenApiResponse(
response = ProjectTaskSerializer,
),
}
)
def create(self, request, *args, **kwargs):
return super().create(request, *args, **kwargs)
@extend_schema(
summary='Fetch all project tasks',
methods=["GET"],
responses = {
200: OpenApiResponse(
description='Success',
response = ProjectTaskSerializer
)
}
)
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
@extend_schema(
summary='Fetch the selected project task',
methods=["GET"],
responses = {
200: OpenApiResponse(
description='Success',
response = ProjectTaskSerializer
)
}
)
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
def get_view_name(self):
if self.detail:
return "Project Task"
return 'Project Tasks'

View File

@ -0,0 +1,72 @@
from drf_spectacular.utils import extend_schema, OpenApiResponse
from rest_framework import viewsets
from access.mixin import OrganizationMixin
from api.serializers.project_management.project_type import ProjectType, ProjectTypeSerializer
from api.views.mixin import OrganizationPermissionAPI
class View(OrganizationMixin, viewsets.ModelViewSet):
permission_classes = [
OrganizationPermissionAPI
]
queryset = ProjectType.objects.all()
serializer_class = ProjectTypeSerializer
@extend_schema(
summary='Create a project type',
request = ProjectTypeSerializer,
responses = {
201: OpenApiResponse(
response = ProjectTypeSerializer,
),
}
)
def create(self, request, *args, **kwargs):
return super().create(request, *args, **kwargs)
@extend_schema(
summary='Fetch all project types',
methods=["GET"],
responses = {
200: OpenApiResponse(
description='Success',
response = ProjectTypeSerializer
)
}
)
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
@extend_schema(
summary='Fetch the selected project type',
methods=["GET"],
responses = {
200: OpenApiResponse(
description='Success',
response = ProjectTypeSerializer
)
}
)
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
def get_view_name(self):
if self.detail:
return ProjectType._meta.verbose_name
return ProjectType._meta.verbose_name_plural

View File

@ -0,0 +1,116 @@
from django.db.models import Q
from django.shortcuts import get_object_or_404
from drf_spectacular.utils import extend_schema, OpenApiResponse, OpenApiRequest, PolymorphicProxySerializer
from rest_framework import generics, viewsets
from rest_framework.response import Response
from access.mixin import OrganizationMixin
from api.serializers.project_management.projects import ProjectSerializer, ProjectImportSerializer
from api.views.mixin import OrganizationPermissionAPI
from project_management.models.projects import Project
from settings.models.user_settings import UserSettings
class View(OrganizationMixin, viewsets.ModelViewSet):
filterset_fields = [
'external_system',
'external_ref',
]
search_fields = [
'name',
'description',
]
permission_classes = [
OrganizationPermissionAPI
]
queryset = Project.objects.all()
# serializer_class = ProjectSerializer
def get_serializer_class(self):
if self.has_organization_permission(
organization = UserSettings.objects.get(user = self.request.user).default_organization,
permissions_required = ['project_management.import_project']
) or self.request.user.is_superuser:
return ProjectImportSerializer
return ProjectSerializer
@extend_schema(
summary = 'Create a project',
description = """**Note:** Users whom lack permssion `import_project`,
will be unable to add, edit and view fields: `created`, `external_ref`, `external_system`,
and `is_deleted`.
""",
methods=["POST"],
request = ProjectImportSerializer,
responses = {
201: OpenApiResponse(description='project created', response=ProjectImportSerializer),
403: OpenApiResponse(description='User is missing create permissions'),
}
)
def create(self, request, *args, **kwargs):
return super().create(request, *args, **kwargs)
@extend_schema(
summary='Fetch projects',
description = """**Note:** Users whom lack permssion `import_project`,
will be unable to add, edit and view fields: `created`, `external_ref`, `external_system`,
and `is_deleted`.
""",
methods=["GET"],
responses = {
200: OpenApiResponse(description='projects', response=ProjectImportSerializer)
}
)
def list(self, request):
return super().list(request)
@extend_schema(
summary='Fetch the selected project',
description = """**Note:** Users whom lack permssion `import_project`,
will be unable to add, edit and view fields: `created`, `external_ref`, `external_system`,
and `is_deleted`.
""",
methods=["GET"],
responses = {
200: OpenApiResponse(description='projects', response=ProjectImportSerializer)
}
)
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
def get_queryset(self):
if self.request.user.is_superuser:
return self.queryset.filter()
else:
return self.queryset.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True))
def get_view_name(self):
if self.detail:
return "Project"
return 'Projects'

View File

@ -0,0 +1,51 @@
from django.contrib.auth.models import Permission
from drf_spectacular.utils import extend_schema, OpenApiResponse
from rest_framework import views
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.reverse import reverse
from core.http.common import Http
class View(views.APIView):
permission_classes = [
IsAuthenticated,
]
@extend_schema(
summary = "Settings Index Page",
description = """This endpoint provides the available settings as available via the API.
""",
methods=["GET"],
parameters = None,
tags = ['settings',],
responses = {
200: OpenApiResponse(description='Inventory upload successful'),
401: OpenApiResponse(description='User Not logged in'),
500: OpenApiResponse(description='Exception occured. View server logs for the Stack Trace'),
}
)
def get(self, request, *args, **kwargs):
status = Http.Status.OK
response_data: dict = {
"permissions": reverse('API:_settings_permissions', request=request),
"project_state": reverse('API:_api_project_state-list', request=request),
"project_type": reverse('API:_api_project_type-list', request=request),
"ticket_categories": reverse('API:_api_ticket_category-list', request=request),
"ticket_comment_categories": reverse('API:_api_ticket_comment_category-list', request=request)
}
return Response(data=response_data,status=status)
def get_view_name(self):
return "Settings"

View File

@ -0,0 +1,67 @@
from drf_spectacular.utils import extend_schema, OpenApiResponse
from rest_framework import views
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from access.functions import permissions
from core.http.common import Http
class View(views.APIView):
permission_classes = [
IsAuthenticated,
]
@extend_schema(
summary = "Fetch available permissions",
description = """This endpoint provides a list of permissions that are available within
Centurion ERP. The format of each permission is `<app name>.<permission>_<model>`.
This endpoint is available to **all** authenticated users.
""",
methods=["GET"],
parameters = None,
tags = ['settings',],
responses = {
200: OpenApiResponse(description='Inventory upload successful'),
401: OpenApiResponse(description='User Not logged in'),
500: OpenApiResponse(description='Exception occured. View server logs for the Stack Trace'),
}
)
def get(self, request, *args, **kwargs):
status = Http.Status.OK
response_data: list = []
try:
for permission in permissions.permission_queryset():
response_data += [ str(f"{permission.content_type.app_label}.{permission.codename}") ]
except PermissionDenied as e:
status = Http.Status.FORBIDDEN
response_data = ''
except Exception as e:
print(f'An error occured{e}')
status = Http.Status.SERVER_ERROR
response_data = 'Unknown Server Error occured'
return Response(data=response_data,status=status)
def get_view_name(self):
return "Permissions"

View File

@ -108,15 +108,19 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'rest_framework',
'rest_framework_json_api',
'django_filters',
'social_django',
'django_celery_results',
'core.apps.CoreConfig',
'access.apps.AccessConfig',
'itam.apps.ItamConfig',
'itim.apps.ItimConfig',
'assistance.apps.AssistanceConfig',
'settings.apps.SettingsConfig',
'drf_spectacular',
'drf_spectacular_sidecar',
'config_management.apps.ConfigManagementConfig',
'project_management.apps.ProjectManagementConfig',
]
MIDDLEWARE = [
@ -255,7 +259,9 @@ if API_ENABLED:
# ),
'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata',
'DEFAULT_FILTER_BACKENDS': (
'rest_framework_json_api.filters.QueryParameterValidationFilter',
# 'rest_framework_json_api.filters.QueryParameterValidationFilter',
'rest_framework.filters.SearchFilter',
'rest_framework_json_api.django_filters.DjangoFilterBackend',
'rest_framework_json_api.filters.OrderingFilter',
'rest_framework_json_api.django_filters.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
@ -355,12 +361,6 @@ if DEBUG:
"127.0.0.1",
]
# Apps Under Development
INSTALLED_APPS += [
'information.apps.InformationConfig',
'project_management.apps.ProjectManagementConfig',
]
if SSO_ENABLED:

View File

@ -135,7 +135,14 @@ class ModelPermissionsAdd:
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
if self.app_namespace:
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
else:
url = reverse(self.url_name_add, kwargs=self.url_add_kwargs)
response = client.put(url, data=self.add_data)
@ -150,7 +157,14 @@ class ModelPermissionsAdd:
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
if self.app_namespace:
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
else:
url = reverse(self.url_name_add, kwargs=self.url_add_kwargs)
client.force_login(self.no_permissions_user)
@ -167,7 +181,14 @@ class ModelPermissionsAdd:
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
if self.app_namespace:
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
else:
url = reverse(self.url_name_add, kwargs=self.url_add_kwargs)
client.force_login(self.different_organization_user)
@ -183,7 +204,14 @@ class ModelPermissionsAdd:
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
if self.app_namespace:
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
else:
url = reverse(self.url_name_add, kwargs=self.url_add_kwargs)
client.force_login(self.view_user)
@ -195,11 +223,18 @@ class ModelPermissionsAdd:
def test_model_add_has_permission(self):
""" Check correct permission for add
Attempt to add as user with no permission
Attempt to add as user with permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
if self.app_namespace:
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
else:
url = reverse(self.url_name_add, kwargs=self.url_add_kwargs)
client.force_login(self.add_user)

View File

@ -1,6 +1,8 @@
import importlib
import pytest
import unittest
from access.models import TenancyObject
from access.tests.abstract.tenancy_object import TenancyObject as TenancyObjectTestCases
@ -40,6 +42,40 @@ class TenancyModel(
""" Model to test """
def test_field_exists_verbose_name_plural(self):
"""Test for existance of field in `<model>.Meta`
Field is required for `templates/detail.html.js`
Attribute `verbose_name_plural` must be defined in `Meta` class.
"""
assert 'verbose_name_plural' in self.model._meta.original_attrs
def test_field_not_empty_verbose_name_plural(self):
"""Test field `<model>.Meta` is not empty
Field is required for `templates/detail.html.js`
Attribute `verbose_name_plural` must be defined in `Meta` class.
"""
assert self.model._meta.original_attrs['verbose_name_plural'] is not None
def test_field_type_verbose_name_plural(self):
"""Test field `<model>.Meta` is not empty
Field is required for `templates/detail.html.js`
Attribute `verbose_name_plural` must be of type str.
"""
assert type(self.model._meta.original_attrs['verbose_name_plural']) is str
class ModelAdd(
AddView

View File

@ -134,6 +134,34 @@ class AddView:
assert type(viewclass.template_name) is str
def test_view_add_function_get_initial_exists(self):
"""Ensure that get_initial exists
Field `get_initial` must be defined as the base class is used for setup.
"""
module = __import__(self.add_module, fromlist=[self.add_view])
view_class = getattr(module, 'Add')
assert hasattr(view_class, 'get_initial')
def test_view_add_function_get_initial_callable(self):
"""Ensure that get_initial is a function
Field `get_initial` must be callable as it's used for setup.
"""
module = __import__(self.add_module, fromlist=[self.add_view])
view_class = getattr(module, 'Add')
func = getattr(view_class, 'get_initial')
assert callable(func)
class ChangeView:
""" Testing of Display view """
@ -524,6 +552,9 @@ class IndexView:
class AllViews(
AddView,
ChangeView,
@ -563,3 +594,33 @@ class AllViews(
index_view: str = None
""" Index Class name to test """
@pytest.mark.skip(reason='write test')
def test_view_index_attribute_missing_permission_required(self):
""" Attribute missing Test
Ensure that `permission_required` attribute is not defined within the view.
this can be done by mocking the inherited class with the `permission_required` attribute
set to a value that if it changed would be considered defined in the created view.
## Why?
This attribute can be dynamically added based of of the view name along with attributes
`model._meta.model_name` and `str(__class__.__name__).lower()`.
Additional test:
- ensure that the attribute does get automagically created.
- ensure that the classes name is one of add, change, delete, display or index.
"""
@pytest.mark.skip(reason='write test')
def test_view_index_attribute_missing_template_name(self):
""" Attribute missing Test
Ensure that `template_name` attribute is not defined within the view if the value
is `form.html.j2`
this valuse is already defined in the base form
"""

View File

@ -24,7 +24,7 @@ from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
from .views import home
from core.views import history
from core.views import history, related_ticket, ticket_linked_item
from settings.views import user_settings
@ -42,12 +42,19 @@ urlpatterns = [
path("account/", include("django.contrib.auth.urls")),
path("organization/", include("access.urls")),
path("assistance/", include("assistance.urls")),
path("itam/", include("itam.urls")),
path("itim/", include("itim.urls")),
path("config_management/", include("config_management.urls")),
path("history/<str:model_name>/<int:model_pk>", history.View.as_view(), name='_history'),
re_path(r'^static/(?P<path>.*)$', serve,{'document_root': settings.STATIC_ROOT}),
path('ticket/<str:ticket_type>/<int:ticket_id>/relate/add', related_ticket.Add.as_view(), name="_ticket_related_add"),
path('ticket/<str:ticket_type>/<int:ticket_id>/linked_item/add', ticket_linked_item.Add.as_view(), name="_ticket_linked_item_add"),
]
@ -72,15 +79,13 @@ if settings.DEBUG:
urlpatterns += [
path("__debug__/", include("debug_toolbar.urls"), name='_debug'),
# Apps Under Development
path("itim/", include("itim.urls")),
path("information/", include("information.urls")),
path("project_management/", include("project_management.urls")),
]
# must be after above
urlpatterns += [
path("project_management/", include("project_management.urls")),
path("settings/", include("settings.urls")),
]

View File

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

View File

@ -0,0 +1,147 @@
from django import forms
from django.urls import reverse
from django.forms import ValidationError
from app import settings
from assistance.models.knowledge_base import KnowledgeBase
from core.forms.common import CommonModelForm
class KnowledgeBaseForm(CommonModelForm):
__name__ = 'asdsa'
class Meta:
fields = '__all__'
model = KnowledgeBase
prefix = 'knowledgebase'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['expiry_date'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local', 'format': "%Y-%m-%dT%H:%M"})
self.fields['expiry_date'].input_formats = settings.DATETIME_FORMAT
self.fields['expiry_date'].format="%Y-%m-%dT%H:%M"
self.fields['release_date'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local', 'format': "%Y-%m-%dT%H:%M"})
self.fields['release_date'].input_formats = settings.DATETIME_FORMAT
self.fields['release_date'].format="%Y-%m-%dT%H:%M"
def clean(self):
cleaned_data = super().clean()
responsible_user = cleaned_data.get("responsible_user")
responsible_teams = cleaned_data.get("responsible_teams")
if not responsible_user and not responsible_teams:
raise ValidationError('A Responsible User or Team must be assigned.')
target_team = cleaned_data.get("target_team")
target_user = cleaned_data.get("target_user")
if not target_team and not target_user:
raise ValidationError('A Target Team or Target User must be assigned.')
if target_team and target_user:
raise ValidationError('Both a Target Team or Target User Cant be assigned at the same time. Use one or the other')
return cleaned_data
class DetailForm(KnowledgeBaseForm):
tabs: dict = {
"details": {
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'title',
'category',
'responsible_user',
'organization',
'is_global',
'c_created',
'c_modified',
],
"right": [
'release_date',
'expiry_date',
'target_user',
'target_team',
]
},
{
"layout": "single",
"name": "Summary",
"fields": [
'summary',
],
"markdown": [
'summary',
]
},
{
"layout": "single",
"name": "Content",
"fields": [
'content',
],
"markdown": [
'content',
]
}
]
},
"notes": {
"name": "Notes",
"slug": "notes",
"sections": []
}
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['c_created'] = forms.DateTimeField(
label = 'Created',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.created,
)
self.fields['c_modified'] = forms.DateTimeField(
label = 'Modified',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.modified,
)
self.tabs['details'].update({
"edit_url": reverse('Assistance:_knowledge_base_change', args=(self.instance.pk,))
})
self.url_index_view = reverse('Assistance:Knowledge Base')

View File

@ -0,0 +1,36 @@
from django.forms import ValidationError
from assistance.models.knowledge_base import KnowledgeBaseCategory
from core.forms.common import CommonModelForm
class KnowledgeBaseCategoryForm(CommonModelForm):
__name__ = 'asdsa'
class Meta:
fields = '__all__'
model = KnowledgeBaseCategory
prefix = 'knowledgebase_category'
def clean(self):
cleaned_data = super().clean()
target_team = cleaned_data.get("target_team")
target_user = cleaned_data.get("target_user")
if target_team and target_user:
raise ValidationError('Both a Target Team or Target User Cant be assigned at the same time. Use one or the other or None')
return cleaned_data

View File

@ -0,0 +1,68 @@
# Generated by Django 5.0.7 on 2024-07-20 14:37
import access.fields
import access.models
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('access', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='KnowledgeBaseCategory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_global', models.BooleanField(default=False)),
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
('name', models.CharField(help_text='Name/Title of the Category', max_length=50, verbose_name='Title')),
('slug', access.fields.AutoSlugField()),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
('parent_category', models.ForeignKey(blank=True, default=None, help_text='Category this category belongs to', null=True, on_delete=django.db.models.deletion.SET_NULL, to='assistance.knowledgebasecategory', verbose_name='Parent Category')),
('target_team', models.ManyToManyField(blank=True, default=None, help_text='Team(s) to grant access to the article', to='access.team', verbose_name='Target Team(s)')),
('target_user', models.ForeignKey(blank=True, default=None, help_text='User(s) to grant access to the article', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Target Users(s)')),
],
options={
'verbose_name': 'Category',
'verbose_name_plural': 'Categorys',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='KnowledgeBase',
fields=[
('is_global', models.BooleanField(default=False)),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('title', models.CharField(help_text='Title of the article', max_length=50, verbose_name='Title')),
('summary', models.TextField(blank=True, default=None, help_text='Short Summary of the article', null=True, verbose_name='Summary')),
('content', models.TextField(blank=True, default=None, help_text='Content of the article. Markdown is supported', null=True, verbose_name='Article Content')),
('release_date', models.DateTimeField(blank=True, default=None, help_text='Date the article will be published', null=True, verbose_name='Publish Date')),
('expiry_date', models.DateTimeField(blank=True, default=None, help_text='Date the article will be removed from published articles', null=True, verbose_name='End Date')),
('public', models.BooleanField(default=False, help_text='Is this article to be made available publically', verbose_name='Public Article')),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
('responsible_teams', models.ManyToManyField(blank=True, default=None, help_text='Team(s) whom is considered the articles owner.', related_name='responsible_teams', to='access.team', verbose_name='Responsible Team(s)')),
('responsible_user', models.ForeignKey(default=None, help_text='User(s) whom is considered the articles owner.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='responsible_user', to=settings.AUTH_USER_MODEL, verbose_name='Responsible User')),
('target_team', models.ManyToManyField(blank=True, default=None, help_text='Team(s) to grant access to the article', to='access.team', verbose_name='Target Team(s)')),
('target_user', models.ForeignKey(blank=True, default=None, help_text='User(s) to grant access to the article', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Target Users(s)')),
('category', models.ForeignKey(default=None, help_text='Article Category', max_length=50, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assistance.knowledgebasecategory', verbose_name='Category')),
],
options={
'verbose_name': 'Article',
'verbose_name_plural': 'Articles',
'ordering': ['title'],
},
),
]

View File

View File

@ -0,0 +1,219 @@
from django.contrib.auth.models import User
from django.db import models
from django.forms import ValidationError
from access.fields import *
from access.models import Team, TenancyObject
class KnowledgeBaseCategory(TenancyObject):
class Meta:
ordering = [
'name',
]
verbose_name = "Category"
verbose_name_plural = "Categorys"
parent_category = models.ForeignKey(
'self',
blank = True,
default = None,
help_text = 'Category this category belongs to',
null = True,
on_delete = models.SET_NULL,
verbose_name = 'Parent Category',
)
name = models.CharField(
blank = False,
help_text = 'Name/Title of the Category',
max_length = 50,
unique = False,
verbose_name = 'Title',
)
slug = AutoSlugField()
target_team = models.ManyToManyField(
Team,
blank = True,
default = None,
help_text = 'Team(s) to grant access to the article',
verbose_name = 'Target Team(s)',
)
target_user = models.ForeignKey(
User,
blank = True,
default = None,
help_text = 'User(s) to grant access to the article',
null = True,
on_delete = models.SET_NULL,
verbose_name = 'Target Users(s)',
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
def __str__(self):
return self.name
class KnowledgeBase(TenancyObject):
class Meta:
ordering = [
'title',
]
verbose_name = "Article"
verbose_name_plural = "Articles"
model_notes = None
id = models.AutoField(
primary_key=True,
unique=True,
blank=False
)
title = models.CharField(
blank = False,
help_text = 'Title of the article',
max_length = 50,
unique = False,
verbose_name = 'Title',
)
summary = models.TextField(
blank = True,
default = None,
help_text = 'Short Summary of the article',
null = True,
verbose_name = 'Summary',
)
content = models.TextField(
blank = True,
default = None,
help_text = 'Content of the article. Markdown is supported',
null = True,
verbose_name = 'Article Content',
)
category = models.ForeignKey(
KnowledgeBaseCategory,
blank = False,
default = None,
help_text = 'Article Category',
max_length = 50,
null = True,
on_delete = models.SET_NULL,
unique = False,
verbose_name = 'Category',
)
release_date = models.DateTimeField(
blank = True,
default = None,
help_text = 'Date the article will be published',
null = True,
verbose_name = 'Publish Date',
)
expiry_date = models.DateTimeField(
blank = True,
default = None,
help_text = 'Date the article will be removed from published articles',
null = True,
verbose_name = 'End Date',
)
target_team = models.ManyToManyField(
Team,
blank = True,
default = None,
help_text = 'Team(s) to grant access to the article',
verbose_name = 'Target Team(s)',
)
target_user = models.ForeignKey(
User,
blank = True,
default = None,
help_text = 'User(s) to grant access to the article',
null = True,
on_delete = models.SET_NULL,
verbose_name = 'Target Users(s)',
)
responsible_user = models.ForeignKey(
User,
blank = False,
default = None,
help_text = 'User(s) whom is considered the articles owner.',
null = True,
on_delete = models.SET_NULL,
related_name = 'responsible_user',
verbose_name = 'Responsible User',
)
responsible_teams = models.ManyToManyField(
Team,
blank = True,
default = None,
help_text = 'Team(s) whom is considered the articles owner.',
related_name = 'responsible_teams',
verbose_name = 'Responsible Team(s)',
)
public = models.BooleanField(
blank = False,
default = False,
help_text = 'Is this article to be made available publically',
verbose_name = 'Public Article',
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
def __str__(self):
return self.title

View File

@ -0,0 +1,40 @@
{% extends 'detail.html.j2' %}
{% load json %}
{% load markdown %}
{% block tabs %}
<form action="" method="post">
{% csrf_token %}
<div id="details" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.details %}
</div>
{% if perms.assistance.change_knowledgebase %}
<div id="notes" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.notes %}
{{ notes_form }}
<input type="submit" name="{{notes_form.prefix}}" value="Submit" />
<div class="comments">
{% if notes %}
{% for note in notes%}
{% include 'note.html.j2' %}
{% endfor %}
{% endif %}
</div>
</div>
{% endif %}
</form>
{% endblock %}

View File

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

View File

@ -0,0 +1,47 @@
{% extends 'base.html.j2' %}
{% block content %}
<input type="button" value="New Article" onclick="window.location='{% url 'Settings:_knowledge_base_category_add' %}';">
<table class="data">
<tr>
<th>Title</th>
<th>Parent</th>
<th>Organization</th>
<th>&nbsp;</th>
</tr>
{% if items %}
{% for item in items %}
<tr>
<td><a href="{% url 'Settings:_knowledge_base_category_view' pk=item.id %}">{{ item.name }}</a></td>
<td>{{ item.parent_category }}</td>
<td>{{ item.organization }}</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="4">Nothing Found</td>
</tr>
{% endif %}
</table>
<br>
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page=1">&laquo; first</a>
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
<a href="?page={{ page_obj.paginator.num_pages }}">last &raquo;</a>
{% endif %}
</span>
</div>
{% endblock %}

View File

@ -0,0 +1,47 @@
{% extends 'base.html.j2' %}
{% block content %}
<input type="button" value="New Article" onclick="window.location='{% url 'Assistance:_knowledge_base_add' %}';">
<table class="data">
<tr>
<th>Title</th>
<th>Category</th>
<th>Organization</th>
<th>&nbsp;</th>
</tr>
{% if items %}
{% for item in items %}
<tr>
<td><a href="{% url 'Assistance:_knowledge_base_view' pk=item.id %}">{{ item.title }}</a></td>
<td><a href="{% url 'Settings:_knowledge_base_category_view' pk=item.category.id %}">{{ item.category }}</a></td>
<td>{{ item.organization }}</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="4">Nothing Found</td>
</tr>
{% endif %}
</table>
<br>
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page=1">&laquo; first</a>
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
<a href="?page={{ page_obj.paginator.num_pages }}">last &raquo;</a>
{% endif %}
</span>
</div>
{% endblock %}

View File

@ -0,0 +1,44 @@
import pytest
import unittest
from django.test import TestCase
from access.models import Organization
from app.tests.abstract.models import TenancyModel
from assistance.models.knowledge_base import KnowledgeBase
@pytest.mark.django_db
class KnowledgeBaseModel(
TestCase,
TenancyModel
):
model = KnowledgeBase
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
2. Create an item
"""
self.organization = Organization.objects.create(name='test_org')
self.item = self.model.objects.create(
organization = self.organization,
title = 'one',
content = 'dict({"key": "one", "existing": "dont_over_write"})'
)
self.second_item = self.model.objects.create(
organization = self.organization,
title = 'one_two',
content = 'dict({"key": "two"})',
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,75 @@
import pytest
import unittest
import requests
from django.test import TestCase, Client
from access.models import Organization
from core.models.history import History
from core.tests.abstract.history_entry import HistoryEntry
from core.tests.abstract.history_entry_parent_model import HistoryEntryParentItem
from assistance.models.knowledge_base import KnowledgeBaseCategory
class KnowledgeBaseHistory(TestCase, HistoryEntry, HistoryEntryParentItem):
model = KnowledgeBaseCategory
@classmethod
def setUpTestData(self):
""" Setup Test """
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.item_create = self.model.objects.create(
name = 'test_item_' + self.model._meta.model_name,
organization = self.organization,
)
self.history_create = History.objects.get(
action = History.Actions.ADD[0],
item_pk = self.item_create.pk,
item_class = self.model._meta.model_name,
)
self.item_change = self.item_create
self.item_change.name = 'test_item_' + self.model._meta.model_name + '_changed'
self.item_change.save()
self.field_after_expected_value = '{"name": "' + self.item_change.name + '"}'
self.history_change = History.objects.get(
action = History.Actions.UPDATE[0],
item_pk = self.item_change.pk,
item_class = self.model._meta.model_name,
)
self.item_delete = self.model.objects.create(
name = 'test_item_delete_' + self.model._meta.model_name,
organization = self.organization,
)
self.deleted_pk = self.item_delete.pk
self.item_delete.delete()
self.history_delete = History.objects.filter(
item_pk = self.deleted_pk,
item_class = self.model._meta.model_name,
)
def test_history_entry_children_delete(self):
""" Model has no child items """
pass

View File

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

View File

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

View File

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

27
app/assistance/urls.py Normal file
View File

@ -0,0 +1,27 @@
from django.urls import path
from assistance.views import knowledge_base
from core.views import ticket, ticket_comment
app_name = "Assistance"
urlpatterns = [
path("information", knowledge_base.Index.as_view(), name="Knowledge Base"),
path("information/add", knowledge_base.Add.as_view(), name="_knowledge_base_add"),
path("information/<int:pk>/edit", knowledge_base.Change.as_view(), name="_knowledge_base_change"),
path("information/<int:pk>/delete", knowledge_base.Delete.as_view(), name="_knowledge_base_delete"),
path("information/<int:pk>", knowledge_base.View.as_view(), name="_knowledge_base_view"),
path('ticket/request', ticket.Index.as_view(), kwargs={'ticket_type': 'request'}, name="Requests"),
path('ticket/<str:ticket_type>/add', ticket.Add.as_view(), name="_ticket_request_add"),
path('ticket/<str:ticket_type>/<int:pk>/edit', ticket.Change.as_view(), name="_ticket_request_change"),
path('ticket/<str:ticket_type>/<int:pk>/delete', ticket.Delete.as_view(), name="_ticket_request_delete"),
path('ticket/<str:ticket_type>/<int:pk>', ticket.View.as_view(), name="_ticket_request_view"),
path('ticket/<str:ticket_type>/<int:ticket_id>/comment/add', ticket_comment.Add.as_view(), name="_ticket_comment_request_add"),
path('ticket/<str:ticket_type>/<int:ticket_id>/comment/<int:pk>/edit', ticket_comment.Change.as_view(), name="_ticket_comment_request_change"),
path('ticket/<str:ticket_type>/<int:ticket_id>/comment/<int:parent_id>/add', ticket_comment.Add.as_view(), name="_ticket_comment_request_reply_add"),
]

View File

View File

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

View File

@ -0,0 +1,191 @@
from django.contrib.auth import decorators as auth_decorator
from django.urls import reverse
from django.utils.decorators import method_decorator
from assistance.forms.knowledge_base_category import KnowledgeBaseCategoryForm
from assistance.models.knowledge_base import KnowledgeBase, KnowledgeBaseCategory
from core.forms.comment import AddNoteForm
from core.models.notes import Notes
from core.views.common import AddView, ChangeView, DeleteView, DisplayView, IndexView
from settings.models.user_settings import UserSettings
class Index(IndexView):
context_object_name = "items"
model = KnowledgeBaseCategory
paginate_by = 10
permission_required = [
'assistance.view_knowledgebasecategory'
]
template_name = 'assistance/kb_category_index.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['model_docs_path'] = self.model._meta.app_label + '/knowledge_base/'
context['content_title'] = 'Knowledge Base Categories'
return context
class Add(AddView):
form_class = KnowledgeBaseCategoryForm
model = KnowledgeBaseCategory
permission_required = [
'assistance.add_knowledgebasecategory',
]
def get_initial(self):
initial: dict = {
'organization': UserSettings.objects.get(user = self.request.user).default_organization
}
if 'pk' in self.kwargs:
if self.kwargs['pk']:
initial.update({'parent': self.kwargs['pk']})
self.model.parent.field.hidden = True
return initial
def get_success_url(self, **kwargs):
return reverse('Settings:KB Categories')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'New Group'
return context
class Change(ChangeView):
context_object_name = "group"
form_class = KnowledgeBaseCategoryForm
model = KnowledgeBaseCategory
permission_required = [
'assistance.change_knowledgebasecategory',
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = self.object.name
return context
def get_success_url(self, **kwargs):
return reverse('Settings:_knowledge_base_category_view', args=(self.kwargs['pk'],))
class View(ChangeView):
context_object_name = "item"
form_class = KnowledgeBaseCategoryForm
model = KnowledgeBaseCategory
permission_required = [
'assistance.view_knowledgebasecategory',
]
template_name = 'assistance/kb_category.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['articles'] = KnowledgeBase.objects.filter(category=self.kwargs['pk'])
context['notes_form'] = AddNoteForm(prefix='note')
context['notes'] = Notes.objects.filter(config_group=self.kwargs['pk'])
context['model_pk'] = self.kwargs['pk']
context['model_name'] = self.model._meta.model_name
context['model_delete_url'] = reverse('Settings:_knowledge_base_category_delete', args=(self.kwargs['pk'],))
context['content_title'] = self.object.name
return context
@method_decorator(auth_decorator.permission_required("assistance.change_knowledgebasecategory", raise_exception=True))
def post(self, request, *args, **kwargs):
item = KnowledgeBase.objects.get(pk=self.kwargs['pk'])
notes = AddNoteForm(request.POST, prefix='note')
if notes.is_bound and notes.is_valid() and notes.instance.note != '':
notes.instance.organization = item.organization
notes.save()
# dont allow saving any post data outside notes.
# todo: figure out what needs to be returned
# return super().post(request, *args, **kwargs)
def get_success_url(self, **kwargs):
return reverse('Settings:_knowledge_base_category_view', args=(self.kwargs['pk'],))
class Delete(DeleteView):
model = KnowledgeBaseCategory
permission_required = [
'assistance.delete_knowledgebasecategory',
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Delete ' + self.object.name
return context
def get_success_url(self, **kwargs):
return reverse('Settings:KB Categories')

View File

@ -1,4 +1,7 @@
from django.db.models import Q
from django import forms
from django.urls import reverse
from app import settings
from config_management.models.groups import ConfigGroups
@ -32,3 +35,94 @@ class ConfigGroupForm(CommonModelForm):
).exclude(
id=int(kwargs['instance'].id)
)
class DetailForm(ConfigGroupForm):
tabs: dict = {
"details": {
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'name',
'parent',
'is_global',
'organization',
'c_created',
'c_modified',
],
"right": [
'model_notes',
]
},
{
"layout": "single",
"fields": [
'config',
]
}
]
},
"child_groups": {
"name": "Child Groups",
"slug": "child_groups",
"sections": []
},
"hosts": {
"name": "Hosts",
"slug": "hosts",
"sections": []
},
"software": {
"name": "Software",
"slug": "software",
"sections": []
},
"configuration": {
"name": "Configuration",
"slug": "configuration",
"sections": []
},
"tickets": {
"name": "Tickets",
"slug": "tickets",
"sections": []
},
"notes": {
"name": "Notes",
"slug": "notes",
"sections": []
}
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['c_created'] = forms.DateTimeField(
label = 'Created',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.created,
)
self.fields['c_modified'] = forms.DateTimeField(
label = 'Modified',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.modified,
)
self.tabs['details'].update({
"edit_url": reverse('Config Management:_group_change', args=(self.instance.pk,))
})
self.url_index_view = reverse('Config Management:Groups')

View File

@ -0,0 +1,21 @@
# Generated by Django 5.0.7 on 2024-08-17 08:05
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('config_management', '0002_configgrouphosts_configgroupsoftware'),
]
operations = [
migrations.AlterModelOptions(
name='configgroups',
options={'verbose_name_plural': 'Config Groups'},
),
migrations.AlterModelOptions(
name='configgroupsoftware',
options={'ordering': ['-action', 'software'], 'verbose_name_plural': 'Config Group Softwares'},
),
]

View File

@ -35,6 +35,12 @@ class GroupsCommonFields(TenancyObject, models.Model):
class ConfigGroups(GroupsCommonFields, SaveHistory):
class Meta:
verbose_name_plural = 'Config Groups'
reserved_config_keys: list = [
'software'
]
@ -264,6 +270,8 @@ class ConfigGroupSoftware(GroupsCommonFields, SaveHistory):
'software'
]
verbose_name_plural = 'Config Group Softwares'
config_group = models.ForeignKey(
ConfigGroups,

View File

@ -1,47 +1,208 @@
{% extends 'base.html.j2' %}
{% extends 'detail.html.j2' %}
{% block content %}
{% load json %}
{% load markdown %}
<script>
function openCity(evt, cityName) {
var i, tabcontent, tablinks;
{% block tabs %}
<form action="" method="post">
{% csrf_token %}
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
<div id="details" class="content-tab">
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(cityName).style.display = "block";
evt.currentTarget.className += " active";
}
</script>
<div class="tab">
<button
onclick="window.location='{% if group.parent %}{% url 'Config Management:_group_view' pk=group.parent.id %}{% else %}{% url 'Config Management:Groups' %}{% endif %}';"
style="vertical-align: middle; padding: auto; margin: 0px">
<svg xmlns="http://www.w3.org/2000/svg" height="25px" viewBox="0 -960 960 960" width="25px"
style="vertical-align: middle; margin: 0px; padding: 0px border: none; " fill="#6a6e73">
<path
d="m313-480 155 156q11 11 11.5 27.5T468-268q-11 11-28 11t-28-11L228-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T468-692q11 11 11 28t-11 28L313-480Zm264 0 155 156q11 11 11.5 27.5T732-268q-11 11-28 11t-28-11L492-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T732-692q11 11 11 28t-11 28L577-480Z" />
</svg>Back to {% if group.parent %}Parent{% else %}Groups{% endif %}</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Children')">Child Groups</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Hosts')">Hosts</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Software')">Software</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Configuration')">Configuration</button>
<button class="tablinks" onclick="openCity(event, 'Notes')">Notes</button>
{% include 'content/section.html.j2' with tab=form.tabs.details %}
</div>
<div id="child_groups" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.child_groups %}
<input type="button" value="Add Child Group" onclick="window.location='{% url 'Config Management:_group_add_child' group.id %}';">
<table class="data">
<tr>
<th>Name</th>
<th>Sub-Groups</th>
<th>&nbsp;</th>
</tr>
{% if child_groups %}
{% for group in child_groups %}
<tr>
<td><a href="{% url 'Config Management:_group_view' pk=group.id %}">{{ group.name }}</a></td>
<td>{{ group.count_children }}</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="4">Nothing Found</td>
</tr>
{% endif %}
</table>
</div>
<div id="hosts" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.hosts %}
<input type="button" value="Add Host" onclick="window.location='{% url 'Config Management:_group_add_host' group.id %}';">
<table class="data">
<tr>
<th>Name</th>
<th>Organization</th>
<th>&nbsp;</th>
</tr>
{% if config_group_hosts %}
{% for host in config_group_hosts %}
<tr>
<td><a href="{% url 'ITAM:_device_view' pk=host.host.id %}">{{ host.host }}</a></td>
<td>{{ host.host.organization }}</td>
<td><a href="{% url 'Config Management:_group_delete_host' group_id=group.id pk=host.id %}">Delete</a></td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="3">Nothing Found</td>
</tr>
{% endif %}
</table>
</div>
<div id="software" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.software %}
<input type="button" value="Add Software Action" onclick="window.location='{% url 'Config Management:_group_software_add' model_pk %}';">
<table>
<thead>
<th>Name</th>
<th>Category</th>
<th>Action</th>
<th>Desired Version</th>
<th>&nbsp;</th>
</thead>
{% if softwares %}
{% for software in softwares %}
<tr>
<td><a href="{% url 'ITAM:_software_view' pk=software.software_id %}">{{ software.software }}</a></td>
<td>{{ software.software.category }}</td>
<td>
{% url 'Config Management:_group_software_change' group_id=group.id pk=software.id as icon_link %}
{% if software.get_action_display == 'Install' %}
{% include 'icons/success_text.html.j2' with icon_text=software.get_action_display icon_link=icon_link %}
{% elif software.get_action_display == 'Remove'%}
{% include 'icons/cross_text.html.j2' with icon_text=software.get_action_display %}
{% else %}
{% include 'icons/add_link.html.j2' with icon_text='Add' %}
{% endif %}
</td>
<td>
{% if software.version %}
{{ software.version }}
{% else %}
-
{% endif %}
</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
{% else %}
<td colspan="5">Nothing Found</td>
{% endif %}
</table>
</div>
<div id="configuration" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.configuration %}
<div>
<textarea cols="90" rows="30" readonly>{{ config }}</textarea>
</div>
</div>
<div id="tickets" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.tickets %}
<table>
<thead>
<th>Name</th>
<th>Status</th>
<th>&nbsp</th>
</thead>
{% if tickets %}
{% for ticket in tickets %}
<tr>
<td>{% concat_strings "#" ticket.ticket.id as ticket_ref %}{{ ticket_ref | markdown | safe}}</td>
<td>{% include 'core/ticket/badge_ticket_status.html.j2' with ticket_status_text=ticket.ticket.get_status_display ticket_status=ticket.ticket.get_status_display|ticket_status %}</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="3">No related tickets exist</td>
</tr>
{% endif %}
</table>
</div>
<div id="notes" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.notes %}
{{ notes_form }}
<input type="submit" name="{{notes_form.prefix}}" value="Submit" />
<div class="comments">
{% if notes %}
{% for note in notes%}
{% include 'note.html.j2' %}
{% endfor %}
{% endif %}
</div>
</div>
</form>
{% endblock %}
{% block contents %}
<form method="post">
<div id="Details" class="tabcontent">
<h3>Details</h3>
@ -60,28 +221,6 @@
<div id="Children" class="tabcontent">
<h3>Child Groups</h3>
<input type="button" value="Add Child Group" onclick="window.location='{% url 'Config Management:_group_add_child' group.id %}';">
<table class="data">
<tr>
<th>Name</th>
<th>Sub-Groups</th>
<th>&nbsp;</th>
</tr>
{% if child_groups %}
{% for group in child_groups %}
<tr>
<td><a href="{% url 'Config Management:_group_view' pk=group.id %}">{{ group.name }}</a></td>
<td>{{ group.count_children }}</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="4">Nothing Found</td>
</tr>
{% endif %}
</table>
</div>
@ -90,28 +229,6 @@
Hosts
</h3>
<input type="button" value="Add Host" onclick="window.location='{% url 'Config Management:_group_add_host' group.id %}';">
<table class="data">
<tr>
<th>Name</th>
<th>Organization</th>
<th>&nbsp;</th>
</tr>
{% if config_group_hosts %}
{% for host in config_group_hosts %}
<tr>
<td><a href="{% url 'ITAM:_device_view' pk=host.host.id %}">{{ host.host }}</a></td>
<td>{{ host.host.organization }}</td>
<td><a href="{% url 'Config Management:_group_delete_host' group_id=group.id pk=host.id %}">Delete</a></td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="3">Nothing Found</td>
</tr>
{% endif %}
</table>
</div>
@ -120,52 +237,11 @@
Software
</h3>
<input type="button" value="Add Software Action" onclick="window.location='{% url 'Config Management:_group_software_add' model_pk %}';">
<table>
<thead>
<th>Name</th>
<th>Category</th>
<th>Action</th>
<th>Desired Version</th>
<th>&nbsp;</th>
</thead>
{% if softwares %}
{% for software in softwares %}
<tr>
<td><a href="{% url 'ITAM:_software_view' pk=software.software_id %}">{{ software.software }}</a></td>
<td>{{ software.software.category }}</td>
<td>
{% url 'Config Management:_group_software_change' group_id=group.id pk=software.id as icon_link %}
{% if software.get_action_display == 'Install' %}
{% include 'icons/success_text.html.j2' with icon_text=software.get_action_display icon_link=icon_link %}
{% elif software.get_action_display == 'Remove'%}
{% include 'icons/cross_text.html.j2' with icon_text=software.get_action_display %}
{% else %}
{% include 'icons/add_link.html.j2' with icon_text='Add' %}
{% endif %}
</td>
<td>
{% if software.version %}
{{ software.version }}
{% else %}
-
{% endif %}
</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
{% else %}
<td colspan="5">Nothing Found</td>
{% endif %}
</table>
</div>
<div id="Configuration" class="tabcontent">
<h3>Configuration</h3>
<div>
<textarea cols="90" rows="30" readonly>{{ config }}</textarea>
</div>
</div>
<div id="Notes" class="tabcontent">

View File

@ -27,7 +27,7 @@ class ConfigGroupPermissions(TestCase, ModelPermissions):
url_name_add = '_group_add'
url_name_change = '_group_view'
url_name_change = '_group_change'
url_name_delete = '_group_delete'

View File

@ -14,16 +14,16 @@ class ConfigManagementViews(
):
add_module = 'config_management.views.groups.groups'
add_view = 'GroupAdd'
add_view = 'Add'
change_module = add_module
change_view = 'GroupView'
change_view = 'View'
delete_module = add_module
delete_view = 'GroupDelete'
delete_view = 'Delete'
display_module = add_module
display_view = 'GroupView'
display_view = 'View'
index_module = add_module
index_view = 'GroupIndexView'
index_view = 'Index'

View File

@ -16,13 +16,13 @@ class ConfigGroupsSoftwareViews(
):
add_module = 'config_management.views.groups.software'
add_view = 'GroupSoftwareAdd'
add_view = 'Add'
change_module = add_module
change_view = 'GroupSoftwareChange'
change_view = 'Change'
delete_module = add_module
delete_view = 'GroupSoftwareDelete'
delete_view = 'Delete'
# display_module = add_module
# display_view = 'GroupView'

View File

@ -1,21 +1,25 @@
from django.urls import path
from config_management.views.groups.groups import GroupIndexView, GroupAdd, GroupDelete, GroupView, GroupHostAdd, GroupHostDelete
from config_management.views.groups.software import GroupSoftwareAdd, GroupSoftwareChange, GroupSoftwareDelete
from config_management.views.groups import groups
from config_management.views.groups.groups import GroupHostAdd, GroupHostDelete
from config_management.views.groups import software
# from config_management.views.groups.software import GroupSoftwareAdd, GroupSoftwareChange, GroupSoftwareDelete
app_name = "Config Management"
urlpatterns = [
path('group', GroupIndexView.as_view(), name='Groups'),
path('group/add', GroupAdd.as_view(), name='_group_add'),
path('group/<int:pk>', GroupView.as_view(), name='_group_view'),
path('group', groups.Index.as_view(), name='Groups'),
path('group/add', groups.Add.as_view(), name='_group_add'),
path('group/<int:pk>', groups.View.as_view(), name='_group_view'),
path('group/<int:pk>/edit', groups.Change.as_view(), name='_group_change'),
path('group/<int:pk>/child', GroupAdd.as_view(), name='_group_add_child'),
path('group/<int:pk>/delete', GroupDelete.as_view(), name='_group_delete'),
path('group/<int:pk>/child', groups.Add.as_view(), name='_group_add_child'),
path('group/<int:pk>/delete', groups.Delete.as_view(), name='_group_delete'),
path("group/<int:pk>/software/add", GroupSoftwareAdd.as_view(), name="_group_software_add"),
path("group/<int:group_id>/software/<int:pk>", GroupSoftwareChange.as_view(), name="_group_software_change"),
path("group/<int:group_id>/software/<int:pk>/delete", GroupSoftwareDelete.as_view(), name="_group_software_delete"),
path("group/<int:pk>/software/add", software.Add.as_view(), name="_group_software_add"),
path("group/<int:group_id>/software/<int:pk>", software.Change.as_view(), name="_group_software_change"),
path("group/<int:group_id>/software/<int:pk>/delete", software.Delete.as_view(), name="_group_software_delete"),
path('group/<int:pk>/host', GroupHostAdd.as_view(), name='_group_add_host'),
path('group/<int:group_id>/host/<int:pk>/delete', GroupHostDelete.as_view(), name='_group_delete_host'),

View File

@ -6,6 +6,7 @@ from django.utils.decorators import method_decorator
from core.forms.comment import AddNoteForm
from core.models.notes import Notes
from core.models.ticket.ticket_linked_items import Ticket, TicketLinkedItem
from core.views.common import AddView, ChangeView, DeleteView, IndexView
from itam.models.device import Device
@ -13,12 +14,12 @@ from itam.models.device import Device
from settings.models.user_settings import UserSettings
from config_management.forms.group_hosts import ConfigGroupHostsForm
from config_management.forms.group.group import ConfigGroupForm
from config_management.forms.group.group import ConfigGroupForm, DetailForm
from config_management.models.groups import ConfigGroups, ConfigGroupHosts, ConfigGroupSoftware
class GroupIndexView(IndexView):
class Index(IndexView):
context_object_name = "groups"
@ -50,7 +51,7 @@ class GroupIndexView(IndexView):
class GroupAdd(AddView):
class Add(AddView):
organization_field = 'organization'
@ -67,9 +68,11 @@ class GroupAdd(AddView):
def get_initial(self):
initial: dict = {
'organization': UserSettings.objects.get(user = self.request.user).default_organization
}
# initial: dict = {
# 'organization': UserSettings.objects.get(user = self.request.user).default_organization
# }
initial = super().get_initial()
if 'pk' in self.kwargs:
@ -102,7 +105,7 @@ class GroupAdd(AddView):
class GroupView(ChangeView):
class Change(ChangeView):
context_object_name = "group"
@ -111,10 +114,39 @@ class GroupView(ChangeView):
model = ConfigGroups
permission_required = [
'config_management.view_configgroups',
'config_management.change_configgroups',
]
template_name = 'form.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = self.object.name
return context
def get_success_url(self, **kwargs):
return reverse('Config Management:_group_view', args=(self.kwargs['pk'],))
class View(ChangeView):
context_object_name = "group"
form_class = DetailForm
model = ConfigGroups
permission_required = [
'config_management.view_configgroups',
]
template_name = 'config_management/group.html.j2'
@ -128,6 +160,11 @@ class GroupView(ChangeView):
context['config_group_hosts'] = ConfigGroupHosts.objects.filter(group_id = self.kwargs['pk']).order_by('-host')
context['tickets'] = TicketLinkedItem.objects.filter(
item = int(self.kwargs['pk']),
item_type = TicketLinkedItem.Modules.CONFIG_GROUP
)
context['notes_form'] = AddNoteForm(prefix='note')
context['notes'] = Notes.objects.filter(config_group=self.kwargs['pk'])
@ -186,7 +223,7 @@ class GroupView(ChangeView):
class GroupDelete(DeleteView):
class Delete(DeleteView):
model = ConfigGroups

View File

@ -9,7 +9,7 @@ from config_management.models.groups import ConfigGroups, ConfigGroupSoftware
from core.views.common import AddView, ChangeView, DeleteView
class GroupSoftwareAdd(AddView):
class Add(AddView):
form_class = SoftwareAdd
@ -65,7 +65,7 @@ class GroupSoftwareAdd(AddView):
class GroupSoftwareChange(ChangeView):
class Change(ChangeView):
form_class = SoftwareUpdate
@ -104,7 +104,7 @@ class GroupSoftwareChange(ChangeView):
class GroupSoftwareDelete(DeleteView):
class Delete(DeleteView):
model = ConfigGroupSoftware

View File

@ -46,9 +46,6 @@ class CommonModelForm(forms.ModelForm):
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 ]
@ -76,11 +73,13 @@ class CommonModelForm(forms.ModelForm):
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)
)
if field.queryset.model.is_global is not None:
self.fields[field_name].queryset = field.queryset.filter(
Q(organization__in=user_organizations_id)
|
Q(is_global = True)
)
else:
@ -98,3 +97,7 @@ class CommonModelForm(forms.ModelForm):
|
Q(manager=user)
)
if hasattr(self, 'instance'):
self.model_name_plural = self.instance._meta.verbose_name_plural

View File

@ -1,4 +1,7 @@
from django import forms
from django.urls import reverse
from app import settings
from core.forms.common import CommonModelForm
from core.models.manufacturer import Manufacturer
@ -24,3 +27,64 @@ class ManufacturerForm(
]
model = Manufacturer
class DetailForm(ManufacturerForm):
tabs: dict = {
"details": {
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'name',
'slug',
'organization',
'is_global',
'c_created',
'c_modified',
],
"right": [
'model_notes',
]
}
]
},
# "notes": {
# "name": "Notes",
# "slug": "notes",
# "sections": []
# }
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['c_created'] = forms.DateTimeField(
label = 'Created',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.created,
)
self.fields['c_modified'] = forms.DateTimeField(
label = 'Modified',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.modified,
)
self.tabs['details'].update({
"edit_url": reverse('Settings:_manufacturer_change', args=(self.instance.pk,))
})
self.url_index_view = reverse('Settings:_manufacturers')

View File

@ -0,0 +1,55 @@
from django import forms
from django.db.models import Q
from django.forms import ValidationError
from app import settings
from core.forms.common import CommonModelForm
from core.models.ticket.ticket import RelatedTickets
class RelatedTicketForm(CommonModelForm):
prefix = 'ticket'
class Meta:
model = RelatedTickets
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['from_ticket_id'].widget = self.fields['from_ticket_id'].hidden_widget()
def clean(self):
cleaned_data = super().clean()
return cleaned_data
def is_valid(self) -> bool:
is_valid = super().is_valid()
check_db = self.Meta.model.objects.filter(
to_ticket_id = self.cleaned_data['to_ticket_id'].id,
from_ticket_id = self.cleaned_data['from_ticket_id'].id,
)
check_db_inverse = self.Meta.model.objects.filter(
to_ticket_id = self.cleaned_data['from_ticket_id'].id,
from_ticket_id = self.cleaned_data['to_ticket_id'].id,
)
if check_db.count() > 0 or check_db_inverse.count() > 0:
raise ValidationError(f"Ticket is already related to #{self.cleaned_data['to_ticket_id'].id}")
is_valid = False
return is_valid

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