diff --git a/app/app/urls.py b/app/app/urls.py index 3c63c075..2ceac18a 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -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///relate/add', related_ticket.Add.as_view(), name="_ticket_related_add"), + path('ticket///linked_item/add', ticket_linked_item.Add.as_view(), name="_ticket_linked_item_add"), + ] diff --git a/app/core/forms/ticket_linked_item.py b/app/core/forms/ticket_linked_item.py new file mode 100644 index 00000000..8fbba565 --- /dev/null +++ b/app/core/forms/ticket_linked_item.py @@ -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() diff --git a/app/core/migrations/0011_ticketlinkeditem.py b/app/core/migrations/0011_ticketlinkeditem.py new file mode 100644 index 00000000..e88602d4 --- /dev/null +++ b/app/core/migrations/0011_ticketlinkeditem.py @@ -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'], + }, + ), + ] diff --git a/app/core/models/ticket/ticket.py b/app/core/models/ticket/ticket.py index 574dbb23..3c1528f5 100644 --- a/app/core/models/ticket/ticket.py +++ b/app/core/models/ticket/ticket.py @@ -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()): diff --git a/app/core/models/ticket/ticket_linked_items.py b/app/core/models/ticket/ticket_linked_items.py index 2b15db2c..759deda2 100644 --- a/app/core/models/ticket/ticket_linked_items.py +++ b/app/core/models/ticket/ticket_linked_items.py @@ -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() diff --git a/app/core/templates/core/ticket.html.j2 b/app/core/templates/core/ticket.html.j2 index c18fd686..998c1537 100644 --- a/app/core/templates/core/ticket.html.j2 +++ b/app/core/templates/core/ticket.html.j2 @@ -62,26 +62,17 @@

Linked Items
-
{% include 'icons/place-holder.svg' %}{% include 'icons/place-holder.svg' %}
+

-
- An item -
-
- another item -
-
- another item -
-
- another item -
-
- another item -
-
- another item -
+ {% if ticket.linked_items %} + {% for linked_item in ticket.linked_items %} +
{{ linked_item | markdown | safe }}
+ {% endfor %} + {% else %} +
Nothing found
+ {% endif%}
diff --git a/app/core/views/ticket_linked_item.py b/app/core/views/ticket_linked_item.py new file mode 100644 index 00000000..ba60e64d --- /dev/null +++ b/app/core/views/ticket_linked_item.py @@ -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'],)) diff --git a/app/project-static/ticketing.css b/app/project-static/ticketing.css index 19f00bbe..a756136d 100644 --- a/app/project-static/ticketing.css +++ b/app/project-static/ticketing.css @@ -208,6 +208,11 @@ width: 33%; } +#data-block.linked-item div#item p{ + margin: 0px; + padding: 0px; +} + #ticket-comments { padding: 10px; diff --git a/docs/projects/centurion_erp/development/models.md b/docs/projects/centurion_erp/development/models.md index 9b08df64..85f0a915 100644 --- a/docs/projects/centurion_erp/development/models.md +++ b/docs/projects/centurion_erp/development/models.md @@ -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.