Compare commits
346 Commits
Author | SHA1 | Date | |
---|---|---|---|
ca3b99cb3a | |||
8d4f686f6c | |||
6f57bd84e7 | |||
6351acabe5 | |||
9d6ea1d7c3 | |||
035c6ed60c | |||
31e88b2f96 | |||
28f51d3bb6 | |||
c634695e4e | |||
983921fc22 | |||
9fc5b0eb09 | |||
b1fc8e0f98 | |||
551473feb7 | |||
105cb63d71 | |||
1dda4a9fb5 | |||
c53ec9ec5d | |||
a44b2479e3 | |||
ec26e16132 | |||
59a930f934 | |||
c7701bb2df | |||
4ac0da6ba2 | |||
19ad262617 | |||
0e987088a3 | |||
c74b89e0d6 | |||
e762713416 | |||
facdd0111b | |||
280abb8841 | |||
064f74736f | |||
d26868eced | |||
b5ec42fc56 | |||
104575780a | |||
1c1f4ecdfa | |||
582ee4031d | |||
fca8ad5a78 | |||
fff3a96889 | |||
dfdc5bac9d | |||
c022551427 | |||
76954c019b | |||
c3de79050e | |||
3cce436938 | |||
ed2bf96626 | |||
3693ddadad | |||
8866f94fab | |||
0ae1395d92 | |||
cf73323dd3 | |||
f019c50e44 | |||
3e56c9861c | |||
d6f475a009 | |||
b2f20766de | |||
81e98cfd6f | |||
8cae85badc | |||
d6e0fd0b46 | |||
8d7d79c29b | |||
92d3692f85 | |||
7f094a9f96 | |||
7c9d6ced6a | |||
9b747b08da | |||
f4695bad1e | |||
744f2f380f | |||
22c6b9d3fe | |||
c1aeb3a258 | |||
0eb6a8bde9 | |||
477f089de3 | |||
64faffa7b6 | |||
2196db9479 | |||
14d949228d | |||
f8e96a556d | |||
5ad974f947 | |||
9c9009ff52 | |||
bd09412f6f | |||
82d48fe27b | |||
1315cc584b | |||
17bed1ef7a | |||
f0dd7bc256 | |||
5b356c3c11 | |||
fbbae64ff2 | |||
34b85441e2 | |||
e9b122cf8c | |||
3f0853654e | |||
0de451af70 | |||
a4926f8a0d | |||
1314f9a1ff | |||
d5e344f67c | |||
d8654fae6d | |||
033f47b6b9 | |||
383bca4ff9 | |||
22f7b1e7c5 | |||
d5ad03546c | |||
f79076ddef | |||
7a31498e91 | |||
95f9d90877 | |||
3bac0c19ac | |||
b8f4123185 | |||
d2e9c838de | |||
5cd51ba00e | |||
a3fafdafbd | |||
df9ad069c4 | |||
2ff3dab014 | |||
ae9526ef57 | |||
5e235617e0 | |||
a373247cda | |||
c81e319aac | |||
d05537a619 | |||
7894ac5522 | |||
64577cf806 | |||
c3307152e8 | |||
a7e99eb5b4 | |||
c45aae7048 | |||
9cb3afeb30 | |||
6e7e6587c2 | |||
5f3c7296b7 | |||
2e15e61059 | |||
574357b60a | |||
1576605acb | |||
9d564ffbb2 | |||
b56f3236fd | |||
6e566b8840 | |||
34a1a19089 | |||
80dc797651 | |||
f2a4223d25 | |||
902aaf31dd | |||
1be23148d7 | |||
88d6a73454 | |||
01c57b37ad | |||
9fbb88fa5f | |||
a0b0d79777 | |||
f3dccd3b84 | |||
56b715797e | |||
11948c9500 | |||
1161bf79aa | |||
40f564b32a | |||
4fdabc16ba | |||
e68dbdfb4c | |||
f2898037b0 | |||
5d116c7224 | |||
2a31815267 | |||
ded6a72072 | |||
7d80857d8d | |||
297e318243 | |||
6cc992f6d6 | |||
6402897329 | |||
5f7d0e474e | |||
09bb2d8e27 | |||
e28dbea05b | |||
9d8c894cff | |||
9942348ba3 | |||
2e98eda8a4 | |||
2ab2b65fc2 | |||
3e684b117f | |||
51f28a6cf8 | |||
212e864db1 | |||
0adfd95ced | |||
c9d05152c9 | |||
a8b21d7c74 | |||
97874b73f6 | |||
948713d13d | |||
63146aa41c | |||
c7f69ad7c1 | |||
008f8c1554 | |||
122216dbe4 | |||
c0ac09b928 | |||
cfda7e5e1e | |||
91af43adba | |||
bfb7176db3 | |||
411cd5d4a3 | |||
91aa87d122 | |||
14bdc67a4a | |||
b86b1fd1ad | |||
00ec5179f9 | |||
200c9d8d8d | |||
b69d210759 | |||
eb4a58ed01 | |||
bc2f30ac9b | |||
e87bbe9ed8 | |||
68785ef6c0 | |||
b78e2adb09 | |||
26c985e683 | |||
58e2b9f7f5 | |||
34f2d4c4d4 | |||
56c3b9d7de | |||
c83ffe542e | |||
b07872c8c2 | |||
1cc196fd06 | |||
dd68bfbea8 | |||
ea5888f39f | |||
78607a0bf9 | |||
fa9cff390a | |||
2613a132a6 | |||
7d8b54a980 | |||
6371fa03a1 | |||
daa872d2e7 | |||
4d1600e396 | |||
63d33c287c | |||
ae72d4ab6a | |||
8a747d1d1f | |||
55e512efb8 | |||
f09e7b77db | |||
4177f71972 | |||
0c3e38c543 | |||
94dd555e9b | |||
10bffe0f0f | |||
aa6baf94a6 | |||
44604d98ab | |||
63077dfa26 | |||
0794e5b58f | |||
c67e1430bd | |||
f3b249d18f | |||
857b8781cb | |||
118d41a53b | |||
2cb21ae4a7 | |||
7e0bd630b5 | |||
a57e977131 | |||
082a351c17 | |||
a47e1977f0 | |||
b0a4d2ca84 | |||
afceaca736 | |||
c59dc7d2bf | |||
e7015570d5 | |||
a68a9e7ef3 | |||
3ea84f008b | |||
47aeac846b | |||
69124cff08 | |||
a99c1bb418 | |||
b80ca93ced | |||
8998292a0f | |||
bc39b1b8b5 | |||
b93d3d2175 | |||
b1277c98ab | |||
c2eaf120b6 | |||
41158e495f | |||
3261342c4f | |||
8b4068ac7e | |||
27958f5e7a | |||
819dc01451 | |||
8277e05205 | |||
f4d96c78e7 | |||
a08d74cd3c | |||
878d2509cd | |||
09247246bb | |||
685b8266e4 | |||
7a2f7fdf3d | |||
ecaa24192f | |||
f49cc9c286 | |||
53ae19eda8 | |||
d8361bf741 | |||
d70f04c63d | |||
0f4b9fef9e | |||
05c18702ff | |||
754c311580 | |||
f6dd5a3156 | |||
53489ec43b | |||
da8d97a274 | |||
8161d67a1f | |||
381d59c18f | |||
55a40fcf4d | |||
cfc690f1c2 | |||
a3bfa921e8 | |||
c670f017a0 | |||
f70c5a28af | |||
c3d64a031d | |||
eb94729277 | |||
c339f17c5c | |||
d7dd2d6d8b | |||
342fe7da9e | |||
910a002201 | |||
5f6c36e823 | |||
cf577bbb4f | |||
b8253ae9ba | |||
978bcf3b45 | |||
f76f81a312 | |||
5793295e1a | |||
6df22314c9 | |||
d7c3e051de | |||
057a39091d | |||
ba8b618b7d | |||
0b86ded4f5 | |||
523341cf4a | |||
ee17095a14 | |||
7829f4b7d8 | |||
058e057088 | |||
c8d7b52fbf | |||
9732656556 | |||
967b9251e2 | |||
3ba89a926b | |||
b04b6fe645 | |||
1829395a8a | |||
6f2d431ae1 | |||
9132608aaf | |||
f0b604b5dc | |||
d1b9283a9a | |||
fe353904d8 | |||
011a6c156e | |||
8662feb1c7 | |||
097b3fe8b6 | |||
7f138d4b68 | |||
6532d0e0d7 | |||
8242d9f269 | |||
28fe89e048 | |||
b709839c38 | |||
638ea466f0 | |||
31bc1e4e76 | |||
0535674a96 | |||
5f3b12a472 | |||
6ec16cbeb0 | |||
1665e519a4 | |||
95979c6095 | |||
8b004466d1 | |||
3f1f2fd8d4 | |||
96ed198efc | |||
6a52730b49 | |||
5c4a802017 | |||
e59a08b351 | |||
2a7857b60d | |||
09afd7f165 | |||
e63bec83e8 | |||
5d74ddfee5 | |||
3c44561b19 | |||
8edb209d16 | |||
81bd635ca4 | |||
6ff3fe5949 | |||
31067aab95 | |||
7b3a007862 | |||
c5a5c393a8 | |||
52db44eac7 | |||
cb9c782d0c | |||
b8c4a540fa | |||
91d85d93c7 | |||
73ca0feb55 | |||
64fd8b5686 | |||
58a9532c70 | |||
3c206f5aef | |||
9bc4f186d5 | |||
f883d4190a | |||
c6fc2d3e7c | |||
e4d1bb4d3c | |||
376faf3d5a | |||
6c0ca9cb86 | |||
326753d0ff | |||
47f95ddae2 | |||
86fc1448a6 | |||
a91ae337c4 | |||
58466fa490 | |||
de78a30a5d | |||
f7d61696d1 | |||
e35ccd360b | |||
5f3a778002 |
2
.cz.yaml
2
.cz.yaml
@ -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
6
.gitignore
vendored
@ -9,3 +9,9 @@ artifacts/
|
||||
volumes/
|
||||
build/
|
||||
pages/
|
||||
node_modules/
|
||||
.markdownlint-cli2.jsonc
|
||||
.markdownlint.json
|
||||
package-lock.json
|
||||
package.json
|
||||
**.junit.xml
|
||||
|
317
CHANGELOG.md
317
CHANGELOG.md
@ -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
|
||||
|
@ -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.
|
||||
|
@ -21,6 +21,7 @@ def permission_queryset():
|
||||
exclude_models = [
|
||||
'appsettings',
|
||||
'chordcounter',
|
||||
'comment',
|
||||
'groupresult',
|
||||
'organization'
|
||||
'settings',
|
||||
|
@ -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,13 +289,21 @@ class OrganizationMixin():
|
||||
|
||||
return True
|
||||
|
||||
if permissions_required:
|
||||
|
||||
perms = permissions_required
|
||||
|
||||
else:
|
||||
|
||||
perms = self.get_permission_required()
|
||||
|
||||
if self.has_organization_permission():
|
||||
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':
|
||||
|
||||
if len(self.kwargs) == 0 or (len(self.kwargs) == 1 and 'ticket_type' in self.kwargs):
|
||||
|
||||
return True
|
||||
|
||||
@ -328,6 +383,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:
|
||||
|
||||
non_organization_models = [
|
||||
|
@ -124,7 +124,9 @@ 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:
|
||||
|
||||
if self.model.is_global:
|
||||
|
||||
return super().get_queryset().filter(
|
||||
models.Q(organization__in=user_organizations)
|
||||
@ -132,6 +134,12 @@ class TenancyManager(models.Manager):
|
||||
models.Q(is_global = True)
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
return super().get_queryset().filter(
|
||||
models.Q(organization__in=user_organizations)
|
||||
)
|
||||
|
||||
return super().get_queryset()
|
||||
|
||||
|
||||
@ -189,6 +197,15 @@ class TenancyObject(SaveHistory):
|
||||
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):
|
||||
class Meta:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
63
app/api/serializers/assistance/request.py
Normal file
63
app/api/serializers/assistance/request.py
Normal 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
|
||||
)
|
195
app/api/serializers/core/ticket.py
Normal file
195
app/api/serializers/core/ticket.py
Normal 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
|
44
app/api/serializers/core/ticket_category.py
Normal file
44
app/api/serializers/core/ticket_category.py
Normal 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)
|
74
app/api/serializers/core/ticket_comment.py
Normal file
74
app/api/serializers/core/ticket_comment.py
Normal 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)
|
42
app/api/serializers/core/ticket_comment_category.py
Normal file
42
app/api/serializers/core/ticket_comment_category.py
Normal 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)
|
@ -53,7 +53,6 @@ class DeviceSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
depth = 1
|
||||
fields = [
|
||||
'id',
|
||||
'is_global',
|
||||
|
63
app/api/serializers/itim/change.py
Normal file
63
app/api/serializers/itim/change.py
Normal 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
|
||||
)
|
63
app/api/serializers/itim/incident.py
Normal file
63
app/api/serializers/itim/incident.py
Normal 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
|
||||
)
|
63
app/api/serializers/itim/problem.py
Normal file
63
app/api/serializers/itim/problem.py
Normal 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
|
||||
)
|
74
app/api/serializers/project_management/project_milestone.py
Normal file
74
app/api/serializers/project_management/project_milestone.py
Normal 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
|
33
app/api/serializers/project_management/project_state.py
Normal file
33
app/api/serializers/project_management/project_state.py
Normal 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)
|
63
app/api/serializers/project_management/project_task.py
Normal file
63
app/api/serializers/project_management/project_task.py
Normal 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
|
||||
)
|
33
app/api/serializers/project_management/project_type.py
Normal file
33
app/api/serializers/project_management/project_type.py
Normal 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)
|
134
app/api/serializers/project_management/projects.py
Normal file
134
app/api/serializers/project_management/projects.py
Normal 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',
|
||||
]
|
@ -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()
|
||||
|
@ -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(
|
||||
|
@ -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'),
|
||||
|
||||
|
1
app/api/views/assistance/__init__.py
Normal file
1
app/api/views/assistance/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .index import *
|
35
app/api/views/assistance/index.py
Normal file
35
app/api/views/assistance/index.py
Normal 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)
|
77
app/api/views/assistance/request_ticket.py
Normal file
77
app/api/views/assistance/request_ticket.py
Normal 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'
|
79
app/api/views/core/ticket_categories.py
Normal file
79
app/api/views/core/ticket_categories.py
Normal 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'
|
79
app/api/views/core/ticket_comment_categories.py
Normal file
79
app/api/views/core/ticket_comment_categories.py
Normal 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'
|
102
app/api/views/core/ticket_comments.py
Normal file
102
app/api/views/core/ticket_comments.py
Normal 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'
|
145
app/api/views/core/tickets.py
Normal file
145
app/api/views/core/tickets.py
Normal 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
|
||||
)
|
@ -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),
|
||||
}
|
||||
|
@ -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):
|
||||
|
1
app/api/views/itim/__init__.py
Normal file
1
app/api/views/itim/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .index import *
|
82
app/api/views/itim/change_ticket.py
Normal file
82
app/api/views/itim/change_ticket.py
Normal 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'
|
81
app/api/views/itim/incident_ticket.py
Normal file
81
app/api/views/itim/incident_ticket.py
Normal 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'
|
36
app/api/views/itim/index.py
Normal file
36
app/api/views/itim/index.py
Normal 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)
|
81
app/api/views/itim/problem_ticket.py
Normal file
81
app/api/views/itim/problem_ticket.py
Normal 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'
|
@ -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:
|
||||
|
1
app/api/views/project_management/__init__.py
Normal file
1
app/api/views/project_management/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .index import *
|
34
app/api/views/project_management/index.py
Normal file
34
app/api/views/project_management/index.py
Normal 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)
|
90
app/api/views/project_management/project_milestone.py
Normal file
90
app/api/views/project_management/project_milestone.py
Normal 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
|
73
app/api/views/project_management/project_state.py
Normal file
73
app/api/views/project_management/project_state.py
Normal 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
|
64
app/api/views/project_management/project_task.py
Normal file
64
app/api/views/project_management/project_task.py
Normal 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'
|
72
app/api/views/project_management/project_type.py
Normal file
72
app/api/views/project_management/project_type.py
Normal 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
|
116
app/api/views/project_management/projects.py
Normal file
116
app/api/views/project_management/projects.py
Normal 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'
|
@ -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)
|
||||
|
@ -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:
|
||||
|
||||
|
@ -135,8 +135,15 @@ class ModelPermissionsAdd:
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
|
||||
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,8 +157,15 @@ class ModelPermissionsAdd:
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
|
||||
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)
|
||||
response = client.post(url, data=self.add_data)
|
||||
@ -167,8 +181,15 @@ class ModelPermissionsAdd:
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
|
||||
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)
|
||||
response = client.post(url, data=self.add_data)
|
||||
@ -183,8 +204,15 @@ class ModelPermissionsAdd:
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
|
||||
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)
|
||||
response = client.post(url, data=self.add_data)
|
||||
@ -195,12 +223,19 @@ 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()
|
||||
|
||||
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)
|
||||
response = client.post(url, data=self.add_data)
|
||||
|
@ -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,
|
||||
|
@ -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")),
|
||||
|
||||
]
|
||||
|
@ -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"),
|
||||
|
||||
]
|
||||
|
@ -87,6 +87,11 @@ class DetailForm(ConfigGroupForm):
|
||||
"slug": "configuration",
|
||||
"sections": []
|
||||
},
|
||||
"tickets": {
|
||||
"name": "Tickets",
|
||||
"slug": "tickets",
|
||||
"sections": []
|
||||
},
|
||||
"notes": {
|
||||
"name": "Notes",
|
||||
"slug": "notes",
|
||||
|
@ -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> </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> </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 %}
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'),
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,6 +73,8 @@ class CommonModelForm(forms.ModelForm):
|
||||
|
||||
if hasattr(field.queryset.model, 'is_global'):
|
||||
|
||||
if field.queryset.model.is_global is not None:
|
||||
|
||||
self.fields[field_name].queryset = field.queryset.filter(
|
||||
Q(organization__in=user_organizations_id)
|
||||
|
|
||||
|
55
app/core/forms/related_ticket.py
Normal file
55
app/core/forms/related_ticket.py
Normal 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
249
app/core/forms/ticket.py
Normal 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__'
|
120
app/core/forms/ticket_categories.py
Normal file
120
app/core/forms/ticket_categories.py
Normal 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')
|
||||
|
163
app/core/forms/ticket_comment.py
Normal file
163
app/core/forms/ticket_comment.py
Normal 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)
|
119
app/core/forms/ticket_comment_category.py
Normal file
119
app/core/forms/ticket_comment_category.py
Normal 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')
|
||||
|
26
app/core/forms/ticket_linked_item.py
Normal file
26
app/core/forms/ticket_linked_item.py
Normal 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()
|
668
app/core/forms/validate_ticket.py
Normal file
668
app/core/forms/validate_ticket.py
Normal 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')
|
276
app/core/forms/validate_ticket_comment.py
Normal file
276
app/core/forms/validate_ticket_comment.py
Normal 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
0
app/core/lib/__init__.py
Normal file
114
app/core/lib/markdown.py
Normal file
114
app/core/lib/markdown.py
Normal 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)
|
155
app/core/lib/markdown_plugins/model_reference.py
Normal file
155
app/core/lib/markdown_plugins/model_reference.py
Normal 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)
|
103
app/core/lib/markdown_plugins/ticket_number.py
Normal file
103
app/core/lib/markdown_plugins/ticket_number.py
Normal 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)
|
47
app/core/lib/slash_commands/__init__.py
Normal file
47
app/core/lib/slash_commands/__init__.py
Normal 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
|
96
app/core/lib/slash_commands/duration.py
Normal file
96
app/core/lib/slash_commands/duration.py
Normal 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
|
148
app/core/lib/slash_commands/linked_model.py
Normal file
148
app/core/lib/slash_commands/linked_model.py
Normal 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
|
98
app/core/lib/slash_commands/related_ticket.py
Normal file
98
app/core/lib/slash_commands/related_ticket.py
Normal 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
|
@ -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),
|
||||
),
|
||||
]
|
@ -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')},
|
||||
),
|
||||
]
|
@ -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,6 +182,8 @@ class SaveHistory(models.Model):
|
||||
# Process the save
|
||||
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
|
||||
|
||||
if self.save_model_history:
|
||||
|
||||
after = self.__dict__.copy()
|
||||
|
||||
self.save_history(before, after)
|
||||
|
0
app/core/models/__init__.py
Normal file
0
app/core/models/__init__.py
Normal file
0
app/core/models/ticket/__init__.py
Normal file
0
app/core/models/ticket/__init__.py
Normal file
1187
app/core/models/ticket/ticket.py
Normal file
1187
app/core/models/ticket/ticket.py
Normal file
File diff suppressed because it is too large
Load Diff
121
app/core/models/ticket/ticket_category.py
Normal file
121
app/core/models/ticket/ticket_category.py
Normal 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
|
435
app/core/models/ticket/ticket_comment.py
Normal file
435
app/core/models/ticket/ticket_comment.py
Normal 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
|
||||
)
|
103
app/core/models/ticket/ticket_comment_category.py
Normal file
103
app/core/models/ticket/ticket_comment_category.py
Normal 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
|
126
app/core/models/ticket/ticket_enum_values.py
Normal file
126
app/core/models/ticket/ticket_enum_values.py
Normal 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'
|
142
app/core/models/ticket/ticket_linked_items.py
Normal file
142
app/core/models/ticket/ticket_linked_items.py
Normal 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()
|
47
app/core/templates/core/index_ticket_categories.html.j2
Normal file
47
app/core/templates/core/index_ticket_categories.html.j2
Normal 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> </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> </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">« 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 »</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endblock %}
|
@ -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> </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> </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">« 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 »</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endblock %}
|
163
app/core/templates/core/ticket.html.j2
Normal file
163
app/core/templates/core/ticket.html.j2
Normal 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 %}
|
@ -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>
|
89
app/core/templates/core/ticket/comment.html.j2
Normal file
89
app/core/templates/core/ticket/comment.html.j2
Normal 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>
|
132
app/core/templates/core/ticket/comment/comment.html.j2
Normal file
132
app/core/templates/core/ticket/comment/comment.html.j2
Normal 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 }} </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 %}
|
25
app/core/templates/core/ticket/icon_status.html.j2
Normal file
25
app/core/templates/core/ticket/icon_status.html.j2
Normal 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 %}
|
67
app/core/templates/core/ticket/index.html.j2
Normal file
67
app/core/templates/core/ticket/index.html.j2
Normal 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> </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">
|
||||
|
||||
</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 %}
|
17
app/core/templates/core/ticket/renderers/ticket_link.html.j2
Normal file
17
app/core/templates/core/ticket/renderers/ticket_link.html.j2
Normal 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>
|
8
app/core/templates/core/ticket/type_icon.html.j2
Normal file
8
app/core/templates/core/ticket/type_icon.html.j2
Normal 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>
|
12
app/core/templates/core/ticket_category.html.j2
Normal file
12
app/core/templates/core/ticket_category.html.j2
Normal 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
Reference in New Issue
Block a user