Compare commits

...

346 Commits
1.1.0 ... 1.2.2

Author SHA1 Message Date
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
262 changed files with 22461 additions and 340 deletions

View File

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

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

@ -1,3 +1,320 @@
## 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

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.

View File

@ -21,6 +21,7 @@ def permission_queryset():
exclude_models = [
'appsettings',
'chordcounter',
'comment',
'groupresult',
'organization'
'settings',

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

@ -124,13 +124,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 +196,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):

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

@ -8,6 +8,22 @@ 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
@ -16,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'),
@ -32,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'),
@ -39,6 +89,8 @@ 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'),

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,4 +1,3 @@
from django.contrib.auth.models import User
from django.utils.safestring import mark_safe
from rest_framework import generics, permissions, routers, viewsets
@ -31,9 +30,12 @@ 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

@ -37,7 +37,11 @@ class View(views.APIView):
status = Http.Status.OK
response_data: dict = {
"permissions": reverse('API:_settings_permissions', request=request)
"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)

View File

@ -108,6 +108,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'rest_framework',
'rest_framework_json_api',
'django_filters',
'social_django',
'django_celery_results',
'core.apps.CoreConfig',
@ -119,6 +120,7 @@ INSTALLED_APPS = [
'drf_spectacular',
'drf_spectacular_sidecar',
'config_management.apps.ConfigManagementConfig',
'project_management.apps.ProjectManagementConfig',
]
MIDDLEWARE = [
@ -257,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',
@ -357,11 +361,6 @@ if DEBUG:
"127.0.0.1",
]
# Apps Under Development
INSTALLED_APPS += [
'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

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

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
@ -50,6 +50,11 @@ urlpatterns = [
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"),
]
@ -74,12 +79,13 @@ if settings.DEBUG:
urlpatterns += [
path("__debug__/", include("debug_toolbar.urls"), name='_debug'),
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

@ -2,6 +2,8 @@ from django.urls import path
from assistance.views import knowledge_base
from core.views import ticket, ticket_comment
app_name = "Assistance"
urlpatterns = [
@ -12,4 +14,14 @@ urlpatterns = [
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

@ -87,6 +87,11 @@ class DetailForm(ConfigGroupForm):
"slug": "configuration",
"sections": []
},
"tickets": {
"name": "Tickets",
"slug": "tickets",
"sections": []
},
"notes": {
"name": "Notes",
"slug": "notes",

View File

@ -130,6 +130,34 @@
</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 %}

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,22 +1,25 @@
from django.urls import path
from config_management.views.groups.groups import GroupIndexView, GroupAdd, GroupChange, 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/<int:pk>/edit', GroupChange.as_view(), name='_group_change'),
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
@ -18,7 +19,7 @@ from config_management.models.groups import ConfigGroups, ConfigGroupHosts, Conf
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 GroupChange(ChangeView):
class Change(ChangeView):
context_object_name = "group"
@ -132,7 +135,7 @@ class GroupChange(ChangeView):
class GroupView(ChangeView):
class View(ChangeView):
context_object_name = "group"
@ -157,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'])
@ -215,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:

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

249
app/core/forms/ticket.py Normal file
View File

@ -0,0 +1,249 @@
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.forms.validate_ticket import TicketValidation
from core.models.ticket.ticket import Ticket, RelatedTickets
class TicketForm(
CommonModelForm,
TicketValidation,
):
class Meta:
model = Ticket
fields = '__all__'
def __init__(self, request, *args, **kwargs):
self.request = request
super().__init__(*args, **kwargs)
self.fields['planned_start_date'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local', 'format': "%Y-%m-%dT%H:%M"})
self.fields['planned_start_date'].input_formats = settings.DATETIME_FORMAT
self.fields['planned_start_date'].format="%Y-%m-%dT%H:%M"
self.fields['planned_finish_date'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local'})
self.fields['planned_finish_date'].input_formats = settings.DATETIME_FORMAT
self.fields['planned_finish_date'].format="%Y-%m-%dT%H:%M"
self.fields['real_start_date'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local'})
self.fields['real_start_date'].input_formats = settings.DATETIME_FORMAT
self.fields['real_start_date'].format="%Y-%m-%dT%H:%M"
self.fields['real_finish_date'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local'})
self.fields['real_finish_date'].input_formats = settings.DATETIME_FORMAT
self.fields['real_finish_date'].format="%Y-%m-%dT%H:%M"
self.fields['description'].widget.attrs = {'style': "height: 800px; width: 900px"}
self.fields['opened_by'].initial = kwargs['user'].pk
self.fields['opened_by'].widget = self.fields['opened_by'].hidden_widget()
self.fields['ticket_type'].widget = self.fields['ticket_type'].hidden_widget()
self.fields['organization'].initial = self.initial['organization']
if self.instance.pk is not None:
del self.fields['organization']
if self.instance.project is not None:
self.fields['milestone'].queryset = self.fields['milestone'].queryset.filter(
project=self.instance.project
)
else:
self.fields['milestone'].queryset = self.fields['milestone'].queryset.filter(
id=0
)
original_fields = self.fields.copy()
ticket_type = []
if kwargs['initial']['type_ticket'] == 'request':
ticket_type = self.Meta.model.fields_itsm_request
self.fields['status'].choices = self.Meta.model.TicketStatus.Request
self.fields['ticket_type'].initial = '1'
self.fields['category'].queryset = self.fields['category'].queryset.filter(
request=True
)
elif kwargs['initial']['type_ticket'] == 'incident':
ticket_type = self.Meta.model.fields_itsm_incident
self.fields['status'].choices = self.Meta.model.TicketStatus.Incident
self.fields['ticket_type'].initial = self.Meta.model.TicketType.INCIDENT.value
self.fields['category'].queryset = self.fields['category'].queryset.filter(
incident=True
)
elif kwargs['initial']['type_ticket'] == 'problem':
ticket_type = self.Meta.model.fields_itsm_problem
self.fields['status'].choices = self.Meta.model.TicketStatus.Problem
self.fields['ticket_type'].initial = self.Meta.model.TicketType.PROBLEM.value
self.fields['category'].queryset = self.fields['category'].queryset.filter(
problem=True
)
elif kwargs['initial']['type_ticket'] == 'change':
ticket_type = self.Meta.model.fields_itsm_change
self.fields['status'].choices = self.Meta.model.TicketStatus.Change
self.fields['ticket_type'].initial = self.Meta.model.TicketType.CHANGE.value
self.fields['category'].queryset = self.fields['category'].queryset.filter(
change=True
)
elif kwargs['initial']['type_ticket'] == 'issue':
ticket_type = self.Meta.model.fields_git_issue
self.fields['status'].choices = self.Meta.model.TicketStatus.Git
self.fields['ticket_type'].initial = self.Meta.model.TicketType.ISSUE.value
elif kwargs['initial']['type_ticket'] == 'merge':
ticket_type = self.Meta.model.fields_git_merge
self.fields['status'].choices = self.Meta.model.TicketStatus.Git
self.fields['ticket_type'].initial = self.Meta.model.TicketType.MERGE_REQUEST.value
elif kwargs['initial']['type_ticket'] == 'project_task':
ticket_type = self.Meta.model.fields_project_task
self.fields['status'].choices = self.Meta.model.TicketStatus.ProjectTask
self._project: int = kwargs['initial']['project']
self.fields['project'].initial = self._project
self.fields['project'].widget = self.fields['project'].hidden_widget()
self.fields['ticket_type'].initial = self.Meta.model.TicketType.PROJECT_TASK.value
self.fields['category'].queryset = self.fields['category'].queryset.filter(
project_task=True
)
if kwargs['user'].is_superuser:
ticket_type += self.Meta.model.tech_fields
self.ticket_type_fields = ticket_type
fields_allowed_by_permission = self.get_fields_allowed_by_permission
allowed_ticket_fields: list = []
for field in fields_allowed_by_permission: # Remove fields not intended for the ticket type
if field in ticket_type:
allowed_ticket_fields = allowed_ticket_fields + [ field ]
for field in original_fields: # Remove fields user cant edit unless field is hidden
if (
(
field not in allowed_ticket_fields and not self.fields[field].widget.is_hidden
)
or
field not in ticket_type
):
# self.fields[field].widget = self.fields[field].hidden_widget()
del self.fields[field]
def clean(self):
cleaned_data = super().clean()
return cleaned_data
def is_valid(self) -> bool:
is_valid = super().is_valid()
self.validate_ticket()
if self._ticket_type == 'change':
self.validate_change_ticket()
elif self._ticket_type == 'incident':
self.validate_incident_ticket()
elif self._ticket_type == 'issue':
# self.validate_issue_ticket()
raise ValidationError(
'This Ticket type is not yet available'
)
elif self._ticket_type == 'merge_request':
# self.validate_merge_request_ticket()
raise ValidationError(
'This Ticket type is not yet available'
)
elif self._ticket_type == 'problem':
self.validate_problem_ticket()
elif self._ticket_type == 'project_task':
self.validate_project_task_ticket()
elif self._ticket_type == 'request':
self.validate_request_ticket()
else:
raise ValidationError('Ticket Type must be set')
return is_valid
class DetailForm(CommonModelForm):
prefix = 'ticket'
class Meta:
model = Ticket
fields = '__all__'

View File

@ -0,0 +1,120 @@
from django import forms
from django.forms import ValidationError
from django.urls import reverse
from app import settings
from core.forms.common import CommonModelForm
from core.models.ticket.ticket_category import TicketCategory
class TicketCategoryForm(CommonModelForm):
class Meta:
fields = '__all__'
model = TicketCategory
prefix = 'ticket_category'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['parent'].queryset = self.fields['parent'].queryset.exclude(
id=self.instance.pk
)
def clean(self):
cleaned_data = super().clean()
pk = self.instance.id
parent = cleaned_data.get("parent")
if pk:
if parent == pk:
raise ValidationError("Category can't have itself as its parent category")
return cleaned_data
class DetailForm(TicketCategoryForm):
tabs: dict = {
"details": {
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'parent',
'name',
'runbook',
'organization',
'c_created',
'c_modified'
],
"right": [
'model_notes',
]
},
{
"layout": "double",
"name": "Ticket Types",
"left": [
'change',
'problem',
'request'
],
"right": [
'incident',
'project_task'
]
},
]
},
"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:_ticket_category_change', kwargs={'pk': self.instance.pk})
})
self.url_index_view = reverse('Settings:_ticket_categories')

View File

@ -0,0 +1,163 @@
from django import forms
from django.db.models import Q
from app import settings
from core.forms.common import CommonModelForm
from core.forms.validate_ticket_comment import TicketCommentValidation
from core.models.ticket.ticket_comment import TicketComment
class CommentForm(
CommonModelForm,
TicketCommentValidation
):
prefix = 'ticket'
class Meta:
model = TicketComment
fields = '__all__'
def __init__(self, request, *args, **kwargs):
self.request = request
super().__init__(*args, **kwargs)
self._ticket_organization = self.fields['ticket'].queryset.model.objects.get(pk=int(self.initial['ticket'])).organization
self._ticket_type = kwargs['initial']['type_ticket']
if 'qs_comment_type' in kwargs['initial']:
self._comment_type = kwargs['initial']['qs_comment_type']
else:
self._comment_type = str(self.instance.get_comment_type_display()).lower()
self.ticket_comment_permissions
self.fields['planned_start_date'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local', 'format': "%Y-%m-%dT%H:%M"})
self.fields['planned_start_date'].input_formats = settings.DATETIME_FORMAT
self.fields['planned_start_date'].format="%Y-%m-%dT%H:%M"
self.fields['planned_finish_date'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local'})
self.fields['planned_finish_date'].input_formats = settings.DATETIME_FORMAT
self.fields['planned_finish_date'].format="%Y-%m-%dT%H:%M"
self.fields['real_start_date'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local'})
self.fields['real_start_date'].input_formats = settings.DATETIME_FORMAT
self.fields['real_start_date'].format="%Y-%m-%dT%H:%M"
self.fields['real_finish_date'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local'})
self.fields['real_finish_date'].input_formats = settings.DATETIME_FORMAT
self.fields['real_finish_date'].format="%Y-%m-%dT%H:%M"
self.fields['body'].widget.attrs = {'style': "height: 800px; width: 900px"}
self.fields['duration'].widget = self.fields['duration'].hidden_widget()
self.fields['user'].initial = kwargs['user'].pk
self.fields['user'].widget = self.fields['user'].hidden_widget()
self.fields['ticket'].widget = self.fields['ticket'].hidden_widget()
self.fields['parent'].widget = self.fields['parent'].hidden_widget()
self.fields['comment_type'].widget = self.fields['comment_type'].hidden_widget()
if not( self._has_import_permission or self._has_triage_permission or request.user.is_superuser ):
self.fields['source'].initial = TicketComment.CommentSource.HELPDESK
self.fields['source'].widget = self.fields['source'].hidden_widget()
else:
self.fields['source'].initial = TicketComment.CommentSource.DIRECT
if self._comment_type == 'task':
self.fields['comment_type'].initial = self.Meta.model.CommentType.TASK
self.fields['category'].queryset = self.fields['category'].queryset.filter(
task = True
)
elif self._comment_type == 'comment':
self.fields['comment_type'].initial = self.Meta.model.CommentType.COMMENT
self.fields['category'].queryset = self.fields['category'].queryset.filter(
comment = True
)
elif self._comment_type == 'solution':
self.fields['comment_type'].initial = self.Meta.model.CommentType.SOLUTION
self.fields['category'].queryset = self.fields['category'].queryset.filter(
solution = True
)
elif self._comment_type == 'notification':
self.fields['comment_type'].initial = self.Meta.model.CommentType.NOTIFICATION
self.fields['category'].queryset = self.fields['category'].queryset.filter(
notification = True
)
allowed_fields = self.fields_allowed
original_fields = self.fields.copy()
for field in original_fields:
if field not in allowed_fields and not self.fields[field].widget.is_hidden:
del self.fields[field]
def clean(self):
cleaned_data = super().clean()
return cleaned_data
def is_valid(self) -> bool:
is_valid = super().is_valid()
validate_ticket_comment: bool = self.validate_ticket_comment()
if not validate_ticket_comment:
is_valid = validate_ticket_comment
return is_valid
class DetailForm(CommentForm):
prefix = 'ticket'
class Meta:
model = TicketComment
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@ -0,0 +1,119 @@
from django import forms
from django.forms import ValidationError
from django.urls import reverse
from app import settings
from core.forms.common import CommonModelForm
from core.models.ticket.ticket_comment_category import TicketCommentCategory
class TicketCommentCategoryForm(CommonModelForm):
class Meta:
fields = '__all__'
model = TicketCommentCategory
prefix = 'ticket_comment_category'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['parent'].queryset = self.fields['parent'].queryset.exclude(
id=self.instance.pk
)
def clean(self):
cleaned_data = super().clean()
pk = self.instance.id
parent = cleaned_data.get("parent")
if pk:
if parent == pk:
raise ValidationError("Category can't have itself as its parent category")
return cleaned_data
class DetailForm(TicketCommentCategoryForm):
tabs: dict = {
"details": {
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'parent',
'name',
'runbook',
'organization',
'c_created',
'c_modified'
],
"right": [
'model_notes',
]
},
{
"layout": "double",
"name": "Comment Types",
"left": [
'comment',
'solution'
],
"right": [
'notification',
'task'
]
},
]
},
"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:_ticket_comment_category_change', kwargs={'pk': self.instance.pk})
})
self.url_index_view = reverse('Settings:_ticket_comment_categories')

View File

@ -0,0 +1,26 @@
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_linked_items import TicketLinkedItem
class TicketLinkedItemForm(CommonModelForm):
prefix = 'ticket_linked_item'
class Meta:
model = TicketLinkedItem
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['organization'].widget = self.fields['organization'].hidden_widget()
self.fields['ticket'].widget = self.fields['ticket'].hidden_widget()

View File

@ -0,0 +1,668 @@
from django.core.exceptions import PermissionDenied, ValidationError
from rest_framework import serializers
from access.mixin import OrganizationMixin
class TicketValidation(
OrganizationMixin,
):
"""Ticket Form/Serializer Validation
Validate a ticket form or api viewset
## Class requirements
- attribute `ticket_type_fields` is set to a list of fields for the ticket type
- attribute `ticket_type` is set to a string value (lowercase) of the ticket type
Raises:
PermissionDenied: User has no allowable fields to edit
PermissionDenied: User is lacking permission to edit a field
serializers.ValidationError: Status field has a value set that does not meet the ticket type
ValidationError: Status field has a value set that does not meet the ticket type
"""
original_object = None
add_fields: list = [
'title',
'description',
'urgency',
'organization'
]
change_fields: list = []
delete_fields: list = [
'is_deleted',
]
import_fields: list = [
'assigned_users',
'assigned_teams',
'category',
'created',
'date_closed',
'estimate',
'external_ref',
'external_system',
'status',
'impact',
'opened_by',
'planned_start_date',
'planned_finish_date',
'priority',
'project',
'milestone',
'real_start_date',
'real_finish_date',
'subscribed_users',
'subscribed_teams',
'ticket_type',
]
triage_fields: list = [
'category',
'assigned_users',
'assigned_teams',
'estimate',
'status',
'impact',
'opened_by',
'planned_start_date',
'planned_finish_date',
'priority',
'project',
'milestone',
'real_start_date',
'real_finish_date',
'subscribed_users',
'subscribed_teams',
]
def combined_validation_error(self, message:str, code:str = None) -> None:
if 'serializers' in self.Meta.__module__:
raise serializers.ValidationError(
detail = message,
code = code
)
else:
raise ValidationError(
message = message,
code = code
)
@property
def get_fields_allowed_by_permission(self):
if hasattr(self, '_fields_allowed_by_permission'):
return self._fields_allowed_by_permission
if not hasattr(self, '_ticket_type'):
self._ticket_type = self.initial['type_ticket']
fields_allowed: list = []
if self.instance is not None:
ticket_organization = self.instance.organization
else:
ticket_organization = self.validated_data['organization']
if ticket_organization is None:
ticket_organization = self.initial['organization']
if ticket_organization is None:
if 'organization' in self.data:
ticket_organization = self.fields['organization'].queryset.model.objects.get(pk=self.data['organization'])
if self.has_organization_permission(
organization=ticket_organization.id,
permissions_required = [ 'core.add_ticket_'+ self._ticket_type ],
) and not self.request.user.is_superuser:
fields_allowed = self.add_fields
if self.has_organization_permission(
organization=ticket_organization.id,
permissions_required = [ 'core.change_ticket_'+ self._ticket_type ],
) and not self.request.user.is_superuser:
if len(fields_allowed) == 0:
fields_allowed = self.add_fields + self.change_fields
else:
fields_allowed = fields_allowed + self.change_fields
if self.has_organization_permission(
organization=ticket_organization.id,
permissions_required = [ 'core.delete_ticket_'+ self._ticket_type ],
) and not self.request.user.is_superuser:
fields_allowed = fields_allowed + self.delete_fields
if self.has_organization_permission(
organization=ticket_organization.id,
permissions_required = [ 'core.import_ticket_'+ self._ticket_type ],
) and not self.request.user.is_superuser:
if hasattr(self, 'serializer_choice_field'):
fields_allowed = fields_allowed + self.import_fields
if self.has_organization_permission(
organization=ticket_organization.id,
permissions_required = [ 'core.triage_ticket_'+ self._ticket_type ],
) and not self.request.user.is_superuser:
fields_allowed = fields_allowed + self.triage_fields
if self.request.user.is_superuser:
all_fields: list = self.add_fields
all_fields = all_fields + self.change_fields
all_fields = all_fields + self.delete_fields
all_fields = all_fields + self.import_fields
all_fields = all_fields + self.triage_fields
fields_allowed = fields_allowed + all_fields
self._fields_allowed_by_permission = fields_allowed
return fields_allowed
@property
def get_user_changed_data(self) -> dict:
"""Create an object with the user 'changed' data.
Due to forms having fields deleted, this function is required
as attribute `cleaned_data` no longer functions per normal.
Returns:
_user_changed_data (dict): Changed data.
"""
if hasattr(self, '_user_changed_data'):
return self._user_changed_data
changed_data: dict = {}
for field in self.get_user_changed_fields:
if hasattr(self.Meta.model, field):
changed_data.update({
field: self.request.POST.dict()[field]
})
if len(changed_data) > 0:
self._user_changed_data = changed_data
return changed_data
@property
def get_user_changed_fields(self) -> list(str()):
"""List of fields the user changed.
This data is sourced from the HTTP/POST data.
Returns:
list: All of the fields that have changed.
"""
if hasattr(self, '_user_changed_fields'):
return self._user_changed_fields
changed_data: list = []
changed_data_exempt = [
'_state',
'csrfmiddlewaretoken',
'ticket_comments',
'url',
]
post_data: dict = self.request.POST.dict().copy()
for field in post_data:
if hasattr(self.Meta.model, field):
changed_data = changed_data + [ field ]
if len(changed_data) > 0:
self._user_changed_fields = changed_data
return changed_data
@property
def validate_field_permission(self):
""" Check field permissions
Users can't edit all fields. They can only adjust fields that they
have the permissions to adjust.
## Required fields
A field marked as required when the instance has no pk, the field will have
it's permission marked as allowed. This is not the case for items thaat are being
edited, i.e. have a pk.
Raises:
ValidationError: Access Denied when user has no ticket permissions assigned
ValidationError: User tried to edit a field they dont have permission to edit.
"""
fields_allowed = self.get_fields_allowed_by_permission
if len(fields_allowed) == 0:
self.combined_validation_error('Access Denied to all fields', code='access_denied_all_fields')
for field in self.get_user_changed_fields:
allowed: bool = False
if (
field in self.fields
and field in self.ticket_type_fields
and (
field in fields_allowed
)
):
allowed = True
if hasattr(self.instance, 'pk'):
if (
field in self.fields
and field in self.ticket_type_fields
and self.instance.pk is None
):
if self.fields[field].required:
allowed = True
elif self.instance is None:
if self.fields[field].required:
allowed = True
if not allowed:
if (
self.field_edited(field)
or (
field not in fields_allowed
and hasattr(self.Meta.model, field)
)
):
self.combined_validation_error(
f'cant edit field: {field}',
code=f'cant_edit_field_{field}',
)
return False
return True
def field_edited(self, field:str) -> bool:
if hasattr(self, 'cleaned_data'): # initial avail in ui
initial_data: dict = self.initial
changed_data: dict = self.get_user_changed_data
elif hasattr(self, 'validated_data'): # API
initial_data:dict = self.instance.__dict__
changed_data: dict = self.validated_data
if field in initial_data:
value = initial_data[field]
elif str(field) + '_id' in initial_data:
value = initial_data[str(field) + '_id']
else:
return True
if field in changed_data:
if changed_data[field] == value:
return False
if hasattr(changed_data[field], 'id'):
if value is None:
return True
if int(value) == changed_data[field].id:
return False
else:
val = value
if value is None:
return True
if type(changed_data[field]) is int:
val = int(value)
elif type(changed_data[field]) is bool:
val = bool(value)
elif type(changed_data[field]) is str:
val = str(value)
if val == changed_data[field]:
return False
else:
return False
return True
def validate_field_milestone(self):
is_valid: bool = False
if self.instance is not None:
if self.instance.milestone is None:
return True
else:
if self.instance.project is None:
self.combined_validation_error(
f'Milestones require a project',
code=f'milestone_requires_project',
)
return False
if self.instance.project.id == self.instance.milestone.project.id:
return True
else:
self.combined_validation_error(
f'Milestone must be from the same project',
code=f'milestone_same_project',
)
return is_valid
def validate_field_organization(self) -> bool:
"""Check `organization field`
Raises:
ValidationError: user tried to change the organization
Returns:
True (bool): OK
False (bool): User tried to edit the organization
"""
is_valid: bool = True
if self.instance is not None:
if self.instance.pk is not None:
if 'organization' in self.get_user_changed_fields:
if self.field_edited('organization'):
is_valid = False
self.combined_validation_error(
f'cant edit field: organization',
code=f'cant_edit_field_organization',
)
return is_valid
def validate_field_status(self):
"""Validate status field
Ticket status depends upon ticket type.
Ensure that the corrent status is used.
"""
is_valid = False
if not hasattr(self, '_ticket_type'):
self._ticket_type = self.initial['type_ticket']
try:
if hasattr(self, 'cleaned_data'):
field = self.cleaned_data['status']
else:
field = self.validated_data['status']
except KeyError:
# field = self.fields['status'].default.value
field = getattr(self.Meta.model, 'status').field.default.value
if self._ticket_type == 'request':
if field in self.Meta.model.TicketStatus.Request._value2member_map_:
is_valid = True
elif self._ticket_type == 'incident':
if field in self.Meta.model.TicketStatus.Incident._value2member_map_:
is_valid = True
elif self._ticket_type == 'problem':
if field in self.Meta.model.TicketStatus.Problem._value2member_map_:
is_valid = True
elif self._ticket_type == 'change':
if field in self.Meta.model.TicketStatus.Change._value2member_map_:
is_valid = True
elif self._ticket_type == 'issue':
if field in self.Meta.model.TicketStatus.Issue._value2member_map_:
is_valid = True
elif self._ticket_type == 'merge':
if field in self.Meta.model.TicketStatus.Merge._value2member_map_:
is_valid = True
elif self._ticket_type == 'project_task':
if field in self.Meta.model.TicketStatus.ProjectTask._value2member_map_:
is_valid = True
if not is_valid:
if hasattr(self, 'validated_data'):
raise serializers.ValidationError('Incorrect Status set')
else:
self.combined_validation_error('Incorrect Status set')
return is_valid
def validate_ticket(self):
"""Validations common to all ticket types."""
is_valid = False
fields: list = []
if hasattr(self, 'validated_data'):
fields = self.validated_data
else:
fields = self.cleaned_data
validate_field_permission = False
if self.validate_field_permission:
validate_field_permission = True
validate_field_organization: bool = False
if self.validate_field_organization():
validate_field_organization = True
validate_field_milestone: bool = False
if self.validate_field_milestone():
validate_field_milestone: bool = True
validate_field_status = False
if self.validate_field_status():
validate_field_status = True
if (
validate_field_permission
and validate_field_status
and validate_field_milestone
and validate_field_organization
):
is_valid = True
return is_valid
def validate_change_ticket(self):
# check status
# check type
pass
def validate_incident_ticket(self):
# check status
# check type
pass
def validate_problem_ticket(self):
# check status
# check type
pass
def validate_request_ticket(self):
# check status
# check type
# self.combined_validation_error('Test to see what it looks like')
pass
def validate_project_task_ticket(self):
if hasattr(self,'_project'):
self.cleaned_data.update({
'project': self._project
})
if self.cleaned_data['project'] is None:
self.combined_validation_error('A project task requires a project')

View File

@ -0,0 +1,276 @@
from django.core.exceptions import PermissionDenied
from django.forms import ValidationError
from rest_framework import serializers
from access.mixin import OrganizationMixin
class TicketCommentValidation(
OrganizationMixin,
):
original_object = None
_comment_type:str = None
"""Human readable comment type. i.e. `request` in lowercase"""
_has_add_permission: bool = False
_has_change_permission: bool = False
_has_delete_permission: bool = False
_has_import_permission: bool = False
_has_triage_permission: bool = False
_ticket_organization = None
"""Ticket Organization as a organization object"""
_ticket_type: str = None
"""Human readable type of ticket. i.e. `request` in lowercase"""
request = None
add_fields: list = [
'body',
'duration'
]
change_fields: list = [
'body',
]
delete_fields: list = [
'is_deleted',
]
import_fields: list = [
'organization',
'parent',
'ticket',
'external_ref',
'external_system',
'comment_type',
'body',
'category',
'created',
'modified',
'private',
'duration',
'template',
'is_template',
'source',
'status',
'responsible_user',
'responsible_team',
'user',
'date_closed',
'planned_start_date',
'planned_finish_date',
'real_start_date',
'real_finish_date',
]
triage_fields: list = [
'category',
'body',
'private',
'duration',
'template',
'is_template',
'source',
'status',
'responsible_user',
'responsible_team',
'planned_start_date',
'planned_finish_date',
'real_start_date',
'real_finish_date',
]
@property
def fields_allowed(self) -> list(str()):
""" Get the allowed fields for a ticket ccomment
Returns:
list(str): A list of allowed fields for the user
"""
if self.request is None:
raise ValueError('Attribute self.request must be set')
fields_allowed: list = []
if self._has_add_permission and not self.request.user.is_superuser:
fields_allowed = self.add_fields
if self._has_change_permission and not self.request.user.is_superuser:
fields_allowed = self.change_fields
if self._has_delete_permission and not self.request.user.is_superuser:
fields_allowed = fields_allowed + self.delete_fields
if self._has_import_permission and not self.request.user.is_superuser:
fields_allowed = fields_allowed + self.import_fields
if self._has_triage_permission and not self.request.user.is_superuser:
fields_allowed = fields_allowed + self.triage_fields
if self.request.user.is_superuser:
all_fields: list = self.add_fields
all_fields = all_fields + self.change_fields
all_fields = all_fields + self.delete_fields
all_fields = all_fields + self.import_fields
all_fields = all_fields + self.triage_fields
fields_allowed = fields_allowed + all_fields
comment_fields = []
if (
self._ticket_type == 'request'
or
self._ticket_type == 'incident'
or
self._ticket_type == 'problem'
or
self._ticket_type == 'change'
or
self._ticket_type == 'project_task'
):
if self._comment_type == 'task':
comment_fields = self.Meta.model.fields_itsm_task
self.fields['comment_type'].initial = self.Meta.model.CommentType.TASK
elif self._comment_type == 'comment':
comment_fields = self.Meta.model.common_itsm_fields
self.fields['comment_type'].initial = self.Meta.model.CommentType.COMMENT
elif self._comment_type == 'solution':
comment_fields = self.Meta.model.common_itsm_fields
self.fields['comment_type'].initial = self.Meta.model.CommentType.SOLUTION
elif self._comment_type == 'notification':
comment_fields = self.Meta.model.fields_itsm_notification
self.fields['comment_type'].initial = self.Meta.model.CommentType.NOTIFICATION
elif self._ticket_type == 'issue':
comment_fields = self.Meta.model.fields_git_issue
elif self._ticket_type == 'merge':
comment_fields = self.Meta.model.fields_git_merge
for comment_field in comment_fields:
if comment_field not in fields_allowed:
comment_fields.remove(comment_field)
return comment_fields
@property
def ticket_comment_permissions(self):
if self._ticket_organization is None:
raise ValueError('Attribute self._ticket_organization must be set')
if self.request is None:
raise ValueError('Attribute self.request must be set')
if self.has_organization_permission(
organization=self._ticket_organization.id,
permissions_required = [ 'core.add_ticket_'+ self._ticket_type ],
) and not self.request.user.is_superuser:
self._has_add_permission = True
if (
self.has_organization_permission(
organization=self._ticket_organization.id,
permissions_required = [ 'core.change_ticketcomment' ],
) or
self.request.user.id == self.instance.user_id
) and not self.request.user.is_superuser:
self._has_change_permission = True
if self.has_organization_permission(
organization=self._ticket_organization.id,
permissions_required = [ 'core.delete_ticketcomment' ],
) and not self.request.user.is_superuser:
self._has_delete_permission = True
if self.has_organization_permission(
organization=self._ticket_organization.id,
permissions_required = [ 'core.import_ticketcomment' ],
) and not self.request.user.is_superuser:
self._has_import_permission = True
if self.has_organization_permission(
organization=self._ticket_organization.id,
permissions_required = [ 'core.triage_ticket_'+ self._ticket_type ],
) and not self.request.user.is_superuser:
self._has_triage_permission = True
if (
not self._has_triage_permission and (
self._comment_type == 'notification' or
self._comment_type == 'task' or
self._comment_type == 'solution'
)
) and not self.request.user.is_superuser:
raise PermissionDenied("You dont have permission for comment types: notification, task and solution")
def validate_ticket_comment(self) -> bool:
is_valid: bool = True
self.ticket_comment_permissions
fields_allowed = self.fields_allowed
for field in self.change_fields:
if field not in fields_allowed:
raise PermissionDenied(f'You tried to edit a field ({field}) that you dont have access to edit')
return is_valid

0
app/core/lib/__init__.py Normal file
View File

114
app/core/lib/markdown.py Normal file
View File

@ -0,0 +1,114 @@
import re
from markdown_it import MarkdownIt
from mdit_py_plugins import admon, anchors, footnote, tasklists
from pygments import highlight
from pygments.formatters.html import HtmlFormatter
from pygments.lexers import get_lexer_by_name
from django.template.loader import render_to_string
from .markdown_plugins import ticket_number, model_reference
class Markdown:
"""Ticket and Comment markdown functions
Intended to be used for all areas of a tickets, projects and comments.
"""
def highlight_func(self, code: str, lang: str, _) -> str | None:
"""Use pygments for code high lighting"""
if not lang:
return None
lexer = get_lexer_by_name(lang)
formatter = HtmlFormatter(style='vs', cssclass='codehilite')
return highlight(code, lexer, formatter)
def render_markdown(self, markdown_text):
"""Render Markdown
implemented using https://markdown-it-py.readthedocs.io/en/latest/index.html
Args:
markdown_text (str): Markdown text
Returns:
str: HTML text
"""
md = (
MarkdownIt(
config = "js-default",
options_update={
'linkify': True,
'highlight': self.highlight_func,
}
)
.enable([
'linkify',
'strikethrough',
'table',
])
.use(admon.admon_plugin)
.use(anchors.anchors_plugin, permalink=True)
.use(footnote.footnote_plugin)
.use(tasklists.tasklists_plugin)
.use(ticket_number.plugin, enabled=True)
.use(model_reference.plugin, enabled=True)
)
return md.render(markdown_text)
def build_ticket_html(self, match):
ticket_id = match.group(1)
try:
if hasattr(self, 'ticket'):
ticket = self.ticket.__class__.objects.get(pk=ticket_id)
else:
ticket = self.__class__.objects.get(pk=ticket_id)
project_id = str('0')
if ticket.project:
project_id = str(ticket.project.id).lower()
context: dict = {
'id': ticket.id,
'name': ticket,
'ticket_type': str(ticket.get_ticket_type_display()).lower(),
'ticket_status': str(ticket.get_status_display()).lower(),
'project_id': project_id,
}
html_link = render_to_string('core/ticket/renderers/ticket_link.html.j2', context)
return str(html_link)
except:
return str('#' + ticket_id)
def ticket_reference(self, text):
return re.sub('#(\d+)', self.build_ticket_html, text)

View File

@ -0,0 +1,155 @@
import re
from django.template import Context, Template
from django.template.loader import render_to_string
from django.urls import reverse
from markdown_it import MarkdownIt
from markdown_it.rules_core import StateCore
from markdown_it.token import Token
# Regex string to match a whitespace character, as specified in
# https://github.github.com/gfm/#whitespace-character
# (spec version 0.29-gfm (2019-04-06))
_GFM_WHITESPACE_RE = r"[ \t\n\v\f\r]"
def plugin(
md: MarkdownIt,
enabled: bool = False,
) -> None:
"""markdown_it plugin to render model references
Placing `$<type>-<number>` within markdown will be rendered as a pretty link to the model.
Args:
md (MarkdownIt): markdown object
enabled (bool, optional): Enable the parsing of model references. Defaults to False.
Returns:
None: nada
"""
def main(state: StateCore) -> None:
tokens = state.tokens
for i in range(0, len(tokens) - 1):
if is_tag_item(tokens, i):
tag_render(tokens[i])
def is_inline(token: Token) -> bool:
return token.type == "inline"
def is_tag_item(tokens: list[Token], index: int) -> bool:
return (
is_inline(tokens[index])
and contains_tag_item(tokens[index])
)
def tag_html(match):
id = match.group('id')
item_type = match.group('type')
try:
if item_type == 'cluster':
from itim.models.clusters import Cluster
model = Cluster
url = reverse('ITIM:_cluster_view', kwargs={'pk': int(id)})
elif item_type == 'config_group':
from config_management.models.groups import ConfigGroups
model = ConfigGroups
url = reverse('Config Management:_group_view', kwargs={'pk': int(id)})
elif item_type == 'device':
from itam.models.device import Device
model = Device
url = reverse('ITAM:_device_view', kwargs={'pk': int(id)})
elif item_type == 'operating_system':
from itam.models.operating_system import OperatingSystem
model = OperatingSystem
url = reverse('ITAM:_operating_system_view', kwargs={'pk': int(id)})
elif item_type == 'service':
from itim.models.services import Service
model = Service
url = reverse('ITIM:_service_view', kwargs={'pk': int(id)})
elif item_type == 'software':
from itam.models.software import Software
model = Software
url = reverse('ITAM:_software_view', kwargs={'pk': int(id)})
if url:
item = model.objects.get(
pk = int(id)
)
html_template = Template('''
<a href="{{ url }}">
{{ name }}, <span style="color: #777; font-size: smaller;">{{ item_type }}</span>
</a>
''')
context = Context({
'url': url,
'item_type': item_type,
'name': item.name
})
html = html_template.render(context)
return html
except Exception as e:
return str(f'${item_type}-{id}')
def tag_render(token: Token) -> None:
assert token.children is not None
checkbox = Token("html_inline", "", 0)
checkbox.content = tag_replace(token.content)
token.children[0] = checkbox
def tag_replace(text):
return re.sub('\$(?P<type>[a-z_]+)-(?P<id>\d+)', tag_html, text)
def contains_tag_item(token: Token) -> bool:
return re.match(rf"(.+)?\$[a-z_]+-\d+{_GFM_WHITESPACE_RE}?(.+)?", token.content) is not None
if enabled:
md.core.ruler.after("inline", "links", fn=main)

View File

@ -0,0 +1,103 @@
import re
from django.template.loader import render_to_string
from markdown_it import MarkdownIt
from markdown_it.rules_core import StateCore
from markdown_it.token import Token
# Regex string to match a whitespace character, as specified in
# https://github.github.com/gfm/#whitespace-character
# (spec version 0.29-gfm (2019-04-06))
_GFM_WHITESPACE_RE = r"[ \t\n\v\f\r]"
def plugin(
md: MarkdownIt,
enabled: bool = False,
) -> None:
"""markdown_it plugin to render ticket numbers
Placing `#<number>` within markdown will be rendered as a pretty link to the ticket.
Args:
md (MarkdownIt): markdown object
enabled (bool, optional): Enable the parsing of ticket references. Defaults to False.
Returns:
None: nada
"""
def main(state: StateCore) -> None:
tokens = state.tokens
for i in range(0, len(tokens) - 1):
if is_tag_item(tokens, i):
tag_render(tokens[i])
def is_inline(token: Token) -> bool:
return token.type == "inline"
def is_tag_item(tokens: list[Token], index: int) -> bool:
return (
is_inline(tokens[index])
and contains_tag_item(tokens[index])
)
def tag_html(match):
ticket_id = match.group(1)
try:
from core.models.ticket.ticket import Ticket
ticket = Ticket.objects.get(pk=ticket_id)
project_id = str('0')
if ticket.project:
project_id = str(ticket.project.id).lower()
context: dict = {
'id': ticket.id,
'name': ticket,
'ticket_type': str(ticket.get_ticket_type_display()).lower(),
'ticket_status': str(ticket.get_status_display()).lower(),
'project_id': project_id,
}
html_link = render_to_string('core/ticket/renderers/ticket_link.html.j2', context)
return html_link
except:
return str('#' + ticket_id)
def tag_render(token: Token) -> None:
assert token.children is not None
checkbox = Token("html_inline", "", 0)
checkbox.content = tag_replace(token.content)
token.children[0] = checkbox
def tag_replace(text):
return re.sub('#(\d+)', tag_html, text)
def contains_tag_item(token: Token) -> bool:
return re.match(rf"(.+)?#(\d+){_GFM_WHITESPACE_RE}?(.+)?", token.content) is not None
if enabled:
md.core.ruler.after("inline", "links", fn=main)

View File

@ -0,0 +1,47 @@
import re
from .duration import Duration
from .related_ticket import CommandRelatedTicket
from .linked_model import CommandLinkedModel
class SlashCommands(
Duration,
CommandRelatedTicket,
CommandLinkedModel,
):
"""Slash Commands Base Class
This class in intended to be included in the following models:
- Ticket
- TicketComment
Testing of regex can be done at https://pythex.org/
"""
def slash_command(self, markdown:str) -> str:
""" Slash Commands Processor
Markdown text that contains a slash command is passed to this function and on the processing
of any valid slash command, the slash command will be removed from the markdown.
If any error occurs when attempting to process the slash command, it will not be removed from
the markdown. This is by design so that the "errored" slash command can be inspected.
Args:
markdown (str): un-processed Markdown
Returns:
str: Markdown without the slash command text.
"""
markdown = re.sub(self.time_spent, self.command_duration, markdown)
markdown = re.sub(self.linked_item, self.command_linked_model, markdown)
markdown = re.sub(self.related_ticket, self.command_related_ticket, markdown)
return markdown

View File

@ -0,0 +1,96 @@
import re
class Duration:
# This summary is used for the user documentation
"""The command keyword is `spend` and you can also use `spent`. The formatting for the time
after the command, is `<digit>` then either `h`, `m`, `s` for hours, minutes and seconds respectively.
Valid commands are as follows:
- /spend 1h1ms
- /spend 1h 1m 1s
For this command to process the following conditions must be met:
- There is either a `<new line>` (`\\n`) or a `<space>` char immediatly before the slash `/`
- There is a `<space>` char after the command keyword, i.e. `/spend<space>1h`
- _Optional_ `<space>` char between the time blocks.
"""
time_spent: str = r'[\s|\n]\/(?P<command>[spend|spent]+)\s(?P<time>(?P<hours>\d+h)?\s?(?P<minutes>[\d]{1,2}m)?\s?(?P<seconds>\d+[s])?)[\s|\n]?'
def command_duration(self, match) -> str:
"""/spend, /spent processor
Slash command usage within a ticket description will add an action comment with the
time spent. For a ticket comment, it's duration field is set to the duration valuee calculated.
Args:
match (re.Match): Grouped matches
Returns:
str: The matched string if the duration calculation is `0`
None: On successfully processing the command
"""
a = 'a'
command = match.group('command')
time:str = str(match.group('time')).replace(' ', '')
hours = match.group('hours')
minutes = match.group('minutes')
seconds = match.group('seconds')
duration: int = 0
if hours is not None:
duration += int(hours[:-1])*60*60
if minutes is not None:
duration += int(minutes[:-1])*60
if seconds is not None:
duration += int(seconds[:-1])
if duration == 0:
#ToDo: Add logging that the slash command could not be processed.
return str(match.string[match.start():match.end()])
if str(self._meta.verbose_name).lower() == 'ticket':
from core.models.ticket.ticket_comment import TicketComment
comment_text = f'added {time} of time spent'
TicketComment.objects.create(
ticket = self,
comment_type = TicketComment.CommentType.ACTION,
body = comment_text,
duration = duration,
user = self.opened_by,
)
elif str(self._meta.verbose_name).lower() == 'comment':
self.duration = duration
else:
#ToDo: Add logging that the slash command could not be processed.
return str(match.string[match.start():match.end()])
return None

View File

@ -0,0 +1,148 @@
import re
class CommandLinkedModel:
# This summary is used for the user documentation
"""Link an item to the current ticket. Supports all ticket
relations: blocked by, blocks and related.
The command keyword is `link` along with the model reference, i.e. `$<type>-<number>`.
Valid commands are as follows:
- /link $device-1
- /link $cluster-55
Available model types for linking are as follows:
- cluster
- config_group
- device
- operating_system
- service
- software
For this command to process the following conditions must be met:
- There is either a `<new line>` (`\\n`) or a `<space>` char immediatly before the slash `/`
- There is a `<space>` char after the command keyword, i.e. `/link<space>$device-101`
"""
linked_item: str = r'[\s|\n]\/(?P<command>[link]+)\s\$(?P<type>[a-z_]+)-(?P<id>\d+)[\s|\n]?'
def command_linked_model(self, match) -> str:
"""/link processor
Slash command usage within a ticket description will add an action comment with the
time spent. For a ticket comment, it's duration field is set to the duration valuee calculated.
Args:
match (re.Match): Named group matches
Returns:
str: The matched string if the duration calculation is `0`
None: On successfully processing the command
"""
a = 'a'
command = match.group('command')
model_type:int = str(match.group('type'))
model_id:int = int(match.group('id'))
try:
from core.models.ticket.ticket_linked_items import TicketLinkedItem
if model_type == 'cluster':
from itim.models.clusters import Cluster
model = Cluster
item_type = TicketLinkedItem.Modules.CLUSTER
elif model_type == 'config_group':
from config_management.models.groups import ConfigGroups
model = ConfigGroups
item_type = TicketLinkedItem.Modules.CONFIG_GROUP
elif model_type == 'device':
from itam.models.device import Device
model = Device
item_type = TicketLinkedItem.Modules.DEVICE
elif model_type == 'operating_system':
from itam.models.operating_system import OperatingSystem
model = OperatingSystem
item_type = TicketLinkedItem.Modules.OPERATING_SYSTEM
elif model_type == 'service':
from itim.models.services import Service
model = Service
item_type = TicketLinkedItem.Modules.SERVICE
elif model_type == 'software':
from itam.models.software import Software
model = Software
item_type = TicketLinkedItem.Modules.SOFTWARE
else:
return str(match.string[match.start():match.end()])
if str(self._meta.verbose_name).lower() == 'ticket':
ticket = self
elif str(self._meta.verbose_name).lower() == 'comment':
ticket = self.ticket
if model:
item = model.objects.get(
pk = model_id
)
TicketLinkedItem.objects.create(
organization = self.organization,
ticket = ticket,
item_type = item_type,
item = item.id
)
return None
except Exception as e:
return str(match.string[match.start():match.end()])
return None

View File

@ -0,0 +1,98 @@
import re
class CommandRelatedTicket:
# This summary is used for the user documentation
"""Add to the current ticket a relationship to another ticket. Supports all ticket
relations: blocked by, blocks and related.
The command keywords are `relate`, `blocks` and `blocked_by` along with the ticket
reference, i.e. `#<ticket-number>`.
Valid commands are as follows:
- /relate #1
- /blocks #1
- /blocked_by #1
For this command to process the following conditions must be met:
- There is either a `<new line>` (`\\n`) or a `<space>` char immediatly before the slash `/`
- There is a `<space>` char after the command keyword, i.e. `/relate<space>#1`
"""
related_ticket: str = r'[\s|\n]\/(?P<command>[relate|blocks|blocked_by]+)\s\#(?P<ticket>\d+)[\s|\n]?'
def command_related_ticket(self, match) -> str:
"""/relate, /blocks and /blocked_by processor
Slash command usage within a ticket description will add an action comment with the
time spent. For a ticket comment, it's duration field is set to the duration valuee calculated.
Args:
match (re.Match): Named group matches
Returns:
str: The matched string if the duration calculation is `0`
None: On successfully processing the command
"""
a = 'a'
command = match.group('command')
ticket_id:int = str(match.group('ticket'))
if ticket_id is not None:
from core.models.ticket.ticket import RelatedTickets
if command == 'relate':
how_related = RelatedTickets.Related.RELATED.value
elif command == 'blocks':
how_related = RelatedTickets.Related.BLOCKS.value
elif command == 'blocked_by':
how_related = RelatedTickets.Related.BLOCKED_BY.value
else:
#ToDo: Add logging that the slash command could not be processed.
return str(match.string[match.start():match.end()])
if str(self._meta.verbose_name).lower() == 'ticket':
from_ticket = self
to_ticket = self.__class__.objects.get(pk = ticket_id)
elif str(self._meta.verbose_name).lower() == 'comment':
from_ticket = self.ticket
to_ticket = self.ticket.__class__.objects.get(pk = ticket_id)
RelatedTickets.objects.create(
from_ticket_id = from_ticket,
how_related = how_related,
to_ticket_id = to_ticket,
organization = self.organization
)
else:
#ToDo: Add logging that the slash command could not be processed.
return str(match.string[match.start():match.end()])
return None

View File

@ -0,0 +1,149 @@
# Generated by Django 5.0.8 on 2024-10-11 14:09
import access.fields
import access.models
import core.lib.slash_commands
import core.models.ticket.ticket
import core.models.ticket.ticket_comment
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0001_initial'),
('core', '0004_notes_service'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='TicketCategory',
fields=[
('is_global', models.BooleanField(default=False)),
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
('id', models.AutoField(help_text='Category ID Number', primary_key=True, serialize=False, unique=True, verbose_name='Number')),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('name', models.CharField(help_text='Category Name', max_length=50, verbose_name='Name')),
('change', models.BooleanField(default=True, help_text='Use category for change tickets', verbose_name='Change Tickets')),
('incident', models.BooleanField(default=True, help_text='Use category for incident tickets', verbose_name='Incident Tickets')),
('problem', models.BooleanField(default=True, help_text='Use category for problem tickets', verbose_name='Problem Tickets')),
('project_task', models.BooleanField(default=True, help_text='Use category for Project tasks', verbose_name='Project Tasks')),
('request', models.BooleanField(default=True, help_text='Use category for request tickets', verbose_name='Request Tickets')),
],
options={
'verbose_name': 'Ticket Category',
'verbose_name_plural': 'Ticket Categories',
'ordering': ['parent__name', 'name'],
},
),
migrations.CreateModel(
name='TicketComment',
fields=[
('id', models.AutoField(help_text='Comment ID Number', primary_key=True, serialize=False, unique=True, verbose_name='Number')),
('external_ref', models.IntegerField(blank=True, default=None, help_text='External System reference', null=True, verbose_name='Reference Number')),
('external_system', models.IntegerField(blank=True, choices=[(1, 'Github'), (2, 'Gitlab'), (9999, 'Custom #1 (Imported)'), (9998, 'Custom #2 (Imported)'), (9997, 'Custom #3 (Imported)'), (9996, 'Custom #4 (Imported)'), (9995, 'Custom #5 (Imported)'), (9994, 'Custom #6 (Imported)'), (9993, 'Custom #7 (Imported)'), (9992, 'Custom #8 (Imported)'), (9991, 'Custom #9 (Imported)')], default=None, help_text='External system this item derives', null=True, verbose_name='External System')),
('comment_type', models.IntegerField(choices=[(1, 'Action'), (2, 'Comment'), (3, 'Task'), (4, 'Notification'), (5, 'Solution')], default=2, help_text='The type of comment this is', validators=[core.models.ticket.ticket_comment.TicketComment.validation_comment_type], verbose_name='Type')),
('body', models.TextField(help_text='Comment contents', verbose_name='Comment')),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('private', models.BooleanField(default=False, help_text='Is this comment private', verbose_name='Private')),
('duration', models.IntegerField(default=0, help_text='Time spent in seconds', verbose_name='Duration')),
('is_template', models.BooleanField(default=False, help_text='Is this comment a template', verbose_name='Template')),
('source', models.IntegerField(choices=[(1, 'Direct'), (2, 'E-Mail'), (3, 'Helpdesk'), (4, 'Phone')], default=1, help_text='Origin type for this comment', verbose_name='Source')),
('status', models.IntegerField(choices=[(1, 'To Do'), (2, 'Done')], default=1, help_text='Status of comment', verbose_name='Status')),
('date_closed', models.DateTimeField(blank=True, help_text='Date ticket closed', null=True, verbose_name='Closed Date')),
('planned_start_date', models.DateTimeField(blank=True, help_text='Planned start date.', null=True, verbose_name='Planned Start Date')),
('planned_finish_date', models.DateTimeField(blank=True, help_text='Planned finish date', null=True, verbose_name='Planned Finish Date')),
('real_start_date', models.DateTimeField(blank=True, help_text='Real start date', null=True, verbose_name='Real Start Date')),
('real_finish_date', models.DateTimeField(blank=True, help_text='Real finish date', null=True, verbose_name='Real Finish Date')),
],
options={
'verbose_name': 'Comment',
'verbose_name_plural': 'Comments',
'ordering': ['ticket', 'parent_id'],
},
bases=(core.lib.slash_commands.SlashCommands, models.Model),
),
migrations.CreateModel(
name='TicketCommentCategory',
fields=[
('is_global', models.BooleanField(default=False)),
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
('id', models.AutoField(help_text='Category ID Number', primary_key=True, serialize=False, unique=True, verbose_name='Number')),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('name', models.CharField(help_text='Category Name', max_length=50, verbose_name='Name')),
('comment', models.BooleanField(default=True, help_text='Use category for standard comment', verbose_name='Comment')),
('notification', models.BooleanField(default=True, help_text='Use category for notification comment', verbose_name='Notification Comment')),
('solution', models.BooleanField(default=True, help_text='Use category for solution comment', verbose_name='Solution Comment')),
('task', models.BooleanField(default=True, help_text='Use category for task comment', verbose_name='Task Comment')),
],
options={
'verbose_name': 'Ticket Comment Category',
'verbose_name_plural': 'Ticket Comment Categories',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='TicketLinkedItem',
fields=[
('id', models.AutoField(help_text='ID Number', primary_key=True, serialize=False, unique=True, verbose_name='Number')),
('item_type', models.IntegerField(choices=[(1, 'Cluster'), (2, 'Config Group'), (3, 'Device'), (4, 'Operating System'), (5, 'Service'), (6, 'Software')], help_text='Python Model location for linked item', verbose_name='Item Type')),
('item', models.IntegerField(help_text='Item ID to link to ticket', verbose_name='Item ID')),
],
options={
'verbose_name': 'Ticket Linked Item',
'verbose_name_plural': 'Ticket linked Items',
'ordering': ['id'],
},
),
migrations.CreateModel(
name='RelatedTickets',
fields=[
('id', models.AutoField(help_text='Ticket ID Number', primary_key=True, serialize=False, unique=True, verbose_name='Number')),
('how_related', models.IntegerField(choices=[(1, 'Related'), (2, 'Blocks'), (3, 'Blocked By')], help_text='How is the ticket related', verbose_name='How Related')),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
],
options={
'ordering': ['id'],
},
),
migrations.CreateModel(
name='Ticket',
fields=[
('id', models.AutoField(help_text='Ticket ID Number', primary_key=True, serialize=False, unique=True, verbose_name='Number')),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('status', models.IntegerField(choices=[(1, 'Draft'), (2, 'New'), (3, 'Assigned'), (6, 'Assigned (Planning)'), (7, 'Pending'), (8, 'Solved'), (4, 'Closed'), (5, 'Invalid'), (10, 'Accepted'), (9, 'Under Observation'), (11, 'Evaluation'), (12, 'Approvals'), (13, 'Testing'), (14, 'Qualification'), (15, 'Applied'), (16, 'Review'), (17, 'Cancelled'), (18, 'Refused')], default=2, help_text='Status of ticket', verbose_name='Status')),
('title', models.CharField(help_text='Title of the Ticket', max_length=100, unique=True, verbose_name='Title')),
('description', models.TextField(help_text='Ticket Description', verbose_name='Description')),
('urgency', models.IntegerField(blank=True, choices=[(1, 'Very Low'), (2, 'Low'), (3, 'Medium'), (4, 'High'), (5, 'Very High')], default=1, help_text='How urgent is this tickets resolution for the user?', null=True, verbose_name='Urgency')),
('impact', models.IntegerField(blank=True, choices=[(1, 'Very Low'), (2, 'Low'), (3, 'Medium'), (4, 'High'), (5, 'Very High')], default=1, help_text='End user assessed impact', null=True, verbose_name='Impact')),
('priority', models.IntegerField(blank=True, choices=[(1, 'Very Low'), (2, 'Low'), (3, 'Medium'), (4, 'High'), (5, 'Very High'), (6, 'Major')], default=1, help_text='What priority should this ticket for its completion', null=True, verbose_name='Priority')),
('external_ref', models.IntegerField(blank=True, default=None, help_text='External System reference', null=True, verbose_name='Reference Number')),
('external_system', models.IntegerField(blank=True, choices=[(1, 'Github'), (2, 'Gitlab'), (9999, 'Custom #1 (Imported)'), (9998, 'Custom #2 (Imported)'), (9997, 'Custom #3 (Imported)'), (9996, 'Custom #4 (Imported)'), (9995, 'Custom #5 (Imported)'), (9994, 'Custom #6 (Imported)'), (9993, 'Custom #7 (Imported)'), (9992, 'Custom #8 (Imported)'), (9991, 'Custom #9 (Imported)')], default=None, help_text='External system this item derives', null=True, verbose_name='External System')),
('ticket_type', models.IntegerField(choices=[(1, 'Request'), (2, 'Incident'), (3, 'Change'), (4, 'Problem'), (5, 'Issue'), (6, 'Merge Request'), (7, 'Project Task')], help_text='The type of ticket this is', validators=[core.models.ticket.ticket.Ticket.validation_ticket_type], verbose_name='Type')),
('is_deleted', models.BooleanField(default=False, help_text='Is the ticket deleted? And ready to be purged', verbose_name='Deleted')),
('date_closed', models.DateTimeField(blank=True, help_text='Date ticket closed', null=True, verbose_name='Closed Date')),
('planned_start_date', models.DateTimeField(blank=True, help_text='Planned start date.', null=True, verbose_name='Planned Start Date')),
('planned_finish_date', models.DateTimeField(blank=True, help_text='Planned finish date', null=True, verbose_name='Planned Finish Date')),
('estimate', models.IntegerField(default=0, help_text='Time Eastimated to complete this ticket in seconds', verbose_name='Estimation')),
('real_start_date', models.DateTimeField(blank=True, help_text='Real start date', null=True, verbose_name='Real Start Date')),
('real_finish_date', models.DateTimeField(blank=True, help_text='Real finish date', null=True, verbose_name='Real Finish Date')),
('assigned_teams', models.ManyToManyField(blank=True, help_text='Assign the ticket to a Team(s)', related_name='assigned_teams', to='access.team', verbose_name='Assigned Team(s)')),
('assigned_users', models.ManyToManyField(blank=True, help_text='Assign the ticket to a User(s)', related_name='assigned_users', to=settings.AUTH_USER_MODEL, verbose_name='Assigned User(s)')),
],
options={
'verbose_name': 'Ticket',
'verbose_name_plural': 'Tickets',
'ordering': ['id'],
'permissions': [('add_ticket_request', 'Can add a request ticket'), ('change_ticket_request', 'Can change any request ticket'), ('delete_ticket_request', 'Can delete a request ticket'), ('import_ticket_request', 'Can import a request ticket'), ('purge_ticket_request', 'Can purge a request ticket'), ('triage_ticket_request', 'Can triage all request ticket'), ('view_ticket_request', 'Can view all request ticket'), ('add_ticket_incident', 'Can add a incident ticket'), ('change_ticket_incident', 'Can change any incident ticket'), ('delete_ticket_incident', 'Can delete a incident ticket'), ('import_ticket_incident', 'Can import a incident ticket'), ('purge_ticket_incident', 'Can purge a incident ticket'), ('triage_ticket_incident', 'Can triage all incident ticket'), ('view_ticket_incident', 'Can view all incident ticket'), ('add_ticket_problem', 'Can add a problem ticket'), ('change_ticket_problem', 'Can change any problem ticket'), ('delete_ticket_problem', 'Can delete a problem ticket'), ('import_ticket_problem', 'Can import a problem ticket'), ('purge_ticket_problem', 'Can purge a problem ticket'), ('triage_ticket_problem', 'Can triage all problem ticket'), ('view_ticket_problem', 'Can view all problem ticket'), ('add_ticket_change', 'Can add a change ticket'), ('change_ticket_change', 'Can change any change ticket'), ('delete_ticket_change', 'Can delete a change ticket'), ('import_ticket_change', 'Can import a change ticket'), ('purge_ticket_change', 'Can purge a change ticket'), ('triage_ticket_change', 'Can triage all change ticket'), ('view_ticket_change', 'Can view all change ticket'), ('add_ticket_project_task', 'Can add a project task'), ('change_ticket_project_task', 'Can change any project task'), ('delete_ticket_project_task', 'Can delete a project task'), ('import_ticket_project_task', 'Can import a project task'), ('purge_ticket_project_task', 'Can purge a project task'), ('triage_ticket_project_task', 'Can triage all project task'), ('view_ticket_project_task', 'Can view all project task')],
},
bases=(core.lib.slash_commands.SlashCommands, models.Model),
),
]

View File

@ -0,0 +1,154 @@
# Generated by Django 5.0.8 on 2024-10-11 14:09
import access.models
import core.models.ticket.ticket_comment
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0001_initial'),
('assistance', '0001_initial'),
('core', '0005_ticketcategory_ticketcomment_ticketcommentcategory_and_more'),
('project_management', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='ticket',
name='milestone',
field=models.ForeignKey(blank=True, help_text='Assign to a milestone', null=True, on_delete=django.db.models.deletion.SET_NULL, to='project_management.projectmilestone', verbose_name='Project Milestone'),
),
migrations.AddField(
model_name='ticket',
name='opened_by',
field=models.ForeignKey(help_text='Who is the ticket for', on_delete=django.db.models.deletion.DO_NOTHING, related_name='opened_by', to=settings.AUTH_USER_MODEL, verbose_name='Opened By'),
),
migrations.AddField(
model_name='ticket',
name='organization',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]),
),
migrations.AddField(
model_name='ticket',
name='project',
field=models.ForeignKey(blank=True, help_text='Assign to a project', null=True, on_delete=django.db.models.deletion.SET_NULL, to='project_management.project', verbose_name='Project'),
),
migrations.AddField(
model_name='ticket',
name='subscribed_teams',
field=models.ManyToManyField(blank=True, help_text='Subscribe a Team(s) to the ticket to receive updates', related_name='subscribed_teams', to='access.team', verbose_name='Subscribed Team(s)'),
),
migrations.AddField(
model_name='ticket',
name='subscribed_users',
field=models.ManyToManyField(blank=True, help_text='Subscribe a User(s) to the ticket to receive updates', related_name='subscribed_users', to=settings.AUTH_USER_MODEL, verbose_name='Subscribed User(s)'),
),
migrations.AddField(
model_name='relatedtickets',
name='from_ticket_id',
field=models.ForeignKey(help_text='This Ticket', on_delete=django.db.models.deletion.CASCADE, related_name='from_ticket_id', to='core.ticket', verbose_name='Ticket'),
),
migrations.AddField(
model_name='relatedtickets',
name='to_ticket_id',
field=models.ForeignKey(help_text='The Related Ticket', on_delete=django.db.models.deletion.CASCADE, related_name='to_ticket_id', to='core.ticket', verbose_name='Related Ticket'),
),
migrations.AddField(
model_name='ticketcategory',
name='organization',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]),
),
migrations.AddField(
model_name='ticketcategory',
name='parent',
field=models.ForeignKey(blank=True, help_text='The Parent Category', null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.ticketcategory', verbose_name='Parent Category'),
),
migrations.AddField(
model_name='ticketcategory',
name='runbook',
field=models.ForeignKey(blank=True, help_text='The runbook for this category', null=True, on_delete=django.db.models.deletion.SET_NULL, to='assistance.knowledgebase', verbose_name='Runbook'),
),
migrations.AddField(
model_name='ticket',
name='category',
field=models.ForeignKey(blank=True, help_text='Category for this ticket', null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.ticketcategory', verbose_name='Category'),
),
migrations.AddField(
model_name='ticketcomment',
name='organization',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]),
),
migrations.AddField(
model_name='ticketcomment',
name='parent',
field=models.ForeignKey(blank=True, default=None, help_text='Parent ID for creating discussion threads', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='core.ticketcomment', verbose_name='Parent Comment'),
),
migrations.AddField(
model_name='ticketcomment',
name='responsible_team',
field=models.ForeignKey(blank=True, default=None, help_text='Team whom is responsible for the completion of comment', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='comment_responsible_team', to='access.team', verbose_name='Responsible Team'),
),
migrations.AddField(
model_name='ticketcomment',
name='responsible_user',
field=models.ForeignKey(blank=True, default=None, help_text='User whom is responsible for the completion of comment', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='comment_responsible_user', to=settings.AUTH_USER_MODEL, verbose_name='Responsible User'),
),
migrations.AddField(
model_name='ticketcomment',
name='template',
field=models.ForeignKey(blank=True, default=None, help_text='Comment Template to use', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='comment_template', to='core.ticketcomment', verbose_name='Template'),
),
migrations.AddField(
model_name='ticketcomment',
name='ticket',
field=models.ForeignKey(blank=True, default=None, help_text='Ticket this comment belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='core.ticket', validators=[core.models.ticket.ticket_comment.TicketComment.validation_ticket_id], verbose_name='Ticket'),
),
migrations.AddField(
model_name='ticketcomment',
name='user',
field=models.ForeignKey(blank=True, help_text='Who made the comment', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='comment_user', to=settings.AUTH_USER_MODEL, verbose_name='User'),
),
migrations.AddField(
model_name='ticketcommentcategory',
name='organization',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]),
),
migrations.AddField(
model_name='ticketcommentcategory',
name='parent',
field=models.ForeignKey(blank=True, help_text='The Parent Category', null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.ticketcommentcategory', verbose_name='Parent Category'),
),
migrations.AddField(
model_name='ticketcommentcategory',
name='runbook',
field=models.ForeignKey(blank=True, help_text='The runbook for this category', null=True, on_delete=django.db.models.deletion.SET_NULL, to='assistance.knowledgebase', verbose_name='Runbook'),
),
migrations.AddField(
model_name='ticketcomment',
name='category',
field=models.ForeignKey(blank=True, default=None, help_text='Category of the comment', null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.ticketcommentcategory', verbose_name='Category'),
),
migrations.AddField(
model_name='ticketlinkeditem',
name='organization',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]),
),
migrations.AddField(
model_name='ticketlinkeditem',
name='ticket',
field=models.ForeignKey(help_text='Ticket the item will be linked to', on_delete=django.db.models.deletion.CASCADE, to='core.ticket', verbose_name='Ticket'),
),
migrations.AlterUniqueTogether(
name='ticket',
unique_together={('external_system', 'external_ref')},
),
migrations.AlterUniqueTogether(
name='ticketcomment',
unique_together={('external_system', 'external_ref')},
),
]

