From e28dbea05bcdd204727dea5ad4a884aae5f4a896 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Sep 2024 10:41:17 +0930 Subject: [PATCH 01/43] feat(core): Add ticket categories ref: #283 #284 --- app/core/forms/ticket_categories.py | 120 +++++++++++++ app/core/migrations/0006_ticketcategory.py | 43 +++++ app/core/models/ticket/ticket_category.py | 111 ++++++++++++ .../core/index_ticket_categories.html.j2 | 29 +++ .../templates/core/ticket_category.html.j2 | 12 ++ app/core/views/ticket_categories.py | 168 ++++++++++++++++++ app/settings/templates/settings/home.html.j2 | 7 + app/settings/urls.py | 8 +- .../centurion_erp/user/core/ticketcategory.md | 44 +++++ mkdocs.yml | 2 + 10 files changed, 543 insertions(+), 1 deletion(-) create mode 100644 app/core/forms/ticket_categories.py create mode 100644 app/core/migrations/0006_ticketcategory.py create mode 100644 app/core/models/ticket/ticket_category.py create mode 100644 app/core/templates/core/index_ticket_categories.html.j2 create mode 100644 app/core/templates/core/ticket_category.html.j2 create mode 100644 app/core/views/ticket_categories.py create mode 100644 docs/projects/centurion_erp/user/core/ticketcategory.md diff --git a/app/core/forms/ticket_categories.py b/app/core/forms/ticket_categories.py new file mode 100644 index 000000000..613bcd11c --- /dev/null +++ b/app/core/forms/ticket_categories.py @@ -0,0 +1,120 @@ +from django import forms +from django.forms import ValidationError +from django.urls import reverse + +from app import settings + +from core.forms.common import CommonModelForm +from core.models.ticket.ticket_category import TicketCategory + + + +class TicketCategoryForm(CommonModelForm): + + + class Meta: + + fields = '__all__' + + model = TicketCategory + + prefix = 'ticket_category' + + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) + + self.fields['parent'].queryset = self.fields['parent'].queryset.exclude( + id=self.instance.pk + ) + + + def clean(self): + + cleaned_data = super().clean() + + pk = self.instance.id + + parent = cleaned_data.get("parent") + + if pk: + + if parent == pk: + + raise ValidationError("Category can't have itself as its parent category") + + return cleaned_data + + + +class DetailForm(TicketCategoryForm): + + + tabs: dict = { + "details": { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "double", + "left": [ + 'parent', + 'name', + 'runbook', + 'organization', + 'c_created', + 'c_modified' + ], + "right": [ + 'model_notes', + ] + }, + { + "layout": "double", + "name": "Ticket Types", + "left": [ + 'change', + 'problem' + 'request' + ], + "right": [ + 'incident', + 'project_task' + ] + }, + ] + }, + "notes": { + "name": "Notes", + "slug": "notes", + "sections": [] + } + } + + + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) + + + self.fields['c_created'] = forms.DateTimeField( + label = 'Created', + input_formats=settings.DATETIME_FORMAT, + disabled = True, + initial = self.instance.created, + ) + + self.fields['c_modified'] = forms.DateTimeField( + label = 'Modified', + input_formats=settings.DATETIME_FORMAT, + disabled = True, + initial = self.instance.modified, + ) + + + self.tabs['details'].update({ + "edit_url": reverse('Settings:_ticket_category_change', kwargs={'pk': self.instance.pk}) + }) + + self.url_index_view = reverse('Settings:_ticket_categories') + diff --git a/app/core/migrations/0006_ticketcategory.py b/app/core/migrations/0006_ticketcategory.py new file mode 100644 index 000000000..3dfe26ac7 --- /dev/null +++ b/app/core/migrations/0006_ticketcategory.py @@ -0,0 +1,43 @@ +# Generated by Django 5.0.8 on 2024-09-13 01:10 + +import access.fields +import access.models +import django.db.models.deletion +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('access', '0001_initial'), + ('assistance', '0001_initial'), + ('core', '0005_ticket_relatedtickets_ticketcomment'), + ] + + operations = [ + migrations.CreateModel( + name='TicketCategory', + fields=[ + ('is_global', models.BooleanField(default=False)), + ('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')), + ('id', models.AutoField(help_text='Category ID Number', primary_key=True, serialize=False, unique=True, verbose_name='Number')), + ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), + ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), + ('name', models.CharField(help_text='Category Name', max_length=50, verbose_name='Name')), + ('change', models.BooleanField(default=True, help_text='Use category for change tickets', verbose_name='Change Tickets')), + ('incident', models.BooleanField(default=True, help_text='Use category for incident tickets', verbose_name='Incident Tickets')), + ('problem', models.BooleanField(default=True, help_text='Use category for problem tickets', verbose_name='Problem Tickets')), + ('project_task', models.BooleanField(default=True, help_text='Use category for Project tasks', verbose_name='Project Tasks')), + ('request', models.BooleanField(default=True, help_text='Use category for request tickets', verbose_name='Request Tickets')), + ('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, help_text='The Parent Category', null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.ticketcategory', verbose_name='Parent Category')), + ('runbook', models.ForeignKey(blank=True, help_text='The runbook for this category', null=True, on_delete=django.db.models.deletion.SET_NULL, to='assistance.knowledgebase', verbose_name='Runbook')), + ], + options={ + 'verbose_name': 'Ticket Category', + 'verbose_name_plural': 'Ticket Categories', + 'ordering': ['name'], + }, + ), + ] diff --git a/app/core/models/ticket/ticket_category.py b/app/core/models/ticket/ticket_category.py new file mode 100644 index 000000000..73e128f42 --- /dev/null +++ b/app/core/models/ticket/ticket_category.py @@ -0,0 +1,111 @@ +from django.db import models + +from access.fields import AutoCreatedField, AutoLastModifiedField +from access.models import TenancyObject, Team + +from assistance.models.knowledge_base import KnowledgeBase + + + +class TicketCategoryCommonFields(TenancyObject): + + class Meta: + abstract = True + + id = models.AutoField( + blank=False, + help_text = 'Category ID Number', + primary_key=True, + unique=True, + verbose_name = 'Number', + ) + + created = AutoCreatedField() + + modified = AutoLastModifiedField() + + + +class TicketCategory(TicketCategoryCommonFields): + + + class Meta: + + ordering = [ + 'name' + ] + + verbose_name = "Ticket Category" + + verbose_name_plural = "Ticket Categories" + + + parent = models.ForeignKey( + 'self', + blank= True, + help_text = 'The Parent Category', + null = True, + on_delete = models.SET_NULL, + verbose_name = 'Parent Category', + ) + + name = models.CharField( + blank = False, + help_text = "Category Name", + max_length = 50, + verbose_name = 'Name', + ) + + runbook = models.ForeignKey( + KnowledgeBase, + blank= True, + help_text = 'The runbook for this category', + null = True, + on_delete = models.SET_NULL, + verbose_name = 'Runbook', + ) + + change = models.BooleanField( + blank = False, + default = True, + help_text = 'Use category for change tickets', + null = False, + verbose_name = 'Change Tickets', + ) + + incident = models.BooleanField( + blank = False, + default = True, + help_text = 'Use category for incident tickets', + null = False, + verbose_name = 'Incident Tickets', + ) + + problem = models.BooleanField( + blank = False, + default = True, + help_text = 'Use category for problem tickets', + null = False, + verbose_name = 'Problem Tickets', + ) + + project_task = models.BooleanField( + blank = False, + default = True, + help_text = 'Use category for Project tasks', + null = False, + verbose_name = 'Project Tasks', + ) + + request = models.BooleanField( + blank = False, + default = True, + help_text = 'Use category for request tickets', + null = False, + verbose_name = 'Request Tickets', + ) + + + def __str__(self): + + return self.name diff --git a/app/core/templates/core/index_ticket_categories.html.j2 b/app/core/templates/core/index_ticket_categories.html.j2 new file mode 100644 index 000000000..470997040 --- /dev/null +++ b/app/core/templates/core/index_ticket_categories.html.j2 @@ -0,0 +1,29 @@ +{% extends 'base.html.j2' %} + +{% block content %} + + + + + + + + + + + {% if items %} + {% for category in items %} + + + + + + + + {% endfor %} + {% else %} + + {% endif%} +
NameOrganizationcreatedmodified 
{{ category.name }}{{ category.organization }}{{ category.created }}{{ category.modified }} 
Nothing Found
+ +{% endblock %} diff --git a/app/core/templates/core/ticket_category.html.j2 b/app/core/templates/core/ticket_category.html.j2 new file mode 100644 index 000000000..d2fc524f7 --- /dev/null +++ b/app/core/templates/core/ticket_category.html.j2 @@ -0,0 +1,12 @@ +{% extends 'detail.html.j2' %} + + +{% block tabs %} + +
+ + {% include 'content/section.html.j2' with tab=form.tabs.details %} + +
+ +{% endblock %} diff --git a/app/core/views/ticket_categories.py b/app/core/views/ticket_categories.py new file mode 100644 index 000000000..f0a3ac5cd --- /dev/null +++ b/app/core/views/ticket_categories.py @@ -0,0 +1,168 @@ +from django.urls import reverse + +from core.forms.comment import AddNoteForm +from core.forms.ticket_categories import DetailForm, TicketCategory, TicketCategoryForm + +from core.models.notes import Notes +from core.views.common import AddView, ChangeView, DeleteView, IndexView + + + +class Add(AddView): + + form_class = TicketCategoryForm + + model = TicketCategory + + permission_required = [ + 'core.add_ticketcategory', + ] + + + def get_initial(self): + + initial = super().get_initial() + + if 'pk' in self.kwargs: + + if self.kwargs['pk']: + + initial.update({'parent': self.kwargs['pk']}) + + self.model.parent.field.hidden = True + + return initial + + + def get_success_url(self, **kwargs): + + return reverse('Settings:_ticket_categories') + + + +class Change(ChangeView): + + form_class = TicketCategoryForm + + model = TicketCategory + + permission_required = [ + 'core.change_ticketcategory', + ] + + + def get_context_data(self, **kwargs): + + context = super().get_context_data(**kwargs) + + context['content_title'] = str(self.object) + + return context + + + def get_success_url(self, **kwargs): + + return reverse('Settings:_ticket_category_view', args=(self.kwargs['pk'],)) + + + +class Delete(DeleteView): + + model = TicketCategory + + permission_required = [ + 'itim.delete_ticketcategory', + ] + + + 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('Settings:_ticket_categories') + + + +class Index(IndexView): + + context_object_name = "items" + + model = TicketCategory + + paginate_by = 10 + + permission_required = [ + 'core.view_ticketcategory' + ] + + template_name = 'core/index_ticket_categories.html.j2' + + + def get_context_data(self, **kwargs): + + context = super().get_context_data(**kwargs) + + context['model_docs_path'] = self.model._meta.app_label + '/' + self.model._meta.model_name + + return context + + + +class View(ChangeView): + + context_object_name = "ticket_categories" + + form_class = DetailForm + + model = TicketCategory + + permission_required = [ + 'core.view_ticketcategory', + ] + + template_name = 'core/ticket_category.html.j2' + + + def get_context_data(self, **kwargs): + + context = super().get_context_data(**kwargs) + + context['notes_form'] = AddNoteForm(prefix='note') + context['notes'] = Notes.objects.filter(service=self.kwargs['pk']) + + context['model_pk'] = self.kwargs['pk'] + context['model_name'] = self.model._meta.model_name + + context['model_delete_url'] = reverse('Settings:_ticket_category_delete', kwargs={'pk': self.kwargs['pk']}) + + + context['content_title'] = self.object.name + + return context + + + # def post(self, request, *args, **kwargs): + + # item = Cluster.objects.get(pk=self.kwargs['pk']) + + # notes = AddNoteForm(request.POST, prefix='note') + + # if notes.is_bound and notes.is_valid() and notes.instance.note != '': + + # notes.instance.service = item + + # notes.instance.organization = item.organization + + # notes.save() + + + def get_success_url(self, **kwargs): + + return reverse('Settings:_ticket_category_view', kwargs={'pk': self.kwargs['pk']}) diff --git a/app/settings/templates/settings/home.html.j2 b/app/settings/templates/settings/home.html.j2 index fda2e5afd..b6ec69cfd 100644 --- a/app/settings/templates/settings/home.html.j2 +++ b/app/settings/templates/settings/home.html.j2 @@ -50,6 +50,13 @@ div#content article h3 { +
+

