test(core): Prevent Closing / Solving of TicketBase Model if not ready

ref: #734 #723 closes #325
This commit is contained in:
2025-05-04 00:00:36 +09:30
parent 8c84ff7c52
commit c773fbc3a5
6 changed files with 354 additions and 48 deletions

View 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'),
),
]

View File

@ -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()

View File

@ -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:

View File

@ -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):

View File

@ -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):