feat(core): add basic ticketing system
ref: #250 #252 #96 #93 #95 #90 #115
@ -24,7 +24,7 @@ from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
|
||||
|
||||
from .views import home
|
||||
|
||||
from core.views import history
|
||||
from core.views import history, related_ticket
|
||||
|
||||
from settings.views import user_settings
|
||||
|
||||
@ -50,6 +50,9 @@ 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"),
|
||||
|
||||
]
|
||||
|
||||
|
||||
|
37
app/core/forms/related_ticket.py
Normal file
@ -0,0 +1,37 @@
|
||||
from django import forms
|
||||
from django.db.models import Q
|
||||
|
||||
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()
|
||||
|
||||
return is_valid
|
139
app/core/forms/ticket.py
Normal file
@ -0,0 +1,139 @@
|
||||
from django import forms
|
||||
from django.db.models import Q
|
||||
|
||||
from app import settings
|
||||
|
||||
from core.forms.common import CommonModelForm
|
||||
|
||||
from core.models.ticket.ticket import Ticket, RelatedTickets
|
||||
|
||||
|
||||
|
||||
class TicketForm(CommonModelForm):
|
||||
|
||||
prefix = 'ticket'
|
||||
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
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['ticket_type'].widget = self.fields['ticket_type'].hidden_widget()
|
||||
|
||||
|
||||
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'
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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.fields['ticket_type'].initial = self.Meta.model.TicketType.PROJECT_TASK
|
||||
|
||||
|
||||
if kwargs['user'].is_superuser:
|
||||
|
||||
ticket_type += self.Meta.model.tech_fields
|
||||
|
||||
|
||||
for field in original_fields:
|
||||
|
||||
if field not in ticket_type:
|
||||
|
||||
del self.fields[field]
|
||||
|
||||
def clean(self):
|
||||
|
||||
cleaned_data = super().clean()
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
|
||||
is_valid = super().is_valid()
|
||||
|
||||
return is_valid
|
||||
|
||||
|
||||
|
||||
class DetailForm(CommonModelForm):
|
||||
|
||||
prefix = 'ticket'
|
||||
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields = '__all__'
|
137
app/core/forms/ticket_comment.py
Normal file
@ -0,0 +1,137 @@
|
||||
from django import forms
|
||||
from django.db.models import Q
|
||||
|
||||
from app import settings
|
||||
|
||||
from core.forms.common import CommonModelForm
|
||||
|
||||
from core.models.ticket.ticket_comment import TicketComment
|
||||
|
||||
|
||||
class CommentForm(CommonModelForm):
|
||||
|
||||
prefix = 'ticket'
|
||||
|
||||
class Meta:
|
||||
model = TicketComment
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
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['body'].widget.attrs = {'style': "height: 800px; width: 900px"}
|
||||
|
||||
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()
|
||||
|
||||
if 'qs_comment_type' in kwargs['initial']:
|
||||
|
||||
comment_type = kwargs['initial']['qs_comment_type']
|
||||
|
||||
else:
|
||||
|
||||
comment_type = str(self.instance.get_comment_type_display()).lower()
|
||||
|
||||
|
||||
original_fields = self.fields.copy()
|
||||
comment_fields = []
|
||||
|
||||
|
||||
if (
|
||||
kwargs['initial']['type_ticket'] == 'request'
|
||||
or
|
||||
kwargs['initial']['type_ticket'] == 'incident'
|
||||
or
|
||||
kwargs['initial']['type_ticket'] == 'problem'
|
||||
or
|
||||
kwargs['initial']['type_ticket'] == 'change'
|
||||
or
|
||||
kwargs['initial']['type_ticket'] == 'project_task'
|
||||
):
|
||||
|
||||
if comment_type == 'task':
|
||||
|
||||
comment_fields = self.Meta.model.fields_itsm_task
|
||||
|
||||
self.fields['comment_type'].initial = self.Meta.model.CommentType.TASK
|
||||
|
||||
elif comment_type == 'comment':
|
||||
|
||||
comment_fields = self.Meta.model.common_itsm_fields
|
||||
|
||||
self.fields['comment_type'].initial = self.Meta.model.CommentType.COMMENT
|
||||
|
||||
|
||||
elif comment_type == 'solution':
|
||||
|
||||
comment_fields = self.Meta.model.common_itsm_fields
|
||||
|
||||
self.fields['comment_type'].initial = self.Meta.model.CommentType.SOLUTION
|
||||
|
||||
elif comment_type == 'notification':
|
||||
|
||||
comment_fields = self.Meta.model.fields_itsm_notification
|
||||
|
||||
self.fields['comment_type'].initial = self.Meta.model.CommentType.NOTIFICATION
|
||||
|
||||
elif kwargs['initial']['type_ticket'] == 'issue':
|
||||
|
||||
comment_fields = self.Meta.model.fields_git_issue
|
||||
|
||||
elif kwargs['initial']['type_ticket'] == 'merge':
|
||||
|
||||
comment_fields = self.Meta.model.fields_git_merge
|
||||
|
||||
|
||||
for field in original_fields:
|
||||
|
||||
if field not in comment_fields:
|
||||
|
||||
del self.fields[field]
|
||||
|
||||
def clean(self):
|
||||
|
||||
cleaned_data = super().clean()
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
|
||||
is_valid = super().is_valid()
|
||||
|
||||
return is_valid
|
||||
|
||||
|
||||
|
||||
class DetailForm(CommentForm):
|
||||
|
||||
prefix = 'ticket'
|
||||
|
||||
class Meta:
|
||||
model = TicketComment
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
110
app/core/migrations/0005_ticket_relatedtickets_ticketcomment.py
Normal file
@ -0,0 +1,110 @@
|
||||
# Generated by Django 5.0.7 on 2024-08-25 07:44
|
||||
|
||||
import access.fields
|
||||
import access.models
|
||||
import core.models.ticket.change_ticket
|
||||
import core.models.ticket.markdown
|
||||
import core.models.ticket.problem_ticket
|
||||
import core.models.ticket.request_ticket
|
||||
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'),
|
||||
('project_management', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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.AutoCreatedField(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')], default=2, help_text='Status of ticket', verbose_name='Status')),
|
||||
('title', models.CharField(help_text='Title of the Ticket', max_length=50, unique=True, verbose_name='Title')),
|
||||
('description', models.TextField(default=None, 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')),
|
||||
('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, default=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, default=True, help_text='Assign the ticket to a User(s)', related_name='assigned_users', to=settings.AUTH_USER_MODEL, verbose_name='Assigned User(s)')),
|
||||
('opened_by', 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')),
|
||||
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
|
||||
('project', models.ForeignKey(blank=True, help_text='Assign to a project', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='project_management.project', verbose_name='Project')),
|
||||
('subscribed_teams', models.ManyToManyField(blank=True, default=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)')),
|
||||
('subscribed_users', models.ManyToManyField(blank=True, default=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)')),
|
||||
],
|
||||
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')],
|
||||
},
|
||||
bases=(models.Model, core.models.ticket.change_ticket.ChangeTicket, core.models.ticket.problem_ticket.ProblemTicket, core.models.ticket.request_ticket.RequestTicket, core.models.ticket.markdown.TicketMarkdown),
|
||||
),
|
||||
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])),
|
||||
('from_ticket_id', models.ForeignKey(help_text='This Ticket', on_delete=django.db.models.deletion.CASCADE, related_name='from_ticket_id', to='core.ticket', verbose_name='Ticket')),
|
||||
('to_ticket_id', 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')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['id'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TicketComment',
|
||||
fields=[
|
||||
('id', models.AutoField(help_text='Comment ID Number', primary_key=True, serialize=False, unique=True, verbose_name='Number')),
|
||||
('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(default=None, help_text='Comment contents', verbose_name='Comment')),
|
||||
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
|
||||
('modified', access.fields.AutoCreatedField(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')),
|
||||
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
|
||||
('parent', models.ForeignKey(blank=True, default=None, 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')),
|
||||
('responsible_team', 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')),
|
||||
('responsible_user', 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')),
|
||||
('template', models.ForeignKey(blank=True, default=None, help_text='Comment Template to use', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='comment_template', to='core.ticketcomment', verbose_name='Template')),
|
||||
('ticket', models.ForeignKey(blank=True, default=None, help_text='Parent ID for creating discussion threads', null=True, on_delete=django.db.models.deletion.CASCADE, to='core.ticket', validators=[core.models.ticket.ticket_comment.TicketComment.validation_ticket_id], verbose_name='Parent Comment')),
|
||||
('user', models.ForeignKey(help_text='Who made the comment', on_delete=django.db.models.deletion.DO_NOTHING, related_name='comment_user', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Comment',
|
||||
'verbose_name_plural': 'Comments',
|
||||
'ordering': ['ticket', 'parent_id'],
|
||||
},
|
||||
bases=(models.Model, core.models.ticket.markdown.TicketMarkdown),
|
||||
),
|
||||
]
|
0
app/core/models/__init__.py
Normal file
1
app/core/models/ticket/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import *
|
@ -1,6 +1,99 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.forms import ValidationError
|
||||
|
||||
from access.models import TenancyObject
|
||||
from access.fields import AutoCreatedField
|
||||
from access.models import TenancyObject, Team
|
||||
|
||||
from .change_ticket import ChangeTicket
|
||||
from .markdown import TicketMarkdown
|
||||
from .problem_ticket import ProblemTicket
|
||||
from .request_ticket import RequestTicket
|
||||
|
||||
from project_management.models.projects import Project
|
||||
|
||||
|
||||
|
||||
class TicketValues:
|
||||
|
||||
|
||||
_DRAFT_INT = '1'
|
||||
_NEW_INT = '2'
|
||||
|
||||
_ASSIGNED_INT = '3'
|
||||
_CLOSED_INT = '4'
|
||||
_INVALID_INT = '5'
|
||||
|
||||
#
|
||||
# ITSM statuses
|
||||
#
|
||||
|
||||
# Requests / Incidents / Problems / Changed
|
||||
_ASSIGNED_PLANNING_INT = '6'
|
||||
_PENDING_INT = '7'
|
||||
|
||||
# Requests / Incidents / Problems
|
||||
_SOLVED_INT = '8'
|
||||
|
||||
# Problem
|
||||
|
||||
_OBSERVATION_INT = '9'
|
||||
|
||||
# Problems / Changes
|
||||
|
||||
_ACCEPTED_INT = '10'
|
||||
|
||||
# Changes
|
||||
|
||||
_EVALUATION_INT = '11'
|
||||
_APPROVALS_INT = '12'
|
||||
_TESTING_INT = '13'
|
||||
_QUALIFICATION_INT = '14'
|
||||
_APPLIED_INT = '15'
|
||||
_REVIEW_INT = '16'
|
||||
_CANCELLED_INT = '17'
|
||||
_REFUSED_INT = '18'
|
||||
|
||||
|
||||
|
||||
|
||||
_DRAFT_STR = 'Draft'
|
||||
_NEW_STR = 'New'
|
||||
|
||||
_ASSIGNED_STR = 'Assigned'
|
||||
_CLOSED_STR = 'Closed'
|
||||
_INVALID_STR = 'Invalid'
|
||||
|
||||
#
|
||||
# ITSM statuses
|
||||
#
|
||||
|
||||
# Requests / Incidents / Problems / Changed
|
||||
_ASSIGNED_PLANNING_STR = 'Assigned (Planning)'
|
||||
_PENDING_STR = 'Pending'
|
||||
|
||||
# Requests / Incidents / Problems
|
||||
_SOLVED_STR = 'Solved'
|
||||
|
||||
# Problem
|
||||
|
||||
_OBSERVATION_STR = 'Under Observation'
|
||||
|
||||
# Problems / Changes
|
||||
|
||||
_ACCEPTED_STR = 'Accepted'
|
||||
|
||||
# Changes
|
||||
|
||||
_EVALUATION_STR = 'Evaluation'
|
||||
_APPROVALS_STR = 'Approvals'
|
||||
_TESTING_STR = 'Testing'
|
||||
_QUALIFICATION_STR = 'Qualification'
|
||||
_APPLIED_STR = 'Applied'
|
||||
_REVIEW_STR = 'Review'
|
||||
_CANCELLED_STR = 'Cancelled'
|
||||
_REFUSED_STR = 'Refused'
|
||||
|
||||
|
||||
|
||||
@ -26,6 +119,10 @@ class TicketCommonFields(models.Model):
|
||||
class Ticket(
|
||||
TenancyObject,
|
||||
TicketCommonFields,
|
||||
ChangeTicket,
|
||||
ProblemTicket,
|
||||
RequestTicket,
|
||||
TicketMarkdown,
|
||||
):
|
||||
|
||||
|
||||
@ -35,3 +132,624 @@ class Ticket(
|
||||
'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'),
|
||||
]
|
||||
|
||||
verbose_name = "Ticket"
|
||||
|
||||
verbose_name_plural = "Tickets"
|
||||
|
||||
|
||||
|
||||
class Ticket_ExternalSystem(models.IntegerChoices): # <null|github|gitlab>
|
||||
GITHUB = '1', 'Github'
|
||||
GITLAB = '2', 'Gitlab'
|
||||
|
||||
CUSTOM_1 = '9999', 'Custom #1 (Imported)'
|
||||
CUSTOM_2 = '9998', 'Custom #2 (Imported)'
|
||||
CUSTOM_3 = '9997', 'Custom #3 (Imported)'
|
||||
CUSTOM_4 = '9996', 'Custom #4 (Imported)'
|
||||
CUSTOM_5 = '9995', 'Custom #5 (Imported)'
|
||||
CUSTOM_6 = '9994', 'Custom #6 (Imported)'
|
||||
CUSTOM_7 = '9993', 'Custom #7 (Imported)'
|
||||
CUSTOM_8 = '9992', 'Custom #8 (Imported)'
|
||||
CUSTOM_9 = '9991', 'Custom #9 (Imported)'
|
||||
|
||||
|
||||
|
||||
class TicketStatus: # <draft|open|closed|in progress|assigned|solved|invalid>
|
||||
""" Ticket Status
|
||||
|
||||
Status of the ticket. By design, not all statuses are available for ALL ticket types.
|
||||
|
||||
## Request / Incident ticket
|
||||
|
||||
- Draft
|
||||
- New
|
||||
- Assigned
|
||||
- Assigned (Planned)
|
||||
- Pending
|
||||
- Solved
|
||||
- Closed
|
||||
|
||||
|
||||
## Problem Ticket
|
||||
|
||||
- Draft
|
||||
- New
|
||||
- Accepted
|
||||
- Assigned
|
||||
- Assigned (Planned)
|
||||
- Pending
|
||||
- Solved
|
||||
- Under Observation
|
||||
- Closed
|
||||
|
||||
## Change Ticket
|
||||
|
||||
- Draft
|
||||
- New
|
||||
- Evaluation
|
||||
- Approvals
|
||||
- Accepted
|
||||
- Pending
|
||||
- Testing
|
||||
- Qualification
|
||||
- Applied
|
||||
- Review
|
||||
- Closed
|
||||
- Cancelled
|
||||
- Refused
|
||||
|
||||
"""
|
||||
|
||||
class Request(models.IntegerChoices):
|
||||
|
||||
DRAFT = TicketValues._DRAFT_INT, TicketValues._DRAFT_STR
|
||||
NEW = TicketValues._NEW_INT, TicketValues._NEW_STR
|
||||
ASSIGNED = TicketValues._ASSIGNED_INT, TicketValues._ASSIGNED_STR
|
||||
ASSIGNED_PLANNING = TicketValues._ASSIGNED_PLANNING_INT, TicketValues._ASSIGNED_PLANNING_STR
|
||||
PENDING = TicketValues._PENDING_INT, TicketValues._PENDING_STR
|
||||
SOLVED = TicketValues._SOLVED_INT, TicketValues._SOLVED_STR
|
||||
CLOSED = TicketValues._CLOSED_INT, TicketValues._CLOSED_STR
|
||||
INVALID = TicketValues._INVALID_INT, TicketValues._INVALID_STR
|
||||
|
||||
|
||||
|
||||
class Incident(models.IntegerChoices):
|
||||
|
||||
DRAFT = TicketValues._DRAFT_INT, TicketValues._DRAFT_STR
|
||||
NEW = TicketValues._NEW_INT, TicketValues._NEW_STR
|
||||
ASSIGNED = TicketValues._ASSIGNED_INT, TicketValues._ASSIGNED_STR
|
||||
ASSIGNED_PLANNING = TicketValues._ASSIGNED_PLANNING_INT, TicketValues._ASSIGNED_PLANNING_STR
|
||||
PENDING = TicketValues._PENDING_INT, TicketValues._PENDING_STR
|
||||
SOLVED = TicketValues._SOLVED_INT, TicketValues._SOLVED_STR
|
||||
CLOSED = TicketValues._CLOSED_INT, TicketValues._CLOSED_STR
|
||||
INVALID = TicketValues._INVALID_INT, TicketValues._INVALID_STR
|
||||
|
||||
|
||||
|
||||
class Problem(models.IntegerChoices):
|
||||
|
||||
DRAFT = TicketValues._DRAFT_INT, TicketValues._DRAFT_STR
|
||||
NEW = TicketValues._NEW_INT, TicketValues._NEW_STR
|
||||
ACCEPTED = TicketValues._ACCEPTED_INT, TicketValues._ACCEPTED_STR
|
||||
ASSIGNED = TicketValues._ASSIGNED_INT, TicketValues._ASSIGNED_STR
|
||||
ASSIGNED_PLANNING = TicketValues._ASSIGNED_PLANNING_INT, TicketValues._ASSIGNED_PLANNING_STR
|
||||
PENDING = TicketValues._PENDING_INT, TicketValues._PENDING_STR
|
||||
SOLVED = TicketValues._SOLVED_INT, TicketValues._SOLVED_STR
|
||||
OBSERVATION = TicketValues._OBSERVATION_INT, TicketValues._OBSERVATION_STR
|
||||
CLOSED = TicketValues._CLOSED_INT, TicketValues._CLOSED_STR
|
||||
INVALID = TicketValues._INVALID_INT, TicketValues._INVALID_STR
|
||||
|
||||
|
||||
|
||||
class Change(models.IntegerChoices):
|
||||
|
||||
DRAFT = TicketValues._DRAFT_INT, TicketValues._DRAFT_STR
|
||||
NEW = TicketValues._NEW_INT, TicketValues._NEW_STR
|
||||
EVALUATION = TicketValues._EVALUATION_INT, TicketValues._EVALUATION_STR
|
||||
APPROVALS = TicketValues._APPROVALS_INT, TicketValues._APPROVALS_STR
|
||||
ACCEPTED = TicketValues._ACCEPTED_INT, TicketValues._ACCEPTED_STR
|
||||
PENDING = TicketValues._PENDING_INT, TicketValues._PENDING_STR
|
||||
TESTING = TicketValues._TESTING_INT, TicketValues._TESTING_STR
|
||||
QUALIFICATION = TicketValues._QUALIFICATION_INT, TicketValues._QUALIFICATION_STR
|
||||
APPLIED = TicketValues._APPLIED_INT, TicketValues._APPLIED_STR
|
||||
REVIEW = TicketValues._REVIEW_INT, TicketValues._REVIEW_STR
|
||||
CLOSED = TicketValues._CLOSED_INT, TicketValues._CLOSED_STR
|
||||
CANCELLED = TicketValues._CANCELLED_INT, TicketValues._CANCELLED_STR
|
||||
REFUSED = TicketValues._REFUSED_INT, TicketValues._REFUSED_STR
|
||||
|
||||
|
||||
class Git(models.IntegerChoices):
|
||||
|
||||
DRAFT = TicketValues._DRAFT_INT, TicketValues._DRAFT_STR
|
||||
NEW = TicketValues._NEW_INT, TicketValues._NEW_STR
|
||||
ASSIGNED = TicketValues._ASSIGNED_INT, TicketValues._ASSIGNED_STR
|
||||
ASSIGNED_PLANNING = TicketValues._ASSIGNED_PLANNING_INT, TicketValues._ASSIGNED_PLANNING_STR
|
||||
CLOSED = TicketValues._CLOSED_INT, TicketValues._CLOSED_STR
|
||||
INVALID = TicketValues._INVALID_INT, TicketValues._INVALID_STR
|
||||
|
||||
|
||||
class ProjectTask(models.IntegerChoices):
|
||||
|
||||
DRAFT = TicketValues._DRAFT_INT, TicketValues._DRAFT_STR
|
||||
NEW = TicketValues._NEW_INT, TicketValues._NEW_STR
|
||||
ASSIGNED = TicketValues._ASSIGNED_INT, TicketValues._ASSIGNED_STR
|
||||
ASSIGNED_PLANNING = TicketValues._ASSIGNED_PLANNING_INT, TicketValues._ASSIGNED_PLANNING_STR
|
||||
PENDING = TicketValues._PENDING_INT, TicketValues._PENDING_STR
|
||||
SOLVED = TicketValues._SOLVED_INT, TicketValues._SOLVED_STR
|
||||
CLOSED = TicketValues._CLOSED_INT, TicketValues._CLOSED_STR
|
||||
INVALID = TicketValues._INVALID_INT, TicketValues._INVALID_STR
|
||||
|
||||
|
||||
|
||||
|
||||
class TicketType(models.IntegerChoices):
|
||||
"""Type of the ticket"""
|
||||
|
||||
REQUEST = '1', 'Request'
|
||||
INCIDENT = '2', 'Incident'
|
||||
CHANGE = '3', 'Change'
|
||||
PROBLEM = '4', 'Problem'
|
||||
ISSUE = '5', 'Issue'
|
||||
MERGE_REQUEST = '6', 'Merge Request'
|
||||
PROJECT_TASK = '7', 'Project Task'
|
||||
|
||||
|
||||
|
||||
class TicketUrgency(models.IntegerChoices): # <null|github|gitlab>
|
||||
VERY_LOW = '1', 'Very Low'
|
||||
LOW = '2', 'Low'
|
||||
MEDIUM = '3', 'Medium'
|
||||
HIGH = '4', 'High'
|
||||
VERY_HIGH = '5', 'Very High'
|
||||
|
||||
|
||||
|
||||
class TicketImpact(models.IntegerChoices):
|
||||
VERY_LOW = '1', 'Very Low'
|
||||
LOW = '2', 'Low'
|
||||
MEDIUM = '3', 'Medium'
|
||||
HIGH = '4', 'High'
|
||||
VERY_HIGH = '5', 'Very High'
|
||||
|
||||
|
||||
|
||||
class TicketPriority(models.IntegerChoices):
|
||||
VERY_LOW = '1', 'Very Low'
|
||||
LOW = '2', 'Low'
|
||||
MEDIUM = '3', 'Medium'
|
||||
HIGH = '4', 'High'
|
||||
VERY_HIGH = '5', 'Very High'
|
||||
MAJOR = '6', 'Major'
|
||||
|
||||
|
||||
|
||||
def validation_ticket_type(field):
|
||||
|
||||
if not field:
|
||||
raise ValidationError('Ticket Type must be set')
|
||||
|
||||
|
||||
def validation_title(field):
|
||||
|
||||
if not field:
|
||||
raise ValueError
|
||||
|
||||
|
||||
model_notes = None
|
||||
|
||||
is_global = None
|
||||
|
||||
|
||||
status = models.IntegerField( # will require validation by ticket type as status for types will be different
|
||||
blank = False,
|
||||
choices=TicketStatus.Request,
|
||||
default = TicketStatus.Request.NEW,
|
||||
help_text = 'Status of ticket',
|
||||
# null=True,
|
||||
verbose_name = 'Status',
|
||||
)
|
||||
|
||||
# category = models.CharField(
|
||||
# blank = False,
|
||||
# help_text = "Category of the Ticket",
|
||||
# max_length = 50,
|
||||
# unique = True,
|
||||
# verbose_name = 'Category',
|
||||
# )
|
||||
|
||||
title = models.CharField(
|
||||
blank = False,
|
||||
help_text = "Title of the Ticket",
|
||||
max_length = 50,
|
||||
unique = True,
|
||||
verbose_name = 'Title',
|
||||
)
|
||||
|
||||
description = models.TextField(
|
||||
blank = False,
|
||||
default = None,
|
||||
help_text = 'Ticket Description',
|
||||
null = False,
|
||||
verbose_name = 'Description',
|
||||
) # text, markdown
|
||||
|
||||
|
||||
urgency = models.IntegerField(
|
||||
blank = True,
|
||||
choices=TicketUrgency,
|
||||
default=TicketUrgency.VERY_LOW,
|
||||
help_text = 'How urgent is this tickets resolution for the user?',
|
||||
null=True,
|
||||
verbose_name = 'Urgency',
|
||||
)
|
||||
|
||||
impact = models.IntegerField(
|
||||
blank = True,
|
||||
choices=TicketImpact,
|
||||
default=TicketImpact.VERY_LOW,
|
||||
help_text = 'End user assessed impact',
|
||||
null=True,
|
||||
verbose_name = 'Impact',
|
||||
)
|
||||
|
||||
priority = models.IntegerField(
|
||||
blank = True,
|
||||
choices=TicketPriority,
|
||||
default=TicketPriority.VERY_LOW,
|
||||
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 reference or null. i.e. github issue number
|
||||
|
||||
|
||||
external_system = models.IntegerField(
|
||||
blank = True,
|
||||
choices=Ticket_ExternalSystem,
|
||||
default=None,
|
||||
help_text = 'External system this item derives',
|
||||
null=True,
|
||||
verbose_name = 'External System',
|
||||
)
|
||||
|
||||
|
||||
ticket_type = models.IntegerField(
|
||||
blank = False,
|
||||
choices=TicketType,
|
||||
help_text = 'The type of ticket this is',
|
||||
validators = [ validation_ticket_type ],
|
||||
verbose_name = 'Type',
|
||||
)
|
||||
|
||||
|
||||
project = models.ForeignKey(
|
||||
Project,
|
||||
blank= True,
|
||||
help_text = 'Assign to a project',
|
||||
null = True,
|
||||
on_delete = models.DO_NOTHING,
|
||||
verbose_name = 'Project',
|
||||
)
|
||||
|
||||
|
||||
opened_by = models.ForeignKey(
|
||||
User,
|
||||
blank= False,
|
||||
help_text = 'Who is the ticket for',
|
||||
null = False,
|
||||
on_delete = models.DO_NOTHING,
|
||||
related_name = 'opened_by',
|
||||
verbose_name = 'Opened By',
|
||||
)
|
||||
|
||||
|
||||
subscribed_users = models.ManyToManyField(
|
||||
User,
|
||||
blank= True,
|
||||
help_text = 'Subscribe a User(s) to the ticket to receive updates',
|
||||
related_name = 'subscribed_users',
|
||||
symmetrical = False,
|
||||
verbose_name = 'Subscribed User(s)',
|
||||
)
|
||||
|
||||
|
||||
subscribed_teams = models.ManyToManyField(
|
||||
Team,
|
||||
blank= True,
|
||||
help_text = 'Subscribe a Team(s) to the ticket to receive updates',
|
||||
related_name = 'subscribed_teams',
|
||||
symmetrical = False,
|
||||
verbose_name = 'Subscribed Team(s)',
|
||||
)
|
||||
|
||||
assigned_users = models.ManyToManyField(
|
||||
User,
|
||||
blank= True,
|
||||
help_text = 'Assign the ticket to a User(s)',
|
||||
related_name = 'assigned_users',
|
||||
symmetrical = False,
|
||||
verbose_name = 'Assigned User(s)',
|
||||
)
|
||||
|
||||
assigned_teams = models.ManyToManyField(
|
||||
Team,
|
||||
blank= True,
|
||||
help_text = 'Assign the ticket to a Team(s)',
|
||||
related_name = 'assigned_teams',
|
||||
symmetrical = False,
|
||||
verbose_name = 'Assigned Team(s)',
|
||||
)
|
||||
|
||||
is_deleted = models.BooleanField(
|
||||
blank = False,
|
||||
default = False,
|
||||
help_text = 'Is the ticket deleted? And ready to be purged',
|
||||
null = False,
|
||||
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',
|
||||
)
|
||||
|
||||
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',
|
||||
)
|
||||
|
||||
|
||||
# ?? date_edit date of last edit
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return self.title
|
||||
|
||||
common_fields: list(str()) = [
|
||||
'organization',
|
||||
'title',
|
||||
'description',
|
||||
'opened_by',
|
||||
'ticket_type'
|
||||
]
|
||||
|
||||
common_itsm_fields: list(str()) = common_fields + [
|
||||
'urgency',
|
||||
|
||||
]
|
||||
|
||||
fields_itsm_request: list(str()) = common_itsm_fields + [
|
||||
|
||||
]
|
||||
|
||||
fields_itsm_incident: list(str()) = common_itsm_fields + [
|
||||
|
||||
]
|
||||
|
||||
fields_itsm_problem: list(str()) = common_itsm_fields + [
|
||||
|
||||
]
|
||||
|
||||
fields_itsm_change: list(str()) = common_itsm_fields + [
|
||||
|
||||
]
|
||||
|
||||
|
||||
common_git_fields: list(str()) = common_fields + [
|
||||
|
||||
]
|
||||
|
||||
fields_git_issue: list(str()) = common_fields + [
|
||||
|
||||
]
|
||||
|
||||
fields_git_merge_request: list(str()) = common_fields + [
|
||||
|
||||
]
|
||||
|
||||
fields_project_task: list(str()) = common_fields + [
|
||||
'category',
|
||||
'urgency',
|
||||
'status',
|
||||
'impact',
|
||||
'priority',
|
||||
'planned_start_date',
|
||||
'planned_finish_date',
|
||||
'real_start_date',
|
||||
'real_finish_date',
|
||||
]
|
||||
|
||||
tech_fields = [
|
||||
'category',
|
||||
'project',
|
||||
'assigned_users',
|
||||
'assigned_teams',
|
||||
'subscribed_teams',
|
||||
'subscribed_users',
|
||||
'status',
|
||||
'urgency',
|
||||
'impact',
|
||||
'priority',
|
||||
'planned_start_date',
|
||||
'planned_finish_date',
|
||||
]
|
||||
|
||||
|
||||
@property
|
||||
def markdown_description(self) -> str:
|
||||
|
||||
return self.render_markdown(self.description)
|
||||
|
||||
@property
|
||||
def related_tickets(self) -> list(dict()):
|
||||
|
||||
related_tickets: list() = []
|
||||
|
||||
query = RelatedTickets.objects.filter(
|
||||
Q(from_ticket_id=self.id)
|
||||
|
|
||||
Q(to_ticket_id=self.id)
|
||||
)
|
||||
|
||||
for related_ticket in query:
|
||||
|
||||
|
||||
how_related:str = str(related_ticket.get_how_related_display()).lower()
|
||||
|
||||
|
||||
if related_ticket.to_ticket_id_id == self.id:
|
||||
|
||||
if str(related_ticket.get_how_related_display()).lower() == 'blocks':
|
||||
|
||||
how_related = 'blocked by'
|
||||
|
||||
elif str(related_ticket.get_how_related_display()).lower() == 'blocked by':
|
||||
|
||||
how_related = 'blocks'
|
||||
|
||||
|
||||
related_tickets += [
|
||||
{
|
||||
'id': related_ticket.id,
|
||||
'type': related_ticket.to_ticket_id.get_ticket_type_display().lower(),
|
||||
'title': related_ticket.to_ticket_id.title,
|
||||
'how_related': how_related.replace(' ', '_'),
|
||||
'icon_filename': str('icons/ticket/ticket_' + how_related.replace(' ', '_') + '.svg')
|
||||
}
|
||||
]
|
||||
|
||||
return related_tickets
|
||||
|
||||
|
||||
@property
|
||||
def comments(self):
|
||||
|
||||
from core.models.ticket.ticket_comment import TicketComment
|
||||
|
||||
return TicketComment.objects.filter(
|
||||
ticket = self.id,
|
||||
parent = None,
|
||||
)
|
||||
|
||||
|
||||
|
||||
class RelatedTickets(TenancyObject):
|
||||
|
||||
class Meta:
|
||||
|
||||
ordering = [
|
||||
'id'
|
||||
]
|
||||
|
||||
class Related(models.IntegerChoices):
|
||||
RELATED = '1', 'Related'
|
||||
|
||||
BLOCKS = '2', 'Blocks'
|
||||
|
||||
BLOCKED_BY = '3', 'Blocked By'
|
||||
|
||||
is_global = None
|
||||
|
||||
model_notes = None
|
||||
|
||||
id = models.AutoField(
|
||||
blank=False,
|
||||
help_text = 'Ticket ID Number',
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
verbose_name = 'Number',
|
||||
)
|
||||
|
||||
from_ticket_id = models.ForeignKey(
|
||||
Ticket,
|
||||
blank= False,
|
||||
help_text = 'This Ticket',
|
||||
null = False,
|
||||
on_delete = models.CASCADE,
|
||||
related_name = 'from_ticket_id',
|
||||
verbose_name = 'Ticket',
|
||||
)
|
||||
|
||||
how_related = models.IntegerField(
|
||||
blank = False,
|
||||
choices = Related,
|
||||
help_text = 'How is the ticket related',
|
||||
verbose_name = 'How Related',
|
||||
)
|
||||
|
||||
to_ticket_id = models.ForeignKey(
|
||||
Ticket,
|
||||
blank= False,
|
||||
help_text = 'The Related Ticket',
|
||||
null = False,
|
||||
on_delete = models.CASCADE,
|
||||
related_name = 'to_ticket_id',
|
||||
verbose_name = 'Related Ticket',
|
||||
)
|
||||
|
388
app/core/models/ticket/ticket_comment.py
Normal file
@ -0,0 +1,388 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.forms import ValidationError
|
||||
|
||||
from access.fields import AutoCreatedField
|
||||
from access.models import TenancyObject, Team
|
||||
|
||||
from .markdown import TicketMarkdown
|
||||
from .ticket import Ticket
|
||||
|
||||
|
||||
|
||||
class TicketComment(
|
||||
TenancyObject,
|
||||
TicketMarkdown,
|
||||
):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
ordering = [
|
||||
'ticket',
|
||||
'parent_id'
|
||||
]
|
||||
|
||||
verbose_name = "Comment"
|
||||
|
||||
verbose_name_plural = "Comments"
|
||||
|
||||
|
||||
|
||||
class CommentSource(models.IntegerChoices):
|
||||
"""Source of the comment"""
|
||||
|
||||
DIRECT = '1', 'Direct'
|
||||
EMAIL = '2', 'E-Mail'
|
||||
HELPDESK = '3', 'Helpdesk'
|
||||
PHONE = '4', 'Phone'
|
||||
|
||||
|
||||
class CommentStatus(models.IntegerChoices):
|
||||
"""Comment Completion Status"""
|
||||
|
||||
TODO = '1', 'To Do'
|
||||
DONE = '2', 'Done'
|
||||
|
||||
|
||||
class CommentType(models.IntegerChoices):
|
||||
"""Type of Comment
|
||||
|
||||
Comment types are as follows:
|
||||
|
||||
- Action
|
||||
|
||||
- Comment
|
||||
|
||||
- Solution
|
||||
|
||||
- Notification
|
||||
|
||||
## Action
|
||||
|
||||
An action comment is for the tracking of what has occured to the ticket.
|
||||
|
||||
## Comment
|
||||
|
||||
This is the default comment type and is what would be normally used.
|
||||
|
||||
## Solution
|
||||
|
||||
This type of comment is an ITSM comment and is used as the means for solving the ticket.\
|
||||
|
||||
## Notification
|
||||
|
||||
This type of comment is intended to be used to send a notification to subscribed users.
|
||||
"""
|
||||
|
||||
ACTION = '1', 'Action'
|
||||
COMMENT = '2', 'Comment'
|
||||
TASK = '3', 'Task'
|
||||
NOTIFICATION = '4', 'Notification'
|
||||
SOLUTION = '5', 'Solution'
|
||||
|
||||
|
||||
def validation_comment_type(field):
|
||||
|
||||
if not field:
|
||||
raise ValidationError('Comment Type must be set')
|
||||
|
||||
|
||||
def validation_ticket_id(field):
|
||||
|
||||
if not field:
|
||||
raise ValidationError('Ticket ID is required')
|
||||
|
||||
|
||||
model_notes = None
|
||||
|
||||
is_global = None
|
||||
|
||||
|
||||
id = models.AutoField(
|
||||
blank=False,
|
||||
help_text = 'Comment ID Number',
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
verbose_name = 'Number',
|
||||
)
|
||||
|
||||
parent = models.ForeignKey(
|
||||
'self',
|
||||
blank= True,
|
||||
default = None,
|
||||
help_text = 'Parent ID for creating discussion threads',
|
||||
null = True,
|
||||
on_delete = models.DO_NOTHING,
|
||||
verbose_name = 'Parent Comment',
|
||||
)
|
||||
|
||||
ticket = models.ForeignKey(
|
||||
Ticket,
|
||||
blank= True,
|
||||
default = None,
|
||||
help_text = 'Parent ID for creating discussion threads',
|
||||
null = True,
|
||||
on_delete = models.CASCADE,
|
||||
validators = [ validation_ticket_id ],
|
||||
verbose_name = 'Parent Comment',
|
||||
)
|
||||
|
||||
comment_type = models.IntegerField(
|
||||
blank = False,
|
||||
choices =CommentType,
|
||||
default = CommentType.COMMENT,
|
||||
help_text = 'The type of comment this is',
|
||||
validators = [ validation_comment_type ],
|
||||
verbose_name = 'Type',
|
||||
)
|
||||
|
||||
body = models.TextField(
|
||||
blank = False,
|
||||
default = None,
|
||||
help_text = 'Comment contents',
|
||||
null = False,
|
||||
verbose_name = 'Comment',
|
||||
)
|
||||
|
||||
created = AutoCreatedField()
|
||||
|
||||
modified = AutoCreatedField()
|
||||
|
||||
private = models.BooleanField(
|
||||
blank = False,
|
||||
default = False,
|
||||
help_text = 'Is this comment private',
|
||||
null = False,
|
||||
verbose_name = 'Private',
|
||||
)
|
||||
|
||||
duration = models.IntegerField(
|
||||
blank = False,
|
||||
default = 0,
|
||||
help_text = 'Time spent in seconds',
|
||||
null = False,
|
||||
verbose_name = 'Duration',
|
||||
)
|
||||
|
||||
|
||||
# category = models.CharField(
|
||||
# blank = False,
|
||||
# help_text = "Category of the Ticket",
|
||||
# max_length = 50,
|
||||
# unique = True,
|
||||
# verbose_name = 'Category',
|
||||
# )
|
||||
|
||||
template = models.ForeignKey(
|
||||
'self',
|
||||
blank= True,
|
||||
default = None,
|
||||
help_text = 'Comment Template to use',
|
||||
null = True,
|
||||
on_delete = models.DO_NOTHING,
|
||||
related_name = 'comment_template',
|
||||
verbose_name = 'Template',
|
||||
)
|
||||
|
||||
is_template = models.BooleanField(
|
||||
blank = False,
|
||||
default = False,
|
||||
help_text = 'Is this comment a template',
|
||||
null = False,
|
||||
verbose_name = 'Template',
|
||||
)
|
||||
|
||||
source = models.IntegerField(
|
||||
blank = False,
|
||||
choices =CommentSource,
|
||||
default = CommentSource.DIRECT,
|
||||
help_text = 'Origin type for this comment',
|
||||
# validators = [ validation_ticket_type ],
|
||||
verbose_name = 'Source',
|
||||
)
|
||||
|
||||
status = models.IntegerField( # will require validation by comment type as status for types will be different
|
||||
blank = False,
|
||||
choices=CommentStatus,
|
||||
default = CommentStatus.TODO,
|
||||
help_text = 'Status of comment',
|
||||
# null=True,
|
||||
verbose_name = 'Status',
|
||||
)
|
||||
|
||||
responsible_user = models.ForeignKey(
|
||||
User,
|
||||
blank= True,
|
||||
default = None,
|
||||
help_text = 'User whom is responsible for the completion of comment',
|
||||
on_delete = models.DO_NOTHING,
|
||||
related_name = 'comment_responsible_user',
|
||||
null = True,
|
||||
verbose_name = 'Responsible User',
|
||||
)
|
||||
|
||||
responsible_team = models.ForeignKey(
|
||||
Team,
|
||||
blank= True,
|
||||
default = None,
|
||||
help_text = 'Team whom is responsible for the completion of comment',
|
||||
on_delete = models.DO_NOTHING,
|
||||
related_name = 'comment_responsible_team',
|
||||
null = True,
|
||||
verbose_name = 'Responsible Team',
|
||||
)
|
||||
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
blank= False,
|
||||
help_text = 'Who made the comment',
|
||||
null = False,
|
||||
on_delete = models.DO_NOTHING,
|
||||
related_name = 'comment_user',
|
||||
verbose_name = 'User',
|
||||
)
|
||||
|
||||
date_closed = models.DateTimeField(
|
||||
blank = True,
|
||||
help_text = 'Date ticket closed',
|
||||
null = True,
|
||||
verbose_name = 'Closed Date',
|
||||
)
|
||||
|
||||
planned_start_date = models.DateTimeField(
|
||||
blank = True,
|
||||
help_text = 'Planned start date.',
|
||||
null = True,
|
||||
verbose_name = 'Planned Start Date',
|
||||
)
|
||||
|
||||
planned_finish_date = models.DateTimeField(
|
||||
blank = True,
|
||||
help_text = 'Planned finish date',
|
||||
null = True,
|
||||
verbose_name = 'Planned Finish Date',
|
||||
)
|
||||
|
||||
real_start_date = models.DateTimeField(
|
||||
blank = True,
|
||||
help_text = 'Real start date',
|
||||
null = True,
|
||||
verbose_name = 'Real Start Date',
|
||||
)
|
||||
|
||||
real_finish_date = models.DateTimeField(
|
||||
blank = True,
|
||||
help_text = 'Real finish date',
|
||||
null = True,
|
||||
verbose_name = 'Real Finish Date',
|
||||
)
|
||||
|
||||
|
||||
common_fields: list(str()) = [
|
||||
'body',
|
||||
'duration',
|
||||
'user',
|
||||
'ticket',
|
||||
'parent',
|
||||
'comment_type',
|
||||
]
|
||||
|
||||
common_itsm_fields: list(str()) = common_fields + [
|
||||
'category',
|
||||
'source',
|
||||
'template',
|
||||
|
||||
]
|
||||
|
||||
fields_itsm_task: list(str()) = common_itsm_fields + [
|
||||
'status',
|
||||
'responsible_user',
|
||||
'responsible_team',
|
||||
'planned_start_date',
|
||||
'planned_finish_date',
|
||||
'real_start_date',
|
||||
'real_finish_date',
|
||||
]
|
||||
|
||||
fields_itsm_notification: list(str()) = common_itsm_fields + [
|
||||
'status',
|
||||
'responsible_user',
|
||||
'responsible_team',
|
||||
'planned_start_date',
|
||||
'planned_finish_date',
|
||||
'real_start_date',
|
||||
'real_finish_date',
|
||||
]
|
||||
|
||||
fields_itsm_incident: list(str()) = common_itsm_fields + [
|
||||
|
||||
]
|
||||
|
||||
fields_itsm_problem: list(str()) = common_itsm_fields + [
|
||||
|
||||
]
|
||||
|
||||
fields_itsm_change: list(str()) = common_itsm_fields + [
|
||||
|
||||
]
|
||||
|
||||
|
||||
common_git_fields: list(str()) = common_fields + [
|
||||
|
||||
]
|
||||
|
||||
fields_git_issue: list(str()) = common_fields + [
|
||||
|
||||
]
|
||||
|
||||
fields_git_merge_request: list(str()) = common_fields + [
|
||||
|
||||
]
|
||||
|
||||
fields_project_task: list(str()) = common_fields + [
|
||||
'category',
|
||||
'urgency',
|
||||
'status',
|
||||
'impact',
|
||||
'priority',
|
||||
'planned_start_date',
|
||||
'planned_finish_date',
|
||||
'real_start_date',
|
||||
'real_finish_date',
|
||||
]
|
||||
|
||||
fields_comment_task: list(str()) = common_itsm_fields + [
|
||||
'status',
|
||||
'responsible_user',
|
||||
'responsible_team',
|
||||
'planned_start_date',
|
||||
'planned_finish_date',
|
||||
'real_start_date',
|
||||
'real_finish_date',
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
@property
|
||||
def markdown_description(self) -> str:
|
||||
|
||||
return self.render_markdown(self.description)
|
||||
|
||||
@property
|
||||
def comment_template_queryset(self):
|
||||
|
||||
query = TicketComment.objects.filter(
|
||||
is_template = True,
|
||||
comment_type = self.comment_type,
|
||||
)
|
||||
|
||||
return query
|
||||
|
||||
@property
|
||||
def threads(self):
|
||||
|
||||
return TicketComment.objects.filter(
|
||||
parent = self.id
|
||||
)
|
142
app/core/templates/core/ticket.html.j2
Normal file
@ -0,0 +1,142 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block additional-stylesheet %}
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'ticketing.css' %}">
|
||||
{% endblock additional-stylesheet %}
|
||||
|
||||
{% load tickets %}
|
||||
|
||||
{% block article %}
|
||||
|
||||
<div id="ticket-content">
|
||||
|
||||
<div id="ticket-data">
|
||||
|
||||
<div id="ticket-description">
|
||||
<div style="/*background-color:yellow;*/ align-items:center; height: 30px; padding: 0px;">
|
||||
<input style="float: right; position: relative; margin: 0px;" type="button" value="Edit" onclick="window.location='{{ edit_url }}';">
|
||||
</div>
|
||||
<div id="markdown" style="padding: 0px; margin: 0px">{{ ticket.description | markdown | safe }}</div>
|
||||
</div>
|
||||
|
||||
<div id="ticket-additional-data">
|
||||
|
||||
<div id="data-block">
|
||||
<h3>
|
||||
<div id="text">Related Tickets</div>
|
||||
<div id="icons">
|
||||
<a href="{% url '_ticket_related_add' ticket_type=ticket_type ticket_id=ticket.id %}">{% include 'icons/ticket/add.svg' %}</a>
|
||||
</div>
|
||||
</h3>
|
||||
|
||||
{% if ticket.related_tickets %}
|
||||
{% for related_ticket in ticket.related_tickets %}
|
||||
<div id="linked-tickets">
|
||||
<div class="icon icon-{{ related_ticket.how_related }}">{% include related_ticket.icon_filename %}</div>
|
||||
<div class="ticket">
|
||||
{% if related_ticket.how_related == 'blocked_by' %}
|
||||
Blocked by
|
||||
{% elif related_ticket.how_related == 'blocks' %}
|
||||
Blocks
|
||||
{% elif related_ticket.how_related == 'related' %}
|
||||
Related to
|
||||
{% endif %}
|
||||
|
||||
{{ related_ticket.title }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div>Nothing Found</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div id="data-block" class="linked-item">
|
||||
<h3>
|
||||
<div id="text">Linked Items</div>
|
||||
<div id="icons">{% include 'icons/place-holder.svg' %}{% include 'icons/place-holder.svg' %}</div>
|
||||
</h3>
|
||||
<div id="item">
|
||||
An item
|
||||
</div>
|
||||
<div id="item">
|
||||
another item
|
||||
</div>
|
||||
<div id="item">
|
||||
another item
|
||||
</div>
|
||||
<div id="item">
|
||||
another item
|
||||
</div>
|
||||
<div id="item">
|
||||
another item
|
||||
</div>
|
||||
<div id="item">
|
||||
another item
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% include 'core/ticket/comment.html.j2' %}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div id="ticket-meta">
|
||||
<h3 class="{{ ticket_type }}-ticket">{{ ticket_type }}</h3>
|
||||
|
||||
<fieldset>
|
||||
<label>Assigned</label>
|
||||
<span>
|
||||
{% if ticket.assigned_users %}
|
||||
{% for user in ticket.assigned_users.all %}
|
||||
{{ user }}
|
||||
{% endfor%}
|
||||
{% endif %}
|
||||
{% if ticket.assigned_teams %}
|
||||
{% for team in ticket.assigned_teams.all %}
|
||||
{{ team }}
|
||||
{% endfor%}
|
||||
{% endif %}
|
||||
</span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label>Status</label>
|
||||
<span>{{ticket.get_status_display }}</span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label>Labels</label>
|
||||
<span>val</span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label>Project</label>
|
||||
<span>
|
||||
{% if ticket.project %}
|
||||
<a href="{% url 'Project Management:_project_view' pk=ticket.project_id %}">{{ ticket.project }}</a>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label>Priority</label>
|
||||
<span>U{{ ticket.get_urgency_display }} / I{{ ticket.get_impact_display }} / P{{ ticket.get_priority_display }}</span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label>Milestone</label>
|
||||
<span>val</span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label>Roadmap(s)</label>
|
||||
<span>val</span>
|
||||
</fieldset>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
55
app/core/templates/core/ticket/comment.html.j2
Normal file
@ -0,0 +1,55 @@
|
||||
|
||||
|
||||
<div id="ticket-comments">
|
||||
<ul>
|
||||
<li>John smith add x as related to this ticket</li>
|
||||
|
||||
{% for comment in ticket.comments %}
|
||||
|
||||
<li>
|
||||
{% include 'core/ticket/comment/comment.html.j2' %}
|
||||
|
||||
{% if comment.threads %}
|
||||
<div id="discussion" style="/*background-color: #fff;*/">
|
||||
<h4 style="display: flex; padding-left: 5px;">
|
||||
Replies
|
||||
{% include 'icons/ticket/expanded.svg' %}
|
||||
</h4>
|
||||
</div>
|
||||
<div style="padding-left: 40px; border-left: 1px solid #177fe66e; border-bottom: 1px solid #177fe66e;">
|
||||
{% if comment.threads %}
|
||||
{% for thread in comment.threads %}
|
||||
|
||||
|
||||
{% include 'core/ticket/comment/comment.html.j2' with comment=thread %}
|
||||
|
||||
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
<div >
|
||||
<div style="padding: 10px; padding-top: 10px">
|
||||
<input type="button" value="Comment" onclick="window.location='{% url 'Assistance:_ticket_comment_request_reply_add' ticket_type ticket.id comment.id %}?comment_type=comment';">
|
||||
<input type="button" value="Task" onclick="window.location='{% url 'Assistance:_ticket_comment_request_reply_add' ticket_type ticket.id comment.id %}?comment_type=task';">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</li>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
<li class="mention">Jane smith mentioned this ticket in xx</li>
|
||||
<li class="mention">sdasfdgdfgdfg dfg dfg dfg d</li>
|
||||
</ul>
|
||||
|
||||
<div id="comment" style="padding: 20px;">
|
||||
|
||||
<input type="button" value="Comment" onclick="window.location='{% url 'Assistance:_ticket_comment_request_add' ticket_type ticket.id %}?comment_type=comment';">
|
||||
<input type="button" value="Task" onclick="window.location='{% url 'Assistance:_ticket_comment_request_add' ticket_type ticket.id %}?comment_type=task';">
|
||||
<input type="button" value="Notification" onclick="window.location='{% url 'Assistance:_ticket_comment_request_add' ticket_type ticket.id %}?comment_type=notification';">
|
||||
<input type="button" value="Resolve" onclick="window.location='{% url 'Assistance:_ticket_comment_request_add' ticket_type ticket.id %}?comment_type=solution';">
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
117
app/core/templates/core/ticket/comment/comment.html.j2
Normal file
@ -0,0 +1,117 @@
|
||||
{% load tickets %}
|
||||
|
||||
{% if comment %}
|
||||
|
||||
{% if comment.get_comment_type_display == 'Action' %}
|
||||
|
||||
{{ comment.body | markdown | safe }}
|
||||
|
||||
{% elif comment.get_comment_type_display == 'Comment' or comment.get_comment_type_display == 'Task' or comment.get_comment_type_display == 'Notification' or comment.get_comment_type_display == 'Solution' %}
|
||||
<div id="comment" class="comment-type-default comment-type-{{ comment.get_comment_type_display }}">
|
||||
|
||||
<h4>
|
||||
<div id="text">
|
||||
{{ comment.user }}
|
||||
{% if comment.get_comment_type_display == 'Task' %}
|
||||
created a task
|
||||
{% elif comment.get_comment_type_display == 'Solution' %}
|
||||
solved
|
||||
{% else %}
|
||||
wrote
|
||||
{% endif %}
|
||||
on {{ comment.created }}
|
||||
</div>
|
||||
<div id="icons">
|
||||
{%if not comment.parent_id %}
|
||||
<a title="Reply with Comment" href="{% url 'Assistance:_ticket_comment_request_reply_add' ticket_type ticket.id comment.id %}?comment_type=comment">
|
||||
{% include 'icons/ticket/reply.svg' %}
|
||||
</a>
|
||||
<a title="Reply with Task" href="{% url 'Assistance:_ticket_comment_request_reply_add' ticket_type ticket.id comment.id %}?comment_type=task">
|
||||
{% include 'icons/ticket/task.svg' %}
|
||||
</a>
|
||||
<a title="Reply with Notification" href="{% url 'Assistance:_ticket_comment_request_reply_add' ticket_type ticket.id comment.id %}?comment_type=notification">
|
||||
{% include 'icons/ticket/notification.svg' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a title="Edit Comment" href="{% url 'Assistance:_ticket_comment_request_change' ticket_type ticket.id comment.id %}">
|
||||
{% include 'icons/ticket/edit.svg' %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</h4>
|
||||
|
||||
<div style="line-height:30px;">
|
||||
{% if comment.get_comment_type_display != 'Notification' %}
|
||||
<fieldset>
|
||||
<label>Source</label>
|
||||
<span>{{ comment.get_source_display }}</span>
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
{% if comment.get_comment_type_display == 'Task' or comment.get_comment_type_display == 'Notification' %}
|
||||
<fieldset>
|
||||
<label>Status</label>
|
||||
<span>{{ comment.get_status_display }}</span>
|
||||
</fieldset>
|
||||
{% if comment.get_comment_type_display == 'Task' %}
|
||||
<fieldset>
|
||||
<label>Responsible User</label>
|
||||
<span>{{ comment.responsible_user }}</span>
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
<fieldset>
|
||||
<label>
|
||||
{% if comment.get_comment_type_display == 'Task' %}
|
||||
Responsible Team
|
||||
{% elif comment.get_comment_type_display == 'Notification' %}
|
||||
Notify Team
|
||||
{% endif %}
|
||||
</label>
|
||||
<span>{{ comment.responsible_team }}</span>
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
<fieldset>
|
||||
<label>Category</label>
|
||||
<span>{{ comment.category }}</span>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div id="markdown" style="margin: 15px; padding: 10px; background-color: #fff;">
|
||||
{{ comment.body | markdown | safe }}
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
{% if comment.get_comment_type_display == 'Task' or comment.get_comment_type_display == 'Notification' %}
|
||||
<fieldset>
|
||||
<label>Planned Start</label>
|
||||
<span>{{ comment.planned_start_date }}</span>
|
||||
</fieldset>
|
||||
{% if comment.get_comment_type_display == 'Task' %}
|
||||
<fieldset>
|
||||
<label>Planned Finish</label>
|
||||
<span>{{ comment.planned_finish_date }}</span>
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
<fieldset>
|
||||
<label>Actual Start</label>
|
||||
<span>{{ comment.real_start_date }}</span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label>Actual Finish</label>
|
||||
<span>{{ comment.real_finish_date }}</span>
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
<fieldset>
|
||||
<label>Duration</label>
|
||||
<span>{{ comment.duration }}</span>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
55
app/core/templates/core/ticket/index.html.j2
Normal file
@ -0,0 +1,55 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block content %}
|
||||
<input type="button" value="New Ticket" onclick="window.location='{{ new_ticket_url }}';">
|
||||
|
||||
<style>
|
||||
|
||||
#status-icon {
|
||||
margin: 0px;
|
||||
|
||||
}
|
||||
|
||||
#status-icon svg{
|
||||
width: 22px;
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<table style="max-width: 100%;">
|
||||
<thead>
|
||||
<th> </th>
|
||||
<th>ID</th>
|
||||
<th>Title</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
</thead>
|
||||
{% for ticket in tickets %}
|
||||
<tr class="clicker">
|
||||
<td id="status-icon">
|
||||
|
||||
</td>
|
||||
<td>{{ ticket.id }}</td>
|
||||
<td>
|
||||
{% if ticket_type == 'change' %}
|
||||
<a href="{% url 'ITIM:_ticket_change_view' ticket_type='change' pk=ticket.id %}">
|
||||
{% elif ticket_type == 'incident' %}
|
||||
<a href="{% url 'ITIM:_ticket_incident_view' ticket_type='incident' pk=ticket.id %}">
|
||||
{% elif ticket_type == 'problem' %}
|
||||
<a href="{% url 'ITIM:_ticket_problem_view' ticket_type='problem' pk=ticket.id %}">
|
||||
{% elif ticket_type == 'request' %}
|
||||
<a href="{% url 'Assistance:_ticket_request_view' ticket_type='request' pk=ticket.id %}">
|
||||
{% else %}
|
||||
<a href=""></a>
|
||||
{% endif %}
|
||||
{{ ticket.title }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ ticket.get_status_display }}</td>
|
||||
<td>{{ ticket.created }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
1
app/core/templates/icons/ticket/add.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M440-280h80v-160h160v-80H520v-160h-80v160H280v80h160v160Zm40 200q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
|
After Width: | Height: | Size: 425 B |
1
app/core/templates/icons/ticket/edit.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M200-200h57l391-391-57-57-391 391v57Zm-80 80v-170l528-527q12-11 26.5-17t30.5-6q16 0 31 6t26 18l55 56q12 11 17.5 26t5.5 30q0 16-5.5 30.5T817-647L290-120H120Zm640-584-56-56 56 56Zm-141 85-28-29 57 57-29-28Z"/></svg>
|
After Width: | Height: | Size: 287 B |
1
app/core/templates/icons/ticket/expanded.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M480-345 240-585l56-56 184 183 184-183 56 56-240 240Z"/></svg>
|
After Width: | Height: | Size: 136 B |
1
app/core/templates/icons/ticket/notification.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M448-201.33h66.67V-391l76 76 46.66-47L480-516.67l-156 156L371-314l77-77v189.67ZM226.67-80q-27 0-46.84-19.83Q160-119.67 160-146.67v-666.66q0-27 19.83-46.84Q199.67-880 226.67-880H574l226 226v507.33q0 27-19.83 46.84Q760.33-80 733.33-80H226.67Zm314-542.67v-190.66h-314v666.66h506.66v-476H540.67Zm-314-190.66v190.66-190.66 666.66-666.66Z"/></svg>
|
After Width: | Height: | Size: 415 B |
1
app/core/templates/icons/ticket/reply.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M760-200v-160q0-50-35-85t-85-35H273l144 144-57 56-240-240 240-240 57 56-144 144h367q83 0 141.5 58.5T840-360v160h-80Z"/></svg>
|
After Width: | Height: | Size: 199 B |
1
app/core/templates/icons/ticket/task.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m435.33-250 228-228L618-523.33l-183 183L338.33-437l-45 45 142 142ZM226.67-80q-27 0-46.84-19.83Q160-119.67 160-146.67v-666.66q0-27 19.83-46.84Q199.67-880 226.67-880H574l226 226v507.33q0 27-19.83 46.84Q760.33-80 733.33-80H226.67Zm314-542.67v-190.66h-314v666.66h506.66v-476H540.67Zm-314-190.66v190.66-190.66 666.66-666.66Z"/></svg>
|
After Width: | Height: | Size: 402 B |
1
app/core/templates/icons/ticket/ticket_blocked_by.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M320-320h320v-320H320v320ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
|
After Width: | Height: | Size: 394 B |
1
app/core/templates/icons/ticket/ticket_blocks.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M360-320h80v-320h-80v320Zm160 0h80v-320h-80v320ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
|
After Width: | Height: | Size: 416 B |
1
app/core/templates/icons/ticket/ticket_related.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M240-400h80q0-59 43-99.5T466-540q36 0 67 16.5t51 43.5h-64v80h200v-200h-80v62q-32-38-76.5-60T466-620q-95 0-160.5 64T240-400ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
|
After Width: | Height: | Size: 491 B |
@ -9,4 +9,4 @@ register = template.Library()
|
||||
@register.filter()
|
||||
@stringfilter
|
||||
def markdown(value):
|
||||
return md.markdown(value, extensions=['markdown.extensions.fenced_code', 'codehilite'])
|
||||
return md.markdown(value, extensions=['markdown.extensions.fenced_code', 'codehilite'])
|
||||
|
13
app/core/templatetags/tickets.py
Normal file
@ -0,0 +1,13 @@
|
||||
from django import template
|
||||
from django.template.defaultfilters import stringfilter
|
||||
|
||||
import markdown as md
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter()
|
||||
@stringfilter
|
||||
def markdown(value):
|
||||
|
||||
return md.markdown(value, extensions=['markdown.extensions.fenced_code', 'codehilite'])
|
63
app/core/tests/unit/ticket/test_ticket_common.py
Normal file
@ -0,0 +1,63 @@
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from app.tests.abstract.models import ModelDisplay, ModelIndex
|
||||
|
||||
|
||||
|
||||
class TicketCommon(
|
||||
TestCase
|
||||
):
|
||||
|
||||
def text_ticket_field_type_opened_by(self):
|
||||
"""Ensure field is of a certain type
|
||||
|
||||
opened_by_field must be of type int
|
||||
"""
|
||||
pass
|
||||
|
||||
def text_ticket_field_value_not_null_opened_by(self):
|
||||
"""Ensure field is not null
|
||||
|
||||
opened_by_field must be set and not null
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def text_ticket_field_value_auto_set_opened_by(self):
|
||||
"""Ensure field is auto set within code
|
||||
|
||||
opened_by_field must be set by code with non-tech user not being able to change
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def text_ticket_field_value_tech_set_opened_by(self):
|
||||
"""Ensure field can be set by a technician
|
||||
|
||||
opened_by_field can be set by a technician
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
def text_ticket_type_fields(self):
|
||||
"""Placeholder test
|
||||
|
||||
following tests to be written:
|
||||
|
||||
- only tech can change tech fields (same org)
|
||||
- non-tech cant see tech fields (same org) during creation
|
||||
- non-tech cant change tech fields (same org)
|
||||
- only tech can change tech fields (different org)
|
||||
- non-tech cant see tech fields (different org) during creation
|
||||
- non-tech cant change tech fields (different org)
|
||||
|
||||
- itsm ticket has the itsm related fields
|
||||
- non-itsm ticket does not have any itsm related fields
|
||||
|
||||
"""
|
||||
pass
|
@ -0,0 +1,25 @@
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from app.tests.abstract.models import ModelDisplay, ModelIndex
|
||||
|
||||
from core.models.ticket.ticket_comment import TicketComment
|
||||
|
||||
|
||||
|
||||
class TicketCommentCommon(
|
||||
TestCase
|
||||
):
|
||||
|
||||
model = TicketComment
|
||||
|
||||
|
||||
def text_ticket_field_type_opened_by(self):
|
||||
"""Replies to comments only to occur on primary comment
|
||||
|
||||
If a comment has a 'parent_id' set, ensure the comment can't be replied to
|
||||
"""
|
||||
pass
|
81
app/core/views/related_ticket.py
Normal file
@ -0,0 +1,81 @@
|
||||
import markdown
|
||||
|
||||
from django.urls import reverse
|
||||
from django.views import generic
|
||||
|
||||
from django_celery_results.models import TaskResult
|
||||
|
||||
from access.mixin import OrganizationPermission
|
||||
|
||||
from core.forms.related_ticket import RelatedTicketForm
|
||||
from core.models.ticket.ticket import RelatedTickets
|
||||
from core.views.common import AddView, ChangeView, DeleteView, IndexView
|
||||
|
||||
from settings.models.user_settings import UserSettings
|
||||
|
||||
|
||||
|
||||
class Add(AddView):
|
||||
|
||||
form_class = RelatedTicketForm
|
||||
|
||||
model = RelatedTickets
|
||||
|
||||
permission_required = [
|
||||
'itam.add_device',
|
||||
]
|
||||
|
||||
template_name = 'form.html.j2'
|
||||
|
||||
|
||||
def get_initial(self):
|
||||
|
||||
initial_values: dict = {
|
||||
'organization': UserSettings.objects.get(user = self.request.user).default_organization,
|
||||
'from_ticket_id': self.kwargs['ticket_id'],
|
||||
}
|
||||
|
||||
return initial_values
|
||||
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
|
||||
if self.kwargs['ticket_type'] == 'request':
|
||||
|
||||
return reverse('Assistance:_ticket_request_view', args=(self.kwargs['ticket_type'],self.object.id,))
|
||||
|
||||
else:
|
||||
|
||||
return reverse('ITIM:_ticket_' + str(self.kwargs['ticket_type']).lower() + '_view', args=(self.kwargs['ticket_type'],self.kwargs['ticket_id'],))
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['content_title'] = 'Ticket Comment'
|
||||
|
||||
return context
|
||||
|
||||
|
||||
|
||||
class Delete(DeleteView):
|
||||
|
||||
model = RelatedTickets
|
||||
|
||||
permission_required = [
|
||||
'itim.delete_cluster',
|
||||
]
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['content_title'] = 'Delete ' + str(self.object)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
|
||||
return reverse('ITIM:Clusters')
|
188
app/core/views/ticket.py
Normal file
@ -0,0 +1,188 @@
|
||||
import markdown
|
||||
|
||||
from django.http import Http404
|
||||
from django.urls import reverse
|
||||
from django.views import generic
|
||||
|
||||
from django_celery_results.models import TaskResult
|
||||
|
||||
from access.mixin import OrganizationPermission
|
||||
|
||||
from core.forms.ticket import DetailForm, TicketForm
|
||||
from core.models.ticket.ticket import Ticket
|
||||
from core.views.common import AddView, ChangeView, DeleteView, IndexView
|
||||
|
||||
from settings.models.user_settings import UserSettings
|
||||
|
||||
|
||||
|
||||
class Add(AddView):
|
||||
|
||||
form_class = TicketForm
|
||||
|
||||
model = Ticket
|
||||
permission_required = [
|
||||
'itam.add_device',
|
||||
]
|
||||
template_name = 'form.html.j2'
|
||||
|
||||
|
||||
def get_initial(self):
|
||||
return {
|
||||
'organization': UserSettings.objects.get(user = self.request.user).default_organization,
|
||||
'type_ticket': self.kwargs['ticket_type'],
|
||||
}
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.is_global = False
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
|
||||
if self.kwargs['ticket_type'] == 'request':
|
||||
|
||||
return reverse('Assistance:_ticket_request_view', args=(self.kwargs['ticket_type'],self.object.id,))
|
||||
|
||||
else:
|
||||
|
||||
return reverse('ITIM:_ticket_' + str(self.kwargs['ticket_type']).lower() + '_view', args=(self.kwargs['ticket_type'],self.object.id,))
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['content_title'] = 'New Ticket'
|
||||
|
||||
return context
|
||||
|
||||
|
||||
|
||||
class Change(ChangeView):
|
||||
|
||||
form_class = TicketForm
|
||||
|
||||
model = Ticket
|
||||
|
||||
permission_required = [
|
||||
'itim.change_cluster',
|
||||
]
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['content_title'] = str(self.object)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def get_initial(self):
|
||||
return {
|
||||
'type_ticket': self.kwargs['ticket_type'],
|
||||
}
|
||||
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
|
||||
return reverse('Assistance:_ticket_request_view', args=(self.kwargs['ticket_type'], self.kwargs['pk'],))
|
||||
|
||||
|
||||
|
||||
class Index(OrganizationPermission, generic.ListView):
|
||||
|
||||
context_object_name = "tickets"
|
||||
|
||||
fields = [
|
||||
"id",
|
||||
'title',
|
||||
'status',
|
||||
'date_created',
|
||||
]
|
||||
|
||||
model = Ticket
|
||||
|
||||
permission_required = [
|
||||
'django_celery_results.view_taskresult',
|
||||
]
|
||||
|
||||
template_name = 'core/ticket/index.html.j2'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
if self.kwargs['ticket_type'] == 'request':
|
||||
|
||||
context['new_ticket_url'] = reverse('Assistance:_ticket_request_add', args=(self.kwargs['ticket_type'],))
|
||||
|
||||
else:
|
||||
|
||||
context['new_ticket_url'] = reverse(str('ITIM:_ticket_' + self.kwargs['ticket_type'] + '_add'), args=(self.kwargs['ticket_type'],))
|
||||
|
||||
|
||||
context['ticket_type'] = self.kwargs['ticket_type']
|
||||
|
||||
context['content_title'] = 'Tickets'
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
if not hasattr(Ticket.TicketType, str(self.kwargs['ticket_type']).upper()):
|
||||
raise Http404
|
||||
|
||||
queryset = super().get_queryset()
|
||||
|
||||
queryset = queryset.filter(
|
||||
ticket_type = Ticket.TicketType[str(self.kwargs['ticket_type']).upper()]
|
||||
)
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
|
||||
return reverse('Settings:_device_model_view', args=(self.kwargs['pk'],))
|
||||
|
||||
|
||||
|
||||
class View(ChangeView):
|
||||
|
||||
model = Ticket
|
||||
|
||||
permission_required = [
|
||||
'itam.view_device',
|
||||
]
|
||||
|
||||
template_name = 'core/ticket.html.j2'
|
||||
|
||||
form_class = DetailForm
|
||||
|
||||
context_object_name = "ticket"
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
context['ticket_type'] = self.kwargs['ticket_type']
|
||||
|
||||
context['model_pk'] = self.kwargs['pk']
|
||||
context['model_name'] = self.model._meta.verbose_name.replace(' ', '')
|
||||
|
||||
# context['model_delete_url'] = reverse('ITAM:_device_delete', args=(self.kwargs['pk'],))
|
||||
|
||||
context['edit_url'] = reverse('Assistance:_ticket_request_change', args=(self.kwargs['ticket_type'], self.kwargs['pk'])) #/assistance/ticket/{{ ticket_type }}/{{ ticket.id }}
|
||||
|
||||
context['content_title'] = self.object.title
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def get_initial(self):
|
||||
return {
|
||||
'type_ticket': self.kwargs['ticket_type'],
|
||||
}
|
97
app/core/views/ticket_comment.py
Normal file
@ -0,0 +1,97 @@
|
||||
import markdown
|
||||
|
||||
from django.urls import reverse
|
||||
from django.views import generic
|
||||
|
||||
from django_celery_results.models import TaskResult
|
||||
|
||||
from access.mixin import OrganizationPermission
|
||||
|
||||
from core.forms.ticket_comment import CommentForm, DetailForm
|
||||
from core.models.ticket.ticket_comment import TicketComment
|
||||
from core.views.common import AddView, ChangeView, DeleteView, IndexView
|
||||
|
||||
from settings.models.user_settings import UserSettings
|
||||
|
||||
|
||||
|
||||
class Add(AddView):
|
||||
|
||||
form_class = CommentForm
|
||||
|
||||
model = TicketComment
|
||||
permission_required = [
|
||||
'itam.add_device',
|
||||
]
|
||||
template_name = 'form.html.j2'
|
||||
|
||||
|
||||
def get_initial(self):
|
||||
|
||||
initial_values: dict = {
|
||||
'organization': UserSettings.objects.get(user = self.request.user).default_organization,
|
||||
'type_ticket': self.kwargs['ticket_type'],
|
||||
'ticket': self.kwargs['ticket_id'],
|
||||
}
|
||||
|
||||
if 'comment_type' in self.request.GET:
|
||||
|
||||
initial_values.update({
|
||||
'qs_comment_type': self.request.GET['comment_type']
|
||||
})
|
||||
|
||||
if 'parent_id' in self.kwargs:
|
||||
|
||||
initial_values.update({
|
||||
'parent': self.kwargs['parent_id']
|
||||
})
|
||||
|
||||
return initial_values
|
||||
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
|
||||
if self.kwargs['ticket_type'] == 'request':
|
||||
return reverse('Assistance:_ticket_request_view', args=(self.kwargs['ticket_type'],self.kwargs['ticket_id']))
|
||||
|
||||
return f"/ticket/"
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['content_title'] = 'Ticket Comment'
|
||||
|
||||
return context
|
||||
|
||||
|
||||
|
||||
class Change(ChangeView):
|
||||
|
||||
form_class = CommentForm
|
||||
|
||||
model = TicketComment
|
||||
|
||||
permission_required = [
|
||||
'itim.change_cluster',
|
||||
]
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['content_title'] = str(self.object)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def get_initial(self):
|
||||
return {
|
||||
'type_ticket': self.kwargs['ticket_type'],
|
||||
}
|
||||
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
|
||||
return reverse('Assistance:_ticket_request_view', args=(self.kwargs['ticket_type'], self.kwargs['ticket_id'],))
|
@ -4,7 +4,7 @@ span.linenos { color: inherit; background-color: transparent; padding-left: 5px;
|
||||
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
.codehilite .hll { background-color: #ffffcc }
|
||||
.codehilite { background: #f8f8f8; }
|
||||
.codehilite { background: #f8f8f8; padding: 5px; border: 1px solid #ccc;}
|
||||
.codehilite .c { color: #3D7B7B; font-style: italic } /* Comment */
|
||||
.codehilite .err { border: 1px solid #FF0000 } /* Error */
|
||||
.codehilite .k { color: #008000; font-weight: bold } /* Keyword */
|
||||
|
477
app/project-static/ticketing.css
Normal file
@ -0,0 +1,477 @@
|
||||
|
||||
#linked-tickets {
|
||||
display: table;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
#linked-tickets .ticket {
|
||||
display: inline-block;
|
||||
line-height: 30px;
|
||||
vertical-align: middle;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
|
||||
#linked-tickets .ticket svg{
|
||||
display: inline;
|
||||
width: 20px;
|
||||
height: 30px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
#linked-tickets .icon {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
line-height: 30px;
|
||||
vertical-align: middle;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
#linked-tickets .icon svg{
|
||||
display: inline;
|
||||
width: 20px;
|
||||
height: 30px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
#linked-tickets .icon.icon-related svg{
|
||||
background-color: #afdbff;
|
||||
border-radius: 10px;
|
||||
fill: #0b91ff;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
#linked-tickets .icon.icon-blocks svg{
|
||||
background-color: #fcd5b1;
|
||||
border-radius: 10px;
|
||||
fill: #e79b37;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
#linked-tickets .icon.icon-blocked_by svg{
|
||||
background-color: #f3c6c6;
|
||||
border-radius: 10px;
|
||||
fill: #ff1c1c;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
|
||||
#ticket-additional-data {
|
||||
padding-right: 10px;
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
||||
|
||||
#ticket-additional-data div {
|
||||
margin-top: 10px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
|
||||
#ticket-content {
|
||||
display: flex;
|
||||
height: auto;
|
||||
margin: 20px;
|
||||
padding: 0px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
||||
#ticket-content div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
#ticket-data {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
#ticket-data div {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
#ticket-description {
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
margin: 0px 10px 0px 0px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#data-block {
|
||||
border: 1px solid #ccc;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
|
||||
#data-block h3 button {
|
||||
float: right;
|
||||
height: 20px;
|
||||
margin-bottom: auto;
|
||||
margin-top: auto;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
#data-block div {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
|
||||
#data-block.linked-item div#item {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
|
||||
#ticket-comments {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
|
||||
#ticket-comments ul {
|
||||
padding: 0px;
|
||||
padding-left: 30px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
|
||||
#ticket-comments li {
|
||||
line-height: 30px;
|
||||
margin: 0px;
|
||||
margin-bottom: 30px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
|
||||
#data-block h3 {
|
||||
background-color: #177ee6;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
font-size: 16px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
margin: 0px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
|
||||
#data-block h3 #text {
|
||||
height: inherit;
|
||||
line-height: inherit;
|
||||
padding: 0px;
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
|
||||
#data-block h3 #icons {
|
||||
height: inherit;
|
||||
line-height: inherit;
|
||||
margin-right: 0px;
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
#data-block h3 #icons svg {
|
||||
height: 30px;
|
||||
margin: 0px;
|
||||
margin-right: 5px;
|
||||
width: 20px;
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
|
||||
#ticket-comments #comment {
|
||||
border: 1px solid #177ee6;
|
||||
}
|
||||
|
||||
#ticket-comments #comment h4 {
|
||||
background-color: #177ee6;
|
||||
border: none;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
margin: 0px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
|
||||
#comment h4 #text {
|
||||
height: inherit;
|
||||
line-height: inherit;
|
||||
padding: 0px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
#comment h4 #icons {
|
||||
border: none;
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
line-height: inherit;
|
||||
margin-right: 0px;
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
|
||||
#ticket-comments #comment h4 #icons svg {
|
||||
fill: #fff;
|
||||
height: 30px;
|
||||
margin: 0px;
|
||||
margin-right: 5px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
|
||||
#discussion {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
|
||||
#discussion h4 {
|
||||
border-left: 1px solid #177fe66e;
|
||||
border-right: 1px solid #177fe66e;
|
||||
display: block;
|
||||
margin: 0px;
|
||||
margin-right: 0px;
|
||||
margin-left: auto;
|
||||
height: 30px;
|
||||
line-height: inherit;
|
||||
text-align: right;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
|
||||
#discussion svg {
|
||||
width: 20px;
|
||||
height: 30px;
|
||||
margin: 0px;
|
||||
margin-right: 5px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
|
||||
#ticket-meta {
|
||||
background-color: #fff;
|
||||
width: 400px;
|
||||
border: 1px solid #ccc;
|
||||
margin-right: 0px;
|
||||
margin-left: auto;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
|
||||
#ticket-meta h3 {
|
||||
line-height: 30px;
|
||||
height: 30px;
|
||||
margin: 0px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
#ticket-meta fieldset {
|
||||
display: block;
|
||||
margin: 10px;
|
||||
line-height: 30px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
||||
#ticket-meta fieldset label {
|
||||
width: 100%;
|
||||
display: block;
|
||||
line-height: inherit;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
#ticket-meta fieldset span {
|
||||
width: 100%;
|
||||
display: block;
|
||||
line-height: inherit;
|
||||
border: none;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
|
||||
#ticket-meta h3.incident-ticket {
|
||||
background-color: #f7baba;
|
||||
}
|
||||
|
||||
|
||||
#ticket-meta h3.request-ticket {
|
||||
background-color: #f7e9ba;
|
||||
}
|
||||
|
||||
|
||||
#ticket-meta h3.change-ticket {
|
||||
background-color: #badff7;
|
||||
}
|
||||
|
||||
|
||||
#ticket-meta h3.problem-ticket {
|
||||
background-color: #f7d0ba;
|
||||
}
|
||||
|
||||
|
||||
#ticket-meta h3.issue-ticket {
|
||||
background-color: #baf7db;
|
||||
}
|
||||
|
||||
|
||||
#ticket-meta h3.project_task-ticket {
|
||||
background-color: #c5baf7;
|
||||
}
|
||||
|
||||
|
||||
.comment-type-default {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
|
||||
.comment-type-Notification {
|
||||
background-color: #96c7ff;
|
||||
}
|
||||
|
||||
|
||||
.comment-type-Solution {
|
||||
background-color: #b7ff96;
|
||||
}
|
||||
|
||||
|
||||
.comment-type-Task {
|
||||
background-color: #f8ff96;
|
||||
}
|
||||
|
||||
|
||||
#comment fieldset {
|
||||
border: none;
|
||||
display: inline-block;
|
||||
line-height: 14pt;
|
||||
width: 200px;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
|
||||
#comment fieldset label {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
|
||||
#comment fieldset span {
|
||||
display: block;
|
||||
line-height: inherit;
|
||||
|
||||
}
|
||||
|
||||
|
||||
#comment hr {
|
||||
border: none;
|
||||
border-bottom: 1px solid #ccc;
|
||||
margin: 0px 5px 0px 5px;
|
||||
}
|
||||
|
||||
|
||||
#ticket-content #markdown h1 {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
font-size: 24px;
|
||||
line-height: 24px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
text-align: left;
|
||||
|
||||
}
|
||||
|
||||
|
||||
#ticket-content #markdown h2 {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
font-size: 20px;
|
||||
line-height: 20px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
text-align: left;
|
||||
|
||||
}
|
||||
|
||||
|
||||
#ticket-content #markdown h3 {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
font-size: 18px;
|
||||
line-height: 18px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
text-align: left;
|
||||
|
||||
}
|
||||
|
||||
|
||||
#ticket-content #markdown h4 {
|
||||
background-color: inherit;
|
||||
color: #000;
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
text-align: left;
|
||||
|
||||
}
|
||||
|
||||
|
||||
#ticket-content #markdown h5 {
|
||||
background-color: inherit;
|
||||
color: #000;
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
text-align: left;
|
||||
|
||||
}
|
||||
|
||||
|
||||
#ticket-content #markdown li {
|
||||
background-color: inherit;
|
||||
font-size: 14px;
|
||||
line-height: 25px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
text-align: left;
|
||||
|
||||
}
|
||||
|
||||
|
||||
#ticket-content #markdown p {
|
||||
background-color: inherit;
|
||||
font-size: 14px;
|
||||
line-height: 25px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
text-align: left;
|
||||
|
||||
}
|
@ -100,6 +100,11 @@ class Project(ProjectCommonFieldsName):
|
||||
)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return self.name
|
||||
|
||||
|
||||
@property
|
||||
def percent_completed(self) -> str: # Auto-Calculate
|
||||
""" How much of the project is completed.
|
||||
|
1
app/templates/icons/place-holder.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#EA3323"><path d="M256-200h447l-84-84q-29 21-64.5 32.5T480-240q-39 0-74.5-12T341-285l-85 85Zm-56-57 84-84q-21-29-32.5-64.5T240-480q0-39 12-74.5t33-64.5l-85-85v447Zm142-142 82-81-82-81q-11 18-16.5 38t-5.5 43q0 23 5.5 43t16.5 38Zm138 79q23 0 43-5.5t38-16.5l-81-82-82 82q18 11 38.5 16.5T480-320Zm0-217 81-81q-18-11-38-16.5t-43-5.5q-23 0-43 5.5T399-618l81 81Zm138 138q11-18 16.5-38t5.5-43q0-23-5.5-43.5T618-562l-81 81 81 82Zm142 142v-447l-85 85q21 29 33 64.5t12 74.5q0 39-11.5 74.5T676-341l84 84ZM619-675l85-85H257l84 84q29-21 64.5-32.5T480-720q39 0 74.5 12t64.5 33ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Z"/></svg>
|
After Width: | Height: | Size: 761 B |