Compare commits

..

34 Commits

Author SHA1 Message Date
Jon
8d071c68df chore: add Merge/Pull request template
#226
2024-08-14 01:52:22 +09:30
Jon
3b1691ff62 docs(roadmap): update completed features
#226
2024-08-14 01:46:21 +09:30
Jon
a77c43d213 docs(base): detail view template
. #24 #226 closes #22
2024-08-14 01:33:48 +09:30
Jon
086959b431 refactor(itim): services now use details template
. #22 #226
2024-08-14 01:23:17 +09:30
Jon
3f117f9d83 feat(base): create detail view templates
purpose is to aid in the development of a detail form

#22 #24 #226
2024-08-14 00:02:25 +09:30
Jon
6a23845a4f docs: initial adding of template page
#22
2024-08-13 15:03:10 +09:30
Jon
b9c6d04e04 chore: add services navigation icon
!43 #69
2024-08-13 13:23:45 +09:30
Jon
32c0027ecf fix(itim): ensure that the service template config is also rendered as part of device config
!43 #69
2024-08-13 13:23:45 +09:30
Jon
dae52e8646 docs: fluff the port and services
!43 closes #69
2024-08-13 13:23:45 +09:30
Jon
890a5651a0 fix(itim): dont render link if no device
!43 #69
2024-08-13 13:23:45 +09:30
Jon
4cb37f8347 feat(itam): Render Service Config with device config
!43 #69
2024-08-13 13:23:45 +09:30
Jon
a2010b9517 feat(itam): Display deployed services for devices
!43 #69
2024-08-13 13:23:45 +09:30
Jon
c95736ce14 feat(itim): Prevent circular service dependencies
!43 #69
2024-08-13 13:23:45 +09:30
Jon
b46c61954c feat(itim): Port number validation to check for valid port numbers
!43 #69
2024-08-13 13:23:45 +09:30
Jon
afe4266600 feat(itim): Prevent Service template from being assigned as dependent service
!43 #69
2024-08-13 13:23:45 +09:30
Jon
0c8d1c8da1 feat(itim): Add service template support
!43 #69
2024-08-13 13:23:45 +09:30
Jon
eac998b5cc fix(itim): Dont show self within service dependencies
!43 #69
2024-08-13 13:23:45 +09:30
Jon
5914782252 feat(itim): Ports for service management
!43 #69
2024-08-13 13:23:45 +09:30
Jon
73d875c4ac feat(itim): Service Management
!43 #69
2024-08-13 13:23:45 +09:30
Jon
8f439f0675 fix(assistance): Only return distinct values when limiting KB articles
!43 #10
2024-08-13 13:23:45 +09:30
Jon
0f102c6aaf docs(assistance): document kb categories for user
!43 closes #10
2024-08-13 13:23:45 +09:30
Jon
4852c6caeb feat(assistance): Filter KB articles to target user
only intended to filter for users whom dont have change perm.

!43 #10
2024-08-13 13:23:45 +09:30
Jon
3fffba2eba feat(assistance): Add date picker to date fields for KB articles
!43 #10
2024-08-13 13:23:45 +09:30
Jon
a1293984ea feat(assistance): Dont display expired articles for "view" users
!43 #10
2024-08-13 13:23:45 +09:30
Jon
4876db50c1 docs(assistance): document kb for user
!43 #10
2024-08-13 13:23:45 +09:30
Jon
425cc066af feat(base): add code highlighting to markdown
!43 #10
2024-08-13 13:23:45 +09:30
Jon
1086f517fa feat(assistance): Categorised Knowledge base articles
!43 #10
2024-08-13 13:23:45 +09:30
Jon
2fdbf87ddd docs(assistance): added pages for knowledgebase
!43 #10
2024-08-13 13:23:45 +09:30
Jon
86228836c7 chore(base): rename information -> assistance
!43 #10
2024-08-13 13:23:45 +09:30
Jon
a6e6c948a5 feat(itim): Add menu entry
!43 #69 #71
2024-08-13 13:23:45 +09:30
Jon
dcdfa8feb7 feat(itam): Ability to add device configuration
!43 fixes #44
2024-08-13 13:23:45 +09:30
Jon
8388d2e695 test(external_link): add tests
!43 fixes #6
2024-08-13 13:23:45 +09:30
Jon
29f269050f feat(settings): New model to allow adding templated links to devices and software
!43 #6
2024-08-13 13:23:45 +09:30
Jon
93c4fc2009 docs: move settings pages into sub-directory
!43 #6
2024-08-13 13:23:45 +09:30
343 changed files with 1796 additions and 27199 deletions

