diff --git a/app/app/settings.py b/app/app/settings.py index 27ca8e51..76364fad 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -360,8 +360,8 @@ # Apps Under Development INSTALLED_APPS += [ - 'project_management.apps.ProjectManagementConfig', - 'information.apps.InformationConfig', + # 'project_management.apps.ProjectManagementConfig', + # 'assistance.apps.AssistanceConfig', ] diff --git a/app/app/urls.py b/app/app/urls.py index 3c4501a2..c810ae47 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -24,7 +24,7 @@ from .views import home -from core.views import history +from core.views import history, ticket from settings.views import user_settings @@ -50,6 +50,13 @@ path("history//", history.View.as_view(), name='_history'), re_path(r'^static/(?P.*)$', serve,{'document_root': settings.STATIC_ROOT}), + + path('ticket/', ticket.View.as_view(), name="_ticket"), + + path('ticket/add', ticket.Add.as_view(), name="_ticket"), + + path('ticket//add', ticket.Add.as_view(), name="_ticket"), + ] @@ -74,10 +81,10 @@ urlpatterns += [ path("__debug__/", include("debug_toolbar.urls"), name='_debug'), - path("project_management/", include("project_management.urls")), + # path("project_management/", include("project_management.urls")), # Apps Under Development - path("itim/", include("itim.urls")), - path("information/", include("information.urls")), + # path("itim/", include("itim.urls")), + # path("information/", include("information.urls")), ] # must be after above diff --git a/app/assistance/urls.py b/app/assistance/urls.py index 9d0e17be..b6e085a1 100644 --- a/app/assistance/urls.py +++ b/app/assistance/urls.py @@ -2,6 +2,8 @@ from assistance.views import knowledge_base +from core.views import ticket + app_name = "Assistance" urlpatterns = [ @@ -12,4 +14,9 @@ path("information//delete", knowledge_base.Delete.as_view(), name="_knowledge_base_delete"), path("information/", knowledge_base.View.as_view(), name="_knowledge_base_view"), + path('ticket/', ticket.Index.as_view(), name="Requests"), + path('ticket//add', ticket.Add.as_view(), name="_ticket_request_add"), + path('ticket///edit', ticket.Change.as_view(), name="_ticket_request_change"), + path('ticket//', ticket.View.as_view(), name="_ticket_request_view"), + ] diff --git a/app/core/forms/ticket.py b/app/core/forms/ticket.py new file mode 100644 index 00000000..153aa38f --- /dev/null +++ b/app/core/forms/ticket.py @@ -0,0 +1,164 @@ +from django import forms +from django.db.models import Q + +from app import settings + +from core.forms.common import CommonModelForm + +from core.models.ticket.ticket import Ticket, RelatedTickets + + +class TicketForm(CommonModelForm): + + prefix = 'ticket' + + class Meta: + model = Ticket + fields = '__all__' + + + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) + + self.fields['planned_start_date'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local', 'format': "%Y-%m-%dT%H:%M"}) + self.fields['planned_start_date'].input_formats = settings.DATETIME_FORMAT + self.fields['planned_start_date'].format="%Y-%m-%dT%H:%M" + + self.fields['planned_finish_date'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local'}) + self.fields['planned_finish_date'].input_formats = settings.DATETIME_FORMAT + self.fields['planned_finish_date'].format="%Y-%m-%dT%H:%M" + + self.fields['real_start_date'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local'}) + self.fields['real_start_date'].input_formats = settings.DATETIME_FORMAT + self.fields['real_start_date'].format="%Y-%m-%dT%H:%M" + + self.fields['real_finish_date'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local'}) + self.fields['real_finish_date'].input_formats = settings.DATETIME_FORMAT + self.fields['real_finish_date'].format="%Y-%m-%dT%H:%M" + + self.fields['description'].widget.attrs = {'style': "height: 800px; width: 900px"} + + # choices = kwargs.pop('camp_dates_choices', ()) + + + self.fields['opened_by'].initial = kwargs['user'].pk + + # self.fields['ticket_type'] = forms.IntegerField( + # ) + + # self.fields['ticket_type'].widget = self.fields['ticket_type'].hidden_widget() + + + original_fields = self.fields.copy() + ticket_type = [] + + if kwargs['initial']['ticket_type'] == 'request': + + ticket_type = self.Meta.model.fields_itsm_request + + self.fields['status'].choices = self.Meta.model.TicketStatus.Request + + self.fields['ticket_type'].initial = self.Meta.model.TicketType.REQUEST + + elif kwargs['initial']['ticket_type'] == 'incident': + + ticket_type = self.Meta.model.fields_itsm_incident + + self.fields['status'].choices = self.Meta.model.TicketStatus.Incident + + self.fields['ticket_type'].initial = self.Meta.model.TicketType.INCIDENT + + elif kwargs['initial']['ticket_type'] == 'problem': + + ticket_type = self.Meta.model.fields_itsm_problem + + self.fields['status'].choices = self.Meta.model.TicketStatus.Problem + + self.fields['ticket_type'].initial = self.Meta.model.TicketType.PROBLEM + + elif kwargs['initial']['ticket_type'] == 'change': + + ticket_type = self.Meta.model.fields_itsm_change + + self.fields['status'].choices = self.Meta.model.TicketStatus.Change + + self.fields['ticket_type'].initial = self.Meta.model.TicketType.CHANGE + + elif kwargs['initial']['ticket_type'] == 'issue': + + ticket_type = self.Meta.model.fields_git_issue + + self.fields['status'].choices = self.Meta.model.TicketStatus.Git + + self.fields['ticket_type'].initial = self.Meta.model.TicketType.ISSUE + + elif kwargs['initial']['ticket_type'] == 'merge': + + ticket_type = self.Meta.model.fields_git_merge + + self.fields['status'].choices = self.Meta.model.TicketStatus.Git + + self.fields['ticket_type'].initial = self.Meta.model.TicketType.MERGE_REQUEST + + elif kwargs['initial']['ticket_type'] == 'project_task': + + ticket_type = self.Meta.model.fields_project_task + + self.fields['status'].choices = self.Meta.model.TicketStatus.ProjectTask + + self.fields['ticket_type'].initial = self.Meta.model.TicketType.PROJECT_TASK + + + if kwargs['user'].is_superuser: + + ticket_type += self.Meta.model.tech_fields + + # else + + # self.fields['opened_by'].widget = self.fields['opened_by'].hidden_widget() + + + for field in original_fields: + + if field not in ticket_type: + + del self.fields[field] + + def clean(self): + + cleaned_data = super().clean() + + return cleaned_data + + def is_valid(self) -> bool: + + is_valid = super().is_valid() + + return is_valid + + + +class DetailForm(CommonModelForm): + + prefix = 'ticket' + + class Meta: + model = Ticket + fields = '__all__' + + + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) + + # self.fields['related_tickts'] = forms.fields.Field( + # # verbose_name = 'Related Tickts', + # ) + + # self.fields['related_tickts'].queryset = RelatedTickets.objects.filter( + # Q(from_ticket_id=self.instance.pk) + # | + # Q(to_ticket_id=self.instance.pk) + # ) + # a ='d' \ No newline at end of file diff --git a/app/core/migrations/0005_ticket_relatedtickets.py b/app/core/migrations/0005_ticket_relatedtickets.py new file mode 100644 index 00000000..36e48062 --- /dev/null +++ b/app/core/migrations/0005_ticket_relatedtickets.py @@ -0,0 +1,73 @@ +# Generated by Django 5.0.7 on 2024-08-23 16:32 + +import access.fields +import access.models +import core.models.ticket.change_ticket +import core.models.ticket.markdown +import core.models.ticket.problem_ticket +import core.models.ticket.request_ticket +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('access', '0001_initial'), + ('core', '0004_notes_service'), + ('project_management', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Ticket', + fields=[ + ('is_global', models.BooleanField(default=False)), + ('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')), + ('id', models.AutoField(help_text='Ticket ID Number', primary_key=True, serialize=False, unique=True, verbose_name='Number')), + ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), + ('modified', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), + ('status', models.IntegerField(choices=[(1, 'Draft'), (2, 'New'), (3, 'Assigned'), (6, 'Assigned (Planning)'), (7, 'Pending'), (8, 'Solved'), (4, 'Closed'), (5, 'Invalid')], default=2, help_text='Status of ticket', verbose_name='Status')), + ('title', models.CharField(help_text='Title of the Ticket', max_length=50, unique=True, verbose_name='Title')), + ('description', models.TextField(default=None, help_text='Ticket Description', verbose_name='Description')), + ('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')], 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')], default=None, help_text='The type of ticket this is', null=True, verbose_name='Type')), + ('is_deleted', models.BooleanField(default=False, help_text='Is the ticket deleted? And ready to be purged', verbose_name='Deleted')), + ('date_closed', models.DateTimeField(blank=True, help_text='Date ticket closed', null=True, verbose_name='Closed Date')), + ('planned_start_date', models.DateTimeField(blank=True, help_text='Planned start date.', null=True, verbose_name='Planned Start Date')), + ('planned_finish_date', models.DateTimeField(blank=True, help_text='Planned finish date', null=True, verbose_name='Planned Finish Date')), + ('real_start_date', models.DateTimeField(blank=True, help_text='Real start date', null=True, verbose_name='Real Start Date')), + ('real_finish_date', models.DateTimeField(blank=True, help_text='Real finish date', null=True, verbose_name='Real Finish Date')), + ('assigned_teams', models.ManyToManyField(blank=True, default=True, help_text='Assign the ticket to a Team(s)', related_name='assigned_teams', to='access.team', verbose_name='Assigned Team(s)')), + ('assigned_users', models.ManyToManyField(blank=True, default=True, help_text='Assign the ticket to a User(s)', related_name='assigned_users', to=settings.AUTH_USER_MODEL, verbose_name='Assigned User(s)')), + ('opened_by', models.ForeignKey(help_text='Who is the ticket for', on_delete=django.db.models.deletion.DO_NOTHING, related_name='opened_by', to=settings.AUTH_USER_MODEL, verbose_name='Opened By')), + ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])), + ('project', models.ForeignKey(blank=True, help_text='Assign to a project', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='project_management.project', verbose_name='Project')), + ('subscribed_teams', models.ManyToManyField(blank=True, default=True, help_text='Subscribe a Team(s) to the ticket to receive updates', related_name='subscribed_teams', to='access.team', verbose_name='Subscribed Team(s)')), + ('subscribed_users', models.ManyToManyField(blank=True, default=True, help_text='Subscribe a User(s) to the ticket to receive updates', related_name='subscribed_users', to=settings.AUTH_USER_MODEL, verbose_name='Subscribed User(s)')), + ], + options={ + 'verbose_name': 'Ticket', + 'verbose_name_plural': 'Tickets', + 'ordering': ['id'], + 'permissions': [('add_ticket_request', 'Can add a request ticket'), ('change_ticket_request', 'Can change any request ticket'), ('delete_ticket_request', 'Can delete a request ticket'), ('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'), ('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'), ('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'), ('purge_ticket_change', 'Can purge a change ticket'), ('triage_ticket_change', 'Can triage all change ticket'), ('view_ticket_change', 'Can view all change ticket')], + }, + bases=(models.Model, core.models.ticket.change_ticket.ChangeTicket, core.models.ticket.problem_ticket.ProblemTicket, core.models.ticket.request_ticket.RequestTicket, core.models.ticket.markdown.TicketMarkdown), + ), + migrations.CreateModel( + name='RelatedTickets', + fields=[ + ('id', models.AutoField(help_text='Ticket ID Number', primary_key=True, serialize=False, unique=True, verbose_name='Number')), + ('how_related', models.IntegerField(choices=[(1, 'Related'), (2, 'Blocks'), (3, 'Blocked By')], help_text='How is the ticket related', verbose_name='How Related')), + ('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'], + }, + ), + ] diff --git a/app/core/migrations/0006_remove_ticket_is_global_remove_ticket_model_notes.py b/app/core/migrations/0006_remove_ticket_is_global_remove_ticket_model_notes.py new file mode 100644 index 00000000..486858f5 --- /dev/null +++ b/app/core/migrations/0006_remove_ticket_is_global_remove_ticket_model_notes.py @@ -0,0 +1,21 @@ +# Generated by Django 5.0.7 on 2024-08-23 16:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_ticket_relatedtickets'), + ] + + operations = [ + migrations.RemoveField( + model_name='ticket', + name='is_global', + ), + migrations.RemoveField( + model_name='ticket', + name='model_notes', + ), + ] diff --git a/app/core/migrations/0007_alter_ticket_ticket_type.py b/app/core/migrations/0007_alter_ticket_ticket_type.py new file mode 100644 index 00000000..81445550 --- /dev/null +++ b/app/core/migrations/0007_alter_ticket_ticket_type.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.7 on 2024-08-23 16:41 + +import core.models.ticket.ticket +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0006_remove_ticket_is_global_remove_ticket_model_notes'), + ] + + operations = [ + migrations.AlterField( + model_name='ticket', + name='ticket_type', + field=models.IntegerField(choices=[(1, 'Request'), (2, 'Incident'), (3, 'Change'), (4, 'Problem'), (5, 'Issue'), (6, 'Merge Request'), (7, 'Project Task')], default=None, help_text='The type of ticket this is', null=True, validators=[core.models.ticket.ticket.Ticket.validation_ticket_type], verbose_name='Type'), + ), + ] diff --git a/app/core/migrations/0008_alter_ticket_ticket_type.py b/app/core/migrations/0008_alter_ticket_ticket_type.py new file mode 100644 index 00000000..538133a6 --- /dev/null +++ b/app/core/migrations/0008_alter_ticket_ticket_type.py @@ -0,0 +1,20 @@ +# Generated by Django 5.0.7 on 2024-08-23 16:43 + +import core.models.ticket.ticket +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0007_alter_ticket_ticket_type'), + ] + + operations = [ + migrations.AlterField( + model_name='ticket', + name='ticket_type', + field=models.IntegerField(choices=[(1, 'Request'), (2, 'Incident'), (3, 'Change'), (4, 'Problem'), (5, 'Issue'), (6, 'Merge Request'), (7, 'Project Task')], default=1, help_text='The type of ticket this is', validators=[core.models.ticket.ticket.Ticket.validation_ticket_type], verbose_name='Type'), + preserve_default=False, + ), + ] diff --git a/app/core/models/__init__.py b/app/core/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/core/models/comment.py b/app/core/models/comment.py index 3b329771..c8c370fa 100644 --- a/app/core/models/comment.py +++ b/app/core/models/comment.py @@ -1,7 +1,13 @@ +from django.contrib.auth.models import User from django.db import models +from django.forms import ValidationError from access.models import TenancyObject +from change_ticket import ChangeTicket +from ticket.markdown import TicketMarkdown +from ticket.ticket import Ticket + class CommentCommonFields(models.Model): @@ -26,8 +32,11 @@ class Meta: class Comment( TenancyObject, CommentCommonFields, + TicketMarkdown, ): + # Validate comment organization is the same as the ticket. + class Meta: @@ -36,3 +45,168 @@ class Meta: 'created', 'id', ] + + verbose_name = "Comment" + + verbose_name_plural = "Comments" + + + + class Comment_ExternalSystem(models.TextChoices): # + GITHUB = '1', 'Github' + GITLAB = '2', 'Gitlab' + + + class CommentStatus(models.TextChoices): # + """ Comment Status + + Status of the Comment. By design, not all statuses are available for ALL comment types. + + ## Tasks + + """ + + TODO = '1', 'Draft' + DONE = '2', 'New' + + + + + class CommentType(models.TextChoices): + """Type of the ticket""" + + COMMENT = '1', 'Comment' + TASK = '2', 'Task' + SOLUTION = '3', 'Solution' + + + parent = models.ForeignKey( + 'self', + blank= True, + default = None, + help_text = 'Parent comment this comment creates a discussion with', + null = True, + on_delete=models.CASCADE, + verbose_name = 'Parent Comment', + ) + + + comment_type = models.IntegerField( + blank = False, + choices=CommentType, + default=None, + help_text = 'The type of comment this is', + null=True, + verbose_name = 'Type', + ) + + + ticket = models.ForeignKey( + Ticket, + blank= True, + default = None, + help_text = 'Parent comment this comment creates a discussion with', + null = True, + on_delete=models.CASCADE, + verbose_name = 'Parent Comment', + ) + + + external_ref = models.IntegerField( + blank = True, + default=None, + help_text = 'External System reference', + null=True, + verbose_name = 'Reference Number', + ) # external reference or null. i.e. github comment number + + + external_system = models.IntegerField( + blank = True, + choices=Comment_ExternalSystem, + default=None, + help_text = 'External system this item derives', + null=True, + verbose_name = 'External System', + ) + + + # user + + + body = models.TextField( + blank = True, + default = None, + help_text = 'Body of Comment', + null = False, + verbose_name = 'Body', + ) # text, markdown. validate to ensure a saved comment that does nothing creates nothing + + + # Duration + + + # is_private + + + status = models.IntegerField( # will require validation by ticket type as status for types will be different + blank = False, + choices=CommentStatus, + default=None, + help_text = 'Status of Comment', + null=True, + verbose_name = 'Status', + ) + + + date_closed = models.DateTimeField( + blank = True, + help_text = 'Date closed', + null = True, + verbose_name = 'Closed Date', + ) + + + + # + # ITSM Fields + # + + + # Category + + # is_template + + # source # helpdesk|direct|phone|email + + # responsible_user # user who task is for + + # scheduled task # when the task is scheduled for + + + + # + # Project Management fields + # + + # planned start date + + # planned finish date + + # planned duration + + # actual start date + + # actual finish date + + + + # def __str__(self): # ??????????? what will this need to be + + # return self.name + + + @property + def markdown_comment(self) -> str: + + return self.render_markdown(self.body) diff --git a/app/core/models/ticket/__init__.py b/app/core/models/ticket/__init__.py new file mode 100644 index 00000000..b9742821 --- /dev/null +++ b/app/core/models/ticket/__init__.py @@ -0,0 +1 @@ +from . import * \ No newline at end of file diff --git a/app/core/models/ticket/change_ticket.py b/app/core/models/ticket/change_ticket.py new file mode 100644 index 00000000..3f9d5e8f --- /dev/null +++ b/app/core/models/ticket/change_ticket.py @@ -0,0 +1,14 @@ +from django.forms import ValidationError + + +class ChangeTicket: + + + @property + def validate_change_ticket(self): + + # check status + + # check type + + pass diff --git a/app/core/models/ticket/markdown.py b/app/core/models/ticket/markdown.py new file mode 100644 index 00000000..e845a680 --- /dev/null +++ b/app/core/models/ticket/markdown.py @@ -0,0 +1,19 @@ + + + +class TicketMarkdown: + """Ticket and Comment markdown functions + + Intended to be used for all areas of a tickets, projects and comments. + """ + + + def render_markdown(self, markdown): + + # Requires context of ticket for ticket markdown + + # Requires context of ticket for comment + + # requires context of project for project task comment + + pass diff --git a/app/core/models/ticket/problem_ticket.py b/app/core/models/ticket/problem_ticket.py new file mode 100644 index 00000000..eb4dd7a2 --- /dev/null +++ b/app/core/models/ticket/problem_ticket.py @@ -0,0 +1,14 @@ +from django.forms import ValidationError + + +class ProblemTicket: + + + @property + def validate_problem_ticket(self): + + # check status + + # check type + + pass diff --git a/app/core/models/ticket/request_ticket.py b/app/core/models/ticket/request_ticket.py new file mode 100644 index 00000000..05cb7809 --- /dev/null +++ b/app/core/models/ticket/request_ticket.py @@ -0,0 +1,14 @@ +from django.forms import ValidationError + + +class RequestTicket: + + + @property + def validate_request_ticket(self): + + # check status + + # check type + + pass diff --git a/app/core/models/ticket/ticket.py b/app/core/models/ticket/ticket.py index e80ef5c6..a14e2675 100644 --- a/app/core/models/ticket/ticket.py +++ b/app/core/models/ticket/ticket.py @@ -1,6 +1,98 @@ +from django.contrib.auth.models import User from django.db import models +from django.forms import ValidationError -from access.models import TenancyObject +from access.fields import AutoCreatedField +from access.models import TenancyObject, Team + +from .change_ticket import ChangeTicket +from .markdown import TicketMarkdown +from .problem_ticket import ProblemTicket +from .request_ticket import RequestTicket + +from project_management.models.projects import Project + + + +class TicketValues: + + + _DRAFT_INT = '1' + _NEW_INT = '2' + + _ASSIGNED_INT = '3' + _CLOSED_INT = '4' + _INVALID_INT = '5' + + # + # ITSM statuses + # + + # Requests / Incidents / Problems / Changed + _ASSIGNED_PLANNING_INT = '6' + _PENDING_INT = '7' + + # Requests / Incidents / Problems + _SOLVED_INT = '8' + + # Problem + + _OBSERVATION_INT = '9' + + # Problems / Changes + + _ACCEPTED_INT = '10' + + # Changes + + _EVALUATION_INT = '11' + _APPROVALS_INT = '12' + _TESTING_INT = '13' + _QUALIFICATION_INT = '14' + _APPLIED_INT = '15' + _REVIEW_INT = '16' + _CANCELLED_INT = '17' + _REFUSED_INT = '18' + + + + + _DRAFT_STR = 'Draft' + _NEW_STR = 'New' + + _ASSIGNED_STR = 'Assigned' + _CLOSED_STR = 'Closed' + _INVALID_STR = 'Invalid' + + # + # ITSM statuses + # + + # Requests / Incidents / Problems / Changed + _ASSIGNED_PLANNING_STR = 'Assigned (Planning)' + _PENDING_STR = 'Pending' + + # Requests / Incidents / Problems + _SOLVED_STR = 'Solved' + + # Problem + + _OBSERVATION_STR = 'Under Observation' + + # Problems / Changes + + _ACCEPTED_STR = 'Accepted' + + # Changes + + _EVALUATION_STR = 'Evaluation' + _APPROVALS_STR = 'Approvals' + _TESTING_STR = 'Testing' + _QUALIFICATION_STR = 'Qualification' + _APPLIED_STR = 'Applied' + _REVIEW_STR = 'Review' + _CANCELLED_STR = 'Cancelled' + _REFUSED_STR = 'Refused' @@ -26,6 +118,10 @@ class Meta: class Ticket( TenancyObject, TicketCommonFields, + ChangeTicket, + ProblemTicket, + RequestTicket, + TicketMarkdown, ): @@ -35,3 +131,581 @@ class Meta: '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'), + ('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'), + ('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'), + ('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'), + ('purge_ticket_change', 'Can purge a change ticket'), + ('triage_ticket_change', 'Can triage all change ticket'), + ('view_ticket_change', 'Can view all change ticket'), + ] + + verbose_name = "Ticket" + + verbose_name_plural = "Tickets" + + + + class Ticket_ExternalSystem(models.IntegerChoices): # + GITHUB = '1', 'Github' + GITLAB = '2', 'Gitlab' + + + + + + # class TicketStatus(models.TextChoices): # + class TicketStatus: # + """ Ticket Status + + Status of the ticket. By design, not all statuses are available for ALL ticket types. + + ## Request / Incident ticket + + - Draft + - New + - Assigned + - Assigned (Planned) + - Pending + - Solved + - Closed + + + ## Problem Ticket + + - Draft + - New + - Accepted + - Assigned + - Assigned (Planned) + - Pending + - Solved + - Under Observation + - Closed + + ## Change Ticket + + - Draft + - New + - Evaluation + - Approvals + - Accepted + - Pending + - Testing + - Qualification + - Applied + - Review + - Closed + - Cancelled + - Refused + + """ + + class Request(models.IntegerChoices): # + + DRAFT = TicketValues._DRAFT_INT, TicketValues._DRAFT_STR + NEW = TicketValues._NEW_INT, TicketValues._NEW_STR + ASSIGNED = TicketValues._ASSIGNED_INT, TicketValues._ASSIGNED_STR + ASSIGNED_PLANNING = TicketValues._ASSIGNED_PLANNING_INT, TicketValues._ASSIGNED_PLANNING_STR + PENDING = TicketValues._PENDING_INT, TicketValues._PENDING_STR + SOLVED = TicketValues._SOLVED_INT, TicketValues._SOLVED_STR + CLOSED = TicketValues._CLOSED_INT, TicketValues._CLOSED_STR + INVALID = TicketValues._INVALID_INT, TicketValues._INVALID_STR + + + + class Incident(models.IntegerChoices): # + + DRAFT = TicketValues._DRAFT_INT, TicketValues._DRAFT_STR + NEW = TicketValues._NEW_INT, TicketValues._NEW_STR + ASSIGNED = TicketValues._ASSIGNED_INT, TicketValues._ASSIGNED_STR + ASSIGNED_PLANNING = TicketValues._ASSIGNED_PLANNING_INT, TicketValues._ASSIGNED_PLANNING_STR + PENDING = TicketValues._PENDING_INT, TicketValues._PENDING_STR + SOLVED = TicketValues._SOLVED_INT, TicketValues._SOLVED_STR + CLOSED = TicketValues._CLOSED_INT, TicketValues._CLOSED_STR + INVALID = TicketValues._INVALID_INT, TicketValues._INVALID_STR + + + + class Problem(models.IntegerChoices): # + + DRAFT = TicketValues._DRAFT_INT, TicketValues._DRAFT_STR + NEW = TicketValues._NEW_INT, TicketValues._NEW_STR + ACCEPTED = TicketValues._ACCEPTED_INT, TicketValues._ACCEPTED_STR + ASSIGNED = TicketValues._ASSIGNED_INT, TicketValues._ASSIGNED_STR + ASSIGNED_PLANNING = TicketValues._ASSIGNED_PLANNING_INT, TicketValues._ASSIGNED_PLANNING_STR + PENDING = TicketValues._PENDING_INT, TicketValues._PENDING_STR + SOLVED = TicketValues._SOLVED_INT, TicketValues._SOLVED_STR + OBSERVATION = TicketValues._OBSERVATION_INT, TicketValues._OBSERVATION_STR + CLOSED = TicketValues._CLOSED_INT, TicketValues._CLOSED_STR + INVALID = TicketValues._INVALID_INT, TicketValues._INVALID_STR + + + + class Change(models.IntegerChoices): # + + DRAFT = TicketValues._DRAFT_INT, TicketValues._DRAFT_STR + NEW = TicketValues._NEW_INT, TicketValues._NEW_STR + EVALUATION = TicketValues._EVALUATION_INT, TicketValues._EVALUATION_STR + APPROVALS = TicketValues._APPROVALS_INT, TicketValues._APPROVALS_STR + ACCEPTED = TicketValues._ACCEPTED_INT, TicketValues._ACCEPTED_STR + PENDING = TicketValues._PENDING_INT, TicketValues._PENDING_STR + TESTING = TicketValues._TESTING_INT, TicketValues._TESTING_STR + QUALIFICATION = TicketValues._QUALIFICATION_INT, TicketValues._QUALIFICATION_STR + APPLIED = TicketValues._APPLIED_INT, TicketValues._APPLIED_STR + REVIEW = TicketValues._REVIEW_INT, TicketValues._REVIEW_STR + CLOSED = TicketValues._CLOSED_INT, TicketValues._CLOSED_STR + CANCELLED = TicketValues._CANCELLED_INT, TicketValues._CANCELLED_STR + REFUSED = TicketValues._REFUSED_INT, TicketValues._REFUSED_STR + + + class Git(models.IntegerChoices): # + + DRAFT = TicketValues._DRAFT_INT, TicketValues._DRAFT_STR + NEW = TicketValues._NEW_INT, TicketValues._NEW_STR + ASSIGNED = TicketValues._ASSIGNED_INT, TicketValues._ASSIGNED_STR + ASSIGNED_PLANNING = TicketValues._ASSIGNED_PLANNING_INT, TicketValues._ASSIGNED_PLANNING_STR + CLOSED = TicketValues._CLOSED_INT, TicketValues._CLOSED_STR + INVALID = TicketValues._INVALID_INT, TicketValues._INVALID_STR + + + class ProjectTask(models.IntegerChoices): + + DRAFT = TicketValues._DRAFT_INT, TicketValues._DRAFT_STR + NEW = TicketValues._NEW_INT, TicketValues._NEW_STR + ASSIGNED = TicketValues._ASSIGNED_INT, TicketValues._ASSIGNED_STR + ASSIGNED_PLANNING = TicketValues._ASSIGNED_PLANNING_INT, TicketValues._ASSIGNED_PLANNING_STR + PENDING = TicketValues._PENDING_INT, TicketValues._PENDING_STR + SOLVED = TicketValues._SOLVED_INT, TicketValues._SOLVED_STR + CLOSED = TicketValues._CLOSED_INT, TicketValues._CLOSED_STR + INVALID = TicketValues._INVALID_INT, TicketValues._INVALID_STR + + + + + class TicketType(models.IntegerChoices): + """Type of the ticket""" + + REQUEST = '1', 'Request' + INCIDENT = '2', 'Incident' + CHANGE = '3', 'Change' + PROBLEM = '4', 'Problem' + ISSUE = '5', 'Issue' + MERGE_REQUEST = '6', 'Merge Request' + PROJECT_TASK = '7', 'Project Task' + + + + def validation_ticket_type(field): + + if not field: + raise ValidationError('Ticket Type must be set') + + + def validation_title(field): + + if not field: + raise ValueError + + + model_notes = None + + is_global = None + + + status = models.IntegerField( # will require validation by ticket type as status for types will be different + blank = False, + choices=TicketStatus.Request, + default = TicketStatus.Request.NEW, + help_text = 'Status of ticket', + # null=True, + verbose_name = 'Status', + ) + + # category = models.CharField( + # blank = False, + # help_text = "Category of the Ticket", + # max_length = 50, + # unique = True, + # verbose_name = 'Category', + # ) + + title = models.CharField( + blank = False, + help_text = "Title of the Ticket", + max_length = 50, + unique = True, + verbose_name = 'Title', + ) + + description = models.TextField( + blank = False, + default = None, + help_text = 'Ticket Description', + null = False, + verbose_name = 'Description', + ) # text, markdown + + + # urgency = models.IntegerField( + # blank = True, + # # choices=TicketType, + # default=None, + # help_text = 'How urgent is this tickets resolution?', + # null=True, + # verbose_name = 'Urgency', + # ) + + # impact = models.IntegerField( + # blank = True, + # # choices=TicketType, + # default=None, + # help_text = 'End user assessed impact', + # null=True, + # verbose_name = 'Impact', + # ) + + # priority = models.IntegerField( + # blank = True, + # # choices=TicketType, + # default=None, + # help_text = 'What priority should this ticket for its completion', + # null=True, + # verbose_name = 'Priority', + # ) + + + external_ref = models.IntegerField( + blank = True, + default=None, + help_text = 'External System reference', + null=True, + verbose_name = 'Reference Number', + ) # external reference or null. i.e. github issue number + + + external_system = models.IntegerField( + blank = True, + choices=Ticket_ExternalSystem, + default=None, + help_text = 'External system this item derives', + null=True, + verbose_name = 'External System', + ) + + + ticket_type = models.IntegerField( + blank = False, + choices=TicketType, + help_text = 'The type of ticket this is', + # null=False, + validators = [ validation_ticket_type ], + verbose_name = 'Type', + ) + + + + # date_opened = models.DateTimeField( # created + # verbose_name = 'Open Date', + # null = True, + # blank = True + # ) + + project = models.ForeignKey( + Project, + blank= True, + # default = True, + help_text = 'Assign to a project', + null = True, + on_delete = models.DO_NOTHING, + verbose_name = 'Project', + ) + + opened_by = models.ForeignKey( + User, + blank= False, + # default = True, + help_text = 'Who is the ticket for', + null = False, + on_delete = models.DO_NOTHING, + related_name = 'opened_by', + verbose_name = 'Opened By', + ) + + subscribed_users = models.ManyToManyField( + User, + blank= True, + default = True, + help_text = 'Subscribe a User(s) to the ticket to receive updates', + related_name = 'subscribed_users', + symmetrical = False, + # null = True, + verbose_name = 'Subscribed User(s)', + ) + + subscribed_teams = models.ManyToManyField( + Team, + blank= True, + default = True, + help_text = 'Subscribe a Team(s) to the ticket to receive updates', + related_name = 'subscribed_teams', + symmetrical = False, + # null = True, + verbose_name = 'Subscribed Team(s)', + ) + + assigned_users = models.ManyToManyField( + User, + blank= True, + default = True, + help_text = 'Assign the ticket to a User(s)', + related_name = 'assigned_users', + symmetrical = False, + # null = True, + verbose_name = 'Assigned User(s)', + ) + + assigned_teams = models.ManyToManyField( + Team, + blank= True, + default = True, + help_text = 'Assign the ticket to a Team(s)', + related_name = 'assigned_teams', + symmetrical = False, + # null = True, + verbose_name = 'Assigned Team(s)', + ) + + is_deleted = models.BooleanField( + blank = False, + default = False, + help_text = 'Is the ticket deleted? And ready to be purged', + null = False, + verbose_name = 'Deleted', + ) + + date_closed = models.DateTimeField( + blank = True, + help_text = 'Date ticket closed', + null = True, + verbose_name = 'Closed Date', + ) + + planned_start_date = models.DateTimeField( + blank = True, + help_text = 'Planned start date.', + null = True, + verbose_name = 'Planned Start Date', + ) + + planned_finish_date = models.DateTimeField( + blank = True, + help_text = 'Planned finish date', + null = True, + verbose_name = 'Planned Finish Date', + ) + + real_start_date = models.DateTimeField( + blank = True, + help_text = 'Real start date', + null = True, + verbose_name = 'Real Start Date', + ) + + real_finish_date = models.DateTimeField( + blank = True, + help_text = 'Real finish date', + null = True, + verbose_name = 'Real Finish Date', + ) + + # related_tickets = models.ManyToManyField( + # 'self', + # blank= True, + # default = True, + # help_text = 'Tickets that are related to each other', + # symmetrical = True, + # # null = True, + # verbose_name = 'Related Tickets', + # ) + + + # ?? date_edit date of last edit + + # def __str__(self): # ??????????? what will this need to be + + # return self.name + + common_fields: list(str()) = [ + 'organization', + 'title', + 'description', + 'opened_by', + 'ticket_type' + ] + + common_itsm_fields: list(str()) = common_fields + [ + 'urgency', + + ] + + fields_itsm_request: list(str()) = common_itsm_fields + [ + + ] + + fields_itsm_incident: list(str()) = common_itsm_fields + [ + + ] + + fields_itsm_problem: list(str()) = common_itsm_fields + [ + + ] + + fields_itsm_change: list(str()) = common_itsm_fields + [ + + ] + + + common_git_fields: list(str()) = common_fields + [ + + ] + + fields_git_issue: list(str()) = common_fields + [ + + ] + + fields_git_merge_request: list(str()) = common_fields + [ + + ] + + fields_project_task: list(str()) = common_fields + [ + 'category', + 'urgency', + 'status', + 'impact', + 'priority', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + ] + + tech_fields = [ + 'category', + 'assigned_teams', + 'subscribed_teams', + 'subscribed_users', + 'status', + 'impact', + 'priority', + 'planned_start_date', + 'planned_finish_date', + ] + + + @property + def markdown_description(self) -> str: + + return self.render_markdown(self.description) + + @property + def related_tickets(self) -> list(dict()): + + related_tickets: list() = [] + + query = RelatedTickets.objects.filter( + Q(from_ticket_id=self.id) + | + Q(to_ticket_id=self.id) + ) + + for related_ticket in query: + + related_tickets += [ + { + 'id': related_ticket.id, + 'title': related_ticket.title, + 'type': related_ticket.ticket_type + } + ] + + return related_tickets + + + + + + + + +class RelatedTickets(models.Model): + + class Meta: + + ordering = [ + 'id' + ] + + class Related(models.IntegerChoices): + RELATED = '1', 'Related' + + BLOCKS = '2', 'Blocks' + + BLOCKED_BY = '3', 'Blocked By' + + + id = models.AutoField( + blank=False, + help_text = 'Ticket ID Number', + primary_key=True, + unique=True, + verbose_name = 'Number', + ) + + from_ticket_id = models.ForeignKey( + Ticket, + blank= False, + help_text = 'This Ticket', + null = False, + on_delete = models.CASCADE, + related_name = 'from_ticket_id', + verbose_name = 'Ticket', + ) + + how_related = models.IntegerField( + blank = False, + choices = Related, + help_text = 'How is the ticket related', + verbose_name = 'How Related', + ) + + to_ticket_id = models.ForeignKey( + Ticket, + blank= False, + help_text = 'The Related Ticket', + null = False, + on_delete = models.CASCADE, + related_name = 'to_ticket_id', + verbose_name = 'Related Ticket', + ) diff --git a/app/core/templates/core/ticket.html.j2 b/app/core/templates/core/ticket.html.j2 new file mode 100644 index 00000000..6d8b0ddc --- /dev/null +++ b/app/core/templates/core/ticket.html.j2 @@ -0,0 +1,571 @@ +{% extends 'base.html.j2' %} + +{% load markdown %} + +{% block title %}Ticket{% endblock %} + +{% block article %} + + + + + +
+
+
+
+ +
+
{{ ticket.description | markdown | safe }}
+
+ + +
+ +
+

