1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -23,4 +23,5 @@
|
||||
"yellow": 60,
|
||||
"green": 90
|
||||
},
|
||||
"telemetry.feedback.enabled": false,
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
# Generated by Django 5.1.9 on 2025-05-11 16:49
|
||||
|
||||
import core.models.ticket_comment_base
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0023_alter_ticketlinkeditem_item_type'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='ticketbase',
|
||||
name='opened_by',
|
||||
field=models.ForeignKey(blank=True, help_text='Who is the ticket for', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='ticket_opened', to=settings.AUTH_USER_MODEL, verbose_name='Opened By'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ticketcommentbase',
|
||||
name='category',
|
||||
field=models.ForeignKey(blank=True, help_text='Category of the comment', null=True, on_delete=django.db.models.deletion.PROTECT, to='core.ticketcommentcategory', verbose_name='Category'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ticketcommentbase',
|
||||
name='comment_type',
|
||||
field=models.CharField(choices=core.models.ticket_comment_base.TicketCommentBase.get_comment_type_choices, help_text='Type this comment is. derived from Meta.verbose_name', max_length=30, validators=[core.models.ticket_comment_base.TicketCommentBase.field_validation_not_empty], verbose_name='Type'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ticketcommentbase',
|
||||
name='external_ref',
|
||||
field=models.IntegerField(blank=True, help_text='External System reference', null=True, verbose_name='Reference Number'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ticketcommentbase',
|
||||
name='external_system',
|
||||
field=models.IntegerField(blank=True, choices=[(1, 'Github'), (2, 'Gitlab'), (9999, 'Custom #1 (Imported)'), (9998, 'Custom #2 (Imported)'), (9997, 'Custom #3 (Imported)'), (9996, 'Custom #4 (Imported)'), (9995, 'Custom #5 (Imported)'), (9994, 'Custom #6 (Imported)'), (9993, 'Custom #7 (Imported)'), (9992, 'Custom #8 (Imported)'), (9991, 'Custom #9 (Imported)')], help_text='External system this item derives', null=True, verbose_name='External System'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ticketcommentbase',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, help_text='Parent ID for creating discussion threads', null=True, on_delete=django.db.models.deletion.PROTECT, to='core.ticketcommentbase', verbose_name='Parent Comment'),
|
||||
),
|
||||
]
|
@ -434,9 +434,9 @@ class TicketBase(
|
||||
|
||||
opened_by = models.ForeignKey(
|
||||
User,
|
||||
blank = False,
|
||||
blank = True,
|
||||
help_text = 'Who is the ticket for',
|
||||
null = False,
|
||||
null = True,
|
||||
on_delete = models.PROTECT,
|
||||
related_name = 'ticket_opened',
|
||||
verbose_name = 'Opened By',
|
||||
@ -567,6 +567,16 @@ class TicketBase(
|
||||
there are unresolved ticket comments.
|
||||
"""
|
||||
|
||||
if self.opened_by is None:
|
||||
|
||||
raise centurion_exception.ValidationError(
|
||||
detail = {
|
||||
'opened_by': 'This field is required.'
|
||||
},
|
||||
code = 'required'
|
||||
)
|
||||
|
||||
|
||||
if self.milestone:
|
||||
|
||||
if self.milestone.project != self.project:
|
||||
@ -578,17 +588,80 @@ class TicketBase(
|
||||
code = 'milestone_different_project'
|
||||
)
|
||||
|
||||
|
||||
if(
|
||||
(
|
||||
self.status == self.TicketStatus.SOLVED
|
||||
and self.get_can_resolve( raise_exceptions = False )
|
||||
)
|
||||
or self.status == self.TicketStatus.INVALID
|
||||
):
|
||||
|
||||
self.is_solved = True
|
||||
|
||||
|
||||
elif( # Re-Open Ticket
|
||||
( self.is_solved or self.is_closed )
|
||||
and self.status != self.TicketStatus.CLOSED
|
||||
and self.status != self.TicketStatus.INVALID
|
||||
and self.status != self.TicketStatus.SOLVED
|
||||
):
|
||||
|
||||
if self.is_closed:
|
||||
|
||||
self.is_closed = False
|
||||
|
||||
if self.is_solved:
|
||||
|
||||
self.is_solved = False
|
||||
|
||||
|
||||
elif not self.is_closed and self.status == self.TicketStatus.CLOSED: # Close Ticket
|
||||
|
||||
self.is_solved = True
|
||||
self.is_closed = True
|
||||
|
||||
|
||||
if self.is_solved:
|
||||
|
||||
self.get_can_resolve( raise_exceptions = True )
|
||||
|
||||
|
||||
|
||||
if self.is_closed:
|
||||
|
||||
self.get_can_close( raise_exceptions = True )
|
||||
|
||||
|
||||
related_model = self.get_related_model()
|
||||
|
||||
if related_model is None:
|
||||
|
||||
related_model = self
|
||||
|
||||
if self.ticket_type != str(related_model._meta.sub_model_type).lower().replace(' ', '_'):
|
||||
|
||||
self.ticket_type = str(related_model._meta.sub_model_type).lower().replace(' ', '_')
|
||||
|
||||
|
||||
if self.date_solved is None and self.is_solved:
|
||||
|
||||
self.date_solved = datetime.datetime.now(tz=datetime.timezone.utc).replace(microsecond=0).isoformat()
|
||||
|
||||
elif self.date_solved is not None and not self.is_solved:
|
||||
|
||||
self.date_solved = None
|
||||
|
||||
|
||||
if self.date_closed is None and self.is_closed:
|
||||
|
||||
self.date_closed = datetime.datetime.now(tz=datetime.timezone.utc).replace(microsecond=0).isoformat()
|
||||
|
||||
if self.date_closed is not None and not self.is_closed:
|
||||
|
||||
self.date_closed = None
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def get_can_close(self, raise_exceptions = False ) -> bool:
|
||||
@ -789,36 +862,6 @@ class TicketBase(
|
||||
|
||||
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||
|
||||
related_model = self.get_related_model()
|
||||
|
||||
if related_model is None:
|
||||
|
||||
related_model = self
|
||||
|
||||
if self.ticket_type != str(related_model._meta.sub_model_type).lower().replace(' ', '_'):
|
||||
|
||||
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()
|
||||
|
||||
|
||||
if(
|
||||
self.description != ''
|
||||
|
@ -278,6 +278,16 @@ class ModelSerializer(
|
||||
|
||||
def validate(self, attrs):
|
||||
|
||||
|
||||
if getattr(self.context['view'], 'action', '') in [ 'create' ]:
|
||||
# Always set that the ticket was opened by user ho is making the request
|
||||
|
||||
try:
|
||||
attrs['opened_by'] = self.context['request'].user
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
attrs = self.validate_field_milestone( attrs )
|
||||
|
||||
attrs = self.validate_field_external_system( attrs )
|
||||
@ -290,15 +300,13 @@ class ModelSerializer(
|
||||
|
||||
status = int(attrs.get('status', 0))
|
||||
|
||||
opened_by_id = int(attrs.get('opened_by_id', 0))
|
||||
opened_by_id = attrs.get('opened_by', 0)
|
||||
|
||||
if self.context.get('request', None):
|
||||
if opened_by_id != 0:
|
||||
|
||||
request_user_id = int(self.context['request'].user.id)
|
||||
opened_by_id = opened_by_id.id
|
||||
|
||||
else:
|
||||
|
||||
request_user_id = 0
|
||||
request_user_id = int(self.context['request'].user.id)
|
||||
|
||||
if opened_by_id == 0:
|
||||
|
||||
@ -364,6 +372,21 @@ class ModelSerializer(
|
||||
)
|
||||
|
||||
|
||||
elif (
|
||||
has_triage_permission
|
||||
or has_import_permission
|
||||
):
|
||||
|
||||
if(
|
||||
(
|
||||
'status' not in attrs
|
||||
or attrs.get('status', 0) == self.Meta.model.TicketStatus.NEW
|
||||
)
|
||||
and 'assigned_to' in attrs
|
||||
):
|
||||
|
||||
attrs['status'] = self.Meta.model.TicketStatus.ASSIGNED
|
||||
|
||||
|
||||
return attrs
|
||||
|
||||
|
@ -18,29 +18,6 @@ from project_management.models.project_milestone import (
|
||||
|
||||
|
||||
|
||||
|
||||
class MockView:
|
||||
|
||||
_has_import: bool = False
|
||||
"""User Permission
|
||||
|
||||
get_permission_required() sets this to `True` when user has import permission.
|
||||
"""
|
||||
|
||||
_has_purge: bool = False
|
||||
"""User Permission
|
||||
|
||||
get_permission_required() sets this to `True` when user has purge permission.
|
||||
"""
|
||||
|
||||
_has_triage: bool = False
|
||||
"""User Permission
|
||||
|
||||
get_permission_required() sets this to `True` when user has triage permission.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class TicketBaseSerializerTestCases:
|
||||
|
||||
|
||||
@ -114,11 +91,8 @@ class TicketBaseSerializerTestCases:
|
||||
'permission_import_required': False,
|
||||
},
|
||||
"opened_by": {
|
||||
'will_create': False,
|
||||
'exception_obj': ValidationError,
|
||||
'exception_code': 'required',
|
||||
'exception_code_key': None,
|
||||
'permission_import_required': True,
|
||||
'will_create': True,
|
||||
'permission_import_required': False,
|
||||
},
|
||||
"subscribed_to": {
|
||||
'will_create': True,
|
||||
@ -224,12 +198,15 @@ class TicketBaseSerializerTestCases:
|
||||
|
||||
request.cls.view_user = User.objects.create_user(username="cafs_test_user_view", password="password")
|
||||
|
||||
request.cls.other_user = User.objects.create_user(username="cafs_test_user_other", password="password")
|
||||
|
||||
|
||||
yield
|
||||
|
||||
with django_db_blocker.unblock():
|
||||
|
||||
request.cls.view_user.delete()
|
||||
request.cls.other_user.delete()
|
||||
|
||||
del request.cls.valid_data
|
||||
|
||||
@ -308,17 +285,23 @@ class TicketBaseSerializerTestCases:
|
||||
pass
|
||||
|
||||
|
||||
def test_serializer_valid_data(self, create_serializer):
|
||||
def test_serializer_valid_data(self, fake_view, create_serializer):
|
||||
"""Serializer Validation Check
|
||||
|
||||
Ensure that when creating an object with valid data, no validation
|
||||
error occurs.
|
||||
"""
|
||||
|
||||
view_set = MockView()
|
||||
view_set = fake_view(
|
||||
user = self.view_user,
|
||||
_has_import = True,
|
||||
_has_triage = True
|
||||
)
|
||||
|
||||
|
||||
serializer = create_serializer(
|
||||
context = {
|
||||
'request': view_set.request,
|
||||
'view': view_set,
|
||||
},
|
||||
data = self.valid_data
|
||||
@ -327,19 +310,22 @@ class TicketBaseSerializerTestCases:
|
||||
assert serializer.is_valid(raise_exception = True)
|
||||
|
||||
|
||||
def test_serializer_valid_data_permission_import(self, create_serializer):
|
||||
def test_serializer_valid_data_permission_import(self, fake_view, create_serializer):
|
||||
"""Serializer Validation Check
|
||||
|
||||
Ensure that when creating an object with valid data, no validation
|
||||
error occurs. when the user has permission import.
|
||||
"""
|
||||
|
||||
view_set = MockView()
|
||||
|
||||
view_set._has_import = True
|
||||
view_set = fake_view(
|
||||
user = self.view_user,
|
||||
_has_import = True,
|
||||
_has_triage = False
|
||||
)
|
||||
|
||||
serializer = create_serializer(
|
||||
context = {
|
||||
'request': view_set.request,
|
||||
'view': view_set,
|
||||
},
|
||||
data = self.valid_data
|
||||
@ -349,7 +335,7 @@ class TicketBaseSerializerTestCases:
|
||||
|
||||
|
||||
|
||||
def test_serializer_valid_data_missing_field_raises_exception(self, parameterized, param_key_test_data,
|
||||
def test_serializer_valid_data_missing_field_raises_exception(self, fake_view, parameterized, param_key_test_data,
|
||||
create_serializer,
|
||||
param_value,
|
||||
param_exception_obj,
|
||||
@ -369,14 +355,16 @@ class TicketBaseSerializerTestCases:
|
||||
|
||||
del valid_data[param_value]
|
||||
|
||||
view_set = MockView()
|
||||
|
||||
view_set._has_import = True
|
||||
view_set = fake_view(
|
||||
user = self.view_user,
|
||||
_has_import = True,
|
||||
)
|
||||
|
||||
with pytest.raises(param_exception_obj) as err:
|
||||
|
||||
serializer = create_serializer(
|
||||
context = {
|
||||
'request': view_set.request,
|
||||
'view': view_set,
|
||||
},
|
||||
data = valid_data,
|
||||
@ -396,7 +384,7 @@ class TicketBaseSerializerTestCases:
|
||||
|
||||
|
||||
|
||||
def test_serializer_valid_data_missing_field_is_valid_permission_import(self, parameterized, param_key_test_data,
|
||||
def test_serializer_valid_data_missing_field_is_valid_permission_import(self, fake_view, parameterized, param_key_test_data,
|
||||
create_serializer,
|
||||
param_value,
|
||||
param_will_create,
|
||||
@ -412,12 +400,14 @@ class TicketBaseSerializerTestCases:
|
||||
|
||||
del valid_data[param_value]
|
||||
|
||||
view_set = MockView()
|
||||
|
||||
view_set._has_import = True
|
||||
view_set = fake_view(
|
||||
user = self.view_user,
|
||||
_has_import = True,
|
||||
)
|
||||
|
||||
serializer = create_serializer(
|
||||
context = {
|
||||
'request': view_set.request,
|
||||
'view': view_set,
|
||||
},
|
||||
data = valid_data
|
||||
@ -510,6 +500,7 @@ class TicketBaseSerializerTestCases:
|
||||
]
|
||||
)
|
||||
def test_serializer_create_validation_status(self,
|
||||
fake_view,
|
||||
create_serializer,
|
||||
name,
|
||||
param_permission_import,
|
||||
@ -535,41 +526,26 @@ class TicketBaseSerializerTestCases:
|
||||
valid_data['status'] = status
|
||||
|
||||
|
||||
view_set = MockView()
|
||||
view_set = fake_view(
|
||||
user = self.other_user,
|
||||
_has_import = param_permission_import,
|
||||
_has_triage = param_permission_triage
|
||||
)
|
||||
|
||||
view_set._has_import = param_permission_import
|
||||
|
||||
view_set._has_triage = param_permission_triage
|
||||
|
||||
|
||||
class MockRequest:
|
||||
|
||||
class MockUser:
|
||||
|
||||
id = 999999
|
||||
|
||||
pk = 999999
|
||||
|
||||
user = MockUser()
|
||||
|
||||
mock_request = MockRequest()
|
||||
|
||||
if param_is_owner:
|
||||
|
||||
mock_request.user.id = self.view_user.pk
|
||||
|
||||
mock_request.user.pk = self.view_user.pk
|
||||
view_set.request.user = self.view_user
|
||||
|
||||
|
||||
serializer = create_serializer(
|
||||
context = {
|
||||
'request': view_set.request,
|
||||
'view': view_set,
|
||||
},
|
||||
data = valid_data
|
||||
)
|
||||
|
||||
serializer.context['request'] = mock_request
|
||||
|
||||
if type(expected_result) is not bool:
|
||||
|
||||
with pytest.raises(ValidationError) as err:
|
||||
@ -585,13 +561,13 @@ class TicketBaseSerializerTestCases:
|
||||
|
||||
|
||||
@pytest.fixture( scope = 'function' )
|
||||
def existing_ticket(self, db, create_serializer):
|
||||
def existing_ticket(self, db, fake_view, create_serializer):
|
||||
|
||||
view_set = MockView()
|
||||
|
||||
view_set._has_import = True
|
||||
|
||||
view_set._has_triage = True
|
||||
view_set = fake_view(
|
||||
user = self.view_user,
|
||||
_has_import = True,
|
||||
_has_triage = True
|
||||
)
|
||||
|
||||
valid_data = self.valid_data.copy()
|
||||
|
||||
@ -601,6 +577,7 @@ class TicketBaseSerializerTestCases:
|
||||
|
||||
serializer = create_serializer(
|
||||
context = {
|
||||
'request': view_set.request,
|
||||
'view': view_set,
|
||||
},
|
||||
data = valid_data
|
||||
@ -641,7 +618,7 @@ class TicketBaseSerializerTestCases:
|
||||
in values_validation_status_change_permission
|
||||
]
|
||||
)
|
||||
def test_serializer_update_validation_status(self,
|
||||
def test_serializer_update_validation_status(self, fake_view,
|
||||
create_serializer,
|
||||
existing_ticket,
|
||||
name,
|
||||
@ -670,42 +647,28 @@ class TicketBaseSerializerTestCases:
|
||||
valid_data['status'] = status
|
||||
|
||||
|
||||
view_set = MockView()
|
||||
|
||||
view_set._has_import = param_permission_import
|
||||
|
||||
view_set._has_triage = param_permission_triage
|
||||
|
||||
|
||||
class MockRequest:
|
||||
|
||||
class MockUser:
|
||||
|
||||
id = 999999
|
||||
|
||||
pk = 999999
|
||||
|
||||
user = MockUser()
|
||||
|
||||
mock_request = MockRequest()
|
||||
view_set = fake_view(
|
||||
user = self.other_user,
|
||||
_has_import = param_permission_import,
|
||||
_has_triage = param_permission_triage,
|
||||
action = 'partial_update',
|
||||
)
|
||||
|
||||
if param_is_owner:
|
||||
|
||||
mock_request.user.id = self.view_user.pk
|
||||
|
||||
mock_request.user.pk = self.view_user.pk
|
||||
view_set.request.user = self.view_user
|
||||
|
||||
|
||||
serializer = create_serializer(
|
||||
instance = existing_ticket,
|
||||
existing_ticket,
|
||||
context = {
|
||||
'request': view_set.request,
|
||||
'view': view_set,
|
||||
},
|
||||
data = valid_data,
|
||||
partial = True,
|
||||
)
|
||||
|
||||
serializer.context['request'] = mock_request
|
||||
|
||||
if type(expected_result) is not bool:
|
||||
|
||||
@ -721,6 +684,305 @@ class TicketBaseSerializerTestCases:
|
||||
|
||||
|
||||
|
||||
@pytest.fixture( scope = 'function' )
|
||||
def fresh_ticket_serializer(self, request, django_db_blocker, fake_view, create_serializer):
|
||||
|
||||
view_set = fake_view(
|
||||
user = request.cls.view_user
|
||||
)
|
||||
|
||||
valid_data = request.cls.valid_data.copy()
|
||||
|
||||
valid_data['title'] = 'ticktet with minimum fields'
|
||||
|
||||
valid_data_keep_fields = [
|
||||
'title',
|
||||
'organization',
|
||||
'description',
|
||||
]
|
||||
|
||||
for field, value in valid_data.copy().items():
|
||||
|
||||
if field not in valid_data_keep_fields:
|
||||
|
||||
del valid_data[field]
|
||||
|
||||
|
||||
serializer = create_serializer(
|
||||
context = {
|
||||
'request': view_set.request,
|
||||
'view': view_set,
|
||||
},
|
||||
data = valid_data
|
||||
)
|
||||
|
||||
|
||||
yield {
|
||||
'serializer': serializer,
|
||||
'valid_data': valid_data,
|
||||
}
|
||||
|
||||
with django_db_blocker.unblock():
|
||||
|
||||
if serializer.instance:
|
||||
|
||||
serializer.instance.delete()
|
||||
|
||||
|
||||
|
||||
def test_action_triage_user_assign_user_sets_status_assigned(self, model, fresh_ticket_serializer):
|
||||
"""Ticket Function Check
|
||||
|
||||
Assigning the ticket must set the status to new
|
||||
"""
|
||||
|
||||
serializer = fresh_ticket_serializer['serializer']
|
||||
|
||||
serializer.initial_data['assigned_to'] = [ self.entity_user.id ]
|
||||
|
||||
serializer.context['view']._has_triage = True
|
||||
|
||||
serializer.is_valid(raise_exception = True)
|
||||
|
||||
serializer.save()
|
||||
|
||||
ticket = serializer.instance
|
||||
|
||||
assert ticket.status == model.TicketStatus.ASSIGNED
|
||||
|
||||
|
||||
|
||||
def test_action_triage_user_assign_user_and_status_no_status_update(self, model, fresh_ticket_serializer):
|
||||
"""Ticket Function Check
|
||||
|
||||
Assigning the ticket and setting the status, does not set the status to assigned
|
||||
"""
|
||||
|
||||
serializer = fresh_ticket_serializer['serializer']
|
||||
|
||||
serializer.initial_data['assigned_to'] = [ self.entity_user.id ]
|
||||
serializer.initial_data['status'] = model.TicketStatus.PENDING
|
||||
|
||||
serializer.context['view']._has_triage = True
|
||||
|
||||
serializer.is_valid(raise_exception = True)
|
||||
|
||||
serializer.save()
|
||||
|
||||
ticket = serializer.instance
|
||||
|
||||
assert ticket.status == model.TicketStatus.PENDING
|
||||
|
||||
|
||||
|
||||
date_action_clear_solved_ticket = [
|
||||
('triage', True, False, 'is_solved', False),
|
||||
('triage', True, False, 'date_solved', None),
|
||||
|
||||
('ticket_owner', False, False, 'is_solved', False),
|
||||
('ticket_owner', False, False, 'date_solved', None),
|
||||
|
||||
('import', False, True, 'is_solved', False),
|
||||
('import', False, True, 'date_solved', None),
|
||||
]
|
||||
|
||||
date_action_clear_closed_ticket = [
|
||||
('triage', True, False, 'is_closed', False),
|
||||
('triage', True, False, 'date_closed', None),
|
||||
|
||||
('ticket_owner', False, False, 'is_closed', False),
|
||||
('ticket_owner', False, False, 'date_closed', None),
|
||||
|
||||
('import', False, True, 'is_closed', False),
|
||||
('import', False, True, 'date_closed', None),
|
||||
]
|
||||
|
||||
|
||||
data_action_reopen_solved_ticket = [
|
||||
*date_action_clear_solved_ticket,
|
||||
]
|
||||
|
||||
date_action_reopen_closed_ticket = [
|
||||
*date_action_clear_solved_ticket,
|
||||
*date_action_clear_closed_ticket
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
argnames = [
|
||||
'name',
|
||||
'triage_user',
|
||||
'import_user',
|
||||
'field_name',
|
||||
'expected',
|
||||
],
|
||||
argvalues = data_action_reopen_solved_ticket,
|
||||
ids = [
|
||||
name +'_'+ field_name +'_'+str(expected).lower() for
|
||||
name,
|
||||
triage_user,
|
||||
import_user,
|
||||
field_name,
|
||||
expected
|
||||
in data_action_reopen_solved_ticket
|
||||
]
|
||||
)
|
||||
def test_action_reopen_solved_ticket(self,
|
||||
model,
|
||||
fresh_ticket_serializer,
|
||||
create_serializer,
|
||||
name,
|
||||
triage_user,
|
||||
import_user,
|
||||
field_name,
|
||||
expected
|
||||
):
|
||||
"""Ticket Action Check
|
||||
|
||||
When a ticket is reopened the following should occur:
|
||||
- is_solved = False
|
||||
- date_closed = None
|
||||
|
||||
Only the following are supposed to be able to re-open a solved ticket:
|
||||
- ticket owner
|
||||
- triage user
|
||||
- import user
|
||||
"""
|
||||
|
||||
# Create Solved Ticket
|
||||
serializer = fresh_ticket_serializer['serializer']
|
||||
|
||||
serializer.initial_data['status'] = model.TicketStatus.SOLVED
|
||||
|
||||
serializer.context['view']._has_triage = True
|
||||
serializer.context['view']._has_import = True
|
||||
|
||||
serializer.is_valid(raise_exception = True)
|
||||
|
||||
serializer.save()
|
||||
|
||||
ticket = serializer.instance
|
||||
|
||||
# Confirm State
|
||||
assert ticket.status == model.TicketStatus.SOLVED
|
||||
assert ticket.is_solved
|
||||
assert ticket.date_solved is not None
|
||||
|
||||
# Re-Open Ticket
|
||||
edit_serializer = create_serializer(
|
||||
ticket,
|
||||
context = serializer.context,
|
||||
data = {
|
||||
'status': model.TicketStatus.NEW
|
||||
},
|
||||
partial = True
|
||||
)
|
||||
|
||||
edit_serializer.context['view']._has_triage = triage_user
|
||||
edit_serializer.context['view']._has_import = import_user
|
||||
|
||||
edit_serializer.is_valid(raise_exception = True)
|
||||
|
||||
edit_serializer.save()
|
||||
|
||||
ticket = edit_serializer.instance
|
||||
|
||||
|
||||
assert getattr(ticket, field_name) == expected
|
||||
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
argnames = [
|
||||
'name',
|
||||
'triage_user',
|
||||
'import_user',
|
||||
'field_name',
|
||||
'expected',
|
||||
],
|
||||
argvalues = date_action_reopen_closed_ticket,
|
||||
ids = [
|
||||
name +'_'+ field_name +'_'+str(expected).lower() for
|
||||
name,
|
||||
triage_user,
|
||||
import_user,
|
||||
field_name,
|
||||
expected
|
||||
in date_action_reopen_closed_ticket
|
||||
]
|
||||
)
|
||||
def test_action_reopen_closed_ticket(self,
|
||||
model,
|
||||
fresh_ticket_serializer,
|
||||
create_serializer,
|
||||
name,
|
||||
triage_user,
|
||||
import_user,
|
||||
field_name,
|
||||
expected
|
||||
):
|
||||
"""Ticket Action Check
|
||||
|
||||
When a ticket is reopened the following should occur:
|
||||
- is_closed = False
|
||||
- date_closed = None
|
||||
- is_solved = False
|
||||
- date_closed = None
|
||||
|
||||
Only the following are supposed to be able to re-open a closed ticket:
|
||||
- ticket owner
|
||||
- triage user
|
||||
- import user
|
||||
"""
|
||||
|
||||
# Create Closed Ticket
|
||||
serializer = fresh_ticket_serializer['serializer']
|
||||
|
||||
serializer.initial_data['status'] = model.TicketStatus.CLOSED
|
||||
|
||||
serializer.context['view']._has_triage = True
|
||||
serializer.context['view']._has_import = True
|
||||
|
||||
serializer.is_valid(raise_exception = True)
|
||||
|
||||
serializer.save()
|
||||
|
||||
ticket = serializer.instance
|
||||
|
||||
# Confirm State
|
||||
assert ticket.status == model.TicketStatus.CLOSED
|
||||
assert ticket.is_closed
|
||||
assert ticket.date_closed is not None
|
||||
assert ticket.is_solved
|
||||
assert ticket.date_solved is not None
|
||||
|
||||
# Re-Open Ticket
|
||||
edit_serializer = create_serializer(
|
||||
ticket,
|
||||
context = serializer.context,
|
||||
data = {
|
||||
'status': model.TicketStatus.NEW
|
||||
},
|
||||
partial = True
|
||||
)
|
||||
|
||||
edit_serializer.context['view']._has_triage = triage_user
|
||||
edit_serializer.context['view']._has_import = import_user
|
||||
|
||||
edit_serializer.is_valid(raise_exception = True)
|
||||
|
||||
edit_serializer.save()
|
||||
|
||||
ticket = edit_serializer.instance
|
||||
|
||||
|
||||
assert getattr(ticket, field_name) == expected
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class TicketBaseSerializerInheritedCases(
|
||||
TicketBaseSerializerTestCases,
|
||||
):
|
||||
|
@ -62,6 +62,7 @@ class APITestCases(
|
||||
'external_system': int(request.cls.model.Ticket_ExternalSystem.CUSTOM_1),
|
||||
'impact': int(request.cls.model.TicketImpact.MEDIUM),
|
||||
'priority': int(request.cls.model.TicketPriority.HIGH),
|
||||
'status': request.cls.model.TicketStatus.CLOSED,
|
||||
})
|
||||
|
||||
|
||||
@ -379,7 +380,9 @@ class APITestCases(
|
||||
'real_start_date': '2025-04-16T00:00:03',
|
||||
'real_finish_date': '2025-04-16T00:00:04',
|
||||
'is_solved': True,
|
||||
'date_solved': '2025-05-12T02:30:01',
|
||||
'is_closed': True,
|
||||
'date_closed': '2025-05-12T02:30:02',
|
||||
}
|
||||
|
||||
url_ns_name = '_api_v2_ticket'
|
||||
|
@ -850,7 +850,7 @@ class TicketBaseModelTestCases(
|
||||
|
||||
|
||||
|
||||
def test_function_called_clean_ticketcommentbase(self, model, mocker):
|
||||
def test_function_called_clean_ticketbase(self, model, mocker):
|
||||
"""Function Check
|
||||
|
||||
Ensure function `TicketBase.clean` is called
|
||||
@ -872,7 +872,7 @@ class TicketBaseModelTestCases(
|
||||
|
||||
|
||||
|
||||
def test_function_called_save_ticketcommentbase(self, model, mocker):
|
||||
def test_function_called_save_ticketbase(self, model, mocker):
|
||||
"""Function Check
|
||||
|
||||
Ensure function `TicketBase.save` is called
|
||||
|
@ -157,25 +157,10 @@ When using slash commands, there is only to be one slash command per line. All s
|
||||
summary: true
|
||||
|
||||
|
||||
## Ticket Types
|
||||
## Re-Opening a Ticket
|
||||
|
||||
::: app.core.models.ticket.ticket.Ticket.TicketType
|
||||
options:
|
||||
inherited_members: false
|
||||
members: []
|
||||
show_bases: false
|
||||
show_submodules: false
|
||||
summary: true
|
||||
To re-open a ticket is as simple as changing the status and saving. Not everyone can re-open a ticket, it depends upon the following:
|
||||
|
||||
- The user who raised the ticket can re-open the ticket only when the status is `SOLVED`
|
||||
|
||||
## Ticket Comments
|
||||
|
||||
Within Centurion ERP the ticket comment model is common to all comment types. As with tickets the differences are the available fields, which depend upon comment type and permissions.
|
||||
|
||||
::: app.core.models.ticket.ticket_comment.TicketComment.CommentType
|
||||
options:
|
||||
inherited_members: false
|
||||
members: []
|
||||
show_bases: false
|
||||
show_submodules: false
|
||||
summary: true
|
||||
- A User with `Triage` permission can re-open a ticket when the status is `SOLVED` or `CLOSED` regardless of who raised the ticket.
|
||||
|
Reference in New Issue
Block a user