View File

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

View File

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

6
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,19 +10,6 @@ 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 = []
@ -39,24 +26,20 @@ class OrganizationMixin():
parent_model (Model): with PK from kwargs['pk']
"""
return self.parent_model.objects.get(pk=self.kwargs[self.parent_model_pk_kwarg])
return self.parent_model.objects.get(pk=self.kwargs['pk'])
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 self.parent_model:
if hasattr(self, 'parent_model'):
obj = self.get_parent_obj()
id = obj.get_organization().id
@ -78,10 +61,6 @@ class OrganizationMixin():
id = 0
if hasattr(self, 'instance') and id is None: # Form Instance
id = self.instance.get_organization()
except AttributeError:
@ -105,10 +84,6 @@ class OrganizationMixin():
pass
if id is not None:
self._object_organization = id
return id
@ -172,10 +147,6 @@ class OrganizationMixin():
user_organizations = []
if hasattr(self, '_user_organizations'):
return self._user_organizations
teams = Team.objects
for group in self.request.user.groups.all():
@ -186,32 +157,14 @@ 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, 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.
"""
def has_organization_permission(self, organization: int=None) -> bool:
has_permission = False
if permissions_required is None:
permissions_required = self.get_permission_required()
if not organization:
organization = self.object_organization()
@ -229,7 +182,7 @@ class OrganizationMixin():
assembled_permission = str(permission["content_type__app_label"]) + '.' + str(permission["codename"])
if assembled_permission in permissions_required and (team['organization_id'] == organization or organization == 0):
if assembled_permission in self.get_permission_required() and (team['organization_id'] == organization or organization == 0):
return True
@ -289,23 +242,15 @@ class OrganizationMixin():
return True
if permissions_required:
perms = self.get_permission_required()
perms = permissions_required
else:
perms = self.get_permission_required()
if self.has_organization_permission(permissions_required = perms):
if self.has_organization_permission():
return True
if self.request.user.has_perms(perms) and str(self.request.method).lower() == 'get':
if self.request.user.has_perms(perms) and len(self.kwargs) == 0 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
return True
for required_permission in self.permission_required:
@ -382,12 +327,6 @@ class OrganizationPermission(AccessMixin, OrganizationMixin):
if not request.user.is_authenticated:
return self.handle_no_permission()
if len(self.permission_required) == 0:
if hasattr(self, 'get_dynamic_permissions'):
self.permission_required = self.get_dynamic_permissions()
if len(self.permission_required) > 0:

View File