+
Related Tickets
+
{% include 'icons/place-holder.svg' %}{% include 'icons/place-holder.svg' %}
+

+ {% if related_tickets %} + {% for related_ticket in related_tickets %} +
+ {{ related_ticket.title }} +
+ {% endfor %} + {% else %} +
+ Nothing found +
+ {% endif %} +
+ +
+

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

+
+ An item +
+
+ another item +
+
+ another item +
+
+ another item +
+
+ another item +
+
+ another item +
+
+ +
+ +
+
    +
  • + {% include 'core/ticket/comment.html.j2' %} +
  • +
  • John smith add x as related to this ticket
  • +
  • + + {% include 'core/ticket/comment.html.j2' %} + + {% include 'core/ticket/discussion.html.j2' %} + + +
  • +
  • Jane smith mentioned this ticket in xx
  • +
  • sdasfdgdfgdfg dfg dfg dfg d
  • +
+
+
+ {% csrf_token %} + +
+
+
+
+
+
+
+ +
+ + +
+

{{ ticket_type }}

+ +
+ + {{ form.assigned_users.value }} +
+
+ + {{ form.status.value }} +
+
+ + val +
+
+ + {{ form.project.value }} +
+
+ + U{{ form.urgency.value }} / I{{ form.impact.value }} / P{{ form.priotity.value }} +
+
+ + val +
+
+ + val +
+ +
+ + +
+{% endblock %} \ No newline at end of file diff --git a/app/core/templates/core/ticket/comment.html.j2 b/app/core/templates/core/ticket/comment.html.j2 new file mode 100644 index 00000000..f5e839fa --- /dev/null +++ b/app/core/templates/core/ticket/comment.html.j2 @@ -0,0 +1,13 @@ +{% load markdown %} + +
+

