diff --git a/.gitignore b/.gitignore index ce96ad09..3a1e0969 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,9 @@ artifacts/ volumes/ build/ pages/ +node_modules/ +.markdownlint-cli2.jsonc +.markdownlint.json +package-lock.json +package.json +**.junit.xml diff --git a/app/access/mixin.py b/app/access/mixin.py index e36536b0..0a0d339c 100644 --- a/app/access/mixin.py +++ b/app/access/mixin.py @@ -248,9 +248,11 @@ class OrganizationMixin(): return True - if self.request.user.has_perms(perms) and len(self.kwargs) == 0 and str(self.request.method).lower() == 'get': + if self.request.user.has_perms(perms) and str(self.request.method).lower() == 'get': - return True + if len(self.kwargs) == 0 or (len(self.kwargs) == 1 and 'ticket_type' in self.kwargs): + + return True for required_permission in self.permission_required: diff --git a/app/core/forms/ticket.py b/app/core/forms/ticket.py index 89d57e60..257111e7 100644 --- a/app/core/forms/ticket.py +++ b/app/core/forms/ticket.py @@ -1,15 +1,20 @@ from django import forms from django.db.models import Q +from django.forms import ValidationError from app import settings from core.forms.common import CommonModelForm +from core.forms.validate_ticket import TicketValidation from core.models.ticket.ticket import Ticket, RelatedTickets -class TicketForm(CommonModelForm): +class TicketForm( + CommonModelForm, + TicketValidation, +): prefix = 'ticket' @@ -18,7 +23,9 @@ class TicketForm(CommonModelForm): fields = '__all__' - def __init__(self, *args, **kwargs): + def __init__(self, request, *args, **kwargs): + + self.request = request super().__init__(*args, **kwargs) @@ -41,6 +48,7 @@ class TicketForm(CommonModelForm): self.fields['description'].widget.attrs = {'style': "height: 800px; width: 900px"} self.fields['opened_by'].initial = kwargs['user'].pk + self.fields['opened_by'].widget = self.fields['opened_by'].hidden_widget() self.fields['ticket_type'].widget = self.fields['ticket_type'].hidden_widget() @@ -116,16 +124,70 @@ class TicketForm(CommonModelForm): del self.fields[field] + def clean(self): cleaned_data = super().clean() return cleaned_data + def is_valid(self) -> bool: is_valid = super().is_valid() + ticket_type_choice_id = int(self.cleaned_data['ticket_type'] - 1) + + ticket_type = str(self.fields['ticket_type'].choices.choices.pop(ticket_type_choice_id)[1]).lower().replace(' ', '_') + + if self.instance.pk: + + self.original_object = self.Meta.model.objects.get(pk=self.instance.pk) + + self.validate_ticket() + + if ticket_type == 'change': + + self.validate_change_ticket() + + elif ticket_type == 'incident': + + self.validate_incident_ticket() + + elif ticket_type == 'issue': + + # self.validate_issue_ticket() + raise ValidationError( + 'This Ticket type is not yet available' + ) + + elif ticket_type == 'merge_request': + + # self.validate_merge_request_ticket() + raise ValidationError( + 'This Ticket type is not yet available' + ) + + elif ticket_type == 'problem': + + self.validate_problem_ticket() + + elif ticket_type == 'project_task': + + # self.validate_project_task_ticket() + raise ValidationError( + 'This Ticket type is not yet available' + ) + + elif ticket_type == 'request': + + self.validate_request_ticket() + + else: + + raise ValidationError('Ticket Type must be set') + + return is_valid diff --git a/app/core/forms/ticket_comment.py b/app/core/forms/ticket_comment.py index 05e458aa..7243abf4 100644 --- a/app/core/forms/ticket_comment.py +++ b/app/core/forms/ticket_comment.py @@ -44,6 +44,9 @@ class CommentForm(CommonModelForm): self.fields['ticket'].widget = self.fields['ticket'].hidden_widget() + self.fields['parent'].widget = self.fields['parent'].hidden_widget() + self.fields['comment_type'].widget = self.fields['comment_type'].hidden_widget() + if 'qs_comment_type' in kwargs['initial']: comment_type = kwargs['initial']['qs_comment_type'] diff --git a/app/core/forms/validate_ticket.py b/app/core/forms/validate_ticket.py new file mode 100644 index 00000000..99bd5865 --- /dev/null +++ b/app/core/forms/validate_ticket.py @@ -0,0 +1,172 @@ +from django.core.exceptions import PermissionDenied +from django.forms import ValidationError + +from access.mixin import OrganizationMixin + + +class TicketValidation( + OrganizationMixin, +): + + original_object = None + + add_fields: list = [ + 'title', + 'description', + 'urgency', + ] + + change_fields: list = [] + + delete_fields: list = [ + 'is_deleted', + ] + + import_fields: list = [ + 'assigned_users', + 'assigned_teams', + 'created', + 'date_closed', + 'external_ref', + 'external_system', + 'impact', + 'opened_by', + 'planned_start_date', + 'planned_finish_date', + 'priority', + 'project', + 'real_start_date', + 'real_finish_date', + 'subscribed_users', + 'subscribed_teams', + ] + + triage_fields: list = [ + 'assigned_users', + 'assigned_teams', + 'impact', + 'opened_by', + 'planned_start_date', + 'planned_finish_date', + 'priority', + 'project', + 'real_start_date', + 'real_finish_date', + 'subscribed_users', + 'subscribed_teams', + ] + + + def validate_field_permission(self): + """ Check field permissions + + Users can't edit all fields. They can only adjust fields that they + have the permissions to adjust. + + Raises: + PermissionDenied: Access Denied when user has no ticket permissions assigned + PermissionDenied: _description_ + """ + + fields_allowed: list = [] + + + if self.permission_check( + request = self.request, + permissions_required = [ 'add_ticket_'+ self.initial['type_ticket'] ] + ) and not self.request.user.is_superuser: + + fields_allowed = fields_allowed + self.add_fields + + if self.permission_check( + request = self.request, + permissions_required = [ 'change_ticket_'+ self.initial['type_ticket'] ] + ) and not self.request.user.is_superuser: + + fields_allowed = fields_allowed + self.change_fields + + if self.permission_check( + request = self.request, + permissions_required = [ 'delete_ticket_'+ self.initial['type_ticket'] ] + ) and not self.request.user.is_superuser: + + fields_allowed = fields_allowed + self.delete_fields + + if self.permission_check( + request = self.request, + permissions_required = [ 'import_ticket_'+ self.initial['type_ticket'] ] + ) and not self.request.user.is_superuser: + + fields_allowed = fields_allowed + self.import_fields + + if self.permission_check( + request = self.request, + permissions_required = [ 'triage_ticket_'+ self.initial['type_ticket'] ] + ) and not self.request.user.is_superuser: + + fields_allowed = fields_allowed + self.triage_fields + + if self.request.user.is_superuser: + + all_fields: list = self.add_fields + all_fields = all_fields + self.change_fields + all_fields = all_fields + self.delete_fields + all_fields = all_fields + self.import_fields + all_fields = all_fields + self.triage_fields + + fields_allowed = fields_allowed + all_fields + + if len(fields_allowed) == 0: + + raise PermissionDenied('Access Denied') + + for field in self.changed_data: + + if field not in fields_allowed: + + raise PermissionDenied(f'cant edit field: {field}') + + + + def validate_ticket(self): + """Validations common to all ticket types.""" + + self.validate_field_permission() + + + + def validate_change_ticket(self): + + # check status + + # check type + + pass + + + def validate_incident_ticket(self): + + # check status + + # check type + + pass + + + def validate_problem_ticket(self): + + # check status + + # check type + + pass + + + def validate_request_ticket(self): + + # check status + + # check type + + # raise ValidationError('Test to see what it looks like') + pass diff --git a/app/core/migrations/0005_ticket_relatedtickets_ticketcomment.py b/app/core/migrations/0005_ticket_relatedtickets_ticketcomment.py index 32cc7199..d546057e 100644 --- a/app/core/migrations/0005_ticket_relatedtickets_ticketcomment.py +++ b/app/core/migrations/0005_ticket_relatedtickets_ticketcomment.py @@ -1,11 +1,8 @@ -# Generated by Django 5.0.7 on 2024-08-26 05:21 +# Generated by Django 5.0.7 on 2024-08-27 07:46 import access.fields import access.models -import core.models.ticket.change_ticket import core.models.ticket.markdown -import core.models.ticket.problem_ticket -import core.models.ticket.request_ticket import core.models.ticket.ticket import core.models.ticket.ticket_comment import django.db.models.deletion @@ -59,7 +56,7 @@ class Migration(migrations.Migration): 'ordering': ['id'], 'permissions': [('add_ticket_request', 'Can add a request ticket'), ('change_ticket_request', 'Can change any request ticket'), ('delete_ticket_request', 'Can delete a request ticket'), ('import_ticket_request', 'Can import a request ticket'), ('purge_ticket_request', 'Can purge a request ticket'), ('triage_ticket_request', 'Can triage all request ticket'), ('view_ticket_request', 'Can view all request ticket'), ('add_ticket_incident', 'Can add a incident ticket'), ('change_ticket_incident', 'Can change any incident ticket'), ('delete_ticket_incident', 'Can delete a incident ticket'), ('import_ticket_incident', 'Can import a incident ticket'), ('purge_ticket_incident', 'Can purge a incident ticket'), ('triage_ticket_incident', 'Can triage all incident ticket'), ('view_ticket_incident', 'Can view all incident ticket'), ('add_ticket_problem', 'Can add a problem ticket'), ('change_ticket_problem', 'Can change any problem ticket'), ('delete_ticket_problem', 'Can delete a problem ticket'), ('import_ticket_problem', 'Can import a problem ticket'), ('purge_ticket_problem', 'Can purge a problem ticket'), ('triage_ticket_problem', 'Can triage all problem ticket'), ('view_ticket_problem', 'Can view all problem ticket'), ('add_ticket_change', 'Can add a change ticket'), ('change_ticket_change', 'Can change any change ticket'), ('delete_ticket_change', 'Can delete a change ticket'), ('import_ticket_change', 'Can import a change ticket'), ('purge_ticket_change', 'Can purge a change ticket'), ('triage_ticket_change', 'Can triage all change ticket'), ('view_ticket_change', 'Can view all change ticket')], }, - bases=(models.Model, core.models.ticket.change_ticket.ChangeTicket, core.models.ticket.problem_ticket.ProblemTicket, core.models.ticket.request_ticket.RequestTicket, core.models.ticket.markdown.TicketMarkdown), + bases=(models.Model, core.models.ticket.markdown.TicketMarkdown), ), migrations.CreateModel( name='RelatedTickets', diff --git a/app/core/models/comment.py b/app/core/models/comment.py deleted file mode 100644 index 3b329771..00000000 --- a/app/core/models/comment.py +++ /dev/null @@ -1,38 +0,0 @@ -from django.db import models - -from access.models import TenancyObject - - - -class CommentCommonFields(models.Model): - - class Meta: - abstract = True - - id = models.AutoField( - blank=False, - help_text = 'Comment ID Number', - primary_key=True, - unique=True, - verbose_name = 'Number', - ) - - created = AutoCreatedField() - - modified = AutoCreatedField() - - - -class Comment( - TenancyObject, - CommentCommonFields, -): - - - class Meta: - - ordering = [ - 'ticket', - 'created', - 'id', - ] 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/ticket.py b/app/core/models/ticket/ticket.py index ff4d35de..005a3f6a 100644 --- a/app/core/models/ticket/ticket.py +++ b/app/core/models/ticket/ticket.py @@ -6,10 +6,8 @@ from django.forms import ValidationError 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 @@ -119,9 +117,6 @@ class TicketCommonFields(models.Model): class Ticket( TenancyObject, TicketCommonFields, - ChangeTicket, - ProblemTicket, - RequestTicket, TicketMarkdown, ): @@ -753,3 +748,10 @@ class RelatedTickets(TenancyObject): related_name = 'to_ticket_id', verbose_name = 'Related Ticket', ) + + + @property + def parent_object(self): + """ Fetch the parent object """ + + return self.from_ticket_id diff --git a/app/core/models/ticket/ticket_comment.py b/app/core/models/ticket/ticket_comment.py index 514db43a..904783b2 100644 --- a/app/core/models/ticket/ticket_comment.py +++ b/app/core/models/ticket/ticket_comment.py @@ -398,6 +398,14 @@ class TicketComment( return query + + @property + def parent_object(self): + """ Fetch the parent object """ + + return self.ticket + + @property def threads(self): diff --git a/app/core/views/ticket.py b/app/core/views/ticket.py index c794f04f..c2e20751 100644 --- a/app/core/views/ticket.py +++ b/app/core/views/ticket.py @@ -21,23 +21,37 @@ class Add(AddView): form_class = TicketForm model = Ticket - permission_required = [ - 'itam.add_device', - ] - template_name = 'form.html.j2' + + + def get_dynamic_permissions(self): + + return [ + str('core.add_ticket_' + self.kwargs['ticket_type']), + ] def get_initial(self): - return { - 'organization': UserSettings.objects.get(user = self.request.user).default_organization, + + initial = super().get_initial() + + initial.update({ 'type_ticket': self.kwargs['ticket_type'], - } + }) + + return initial + def form_valid(self, form): form.instance.is_global = False return super().form_valid(form) + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['request'] = self.request + return kwargs + + def get_success_url(self, **kwargs): if self.kwargs['ticket_type'] == 'request': @@ -64,9 +78,12 @@ class Change(ChangeView): model = Ticket - permission_required = [ - 'itim.change_cluster', - ] + + def get_dynamic_permissions(self): + + return [ + str('core.change_ticket_' + self.kwargs['ticket_type']), + ] def get_context_data(self, **kwargs): @@ -78,6 +95,12 @@ class Change(ChangeView): return context + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['request'] = self.request + return kwargs + + def get_initial(self): return { 'type_ticket': self.kwargs['ticket_type'], @@ -103,12 +126,16 @@ class Index(OrganizationPermission, generic.ListView): model = Ticket - permission_required = [ - 'django_celery_results.view_taskresult', - ] - template_name = 'core/ticket/index.html.j2' + + def get_dynamic_permissions(self): + + return [ + str('core.view_ticket_' + self.kwargs['ticket_type']), + ] + + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -152,10 +179,6 @@ class View(ChangeView): model = Ticket - permission_required = [ - 'itam.view_device', - ] - template_name = 'core/ticket.html.j2' form_class = DetailForm @@ -163,6 +186,14 @@ class View(ChangeView): context_object_name = "ticket" + def get_dynamic_permissions(self): + + return [ + str('core.view_ticket_' + self.kwargs['ticket_type']), + ] + + + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) diff --git a/makefile b/makefile index ebf67ea4..39260936 100644 --- a/makefile +++ b/makefile @@ -14,6 +14,7 @@ prepare: ${ACTIVATE_VENV}; pip install -r website-template/gitlab-ci/mkdocs/requirements.txt; pip install -r gitlab-ci/lint/requirements.txt; + pip install -r gitlab-ci/mkdocs/requirements.txt; pip install -r requirements.txt; pip install -r requirements_test.txt; npm install markdownlint-cli2;