feat(core): Ability to link items to all ticket types

ref: #296 #308
This commit is contained in:
2024-09-20 15:21:56 +09:30
parent c3de79050e
commit 76954c019b
9 changed files with 286 additions and 26 deletions

View File

@ -24,7 +24,7 @@ from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
from .views import home
from core.views import history, related_ticket
from core.views import history, related_ticket, ticket_linked_item
from settings.views import user_settings
@ -53,6 +53,8 @@ urlpatterns = [
path('ticket/<str:ticket_type>/<int:ticket_id>/relate/add', related_ticket.Add.as_view(), name="_ticket_related_add"),
path('ticket/<str:ticket_type>/<int:ticket_id>/linked_item/add', ticket_linked_item.Add.as_view(), name="_ticket_linked_item_add"),
]

View File

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

View File

@ -0,0 +1,31 @@
# Generated by Django 5.0.8 on 2024-09-20 05:50
import access.models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0001_initial'),
('core', '0010_alter_ticketcategory_options'),
]
operations = [
migrations.CreateModel(
name='TicketLinkedItem',
fields=[
('id', models.AutoField(help_text='ID Number', primary_key=True, serialize=False, unique=True, verbose_name='Number')),
('item_type', models.IntegerField(choices=[(1, 'Cluster'), (2, 'Config Group'), (3, 'Device'), (4, 'Operating System'), (5, 'Service'), (6, 'Software')], help_text='Python Model location for linked item', verbose_name='Item Type')),
('item', models.IntegerField(help_text='Item ID to link to ticket', verbose_name='Item ID')),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
('ticket', models.ForeignKey(help_text='Ticket the item will be linked to', on_delete=django.db.models.deletion.CASCADE, to='core.ticket', verbose_name='Ticket')),
],
options={
'verbose_name': 'Ticket Linked Item',
'verbose_name_plural': 'Ticket linked Items',
'ordering': ['id'],
},
),
]

View File