+
Jon Smith wrote on xx Aug 2024
+
{% include 'icons/place-holder.svg' %}{% include 'icons/place-holder.svg' %}
+ +

+
+ a comment +
+ +
diff --git a/app/core/templates/core/ticket/discussion.html.j2 b/app/core/templates/core/ticket/discussion.html.j2 new file mode 100644 index 00000000..2602ed7a --- /dev/null +++ b/app/core/templates/core/ticket/discussion.html.j2 @@ -0,0 +1,11 @@ + +
+ +

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

+ + {% include 'core/ticket/comment.html.j2' %} + +
diff --git a/app/core/templates/core/ticket/index.html.j2 b/app/core/templates/core/ticket/index.html.j2 new file mode 100644 index 00000000..35a8e641 --- /dev/null +++ b/app/core/templates/core/ticket/index.html.j2 @@ -0,0 +1,41 @@ +{% extends 'base.html.j2' %} + +{% block content %} + + + + + + + + + + + + + {% for ticket in tickets %} + + + + + + + + {% endfor %} +
 IDTitleStatusCreated
+ {{ ticket.ticket_type }}{% include 'icons/place-holder.svg' %} + {{ ticket.id }}{{ ticket.title }}{{ ticket.status }}{{ ticket.created }}
+ +{% endblock %} \ No newline at end of file diff --git a/app/core/templatetags/markdown.py b/app/core/templatetags/markdown.py index 867b2f80..8cf8e6c9 100644 --- a/app/core/templatetags/markdown.py +++ b/app/core/templatetags/markdown.py @@ -9,4 +9,15 @@ @register.filter() @stringfilter def markdown(value): - return md.markdown(value, extensions=['markdown.extensions.fenced_code', 'codehilite']) \ No newline at end of file + + return md.markdown(value, extensions=['markdown.extensions.fenced_code', 'codehilite']) + + +@register.filter() +@stringfilter +def count_lines(value) -> int: + print(f'value: {value}') + val = str(value).split('\\n') + print(f'lines: {len(val)}') + return len(val) + diff --git a/app/core/tests/unit/ticket/test_ticket_common.py b/app/core/tests/unit/ticket/test_ticket_common.py new file mode 100644 index 00000000..3a336da5 --- /dev/null +++ b/app/core/tests/unit/ticket/test_ticket_common.py @@ -0,0 +1,63 @@ +import pytest +import unittest +import requests + +from django.test import TestCase + +from app.tests.abstract.models import ModelDisplay, ModelIndex + + + +class TicketCommon( + TestCase +): + + def text_ticket_field_type_opened_by(self): + """Ensure field is of a certain type + + opened_by_field must be of type int + """ + pass + + def text_ticket_field_value_not_null_opened_by(self): + """Ensure field is not null + + opened_by_field must be set and not null + """ + pass + + + def text_ticket_field_value_auto_set_opened_by(self): + """Ensure field is auto set within code + + opened_by_field must be set by code with non-tech user not being able to change + """ + pass + + + def text_ticket_field_value_tech_set_opened_by(self): + """Ensure field can be set by a technician + + opened_by_field can be set by a technician + """ + pass + + + + def text_ticket_type_fields(self): + """Placeholder test + + following tests to be written: + + - only tech can change tech fields (same org) + - non-tech cant see tech fields (same org) during creation + - non-tech cant change tech fields (same org) + - only tech can change tech fields (different org) + - non-tech cant see tech fields (different org) during creation + - non-tech cant change tech fields (different org) + + - itsm ticket has the itsm related fields + - non-itsm ticket does not have any itsm related fields + + """ + pass diff --git a/app/core/views/ticket.py b/app/core/views/ticket.py new file mode 100644 index 00000000..dfe7c570 --- /dev/null +++ b/app/core/views/ticket.py @@ -0,0 +1,242 @@ +import markdown + +from django.urls import reverse +from django.views import generic + +from django_celery_results.models import TaskResult + +from access.mixin import OrganizationPermission + +from core.forms.ticket import DetailForm, TicketForm +from core.models.ticket.ticket import Ticket +from core.views.common import AddView, ChangeView, DeleteView, IndexView + +from settings.models.user_settings import UserSettings + + +class Add(AddView): + + form_class = TicketForm + + model = Ticket + permission_required = [ + 'itam.add_device', + ] + template_name = 'form.html.j2' + + + def get_initial(self): + return { + 'organization': UserSettings.objects.get(user = self.request.user).default_organization, + # 'status': self.model.TicketStatus.NEW, + 'ticket_type': self.kwargs['ticket_type'], + } + + def form_valid(self, form): + form.instance.is_global = False + return super().form_valid(form) + + + def get_success_url(self, **kwargs): + + return f"/ticket/" + + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + context['content_title'] = 'New Ticket' + + return context + + + + +class Change(ChangeView): + + form_class = TicketForm + + model = Ticket + + permission_required = [ + 'itim.change_cluster', + ] + + + def get_context_data(self, **kwargs): + + context = super().get_context_data(**kwargs) + + context['content_title'] = str(self.object) + + return context + + + def get_initial(self): + return { + # 'organization': UserSettings.objects.get(user = self.request.user).default_organization, + # 'status': self.model.TicketStatus.NEW, + 'ticket_type': self.kwargs['ticket_type'], + } + + + def get_success_url(self, **kwargs): + + return reverse('Assistance:_ticket_request_view', args=(self.kwargs['ticket_type'], self.kwargs['pk'],)) + + + +class Index(OrganizationPermission, generic.ListView): + + context_object_name = "tickets" + + fields = [ + "id", + 'title', + 'status', + 'date_created', + ] + + model = Ticket + + permission_required = [ + 'django_celery_results.view_taskresult', + ] + + template_name = 'core/ticket/index.html.j2' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + context['new_ticket_url'] = reverse('Assistance:_ticket_request_add', args=(self.kwargs['ticket_type'],)) + + context['content_title'] = 'Background Task Results' + + return context + + + def get_success_url(self, **kwargs): + + return reverse('Settings:_device_model_view', args=(self.kwargs['pk'],)) + + + +# class View(OrganizationPermission, generic.ListView): + +# context_object_name = "ticket" + +# fields = [ +# # "task_id", +# # 'task_name', +# # 'status', +# # 'task_args', +# '__all__', +# ] + +# model = Ticket + +# permission_required = [ +# 'django_celery_results.view_taskresult', +# ] + +# template_name = 'core/ticket.html.j2' + + +# def get_context_data(self, **kwargs): +# context = super().get_context_data(**kwargs) + +# context['content_title'] = f"Ticket " + +# return context + + + # def post(self, request, *args, **kwargs): + # pass + +class View(ChangeView): + + model = Ticket + + permission_required = [ + 'itam.view_device', + ] + + template_name = 'core/ticket.html.j2' + + form_class = DetailForm + + context_object_name = "ticket" + + # paginate_by = 10 + + + def get_context_data(self, **kwargs): + + context = super().get_context_data(**kwargs) + + + context['ticket_type'] = self.kwargs['ticket_type'] + + context['model_pk'] = self.kwargs['pk'] + context['model_name'] = self.model._meta.verbose_name.replace(' ', '') + + # context['model_delete_url'] = reverse('ITAM:_device_delete', args=(self.kwargs['pk'],)) + + context['edit_url'] = reverse('Assistance:_ticket_request_change', args=(self.kwargs['ticket_type'], self.kwargs['pk'])) #/assistance/ticket/{{ ticket_type }}/{{ ticket.id }} + + context['content_title'] = self.object.title + + return context + + + def get_initial(self): + return { + # 'organization': UserSettings.objects.get(user = self.request.user).default_organization, + # 'status': self.model.TicketStatus.NEW, + 'ticket_type': self.kwargs['ticket_type'], + } + + + # def post(self, request, *args, **kwargs): + + # device = Device.objects.get(pk=self.kwargs['pk']) + + # try: + + # existing_os = DeviceOperatingSystem.objects.get(device=self.kwargs['pk']) + + # except DeviceOperatingSystem.DoesNotExist: + + # existing_os = None + + # operating_system = OperatingSystemForm(request.POST, prefix='operating_system', instance=existing_os) + + # if operating_system.is_bound and operating_system.is_valid(): + + # if request.user.has_perm('itam.change_device'): + + # operating_system.instance.organization = device.organization + # operating_system.instance.device = device + + # operating_system.save() + + + # notes = AddNoteForm(request.POST, prefix='note') + + # if notes.is_bound and notes.is_valid() and notes.instance.note != '': + + # if request.user.has_perm('core.add_notes'): + + # notes.instance.organization = device.organization + # notes.instance.device = device + # notes.instance.usercreated = request.user + + # notes.save() + + + # return super().post(request, *args, **kwargs) + + + # def get_success_url(self, **kwargs): + + # return f"/itam/device/{self.kwargs['pk']}/" diff --git a/app/itim/migrations/0004_alter_service_config_key_variable.py b/app/itim/migrations/0004_alter_service_config_key_variable.py new file mode 100644 index 00000000..626826a0 --- /dev/null +++ b/app/itim/migrations/0004_alter_service_config_key_variable.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.7 on 2024-08-23 13:51 + +import itim.models.services +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('itim', '0003_clustertype_config'), + ] + + operations = [ + migrations.AlterField( + model_name='service', + name='config_key_variable', + field=models.CharField(blank=True, help_text='Key name to use when merging with cluster/device config.', max_length=50, null=True, validators=[itim.models.services.Service.validate_config_key_variable], verbose_name='Configuration Key'), + ), + ] diff --git a/app/project-static/icons/place-holder.svg b/app/project-static/icons/place-holder.svg new file mode 100644 index 00000000..312a10e7 --- /dev/null +++ b/app/project-static/icons/place-holder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/templates/icons/place-holder.svg b/app/templates/icons/place-holder.svg new file mode 100644 index 00000000..312a10e7 --- /dev/null +++ b/app/templates/icons/place-holder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/projects/centurion_erp/user/core/tickets.md b/docs/projects/centurion_erp/user/core/tickets.md new file mode 100644 index 00000000..d71de0ce --- /dev/null +++ b/docs/projects/centurion_erp/user/core/tickets.md @@ -0,0 +1,22 @@ +--- +title: Tickets +description: Ticket system Documentation as part of the Core Module for Centurion ERP by No Fuss Computing +date: 2024-08-23 +template: project.html +about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp +--- + +The ticketing system within Centurion ERP is common to all ticket types. The differences are primarily fields and the value of fields. + + +## Permisssions + +- `add_ticket_` + +- `change_ticket_` + +- `delete_ticket_` + +- `view_ticket_` + +- `triage_ticket_`