Merge pull request #744 from nofusscomputing/test-ticket-comment-base
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@ -4,6 +4,7 @@ __pycache__
|
||||
**.sqlite3
|
||||
**.sqlite
|
||||
**.coverage
|
||||
.coverage*
|
||||
artifacts/
|
||||
**.tmp.*
|
||||
volumes/
|
||||
@ -19,3 +20,4 @@ package.json
|
||||
feature_flags.json
|
||||
coverage_*.json
|
||||
*-coverage.xml
|
||||
log/
|
||||
|
@ -1,3 +1,22 @@
|
||||
## Version 1.17.0
|
||||
|
||||
- Added setting for log files.
|
||||
|
||||
Enables user to specify a default path for centurion's logging. Add the following to your settings file `/etc/itsm/settings.py`
|
||||
|
||||
``` py
|
||||
LOG_FILES = {
|
||||
"centurion": "/var/log/centurion.log", # Normal Centurion Operations
|
||||
"weblog": "/var/log/weblog.log", # All web requests made to Centurion
|
||||
"rest_api": "/var/log/rest_api.log", # Rest API
|
||||
"catch_all":"/var/log/catch-all.log" # A catch all log. Note: does not log anything that has already been logged.
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
With this new setting, the previous setting `LOGGING` will no longer function.
|
||||
|
||||
|
||||
## Version 1.16.0
|
||||
|
||||
- Employees model added behind feature flag `2025-00002` and will remain behind this flag until production ready.
|
||||
|
@ -130,7 +130,7 @@ class OrganizationMixin:
|
||||
parent_model (Model): with PK from kwargs['pk']
|
||||
"""
|
||||
|
||||
return self.parent_model.objects.get(pk=self.kwargs[self.parent_model_pk_kwarg])
|
||||
return self.get_parent_model().objects.get(pk=self.kwargs[self.parent_model_pk_kwarg])
|
||||
|
||||
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
# from django.conf import settings
|
||||
import logging
|
||||
from django.db import models
|
||||
# from django.contrib.auth.models import User, Group
|
||||
|
||||
@ -193,6 +193,16 @@ class TenancyObject(SaveHistory):
|
||||
only be used when there is model inheritence.
|
||||
"""
|
||||
|
||||
_log: logging.Logger = None
|
||||
|
||||
def get_log(self):
|
||||
|
||||
if self._log is None:
|
||||
|
||||
self._log = logging.getLogger('centurion.' + self._meta.app_label)
|
||||
|
||||
return self._log
|
||||
|
||||
page_layout: list = None
|
||||
|
||||
note_basename: str = None
|
||||
|
@ -14,6 +14,25 @@ from app.tests.common import DoesNotExist
|
||||
|
||||
|
||||
class APIFieldsTestCases:
|
||||
""" API field Rendering Test Suite
|
||||
|
||||
This test suite tests the rendering of API fieilds.
|
||||
|
||||
## Additional Items
|
||||
|
||||
You may find a scenario where you are unable to have all fileds available
|
||||
within a single request. to overcome this this test suite has the features
|
||||
available wherein you can prepare an additional item for an additional
|
||||
check. the following is required before the API request is made
|
||||
(setup_post fixture):
|
||||
|
||||
- additional item created and stored in attribute `self.item_two`
|
||||
- additional url as a string and stored in attribute `self.url_two`
|
||||
|
||||
Once you have these two objects, an additional check will be done and each
|
||||
test will check both API requests. if the field is found in either api
|
||||
request the test will pass
|
||||
"""
|
||||
|
||||
@property
|
||||
def parameterized_test_data(self) -> dict:
|
||||
@ -135,6 +154,8 @@ class APIFieldsTestCases:
|
||||
organization = request.cls.organization,
|
||||
)
|
||||
|
||||
request.cls.view_team = view_team
|
||||
|
||||
view_team.permissions.set([view_permissions])
|
||||
|
||||
|
||||
@ -176,10 +197,25 @@ class APIFieldsTestCases:
|
||||
|
||||
request.cls.api_data = response.data
|
||||
|
||||
item_two = getattr(request.cls, 'url_two', None)
|
||||
|
||||
if item_two:
|
||||
|
||||
response_two = client.get(request.cls.url_two)
|
||||
|
||||
request.cls.api_data_two = response_two.data
|
||||
|
||||
else:
|
||||
|
||||
request.cls.api_data_two = {}
|
||||
|
||||
|
||||
yield
|
||||
|
||||
del request.cls.url_view_kwargs['pk']
|
||||
|
||||
del request.cls.api_data_two
|
||||
|
||||
|
||||
|
||||
|
||||
@ -201,13 +237,21 @@ class APIFieldsTestCases:
|
||||
|
||||
api_data = recursearray(self.api_data, param_value)
|
||||
|
||||
api_data_two = recursearray(self.api_data_two, param_value)
|
||||
|
||||
if param_expected is DoesNotExist:
|
||||
|
||||
assert api_data['key'] not in api_data['obj']
|
||||
assert(
|
||||
api_data['key'] not in api_data['obj']
|
||||
and api_data_two['key'] not in api_data_two['obj']
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
assert api_data['key'] in api_data['obj']
|
||||
assert(
|
||||
api_data['key'] in api_data['obj']
|
||||
or api_data_two['key'] in api_data_two['obj']
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -219,13 +263,21 @@ class APIFieldsTestCases:
|
||||
|
||||
api_data = recursearray(self.api_data, param_value)
|
||||
|
||||
api_data_two = recursearray(self.api_data_two, param_value)
|
||||
|
||||
if param_expected is DoesNotExist:
|
||||
|
||||
assert api_data['key'] not in api_data['obj']
|
||||
assert(
|
||||
api_data['key'] not in api_data['obj']
|
||||
and api_data_two['key'] not in api_data_two['obj']
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
assert type( api_data['value'] ) is param_expected
|
||||
assert(
|
||||
type( api_data['value'] ) is param_expected
|
||||
or type( api_data_two.get('value', 'is empty') ) is param_expected
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
import importlib
|
||||
import logging
|
||||
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from rest_framework import viewsets, pagination
|
||||
@ -242,6 +244,8 @@ class Retrieve(
|
||||
status = 501
|
||||
)
|
||||
|
||||
self.get_log().exception(e)
|
||||
|
||||
else:
|
||||
|
||||
response = Response(
|
||||
@ -315,6 +319,8 @@ class Update(
|
||||
status = 501
|
||||
)
|
||||
|
||||
self.get_log().exception(e)
|
||||
|
||||
else:
|
||||
|
||||
response = Response(
|
||||
@ -382,6 +388,8 @@ class Update(
|
||||
status = 501
|
||||
)
|
||||
|
||||
self.get_log().exception(e)
|
||||
|
||||
else:
|
||||
|
||||
response = Response(
|
||||
@ -431,6 +439,16 @@ class CommonViewSet(
|
||||
_Optional_, if specified will be add to list view metadata
|
||||
"""
|
||||
|
||||
_log: logging.Logger = None
|
||||
|
||||
def get_log(self):
|
||||
|
||||
if self._log is None:
|
||||
|
||||
self._log = logging.getLogger('centurion.' + self.model._meta.app_label)
|
||||
|
||||
return self._log
|
||||
|
||||
|
||||
metadata_class = ReactUIMetadata
|
||||
""" Metadata Class
|
||||
|
@ -80,6 +80,95 @@ FEATURE_FLAG_OVERRIDES = None # Feature Flags to override fetched feature flags
|
||||
# PROMETHEUS_METRICS_EXPORT_PORT = 8010
|
||||
# PROMETHEUS_METRICS_EXPORT_ADDRESS = ''
|
||||
|
||||
|
||||
LOG_FILES = { # defaults for devopment. docker includes settings has correct locations
|
||||
"centurion": "../log/centurion.log",
|
||||
"weblog": "../log/weblog.log",
|
||||
"rest_api": "../log/rest_api.log",
|
||||
"catch_all":"../log/catch-all.log"
|
||||
}
|
||||
|
||||
CENTURION_LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"console": {
|
||||
"format": "{asctime} {levelname} {message}",
|
||||
"style": "{",
|
||||
},
|
||||
"verbose": {
|
||||
"format": "{asctime} {levelname} {name} {module} {process:d} {thread:d} {message}",
|
||||
"style": "{",
|
||||
},
|
||||
"simple": {
|
||||
"format": "{levelname} {message}",
|
||||
"style": "{",
|
||||
},
|
||||
"web_log": {
|
||||
"format": "{asctime} {levelname} {name} {module} {process:d} {thread:d} {message}",
|
||||
"style": "{",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
'console': {
|
||||
'level': 'INFO',
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'console',
|
||||
},
|
||||
"file_centurion": {
|
||||
"level": "INFO",
|
||||
"class": "logging.FileHandler",
|
||||
"filename": "centurion.log",
|
||||
'formatter': 'verbose',
|
||||
},
|
||||
"file_weblog": {
|
||||
"level": "INFO",
|
||||
"class": "logging.FileHandler",
|
||||
"filename": "weblog.log",
|
||||
'formatter': 'web_log',
|
||||
},
|
||||
"file_rest_api": {
|
||||
"level": "INFO",
|
||||
"class": "logging.FileHandler",
|
||||
"filename": "rest_api.log",
|
||||
'formatter': 'verbose',
|
||||
},
|
||||
"file_catch_all": {
|
||||
"level": "INFO",
|
||||
"class": "logging.FileHandler",
|
||||
"filename": "catch-all.log",
|
||||
'formatter': 'verbose',
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
"centurion": {
|
||||
"handlers": ['console', 'file_centurion'],
|
||||
"level": "INFO",
|
||||
"propagate": False,
|
||||
},
|
||||
"django.server": {
|
||||
"handlers": ["file_weblog", 'console'],
|
||||
"level": "INFO",
|
||||
"propagate": False,
|
||||
},
|
||||
"django": {
|
||||
"handlers": ['console', 'file_catch_all'],
|
||||
"level": "INFO",
|
||||
"propagate": False,
|
||||
},
|
||||
'rest_framework': {
|
||||
'handlers': ['file_rest_api', 'console'],
|
||||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
'': {
|
||||
'handlers': ['file_catch_all'],
|
||||
'level': 'INFO',
|
||||
'propagate': True,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
METRICS_ENABLED = False # Enable Metrics
|
||||
METRICS_EXPORT_PORT = 8080 # Port to serve metrics on
|
||||
METRICS_MULTIPROC_DIR = '/tmp/prometheus' # path the metrics from multiple-process' save to
|
||||
@ -393,7 +482,22 @@ CSRF_TRUSTED_ORIGINS = [
|
||||
*TRUSTED_ORIGINS
|
||||
]
|
||||
|
||||
|
||||
# Add the user specified log files
|
||||
CENTURION_LOGGING['handlers']['file_centurion']['filename'] = LOG_FILES['centurion']
|
||||
CENTURION_LOGGING['handlers']['file_weblog']['filename'] = LOG_FILES['weblog']
|
||||
CENTURION_LOGGING['handlers']['file_rest_api']['filename'] = LOG_FILES['rest_api']
|
||||
CENTURION_LOGGING['handlers']['file_catch_all']['filename'] = LOG_FILES['catch_all']
|
||||
|
||||
|
||||
if str(CENTURION_LOGGING['handlers']['file_centurion']['filename']).startswith('../log'):
|
||||
|
||||
if not os.path.exists('../log'): # Create log dir
|
||||
|
||||
os.makedirs('../log')
|
||||
|
||||
if DEBUG:
|
||||
|
||||
INSTALLED_APPS += [
|
||||
'debug_toolbar',
|
||||
]
|
||||
@ -407,6 +511,10 @@ if DEBUG:
|
||||
]
|
||||
|
||||
|
||||
# Setup Logging
|
||||
LOGGING = CENTURION_LOGGING
|
||||
|
||||
|
||||
if METRICS_ENABLED:
|
||||
|
||||
INSTALLED_APPS += [ 'django_prometheus', ]
|
||||
|
@ -67,7 +67,10 @@ class SlashCommands(
|
||||
|
||||
returned_line = ''
|
||||
|
||||
if command == 'spend':
|
||||
if(
|
||||
command == 'spend'
|
||||
or command == 'spent'
|
||||
):
|
||||
|
||||
returned_line = re.sub(self.time_spent, self.command_duration, line)
|
||||
|
||||
|
@ -82,7 +82,10 @@ For this command to process the following conditions must be met:
|
||||
user = self.opened_by,
|
||||
)
|
||||
|
||||
elif str(self._meta.verbose_name).lower().replace(' ', '_') == 'ticket_comment':
|
||||
elif(
|
||||
str(self._meta.verbose_name).lower().replace(' ', '_') == 'ticket_comment'
|
||||
or str(self.__class__.__name__).lower().startswith('ticketcomment')
|
||||
):
|
||||
|
||||
self.duration = duration
|
||||
|
||||
|
@ -819,5 +819,18 @@ class TicketBase(
|
||||
|
||||
self.date_closed = datetime.datetime.now(tz=datetime.timezone.utc).replace(microsecond=0).isoformat()
|
||||
|
||||
|
||||
if(
|
||||
self.description != ''
|
||||
and self.description is not None
|
||||
):
|
||||
|
||||
description = self.slash_command(self.description)
|
||||
|
||||
if description != self.description:
|
||||
|
||||
self.description = description
|
||||
|
||||
|
||||
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
|
||||
|
||||
|
@ -45,6 +45,19 @@ class TicketCommentBase(
|
||||
verbose_name_plural = "Ticket Comments"
|
||||
|
||||
|
||||
def field_validation_not_empty(value):
|
||||
|
||||
if value == '' or value is None:
|
||||
|
||||
raise centurion_exception.ValidationError(
|
||||
detail = {
|
||||
'comment_type': 'Comment Type requires a value.'
|
||||
},
|
||||
code = 'comment_type_empty_or_null'
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
model_notes = None
|
||||
|
||||
@ -62,7 +75,6 @@ class TicketCommentBase(
|
||||
parent = models.ForeignKey(
|
||||
'self',
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Parent ID for creating discussion threads',
|
||||
null = True,
|
||||
on_delete = models.PROTECT,
|
||||
@ -80,7 +92,6 @@ class TicketCommentBase(
|
||||
|
||||
external_ref = models.IntegerField(
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'External System reference',
|
||||
null = True,
|
||||
verbose_name = 'Reference Number',
|
||||
@ -89,7 +100,6 @@ class TicketCommentBase(
|
||||
external_system = models.IntegerField(
|
||||
blank = True,
|
||||
choices=TicketBase.Ticket_ExternalSystem,
|
||||
default=None,
|
||||
help_text = 'External system this item derives',
|
||||
null=True,
|
||||
verbose_name = 'External System',
|
||||
@ -98,7 +108,7 @@ class TicketCommentBase(
|
||||
@property
|
||||
def get_comment_type(self):
|
||||
|
||||
comment_type = str(self.Meta.sub_model_type).lower().replace(
|
||||
comment_type = str(self._meta.sub_model_type).lower().replace(
|
||||
' ', '_'
|
||||
)
|
||||
|
||||
@ -128,13 +138,15 @@ class TicketCommentBase(
|
||||
help_text = 'Type this comment is. derived from Meta.verbose_name',
|
||||
max_length = 30,
|
||||
null = False,
|
||||
validators = [
|
||||
field_validation_not_empty
|
||||
],
|
||||
verbose_name = 'Type',
|
||||
)
|
||||
|
||||
category = models.ForeignKey(
|
||||
TicketCommentCategory,
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Category of the comment',
|
||||
null = True,
|
||||
on_delete = models.PROTECT,
|
||||
@ -411,10 +423,3 @@ class TicketCommentBase(
|
||||
if hasattr(self.ticket, '_ticket_comments'):
|
||||
|
||||
del self.ticket._ticket_comments
|
||||
|
||||
# if self.comment_type == self.CommentType.SOLUTION:
|
||||
|
||||
# update_ticket = self.ticket.__class__.objects.get(pk=self.ticket.id)
|
||||
# update_ticket.status = int(TicketBase.TicketStatus.All.SOLVED.value)
|
||||
|
||||
# update_ticket.save()
|
@ -62,6 +62,8 @@ class TicketCommentSolution(
|
||||
|
||||
self.date_closed = datetime.datetime.now(tz=datetime.timezone.utc).replace(microsecond=0).isoformat()
|
||||
|
||||
super().save(force_insert = force_insert, force_update = force_update, using = using, update_fields = update_fields)
|
||||
|
||||
self.ticket.is_solved = self.is_closed
|
||||
|
||||
self.ticket.date_solved = self.date_closed
|
||||
@ -70,8 +72,6 @@ class TicketCommentSolution(
|
||||
|
||||
self.ticket.save()
|
||||
|
||||
super().save(force_insert = force_insert, force_update = force_update, using = using, update_fields = update_fields)
|
||||
|
||||
# clear comment cache
|
||||
if hasattr(self.ticket, '_ticket_comments'):
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,53 @@
|
||||
import pytest
|
||||
|
||||
from core.tests.functional.slash_commands.test_slash_command_related import SlashCommandsTicketInheritedTestCases
|
||||
|
||||
|
||||
|
||||
class TicketBaseModelTestCases(
|
||||
SlashCommandsTicketInheritedTestCases
|
||||
):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ticket(self, request, django_db_blocker, model):
|
||||
""" Ticket that requires body
|
||||
|
||||
when using this fixture, set the `description` then call ticket.save()
|
||||
before use.
|
||||
"""
|
||||
|
||||
with django_db_blocker.unblock():
|
||||
|
||||
ticket = model()
|
||||
|
||||
ticket.organization = request.cls.organization
|
||||
ticket.title = 'A ticket for slash commands'
|
||||
ticket.opened_by = request.cls.ticket_user
|
||||
|
||||
# ticket = TicketBase.objects.create(
|
||||
# organization = request.cls.organization,
|
||||
# title = 'A ticket for slash commands',
|
||||
# opened_by = request.cls.ticket_user,
|
||||
# )
|
||||
|
||||
yield ticket
|
||||
|
||||
with django_db_blocker.unblock():
|
||||
|
||||
ticket.delete()
|
||||
|
||||
|
||||
class TicketBaseModelInheritedTestCases(
|
||||
TicketBaseModelTestCases
|
||||
):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class TicketBaseModelPyTest(
|
||||
TicketBaseModelTestCases
|
||||
):
|
||||
|
||||
pass
|
14
app/core/tests/functional/ticket_comment_base/conftest.py
Normal file
14
app/core/tests/functional/ticket_comment_base/conftest.py
Normal file
@ -0,0 +1,14 @@
|
||||
import pytest
|
||||
|
||||
from core.models.ticket_comment_base import TicketCommentBase
|
||||
|
||||
|
||||
|
||||
@pytest.fixture( scope = 'class')
|
||||
def model(request):
|
||||
|
||||
request.cls.model = TicketCommentBase
|
||||
|
||||
yield request.cls.model
|
||||
|
||||
del request.cls.model
|
@ -0,0 +1,85 @@
|
||||
import pytest
|
||||
|
||||
from core.models.ticket.ticket import Ticket
|
||||
from core.tests.functional.slash_commands.test_slash_command_related import SlashCommandsTicketCommentInheritedTestCases
|
||||
|
||||
|
||||
|
||||
class TicketCommentBaseModelTestCases(
|
||||
SlashCommandsTicketCommentInheritedTestCases
|
||||
):
|
||||
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ticket(self, request, django_db_blocker):
|
||||
""" Ticket that requires body
|
||||
|
||||
when using this fixture, set the `description` then call ticket.save()
|
||||
before use.
|
||||
"""
|
||||
|
||||
from core.models.ticket_comment_base import TicketBase
|
||||
|
||||
with django_db_blocker.unblock():
|
||||
|
||||
ticket = TicketBase()
|
||||
|
||||
ticket.organization = request.cls.organization
|
||||
ticket.title = 'A ticket for slash commands'
|
||||
ticket.opened_by = request.cls.ticket_user
|
||||
|
||||
ticket = TicketBase.objects.create(
|
||||
organization = request.cls.organization,
|
||||
title = 'A ticket for slash commands',
|
||||
opened_by = request.cls.ticket_user,
|
||||
)
|
||||
|
||||
yield ticket
|
||||
|
||||
with django_db_blocker.unblock():
|
||||
|
||||
ticket.delete()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ticket_comment(self, request, django_db_blocker, ticket, model):
|
||||
""" Ticket Comment that requires body
|
||||
|
||||
when using this fixture, set the `body` then call ticket_comment.save()
|
||||
before use.
|
||||
"""
|
||||
|
||||
with django_db_blocker.unblock():
|
||||
|
||||
ticket.title = 'slash command ticket with comment'
|
||||
|
||||
ticket.save()
|
||||
|
||||
ticket_comment = model()
|
||||
|
||||
ticket_comment.user = request.cls.entity_user
|
||||
|
||||
ticket_comment.ticket = ticket
|
||||
|
||||
ticket_comment.comment_type = model._meta.sub_model_type
|
||||
|
||||
yield ticket_comment
|
||||
|
||||
ticket_comment.delete()
|
||||
|
||||
|
||||
|
||||
class TicketCommentBaseModelInheritedTestCases(
|
||||
TicketCommentBaseModelTestCases
|
||||
):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class TicketCommentBaseModelPyTest(
|
||||
TicketCommentBaseModelTestCases
|
||||
):
|
||||
|
||||
pass
|
@ -0,0 +1,14 @@
|
||||
import pytest
|
||||
|
||||
from core.models.ticket_comment_solution import TicketCommentSolution
|
||||
|
||||
|
||||
|
||||
@pytest.fixture( scope = 'class')
|
||||
def model(request):
|
||||
|
||||
request.cls.model = TicketCommentSolution
|
||||
|
||||
yield request.cls.model
|
||||
|
||||
del request.cls.model
|
@ -0,0 +1,28 @@
|
||||
from core.tests.functional.ticket_comment_base.test_functional_ticket_comment_base_model import TicketCommentBaseModelInheritedTestCases
|
||||
|
||||
|
||||
class TicketCommentSolutionModelTestCases(
|
||||
TicketCommentBaseModelInheritedTestCases
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
# check closes ticket
|
||||
|
||||
# check ticket status changes to solved
|
||||
|
||||
|
||||
|
||||
class TicketCommentSolutionModelInheritedTestCases(
|
||||
TicketCommentSolutionModelTestCases
|
||||
):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class TicketCommentSolutionModelPyTest(
|
||||
TicketCommentSolutionModelTestCases
|
||||
):
|
||||
|
||||
pass
|
@ -850,6 +850,71 @@ class TicketBaseModelTestCases(
|
||||
|
||||
|
||||
|
||||
def test_function_called_clean_ticketcommentbase(self, model, mocker):
|
||||
"""Function Check
|
||||
|
||||
Ensure function `TicketBase.clean` is called
|
||||
"""
|
||||
|
||||
spy = mocker.spy(TicketBase, 'clean')
|
||||
|
||||
valid_data = self.kwargs_create_item.copy()
|
||||
|
||||
valid_data['title'] = 'was clean called'
|
||||
|
||||
del valid_data['external_system']
|
||||
|
||||
model.objects.create(
|
||||
**valid_data
|
||||
)
|
||||
|
||||
assert spy.assert_called_once
|
||||
|
||||
|
||||
|
||||
def test_function_called_save_ticketcommentbase(self, model, mocker):
|
||||
"""Function Check
|
||||
|
||||
Ensure function `TicketBase.save` is called
|
||||
"""
|
||||
|
||||
spy = mocker.spy(TicketBase, 'save')
|
||||
|
||||
valid_data = self.kwargs_create_item.copy()
|
||||
|
||||
valid_data['title'] = 'was save called'
|
||||
|
||||
del valid_data['external_system']
|
||||
|
||||
model.objects.create(
|
||||
**valid_data
|
||||
)
|
||||
|
||||
assert spy.assert_called_once
|
||||
|
||||
|
||||
def test_function_save_called_slash_command(self, model, mocker, ticket):
|
||||
"""Function Check
|
||||
|
||||
Ensure function `TicketCommentBase.clean` is called
|
||||
"""
|
||||
|
||||
spy = mocker.spy(self.model, 'slash_command')
|
||||
|
||||
valid_data = self.kwargs_create_item.copy()
|
||||
|
||||
valid_data['title'] = 'was save called'
|
||||
|
||||
del valid_data['external_system']
|
||||
|
||||
item = model.objects.create(
|
||||
**valid_data
|
||||
)
|
||||
|
||||
spy.assert_called_with(item, valid_data['description'])
|
||||
|
||||
|
||||
|
||||
class TicketBaseModelInheritedCases(
|
||||
TicketBaseModelTestCases,
|
||||
):
|
||||
@ -900,3 +965,27 @@ class TicketBaseModelPyTest(
|
||||
"""
|
||||
|
||||
assert type(self.model().get_related_model()) is type(None)
|
||||
|
||||
|
||||
def test_function_save_called_slash_command(self, model, mocker, ticket):
|
||||
"""Function Check
|
||||
|
||||
This test case is a duplicate of a test with the same name. This
|
||||
test is required so that the base class `save()` function can be tested.
|
||||
|
||||
Ensure function `TicketCommentBase.clean` is called
|
||||
"""
|
||||
|
||||
spy = mocker.spy(self.model, 'slash_command')
|
||||
|
||||
valid_data = self.kwargs_create_item.copy()
|
||||
|
||||
valid_data['title'] = 'was save called'
|
||||
|
||||
del valid_data['external_system']
|
||||
|
||||
item = model.objects.create(
|
||||
**valid_data
|
||||
)
|
||||
|
||||
spy.assert_called_with(item, valid_data['description'])
|
||||
|
0
app/core/tests/unit/ticket_comment_base/__init__.py
Normal file
0
app/core/tests/unit/ticket_comment_base/__init__.py
Normal file
14
app/core/tests/unit/ticket_comment_base/conftest.py
Normal file
14
app/core/tests/unit/ticket_comment_base/conftest.py
Normal file
@ -0,0 +1,14 @@
|
||||
import pytest
|
||||
|
||||
from core.models.ticket_comment_base import TicketCommentBase
|
||||
|
||||
|
||||
|
||||
@pytest.fixture( scope = 'class')
|
||||
def model(request):
|
||||
|
||||
request.cls.model = TicketCommentBase
|
||||
|
||||
yield request.cls.model
|
||||
|
||||
del request.cls.model
|
@ -0,0 +1,374 @@
|
||||
import pytest
|
||||
|
||||
from django.contrib.auth.models import ContentType, Permission, User
|
||||
from django.shortcuts import reverse
|
||||
|
||||
from rest_framework.relations import Hyperlink
|
||||
|
||||
from app.tests.common import DoesNotExist
|
||||
|
||||
from api.tests.unit.test_unit_api_fields import (
|
||||
APIFieldsInheritedCases,
|
||||
)
|
||||
|
||||
from core.models.ticket_comment_base import (
|
||||
Entity,
|
||||
TicketBase,
|
||||
TicketCommentBase,
|
||||
TicketCommentCategory
|
||||
)
|
||||
|
||||
|
||||
|
||||
class TicketCommentBaseAPITestCases(
|
||||
APIFieldsInheritedCases,
|
||||
):
|
||||
|
||||
base_model = TicketCommentBase
|
||||
|
||||
|
||||
@pytest.fixture( scope = 'class')
|
||||
def setup_model(self, request, django_db_blocker,
|
||||
model,
|
||||
):
|
||||
|
||||
with django_db_blocker.unblock():
|
||||
|
||||
|
||||
ticket_view_permission = Permission.objects.get(
|
||||
codename = 'view_' + TicketBase._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = TicketBase._meta.app_label,
|
||||
model = TicketBase._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
request.cls.view_team.permissions.add( ticket_view_permission )
|
||||
|
||||
|
||||
|
||||
|
||||
category = TicketCommentCategory.objects.create(
|
||||
organization = request.cls.organization,
|
||||
name = 'comment category'
|
||||
)
|
||||
|
||||
ticket_user = User.objects.create_user(username="ticket_user", password="password")
|
||||
|
||||
ticket = TicketBase.objects.create(
|
||||
organization = request.cls.organization,
|
||||
title = 'ticket comment title',
|
||||
opened_by = ticket_user,
|
||||
)
|
||||
|
||||
|
||||
comment_user = Entity.objects.create(
|
||||
organization = request.cls.organization,
|
||||
)
|
||||
|
||||
request.cls.comment_user = comment_user
|
||||
|
||||
|
||||
valid_data = request.cls.kwargs_create_item.copy()
|
||||
|
||||
valid_data['body'] = 'the template comment'
|
||||
|
||||
del valid_data['external_ref']
|
||||
del valid_data['external_system']
|
||||
del valid_data['category']
|
||||
del valid_data['template']
|
||||
del valid_data['parent']
|
||||
|
||||
valid_data['comment_type'] = TicketCommentBase._meta.sub_model_type
|
||||
valid_data['ticket'] = ticket
|
||||
valid_data['user'] = request.cls.comment_user
|
||||
|
||||
|
||||
template_comment = TicketCommentBase.objects.create(
|
||||
**valid_data
|
||||
)
|
||||
|
||||
|
||||
request.cls.kwargs_create_item.update({
|
||||
'category': category,
|
||||
'ticket': ticket,
|
||||
'user': comment_user,
|
||||
'parent': None,
|
||||
'template': template_comment,
|
||||
'comment_type': model._meta.sub_model_type
|
||||
})
|
||||
|
||||
|
||||
yield
|
||||
|
||||
|
||||
with django_db_blocker.unblock():
|
||||
|
||||
template_comment.delete()
|
||||
|
||||
category.delete()
|
||||
|
||||
del request.cls.comment_user
|
||||
|
||||
for comment in ticket.ticketcommentbase_set.all():
|
||||
|
||||
comment.delete()
|
||||
|
||||
ticket.delete()
|
||||
|
||||
ticket_user.delete()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.fixture( scope = 'class')
|
||||
def post_model(self, request, model, django_db_blocker ):
|
||||
|
||||
request.cls.url_view_kwargs.update({
|
||||
'ticket_id': request.cls.item.ticket.id
|
||||
})
|
||||
|
||||
if (
|
||||
model != self.base_model
|
||||
or self.item.parent
|
||||
):
|
||||
|
||||
request.cls.url_view_kwargs.update({
|
||||
'ticket_comment_model': model._meta.sub_model_type
|
||||
})
|
||||
|
||||
|
||||
valid_data = request.cls.kwargs_create_item.copy()
|
||||
valid_data['body'] = 'the child comment'
|
||||
|
||||
valid_data['comment_type'] = TicketCommentBase._meta.sub_model_type
|
||||
valid_data['parent'] = request.cls.item
|
||||
valid_data['ticket'] = request.cls.item.ticket
|
||||
valid_data['user'] = request.cls.comment_user
|
||||
|
||||
del valid_data['external_ref']
|
||||
del valid_data['external_system']
|
||||
del valid_data['category']
|
||||
del valid_data['template']
|
||||
|
||||
with django_db_blocker.unblock():
|
||||
|
||||
request.cls.item.ticket.is_closed = False
|
||||
request.cls.item.ticket.date_closed = None
|
||||
request.cls.item.ticket.is_solved = False
|
||||
request.cls.item.ticket.date_solved = None
|
||||
request.cls.item.ticket.status = TicketBase.TicketStatus.NEW
|
||||
request.cls.item.ticket.save()
|
||||
|
||||
request.cls.item_two = model.objects.create(
|
||||
**valid_data
|
||||
)
|
||||
|
||||
url_ns_name = '_api_v2_ticket_comment_base_sub_thread'
|
||||
|
||||
request.cls.url_two = reverse(
|
||||
'v2:' + url_ns_name + '-detail',
|
||||
kwargs = {
|
||||
**request.cls.url_view_kwargs,
|
||||
'pk': request.cls.item_two.id,
|
||||
'parent_id': request.cls.item.id,
|
||||
'ticket_comment_model': model._meta.sub_model_type
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
yield
|
||||
|
||||
with django_db_blocker.unblock():
|
||||
|
||||
request.cls.item_two.delete(keep_parents = False)
|
||||
|
||||
del request.cls.item_two
|
||||
|
||||
del request.cls.url_two
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.fixture( scope = 'class', autouse = True)
|
||||
def class_setup(self, request, django_db_blocker,
|
||||
setup_pre,
|
||||
setup_model,
|
||||
create_model,
|
||||
post_model,
|
||||
setup_post,
|
||||
):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@property
|
||||
def parameterized_test_data(self):
|
||||
|
||||
return {
|
||||
|
||||
'parent': {
|
||||
'expected': dict
|
||||
},
|
||||
'parent.id': {
|
||||
'expected': int
|
||||
},
|
||||
'parent.display_name': {
|
||||
'expected': str
|
||||
},
|
||||
'parent.url': {
|
||||
'expected': str
|
||||
},
|
||||
|
||||
|
||||
'ticket': {
|
||||
'expected': dict
|
||||
},
|
||||
'ticket.id': {
|
||||
'expected': int
|
||||
},
|
||||
'ticket.display_name': {
|
||||
'expected': str
|
||||
},
|
||||
'ticket.url': {
|
||||
'expected': str
|
||||
},
|
||||
|
||||
'external_ref': {
|
||||
'expected': int
|
||||
},
|
||||
'external_system': {
|
||||
'expected': int
|
||||
},
|
||||
'comment_type': {
|
||||
'expected': str
|
||||
},
|
||||
'category': {
|
||||
'expected': dict
|
||||
},
|
||||
'category.id': {
|
||||
'expected': int
|
||||
},
|
||||
'category.display_name': {
|
||||
'expected': str
|
||||
},
|
||||
'category.url': {
|
||||
'expected': Hyperlink
|
||||
},
|
||||
|
||||
'body': {
|
||||
'expected': str
|
||||
},
|
||||
'private': {
|
||||
'expected': bool
|
||||
},
|
||||
'duration': {
|
||||
'expected': int
|
||||
},
|
||||
'estimation': {
|
||||
'expected': int
|
||||
},
|
||||
'template': {
|
||||
'expected': dict
|
||||
},
|
||||
'template.id': {
|
||||
'expected': int
|
||||
},
|
||||
'template.display_name': {
|
||||
'expected': str
|
||||
},
|
||||
'template.url': {
|
||||
'expected': str
|
||||
},
|
||||
|
||||
'is_template': {
|
||||
'expected': bool
|
||||
},
|
||||
'source': {
|
||||
'expected': int
|
||||
},
|
||||
'user': {
|
||||
'expected': dict
|
||||
},
|
||||
'user.id': {
|
||||
'expected': int
|
||||
},
|
||||
'user.display_name': {
|
||||
'expected': str
|
||||
},
|
||||
'user.url': {
|
||||
'expected': Hyperlink
|
||||
},
|
||||
|
||||
'is_closed': {
|
||||
'expected': bool
|
||||
},
|
||||
'date_closed': {
|
||||
'expected': str
|
||||
},
|
||||
|
||||
'_urls.threads': {
|
||||
'expected': str
|
||||
},
|
||||
# Below fields dont exist.
|
||||
|
||||
'display_name': {
|
||||
'expected': DoesNotExist
|
||||
},
|
||||
'model_notes': {
|
||||
'expected': DoesNotExist
|
||||
},
|
||||
'_urls.notes': {
|
||||
'expected': DoesNotExist
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
kwargs_create_item: dict = {
|
||||
'parent': '',
|
||||
'ticket': '',
|
||||
'external_ref': 123,
|
||||
'external_system': TicketBase.Ticket_ExternalSystem.CUSTOM_1,
|
||||
'comment_type': '',
|
||||
'category': '',
|
||||
'body': 'the ticket comment',
|
||||
'private': False,
|
||||
'duration': 1,
|
||||
'estimation': 2,
|
||||
'template': '',
|
||||
'is_template': True,
|
||||
'source': TicketBase.TicketSource.HELPDESK,
|
||||
'user': '',
|
||||
'is_closed': True,
|
||||
'date_closed': '2025-05-09T19:32Z',
|
||||
}
|
||||
|
||||
|
||||
|
||||
url_ns_name = '_api_v2_ticket_comment_base'
|
||||
"""Url namespace (optional, if not required) and url name"""
|
||||
|
||||
|
||||
|
||||
class TicketCommentBaseAPIInheritedCases(
|
||||
TicketCommentBaseAPITestCases,
|
||||
):
|
||||
|
||||
kwargs_create_item: dict = None
|
||||
|
||||
model = None
|
||||
|
||||
url_ns_name = '_api_v2_ticket_comment_base_sub'
|
||||
|
||||
|
||||
|
||||
class TicketCommentBaseAPIPyTest(
|
||||
TicketCommentBaseAPITestCases,
|
||||
):
|
||||
|
||||
pass
|
@ -0,0 +1,576 @@
|
||||
import pytest
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from access.models.person import Person
|
||||
|
||||
from app.tests.unit.test_unit_models import (
|
||||
PyTestTenancyObjectInheritedCases,
|
||||
)
|
||||
|
||||
from core.models.ticket_comment_base import TicketBase, TicketCommentBase, TicketCommentCategory
|
||||
|
||||
|
||||
|
||||
class TicketCommentBaseModelTestCases(
|
||||
PyTestTenancyObjectInheritedCases,
|
||||
):
|
||||
|
||||
base_model = TicketCommentBase
|
||||
|
||||
sub_model_type = 'comment'
|
||||
"""Sub Model Type
|
||||
|
||||
sub-models must have this attribute defined in `ModelName.Meta.sub_model_type`
|
||||
"""
|
||||
|
||||
kwargs_create_item: dict = {
|
||||
'parent': None,
|
||||
'ticket': '',
|
||||
'external_ref': 0,
|
||||
'external_system': TicketBase.Ticket_ExternalSystem.CUSTOM_1,
|
||||
'comment_type': sub_model_type,
|
||||
'category': '',
|
||||
'body': 'asdasdas',
|
||||
'private': False,
|
||||
'template': None,
|
||||
'source': TicketBase.TicketSource.HELPDESK,
|
||||
'user': '',
|
||||
'is_closed': True,
|
||||
'date_closed': '2025-05-08T17:10Z',
|
||||
}
|
||||
|
||||
|
||||
parameterized_fields: dict = {
|
||||
"is_global": {
|
||||
'field_type': None,
|
||||
'field_parameter_default_exists': None,
|
||||
'field_parameter_default_value': None,
|
||||
'field_parameter_verbose_name_type': None
|
||||
},
|
||||
"model_notes": {
|
||||
'field_type': None,
|
||||
'field_parameter_default_exists': None,
|
||||
'field_parameter_default_value': None,
|
||||
'field_parameter_verbose_name_type': None
|
||||
},
|
||||
"parent": {
|
||||
'field_type': models.ForeignKey,
|
||||
'field_parameter_default_exists': False,
|
||||
'field_parameter_verbose_name_type': str,
|
||||
},
|
||||
"ticket": {
|
||||
'field_type': models.ForeignKey,
|
||||
'field_parameter_default_exists': False,
|
||||
'field_parameter_verbose_name_type': str,
|
||||
},
|
||||
"external_ref": {
|
||||
'field_type': models.fields.IntegerField,
|
||||
'field_parameter_default_exists': False,
|
||||
'field_parameter_verbose_name_type': str,
|
||||
},
|
||||
"external_system": {
|
||||
'field_type': models.fields.IntegerField,
|
||||
'field_parameter_default_exists': False,
|
||||
'field_parameter_verbose_name_type': str,
|
||||
},
|
||||
"comment_type": {
|
||||
'field_type': models.fields.CharField,
|
||||
'field_parameter_default_exists': False,
|
||||
'field_parameter_verbose_name_type': str,
|
||||
},
|
||||
"category": {
|
||||
'field_type': models.ForeignKey,
|
||||
'field_parameter_default_exists': False,
|
||||
'field_parameter_verbose_name_type': str,
|
||||
},
|
||||
"body": {
|
||||
'field_type': models.fields.TextField,
|
||||
'field_parameter_default_exists': False,
|
||||
'field_parameter_verbose_name_type': str,
|
||||
},
|
||||
"private": {
|
||||
'field_type': models.fields.BooleanField,
|
||||
'field_parameter_default_exists': True,
|
||||
'field_parameter_default_value': False,
|
||||
'field_parameter_verbose_name_type': str,
|
||||
},
|
||||
"duration": {
|
||||
'field_type': models.fields.IntegerField,
|
||||
'field_parameter_default_exists': True,
|
||||
'field_parameter_default_value': 0,
|
||||
'field_parameter_verbose_name_type': str,
|
||||
},
|
||||
"estimation": {
|
||||
'field_type': models.fields.IntegerField,
|
||||
'field_parameter_default_exists': True,
|
||||
'field_parameter_default_value': 0,
|
||||
'field_parameter_verbose_name_type': str,
|
||||
},
|
||||
"template": {
|
||||
'field_type': models.ForeignKey,
|
||||
'field_parameter_default_exists': True,
|
||||
'field_parameter_verbose_name_type': str,
|
||||
},
|
||||
"source": {
|
||||
'field_type': models.fields.IntegerField,
|
||||
'field_parameter_default_exists': True,
|
||||
'field_parameter_default_value': TicketBase.TicketSource.HELPDESK,
|
||||
'field_parameter_verbose_name_type': str,
|
||||
},
|
||||
"user": {
|
||||
'field_type': models.ForeignKey,
|
||||
'field_parameter_default_exists': False,
|
||||
'field_parameter_default_value': None,
|
||||
'field_parameter_verbose_name_type': str,
|
||||
},
|
||||
"is_closed": {
|
||||
'field_type': models.fields.BooleanField,
|
||||
'field_parameter_default_exists': True,
|
||||
'field_parameter_default_value': False,
|
||||
'field_parameter_verbose_name_type': str,
|
||||
},
|
||||
"date_closed": {
|
||||
'field_type': models.fields.DateTimeField,
|
||||
'field_parameter_default_exists': False,
|
||||
'field_parameter_verbose_name_type': str,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture( scope = 'class')
|
||||
def setup_model(self,
|
||||
request,
|
||||
model,
|
||||
django_db_blocker,
|
||||
organization_one,
|
||||
organization_two
|
||||
):
|
||||
|
||||
request.cls.model = model
|
||||
|
||||
with django_db_blocker.unblock():
|
||||
|
||||
request.cls.organization = organization_one
|
||||
|
||||
request.cls.different_organization = organization_two
|
||||
|
||||
kwargs_create_item = {}
|
||||
|
||||
for base in reversed(request.cls.__mro__):
|
||||
|
||||
if hasattr(base, 'kwargs_create_item'):
|
||||
|
||||
if base.kwargs_create_item is None:
|
||||
|
||||
continue
|
||||
|
||||
kwargs_create_item.update(**base.kwargs_create_item)
|
||||
|
||||
|
||||
if len(kwargs_create_item) > 0:
|
||||
|
||||
request.cls.kwargs_create_item = kwargs_create_item
|
||||
|
||||
|
||||
request.cls.view_user = User.objects.create_user(username="cafs_test_user_view", password="password")
|
||||
|
||||
comment_category = TicketCommentCategory.objects.create(
|
||||
organization = request.cls.organization,
|
||||
name = 'test cat comment'
|
||||
)
|
||||
|
||||
ticket = TicketBase.objects.create(
|
||||
organization = request.cls.organization,
|
||||
title = 'tester comment ticket',
|
||||
description = 'aa',
|
||||
opened_by = request.cls.view_user,
|
||||
)
|
||||
|
||||
user = Person.objects.create(
|
||||
organization = request.cls.organization,
|
||||
f_name = 'ip',
|
||||
l_name = 'funny'
|
||||
)
|
||||
|
||||
request.cls.kwargs_create_item.update({
|
||||
'category': comment_category,
|
||||
'ticket': ticket,
|
||||
'user': user,
|
||||
})
|
||||
|
||||
|
||||
if 'organization' not in request.cls.kwargs_create_item:
|
||||
|
||||
request.cls.kwargs_create_item.update({
|
||||
'organization': request.cls.organization
|
||||
})
|
||||
|
||||
yield
|
||||
|
||||
with django_db_blocker.unblock():
|
||||
|
||||
del request.cls.kwargs_create_item
|
||||
|
||||
comment_category.delete()
|
||||
|
||||
ticket.delete()
|
||||
|
||||
user.delete()
|
||||
|
||||
request.cls.view_user.delete()
|
||||
|
||||
|
||||
|
||||
@pytest.fixture( scope = 'class', autouse = True)
|
||||
def class_setup(self,
|
||||
setup_model,
|
||||
create_model,
|
||||
):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ticket(self, request, django_db_blocker):
|
||||
|
||||
with django_db_blocker.unblock():
|
||||
|
||||
ticket = TicketBase.objects.create(
|
||||
organization = request.cls.organization,
|
||||
title = 'per function_ticket',
|
||||
opened_by = request.cls.view_user,
|
||||
)
|
||||
|
||||
yield ticket
|
||||
|
||||
|
||||
with django_db_blocker.unblock():
|
||||
|
||||
for comment in ticket.ticketcommentbase_set.all():
|
||||
|
||||
comment.delete()
|
||||
|
||||
ticket.delete()
|
||||
|
||||
|
||||
def test_create_validation_exception_no_organization(self):
|
||||
""" Tenancy objects must have an organization
|
||||
|
||||
This test case is an over-ride of a test with the same name. this test
|
||||
is not required as the organization is derived from the ticket.
|
||||
|
||||
Must not be able to create an item without an organization
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def test_class_inherits_ticketcommentbase(self):
|
||||
""" Class inheritence
|
||||
|
||||
TenancyObject must inherit SaveHistory
|
||||
"""
|
||||
|
||||
assert issubclass(self.model, TicketCommentBase)
|
||||
|
||||
|
||||
def test_attribute_meta_exists_permissions(self):
|
||||
"""Attribute Check
|
||||
|
||||
Ensure attribute `Meta.permissions` exists
|
||||
"""
|
||||
|
||||
assert hasattr(self.model._meta, 'permissions')
|
||||
|
||||
|
||||
def test_attribute_meta_not_none_permissions(self):
|
||||
"""Attribute Check
|
||||
|
||||
Ensure attribute `Meta.permissions` does not have a value of none
|
||||
"""
|
||||
|
||||
assert self.model._meta.permissions is not None
|
||||
|
||||
|
||||
def test_attribute_meta_type_permissions(self):
|
||||
"""Attribute Check
|
||||
|
||||
Ensure attribute `Meta.permissions` value is of type list
|
||||
"""
|
||||
|
||||
assert type(self.model._meta.permissions) is list
|
||||
|
||||
|
||||
def test_attribute_value_permissions_has_import(self):
|
||||
"""Attribute Check
|
||||
|
||||
Ensure attribute `Meta.permissions` value contains permission
|
||||
`import`
|
||||
"""
|
||||
|
||||
permission_found = False
|
||||
|
||||
for permission, description in self.model._meta.permissions:
|
||||
|
||||
if permission == 'import_' + self.model._meta.model_name:
|
||||
|
||||
permission_found = True
|
||||
break
|
||||
|
||||
assert permission_found
|
||||
|
||||
|
||||
def test_attribute_value_permissions_has_triage(self):
|
||||
"""Attribute Check
|
||||
|
||||
Ensure attribute `Meta.permissions` value contains permission
|
||||
`triage`
|
||||
"""
|
||||
|
||||
permission_found = False
|
||||
|
||||
for permission, description in self.model._meta.permissions:
|
||||
|
||||
if permission == 'triage_' + self.model._meta.model_name:
|
||||
|
||||
permission_found = True
|
||||
break
|
||||
|
||||
assert permission_found
|
||||
|
||||
|
||||
def test_attribute_value_permissions_has_purge(self):
|
||||
"""Attribute Check
|
||||
|
||||
Ensure attribute `Meta.permissions` value contains permission
|
||||
`purge`
|
||||
"""
|
||||
|
||||
permission_found = False
|
||||
|
||||
for permission, description in self.model._meta.permissions:
|
||||
|
||||
if permission == 'purge_' + self.model._meta.model_name:
|
||||
|
||||
permission_found = True
|
||||
break
|
||||
|
||||
assert permission_found
|
||||
|
||||
|
||||
def test_attribute_meta_type_sub_model_type(self):
|
||||
"""Attribute Check
|
||||
|
||||
Ensure attribute `Meta.sub_model_type` value is of type str
|
||||
"""
|
||||
|
||||
assert type(self.model._meta.sub_model_type) is str
|
||||
|
||||
|
||||
def test_attribute_meta_value_sub_model_type(self):
|
||||
"""Attribute Check
|
||||
|
||||
Ensure attribute `Meta.sub_model_type` value is correct
|
||||
"""
|
||||
|
||||
assert self.model._meta.sub_model_type == self.sub_model_type
|
||||
|
||||
|
||||
def test_attribute_type_get_comment_type(self):
|
||||
"""Attribute Check
|
||||
|
||||
Ensure attribute `get_comment_type` value is correct
|
||||
"""
|
||||
|
||||
assert self.item.get_comment_type == self.item._meta.sub_model_type
|
||||
|
||||
|
||||
|
||||
def test_function_get_related_model(self):
|
||||
"""Function Check
|
||||
|
||||
Confirm function `get_related_model` returns `None` for self
|
||||
"""
|
||||
|
||||
assert self.item.get_related_model() == None
|
||||
|
||||
|
||||
|
||||
def test_function_get_related_field_name(self):
|
||||
"""Function Check
|
||||
|
||||
Confirm function `get_related_field_name` returns an empty string
|
||||
for self
|
||||
"""
|
||||
|
||||
assert self.item.get_related_field_name() == ''
|
||||
|
||||
|
||||
|
||||
def test_function_get_url(self):
|
||||
"""Function Check
|
||||
|
||||
Confirm function `get_url` returns the correct url
|
||||
"""
|
||||
|
||||
if self.item.parent:
|
||||
|
||||
expected_value = '/core/ticket/' + str(self.item.ticket.id) + '/' + self.sub_model_type + '/' + str(
|
||||
self.item.parent.id) + '/threads/' + str(self.item.id)
|
||||
|
||||
else:
|
||||
|
||||
expected_value = '/core/ticket/' + str( self.item.ticket.id) + '/' + self.sub_model_type + '/' + str(self.item.id)
|
||||
|
||||
assert self.item.get_url() == '/api/v2' + expected_value
|
||||
|
||||
|
||||
def test_function_parent_object(self):
|
||||
"""Function Check
|
||||
|
||||
Confirm function `parent_object` returns the ticket
|
||||
"""
|
||||
|
||||
assert self.item.parent_object == self.item.ticket
|
||||
|
||||
|
||||
def test_function_clean_validation_mismatch_comment_type_raises_exception(self):
|
||||
"""Function Check
|
||||
|
||||
Ensure function `clean` does validation
|
||||
"""
|
||||
|
||||
valid_data = self.kwargs_create_item.copy()
|
||||
|
||||
valid_data['comment_type'] = 'Nope'
|
||||
|
||||
with pytest.raises(ValidationError) as err:
|
||||
|
||||
self.model.objects.create(
|
||||
**valid_data
|
||||
)
|
||||
|
||||
assert err.value.get_codes()['comment_type'] == 'comment_type_wrong_endpoint'
|
||||
|
||||
|
||||
|
||||
def test_function_called_clean_ticketcommentbase(self, model, mocker, ticket):
|
||||
"""Function Check
|
||||
|
||||
Ensure function `TicketCommentBase.clean` is called
|
||||
"""
|
||||
|
||||
spy = mocker.spy(TicketCommentBase, 'clean')
|
||||
|
||||
valid_data = self.kwargs_create_item.copy()
|
||||
|
||||
valid_data['ticket'] = ticket
|
||||
|
||||
del valid_data['external_system']
|
||||
del valid_data['external_ref']
|
||||
|
||||
model.objects.create(
|
||||
**valid_data
|
||||
)
|
||||
|
||||
assert spy.assert_called_once
|
||||
|
||||
|
||||
def test_function_save_called_slash_command(self, model, mocker, ticket):
|
||||
"""Function Check
|
||||
|
||||
Ensure function `TicketCommentBase.clean` is called
|
||||
"""
|
||||
|
||||
spy = mocker.spy(self.model, 'slash_command')
|
||||
|
||||
valid_data = self.kwargs_create_item.copy()
|
||||
|
||||
valid_data['ticket'] = ticket
|
||||
|
||||
del valid_data['external_system']
|
||||
del valid_data['external_ref']
|
||||
|
||||
item = model.objects.create(
|
||||
**valid_data
|
||||
)
|
||||
|
||||
spy.assert_called_with(item, valid_data['body'])
|
||||
|
||||
|
||||
|
||||
class TicketCommentBaseModelInheritedCases(
|
||||
TicketCommentBaseModelTestCases,
|
||||
):
|
||||
"""Sub-Ticket Test Cases
|
||||
|
||||
Test Cases for Ticket models that inherit from model TicketCommentBase
|
||||
"""
|
||||
|
||||
kwargs_create_item: dict = {}
|
||||
|
||||
model = None
|
||||
|
||||
|
||||
sub_model_type = None
|
||||
"""Ticket Sub Model Type
|
||||
|
||||
Ticket sub-models must have this attribute defined in `ModelNam.Meta.sub_model_type`
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class TicketCommentBaseModelPyTest(
|
||||
TicketCommentBaseModelTestCases,
|
||||
):
|
||||
|
||||
|
||||
def test_function_clean_validation_close_raises_exception(self, ticket):
|
||||
"""Function Check
|
||||
|
||||
Ensure function `clean` does validation
|
||||
"""
|
||||
|
||||
valid_data = self.kwargs_create_item.copy()
|
||||
|
||||
valid_data['ticket'] = ticket
|
||||
|
||||
del valid_data['date_closed']
|
||||
|
||||
with pytest.raises(ValidationError) as err:
|
||||
|
||||
self.model.objects.create(
|
||||
**valid_data
|
||||
)
|
||||
|
||||
assert err.value.get_codes()['date_closed'] == 'ticket_closed_no_date'
|
||||
|
||||
|
||||
def test_function_save_called_slash_command(self, model, mocker, ticket):
|
||||
"""Function Check
|
||||
|
||||
This test case is a duplicate of a test with the same name. This
|
||||
test is required so that the base class `save()` function can be tested.
|
||||
|
||||
Ensure function `TicketCommentBase.clean` is called
|
||||
"""
|
||||
|
||||
spy = mocker.spy(self.model, 'slash_command')
|
||||
|
||||
valid_data = self.kwargs_create_item.copy()
|
||||
|
||||
valid_data['ticket'] = ticket
|
||||
|
||||
del valid_data['external_system']
|
||||
del valid_data['external_ref']
|
||||
|
||||
item = model.objects.create(
|
||||
**valid_data
|
||||
)
|
||||
|
||||
spy.assert_called_with(item, valid_data['body'])
|
@ -0,0 +1,121 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from api.tests.unit.test_unit_common_viewset import SubModelViewSetInheritedCases
|
||||
|
||||
from core.viewsets.ticket_comment import (
|
||||
NoDocsViewSet,
|
||||
TicketBase,
|
||||
TicketCommentBase,
|
||||
ViewSet
|
||||
)
|
||||
|
||||
|
||||
class TicketCommentBaseViewsetTestCases(
|
||||
SubModelViewSetInheritedCases,
|
||||
):
|
||||
|
||||
model = None
|
||||
|
||||
viewset = ViewSet
|
||||
|
||||
base_model = TicketCommentBase
|
||||
|
||||
route_name = None
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
||||
|
||||
self.viewset = ViewSet
|
||||
|
||||
|
||||
if self.model is None:
|
||||
|
||||
self.model = TicketCommentBase
|
||||
|
||||
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
self.ticket = TicketBase.objects.create(
|
||||
organization = self.organization,
|
||||
title = 'ticket comment test',
|
||||
opened_by = self.view_user,
|
||||
)
|
||||
|
||||
self.kwargs = {
|
||||
'ticket_id': self.ticket.id
|
||||
}
|
||||
|
||||
if self.model is not TicketCommentBase:
|
||||
|
||||
self.kwargs = {
|
||||
**self.kwargs,
|
||||
'ticket_comment_model': self.model._meta.sub_model_type
|
||||
}
|
||||
|
||||
self.viewset.kwargs = self.kwargs
|
||||
|
||||
|
||||
client = Client()
|
||||
|
||||
url = reverse(
|
||||
self.route_name + '-list',
|
||||
kwargs = self.kwargs
|
||||
)
|
||||
|
||||
client.force_login(self.view_user)
|
||||
|
||||
self.http_options_response_list = client.options(url)
|
||||
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
|
||||
cls.ticket.delete()
|
||||
|
||||
super().tearDownClass()
|
||||
|
||||
|
||||
|
||||
def test_view_attr_value_model_kwarg(self):
|
||||
"""Attribute Test
|
||||
|
||||
Attribute `model_kwarg` must be equal to model._meta.sub_model_type
|
||||
"""
|
||||
|
||||
view_set = self.viewset()
|
||||
|
||||
assert view_set.model_kwarg == 'ticket_comment_model'
|
||||
|
||||
|
||||
|
||||
class TicketCommentBaseViewsetInheritedCases(
|
||||
TicketCommentBaseViewsetTestCases,
|
||||
):
|
||||
"""Test Suite for Sub-Models of TicketCommentBase
|
||||
|
||||
Use this Test suit if your sub-model inherits directly from TicketCommentBase.
|
||||
"""
|
||||
|
||||
model: str = None
|
||||
"""name of the model to test"""
|
||||
|
||||
route_name = 'v2:_api_v2_ticket_comment_base_sub'
|
||||
|
||||
|
||||
|
||||
class TicketCommentBaseViewsetTest(
|
||||
TicketCommentBaseViewsetTestCases,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
kwargs = {}
|
||||
|
||||
route_name = 'v2:_api_v2_ticket_comment_base'
|
||||
|
||||
viewset = NoDocsViewSet
|
14
app/core/tests/unit/ticket_comment_solution/conftest.py
Normal file
14
app/core/tests/unit/ticket_comment_solution/conftest.py
Normal file
@ -0,0 +1,14 @@
|
||||
import pytest
|
||||
|
||||
from core.models.ticket_comment_solution import TicketCommentSolution
|
||||
|
||||
|
||||
|
||||
@pytest.fixture( scope = 'class')
|
||||
def model(request):
|
||||
|
||||
request.cls.model = TicketCommentSolution
|
||||
|
||||
yield request.cls.model
|
||||
|
||||
del request.cls.model
|
@ -0,0 +1,84 @@
|
||||
import pytest
|
||||
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from core.models.ticket_comment_solution import TicketCommentSolution
|
||||
from core.tests.unit.ticket_comment_base.test_unit_ticket_comment_base_model import (
|
||||
TicketCommentBaseModelInheritedCases
|
||||
)
|
||||
|
||||
|
||||
class TicketCommentSolutionModelTestCases(
|
||||
TicketCommentBaseModelInheritedCases,
|
||||
):
|
||||
|
||||
sub_model_type = 'solution'
|
||||
"""Sub Model Type
|
||||
|
||||
sub-models must have this attribute defined in `ModelName.Meta.sub_model_type`
|
||||
"""
|
||||
|
||||
kwargs_create_item: dict = {
|
||||
'comment_type': sub_model_type,
|
||||
}
|
||||
|
||||
|
||||
def test_class_inherits_ticketcommentsolution(self):
|
||||
""" Class inheritence
|
||||
|
||||
TenancyObject must inherit SaveHistory
|
||||
"""
|
||||
|
||||
assert issubclass(self.model, TicketCommentSolution)
|
||||
|
||||
|
||||
|
||||
def test_function_called_clean_ticketcommentsolution(self, model, mocker, ticket):
|
||||
"""Function Check
|
||||
|
||||
Ensure function `TicketCommentBase.clean` is called
|
||||
"""
|
||||
|
||||
spy = mocker.spy(TicketCommentSolution, 'clean')
|
||||
|
||||
valid_data = self.kwargs_create_item.copy()
|
||||
|
||||
valid_data['ticket'] = ticket
|
||||
|
||||
del valid_data['external_system']
|
||||
del valid_data['external_ref']
|
||||
|
||||
model.objects.create(
|
||||
**valid_data
|
||||
)
|
||||
|
||||
assert spy.assert_called_once
|
||||
|
||||
|
||||
|
||||
class TicketCommentSolutionModelInheritedCases(
|
||||
TicketCommentSolutionModelTestCases,
|
||||
):
|
||||
"""Sub-Ticket Test Cases
|
||||
|
||||
Test Cases for Ticket models that inherit from model TicketCommentSolution
|
||||
"""
|
||||
|
||||
kwargs_create_item: dict = {}
|
||||
|
||||
model = None
|
||||
|
||||
|
||||
sub_model_type = None
|
||||
"""Ticket Sub Model Type
|
||||
|
||||
Ticket sub-models must have this attribute defined in `ModelNam.Meta.sub_model_type`
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class TicketCommentSolutionModelPyTest(
|
||||
TicketCommentSolutionModelTestCases,
|
||||
):
|
||||
|
||||
pass
|
@ -0,0 +1,39 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from core.models.ticket_comment_solution import TicketCommentSolution
|
||||
from core.tests.unit.ticket_comment_base.test_unit_ticket_comment_base_viewset import TicketCommentBaseViewsetInheritedCases
|
||||
|
||||
|
||||
|
||||
class TicketCommentSolutionViewsetTestCases(
|
||||
TicketCommentBaseViewsetInheritedCases,
|
||||
):
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
||||
self.model = TicketCommentSolution
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
|
||||
class TicketCommentSolutionViewsetInheritedCases(
|
||||
TicketCommentSolutionViewsetTestCases,
|
||||
):
|
||||
"""Test Suite for Sub-Models of TicketBase
|
||||
|
||||
Use this Test suit if your sub-model inherits directly from TicketCommentSolution.
|
||||
"""
|
||||
|
||||
model: str = None
|
||||
"""name of the model to test"""
|
||||
|
||||
|
||||
|
||||
class TicketCommentSolutionViewsetTest(
|
||||
TicketCommentSolutionViewsetTestCases,
|
||||
TestCase,
|
||||
):
|
||||
pass
|
@ -0,0 +1,31 @@
|
||||
from core.tests.unit.ticket_comment_base.test_unit_ticket_comment_base_api_fields import (
|
||||
TicketCommentBaseAPIInheritedCases
|
||||
)
|
||||
|
||||
|
||||
|
||||
class TicketCommentSolutionAPITestCases(
|
||||
TicketCommentBaseAPIInheritedCases,
|
||||
):
|
||||
|
||||
parameterized_test_data = {}
|
||||
|
||||
kwargs_create_item: dict = {}
|
||||
|
||||
|
||||
|
||||
class TicketCommentSolutionAPIInheritedCases(
|
||||
TicketCommentSolutionAPITestCases,
|
||||
):
|
||||
|
||||
kwargs_create_item: dict = {None}
|
||||
|
||||
model = None
|
||||
|
||||
|
||||
|
||||
class TicketCommentSolutionAPIPyTest(
|
||||
TicketCommentSolutionAPITestCases,
|
||||
):
|
||||
|
||||
pass
|
@ -40,4 +40,6 @@ As with any other object within Centurion, the addition of a feature requires it
|
||||
|
||||
- API Permissions `core.tests.functional.ticket_base.test_functional_ticket_base_permission.TicketBasePermissionsAPIInheritedCases`
|
||||
|
||||
- Model `app.core.tests.functional.ticket_base.test_functional_ticket_base_model.TicketBaseModelInheritedTestCases` _(if inheriting from `TicketBase`)_ Test cases for sub-models
|
||||
|
||||
The above listed test cases cover **all** tests for objects that are inherited from the base class. To complete the tests, you will need to add test cases for the differences your model introduces.
|
||||
|
@ -0,0 +1,45 @@
|
||||
---
|
||||
title: Ticket Comment
|
||||
description: Centurion ERP Base Model Ticket Comment development documentation
|
||||
date: 2025-04-16
|
||||
template: project.html
|
||||
about: https://github.com/nofusscomputing/centurion_erp
|
||||
---
|
||||
|
||||
Ticket Comments is a base model within Centurion ERP. This base provides the core features for all subsequent sub-ticket_comment models. As such extending Centurion ERP with a new ticket comment type is a simple process. The adding of a ticket comment type only requires that you extend an existing ticket model containing only the changes for your new ticket type.
|
||||
|
||||
|
||||
## Core Features
|
||||
|
||||
- ...
|
||||
|
||||
|
||||
## History
|
||||
|
||||
Ticketing does not use the standard history model of Centurion ERP. History for a ticket is kept in the form of action comments. As each change to a ticket occurs, an action comment is created denoting the from and to in relation to a change.
|
||||
|
||||
|
||||
## Model
|
||||
|
||||
When creating your sub-model, do not re-define any field that is already specified within the model you are inheriting from. This is however, with the exception of the code docs specifying otherwise.
|
||||
|
||||
|
||||
## Testing
|
||||
|
||||
As with any other object within Centurion, the addition of a feature requires it be tested. The following Test Suites are available:
|
||||
|
||||
- `Unit` Test Cases
|
||||
|
||||
- `core.tests.unit.ticket_comment_base.<*>.<Inherited class name>InheritedCases` _(if inheriting from `TicketCommentBase`)_ Test cases for sub-models
|
||||
|
||||
- ViewSet `core.tests.unit.ticket_comment_base.test_unit_ticket_comment_base_viewset.TicketCommentBaseViewsetInheritedCases`
|
||||
|
||||
- `Functional` Test Cases
|
||||
|
||||
- `core.tests.functional.ticket_comment_base.<*>.<Inherited class name>InheritedCases` _(if inheriting from `TicketCommentBase`)_ Test cases for sub-models
|
||||
|
||||
- API Permissions `core.tests.functional.ticket_comment_base.test_functional_ticket_comment_base_permission.TicketCommentBasePermissionsAPIInheritedCases`
|
||||
|
||||
- Model `app.core.tests.functional.ticket_comment_base.test_functional_ticket_comment_base_model.TicketCommentBaseModelInheritedTestCases` _(if inheriting from `TicketCommentBase`)_ Test cases for sub-models
|
||||
|
||||
The above listed test cases cover **all** tests for objects that are inherited from the base class. To complete the tests, you will need to add test cases for the differences your model introduces.
|
@ -96,7 +96,7 @@ We do have some core sub-models available. There intended purpose is to serve as
|
||||
|
||||
- [Ticket](./core/ticket.md)
|
||||
|
||||
- Ticket Comment
|
||||
- [Ticket Comment](./core/ticket_comment.md)
|
||||
|
||||
All sub-models are intended to be extended and contain the core features for ALL models. This aids in extensibility and reduces the work required to add a model.
|
||||
|
||||
|
@ -33,6 +33,15 @@ FEATURE_FLAGGING_ENABLED = True # Turn Feature Flagging on/off
|
||||
|
||||
FEATURE_FLAG_OVERRIDES = [] # Feature Flag Overrides. Takes preceedence over downloaded feature flags.
|
||||
|
||||
|
||||
LOG_FILES = { # Location where log files will be created
|
||||
"centurion": "/var/log/centurion.log",
|
||||
"weblog": "/var/log/weblog.log",
|
||||
"rest_api": "/var/log/rest_api.log",
|
||||
"catch_all":"/var/log/catch-all.log"
|
||||
}
|
||||
|
||||
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
|
||||
SECURE_SSL_REDIRECT = True
|
||||
|
@ -153,6 +153,8 @@ nav:
|
||||
|
||||
- projects/centurion_erp/development/core/ticket.md
|
||||
|
||||
- projects/centurion_erp/development/core/ticket_comment.md
|
||||
|
||||
- projects/centurion_erp/development/views.md
|
||||
|
||||
- User:
|
||||
|
@ -1,23 +1,23 @@
|
||||
|
||||
Django==5.1.8
|
||||
django==5.1.9
|
||||
django-cors-headers==4.4.0
|
||||
|
||||
django-debug-toolbar==5.1.0
|
||||
social-auth-app-django==5.4.1
|
||||
|
||||
djangorestframework==3.15.2
|
||||
djangorestframework-jsonapi==7.0.2
|
||||
djangorestframework==3.16.0
|
||||
djangorestframework-jsonapi==7.1.0
|
||||
|
||||
# DRF
|
||||
pyyaml>=6.0.1
|
||||
pyyaml>=6.0.2
|
||||
django-filter==24.2
|
||||
|
||||
# OpenAPI Schema
|
||||
uritemplate==4.1.1
|
||||
coreapi==2.3.3
|
||||
|
||||
drf-spectacular==0.27.2
|
||||
drf-spectacular[sidecar]==0.27.2
|
||||
drf-spectacular==0.28.0
|
||||
drf-spectacular[sidecar]==0.28.0
|
||||
|
||||
django_split_settings==1.3.1
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
pytest==8.3.5
|
||||
pytest-django==4.11.1
|
||||
pytest-mock==3.14.0
|
||||
coverage==7.8.0
|
||||
pytest-cov==6.1.1
|
||||
|
||||
|
Reference in New Issue
Block a user