@ -124,21 +124,13 @@ class TenancyManager(models.Manager):
user_organizations += [ team_user.team.organization.id ]
if len(user_organizations) > 0 and not user.is_superuser and self.model.is_global is not None:
if len(user_organizations) > 0 and not user.is_superuser:
if self.model.is_global:
return super().get_queryset().filter(
models.Q(organization__in=user_organizations)
|
models.Q(is_global = True)
)
else:
return super().get_queryset().filter(
models.Q(organization__in=user_organizations)
)
return super().get_queryset().filter(
models.Q(organization__in=user_organizations)
|
models.Q(is_global = True)
)
return super().get_queryset()
@ -196,15 +188,6 @@ class TenancyObject(SaveHistory):
def get_organization(self) -> Organization:
return self.organization
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
if self.organization is None:
raise ValidationError('Organization not defined')
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
class Team(Group, TenancyObject):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,25 +5,6 @@ from rest_framework.urlpatterns import format_suffix_patterns
from .views import access, config, index
from api.views.settings import permissions
from api.views.settings import index as settings
from api.views import assistance, itim, project_management
from api.views.assistance import request_ticket
from api.views.core import (
ticket_categories,
ticket_comment_categories,
ticket_comments as core_ticket_comments
)
from api.views.itim import change_ticket, incident_ticket, problem_ticket
from api.views.project_management import (
projects,
project_milestone,
project_state,
project_type,
project_task
)
from .views.itam import software, config as itam_config
from .views.itam.device import DeviceViewSet
from .views.itam import inventory
@ -32,47 +13,15 @@ from .views.itam import inventory
app_name = "API"
router = DefaultRouter(trailing_slash=False)
router = DefaultRouter()
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'),
@ -80,8 +29,6 @@ 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'),
@ -89,11 +36,6 @@ urlpatterns = [
path("organization/<int:organization_id>/team/<int:group_ptr_id>/permissions", access.TeamPermissionDetail.as_view(), name='_api_team_permission'),
path("organization/team/", access.TeamList.as_view(), name='_api_teams'),
path("project_management", project_management.index.Index.as_view(), name="_api_project_management"),
path("settings", settings.View.as_view(), name='_settings'),
path("settings/permissions", permissions.View.as_view(), name='_settings_permissions'),
]
urlpatterns = format_suffix_patterns(urlpatterns)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,105 +0,0 @@
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_view_name(self):
if self.detail:
return "Project"
return 'Projects'

View File

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

View File

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

View File