View File

@ -8,6 +8,11 @@ from core.models.history import History
class SaveHistory(models.Model):
save_model_history: bool = True
"""When set, history will be saved.
By default, ALL models must save history.
"""
class Meta:
abstract = True
@ -25,6 +30,7 @@ class SaveHistory(models.Model):
"""
remove_keys = [
'_django_version',
'_state',
'created',
'modified'
@ -176,9 +182,11 @@ class SaveHistory(models.Model):
# Process the save
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
after = self.__dict__.copy()
if self.save_model_history:
self.save_history(before, after)
after = self.__dict__.copy()
self.save_history(before, after)
def delete_history(self, item_pk, item_class):

View File

View File

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,121 @@
from django.db import models
from access.fields import AutoCreatedField, AutoLastModifiedField
from access.models import TenancyObject, Team
from assistance.models.knowledge_base import KnowledgeBase
class TicketCategoryCommonFields(TenancyObject):
class Meta:
abstract = True
id = models.AutoField(
blank=False,
help_text = 'Category ID Number',
primary_key=True,
unique=True,
verbose_name = 'Number',
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
class TicketCategory(TicketCategoryCommonFields):
class Meta:
ordering = [
'parent__name',
'name',
]
verbose_name = "Ticket Category"
verbose_name_plural = "Ticket Categories"
parent = models.ForeignKey(
'self',
blank= True,
help_text = 'The Parent Category',
null = True,
on_delete = models.SET_NULL,
verbose_name = 'Parent Category',
)
name = models.CharField(
blank = False,
help_text = "Category Name",
max_length = 50,
verbose_name = 'Name',
)
runbook = models.ForeignKey(
KnowledgeBase,
blank= True,
help_text = 'The runbook for this category',
null = True,
on_delete = models.SET_NULL,
verbose_name = 'Runbook',
)
change = models.BooleanField(
blank = False,
default = True,
help_text = 'Use category for change tickets',
null = False,
verbose_name = 'Change Tickets',
)
incident = models.BooleanField(
blank = False,
default = True,
help_text = 'Use category for incident tickets',
null = False,
verbose_name = 'Incident Tickets',
)
problem = models.BooleanField(
blank = False,
default = True,
help_text = 'Use category for problem tickets',
null = False,
verbose_name = 'Problem Tickets',
)
project_task = models.BooleanField(
blank = False,
default = True,
help_text = 'Use category for Project tasks',
null = False,
verbose_name = 'Project Tasks',
)
request = models.BooleanField(
blank = False,
default = True,
help_text = 'Use category for request tickets',
null = False,
verbose_name = 'Request Tickets',
)
@property
def recusive_name(self):
if self.parent:
return str(self.parent.recusive_name + ' > ' + self.name )
return self.name
def __str__(self):
return self.recusive_name

View File

@ -0,0 +1,435 @@
from django.contrib.auth.models import User
from django.db import models
from django.forms import ValidationError
from access.fields import AutoCreatedField, AutoLastModifiedField
from access.models import TenancyObject, Team
from core.lib.slash_commands import SlashCommands
from .ticket import Ticket
from .ticket_comment_category import TicketCommentCategory
class TicketComment(
SlashCommands,
TenancyObject,
):
save_model_history: bool = False
class Meta:
ordering = [
'ticket',
'parent_id'
]
unique_together = ('external_system', 'external_ref',)
verbose_name = "Comment"
verbose_name_plural = "Comments"
class CommentSource(models.IntegerChoices):
"""Source of the comment"""
DIRECT = '1', 'Direct'
EMAIL = '2', 'E-Mail'
HELPDESK = '3', 'Helpdesk'
PHONE = '4', 'Phone'
class CommentStatus(models.IntegerChoices):
"""Comment Completion Status"""
TODO = '1', 'To Do'
DONE = '2', 'Done'
class CommentType(models.IntegerChoices):
"""
Comment types are as follows:
- Action
- Comment
- Solution
- Notification
## Action
An action comment is for the tracking of what has occured to the ticket.
## Comment
This is the default comment type and is what would be normally used.
## Solution
This type of comment is an ITSM comment and is used as the means for solving the ticket.\
## Notification
This type of comment is intended to be used to send a notification to subscribed users.
"""
ACTION = '1', 'Action'
COMMENT = '2', 'Comment'
TASK = '3', 'Task'
NOTIFICATION = '4', 'Notification'
SOLUTION = '5', 'Solution'
def validation_comment_type(field):
if not field:
raise ValidationError('Comment Type must be set')
def validation_ticket_id(field):
if not field:
raise ValidationError('Ticket ID is required')
model_notes = None
is_global = None
id = models.AutoField(
blank=False,
help_text = 'Comment ID Number',
primary_key=True,
unique=True,
verbose_name = 'Number',
)
parent = models.ForeignKey(
'self',
blank= True,
default = None,
help_text = 'Parent ID for creating discussion threads',
null = True,
on_delete = models.DO_NOTHING,
verbose_name = 'Parent Comment',
)
ticket = models.ForeignKey(
Ticket,
blank= True,
default = None,
help_text = 'Ticket this comment belongs to',
null = True,
on_delete = models.CASCADE,
validators = [ validation_ticket_id ],
verbose_name = 'Ticket',
)
external_ref = models.IntegerField(
blank = True,
default=None,
help_text = 'External System reference',
null=True,
verbose_name = 'Reference Number',
) # external reference or null. i.e. github issue number
external_system = models.IntegerField(
blank = True,
choices=Ticket.Ticket_ExternalSystem,
default=None,
help_text = 'External system this item derives',
null=True,
verbose_name = 'External System',
)
comment_type = models.IntegerField(
blank = False,
choices =CommentType,
default = CommentType.COMMENT,
help_text = 'The type of comment this is',
validators = [ validation_comment_type ],
verbose_name = 'Type',
)
body = models.TextField(
blank = False,
help_text = 'Comment contents',
null = False,
verbose_name = 'Comment',
)
created = AutoCreatedField(
editable = True,
)
modified = AutoLastModifiedField()
private = models.BooleanField(
blank = False,
default = False,
help_text = 'Is this comment private',
null = False,
verbose_name = 'Private',
)
duration = models.IntegerField(
blank = False,
default = 0,
help_text = 'Time spent in seconds',
null = False,
verbose_name = 'Duration',
)
category = models.ForeignKey(
TicketCommentCategory,
blank= True,
default = None,
help_text = 'Category of the comment',
null = True,
on_delete = models.SET_NULL,
verbose_name = 'Category',
)
template = models.ForeignKey(
'self',
blank= True,
default = None,
help_text = 'Comment Template to use',
null = True,
on_delete = models.SET_NULL,
related_name = 'comment_template',
verbose_name = 'Template',
)
is_template = models.BooleanField(
blank = False,
default = False,
help_text = 'Is this comment a template',
null = False,
verbose_name = 'Template',
)
source = models.IntegerField(
blank = False,
choices =CommentSource,
default = CommentSource.DIRECT,
help_text = 'Origin type for this comment',
# validators = [ validation_ticket_type ],
verbose_name = 'Source',
)
status = models.IntegerField( # will require validation by comment type as status for types will be different
blank = False,
choices=CommentStatus,
default = CommentStatus.TODO,
help_text = 'Status of comment',
# null=True,
verbose_name = 'Status',
)
responsible_user = models.ForeignKey(
User,
blank= True,
default = None,
help_text = 'User whom is responsible for the completion of comment',
on_delete = models.DO_NOTHING,
related_name = 'comment_responsible_user',
null = True,
verbose_name = 'Responsible User',
)
responsible_team = models.ForeignKey(
Team,
blank= True,
default = None,
help_text = 'Team whom is responsible for the completion of comment',
on_delete = models.DO_NOTHING,
related_name = 'comment_responsible_team',
null = True,
verbose_name = 'Responsible Team',
)
user = models.ForeignKey(
User,
blank= True,
help_text = 'Who made the comment',
null = True,
on_delete = models.DO_NOTHING,
related_name = 'comment_user',
verbose_name = 'User',
)
date_closed = models.DateTimeField(
blank = True,
help_text = 'Date ticket closed',
null = True,
verbose_name = 'Closed Date',
)
planned_start_date = models.DateTimeField(
blank = True,
help_text = 'Planned start date.',
null = True,
verbose_name = 'Planned Start Date',
)
planned_finish_date = models.DateTimeField(
blank = True,
help_text = 'Planned finish date',
null = True,
verbose_name = 'Planned Finish Date',
)
real_start_date = models.DateTimeField(
blank = True,
help_text = 'Real start date',
null = True,
verbose_name = 'Real Start Date',
)
real_finish_date = models.DateTimeField(
blank = True,
help_text = 'Real finish date',
null = True,
verbose_name = 'Real Finish Date',
)
common_fields: list(str()) = [
'body',
'duration',
'user',
'ticket',
'parent',
'comment_type',
]
common_itsm_fields: list(str()) = common_fields + [
'category',
'source',
'template',
]
fields_itsm_task: list(str()) = common_itsm_fields + [
'status',
'responsible_user',
'responsible_team',
'planned_start_date',
'planned_finish_date',
'real_start_date',
'real_finish_date',
]
fields_itsm_notification: list(str()) = common_itsm_fields + [
'status',
'responsible_user',
'responsible_team',
'planned_start_date',
'planned_finish_date',
'real_start_date',
'real_finish_date',
]
fields_itsm_incident: list(str()) = common_itsm_fields + [
]
fields_itsm_problem: list(str()) = common_itsm_fields + [
]
fields_itsm_change: list(str()) = common_itsm_fields + [
]
common_git_fields: list(str()) = common_fields + [
]
fields_git_issue: list(str()) = common_fields + [
]
fields_git_merge_request: list(str()) = common_fields + [
]
fields_project_task: list(str()) = common_fields + [
'category',
'urgency',
'status',
'impact',
'priority',
'planned_start_date',
'planned_finish_date',
'real_start_date',
'real_finish_date',
]
fields_comment_task: list(str()) = common_itsm_fields + [
'status',
'responsible_user',
'responsible_team',
'planned_start_date',
'planned_finish_date',
'real_start_date',
'real_finish_date',
]
@property
def action_comment(self):
return self.user.username + ' ' + self.body + ' on ' + str(self.created)
@property
def comment_template_queryset(self):
query = TicketComment.objects.filter(
is_template = True,
comment_type = self.comment_type,
)
return query
@property
def parent_object(self):
""" Fetch the parent object """
return self.ticket
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
self.organization = self.ticket.organization
self.body = self.slash_command(self.body)
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
if self.comment_type == self.CommentType.SOLUTION:
update_ticket = self.ticket.__class__.objects.get(pk=self.ticket.id)
update_ticket.status = int(Ticket.TicketStatus.All.SOLVED.value)
update_ticket.save()
@property
def threads(self):
return TicketComment.objects.filter(
parent = self.id
)

View File

@ -0,0 +1,103 @@
from django.db import models
from access.fields import AutoCreatedField, AutoLastModifiedField
from access.models import TenancyObject, Team
from assistance.models.knowledge_base import KnowledgeBase
class TicketCommentCategoryCommonFields(TenancyObject):
class Meta:
abstract = True
id = models.AutoField(
blank=False,
help_text = 'Category ID Number',
primary_key=True,
unique=True,
verbose_name = 'Number',
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
class TicketCommentCategory(TicketCommentCategoryCommonFields):
class Meta:
ordering = [
'name'
]
verbose_name = "Ticket Comment Category"
verbose_name_plural = "Ticket Comment Categories"
parent = models.ForeignKey(
'self',
blank= True,
help_text = 'The Parent Category',
null = True,
on_delete = models.SET_NULL,
verbose_name = 'Parent Category',
)
name = models.CharField(
blank = False,
help_text = "Category Name",
max_length = 50,
verbose_name = 'Name',
)
runbook = models.ForeignKey(
KnowledgeBase,
blank= True,
help_text = 'The runbook for this category',
null = True,
on_delete = models.SET_NULL,
verbose_name = 'Runbook',
)
comment = models.BooleanField(
blank = False,
default = True,
help_text = 'Use category for standard comment',
null = False,
verbose_name = 'Comment',
)
notification = models.BooleanField(
blank = False,
default = True,
help_text = 'Use category for notification comment',
null = False,
verbose_name = 'Notification Comment',
)
solution = models.BooleanField(
blank = False,
default = True,
help_text = 'Use category for solution comment',
null = False,
verbose_name = 'Solution Comment',
)
task = models.BooleanField(
blank = False,
default = True,
help_text = 'Use category for task comment',
null = False,
verbose_name = 'Task Comment',
)
def __str__(self):
return self.name

View File

@ -0,0 +1,126 @@
class TicketValues:
_DRAFT_INT = '1'
_NEW_INT = '2'
_ASSIGNED_INT = '3'
_CLOSED_INT = '4'
_INVALID_INT = '5'
#
# ITSM statuses
#
# Requests / Incidents / Problems / Changed
_ASSIGNED_PLANNING_INT = '6'
_PENDING_INT = '7'
# Requests / Incidents / Problems
_SOLVED_INT = '8'
# Problem
_OBSERVATION_INT = '9'
# Problems / Changes
_ACCEPTED_INT = '10'
# Changes
_EVALUATION_INT = '11'
_APPROVALS_INT = '12'
_TESTING_INT = '13'
_QUALIFICATION_INT = '14'
_APPLIED_INT = '15'
_REVIEW_INT = '16'
_CANCELLED_INT = '17'
_REFUSED_INT = '18'
_DRAFT_STR = 'Draft'
_NEW_STR = 'New'
_ASSIGNED_STR = 'Assigned'
_CLOSED_STR = 'Closed'
_INVALID_STR = 'Invalid'
#
# ITSM statuses
#
# Requests / Incidents / Problems / Changed
_ASSIGNED_PLANNING_STR = 'Assigned (Planning)'
_PENDING_STR = 'Pending'
# Requests / Incidents / Problems
_SOLVED_STR = 'Solved'
# Problem
_OBSERVATION_STR = 'Under Observation'
# Problems / Changes
_ACCEPTED_STR = 'Accepted'
# Changes
_EVALUATION_STR = 'Evaluation'
_APPROVALS_STR = 'Approvals'
_TESTING_STR = 'Testing'
_QUALIFICATION_STR = 'Qualification'
_APPLIED_STR = 'Applied'
_REVIEW_STR = 'Review'
_CANCELLED_STR = 'Cancelled'
_REFUSED_STR = 'Refused'
class ExternalSystem:
_GITHUB_INT = '1'
_GITHUB_VALUE = 'Github'
_GITLAB_INT = '2'
_GITLAB_VALUE = 'Gitlab'
_CUSTOM_1_INT = '9999'
_CUSTOM_1_VALUE = 'Custom #1 (Imported)'
_CUSTOM_2_INT = '9998'
_CUSTOM_2_VALUE = 'Custom #2 (Imported)'
_CUSTOM_3_INT = '9997'
_CUSTOM_3_VALUE = 'Custom #3 (Imported)'
_CUSTOM_4_INT = '9996'
_CUSTOM_4_VALUE = 'Custom #4 (Imported)'
_CUSTOM_5_INT = '9995'
_CUSTOM_5_VALUE = 'Custom #5 (Imported)'
_CUSTOM_6_INT = '9994'
_CUSTOM_6_VALUE = 'Custom #6 (Imported)'
_CUSTOM_7_INT = '9993'
_CUSTOM_7_VALUE = 'Custom #7 (Imported)'
_CUSTOM_8_INT = '9992'
_CUSTOM_8_VALUE = 'Custom #8 (Imported)'
_CUSTOM_9_INT = '9991'
_CUSTOM_9_VALUE = 'Custom #9 (Imported)'
class Priority:
_VERY_LOW_INT = '1'
_VERY_LOW_VALUE = 'Very Low'
_LOW_INT = '2'
_LOW_VALUE = 'Low'
_MEDIUM_INT = '3'
_MEDIUM_VALUE = 'Medium'
_HIGH_INT = '4'
_HIGH_VALUE = 'High'
_VERY_HIGH_INT = '5'
_VERY_HIGH_VALUE = 'Very High'
_MAJOR_INT = '6'
_MAJOR_VALUE = 'Major'

View File

@ -0,0 +1,142 @@
from django.db import models
from .ticket_enum_values import TicketValues
from access.models import TenancyObject
from core.middleware.get_request import get_request
from core.models.ticket.ticket import Ticket
class TicketLinkedItem(TenancyObject):
class Meta:
ordering = [
'id'
]
verbose_name = 'Ticket Linked Item'
verbose_name_plural = 'Ticket linked Items'
class Modules(models.IntegerChoices):
CLUSTER = 1, 'Cluster'
CONFIG_GROUP = 2, 'Config Group'
DEVICE = 3, 'Device'
OPERATING_SYSTEM = 4, 'Operating System'
SERVICE = 5, 'Service'
SOFTWARE = 6, 'Software'
is_global = None
model_notes = None
id = models.AutoField(
blank=False,
help_text = 'ID Number',
primary_key=True,
unique=True,
verbose_name = 'Number',
)
ticket = models.ForeignKey(
Ticket,
blank= False,
help_text = 'Ticket the item will be linked to',
null = False,
on_delete = models.CASCADE,
verbose_name = 'Ticket',
)
item_type = models.IntegerField(
blank= False,
choices = Modules,
help_text = 'Python Model location for linked item',
null = False,
verbose_name = 'Item Type',
)
item = models.IntegerField(
blank = False,
help_text = 'Item ID to link to ticket',
null = False,
verbose_name = 'Item ID',
)
def __str__(self) -> str:
item_type: str = None
if self.item_type == TicketLinkedItem.Modules.CLUSTER:
item_type = 'cluster'
elif self.item_type == TicketLinkedItem.Modules.CONFIG_GROUP:
item_type = 'config_group'
elif self.item_type == TicketLinkedItem.Modules.DEVICE:
item_type = 'device'
elif self.item_type == TicketLinkedItem.Modules.OPERATING_SYSTEM:
item_type = 'operating_system'
elif self.item_type == TicketLinkedItem.Modules.SERVICE:
item_type = 'service'
elif self.item_type == TicketLinkedItem.Modules.SOFTWARE:
item_type = 'software'
if item_type:
return f'${item_type}-{int(self.item)}'
return str(self.item)
@property
def parent_object(self):
""" Fetch the parent object """
return self.ticket
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
request = get_request()
if request:
if request.user.pk:
comment_user = request.user
else:
comment_user = None
else:
comment_user = None
from core.models.ticket.ticket_comment import TicketComment
comment = TicketComment.objects.create(
ticket = self.ticket,
comment_type = TicketComment.CommentType.ACTION,
body = f'linked {self}',
source = TicketComment.CommentSource.DIRECT,
user = comment_user,
)
comment.save()

View File

@ -0,0 +1,47 @@
{% extends 'base.html.j2' %}
{% block content %}
<input type="button" value="<< Back to settings" onclick="window.location='{% url 'Settings:Settings' %}';">
<input type="button" value="New Category" onclick="window.location='{% url 'Settings:_ticket_category_add' %}';">
<table style="max-width: 100%;">
<thead>
<th>Name</th>
<th>Organization</th>
<th>created</th>
<th>modified</th>
<th>&nbsp;</th>
</thead>
{% if items %}
{% for category in items %}
<tr>
<td><a href="{% url 'Settings:_ticket_category_view' pk=category.id %}">{{ category.name }}</a></td>
<td>{{ category.organization }}</td>
<td>{{ category.created }}</td>
<td>{{ category.modified }}</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
{% else %}
<tr><td colspan="5">Nothing Found</td></tr>
{% endif%}
</table>
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page=1">&laquo; first</a>
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
<a href="?page={{ page_obj.paginator.num_pages }}">last &raquo;</a>
{% endif %}
</span>
</div>
{% endblock %}

View File

@ -0,0 +1,47 @@
{% extends 'base.html.j2' %}
{% block content %}
<input type="button" value="<< Back to settings" onclick="window.location='{% url 'Settings:Settings' %}';">
<input type="button" value="New Category" onclick="window.location='{% url 'Settings:_ticket_comment_category_add' %}';">
<table style="max-width: 100%;">
<thead>
<th>Name</th>
<th>Organization</th>
<th>created</th>
<th>modified</th>
<th>&nbsp;</th>
</thead>
{% if items %}
{% for category in items %}
<tr>
<td><a href="{% url 'Settings:_ticket_comment_category_view' pk=category.id %}">{{ category.name }}</a></td>
<td>{{ category.organization }}</td>
<td>{{ category.created }}</td>
<td>{{ category.modified }}</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
{% else %}
<tr><td colspan="5">Nothing Found</td></tr>
{% endif%}
</table>
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page=1">&laquo; first</a>
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
<a href="?page={{ page_obj.paginator.num_pages }}">last &raquo;</a>
{% endif %}
</span>
</div>
{% endblock %}

View File

@ -0,0 +1,163 @@
{% extends 'base.html.j2' %}
{% block additional-stylesheet %}
{% load static %}
<link rel="stylesheet" href="{% static 'ticketing.css' %}">
{% endblock additional-stylesheet %}
{% load markdown %}
{% block article %}
<div id="ticket-content">
<div id="ticket-data">
<div id="ticket-description">
<h3 class="{{ ticket_type }}-ticket">
<span class="sub-script">opened by</span> {{ ticket.opened_by }} <span class="sub-script">on</span> {{ ticket.created }}
{% if ticket.created|date_time_seconds != ticket.modified|date_time_seconds %}
<span class="sub-script">Updated</span> {{ ticket.modified }}
{% endif %}
</h3>
<div>
<input style="float: right; position: relative; margin: 0px;" type="button" value="Edit" onclick="window.location='{{ edit_url }}';">
</div>
<div id="markdown">{{ ticket.description | markdown | safe }}</div>
</div>
<div id="ticket-additional-data">
<div id="data-block">
<h3>
<div id="text">Related Tickets</div>
<div id="icons">
<a href="{% url '_ticket_related_add' ticket_type=ticket_type ticket_id=ticket.id %}">{% include 'icons/ticket/add.svg' %}</a>
</div>
</h3>
{% if ticket.related_tickets %}
{% for related_ticket in ticket.related_tickets %}
<div id="linked-tickets" class="{{ related_ticket.type }}-ticket">
<div class="icon icon-{{ related_ticket.how_related }}">{% include related_ticket.icon_filename %}</div>
<div id="markdown" class="ticket">
{% if related_ticket.how_related == 'blocked_by' %}
Blocked by
{% elif related_ticket.how_related == 'blocks' %}
Blocks
{% elif related_ticket.how_related == 'related' %}
Related to
{% endif %}
<span style="display: inline-block;">{{ related_ticket.markdown | markdown | safe }}</span>
</div>
</div>
{% endfor %}
{% else %}
<div>Nothing Found</div>
{% endif %}
</div>
<div id="data-block" class="linked-item">
<h3>
<div id="text">Linked Items</div>
<div id="icons">
<a href="{% url '_ticket_linked_item_add' ticket_type=ticket_type ticket_id=ticket.id %}">{% include 'icons/ticket/add.svg' %}</a>
</div>
</h3>
{% if ticket.linked_items %}
{% for linked_item in ticket.linked_items %}
<div id="item">{{ linked_item | markdown | safe }}</div>
{% endfor %}
{% else %}
<div style="text-align:center;%">Nothing found</div>
{% endif%}
</div>
</div>
{% include 'core/ticket/comment.html.j2' %}
</div>
<div id="ticket-meta">
<h3 class="{{ ticket_type }}-ticket">
{{ ticket_type }} <span style="font-size: smaller;">#{{ ticket.id }}
{% if ticket.external_ref %}
<span style="display: inline-block;" title="External system {{ ticket.get_external_system_display }}">(#{{ ticket.external_ref }})</span>
{% endif %}</span>
</h3>
<fieldset>
<label>Assigned</label>
<span class="text">
{% if ticket.assigned_users %}
{% for user in ticket.assigned_users.all %}
{{ user }}
{% endfor%}
{% endif %}
{% if ticket.assigned_teams %}
{% for team in ticket.assigned_teams.all %}
{{ team }}
{% endfor%}
{% endif %}
</span>
</fieldset>
<fieldset>
<label>Status</label>
<span>{% include 'core/ticket/badge_ticket_status.html.j2' with ticket_status_text=ticket.get_status_display ticket_status=ticket.get_status_display|ticket_status %}</span>
</fieldset>
<fieldset>
<label>Labels</label>
<span class="text">val</span>
</fieldset>
{% if ticket.category %}
<fieldset>
<label>Category</label>
<span class="text">
<a href="{% url 'Settings:_ticket_category_view' pk=ticket.category.id %}">
{{ ticket.category }}
</a>
</span>
</fieldset>
{% endif %}
{% if ticket.project %}
<fieldset>
<label>Project</label>
<span class="text">
<a href="{% url 'Project Management:_project_view' pk=ticket.project_id %}">{{ ticket.project }}</a>
</span>
</fieldset>
{% endif %}
{% if ticket.milestone %}
<fieldset>
<label>Milestone</label>
<span class="text">
<a href="{% url 'Project Management:_project_milestone_view' project_id=ticket.project_id pk=ticket.milestone.id %}">{{ ticket.milestone }}</a>
</span>
</fieldset>
{% endif %}
<fieldset>
<label>Priority</label>
<span class="text">U{{ ticket.get_urgency_display }} / I{{ ticket.get_impact_display }} / P{{ ticket.get_priority_display }}</span>
</fieldset>
<fieldset>
<label>Duration</label>
<span class="text">{{ ticket.duration_ticket|to_duration }}</span>
</fieldset>
<fieldset>
<label>Milestone</label>
<span class="text">val</span>
</fieldset>
<fieldset>
<label>Roadmap(s)</label>
<span class="text">val</span>
</fieldset>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,6 @@
<span id="badge">
<span id="icon" class="ticket-status ticket-status-{{ ticket_status }}">
{% include 'core/ticket/icon_status.html.j2' %}
</span>
<span id="text">{{ ticket_status_text }}</span>
</span>

View File

@ -0,0 +1,89 @@
{% load i18n %}
{% if ticket_type == 'change'%}
{% translate 'ITIM:_ticket_comment_change_reply_add' as comment_reply_url %}
{% elif ticket_type == 'incident'%}
{% translate 'ITIM:_ticket_comment_incident_reply_add' as comment_reply_url %}
{% elif ticket_type == 'project_task'%}
{% translate 'Project Management:_project_task_comment_reply_add' as comment_reply_url %}
{% elif ticket_type == 'problem'%}
{% translate 'ITIM:_ticket_comment_problem_reply_add' as comment_reply_url %}
{% elif ticket_type == 'request'%}
{% translate 'Assistance:_ticket_comment_request_reply_add' as comment_reply_url %}
{% endif %}
<div id="ticket-comments">
<ul>
{% for comment in ticket.comments %}
<li {% if comment.get_comment_type_display == 'Action' %}id="{{ comment.get_comment_type_display }}"{% endif %}>
{% include 'core/ticket/comment/comment.html.j2' %}
{% if comment.threads %}
<div id="discussion" style="/*background-color: #fff;*/">
<h4 style="display: flex; padding-left: 5px;">
Replies
{% include 'icons/ticket/expanded.svg' %}
</h4>
</div>
<div style="padding-left: 40px; border-left: 1px solid #177fe66e; border-bottom: 1px solid #177fe66e;">
{% if comment.threads %}
{% for thread in comment.threads %}
{% include 'core/ticket/comment/comment.html.j2' with comment=thread %}
{% endfor %}
{% endif %}
<div >
<div style="padding: 10px; padding-top: 10px">
{% if ticket_type == 'project_task'%}
<input type="button" value="Comment" onclick="window.location='{% url comment_reply_url ticket.project.id ticket_type ticket.id comment.id %}?comment_type=comment';">
<input type="button" value="Task" onclick="window.location='{% url comment_reply_url ticket.project.id ticket_type ticket.id comment.id %}?comment_type=task';">
{% else %}
<input type="button" value="Comment" onclick="window.location='{% url comment_reply_url ticket_type ticket.id comment.id %}?comment_type=comment';">
<input type="button" value="Task" onclick="window.location='{% url comment_reply_url ticket_type ticket.id comment.id %}?comment_type=task';">
{% endif %}
</div>
</div>
{% endif %}
</li>
{% endfor %}
</ul>
<div id="comment" style="padding: 20px;">
{% if ticket_type == 'change'%}
{% translate 'ITIM:_ticket_comment_change_add' as comment_url %}
{% elif ticket_type == 'incident'%}
{% translate 'ITIM:_ticket_comment_incident_add' as comment_url %}
{% elif ticket_type == 'project_task'%}
{% translate 'Project Management:_project_task_comment_add' as comment_url %}
{% elif ticket_type == 'problem'%}
{% translate 'ITIM:_ticket_comment_problem_add' as comment_url %}
{% elif ticket_type == 'request'%}
{% translate 'Assistance:_ticket_comment_request_add' as comment_url %}
{% endif %}
{% if ticket_type == 'project_task'%}
<input type="button" value="Comment" onclick="window.location='{% url comment_url ticket.project.id ticket_type ticket.id%}?comment_type=comment';">
<input type="button" value="Task" onclick="window.location='{% url comment_url ticket.project.id ticket_type ticket.id %}?comment_type=task';">
<input type="button" value="Notification" onclick="window.location='{% url comment_url ticket.project.id ticket_type ticket.id %}?comment_type=notification';">
<input type="button" value="Resolve" onclick="window.location='{% url comment_url ticket.project.id ticket_type ticket.id %}?comment_type=solution';">
{% else %}
<input type="button" value="Comment" onclick="window.location='{% url comment_url ticket_type ticket.id%}?comment_type=comment';">
<input type="button" value="Task" onclick="window.location='{% url comment_url ticket_type ticket.id %}?comment_type=task';">
<input type="button" value="Notification" onclick="window.location='{% url comment_url ticket_type ticket.id %}?comment_type=notification';">
<input type="button" value="Resolve" onclick="window.location='{% url comment_url ticket_type ticket.id %}?comment_type=solution';">
{% endif %}
</div>
</div>

View File

@ -0,0 +1,132 @@
{% if comment %}
{% load markdown %}
{% if comment.get_comment_type_display == 'Action' %}
<div style="padding: 0px; margin: 0px;"><span style="display: inline-block;">{{ comment.user.username }}&nbsp;</span><span style="display: inline-block;">{{ comment.body | markdown | safe }}</span></div>
{% elif comment.get_comment_type_display == 'Comment' or comment.get_comment_type_display == 'Task' or comment.get_comment_type_display == 'Notification' or comment.get_comment_type_display == 'Solution' %}
<div id="comment" class="comment-type-default comment-type-{{ comment.get_comment_type_display }}">
<h4>
<div id="text">
{{ comment.user }}
<span class="sub-script">{% if comment.get_comment_type_display == 'Task' %}
created a task
{% elif comment.get_comment_type_display == 'Solution' %}
solved
{% else %}
wrote
{% endif %}
on</span> {{ comment.created }}
{% if comment.created|date_time_seconds != comment.modified|date_time_seconds %}
<span class="sub-script">Updated</span> {{ comment.modified }}
{% endif %}
</div>
<div id="icons">
{%if not comment.parent_id %}
{% if ticket_type == 'project_task'%}
<a title="Reply with Comment" href="{% url comment_reply_url ticket.project.id ticket_type ticket.id comment.id %}?comment_type=comment">
{% include 'icons/ticket/reply.svg' %}
</a>
<a title="Reply with Task" href="{% url comment_reply_url ticket.project.id ticket_type ticket.id comment.id %}?comment_type=task">
{% include 'icons/ticket/task.svg' %}
</a>
<a title="Reply with Notification" href="{% url comment_reply_url ticket.project.id ticket_type ticket.id comment.id %}?comment_type=notification">
{% include 'icons/ticket/notification.svg' %}
</a>
{% else %}
<a title="Reply with Comment" href="{% url comment_reply_url ticket_type ticket.id comment.id %}?comment_type=comment">
{% include 'icons/ticket/reply.svg' %}
</a>
<a title="Reply with Task" href="{% url comment_reply_url ticket_type ticket.id comment.id %}?comment_type=task">
{% include 'icons/ticket/task.svg' %}
</a>
<a title="Reply with Notification" href="{% url comment_reply_url ticket_type ticket.id comment.id %}?comment_type=notification">
{% include 'icons/ticket/notification.svg' %}
</a>
{% endif %}
{% endif %}
<a title="Edit Comment" href="{% url 'Assistance:_ticket_comment_request_change' ticket_type ticket.id comment.id %}">
{% include 'icons/ticket/edit.svg' %}
</a>
</div>
</h4>
<div style="line-height:30px;">
{% if comment.get_comment_type_display != 'Notification' %}
<fieldset>
<label>Source</label>
<span>{{ comment.get_source_display }}</span>
</fieldset>
{% endif %}
{% if comment.get_comment_type_display == 'Task' or comment.get_comment_type_display == 'Notification' %}
<fieldset>
<label>Status</label>
<span>{{ comment.get_status_display }}</span>
</fieldset>
{% if comment.get_comment_type_display == 'Task' %}
<fieldset>
<label>Responsible User</label>
<span>{{ comment.responsible_user }}</span>
</fieldset>
{% endif %}
<fieldset>
<label>
{% if comment.get_comment_type_display == 'Task' %}
Responsible Team
{% elif comment.get_comment_type_display == 'Notification' %}
Notify Team
{% endif %}
</label>
<span>{{ comment.responsible_team }}</span>
</fieldset>
{% endif %}
<fieldset>
<label>Category</label>
<span>{{ comment.category }}</span>
</fieldset>
</div>
<hr />
<div id="markdown" style="margin: 15px; padding: 10px; background-color: #fff;">
{{ comment.body | markdown | safe }}
</div>
<hr />
<div>
{% if comment.get_comment_type_display == 'Task' or comment.get_comment_type_display == 'Notification' %}
<fieldset>
<label>Planned Start</label>
<span>{{ comment.planned_start_date }}</span>
</fieldset>
{% if comment.get_comment_type_display == 'Task' %}
<fieldset>
<label>Planned Finish</label>
<span>{{ comment.planned_finish_date }}</span>
</fieldset>
{% endif %}
<fieldset>
<label>Actual Start</label>
<span>{{ comment.real_start_date }}</span>
</fieldset>
<fieldset>
<label>Actual Finish</label>
<span>{{ comment.real_finish_date }}</span>
</fieldset>
{% endif %}
<fieldset>
<label>Duration</label>
<span>{{ comment.duration|to_duration }}</span>
</fieldset>
</div>
</div>
{% endif %}
{% endif %}

View File

@ -0,0 +1,25 @@
{% if ticket_status == 'new' %}
{% include 'icons/ticket/add.svg' %}
{% elif ticket_status == 'assigned' %}
{% include 'icons/ticket/status_assigned.svg' %}
{% elif ticket_status == 'assigned_planning' %}
{% include 'icons/ticket/status_assigned_planning.svg' %}
{% elif ticket_status == 'closed' %}
{% include 'icons/ticket/status_closed.svg' %}
{% elif ticket_status == 'draft' %}
{% include 'icons/ticket/add.svg' %}
{% elif ticket_status == 'pending' %}
{% include 'icons/ticket/status_pending.svg' %}
{% elif ticket_status == 'solved' %}
{% include 'icons/ticket/status_solved.svg' %}
{% elif ticket_status == 'invalid' %}
{% include 'icons/ticket/status_invalid.svg' %}
{% elif ticket_status == 'approvals' %}
{% include 'icons/ticket/status_approvals.svg' %}
{% elif ticket_status == 'accepted' %}
{% include 'icons/ticket/status_accepted.svg' %}
{% elif ticket_status == 'evaluation' %}
{% include 'icons/ticket/status_evaluation.svg' %}
{% elif ticket_status == 'testing' %}
{% include 'icons/ticket/status_testing.svg' %}
{% endif %}

View File

@ -0,0 +1,67 @@
{% extends 'base.html.j2' %}
{% block additional-stylesheet %}
{% load static %}
<link rel="stylesheet" href="{% static 'ticketing.css' %}">
{% endblock additional-stylesheet %}
{% load markdown %}
{% block content %}
<input type="button" value="New Ticket" onclick="window.location='{{ new_ticket_url }}';">
<style>
#status-icon {
margin: 0px;
}
#status-icon svg{
width: 22px;
}
</style>
<table style="max-width: 100%;">
<thead>
<th>&nbsp;</th>
<th>ID</th>
<th>Title</th>
<th>Status</th>
<th>Opened By</th>
<th>organization</th>
<th>Created</th>
</thead>
{% for ticket in tickets %}
<tr class="clicker">
<td id="status-icon">
&nbsp;
</td>
<td>{{ ticket.id }}</td>
<td>
{% if ticket_type == 'change' %}
<a href="{% url 'ITIM:_ticket_change_view' ticket_type='change' pk=ticket.id %}">
{% elif ticket_type == 'incident' %}
<a href="{% url 'ITIM:_ticket_incident_view' ticket_type='incident' pk=ticket.id %}">
{% elif ticket_type == 'problem' %}
<a href="{% url 'ITIM:_ticket_problem_view' ticket_type='problem' pk=ticket.id %}">
{% elif ticket_type == 'request' %}
<a href="{% url 'Assistance:_ticket_request_view' ticket_type='request' pk=ticket.id %}">
{% else %}
<a href=""></a>
{% endif %}
{{ ticket.title }}
</a>
</td>
<td>{% include 'core/ticket/badge_ticket_status.html.j2' with ticket_status_text=ticket.get_status_display ticket_status=ticket.get_status_display|ticket_status %}</td>
<td>{{ ticket.opened_by }}</td>
<td>{{ ticket.organization.name }}</td>
<td>{{ ticket.created }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@ -0,0 +1,17 @@
<span id="rendered-ticket-link">
<span id="icon" class="ticket-status ticket-status-{{ ticket_status }}">{% include 'core/ticket/icon_status.html.j2' %}</span>
<span id="text">
<a href=" {% if ticket_type == 'change' %}
{% url 'ITIM:_ticket_change_view' ticket_type='change' pk=id %}
{% elif ticket_type == 'incident' %}
{% url 'ITIM:_ticket_incident_view' ticket_type='incident' pk=id %}
{% elif ticket_type == 'problem' %}
{% url 'ITIM:_ticket_problem_view' ticket_type='problem' pk=id %}
{% elif ticket_type == 'request' %}
{% url 'Assistance:_ticket_request_view' ticket_type='request' pk=id %}
{% elif ticket_type == 'project task' %}
{% url 'Project Management:_project_task_view' project_id=project_id ticket_type='project_task' pk=id %}
{% endif %}"><span style="color: #777;">#{{ id }}</span> {{ name }}, <span style="color: #777;">{{ ticket_type }}</span></a>
</span>
</span>

View File

@ -0,0 +1,8 @@
<span id="ticket-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" class="{{ ticket_type }}-ticket">
<path d="M319.33-246.67h321.34v-66.66H319.33v66.66Zm0-166.66h321.34V-480H319.33v66.67ZM226.67-80q-27 0-46.84-19.83Q160-119.67 160-146.67v-666.66q0-27 19.83-46.84Q199.67-880 226.67-880H574l226 226v507.33q0 27-19.83 46.84Q760.33-80 733.33-80H226.67Zm314-542.67v-190.66h-314v666.66h506.66v-476H540.67Zm-314-190.66v190.66-190.66 666.66-666.66Z"/>
</svg>
</span>

View File

@ -0,0 +1,12 @@
{% extends 'detail.html.j2' %}
{% block tabs %}
<div id="details" class="content-tab">
{% include 'content/section.html.j2' with tab=form.tabs.details %}
</div>
{% endblock %}

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