Core

+ +
+

ITAM

diff --git a/app/settings/urls.py b/app/settings/urls.py index 540e496e2..e78bceaac 100644 --- a/app/settings/urls.py +++ b/app/settings/urls.py @@ -2,7 +2,7 @@ from assistance.views import knowledge_base_category -from core.views import celery_log, ticket_categories +from core.views import celery_log, ticket_categories, ticket_comment_category from settings.views import app_settings, home, device_models, device_types, external_link, manufacturer, software_categories @@ -71,6 +71,12 @@ path("ticket_categories//edit", ticket_categories.Change.as_view(), name="_ticket_category_change"), path("ticket_categories//delete", ticket_categories.Delete.as_view(), name="_ticket_category_delete"), + path("ticket_comment_categories", ticket_comment_category.Index.as_view(), name="_ticket_comment_categories"), + path("ticket_comment_categories/", ticket_comment_category.View.as_view(), name="_ticket_comment_category_view"), + path("ticket_comment_categories/add", ticket_comment_category.Add.as_view(), name="_ticket_comment_category_add"), + path("ticket_comment_categories//edit", ticket_comment_category.Change.as_view(), name="_ticket_comment_category_change"), + path("ticket_comment_categories//delete", ticket_comment_category.Delete.as_view(), name="_ticket_comment_category_delete"), + path("task_results", celery_log.Index.as_view(), name="_task_results"), path("task_result/", celery_log.View.as_view(), name="_task_result_view"), diff --git a/docs/projects/centurion_erp/user/core/ticket_comment_category.md b/docs/projects/centurion_erp/user/core/ticket_comment_category.md new file mode 100644 index 000000000..f6d7f935b --- /dev/null +++ b/docs/projects/centurion_erp/user/core/ticket_comment_category.md @@ -0,0 +1,40 @@ +--- +title: Ticket Comment Categories +description: Ticket Comment Categories Documentation as part of the Core Module for Centurion ERP by No Fuss Computing +date: 2024-09-13 +template: project.html +about: https://github.com/nofusscomputing/centurion_erp +--- + +Ticket comment categories enables you to sort ticket comments to aid in reporting. + + +## Fields + +- parent + + The parent category. This field enables nesting of categories + +- name + + The name for this category + +- [runbook](../assistance/knowledge_base.md) + + The runbook for this category + +- comment + + Should this category be available for standard comments + +- notification + + Should this category be available for notification comments + +- solution + + Should this category be available for solution comments + +- task + + Should this category be available for task comments diff --git a/mkdocs.yml b/mkdocs.yml index 2c1931252..85a345365 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -196,6 +196,8 @@ nav: - projects/centurion_erp/user/core/ticketcategory.md + - projects/centurion_erp/user/core/ticket_comment_category.md + - ITAM: - projects/centurion_erp/user/itam/index.md From 56b715797e32e78db4ccdc77f7402bfcf6d10ec7 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Sep 2024 13:30:36 +0930 Subject: [PATCH 17/43] feat(core): Ability to assign categories to ticket comments ref: #14 #96 #93 #95 #90 #283 #283 #284 --- app/core/forms/ticket_comment.py | 16 +++++++++++++++ app/core/forms/validate_ticket_comment.py | 2 ++ ...ticketcategory_ticket_category_and_more.py | 13 ++++++------ app/core/models/ticket/ticket_comment.py | 20 ++++++++++--------- .../models/ticket/ticket_comment_category.py | 8 ++++---- 5 files changed, 40 insertions(+), 19 deletions(-) diff --git a/app/core/forms/ticket_comment.py b/app/core/forms/ticket_comment.py index 91c6df23b..23c2aa199 100644 --- a/app/core/forms/ticket_comment.py +++ b/app/core/forms/ticket_comment.py @@ -80,18 +80,34 @@ def __init__(self, request, *args, **kwargs): self.fields['comment_type'].initial = self.Meta.model.CommentType.TASK + self.fields['category'].queryset = self.fields['category'].queryset.filter( + task = True + ) + elif self._comment_type == 'comment': self.fields['comment_type'].initial = self.Meta.model.CommentType.COMMENT + self.fields['category'].queryset = self.fields['category'].queryset.filter( + comment = True + ) + elif self._comment_type == 'solution': self.fields['comment_type'].initial = self.Meta.model.CommentType.SOLUTION + self.fields['category'].queryset = self.fields['category'].queryset.filter( + solution = True + ) + elif self._comment_type == 'notification': self.fields['comment_type'].initial = self.Meta.model.CommentType.NOTIFICATION + self.fields['category'].queryset = self.fields['category'].queryset.filter( + notification = True + ) + allowed_fields = self.fields_allowed diff --git a/app/core/forms/validate_ticket_comment.py b/app/core/forms/validate_ticket_comment.py index 3c0e1a8ee..589e5d7d9 100644 --- a/app/core/forms/validate_ticket_comment.py +++ b/app/core/forms/validate_ticket_comment.py @@ -55,6 +55,7 @@ class TicketCommentValidation( 'external_system', 'comment_type', 'body', + 'category', 'created', 'modified', 'private', @@ -74,6 +75,7 @@ class TicketCommentValidation( ] triage_fields: list = [ + 'category', 'body', 'private', 'duration', diff --git a/app/core/migrations/0005_ticket_relatedtickets_ticketcategory_ticket_category_and_more.py b/app/core/migrations/0005_ticket_relatedtickets_ticketcategory_ticket_category_and_more.py index 49bca34a8..726619de7 100644 --- a/app/core/migrations/0005_ticket_relatedtickets_ticketcategory_ticket_category_and_more.py +++ b/app/core/migrations/0005_ticket_relatedtickets_ticketcategory_ticket_category_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.8 on 2024-09-13 03:43 +# Generated by Django 5.0.8 on 2024-09-13 03:59 import access.fields import access.models @@ -108,10 +108,10 @@ class Migration(migrations.Migration): ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), ('name', models.CharField(help_text='Category Name', max_length=50, verbose_name='Name')), - ('comment', models.BooleanField(default=True, help_text='Use category for standard comment', verbose_name='Change Comment')), - ('notification', models.BooleanField(default=True, help_text='Use category for notification comment', verbose_name='Incident Comment')), - ('solution', models.BooleanField(default=True, help_text='Use category for solution comment', verbose_name='Problem Comment')), - ('task', models.BooleanField(default=True, help_text='Use category for task comment', verbose_name='Project Comment')), + ('comment', models.BooleanField(default=True, help_text='Use category for standard comment', verbose_name='Comment')), + ('notification', models.BooleanField(default=True, help_text='Use category for notification comment', verbose_name='Notification Comment')), + ('solution', models.BooleanField(default=True, help_text='Use category for solution comment', verbose_name='Solution Comment')), + ('task', models.BooleanField(default=True, help_text='Use category for task comment', verbose_name='Task Comment')), ('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, help_text='The Parent Category', null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.ticketcommentcategory', verbose_name='Parent Category')), ('runbook', models.ForeignKey(blank=True, help_text='The runbook for this category', null=True, on_delete=django.db.models.deletion.SET_NULL, to='assistance.knowledgebase', verbose_name='Runbook')), @@ -150,9 +150,10 @@ class Migration(migrations.Migration): ('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')), + ('template', models.ForeignKey(blank=True, default=None, help_text='Comment Template to use', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='comment_template', to='core.ticketcomment', verbose_name='Template')), ('ticket', models.ForeignKey(blank=True, default=None, help_text='Ticket this comment belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='core.ticket', validators=[core.models.ticket.ticket_comment.TicketComment.validation_ticket_id], verbose_name='Ticket')), ('user', models.ForeignKey(blank=True, help_text='Who made the comment', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='comment_user', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ('category', models.ForeignKey(blank=True, default=None, help_text='Category of the comment', null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.ticketcommentcategory', verbose_name='Category')), ], options={ 'verbose_name': 'Comment', diff --git a/app/core/models/ticket/ticket_comment.py b/app/core/models/ticket/ticket_comment.py index 288f4836f..2a07a555f 100644 --- a/app/core/models/ticket/ticket_comment.py +++ b/app/core/models/ticket/ticket_comment.py @@ -6,6 +6,7 @@ from access.models import TenancyObject, Team from .ticket import Ticket +from .ticket_comment_category import TicketCommentCategory @@ -186,14 +187,15 @@ def validation_ticket_id(field): verbose_name = 'Duration', ) - - # category = models.CharField( - # blank = False, - # help_text = "Category of the Ticket", - # max_length = 50, - # unique = True, - # verbose_name = 'Category', - # ) + category = models.ForeignKey( + TicketCommentCategory, + blank= True, + default = None, + help_text = 'Category of the comment', + null = True, + on_delete = models.SET_NULL, + verbose_name = 'Category', + ) template = models.ForeignKey( 'self', @@ -201,7 +203,7 @@ def validation_ticket_id(field): default = None, help_text = 'Comment Template to use', null = True, - on_delete = models.DO_NOTHING, + on_delete = models.SET_NULL, related_name = 'comment_template', verbose_name = 'Template', ) diff --git a/app/core/models/ticket/ticket_comment_category.py b/app/core/models/ticket/ticket_comment_category.py index 2d4d8230c..78e52ec97 100644 --- a/app/core/models/ticket/ticket_comment_category.py +++ b/app/core/models/ticket/ticket_comment_category.py @@ -70,7 +70,7 @@ class Meta: default = True, help_text = 'Use category for standard comment', null = False, - verbose_name = 'Change Comment', + verbose_name = 'Comment', ) notification = models.BooleanField( @@ -78,7 +78,7 @@ class Meta: default = True, help_text = 'Use category for notification comment', null = False, - verbose_name = 'Incident Comment', + verbose_name = 'Notification Comment', ) solution = models.BooleanField( @@ -86,7 +86,7 @@ class Meta: default = True, help_text = 'Use category for solution comment', null = False, - verbose_name = 'Problem Comment', + verbose_name = 'Solution Comment', ) task = models.BooleanField( @@ -94,7 +94,7 @@ class Meta: default = True, help_text = 'Use category for task comment', null = False, - verbose_name = 'Project Comment', + verbose_name = 'Task Comment', ) From f3dccd3b8437f01b338aa04adcc1daba345fe2fd Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Sep 2024 13:37:49 +0930 Subject: [PATCH 18/43] test(core): ticket comment category tenancy model checks ref: #283 #284 --- ...t_comment_category_access_tenancy_object.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_access_tenancy_object.py diff --git a/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_access_tenancy_object.py b/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_access_tenancy_object.py new file mode 100644 index 000000000..0b53faae9 --- /dev/null +++ b/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_access_tenancy_object.py @@ -0,0 +1,18 @@ +import pytest +import unittest +import requests + +from django.test import TestCase, Client + +from access.tests.abstract.tenancy_object import TenancyObject + +from core.models.ticket.ticket_comment_category import TicketCommentCategory + + + +class TicketCommentCategoryTenancyObject( + TestCase, + TenancyObject +): + + model = TicketCommentCategory From a0b0d79777e748ebdd74dcf9cf6cc41dba5245cb Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Sep 2024 13:38:03 +0930 Subject: [PATCH 19/43] test(core): ticket comment category history checks ref: #283 #284 --- ...st_ticket_comment_category_core_history.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_core_history.py diff --git a/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_core_history.py b/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_core_history.py new file mode 100644 index 000000000..2b17b0906 --- /dev/null +++ b/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_core_history.py @@ -0,0 +1,75 @@ +import pytest +import unittest +import requests + +from django.test import TestCase, Client + + +from access.models import Organization + +from core.models.history import History +from core.models.ticket.ticket_comment_category import TicketCommentCategory +from core.tests.abstract.history_entry import HistoryEntry +from core.tests.abstract.history_entry_parent_model import HistoryEntryParentItem + +# from itam.models.device import Device + + + +class TicketCommentCategoryHistory(TestCase, HistoryEntry, HistoryEntryParentItem): + + + model = TicketCommentCategory + + + @classmethod + def setUpTestData(self): + """ Setup Test """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.item_create = self.model.objects.create( + name = 'test_item_' + self.model._meta.model_name, + organization = self.organization + ) + + + self.history_create = History.objects.get( + action = History.Actions.ADD[0], + item_pk = self.item_create.pk, + item_class = self.model._meta.model_name, + ) + + + self.item_change = self.item_create + self.item_change.name = 'test_item_' + self.model._meta.model_name + '_changed' + self.item_change.save() + + self.field_after_expected_value = '{"name": "test_item_' + self.model._meta.model_name + '_changed"}' + + self.history_change = History.objects.get( + action = History.Actions.UPDATE[0], + item_pk = self.item_change.pk, + item_class = self.model._meta.model_name, + ) + + self.item_delete = self.model.objects.create( + name = 'test_item_delete_' + self.model._meta.model_name, + organization = self.organization + ) + + self.deleted_pk = self.item_delete.pk + + self.item_delete.delete() + + self.history_delete = History.objects.filter( + item_pk = self.deleted_pk, + item_class = self.model._meta.model_name, + ) + + self.history_delete_children = History.objects.filter( + item_parent_pk = self.deleted_pk, + item_parent_class = self.model._meta.model_name, + ) From 9fbb88fa5fe520989bc48275e8e420a1b1f0d72e Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Sep 2024 13:38:15 +0930 Subject: [PATCH 20/43] test(core): ticket comment category ui permission checks ref: #283 #284 --- ...test_ticket_comment_category_permission.py | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_permission.py diff --git a/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_permission.py b/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_permission.py new file mode 100644 index 000000000..a7b00f27d --- /dev/null +++ b/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_permission.py @@ -0,0 +1,187 @@ +# from django.conf import settings +from django.contrib.auth import get_user_model +from django.contrib.auth.models import AnonymousUser, User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import TestCase, Client + +import pytest +import unittest +import requests + +from access.models import Organization, Team, TeamUsers, Permission + +from app.tests.abstract.model_permissions import ModelPermissions + +from core.models.ticket.ticket_comment_category import TicketCommentCategory + + +class TicketCommentCategoryPermissions(TestCase, ModelPermissions): + + model = TicketCommentCategory + + app_namespace = 'Settings' + + url_name_view = '_ticket_comment_category_view' + + url_name_add = '_ticket_comment_category_add' + + url_name_change = '_ticket_comment_category_change' + + url_name_delete = '_ticket_comment_category_delete' + + url_delete_response = reverse('Settings:_ticket_comment_categories') + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a category + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + self.item = self.model.objects.create( + organization=organization, + name = 'manufacturerone' + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + # self.url_add_kwargs = {'pk': self.item.id} + + self.add_data = {'name': 'manufacturer', 'organization': self.organization.id} + + self.url_change_kwargs = {'pk': self.item.id} + + self.change_data = {'name': 'manufacturer', 'organization': self.organization.id} + + self.url_delete_kwargs = {'pk': self.item.id} + + self.delete_data = {'name': 'manufacturer'} + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From 01c57b37ade7bce9d79922f771525b3eeafeb728 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Sep 2024 13:40:03 +0930 Subject: [PATCH 21/43] test(core): ticket comment category view checks ref: #283 #284 --- .../test_ticket_comment_category_views.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_views.py diff --git a/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_views.py b/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_views.py new file mode 100644 index 000000000..e8ff79d0c --- /dev/null +++ b/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_views.py @@ -0,0 +1,33 @@ +import pytest +import unittest +import requests + +from django.test import TestCase + +from app.tests.abstract.models import PrimaryModel, ModelAdd, ModelChange, ModelDelete + + + +# class TicketCommentViews( +# TestCase, +# PrimaryModel +# ): +class TicketCommentCategoryViews( + TestCase, + PrimaryModel +): + + add_module = 'core.views.ticket_comment_category' + add_view = 'Add' + + change_module = add_module + change_view = 'Change' + + delete_module = add_module + delete_view = 'Delete' + + display_module = add_module + display_view = 'View' + + index_module = add_module + index_view = 'Index' From 88d6a734548c13ab7830b1e8831c551e72e9563f Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Sep 2024 13:40:26 +0930 Subject: [PATCH 22/43] test(core): ticket comment category tenancy model checks ref: #283 #284 --- .../test_ticket_comment_category.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 app/core/tests/unit/ticket_comment_category/test_ticket_comment_category.py diff --git a/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category.py b/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category.py new file mode 100644 index 000000000..5cad5ff98 --- /dev/null +++ b/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category.py @@ -0,0 +1,27 @@ +import pytest +# import unittest +# import requests + +from django.test import TestCase + +from app.tests.abstract.models import TenancyModel + +from core.models.ticket.ticket_comment_category import TicketCommentCategory + + +class TicketCommentCategoryModel( + TestCase, + TenancyModel +): + + model = TicketCommentCategory + + + # def test_attribute_duration_ticket_value(self): + # """Attribute value test + + # This aattribute calculates the ticket duration from + # it's comments. must return total time in seconds + # """ + + # pass \ No newline at end of file From 1be23148d7b9daeedf94d14afe4413d7ed625bc7 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Sep 2024 13:40:57 +0930 Subject: [PATCH 23/43] test(core): add missing ticket category view checks ref: #283 #284 --- .../tests/unit/ticket_category/test_ticket_category_views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/core/tests/unit/ticket_category/test_ticket_category_views.py b/app/core/tests/unit/ticket_category/test_ticket_category_views.py index 4269318d6..2c001e842 100644 --- a/app/core/tests/unit/ticket_category/test_ticket_category_views.py +++ b/app/core/tests/unit/ticket_category/test_ticket_category_views.py @@ -14,8 +14,7 @@ # ): class TicketCategoryViews( TestCase, - ModelAdd, - ModelChange, + PrimaryModel ): add_module = 'core.views.ticket_categories' From 902aaf31dd94aa2fbe17e5ddf42c14bc54dd3b8a Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Sep 2024 13:41:20 +0930 Subject: [PATCH 24/43] fix(core): Correct view permissions for ticket comment category ref: #283 #284 --- app/core/views/ticket_comment_category.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/core/views/ticket_comment_category.py b/app/core/views/ticket_comment_category.py index 4b9fd781b..f7ae63bf2 100644 --- a/app/core/views/ticket_comment_category.py +++ b/app/core/views/ticket_comment_category.py @@ -15,7 +15,7 @@ class Add(AddView): model = TicketCommentCategory permission_required = [ - 'core.add_ticketcategory', + 'core.add_ticketcommentcategory', ] @@ -47,7 +47,7 @@ class Change(ChangeView): model = TicketCommentCategory permission_required = [ - 'core.change_ticketcategory', + 'core.change_ticketcommentcategory', ] @@ -71,7 +71,7 @@ class Delete(DeleteView): model = TicketCommentCategory permission_required = [ - 'core.delete_ticketcategory', + 'core.delete_ticketcommentcategory', ] @@ -99,7 +99,7 @@ class Index(IndexView): paginate_by = 10 permission_required = [ - 'core.view_ticketcategory' + 'core.view_ticketcommentcategory' ] template_name = 'core/index_ticket_comment_categories.html.j2' @@ -124,7 +124,7 @@ class View(ChangeView): model = TicketCommentCategory permission_required = [ - 'core.view_ticketcategory', + 'core.view_ticketcommentcategory', ] template_name = 'core/ticket_comment_category.html.j2' From f2a4223d25be75bb4049f75e9337759255c009b7 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Sep 2024 13:54:42 +0930 Subject: [PATCH 25/43] feat(core): Add ticket comment category API endpoint ref: #283 #284 --- .../core/ticket_comment_category.py | 42 ++++++++++ app/api/urls.py | 3 + .../views/core/ticket_comment_categories.py | 79 +++++++++++++++++++ app/api/views/settings/index.py | 3 +- 4 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 app/api/serializers/core/ticket_comment_category.py create mode 100644 app/api/views/core/ticket_comment_categories.py diff --git a/app/api/serializers/core/ticket_comment_category.py b/app/api/serializers/core/ticket_comment_category.py new file mode 100644 index 000000000..dbb03d6d6 --- /dev/null +++ b/app/api/serializers/core/ticket_comment_category.py @@ -0,0 +1,42 @@ +from django.urls import reverse + +from rest_framework import serializers +from rest_framework.fields import empty + + +from core.models.ticket.ticket_comment_category import TicketCommentCategory + + + +class TicketCommentCategorySerializer( + serializers.ModelSerializer, +): + + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_ticket_comment_category-detail", format="html" + ) + + + class Meta: + + model = TicketCommentCategory + + fields = '__all__' + + read_only_fields = [ + 'id', + 'url', + ] + + + def __init__(self, instance=None, data=empty, **kwargs): + + if instance is not None: + + if hasattr(instance, 'id'): + + self.fields.fields['parent'].queryset = self.fields.fields['parent'].queryset.exclude( + id=instance.id + ) + + super().__init__(instance=instance, data=data, **kwargs) diff --git a/app/api/urls.py b/app/api/urls.py index 6c46fd1b0..9abc1f9ba 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -12,6 +12,7 @@ from api.views.assistance import request_ticket from api.views.core import ( ticket_categories, + ticket_comment_categories, ticket_comments as core_ticket_comments ) from api.views.itim import change_ticket, incident_ticket, problem_ticket @@ -49,6 +50,8 @@ router.register('settings/ticket_categories', ticket_categories.View, basename='_api_ticket_category') +router.register('settings/ticket_comment_categories', ticket_comment_categories.View, basename='_api_ticket_comment_category') + router.register('software', software.SoftwareViewSet, basename='software') diff --git a/app/api/views/core/ticket_comment_categories.py b/app/api/views/core/ticket_comment_categories.py new file mode 100644 index 000000000..94cf32211 --- /dev/null +++ b/app/api/views/core/ticket_comment_categories.py @@ -0,0 +1,79 @@ +from django.shortcuts import get_object_or_404 + +from drf_spectacular.utils import extend_schema, OpenApiResponse + +from rest_framework import generics, viewsets + +from access.mixin import OrganizationMixin + +from api.serializers.core.ticket_comment_category import TicketCommentCategory, TicketCommentCategorySerializer +from api.views.mixin import OrganizationPermissionAPI + + + +class View(OrganizationMixin, viewsets.ModelViewSet): + + permission_classes = [ + OrganizationPermissionAPI + ] + + queryset = TicketCommentCategory.objects.all() + + serializer_class = TicketCommentCategorySerializer + + + @extend_schema( + summary='Create a ticket comment category', + request = TicketCommentCategorySerializer, + responses = { + 201: OpenApiResponse(description='Ticket category created', response=TicketCommentCategorySerializer), + 403: OpenApiResponse(description='User tried to edit field they dont have access to'), + } + ) + def create(self, request, *args, **kwargs): + + return super().create(request, *args, **kwargs) + + + @extend_schema( + summary='Fetch all of the ticket comment categories', + methods=["GET"], + responses = { + 200: OpenApiResponse(description='Success', response=TicketCommentCategorySerializer), + } + ) + def list(self, request, *args, **kwargs): + + return super().list(request, *args, **kwargs) + + + @extend_schema( + summary='Fetch the selected ticket comment category', + methods=["GET"], + responses = { + 200: OpenApiResponse(description='Success', response=TicketCommentCategorySerializer), + } + ) + def retrieve(self, request, *args, **kwargs): + + return super().retrieve(request, *args, **kwargs) + + + @extend_schema( + summary='Update a ticket comment category', + methods=["PUT"], + responses = { + 200: OpenApiResponse(description='Ticket comment updated', response=TicketCommentCategorySerializer), + 403: OpenApiResponse(description='User tried to edit field they dont have access to'), + } + ) + def update(self, request, *args, **kwargs): + + return super().update(request, *args, **kwargs) + + + def get_view_name(self): + if self.detail: + return "Ticket Comment Category" + + return 'Ticket Comment Categories' diff --git a/app/api/views/settings/index.py b/app/api/views/settings/index.py index cea0d7fa6..33d2ab4c2 100644 --- a/app/api/views/settings/index.py +++ b/app/api/views/settings/index.py @@ -38,7 +38,8 @@ def get(self, request, *args, **kwargs): response_data: dict = { "permissions": reverse('API:_settings_permissions', request=request), - "ticket_categories": reverse('API:_api_ticket_category-list', request=request) + "ticket_categories": reverse('API:_api_ticket_category-list', request=request), + "ticket_comment_categories": reverse('API:_api_ticket_comment_category-list', request=request) } return Response(data=response_data,status=status) From 80dc79765112a6bbc4033f17b803177cf8d55e5f Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Sep 2024 13:55:09 +0930 Subject: [PATCH 26/43] test(core): Ticket comment category API permission checks ref: #284 closes #283 --- ..._ticket_comment_category_permission_api.py | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_permission_api.py diff --git a/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_permission_api.py b/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_permission_api.py new file mode 100644 index 000000000..fe0f35ebd --- /dev/null +++ b/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_permission_api.py @@ -0,0 +1,176 @@ +import pytest +import unittest +import requests + +from django.contrib.auth.models import AnonymousUser, User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions import APIPermissions + +from core.models.ticket.ticket_comment_category import TicketCommentCategory + + +class TicketCommentCategoryPermissionsAPI(TestCase, APIPermissions): + + + model = TicketCommentCategory + + app_namespace = 'API' + + url_name = '_api_ticket_comment_category-detail' + + url_list = '_api_ticket_comment_category-list' + + change_data = {'name': 'category'} + + delete_data = {'name': 'software'} + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a software + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + self.item = self.model.objects.create( + organization=organization, + name = 'softwareone' + ) + + + # self.url_kwargs = {'pk': self.item.id} + + self.url_view_kwargs = {'pk': self.item.id} + + self.add_data = {'name': 'software', 'organization': self.organization.id} + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From 34a1a190894666a8d4d6fce02d04a332c7b72593 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Sep 2024 14:34:19 +0930 Subject: [PATCH 27/43] test(core): Project Tenancy object checks ref: #14 #284 --- .../test_project_access_tenancy_object.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 app/project_management/tests/unit/project/test_project_access_tenancy_object.py diff --git a/app/project_management/tests/unit/project/test_project_access_tenancy_object.py b/app/project_management/tests/unit/project/test_project_access_tenancy_object.py new file mode 100644 index 000000000..43218f1df --- /dev/null +++ b/app/project_management/tests/unit/project/test_project_access_tenancy_object.py @@ -0,0 +1,18 @@ +import pytest +import unittest +import requests + +from django.test import TestCase, Client + +from access.tests.abstract.tenancy_object import TenancyObject + +from project_management.models.projects import Project + + + +class ProjectTenancyObject( + TestCase, + TenancyObject +): + + model = Project From 6e566b8840b4772d0b82cc328fadd0ed545d3a14 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Sep 2024 14:34:28 +0930 Subject: [PATCH 28/43] test(core): Project history checks ref: #14 #284 --- .../unit/project/test_project_core_history.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 app/project_management/tests/unit/project/test_project_core_history.py diff --git a/app/project_management/tests/unit/project/test_project_core_history.py b/app/project_management/tests/unit/project/test_project_core_history.py new file mode 100644 index 000000000..799874fd3 --- /dev/null +++ b/app/project_management/tests/unit/project/test_project_core_history.py @@ -0,0 +1,74 @@ +import pytest +import unittest +import requests + +from django.test import TestCase, Client + + +from access.models import Organization + +from core.models.history import History +from core.tests.abstract.history_entry import HistoryEntry +from core.tests.abstract.history_entry_parent_model import HistoryEntryParentItem + +from project_management.models.projects import Project + + + +class ProjectHistory(TestCase, HistoryEntry, HistoryEntryParentItem): + + + model = Project + + + @classmethod + def setUpTestData(self): + """ Setup Test """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.item_create = self.model.objects.create( + name = 'test_item_' + self.model._meta.model_name, + organization = self.organization + ) + + + self.history_create = History.objects.get( + action = History.Actions.ADD[0], + item_pk = self.item_create.pk, + item_class = self.model._meta.model_name, + ) + + + self.item_change = self.item_create + self.item_change.name = 'test_item_' + self.model._meta.model_name + '_changed' + self.item_change.save() + + self.field_after_expected_value = '{"name": "test_item_' + self.model._meta.model_name + '_changed"}' + + self.history_change = History.objects.get( + action = History.Actions.UPDATE[0], + item_pk = self.item_change.pk, + item_class = self.model._meta.model_name, + ) + + self.item_delete = self.model.objects.create( + name = 'test_item_delete_' + self.model._meta.model_name, + organization = self.organization + ) + + self.deleted_pk = self.item_delete.pk + + self.item_delete.delete() + + self.history_delete = History.objects.filter( + item_pk = self.deleted_pk, + item_class = self.model._meta.model_name, + ) + + self.history_delete_children = History.objects.filter( + item_parent_pk = self.deleted_pk, + item_parent_class = self.model._meta.model_name, + ) From b56f3236fd9f6f451549057ea54e71495a416343 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Sep 2024 14:34:38 +0930 Subject: [PATCH 29/43] test(core): Project API permission checks ref: #14 #284 --- .../project/test_project_permission_api.py | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 app/project_management/tests/unit/project/test_project_permission_api.py diff --git a/app/project_management/tests/unit/project/test_project_permission_api.py b/app/project_management/tests/unit/project/test_project_permission_api.py new file mode 100644 index 000000000..fe0f35ebd --- /dev/null +++ b/app/project_management/tests/unit/project/test_project_permission_api.py @@ -0,0 +1,176 @@ +import pytest +import unittest +import requests + +from django.contrib.auth.models import AnonymousUser, User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions import APIPermissions + +from core.models.ticket.ticket_comment_category import TicketCommentCategory + + +class TicketCommentCategoryPermissionsAPI(TestCase, APIPermissions): + + + model = TicketCommentCategory + + app_namespace = 'API' + + url_name = '_api_ticket_comment_category-detail' + + url_list = '_api_ticket_comment_category-list' + + change_data = {'name': 'category'} + + delete_data = {'name': 'software'} + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a software + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + self.item = self.model.objects.create( + organization=organization, + name = 'softwareone' + ) + + + # self.url_kwargs = {'pk': self.item.id} + + self.url_view_kwargs = {'pk': self.item.id} + + self.add_data = {'name': 'software', 'organization': self.organization.id} + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From 9d564ffbb2b1a28f0151349883f7acbf1522af47 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Sep 2024 14:34:47 +0930 Subject: [PATCH 30/43] test(core): Project UI permission checks ref: #14 #284 --- .../unit/project/test_project_permission.py | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 app/project_management/tests/unit/project/test_project_permission.py diff --git a/app/project_management/tests/unit/project/test_project_permission.py b/app/project_management/tests/unit/project/test_project_permission.py new file mode 100644 index 000000000..77b1548a3 --- /dev/null +++ b/app/project_management/tests/unit/project/test_project_permission.py @@ -0,0 +1,187 @@ +# from django.conf import settings +from django.contrib.auth import get_user_model +from django.contrib.auth.models import AnonymousUser, User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import TestCase, Client + +import pytest +import unittest +import requests + +from access.models import Organization, Team, TeamUsers, Permission + +from app.tests.abstract.model_permissions import ModelPermissions + +from project_management.models.projects import Project + + +class ProjectPermissions(TestCase, ModelPermissions): + + model = Project + + app_namespace = 'Project Management' + + url_name_view = '_project_view' + + url_name_add = '_project_add' + + url_name_change = '_project_change' + + url_name_delete = '_project_delete' + + url_delete_response = reverse('Project Management:Projects') + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a category + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + self.item = self.model.objects.create( + organization=organization, + name = 'manufacturerone' + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + # self.url_add_kwargs = {'pk': self.item.id} + + self.add_data = {'name': 'manufacturer', 'organization': self.organization.id} + + self.url_change_kwargs = {'pk': self.item.id} + + self.change_data = {'name': 'manufacturer', 'organization': self.organization.id} + + self.url_delete_kwargs = {'pk': self.item.id} + + self.delete_data = {'name': 'manufacturer'} + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From 1576605acb3e191d7858987c9b9365931d967881 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Sep 2024 14:34:57 +0930 Subject: [PATCH 31/43] test(core): Project view checks ref: #14 #284 --- .../tests/unit/project/test_project_views.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 app/project_management/tests/unit/project/test_project_views.py diff --git a/app/project_management/tests/unit/project/test_project_views.py b/app/project_management/tests/unit/project/test_project_views.py new file mode 100644 index 000000000..70388800f --- /dev/null +++ b/app/project_management/tests/unit/project/test_project_views.py @@ -0,0 +1,33 @@ +import pytest +import unittest +import requests + +from django.test import TestCase + +from app.tests.abstract.models import PrimaryModel, ModelAdd, ModelChange, ModelDelete + + + +# class TicketCommentViews( +# TestCase, +# PrimaryModel +# ): +class ProjectViews( + TestCase, + PrimaryModel +): + + add_module = 'project_management.views.project' + add_view = 'Add' + + change_module = add_module + change_view = 'Change' + + delete_module = add_module + delete_view = 'Delete' + + display_module = add_module + display_view = 'View' + + index_module = add_module + index_view = 'Index' From 574357b60aca934e7ac331708206bf4760c22d1e Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Sep 2024 14:35:09 +0930 Subject: [PATCH 32/43] test(core): Project tenancy model checks ref: #14 #284 --- .../tests/unit/project/test_project.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 app/project_management/tests/unit/project/test_project.py diff --git a/app/project_management/tests/unit/project/test_project.py b/app/project_management/tests/unit/project/test_project.py new file mode 100644 index 000000000..75c10deae --- /dev/null +++ b/app/project_management/tests/unit/project/test_project.py @@ -0,0 +1,27 @@ +import pytest +# import unittest +# import requests + +from django.test import TestCase + +from app.tests.abstract.models import TenancyModel + +from project_management.models.projects import Project + + +class ProjectModel( + TestCase, + TenancyModel +): + + model = Project + + + # def test_attribute_duration_ticket_value(self): + # """Attribute value test + + # This aattribute calculates the ticket duration from + # it's comments. must return total time in seconds + # """ + + # pass \ No newline at end of file From 2e15e61059ca3db5e8d43157e22b72aaa7748d05 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Sep 2024 14:35:37 +0930 Subject: [PATCH 33/43] fix(project_management): correct project view permissions ref: #14 #284 --- app/project_management/views/project.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/project_management/views/project.py b/app/project_management/views/project.py index cdde66b59..7b6f5f227 100644 --- a/app/project_management/views/project.py +++ b/app/project_management/views/project.py @@ -117,7 +117,9 @@ class Index(IndexView): model = Project - permission_required = 'project_management.view_project' + permission_required = [ + 'project_management.view_project', + ] template_name = 'project_management/project_index.html.j2' @@ -151,8 +153,7 @@ class View(ChangeView): model = Project permission_required = [ - 'itam.view_device', - 'itam.change_device' + 'project_management.view_project' ] template_name = 'project_management/project.html.j2' From 5f3c7296b71b81f3c9059869ee58e5bbf643fb3a Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Sep 2024 14:39:50 +0930 Subject: [PATCH 34/43] feat(project_management): remove requirement for code field to be populated ref: #14 #284 --- ...comment_ticketcommentcategory_and_more.py} | 143 +++++++----------- ...roject_ticket_subscribed_teams_and_more.py | 129 ++++++++++++++++ .../migrations/0001_initial.py | 4 +- app/project_management/models/projects.py | 4 +- 4 files changed, 190 insertions(+), 90 deletions(-) rename app/core/migrations/{0005_ticket_relatedtickets_ticketcategory_ticket_category_and_more.py => 0005_ticketcategory_ticketcomment_ticketcommentcategory_and_more.py} (74%) create mode 100644 app/core/migrations/0006_ticket_project_ticket_subscribed_teams_and_more.py diff --git a/app/core/migrations/0005_ticket_relatedtickets_ticketcategory_ticket_category_and_more.py b/app/core/migrations/0005_ticketcategory_ticketcomment_ticketcommentcategory_and_more.py similarity index 74% rename from app/core/migrations/0005_ticket_relatedtickets_ticketcategory_ticket_category_and_more.py rename to app/core/migrations/0005_ticketcategory_ticketcomment_ticketcommentcategory_and_more.py index 726619de7..9ad5d5d0d 100644 --- a/app/core/migrations/0005_ticket_relatedtickets_ticketcategory_ticket_category_and_more.py +++ b/app/core/migrations/0005_ticketcategory_ticketcomment_ticketcommentcategory_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.8 on 2024-09-13 03:59 +# Generated by Django 5.0.8 on 2024-09-13 05:06 import access.fields import access.models @@ -14,62 +14,11 @@ class Migration(migrations.Migration): dependencies = [ ('access', '0001_initial'), - ('assistance', '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.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), - ('status', models.IntegerField(choices=[(1, 'Draft'), (2, 'New'), (3, 'Assigned'), (6, 'Assigned (Planning)'), (7, 'Pending'), (8, 'Solved'), (4, 'Closed'), (5, 'Invalid'), (10, 'Accepted'), (9, 'Under Observation'), (11, 'Evaluation'), (12, 'Approvals'), (13, 'Testing'), (14, 'Qualification'), (15, 'Applied'), (16, 'Review'), (17, 'Cancelled'), (18, 'Refused')], default=2, help_text='Status of ticket', verbose_name='Status')), - ('title', models.CharField(help_text='Title of the Ticket', max_length=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, help_text='Assign the ticket to a Team(s)', related_name='assigned_teams', to='access.team', verbose_name='Assigned Team(s)')), - ('assigned_users', models.ManyToManyField(blank=True, help_text='Assign the ticket to a User(s)', related_name='assigned_users', to=settings.AUTH_USER_MODEL, verbose_name='Assigned User(s)')), - ('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, 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, 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'), ('add_ticket_project_task', 'Can add a project task'), ('change_ticket_project_task', 'Can change any project task'), ('delete_ticket_project_task', 'Can delete a project task'), ('import_ticket_project_task', 'Can import a project task'), ('purge_ticket_project_task', 'Can purge a project task'), ('triage_ticket_project_task', 'Can triage all project task'), ('view_ticket_project_task', 'Can view all project task')], - }, - ), - 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='TicketCategory', fields=[ @@ -84,9 +33,6 @@ class Migration(migrations.Migration): ('problem', models.BooleanField(default=True, help_text='Use category for problem tickets', verbose_name='Problem Tickets')), ('project_task', models.BooleanField(default=True, help_text='Use category for Project tasks', verbose_name='Project Tasks')), ('request', models.BooleanField(default=True, help_text='Use category for request tickets', verbose_name='Request Tickets')), - ('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, help_text='The Parent Category', null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.ticketcategory', verbose_name='Parent Category')), - ('runbook', models.ForeignKey(blank=True, help_text='The runbook for this category', null=True, on_delete=django.db.models.deletion.SET_NULL, to='assistance.knowledgebase', verbose_name='Runbook')), ], options={ 'verbose_name': 'Ticket Category', @@ -94,10 +40,32 @@ class Migration(migrations.Migration): 'ordering': ['name'], }, ), - migrations.AddField( - model_name='ticket', - name='category', - field=models.ForeignKey(blank=True, help_text='Category for this ticket', null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.ticketcategory', verbose_name='Category'), + migrations.CreateModel( + name='TicketComment', + fields=[ + ('id', models.AutoField(help_text='Comment ID Number', primary_key=True, serialize=False, unique=True, verbose_name='Number')), + ('external_ref', models.IntegerField(blank=True, default=None, help_text='External System reference', null=True, verbose_name='Reference Number')), + ('external_system', models.IntegerField(blank=True, choices=[(1, 'Github'), (2, 'Gitlab'), (9999, 'Custom #1 (Imported)'), (9998, 'Custom #2 (Imported)'), (9997, 'Custom #3 (Imported)'), (9996, 'Custom #4 (Imported)'), (9995, 'Custom #5 (Imported)'), (9994, 'Custom #6 (Imported)'), (9993, 'Custom #7 (Imported)'), (9992, 'Custom #8 (Imported)'), (9991, 'Custom #9 (Imported)')], default=None, help_text='External system this item derives', null=True, verbose_name='External System')), + ('comment_type', models.IntegerField(choices=[(1, 'Action'), (2, 'Comment'), (3, 'Task'), (4, 'Notification'), (5, 'Solution')], default=2, help_text='The type of comment this is', validators=[core.models.ticket.ticket_comment.TicketComment.validation_comment_type], verbose_name='Type')), + ('body', models.TextField(default=None, help_text='Comment contents', verbose_name='Comment')), + ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), + ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), + ('private', models.BooleanField(default=False, help_text='Is this comment private', verbose_name='Private')), + ('duration', models.IntegerField(default=0, help_text='Time spent in seconds', verbose_name='Duration')), + ('is_template', models.BooleanField(default=False, help_text='Is this comment a template', verbose_name='Template')), + ('source', models.IntegerField(choices=[(1, 'Direct'), (2, 'E-Mail'), (3, 'Helpdesk'), (4, 'Phone')], default=1, help_text='Origin type for this comment', verbose_name='Source')), + ('status', models.IntegerField(choices=[(1, 'To Do'), (2, 'Done')], default=1, help_text='Status of comment', verbose_name='Status')), + ('date_closed', models.DateTimeField(blank=True, help_text='Date ticket closed', null=True, verbose_name='Closed Date')), + ('planned_start_date', models.DateTimeField(blank=True, help_text='Planned start date.', null=True, verbose_name='Planned Start Date')), + ('planned_finish_date', models.DateTimeField(blank=True, help_text='Planned finish date', null=True, verbose_name='Planned Finish Date')), + ('real_start_date', models.DateTimeField(blank=True, help_text='Real start date', null=True, verbose_name='Real Start Date')), + ('real_finish_date', models.DateTimeField(blank=True, help_text='Real finish date', null=True, verbose_name='Real Finish Date')), + ], + options={ + 'verbose_name': 'Comment', + 'verbose_name_plural': 'Comments', + 'ordering': ['ticket', 'parent_id'], + }, ), migrations.CreateModel( name='TicketCommentCategory', @@ -112,9 +80,6 @@ class Migration(migrations.Migration): ('notification', models.BooleanField(default=True, help_text='Use category for notification comment', verbose_name='Notification Comment')), ('solution', models.BooleanField(default=True, help_text='Use category for solution comment', verbose_name='Solution Comment')), ('task', models.BooleanField(default=True, help_text='Use category for task comment', verbose_name='Task Comment')), - ('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, help_text='The Parent Category', null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.ticketcommentcategory', verbose_name='Parent Category')), - ('runbook', models.ForeignKey(blank=True, help_text='The runbook for this category', null=True, on_delete=django.db.models.deletion.SET_NULL, to='assistance.knowledgebase', verbose_name='Runbook')), ], options={ 'verbose_name': 'Ticket Comment Category', @@ -122,44 +87,48 @@ class Migration(migrations.Migration): 'ordering': ['name'], }, ), - migrations.AlterUniqueTogether( - name='ticket', - unique_together={('external_system', 'external_ref')}, + migrations.CreateModel( + name='RelatedTickets', + fields=[ + ('id', models.AutoField(help_text='Ticket ID Number', primary_key=True, serialize=False, unique=True, verbose_name='Number')), + ('how_related', models.IntegerField(choices=[(1, 'Related'), (2, 'Blocks'), (3, 'Blocked By')], help_text='How is the ticket related', verbose_name='How Related')), + ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])), + ], + options={ + 'ordering': ['id'], + }, ), migrations.CreateModel( - name='TicketComment', + name='Ticket', fields=[ - ('id', models.AutoField(help_text='Comment ID Number', primary_key=True, serialize=False, unique=True, verbose_name='Number')), - ('external_ref', models.IntegerField(blank=True, default=None, help_text='External System reference', null=True, verbose_name='Reference Number')), - ('external_system', models.IntegerField(blank=True, choices=[(1, 'Github'), (2, 'Gitlab'), (9999, 'Custom #1 (Imported)'), (9998, 'Custom #2 (Imported)'), (9997, 'Custom #3 (Imported)'), (9996, 'Custom #4 (Imported)'), (9995, 'Custom #5 (Imported)'), (9994, 'Custom #6 (Imported)'), (9993, 'Custom #7 (Imported)'), (9992, 'Custom #8 (Imported)'), (9991, 'Custom #9 (Imported)')], default=None, help_text='External system this item derives', null=True, verbose_name='External System')), - ('comment_type', models.IntegerField(choices=[(1, 'Action'), (2, 'Comment'), (3, 'Task'), (4, 'Notification'), (5, 'Solution')], default=2, help_text='The type of comment this is', validators=[core.models.ticket.ticket_comment.TicketComment.validation_comment_type], verbose_name='Type')), - ('body', models.TextField(default=None, help_text='Comment contents', verbose_name='Comment')), + ('id', models.AutoField(help_text='Ticket ID Number', primary_key=True, serialize=False, unique=True, verbose_name='Number')), ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), - ('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')), + ('status', models.IntegerField(choices=[(1, 'Draft'), (2, 'New'), (3, 'Assigned'), (6, 'Assigned (Planning)'), (7, 'Pending'), (8, 'Solved'), (4, 'Closed'), (5, 'Invalid'), (10, 'Accepted'), (9, 'Under Observation'), (11, 'Evaluation'), (12, 'Approvals'), (13, 'Testing'), (14, 'Qualification'), (15, 'Applied'), (16, 'Review'), (17, 'Cancelled'), (18, 'Refused')], default=2, help_text='Status of ticket', verbose_name='Status')), + ('title', models.CharField(help_text='Title of the Ticket', max_length=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, help_text='Assign the ticket to a Team(s)', related_name='assigned_teams', to='access.team', verbose_name='Assigned Team(s)')), + ('assigned_users', models.ManyToManyField(blank=True, help_text='Assign the ticket to a User(s)', related_name='assigned_users', to=settings.AUTH_USER_MODEL, verbose_name='Assigned User(s)')), + ('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])), - ('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.SET_NULL, related_name='comment_template', to='core.ticketcomment', verbose_name='Template')), - ('ticket', models.ForeignKey(blank=True, default=None, help_text='Ticket this comment belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='core.ticket', validators=[core.models.ticket.ticket_comment.TicketComment.validation_ticket_id], verbose_name='Ticket')), - ('user', models.ForeignKey(blank=True, help_text='Who made the comment', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='comment_user', to=settings.AUTH_USER_MODEL, verbose_name='User')), - ('category', models.ForeignKey(blank=True, default=None, help_text='Category of the comment', null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.ticketcommentcategory', verbose_name='Category')), ], options={ - 'verbose_name': 'Comment', - 'verbose_name_plural': 'Comments', - 'ordering': ['ticket', 'parent_id'], - 'unique_together': {('external_system', 'external_ref')}, + 'verbose_name': 'Ticket', + 'verbose_name_plural': 'Tickets', + 'ordering': ['id'], + 'permissions': [('add_ticket_request', 'Can add a request ticket'), ('change_ticket_request', 'Can change any request ticket'), ('delete_ticket_request', 'Can delete a request ticket'), ('import_ticket_request', 'Can import a request ticket'), ('purge_ticket_request', 'Can purge a request ticket'), ('triage_ticket_request', 'Can triage all request ticket'), ('view_ticket_request', 'Can view all request ticket'), ('add_ticket_incident', 'Can add a incident ticket'), ('change_ticket_incident', 'Can change any incident ticket'), ('delete_ticket_incident', 'Can delete a incident ticket'), ('import_ticket_incident', 'Can import a incident ticket'), ('purge_ticket_incident', 'Can purge a incident ticket'), ('triage_ticket_incident', 'Can triage all incident ticket'), ('view_ticket_incident', 'Can view all incident ticket'), ('add_ticket_problem', 'Can add a problem ticket'), ('change_ticket_problem', 'Can change any problem ticket'), ('delete_ticket_problem', 'Can delete a problem ticket'), ('import_ticket_problem', 'Can import a problem ticket'), ('purge_ticket_problem', 'Can purge a problem ticket'), ('triage_ticket_problem', 'Can triage all problem ticket'), ('view_ticket_problem', 'Can view all problem ticket'), ('add_ticket_change', 'Can add a change ticket'), ('change_ticket_change', 'Can change any change ticket'), ('delete_ticket_change', 'Can delete a change ticket'), ('import_ticket_change', 'Can import a change ticket'), ('purge_ticket_change', 'Can purge a change ticket'), ('triage_ticket_change', 'Can triage all change ticket'), ('view_ticket_change', 'Can view all change ticket'), ('add_ticket_project_task', 'Can add a project task'), ('change_ticket_project_task', 'Can change any project task'), ('delete_ticket_project_task', 'Can delete a project task'), ('import_ticket_project_task', 'Can import a project task'), ('purge_ticket_project_task', 'Can purge a project task'), ('triage_ticket_project_task', 'Can triage all project task'), ('view_ticket_project_task', 'Can view all project task')], }, ), ] diff --git a/app/core/migrations/0006_ticket_project_ticket_subscribed_teams_and_more.py b/app/core/migrations/0006_ticket_project_ticket_subscribed_teams_and_more.py new file mode 100644 index 000000000..834f0003f --- /dev/null +++ b/app/core/migrations/0006_ticket_project_ticket_subscribed_teams_and_more.py @@ -0,0 +1,129 @@ +# Generated by Django 5.0.8 on 2024-09-13 05:06 + +import access.models +import core.models.ticket.ticket_comment +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('access', '0001_initial'), + ('assistance', '0001_initial'), + ('core', '0005_ticketcategory_ticketcomment_ticketcommentcategory_and_more'), + ('project_management', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='ticket', + name='project', + field=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'), + ), + migrations.AddField( + model_name='ticket', + name='subscribed_teams', + field=models.ManyToManyField(blank=True, help_text='Subscribe a Team(s) to the ticket to receive updates', related_name='subscribed_teams', to='access.team', verbose_name='Subscribed Team(s)'), + ), + migrations.AddField( + model_name='ticket', + name='subscribed_users', + field=models.ManyToManyField(blank=True, help_text='Subscribe a User(s) to the ticket to receive updates', related_name='subscribed_users', to=settings.AUTH_USER_MODEL, verbose_name='Subscribed User(s)'), + ), + migrations.AddField( + model_name='relatedtickets', + name='from_ticket_id', + field=models.ForeignKey(help_text='This Ticket', on_delete=django.db.models.deletion.CASCADE, related_name='from_ticket_id', to='core.ticket', verbose_name='Ticket'), + ), + migrations.AddField( + model_name='relatedtickets', + name='to_ticket_id', + field=models.ForeignKey(help_text='The Related Ticket', on_delete=django.db.models.deletion.CASCADE, related_name='to_ticket_id', to='core.ticket', verbose_name='Related Ticket'), + ), + migrations.AddField( + model_name='ticketcategory', + name='organization', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]), + ), + migrations.AddField( + model_name='ticketcategory', + name='parent', + field=models.ForeignKey(blank=True, help_text='The Parent Category', null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.ticketcategory', verbose_name='Parent Category'), + ), + migrations.AddField( + model_name='ticketcategory', + name='runbook', + field=models.ForeignKey(blank=True, help_text='The runbook for this category', null=True, on_delete=django.db.models.deletion.SET_NULL, to='assistance.knowledgebase', verbose_name='Runbook'), + ), + migrations.AddField( + model_name='ticket', + name='category', + field=models.ForeignKey(blank=True, help_text='Category for this ticket', null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.ticketcategory', verbose_name='Category'), + ), + migrations.AddField( + model_name='ticketcomment', + name='organization', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]), + ), + migrations.AddField( + model_name='ticketcomment', + name='parent', + field=models.ForeignKey(blank=True, default=None, help_text='Parent ID for creating discussion threads', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='core.ticketcomment', verbose_name='Parent Comment'), + ), + migrations.AddField( + model_name='ticketcomment', + name='responsible_team', + field=models.ForeignKey(blank=True, default=None, help_text='Team whom is responsible for the completion of comment', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='comment_responsible_team', to='access.team', verbose_name='Responsible Team'), + ), + migrations.AddField( + model_name='ticketcomment', + name='responsible_user', + field=models.ForeignKey(blank=True, default=None, help_text='User whom is responsible for the completion of comment', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='comment_responsible_user', to=settings.AUTH_USER_MODEL, verbose_name='Responsible User'), + ), + migrations.AddField( + model_name='ticketcomment', + name='template', + field=models.ForeignKey(blank=True, default=None, help_text='Comment Template to use', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='comment_template', to='core.ticketcomment', verbose_name='Template'), + ), + migrations.AddField( + model_name='ticketcomment', + name='ticket', + field=models.ForeignKey(blank=True, default=None, help_text='Ticket this comment belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='core.ticket', validators=[core.models.ticket.ticket_comment.TicketComment.validation_ticket_id], verbose_name='Ticket'), + ), + migrations.AddField( + model_name='ticketcomment', + name='user', + field=models.ForeignKey(blank=True, help_text='Who made the comment', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='comment_user', to=settings.AUTH_USER_MODEL, verbose_name='User'), + ), + migrations.AddField( + model_name='ticketcommentcategory', + name='organization', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]), + ), + migrations.AddField( + model_name='ticketcommentcategory', + name='parent', + field=models.ForeignKey(blank=True, help_text='The Parent Category', null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.ticketcommentcategory', verbose_name='Parent Category'), + ), + migrations.AddField( + model_name='ticketcommentcategory', + name='runbook', + field=models.ForeignKey(blank=True, help_text='The runbook for this category', null=True, on_delete=django.db.models.deletion.SET_NULL, to='assistance.knowledgebase', verbose_name='Runbook'), + ), + migrations.AddField( + model_name='ticketcomment', + name='category', + field=models.ForeignKey(blank=True, default=None, help_text='Category of the comment', null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.ticketcommentcategory', verbose_name='Category'), + ), + migrations.AlterUniqueTogether( + name='ticket', + unique_together={('external_system', 'external_ref')}, + ), + migrations.AlterUniqueTogether( + name='ticketcomment', + unique_together={('external_system', 'external_ref')}, + ), + ] diff --git a/app/project_management/migrations/0001_initial.py b/app/project_management/migrations/0001_initial.py index 42e9a3264..5eedf1007 100644 --- a/app/project_management/migrations/0001_initial.py +++ b/app/project_management/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.8 on 2024-09-10 06:20 +# Generated by Django 5.0.8 on 2024-09-13 05:06 import access.fields import access.models @@ -28,7 +28,7 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=50, unique=True)), ('slug', access.fields.AutoSlugField()), ('description', models.TextField(blank=True, default=None, null=True)), - ('code', models.CharField(help_text='Project Code', max_length=25, unique=True)), + ('code', models.CharField(blank=True, help_text='Project Code', max_length=25, null=True, unique=True, verbose_name='Project Code')), ('planned_start_date', models.DateTimeField(blank=True, help_text='When the project is planned to have been started by.', null=True, verbose_name='Planned Start Date')), ('planned_finish_date', models.DateTimeField(blank=True, help_text='When the project is planned to be finished by.', null=True, verbose_name='Planned Finish Date')), ('real_start_date', models.DateTimeField(blank=True, help_text='When work commenced on the project.', null=True, verbose_name='Real Start Date')), diff --git a/app/project_management/models/projects.py b/app/project_management/models/projects.py index 0dfe0e53c..2fffd511f 100644 --- a/app/project_management/models/projects.py +++ b/app/project_management/models/projects.py @@ -41,10 +41,12 @@ class Meta: # project_type code = models.CharField( - blank = False, + blank = True, help_text = 'Project Code', max_length = 25, + null = True, unique = True, + verbose_name = 'Project Code', ) planned_start_date = models.DateTimeField( From 6e7e6587c249db652768280873f802201a61781a Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Sep 2024 15:01:41 +0930 Subject: [PATCH 35/43] docs: update roadmap ref: #284 --- docs/projects/centurion_erp/index.md | 44 +++++++++++++--------------- mkdocs.yml | 3 ++ 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/docs/projects/centurion_erp/index.md b/docs/projects/centurion_erp/index.md index d17d2381f..756fa9f9c 100644 --- a/docs/projects/centurion_erp/index.md +++ b/docs/projects/centurion_erp/index.md @@ -28,14 +28,13 @@ Whilst there are many Enterprise Rescource Planning (ERP) applications, Centurio Centurion ERP contains the following modules: -- [Companion Ansible Collection](../ansible/collections/centurion/index.md) - -- [Configuration Management](./user/config_management/index.md) +- Change Management -- [IT Asset Management (ITAM)](./user/itam/index.md) +- [Cluster Management](./user/itim/cluster.md) -- [Knowledge Base](./user/assistance/knowledge_base.md) +- [Companion Ansible Collection](../ansible/collections/centurion/index.md) +- [Configuration Management](./user/config_management/index.md) - **Core Features:** @@ -51,6 +50,21 @@ Centurion ERP contains the following modules: - [Single Sign-On {SSO}](./user/configuration.md#single-sign-on) +- Incident Management + +- [IT Asset Management (ITAM)](./user/itam/index.md) + +- **Knowledge Management:** + + - [Knowledge Base](./user/assistance/knowledge_base.md) + +- Problem Management + +- [Project Management](./user/project_management/index.md) + +- Request Management + +- [Service Management](./user/itim/service.md) ## Documentation @@ -67,7 +81,7 @@ Specific features for a module can be found on the module's documentation un the ## Development -It's important to us that Centurion ERP remaining stable. To assist with this we do test Centurion during it's development cycle. Testing reports are available and can be viewed from [Gitlab](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests) on each Merge Request. You will find a link to the last report conducted as part of that merge request just below the Merge Request's description. +It's important to us that Centurion ERP remaining stable. To assist with this we do test Centurion during it's development cycle. Testing reports are available and can be viewed from [Github](https://github.com/nofusscomputing/centurion_erp/actions/workflows/ci.yaml). !!! info If you find any test that is less than sufficient, or does not exist; please let us know. If you know a better way of doing the test, even better. We welcome your contribution/feedback. @@ -75,7 +89,7 @@ It's important to us that Centurion ERP remaining stable. To assist with this we ## Roadmap / Planned Features -Below is a list of modules/features we intend to add to Centurion. To find out what we are working on now please view the [Milestones](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/milestones) on Gitlab. +Below is a list of modules/features we intend to add to Centurion. To find out what we are working on now please view the [Milestones](https://github.com/nofusscomputing/centurion_erp/milestones) on Github. - **Planned Modules:** @@ -85,10 +99,6 @@ Below is a list of modules/features we intend to add to Centurion. To find out w - Asset Management _[see #89](https://github.com/nofusscomputing/centurion_erp/issues/88)_ - - Change Management _[see #90](https://github.com/nofusscomputing/centurion_erp/issues/90)_ - - - Config Management - - Core - Location Management (Regions, Sites and Locations) _[see #62](https://github.com/nofusscomputing/centurion_erp/issues/62)_ @@ -103,20 +113,14 @@ Below is a list of modules/features we intend to add to Centurion. To find out w - Human Resource Management _[see #92](https://github.com/nofusscomputing/centurion_erp/issues/92)_ - - Incident Management _[see #93](https://github.com/nofusscomputing/centurion_erp/issues/93)_ - - IT Asset Management (ITAM) - Licence Management _[see #4](https://github.com/nofusscomputing/centurion_erp/issues/4)_ - IT Infrastructure Management (ITIM) _[see #61](https://github.com/nofusscomputing/centurion_erp/issues/61)_ - - Cluster Management _[see #71](https://github.com/nofusscomputing/centurion_erp/issues/71)_ - - Database Management _[see #72](https://github.com/nofusscomputing/centurion_erp/issues/72)_ - - Service Management _[see #19](https://github.com/nofusscomputing/centurion_erp/issues/19)_ - - Software Package Management _[see #96](https://github.com/nofusscomputing/centurion_erp/issues/96)_ - Role Management _[see #70](https://github.com/nofusscomputing/centurion_erp/issues/70)_ @@ -131,12 +135,6 @@ Below is a list of modules/features we intend to add to Centurion. To find out w - Supplier Management _[see #123](https://github.com/nofusscomputing/centurion_erp/issues/123)_ - - Project Management _[see #14](https://github.com/nofusscomputing/centurion_erp/issues/14)_ - - - Problem Management _[see #95](https://github.com/nofusscomputing/centurion_erp/issues/95)_ - - - Request Management _[see #96](https://github.com/nofusscomputing/centurion_erp/issues/96)_ - - **Planned Integrations:** diff --git a/mkdocs.yml b/mkdocs.yml index 85a345365..fa8915c9a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,6 +5,9 @@ docs_dir: 'docs' repo_name: Centurion ERP repo_url: https://github.com/nofusscomputing/centurion_erp edit_uri: '/edit/development/docs/' +theme: + icon: + repo: fontawesome/brands/github plugins: mkdocstrings: From 9cb3afeb30b1a8dccbfdd3fea979619d77698dee Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Sep 2024 16:19:42 +0930 Subject: [PATCH 36/43] feat(core): Disable HTML tag rendering for markdown ref: #284 closes #271 --- app/core/lib/markdown.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/core/lib/markdown.py b/app/core/lib/markdown.py index 9e68ab397..1118bc974 100644 --- a/app/core/lib/markdown.py +++ b/app/core/lib/markdown.py @@ -49,7 +49,7 @@ def render_markdown(self, markdown_text): md = ( MarkdownIt( - config = "commonmark", + config = "js-default", options_update={ 'linkify': True, 'highlight': self.highlight_func, From c45aae7048afd27eb994186534f78a921af598c1 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Sep 2024 16:22:43 +0930 Subject: [PATCH 37/43] chore: docs linting errors ref: #284 --- docs/projects/centurion_erp/index.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/projects/centurion_erp/index.md b/docs/projects/centurion_erp/index.md index 756fa9f9c..14eb0dfb4 100644 --- a/docs/projects/centurion_erp/index.md +++ b/docs/projects/centurion_erp/index.md @@ -66,6 +66,7 @@ Centurion ERP contains the following modules: - [Service Management](./user/itim/service.md) + ## Documentation Documentation is broken down into three areas, they are: @@ -81,7 +82,7 @@ Specific features for a module can be found on the module's documentation un the ## Development -It's important to us that Centurion ERP remaining stable. To assist with this we do test Centurion during it's development cycle. Testing reports are available and can be viewed from [Github](https://github.com/nofusscomputing/centurion_erp/actions/workflows/ci.yaml). +It's important to us that Centurion ERP remaining stable. To assist with this we do test Centurion during it's development cycle. Testing reports are available and can be viewed from [Github](https://github.com/nofusscomputing/centurion_erp/actions/workflows/ci.yaml). !!! info If you find any test that is less than sufficient, or does not exist; please let us know. If you know a better way of doing the test, even better. We welcome your contribution/feedback. From a7e99eb5b4bf82da46910b680061f667ffe9351e Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Sep 2024 21:07:35 +0930 Subject: [PATCH 38/43] refactor: reduce action comment spacing ref: #24 #284 --- app/project-static/ticketing.css | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/project-static/ticketing.css b/app/project-static/ticketing.css index ed767a17c..19f00bbeb 100644 --- a/app/project-static/ticketing.css +++ b/app/project-static/ticketing.css @@ -221,13 +221,21 @@ } -#ticket-comments li { +#ticket-comments li:not(#Action) { line-height: 30px; margin: 0px; margin-bottom: 30px; padding-left: 10px; } +#ticket-comments li#Action { + line-height: 30px; + margin: 0px; + margin-bottom: -10px; + margin-top: -20px; + padding-left: 10px; +} + #data-block h3 { background-color: #177ee6; From c3307152e8ec696a9a656c8a1471b27bd461488e Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Sep 2024 21:58:19 +0930 Subject: [PATCH 39/43] feat(core): Add slash command `/spend` for ticket and ticket comments ref: #284 closes #286 --- app/core/forms/ticket_comment.py | 10 ++- app/core/lib/slash_commands/__init__.py | 37 ++++++++++ app/core/lib/slash_commands/duration.py | 91 ++++++++++++++++++++++++ app/core/models/ticket/ticket.py | 11 +++ app/core/models/ticket/ticket_comment.py | 5 ++ 5 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 app/core/lib/slash_commands/__init__.py create mode 100644 app/core/lib/slash_commands/duration.py diff --git a/app/core/forms/ticket_comment.py b/app/core/forms/ticket_comment.py index 23c2aa199..0a93265e8 100644 --- a/app/core/forms/ticket_comment.py +++ b/app/core/forms/ticket_comment.py @@ -61,6 +61,8 @@ def __init__(self, request, *args, **kwargs): self.fields['body'].widget.attrs = {'style': "height: 800px; width: 900px"} + self.fields['duration'].widget = self.fields['duration'].hidden_widget() + self.fields['user'].initial = kwargs['user'].pk self.fields['user'].widget = self.fields['user'].hidden_widget() @@ -69,10 +71,16 @@ def __init__(self, request, *args, **kwargs): self.fields['parent'].widget = self.fields['parent'].hidden_widget() self.fields['comment_type'].widget = self.fields['comment_type'].hidden_widget() - if not self._has_import_permission or not self._has_triage_permission: + if not( self._has_import_permission or self._has_triage_permission or request.user.is_superuser ): + self.fields['source'].initial = TicketComment.CommentSource.HELPDESK + self.fields['source'].widget = self.fields['source'].hidden_widget() + else: + + self.fields['source'].initial = TicketComment.CommentSource.DIRECT + diff --git a/app/core/lib/slash_commands/__init__.py b/app/core/lib/slash_commands/__init__.py new file mode 100644 index 000000000..ec5666c23 --- /dev/null +++ b/app/core/lib/slash_commands/__init__.py @@ -0,0 +1,37 @@ +import re + +from core.lib.slash_commands.duration import Duration + + +class SlashCommands( + Duration +): + """Slash Commands Base Class + + This class in intended to be included in the following models: + + - Ticket + + - TicketComment + """ + + + def slash_command(self, markdown:str) -> str: + """ Slash Commands Processor + + Markdown text that contains a slash command is passed to this function and on the processing + of any valid slash command, the slash command will be removed from the markdown. + + If any error occurs when attempting to process the slash command, it will not be removed from + the markdown. This is by design so that the "errored" slash command can be inspected. + + Args: + markdown (str): un-processed Markdown + + Returns: + str: Markdown without the slash command text. + """ + + markdown = re.sub(self.time_spent, self.command_duration, markdown) + + return markdown diff --git a/app/core/lib/slash_commands/duration.py b/app/core/lib/slash_commands/duration.py new file mode 100644 index 000000000..1d9315b92 --- /dev/null +++ b/app/core/lib/slash_commands/duration.py @@ -0,0 +1,91 @@ +import re + + +class Duration: + """Duration Slash Command + + The command keyword is `spend` and you can also use `spent`. The formatting for the time + after the command, is then either `h`, `m`, `s` for hours, minutes and seconds respectively. + + Valid commands are as follows: + + - /spend 1h1ms + + - /spend 1h 1m 1s + + For this command to process the following conditions must be met: + + - There is either a `` (`\n`) or a `` char immediatly before the slash `/` + + - There is a `` char after the command keyword, i.e. `/spend1h` + + - _Optional_ `` char between the time blocks. + """ + + + time_spent: str = r'[\s|\n]\/(?P[spend|spent]+)\s(?P