@ -108,7 +108,6 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'rest_framework',
'rest_framework_json_api',
'django_filters',
'social_django',
'django_celery_results',
'core.apps.CoreConfig',
@ -120,7 +119,6 @@ INSTALLED_APPS = [
'drf_spectacular',
'drf_spectacular_sidecar',
'config_management.apps.ConfigManagementConfig',
'project_management.apps.ProjectManagementConfig',
]
MIDDLEWARE = [
@ -259,9 +257,7 @@ if API_ENABLED:
# ),
'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata',
'DEFAULT_FILTER_BACKENDS': (
# 'rest_framework_json_api.filters.QueryParameterValidationFilter',
'rest_framework.filters.SearchFilter',
'rest_framework_json_api.django_filters.DjangoFilterBackend',
'rest_framework_json_api.filters.QueryParameterValidationFilter',
'rest_framework_json_api.filters.OrderingFilter',
'rest_framework_json_api.django_filters.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
@ -361,6 +357,11 @@ if DEBUG:
"127.0.0.1",
]
# Apps Under Development
INSTALLED_APPS += [
'project_management.apps.ProjectManagementConfig',
]
if SSO_ENABLED:

View File

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

View File

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

View File

@ -134,34 +134,6 @@ 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 """
@ -552,9 +524,6 @@ class IndexView:
class AllViews(
AddView,
ChangeView,

View File

@ -24,7 +24,7 @@ from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
from .views import home
from core.views import history, related_ticket, ticket_linked_item
from core.views import history
from settings.views import user_settings
@ -50,11 +50,6 @@ 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"),
]
@ -79,13 +74,12 @@ if settings.DEBUG:
urlpatterns += [
path("__debug__/", include("debug_toolbar.urls"), name='_debug'),
path("project_management/", include("project_management.urls")),
]
# must be after above
urlpatterns += [
path("project_management/", include("project_management.urls")),
path("settings/", include("settings.urls")),
]

View File

@ -1,6 +1,5 @@
from django import forms
from django.urls import reverse
from django.forms import ValidationError
from app import settings
@ -64,84 +63,3 @@ class KnowledgeBaseForm(CommonModelForm):
return cleaned_data
class DetailForm(KnowledgeBaseForm):
tabs: dict = {
"details": {
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'title',
'category',
'responsible_user',
'organization',
'is_global',
'c_created',
'c_modified',
],
"right": [
'release_date',
'expiry_date',
'target_user',
'target_team',
]
},
{
"layout": "single",
"name": "Summary",
"fields": [
'summary',
],
"markdown": [
'summary',
]
},
{
"layout": "single",
"name": "Content",
"fields": [
'content',
],
"markdown": [
'content',
]
}
]
},
"notes": {
"name": "Notes",
"slug": "notes",
"sections": []
}
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['c_created'] = forms.DateTimeField(
label = 'Created',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.created,
)
self.fields['c_modified'] = forms.DateTimeField(
label = 'Modified',
input_formats=settings.DATETIME_FORMAT,
disabled = True,
initial = self.instance.modified,
)
self.tabs['details'].update({
"edit_url": reverse('Assistance:_knowledge_base_change', args=(self.instance.pk,))
})
self.url_index_view = reverse('Assistance:Knowledge Base')

View File

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

View File

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

View File

@ -7,7 +7,7 @@ from django.utils.decorators import method_decorator
from access.models import TeamUsers
from assistance.forms.knowledge_base import DetailForm, KnowledgeBaseForm
from assistance.forms.knowledge_base import KnowledgeBaseForm
from assistance.models.knowledge_base import KnowledgeBase
from core.forms.comment import AddNoteForm
@ -139,7 +139,7 @@ class View(ChangeView):
context_object_name = "kb"
form_class = DetailForm
form_class = KnowledgeBaseForm
model = KnowledgeBase
@ -168,7 +168,7 @@ class View(ChangeView):
return context
# @method_decorator(auth_decorator.permission_required("assistance.change_knowledgebase", raise_exception=True))
@method_decorator(auth_decorator.permission_required("assistance.change_knowledgebase", raise_exception=True))
def post(self, request, *args, **kwargs):
item = KnowledgeBase.objects.get(pk=self.kwargs['pk'])

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,25 +1,21 @@
from django.urls import path
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
from config_management.views.groups.groups import GroupIndexView, GroupAdd, GroupDelete, GroupView, GroupHostAdd, GroupHostDelete
from config_management.views.groups.software import GroupSoftwareAdd, GroupSoftwareChange, GroupSoftwareDelete
app_name = "Config Management"
urlpatterns = [
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', 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>/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>/child', GroupAdd.as_view(), name='_group_add_child'),
path('group/<int:pk>/delete', GroupDelete.as_view(), name='_group_delete'),
path("group/<int:pk>/software/add", 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>/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>/host', GroupHostAdd.as_view(), name='_group_add_host'),
path('group/<int:group_id>/host/<int:pk>/delete', GroupHostDelete.as_view(), name='_group_delete_host'),

View File

@ -6,7 +6,6 @@ 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
@ -14,12 +13,12 @@ from itam.models.device import Device
from settings.models.user_settings import UserSettings
from config_management.forms.group_hosts import ConfigGroupHostsForm
from config_management.forms.group.group import ConfigGroupForm, DetailForm
from config_management.forms.group.group import ConfigGroupForm
from config_management.models.groups import ConfigGroups, ConfigGroupHosts, ConfigGroupSoftware
class Index(IndexView):
class GroupIndexView(IndexView):
context_object_name = "groups"
@ -51,7 +50,7 @@ class Index(IndexView):
class Add(AddView):
class GroupAdd(AddView):
organization_field = 'organization'
@ -68,11 +67,9 @@ class Add(AddView):
def get_initial(self):
# initial: dict = {
# 'organization': UserSettings.objects.get(user = self.request.user).default_organization
# }
initial = super().get_initial()
initial: dict = {
'organization': UserSettings.objects.get(user = self.request.user).default_organization
}
if 'pk' in self.kwargs:
@ -105,7 +102,7 @@ class Add(AddView):
class Change(ChangeView):
class GroupView(ChangeView):
context_object_name = "group"
@ -113,38 +110,9 @@ class Change(ChangeView):
model = ConfigGroups
permission_required = [
'config_management.change_configgroups',
]
template_name = 'form.html.j2'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = self.object.name
return context
def get_success_url(self, **kwargs):
return reverse('Config Management:_group_view', args=(self.kwargs['pk'],))
class View(ChangeView):
context_object_name = "group"
form_class = DetailForm
model = ConfigGroups
permission_required = [
'config_management.view_configgroups',
'config_management.change_configgroups',
]
template_name = 'config_management/group.html.j2'
@ -160,11 +128,6 @@ class View(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'])
@ -223,7 +186,7 @@ class View(ChangeView):
class Delete(DeleteView):
class GroupDelete(DeleteView):
model = ConfigGroups

View File

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

View File

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

View File

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

View File

@ -1,55 +0,0 @@
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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,26 +0,0 @@
from django import forms
from django.db.models import Q
from django.forms import ValidationError
from app import settings
from core.forms.common import CommonModelForm
from core.models.ticket.ticket_linked_items import TicketLinkedItem
class TicketLinkedItemForm(CommonModelForm):
prefix = 'ticket_linked_item'
class Meta:
model = TicketLinkedItem
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['organization'].widget = self.fields['organization'].hidden_widget()
self.fields['ticket'].widget = self.fields['ticket'].hidden_widget()

View File

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

View File

@ -1,276 +0,0 @@
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,21 +0,0 @@
# Generated by Django 5.0.7 on 2024-08-17 08:05
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0002_notes'),
]
operations = [
migrations.AlterModelOptions(
name='manufacturer',
options={'ordering': ['name'], 'verbose_name_plural': 'Manufacturers'},
),
migrations.AlterModelOptions(
name='notes',
options={'ordering': ['-created'], 'verbose_name_plural': 'Notes'},
),
]

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.7 on 2024-08-17 08:05
# Generated by Django 5.0.7 on 2024-07-21 02:35
import django.db.models.deletion
from django.db import migrations, models
@ -7,7 +7,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0003_alter_manufacturer_options_alter_notes_options'),
('core', '0002_notes'),
('itim', '0001_initial'),
]

View File

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

View File

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

View File

@ -8,11 +8,6 @@ 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
@ -30,7 +25,6 @@ class SaveHistory(models.Model):
"""
remove_keys = [
'_django_version',
'_state',
'created',
'modified'
@ -47,18 +41,6 @@ class SaveHistory(models.Model):
value = bool(before[entry])
elif (
"{" in str(after[entry])
and
"}" in str(after[entry])
) or (
"[" in str(after[entry])
and
"]" in str(after[entry])
):
value = str(after[entry]).replace("'", '\"')
else:
value = str(before[entry])
@ -80,18 +62,6 @@ class SaveHistory(models.Model):
value = bool(after[entry])
elif (
"{" in str(after[entry])
and
"}" in str(after[entry])
) or (
"[" in str(after[entry])
and
"]" in str(after[entry])
):
value = str(after[entry]).replace("'", '\"')
else:
value = str(after[entry])
@ -182,11 +152,9 @@ 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()
after = self.__dict__.copy()
self.save_history(before, after)
self.save_history(before, after)
def delete_history(self, item_pk, item_class):

View File

@ -34,9 +34,6 @@ class Manufacturer(TenancyObject, ManufacturerCommonFields, SaveHistory):
'name'
]
verbose_name_plural = 'Manufacturers'
name = models.CharField(
blank = False,
max_length = 50,

View File

@ -46,9 +46,6 @@ class Notes(NotesCommonFields):
'-created'
]
verbose_name_plural = 'Notes'
note = models.TextField(
verbose_name = 'Note',

File diff suppressed because it is too large Load Diff

View File

@ -1,121 +0,0 @@
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

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