test(core): Prevent Closing / Solving of TicketBase Model if not ready
ref: #734 #723 closes #325
This commit is contained in:
19
app/core/migrations/0029_alter_ticketbase_parent_ticket.py
Normal file
19
app/core/migrations/0029_alter_ticketbase_parent_ticket.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.8 on 2025-05-03 17:07
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0028_alter_ticketcommentsolution_options'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='ticketbase',
|
||||
name='parent_ticket',
|
||||
field=models.ForeignKey(blank=True, help_text='Parent of this ticket', null=True, on_delete=django.db.models.deletion.PROTECT, to='core.ticketbase', verbose_name='Parent Ticket'),
|
||||
),
|
||||
]
|
@ -139,6 +139,7 @@ class TicketBase(
|
||||
external_system = models.IntegerField(
|
||||
blank = True,
|
||||
choices = Ticket_ExternalSystem,
|
||||
default = None,
|
||||
help_text = 'External system this item derives',
|
||||
null = True,
|
||||
verbose_name = 'External System',
|
||||
@ -146,6 +147,7 @@ class TicketBase(
|
||||
|
||||
external_ref = models.IntegerField(
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'External System reference',
|
||||
null = True,
|
||||
verbose_name = 'Reference Number',
|
||||
@ -591,11 +593,23 @@ class TicketBase(
|
||||
|
||||
def get_can_close(self, raise_exceptions = False ) -> bool:
|
||||
|
||||
if not self.is_solved and not raise_exceptions:
|
||||
if(
|
||||
(
|
||||
not self.get_can_resolve( raise_exceptions = False)
|
||||
or not self.is_solved
|
||||
)
|
||||
and not raise_exceptions
|
||||
):
|
||||
|
||||
return False
|
||||
|
||||
elif not self.is_solved and raise_exceptions:
|
||||
elif(
|
||||
(
|
||||
not self.get_can_resolve( raise_exceptions = False)
|
||||
or not self.is_solved
|
||||
)
|
||||
and raise_exceptions
|
||||
):
|
||||
|
||||
raise centurion_exception.ValidationError(
|
||||
detail = {
|
||||
@ -611,22 +625,25 @@ class TicketBase(
|
||||
|
||||
ticket_comments = self.get_comments( include_threads = True )
|
||||
|
||||
if self.is_solved:
|
||||
for comment in ticket_comments:
|
||||
|
||||
for comment in ticket_comments:
|
||||
if self.status == self.TicketStatus.INVALID:
|
||||
|
||||
if not comment.is_closed and not raise_exceptions:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
elif not comment.is_closed and raise_exceptions:
|
||||
if not comment.is_closed and not raise_exceptions:
|
||||
|
||||
raise centurion_exception.ValidationError(
|
||||
detail = {
|
||||
'status': 'You cant solved a ticket when there are un-resolved comments.'
|
||||
},
|
||||
code = 'resolution_with_un_resolved_comment_denied'
|
||||
)
|
||||
return False
|
||||
|
||||
elif not comment.is_closed and raise_exceptions:
|
||||
|
||||
raise centurion_exception.ValidationError(
|
||||
detail = {
|
||||
'status': 'You cant solve a ticket when there are un-resolved comments.'
|
||||
},
|
||||
code = 'resolution_with_un_resolved_comment_denied'
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@ -782,10 +799,22 @@ class TicketBase(
|
||||
|
||||
self.ticket_type = str(related_model._meta.sub_model_type).lower().replace(' ', '_')
|
||||
|
||||
if(
|
||||
(
|
||||
self.status == self.TicketStatus.SOLVED
|
||||
and self.get_can_resolve( raise_exceptions = False )
|
||||
)
|
||||
or self.status == self.TicketStatus.INVALID
|
||||
):
|
||||
|
||||
self.is_solved = True
|
||||
|
||||
|
||||
if self.date_solved is None and self.is_solved:
|
||||
|
||||
self.date_solved = datetime.datetime.now(tz=datetime.timezone.utc).replace(microsecond=0).isoformat()
|
||||
|
||||
|
||||
if self.date_closed is None and self.is_closed:
|
||||
|
||||
self.date_closed = datetime.datetime.now(tz=datetime.timezone.utc).replace(microsecond=0).isoformat()
|
||||
|
@ -254,20 +254,6 @@ class TicketCommentBase(
|
||||
code = 'ticket_closed_no_date'
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
|
||||
self.ticket.get_can_resolve(raise_exceptions = True)
|
||||
|
||||
except centurion_exception.ValidationError as err:
|
||||
|
||||
raise centurion_exception.ValidationError(
|
||||
detail = {
|
||||
'body': err.detail['status']
|
||||
},
|
||||
code = err.code
|
||||
)
|
||||
|
||||
|
||||
if self.comment_type != self._meta.sub_model_type:
|
||||
|
||||
|
@ -31,6 +31,8 @@ class TicketCommentSolution(
|
||||
|
||||
def clean(self):
|
||||
|
||||
super().clean()
|
||||
|
||||
if self.ticket.is_solved:
|
||||
|
||||
raise centurion_exception.ValidationError(
|
||||
@ -38,8 +40,20 @@ class TicketCommentSolution(
|
||||
code = 'ticket_already_solved'
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
|
||||
self.ticket.get_can_resolve(raise_exceptions = True)
|
||||
|
||||
except centurion_exception.ValidationError as err:
|
||||
|
||||
raise centurion_exception.ValidationError(
|
||||
detail = {
|
||||
'body': err.detail['status']
|
||||
},
|
||||
code = err.code
|
||||
)
|
||||
|
||||
super().clean()
|
||||
|
||||
|
||||
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||
|
@ -53,12 +53,14 @@ class TicketBaseModelTestCases(
|
||||
},
|
||||
"external_system": {
|
||||
'field_type': models.fields.IntegerField,
|
||||
'field_parameter_default_exists': False,
|
||||
'field_parameter_default_exists': True,
|
||||
'field_parameter_default_value': None,
|
||||
'field_parameter_verbose_name_type': str,
|
||||
},
|
||||
"external_ref": {
|
||||
'field_type': models.fields.IntegerField,
|
||||
'field_parameter_default_exists': False,
|
||||
'field_parameter_default_exists': True,
|
||||
'field_parameter_default_value': None,
|
||||
'field_parameter_verbose_name_type': str
|
||||
},
|
||||
"parent_ticket": {
|
||||
@ -544,14 +546,145 @@ class TicketBaseModelTestCases(
|
||||
assert type(self.model().get_can_close()) is bool
|
||||
|
||||
|
||||
def test_function_get_can_close_value_false(self):
|
||||
|
||||
@pytest.fixture( scope = 'function' )
|
||||
def ticket(self, db, model):
|
||||
|
||||
kwargs = self.kwargs_create_item.copy()
|
||||
|
||||
kwargs['title'] = 'can close ticket'
|
||||
|
||||
ticket = self.model.objects.create(
|
||||
**kwargs,
|
||||
status = self.model._meta.get_field('status').default,
|
||||
)
|
||||
|
||||
yield ticket
|
||||
|
||||
if ticket.pk is not None:
|
||||
|
||||
ticket.delete()
|
||||
|
||||
|
||||
@pytest.fixture( scope = 'function' )
|
||||
def ticket_comment(self, db, ticket):
|
||||
|
||||
comment = TicketCommentBase.objects.create(
|
||||
ticket = ticket,
|
||||
body = 'comment body',
|
||||
comment_type = TicketCommentBase._meta.sub_model_type,
|
||||
)
|
||||
|
||||
yield comment
|
||||
|
||||
if comment.pk is not None:
|
||||
|
||||
comment.delete()
|
||||
|
||||
|
||||
values_function_get_can_close = [
|
||||
('no_comments_default_status', False, None, True, None, False),
|
||||
|
||||
('no_comments_set_draft', False, None, True, TicketBase.TicketStatus.DRAFT, False),
|
||||
('no_comments_set_new', False, None, True, TicketBase.TicketStatus.NEW, False),
|
||||
('no_comments_set_assigned', False, None, True, TicketBase.TicketStatus.ASSIGNED, False),
|
||||
('no_comments_set_assigned_planning', False, None, True, TicketBase.TicketStatus.ASSIGNED_PLANNING, False),
|
||||
('no_comments_set_pending', False, None, True, TicketBase.TicketStatus.PENDING, False),
|
||||
('no_comments_set_solved', False, None, True, TicketBase.TicketStatus.SOLVED, True),
|
||||
('no_comments_set_invalid', False, None, True, TicketBase.TicketStatus.INVALID, True),
|
||||
|
||||
('comment_closed_default_status', True, True, True, True, False),
|
||||
|
||||
('comment_closed_set_draft', True, True, True, TicketBase.TicketStatus.DRAFT, False),
|
||||
('comment_closed_set_new', True, True, True, TicketBase.TicketStatus.NEW, False),
|
||||
('comment_closed_set_assigned', True, True, True, TicketBase.TicketStatus.ASSIGNED, False),
|
||||
('comment_closed_set_assigned_planning', True, True, True, TicketBase.TicketStatus.ASSIGNED_PLANNING, False),
|
||||
('comment_closed_set_pending', True, True, True, TicketBase.TicketStatus.PENDING, False),
|
||||
('comment_closed_set_solved', True, True, True, TicketBase.TicketStatus.SOLVED, True),
|
||||
('comment_closed_set_invalid', True, True, True, TicketBase.TicketStatus.INVALID, True),
|
||||
|
||||
('comment_not_closed_default_status', True, False, False, None, False),
|
||||
|
||||
('comment_not_closed_set_draft', True, False, False, TicketBase.TicketStatus.DRAFT, False),
|
||||
('comment_not_closed_set_new', True, False, False, TicketBase.TicketStatus.NEW, False),
|
||||
('comment_not_closed_set_assigned', True, False, False, TicketBase.TicketStatus.ASSIGNED, False),
|
||||
('comment_not_closed_set_assigned_planning', True, False, False, TicketBase.TicketStatus.ASSIGNED_PLANNING, False),
|
||||
('comment_not_closed_set_pending', True, False, False, TicketBase.TicketStatus.PENDING, False),
|
||||
('comment_not_closed_set_solved', True, False, False, TicketBase.TicketStatus.SOLVED, False),
|
||||
('comment_not_closed_set_invalid', True, False, True, TicketBase.TicketStatus.INVALID, True),
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
argnames = [
|
||||
'name',
|
||||
'param_has_comment',
|
||||
'param_comment_is_closed',
|
||||
'expected_value_solve',
|
||||
'param_ticket_status',
|
||||
'expected_value_close',
|
||||
],
|
||||
argvalues = values_function_get_can_close,
|
||||
ids = [
|
||||
name +'_'+ str(param_has_comment).lower() +'_'+ str(param_ticket_status).lower() +'_'+str(expected_value_close).lower() for
|
||||
name,
|
||||
param_has_comment,
|
||||
param_comment_is_closed,
|
||||
expected_value_solve,
|
||||
param_ticket_status,
|
||||
expected_value_close,
|
||||
in values_function_get_can_close
|
||||
]
|
||||
)
|
||||
def test_function_get_can_close(self, ticket_comment,
|
||||
name,
|
||||
param_has_comment,
|
||||
param_comment_is_closed,
|
||||
expected_value_solve,
|
||||
param_ticket_status,
|
||||
expected_value_close,
|
||||
):
|
||||
"""Function test
|
||||
|
||||
Ensure that function `get_can_close` returns a value of `False` when
|
||||
the ticket can not be closed
|
||||
Ensure that function `get_can_close` works as intended:
|
||||
- can't close ticket with unresolved comments
|
||||
- can't close ticket when ticket not solved
|
||||
- can close ticket with no comments when ticket solved.
|
||||
- can close ticket if status invalid regardless of comment status
|
||||
"""
|
||||
|
||||
assert self.model().get_can_close() == False
|
||||
ticket = ticket_comment.ticket
|
||||
|
||||
if param_has_comment:
|
||||
|
||||
if param_comment_is_closed is not None:
|
||||
|
||||
ticket_comment.is_closed = param_comment_is_closed
|
||||
|
||||
|
||||
if type(param_comment_is_closed) is bool and param_comment_is_closed:
|
||||
|
||||
ticket_comment.date_closed = datetime.datetime.now(tz=datetime.timezone.utc).replace(microsecond=0).isoformat()
|
||||
|
||||
|
||||
ticket_comment.save()
|
||||
|
||||
else:
|
||||
|
||||
ticket_comment.delete()
|
||||
|
||||
|
||||
if param_ticket_status is not None:
|
||||
|
||||
try:
|
||||
|
||||
ticket.status = param_ticket_status
|
||||
ticket.save()
|
||||
|
||||
except centurion_exceptions.ValidationError:
|
||||
pass
|
||||
|
||||
|
||||
assert ticket.get_can_close() == expected_value_close
|
||||
|
||||
|
||||
|
||||
@ -564,15 +697,78 @@ class TicketBaseModelTestCases(
|
||||
assert type(self.model().get_can_resolve()) is bool
|
||||
|
||||
|
||||
@pytest.mark.skip( reason = 'write test')
|
||||
def test_function_get_can_resolve_value_false(self):
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
argnames = [
|
||||
'name',
|
||||
'param_has_comment',
|
||||
'param_comment_is_closed',
|
||||
'expected_value_solve',
|
||||
'param_ticket_status',
|
||||
'expected_value_close',
|
||||
],
|
||||
argvalues = values_function_get_can_close,
|
||||
ids = [
|
||||
name +'_'+ str(param_has_comment).lower() +'_'+ str(param_ticket_status).lower() +'_'+str(expected_value_solve).lower() for
|
||||
name,
|
||||
param_has_comment,
|
||||
param_comment_is_closed,
|
||||
expected_value_solve,
|
||||
param_ticket_status,
|
||||
expected_value_close,
|
||||
in values_function_get_can_close
|
||||
]
|
||||
)
|
||||
def test_function_get_can_resolve(self, ticket_comment,
|
||||
name,
|
||||
param_has_comment,
|
||||
param_comment_is_closed,
|
||||
expected_value_solve,
|
||||
param_ticket_status,
|
||||
expected_value_close,
|
||||
):
|
||||
"""Function test
|
||||
|
||||
Ensure that function `get_can_resolve` returns a value of `False` when
|
||||
the ticket can not be closed
|
||||
Ensure that function `get_can_resolve` works as intended:
|
||||
- can't solve ticket with unresolved comments
|
||||
- can solve ticket with no comments.
|
||||
- can solve ticket if status invalid regardless of comment status
|
||||
"""
|
||||
|
||||
assert self.model().get_can_resolve() == False
|
||||
ticket = ticket_comment.ticket
|
||||
|
||||
if param_has_comment:
|
||||
|
||||
if param_comment_is_closed is not None:
|
||||
|
||||
ticket_comment.is_closed = param_comment_is_closed
|
||||
|
||||
|
||||
if type(param_comment_is_closed) is bool and param_comment_is_closed:
|
||||
|
||||
ticket_comment.date_closed = datetime.datetime.now(tz=datetime.timezone.utc).replace(microsecond=0).isoformat()
|
||||
|
||||
|
||||
ticket_comment.save()
|
||||
|
||||
else:
|
||||
|
||||
ticket_comment.delete()
|
||||
|
||||
|
||||
if param_ticket_status is not None:
|
||||
|
||||
try:
|
||||
|
||||
ticket.status = param_ticket_status
|
||||
ticket.save()
|
||||
|
||||
except centurion_exceptions.ValidationError:
|
||||
pass
|
||||
|
||||
|
||||
assert ticket.get_can_resolve() == expected_value_solve
|
||||
|
||||
|
||||
|
||||
def test_function_get_can_resolve_value_true(self):
|
||||
|
Reference in New Issue
Block a user