@ -3,7 +3,6 @@ from django.db import models
from django.db.models import Q, signals, Sum
from django.forms import ValidationError
# from .ticket_enum import TicketEnum
from .ticket_enum_values import TicketValues
from access.fields import AutoCreatedField, AutoLastModifiedField
@ -672,6 +671,30 @@ class Ticket(
return str(duration)
@property
def linked_items(self) -> list(dict()):
"""Fetch items linked to ticket
Returns:
List of dict (list): List of dictionary with fields: id, name, type and url.
Empty List (list): No items were found
"""
linked_items: list = []
from core.models.ticket.ticket_linked_items import TicketLinkedItem
items = TicketLinkedItem.objects.filter(
ticket = self
)
if len(items) > 0:
linked_items = items
return linked_items
@property
def related_tickets(self) -> list(dict()):

View File

@ -4,6 +4,7 @@ from .ticket_enum_values import TicketValues
from access.models import TenancyObject
from core.middleware.get_request import get_request
from core.models.ticket.ticket import Ticket
@ -16,10 +17,18 @@ class TicketLinkedItem(TenancyObject):
'id'
]
class Modules(models.TextChoices):
DEVICE = 'itam.models.device', 'Device'
OPERATING_SYSTEM = 'itam.models.operating_system', 'Operating System'
SOFTWARE = 'itam.models.software', 'Software'
verbose_name = 'Ticket Linked Item'
verbose_name_plural = 'Ticket linked Items'
class Modules(models.IntegerChoices):
CLUSTER = 1, 'Cluster'
CONFIG_GROUP = 2, 'Config Group'
DEVICE = 3, 'Device'
OPERATING_SYSTEM = 4, 'Operating System'
SERVICE = 5, 'Service'
SOFTWARE = 6, 'Software'
is_global = None
@ -42,7 +51,7 @@ class TicketLinkedItem(TenancyObject):
verbose_name = 'Ticket',
)
module = models.CharField(
item_type = models.IntegerField(
blank= False,
choices = Modules,
help_text = 'Python Model location for linked item',
@ -56,3 +65,78 @@ class TicketLinkedItem(TenancyObject):
null = False,
verbose_name = 'Item ID',
)
def __str__(self) -> str:
item_type: str = None
if self.item_type == TicketLinkedItem.Modules.CLUSTER:
item_type = 'cluster'
elif self.item_type == TicketLinkedItem.Modules.CONFIG_GROUP:
item_type = 'config_group'
elif self.item_type == TicketLinkedItem.Modules.DEVICE:
item_type = 'device'
elif self.item_type == TicketLinkedItem.Modules.OPERATING_SYSTEM:
item_type = 'operating_system'
elif self.item_type == TicketLinkedItem.Modules.SERVICE:
item_type = 'service'
elif self.item_type == TicketLinkedItem.Modules.SOFTWARE:
item_type = 'software'
if item_type:
return f'${item_type}-{int(self.item)}'
return str(self.item)
@property
def parent_object(self):
""" Fetch the parent object """
return self.ticket
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
request = get_request()
if request:
if request.user.pk:
comment_user = request.user
else:
comment_user = None
else:
comment_user = None
from core.models.ticket.ticket_comment import TicketComment
comment = TicketComment.objects.create(
ticket = self.ticket,
comment_type = TicketComment.CommentType.ACTION,
body = f'linked {self}',
source = TicketComment.CommentSource.DIRECT,
user = comment_user,
)
comment.save()

View File

@ -62,26 +62,17 @@
<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>
<div id="icons">
<a href="{% url '_ticket_linked_item_add' ticket_type=ticket_type ticket_id=ticket.id %}">{% include 'icons/ticket/add.svg' %}</a>
</div>
</h3>
<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>
{% if ticket.linked_items %}
{% for linked_item in ticket.linked_items %}
<div id="item">{{ linked_item | markdown | safe }}</div>
{% endfor %}
{% else %}
<div style="text-align:center;%">Nothing found</div>
{% endif%}
</div>
</div>

View File

@ -0,0 +1,92 @@
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_linked_item import TicketLinkedItem, TicketLinkedItemForm
from core.views.common import AddView, ChangeView, DeleteView, IndexView
from settings.models.user_settings import UserSettings
class Add(AddView):
form_class = TicketLinkedItemForm
model = TicketLinkedItem
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,
'ticket': 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.kwargs['ticket_id'],))
elif self.kwargs['ticket_type'] == 'project_task':
return reverse('Project Management:_project_task_view', args=(self.object.from_ticket_id.project.id, self.kwargs['ticket_type'],self.kwargs['ticket_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 Linked Item'
return context
class Delete(DeleteView):
model = TicketLinkedItem
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):
if self.kwargs['ticket_type'] == 'request':
return reverse('Assistance:_ticket_request_view', args=(self.kwargs['ticket_type'],self.kwargs['ticket_id'],))
elif self.kwargs['ticket_type'] == 'project_task':
return reverse('Project Management:_project_task_view', args=(self.object.from_ticket_id.project.id, self.kwargs['ticket_type'],self.kwargs['ticket_id'],))
else:
return reverse('ITIM:_ticket_' + str(self.kwargs['ticket_type']).lower() + '_view', args=(self.kwargs['ticket_type'],self.kwargs['ticket_id'],))

View File

@ -208,6 +208,11 @@
width: 33%;
}
#data-block.linked-item div#item p{
margin: 0px;
padding: 0px;
}
#ticket-comments {
padding: 10px;

View File

@ -45,6 +45,12 @@ All models must meet the following requirements:
- If creating a new model, function `access.functions.permissions.permission_queryset()` has been updated to display the models permission(s)
## Checklist
This section details the additional items that may need to be done when adding a new model:
- If the model is a primary model, add to model reference rendering in `app/core/lib/markdown_plugins/model_reference.py` function `tag_html`
## History
Currently the adding of history to a model is a manual process. edit the file located at `core.views.history` and within `View.get_object` add the model to the `switch` statement.