refactor(base): rename app to centurion

ref: #764
This commit is contained in:
2025-05-16 22:10:38 +09:30
parent 3d2d759d6b
commit 17c7980e03
248 changed files with 229 additions and 308 deletions

View File

@ -0,0 +1,3 @@
from .celery import worker as celery_app
__all__ = ('celery_app',)

16
app/centurion/asgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
ASGI config for itsm project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'centurion.settings')
application = get_asgi_application()

View File

@ -0,0 +1,224 @@
import re
from centurion.urls import urlpatterns
from django.conf import settings
from django.urls import URLPattern, URLResolver
from access.models.tenant import Tenant as Organization
from settings.models.user_settings import UserSettings
def build_details(context) -> dict:
return {
'project_url': settings.BUILD_REPO,
'sha': settings.BUILD_SHA,
'version': settings.BUILD_VERSION,
}
def request(request):
return request.get_full_path()
def social_backends(request):
""" Fetch Backend Names
Required for use on the login page to dynamically build the social auth URLS
Returns:
list(str): backend name
"""
from importlib import import_module
social_backends = []
if hasattr(settings, 'SSO_BACKENDS'):
for backend in settings.SSO_BACKENDS:
paths = str(backend).split('.')
module = import_module(paths[0] + '.' + paths[1] + '.' + paths[2])
backend_class = getattr(module, paths[3])
backend = backend_class.name
social_backends += [ str(backend) ]
return social_backends
def user_settings(context) -> int:
""" Provides the settings ID for the current user.
If user settings object doesn't exist, it's probably a new user. So create their settings row.
Returns:
int: model usersettings Primary Key
"""
if context.user.is_authenticated:
settings = UserSettings.objects.filter(user=context.user)
if not settings.exists():
UserSettings.objects.create(user=context.user)
settings = UserSettings.objects.filter(user=context.user)
return settings[0].pk
return None
def user_default_organization(context) -> int:
""" Provides the users default organization.
Returns:
int: Users Default Organization
"""
if context.user.is_authenticated:
settings = UserSettings.objects.filter(user=context.user)
if settings[0].default_organization:
return settings[0].default_organization.id
return None
def nav_items(context) -> list(dict()):
""" Fetch All Project URLs
Collect the project URLs for use in creating the site navigation.
The returned list contains a dictionary with the following items:
name: {str} Group Name
urls: {list} List of URLs for the group
is_active: {bool} if any of the links in this group are active
Each group url list item contains a dicionary with the following items:
name: {str} The display name for the link
url: {str} link URL
is_active: {bool} if this link is the active URL
Returns:
list: Items user has view access to
"""
dnav = []
re_pattern = re.compile('[a-z/0-9]+')
for nav_group in urlpatterns:
group_active = False
ignored_apps = [
'admin',
'djdt', # Debug application
'api',
'social',
]
nav_items = []
if (
isinstance(nav_group, URLPattern)
):
group_name = str(nav_group.name)
elif (
isinstance(nav_group, URLResolver)
):
if nav_group.app_name is not None and str(nav_group.app_name).lower() not in ignored_apps:
group_name = str(nav_group.app_name)
for pattern in nav_group.url_patterns:
is_active = False
url = '/' + str(nav_group.pattern) + str(pattern.pattern)
if str(context.path).startswith(url):
is_active = True
if str(context.path).startswith('/' + str(nav_group.pattern)):
group_active = True
if (
pattern.pattern.name is not None
and
not str(pattern.pattern.name).startswith('_')
):
name = str(pattern.name)
if hasattr(pattern.callback.view_class, 'permission_required'):
permissions_required = pattern.callback.view_class.permission_required
user_has_perm = False
if type(permissions_required) is list:
user_has_perm = context.user.has_perms(permissions_required)
else:
user_has_perm = context.user.has_perm(permissions_required)
if hasattr(pattern.callback.view_class, 'model'):
if pattern.callback.view_class.model is Organization and context.user.is_authenticated:
organizations = Organization.objects.filter(manager = context.user)
if len(organizations) > 0:
user_has_perm = True
if str(nav_group.app_name).lower() == 'settings':
user_has_perm = True
if context.user.is_superuser:
user_has_perm = True
if user_has_perm:
nav_items = nav_items + [ {
'name': name,
'url': url,
'is_active': is_active
} ]
if len(nav_items) > 0:
dnav = dnav + [{
'name': group_name,
'urls': nav_items,
'is_active': group_active
}]
return dnav
def common(context):
return {
'build_details': build_details(context),
'nav_items': nav_items(context),
'social_backends': social_backends(context),
'user_settings': user_settings(context),
'user_default_organization': user_default_organization(context)
}

View File

@ -0,0 +1,33 @@
def merge_software(software: list, new_software: list) -> list:
""" Merge two lists of software actions
Args:
software (list(dict)): Original list to merge over
new_software (list(dict)): new list to use to merge over
Returns:
list(dict): merged list of software actions
"""
merge_software = []
merge: dict = {}
for original in software:
merge.update({
original['name']: original
})
for new in new_software:
merge.update({
new['name']: new
})
for key, value in merge.items():
merge_software = merge_software + [ value ]
return merge_software

View File

@ -0,0 +1,38 @@
import zoneinfo
from django.utils import timezone
class TimezoneMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
def _activate_tz(tz):
timezone.activate(zoneinfo.ZoneInfo(tz))
# tzname = request.session.get("django_timezone", None)
# if tzname:
# _activate_tz(tzname)
# else:
user = request.user
if hasattr(user, 'user_settings'):
tzname = user.user_settings.all()[0].timezone
# set the cookie
# request.session['django_timezone'] = tzname
_activate_tz(tzname)
else:
timezone.deactivate()
return self.get_response(request)

View File

@ -0,0 +1,69 @@
from django.contrib.auth.models import ContentType
from rest_framework import serializers
from rest_framework.reverse import reverse
class ContentTypeBaseSerializer(serializers.ModelSerializer):
display_name = serializers.SerializerMethodField('get_display_name')
def get_display_name(self, item) -> str:
return str( item )
url = serializers.HyperlinkedIdentityField(
view_name="v2:_api_v2_content_type-detail", format="html"
)
class Meta:
model = ContentType
fields = '__all__'
fields = [
'id',
'display_name',
'url'
]
read_only_fields = [
'id',
'display_name',
'url'
]
class ContentTypeViewSerializer(ContentTypeBaseSerializer):
_urls = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> dict:
return {
'_self': reverse("v2:_api_v2_content_type-detail", request=self._context['view'].request, kwargs={'pk': item.pk}),
}
class Meta:
model = ContentType
fields = [
'id',
'app_label',
'model',
'_urls',
]
read_only_fields = [
'id',
'app_label',
'model',
'_urls',
]

View File

@ -0,0 +1,77 @@
from django.contrib.auth.models import Permission
from rest_framework import serializers
from rest_framework.reverse import reverse
from centurion.serializers.content_type import ContentTypeBaseSerializer
class PermissionBaseSerializer(serializers.ModelSerializer):
display_name = serializers.SerializerMethodField('get_display_name')
def get_display_name(self, item) -> str:
return str( item )
url = serializers.HyperlinkedIdentityField(
view_name="v2:_api_v2_permission-detail", format="html"
)
class Meta:
model = Permission
fields = '__all__'
fields = [
'id',
'display_name',
'url'
]
read_only_fields = [
'id',
'display_name',
'url'
]
class PermissionViewSerializer(PermissionBaseSerializer):
content_type = ContentTypeBaseSerializer()
_urls = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> dict:
return {
'_self': reverse("v2:_api_v2_permission-detail", request=self._context['view'].request, kwargs={'pk': item.pk}),
}
class Meta:
model = Permission
fields = [
'id',
'name',
'display_name',
'codename',
'content_type',
'_urls',
]
read_only_fields = [
'id',
'name',
'display_name',
'codename',
'content_type',
'_urls',
]

View File

@ -0,0 +1,46 @@
import django
from rest_framework import serializers
User = django.contrib.auth.get_user_model()
class UserBaseSerializer(serializers.ModelSerializer):
display_name = serializers.SerializerMethodField('get_display_name')
def get_display_name(self, item) -> str:
return str( item )
url = serializers.HyperlinkedIdentityField(
view_name="v2:_api_v2_user-detail", format="html"
)
class Meta:
model = User
fields = '__all__'
fields = [
'id',
'display_name',
'first_name',
'last_name',
'username',
'is_active',
'url'
]
read_only_fields = [
'id',
'display_name',
'first_name',
'last_name',
'username',
'is_active',
'url'
]

702
app/centurion/settings.py Normal file
View File

@ -0,0 +1,702 @@
"""
Django settings for itsm project.
Generated by 'django-admin startproject' using Django 5.0.4.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""
import hashlib
import os
import sys
from pathlib import Path
from split_settings.tools import optional, include
import django.db.models.options as options
options.DEFAULT_NAMES = (*options.DEFAULT_NAMES, 'sub_model_type', 'itam_sub_model_type')
AUTH_USER_MODEL = 'auth.User'
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
SETTINGS_DIR = '/etc/itsm' # Primary Settings Directory
BUILD_REPO = os.getenv('CI_PROJECT_URL')
BUILD_SHA = os.getenv('CI_COMMIT_SHA')
BUILD_VERSION = os.getenv('CI_COMMIT_TAG')
DOCS_ROOT = 'https://nofusscomputing.com/projects/centurion_erp/user/'
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
# Celery settings
CELERY_ACCEPT_CONTENT = ['json']
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True # broker_connection_retry_on_startup
CELERY_BROKER_URL = 'amqp://admin:admin@127.0.0.1:5672/itsm'
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#broker-use-ssl
# import ssl
# broker_use_ssl = {
# 'keyfile': '/var/ssl/private/worker-key.pem',
# 'certfile': '/var/ssl/amqp-server-cert.pem',
# 'ca_certs': '/var/ssl/myca.pem',
# 'cert_reqs': ssl.CERT_REQUIRED
# }
CELERY_BROKER_POOL_LIMIT = 3 # broker_pool_limit
CELERY_CACHE_BACKEND = 'django-cache'
CELERY_ENABLE_UTC = True
CELERY_RESULT_BACKEND = 'django-db'
CELERY_RESULT_EXTENDED = True
CELERY_TASK_SERIALIZER = 'json'
CELERY_TIMEZONE = 'UTC'
CELERY_TASK_DEFAULT_EXCHANGE = 'ITSM' # task_default_exchange
CELERY_TASK_DEFAULT_PRIORITY = 10 # 1-10=LOW-HIGH task_default_priority
# CELERY_TASK_DEFAULT_QUEUE = 'background'
CELERY_TASK_TIME_LIMIT = 3600 # task_time_limit
CELERY_TASK_TRACK_STARTED = True # task_track_started
# dont set concurrency for docer as it defaults to CPU count
CELERY_WORKER_CONCURRENCY = 2 # worker_concurrency - Default: Number of CPU cores
CELERY_WORKER_DEDUPLICATE_SUCCESSFUL_TASKS = True # worker_deduplicate_successful_tasks
CELERY_WORKER_MAX_TASKS_PER_CHILD = 1 # worker_max_tasks_per_child
# CELERY_WORKER_MAX_MEMORY_PER_CHILD = 10000 # 10000=10mb worker_max_memory_per_child - Default: No limit. Type: int (kilobytes)
CELERY_TASK_SEND_SENT_EVENT = True
CELERY_WORKER_SEND_TASK_EVENTS = True # worker_send_task_events
FEATURE_FLAGGING_ENABLED = True # Turn Feature Flagging on/off
FEATURE_FLAG_OVERRIDES = None # Feature Flags to override fetched feature flags
# PROMETHEUS_METRICS_EXPORT_PORT_RANGE = range(8010, 8010)
# 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
RUNNING_TESTS = 'test' in str(sys.argv)
# django setting.
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
'LOCATION': 'my_cache_table',
}
}
#
# Defaults
#
ALLOWED_HOSTS = [ '*' ] # Site host to serve
DEBUG = False # SECURITY WARNING: don't run with debug turned on in production!
SITE_URL = 'http://127.0.0.1' # domain with HTTP method for the sites URL
SECRET_KEY = None # You need to generate this
SESSION_COOKIE_AGE = 1209600 # Age the session cookie should live for in seconds.
SSO_ENABLED = False # Enable SSO
SSO_LOGIN_ONLY_BACKEND = None # Use specified SSO backend as the ONLY method to login. (builting login form will not be used)
TRUSTED_ORIGINS = [] # list of trusted domains for CSRF
# Application definition
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 86400
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # ToDo: https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header
# SECURE_SSL_REDIRECT = True # Commented out so tests pass
# SECURE_SSL_HOST = # ToDo: https://docs.djangoproject.com/en/dev/ref/settings/#secure-ssl-host
SESSION_COOKIE_SECURE = True
# USE_X_FORWARDED_HOST = True # ToDo: https://docs.djangoproject.com/en/dev/ref/settings/#use-x-forwarded-host
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'corsheaders',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework_json_api',
'django_filters',
'social_django',
'django_celery_results',
'core.apps.CoreConfig',
'access.apps.AccessConfig',
'itam.apps.ItamConfig',
'itim.apps.ItimConfig',
'assistance.apps.AssistanceConfig',
'settings.apps.SettingsConfig',
'drf_spectacular',
'drf_spectacular_sidecar',
'config_management.apps.ConfigManagementConfig',
'project_management.apps.ProjectManagementConfig',
'devops.apps.DevOpsConfig',
'centurion_feature_flag.apps.CenturionFeatureFlagConfig',
'human_resources.apps.HumanResourcesConfig',
'itops.apps.ItOpsConfig',
'accounting.apps.AccountingConfig',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.middleware.common.CommonMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'access.middleware.request.RequestTenancy',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'core.middleware.get_request.RequestMiddleware',
'centurion.middleware.timezone.TimezoneMiddleware',
'centurion_feature_flag.middleware.feature_flag.FeatureFlagMiddleware',
]
ROOT_URLCONF = 'centurion.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / "templates"],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'social_django.context_processors.backends',
'social_django.context_processors.login_redirect',
'centurion.context_processors.base.common',
],
},
},
]
WSGI_APPLICATION = 'centurion.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': str(BASE_DIR / 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
LOGIN_REDIRECT_URL = "http://127.0.0.1:3000"
LOGOUT_REDIRECT_URL = "login"
LOGIN_URL = '/account/login'
LOGIN_REQUIRED = True
# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
STATIC_URL = 'static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATICFILES_DIRS = [
BASE_DIR / "project-static",
]
# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
SITE_TITLE = "Centurion ERP"
API_ENABLED = True
if API_ENABLED:
INSTALLED_APPS += [
'api.apps.ApiConfig',
]
REST_FRAMEWORK = {
'PAGE_SIZE': 10,
'EXCEPTION_HANDLER': 'rest_framework_json_api.exceptions.exception_handler',
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': [
'api.auth.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PAGINATION_CLASS':
'rest_framework_json_api.pagination.JsonApiPageNumberPagination',
# leaving these uncommented, even though are the default renderers
# causes the api to require inputs the fields under an 'attributes' key
# 'DEFAULT_PARSER_CLASSES': (
# 'rest_framework_json_api.parsers.JSONParser',
# 'rest_framework.parsers.FormParser',
# 'rest_framework.parsers.MultiPartParser'
# ),
# leaving these uncommented, even though are the default renderers
# causes the api to output the fields under a 'attributes' key
# 'DEFAULT_RENDERER_CLASSES': (
# 'rest_framework_json_api.renderers.JSONRenderer',
# 'rest_framework_json_api.renderers.BrowsableAPIRenderer',
# ),
'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata',
'DEFAULT_FILTER_BACKENDS': (
# 'rest_framework_json_api.filters.QueryParameterValidationFilter',
'rest_framework.filters.SearchFilter',
'rest_framework_json_api.django_filters.DjangoFilterBackend',
'rest_framework_json_api.filters.OrderingFilter',
),
# 'SEARCH_PARAM': 'filter[search]',
# 'TEST_REQUEST_RENDERER_CLASSES': (
# 'rest_framework_json_api.renderers.JSONRenderer',
# ),
# 'TEST_REQUEST_DEFAULT_FORMAT': 'vnd.api+json'
'TEST_REQUEST_DEFAULT_FORMAT': 'json',
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning',
'DEFAULT_VERSION': 'v1',
'ALLOWED_VERSIONS': [
'v1',
'v2'
]
}
SPECTACULAR_SETTINGS = {
'TITLE': 'Centurion ERP API',
'DESCRIPTION': """This UI exists to server the purpose of being the API documentation.
Centurion ERP's API is versioned, with [v1 Depreciated](/api/swagger) and [v2 as the current](/api/v2/docs).
For CRUD actions `Add`, `update` and `replace` the serializer that returns is the Models `View` serializer.
**Note:** _API v2 is currently in beta phase. AS such is subject to change. When the new UI ius released, API v2 will move to stable._
## Authentication
Access to the API is restricted and requires authentication. Available authentication methods are:
- Session
- Token
Session authentication is made available after logging into the application via the login interface.
Token authentication is via an API token that a user will generate within their
[settings panel](https://nofusscomputing.com/projects/django-template/user/user_settings/#api-tokens).
## Examples
curl:
- Simple API Request: `curl -X GET <url>/api/ -H 'Authorization: Token <token>'`
- Post an Inventory File:
``` bash
curl --header "Content-Type: application/json" \\
--header "Authorization: Token <token>" \\
--request POST \\
--data @<path to inventory file>/<file name>.json \\
<url>/api/device/inventory
```
""",
'VERSION': '',
'SCHEMA_PATH_PREFIX': '/api/v2/|/api/',
'SERVE_INCLUDE_SCHEMA': False,
'SWAGGER_UI_DIST': 'SIDECAR',
'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
"SWAGGER_UI_SETTINGS": '''{
filter: true,
defaultModelsExpandDepth: -1,
deepLinking: true,
}''',
'REDOC_DIST': 'SIDECAR',
'PREPROCESSING_HOOKS': [
'drf_spectacular.hooks.preprocess_exclude_path_format'
],
}
DATETIME_FORMAT = 'j N Y H:i:s'
#
# Settings for unit tests
#
if RUNNING_TESTS:
SECRET_KEY = 'django-insecure-tests_are_being_run'
#
# Load user settings files
#
if os.path.isdir(SETTINGS_DIR):
settings_files = os.path.join(SETTINGS_DIR, '*.py')
include(optional(settings_files))
#
# Settings to reset to prevent user from over-riding
#
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
)
CSRF_TRUSTED_ORIGINS = [
SITE_URL,
*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') and not RUNNING_TESTS:
if not os.path.exists(os.path.join(BASE_DIR, 'log')): # Create log dir
os.makedirs(os.path.join(BASE_DIR, 'log'))
if DEBUG:
INSTALLED_APPS += [
'debug_toolbar',
]
MIDDLEWARE += [
'debug_toolbar.middleware.DebugToolbarMiddleware',
]
INTERNAL_IPS = [
"127.0.0.1",
]
if not RUNNING_TESTS:
# Setup Logging
LOGGING = CENTURION_LOGGING
if METRICS_ENABLED:
INSTALLED_APPS += [ 'django_prometheus', ]
MIDDLEWARE = [
'django_prometheus.middleware.PrometheusBeforeMiddleware'
] + MIDDLEWARE + [
'django_prometheus.middleware.PrometheusAfterMiddleware',
]
if DATABASES['default']['ENGINE'] == 'django.db.backends.sqlite3':
DATABASES['default']['ENGINE'] = 'django_prometheus.db.backends.sqlite3',
if SSO_ENABLED:
if SSO_LOGIN_ONLY_BACKEND:
LOGIN_URL = f'/sso/login/{SSO_LOGIN_ONLY_BACKEND}/'
AUTHENTICATION_BACKENDS += (
*SSO_BACKENDS,
)
SOCIAL_AUTH_PIPELINE = (
'social_core.pipeline.social_auth.social_details',
'social_core.pipeline.social_auth.social_uid',
'social_core.pipeline.social_auth.social_user',
'social_core.pipeline.user.get_username',
'social_core.pipeline.social_auth.associate_by_email',
'social_core.pipeline.user.create_user',
'social_core.pipeline.social_auth.associate_user',
'social_core.pipeline.social_auth.load_extra_data',
'social_core.pipeline.user.user_details',
)
if BUILD_VERSION:
feature_flag_version = str(BUILD_VERSION) + '+' + str(BUILD_SHA)[:8]
else:
if BUILD_SHA is not None:
feature_flag_version = str(BUILD_SHA)
else:
feature_flag_version = 'development'
""" Unique ID Rational
Unique ID generation required to determine how many installations are deployed. Also provides the opportunity
should it be required in the future to enable feature flags on a per `unique_id`.
Objects:
- CELERY_BROKER_URL
- SITE_URL
- SECRET_KEY
Will provide enough information alone once hashed, to identify a majority of deployments as unique.
Adding object `feature_flag_version`, Ensures that as each release occurs that a deployments `unique_id` will
change, thus preventing long term monitoring of a deployments usage of Centurion.
value `DOCS_ROOT` is added so there is more data to hash.
You are advised not to change the `unique_id` as you may inadvertantly reduce your privacy. However the choice
is yours. If you do change the value ensure that it's still hashed as a sha256 hash.
"""
unique_id = str(f'{CELERY_BROKER_URL}{DOCS_ROOT}{SITE_URL}{SECRET_KEY}{feature_flag_version}')
unique_id = hashlib.sha256(unique_id.encode()).hexdigest()
if FEATURE_FLAGGING_ENABLED:
FEATURE_FLAGGING_URL = 'https://alfred.nofusscomputing.com/api/v2/public/4/flags/1'
if DEBUG:
FEATURE_FLAGGING_URL = 'http://127.0.0.1:8002/api/v2/public/1/flags/2844'
feature_flag = {
'url': str(FEATURE_FLAGGING_URL),
'user_agent': 'Centurion ERP',
'cache_dir': str(BASE_DIR) + '/',
'disable_downloading': False,
'unique_id': unique_id,
'version': feature_flag_version,
}
if FEATURE_FLAG_OVERRIDES:
feature_flag.update({
'over_rides': FEATURE_FLAG_OVERRIDES
})
if DEBUG or RUNNING_TESTS:
feature_flag.update({ 'disable_downloading': True, })
debug_feature_flags = [
{
"2025-00001": {
"name": "DevOps/Git Repositories",
"description": "Disables Git Repositories and Git Groups. see https://github.com/nofusscomputing/centurion_erp/issues/515",
"enabled": True,
"created": "",
"modified": ""
}
},
{
"2025-00002": {
"name": "Entities",
"description": "Entities see https://github.com/nofusscomputing/centurion_erp/issues/704",
"enabled": True,
"created": "",
"modified": ""
}
},
{
"2025-00003": {
"name": "Role Based Access Control (RBAC)",
"description": "Refactor of authentication and authorization to be RBAC based. see https://github.com/nofusscomputing/centurion_erp/issues/551",
"enabled": True,
"created": "",
"modified": ""
}
},
{
"2025-00004": {
"name": "Accounting Module",
"description": "Accounting related functions. see https://github.com/nofusscomputing/centurion_erp/issues/88",
"enabled": True,
"created": "",
"modified": ""
}
},
{
"2025-00005": {
"name": "Human Resources/Employee",
"description": "Employee Model. see https://github.com/nofusscomputing/centurion_erp/issues/92",
"enabled": True,
"created": "",
"modified": ""
}
},
{
"2025-00006": {
"name": "Ticket Models",
"description": "Ticket Model re-write. see https://github.com/nofusscomputing/centurion_erp/issues/564",
"enabled": True,
"created": "",
"modified": ""
}
},
{
"2025-00007": {
"name": "itam.ITAMAssetBase",
"description": "ITAM Asset Base model. see https://github.com/nofusscomputing/centurion_erp/issues/692",
"enabled": True,
"created": "",
"modified": ""
}
},
{
"2025-00008": {
"name": "access.Company",
"description": "Company Entity Role. See https://github.com/nofusscomputing/centurion_erp/issues/704",
"enabled": True,
"created": "",
"modified": ""
}
}
]
feature_flag.update({
'over_rides': debug_feature_flags
})

View File

View File

View File

@ -0,0 +1,53 @@
import django
from access.middleware.request import Tenancy
from access.models.tenant import Tenant as Organization
from settings.models.app_settings import AppSettings
User = django.contrib.auth.get_user_model()
class MockView:
action: str = None
app_settings: AppSettings = None
kwargs: dict = {}
request = None
def __init__(self, user: User, model = None):
app_settings = AppSettings.objects.select_related('global_organization').get(
owner_organization = None
)
if model is not None:
self.model = model
self.request = MockRequest( user = user, app_settings = app_settings)
class MockRequest:
tenancy: Tenancy = None
user = None
def __init__(self, user: User, app_settings):
self.user = user
self.app_settings = app_settings
self.tenancy = Tenancy(
user = user,
app_settings = app_settings
)

View File

@ -0,0 +1,487 @@
import pytest
import unittest
from django.test import Client
from django.shortcuts import reverse
class ModelPermissionsView:
""" Tests for checking model view permissions """
app_namespace: str = None
url_name_view: str
url_view_kwargs: dict = None
def test_model_view_user_anon_denied(self):
""" Check correct permission for view
Attempt to view as anon user
"""
client = Client()
if self.app_namespace:
url = reverse(self.app_namespace + ':' + self.url_name_view, kwargs=self.url_view_kwargs)
else:
url = reverse(self.url_name_view, kwargs=self.url_view_kwargs)
response = client.get(url)
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_model_view_no_permission_denied(self):
""" Check correct permission for view
Attempt to view with user missing permission
"""
client = Client()
if self.app_namespace:
url = reverse(self.app_namespace + ':' + self.url_name_view, kwargs=self.url_view_kwargs)
else:
url = reverse(self.url_name_view, kwargs=self.url_view_kwargs)
client.force_login(self.no_permissions_user)
response = client.get(url)
assert response.status_code == 403
def test_model_view_different_organizaiton_denied(self):
""" Check correct permission for view
Attempt to view with user from different organization
"""
client = Client()
if self.app_namespace:
url = reverse(self.app_namespace + ':' + self.url_name_view, kwargs=self.url_view_kwargs)
else:
url = reverse(self.url_name_view, kwargs=self.url_view_kwargs)
client.force_login(self.different_organization_user)
response = client.get(url)
assert response.status_code == 403
def test_model_view_has_permission(self):
""" Check correct permission for view
Attempt to view as user with view permission
"""
client = Client()
if self.app_namespace:
url = reverse(self.app_namespace + ':' + self.url_name_view, kwargs=self.url_view_kwargs)
else:
url = reverse(self.url_name_view, kwargs=self.url_view_kwargs)
client.force_login(self.view_user)
response = client.get(url)
assert response.status_code == 200
class ModelPermissionsAdd:
""" Tests for checking model Add permissions """
app_namespace: str = None
url_name_add: str
url_add_kwargs: dict = None
add_data: dict = None
@pytest.mark.skip(reason="ToDO: write test")
def test_model_requires_attribute_parent_model(self):
""" Child model requires 'django view' attribute 'parent_model'
When a child-model is added the parent model is required so that the organization can be detrmined.
"""
pass
def test_model_add_user_anon_denied(self):
""" Check correct permission for add
Attempt to add as anon user
"""
client = Client()
if self.app_namespace:
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
else:
url = reverse(self.url_name_add, kwargs=self.url_add_kwargs)
response = client.put(url, data=self.add_data)
assert response.status_code == 302 and response.url.startswith('/account/login')
# @pytest.mark.skip(reason="ToDO: figure out why fails")
def test_model_add_no_permission_denied(self):
""" Check correct permission for add
Attempt to add as user with no permissions
"""
client = Client()
if self.app_namespace:
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
else:
url = reverse(self.url_name_add, kwargs=self.url_add_kwargs)
client.force_login(self.no_permissions_user)
response = client.post(url, data=self.add_data)
assert response.status_code == 403
# @pytest.mark.skip(reason="ToDO: figure out why fails")
def test_model_add_different_organization_denied(self):
""" Check correct permission for add
attempt to add as user from different organization
"""
client = Client()
if self.app_namespace:
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
else:
url = reverse(self.url_name_add, kwargs=self.url_add_kwargs)
client.force_login(self.different_organization_user)
response = client.post(url, data=self.add_data)
assert response.status_code == 403
def test_model_add_permission_view_denied(self):
""" Check correct permission for add
Attempt to add a user with view permission
"""
client = Client()
if self.app_namespace:
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
else:
url = reverse(self.url_name_add, kwargs=self.url_add_kwargs)
client.force_login(self.view_user)
response = client.post(url, data=self.add_data)
assert response.status_code == 403
def test_model_add_has_permission(self):
""" Check correct permission for add
Attempt to add as user with permission
"""
client = Client()
if self.app_namespace:
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
else:
url = reverse(self.url_name_add, kwargs=self.url_add_kwargs)
client.force_login(self.add_user)
response = client.post(url, data=self.add_data)
assert response.status_code == 200
class ModelPermissionsChange:
""" Tests for checking model change permissions """
app_namespace: str = None
url_name_change: str
url_change_kwargs: dict = None
change_data: dict = None
def test_model_change_user_anon_denied(self):
""" Check correct permission for change
Attempt to change as anon
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_change, kwargs=self.url_change_kwargs)
response = client.patch(url, data=self.change_data)
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_model_change_no_permission_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user without permissions
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_change, kwargs=self.url_change_kwargs)
client.force_login(self.no_permissions_user)
response = client.post(url, data=self.change_data)
assert response.status_code == 403
def test_model_change_different_organization_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user from different organization
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_change, kwargs=self.url_change_kwargs)
client.force_login(self.different_organization_user)
response = client.post(url, data=self.change_data)
assert response.status_code == 403
def test_model_change_permission_view_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with view permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_change, kwargs=self.url_change_kwargs)
client.force_login(self.view_user)
response = client.post(url, data=self.change_data)
assert response.status_code == 403
def test_model_change_permission_add_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with add permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_change, kwargs=self.url_change_kwargs)
client.force_login(self.add_user)
response = client.post(url, data=self.change_data)
assert response.status_code == 403
def test_model_change_has_permission(self):
""" Check correct permission for change
Make change with user who has change permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_change, kwargs=self.url_change_kwargs)
client.force_login(self.change_user)
response = client.post(url, data=self.change_data)
assert response.status_code == 200
class ModelPermissionsDelete:
""" Tests for checking model delete permissions """
app_namespace: str = None
url_name_delete: str
url_delete_kwargs: dict = None
url_delete_response: str
delete_data: dict = None
def test_model_delete_user_anon_denied(self):
""" Check correct permission for delete
Attempt to delete item as anon user
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_delete, kwargs=self.url_delete_kwargs)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_model_delete_no_permission_denied(self):
""" Check correct permission for delete
Attempt to delete as user with no permissons
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_delete, kwargs=self.url_delete_kwargs)
client.force_login(self.no_permissions_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_model_delete_different_organization_denied(self):
""" Check correct permission for delete
Attempt to delete as user from different organization
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_delete, kwargs=self.url_delete_kwargs)
client.force_login(self.different_organization_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_model_delete_permission_view_denied(self):
""" Check correct permission for delete
Attempt to delete as user with veiw permission only
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_delete, kwargs=self.url_delete_kwargs)
client.force_login(self.view_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_model_delete_permission_add_denied(self):
""" Check correct permission for delete
Attempt to delete as user with add permission only
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_delete, kwargs=self.url_delete_kwargs)
client.force_login(self.add_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_model_delete_permission_change_denied(self):
""" Check correct permission for delete
Attempt to delete as user with change permission only
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_delete, kwargs=self.url_delete_kwargs)
client.force_login(self.change_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_model_delete_has_permission(self):
""" Check correct permission for delete
Delete item as user with delete permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_delete, kwargs=self.url_delete_kwargs)
client.force_login(self.delete_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 302 and response.url == self.url_delete_response
class ModelPermissions(
ModelPermissionsView,
ModelPermissionsAdd,
ModelPermissionsChange,
ModelPermissionsDelete
):
""" Tests for checking model permissions """
app_namespace: str = None

View File

@ -0,0 +1,62 @@
from centurion.tests.abstract.views import AddView, ChangeView, DeleteView, DisplayView, IndexView
class ModelAdd(
AddView
):
""" Unit Tests for Model Add """
class ModelChange(
ChangeView
):
""" Unit Tests for Model Change """
class ModelDelete(
DeleteView
):
""" Unit Tests for Model delete """
class ModelDisplay(
DisplayView
):
""" Unit Tests for Model display """
class ModelIndex(
IndexView
):
""" Unit Tests for Model index """
class ModelCommon(
ModelAdd,
ModelChange,
ModelDelete,
ModelDisplay
):
""" Unit Tests for all models """
class PrimaryModel(
ModelCommon,
ModelIndex
):
""" Tests for Primary Models
A Primary model is a model that is deemed a model that has the following views:
- Add
- Change
- Delete
- Display
- Index
"""

View File

@ -0,0 +1,626 @@
import inspect
import pytest
import unittest
class AddView:
""" Testing of Display view """
add_module: str = None
""" Full module path to test """
add_view: str = None
""" View Class name to test """
def test_view_add_attribute_not_exists_fields(self):
""" Attribute does not exists test
Ensure that `fields` attribute is not defined as the expectation is that a form will be used.
"""
module = __import__(self.add_module, fromlist=[self.add_view])
assert hasattr(module, self.add_view)
viewclass = getattr(module, self.add_view)
assert viewclass.fields is None
def test_view_add_attribute_exists_form_class(self):
""" Attribute exists test
Ensure that `form_class` attribute is defined as it's required.
"""
module = __import__(self.add_module, fromlist=[self.add_view])
assert hasattr(module, self.add_view)
viewclass = getattr(module, self.add_view)
assert hasattr(viewclass, 'form_class')
def test_view_add_attribute_type_form_class(self):
""" Attribute Type Test
Ensure that `form_class` attribute is a class.
"""
module = __import__(self.add_module, fromlist=[self.add_view])
assert hasattr(module, self.add_view)
viewclass = getattr(module, self.add_view)
assert inspect.isclass(viewclass.form_class)
def test_view_add_attribute_exists_model(self):
""" Attribute exists test
Ensure that `model` attribute is defined as it's required .
"""
module = __import__(self.add_module, fromlist=[self.add_view])
assert hasattr(module, self.add_view)
viewclass = getattr(module, self.add_view)
assert hasattr(viewclass, 'model')
def test_view_add_attribute_exists_permission_required(self):
""" Attribute exists test
Ensure that `permission_required` attribute is defined as it's required.
"""
module = __import__(self.add_module, fromlist=[self.add_view])
assert hasattr(module, self.add_view)
viewclass = getattr(module, self.add_view)
assert hasattr(viewclass, 'permission_required')
def test_view_add_attribute_type_permission_required(self):
""" Attribute Type Test
Ensure that `permission_required` attribute is a list
"""
module = __import__(self.add_module, fromlist=[self.add_view])
assert hasattr(module, self.add_view)
viewclass = getattr(module, self.add_view)
assert type(viewclass.permission_required) is list
def test_view_add_attribute_exists_template_name(self):
""" Attribute exists test
Ensure that `template_name` attribute is defined as it's required.
"""
module = __import__(self.add_module, fromlist=[self.add_view])
assert hasattr(module, self.add_view)
viewclass = getattr(module, self.add_view)
assert hasattr(viewclass, 'template_name')
def test_view_add_attribute_type_template_name(self):
""" Attribute Type Test
Ensure that `template_name` attribute is a string.
"""
module = __import__(self.add_module, fromlist=[self.add_view])
assert hasattr(module, self.add_view)
viewclass = getattr(module, self.add_view)
assert type(viewclass.template_name) is str
def test_view_add_function_get_initial_exists(self):
"""Ensure that get_initial exists
Field `get_initial` must be defined as the base class is used for setup.
"""
module = __import__(self.add_module, fromlist=[self.add_view])
view_class = getattr(module, 'Add')
assert hasattr(view_class, 'get_initial')
def test_view_add_function_get_initial_callable(self):
"""Ensure that get_initial is a function
Field `get_initial` must be callable as it's used for setup.
"""
module = __import__(self.add_module, fromlist=[self.add_view])
view_class = getattr(module, 'Add')
func = getattr(view_class, 'get_initial')
assert callable(func)
class ChangeView:
""" Testing of Display view """
change_module: str = None
""" Full module path to test """
change_view: str = None
""" Change Class name to test """
def test_view_change_attribute_not_exists_fields(self):
""" Attribute does not exists test
Ensure that `fields` attribute is not defined as the expectation is that a form will be used.
"""
module = __import__(self.change_module, fromlist=[self.change_view])
assert hasattr(module, self.change_view)
viewclass = getattr(module, self.change_view)
assert viewclass.fields is None
def test_view_change_attribute_exists_form_class(self):
""" Attribute exists test
Ensure that `form_class` attribute is defined as it's required.
"""
module = __import__(self.change_module, fromlist=[self.change_view])
assert hasattr(module, self.change_view)
viewclass = getattr(module, self.change_view)
assert hasattr(viewclass, 'form_class')
def test_view_change_attribute_type_form_class(self):
""" Attribute Type Test
Ensure that `form_class` attribute is a string.
"""
module = __import__(self.change_module, fromlist=[self.change_view])
assert hasattr(module, self.change_view)
viewclass = getattr(module, self.change_view)
assert inspect.isclass(viewclass.form_class)
def test_view_change_attribute_exists_model(self):
""" Attribute exists test
Ensure that `model` attribute is defined as it's required .
"""
module = __import__(self.change_module, fromlist=[self.change_view])
assert hasattr(module, self.change_view)
viewclass = getattr(module, self.change_view)
assert hasattr(viewclass, 'model')
def test_view_change_attribute_exists_permission_required(self):
""" Attribute exists test
Ensure that `permission_required` attribute is defined as it's required.
"""
module = __import__(self.change_module, fromlist=[self.change_view])
assert hasattr(module, self.change_view)
viewclass = getattr(module, self.change_view)
assert hasattr(viewclass, 'permission_required')
def test_view_change_attribute_type_permission_required(self):
""" Attribute Type Test
Ensure that `permission_required` attribute is a list
"""
module = __import__(self.change_module, fromlist=[self.change_view])
assert hasattr(module, self.change_view)
viewclass = getattr(module, self.change_view)
assert type(viewclass.permission_required) is list
def test_view_change_attribute_exists_template_name(self):
""" Attribute exists test
Ensure that `template_name` attribute is defined as it's required.
"""
module = __import__(self.change_module, fromlist=[self.change_view])
assert hasattr(module, self.change_view)
viewclass = getattr(module, self.change_view)
assert hasattr(viewclass, 'template_name')
def test_view_change_attribute_type_template_name(self):
""" Attribute Type Test
Ensure that `template_name` attribute is a string.
"""
module = __import__(self.change_module, fromlist=[self.change_view])
assert hasattr(module, self.change_view)
viewclass = getattr(module, self.change_view)
assert type(viewclass.template_name) is str
class DeleteView:
""" Testing of Display view """
delete_module: str = None
""" Full module path to test """
delete_view: str = None
""" Delete Class name to test """
def test_view_delete_attribute_exists_model(self):
""" Attribute exists test
Ensure that `model` attribute is defined as it's required .
"""
module = __import__(self.delete_module, fromlist=[self.delete_view])
assert hasattr(module, self.delete_view)
viewclass = getattr(module, self.delete_view)
assert hasattr(viewclass, 'model')
def test_view_delete_attribute_exists_permission_required(self):
""" Attribute exists test
Ensure that `model` attribute is defined as it's required .
"""
module = __import__(self.delete_module, fromlist=[self.delete_view])
assert hasattr(module, self.delete_view)
viewclass = getattr(module, self.delete_view)
assert hasattr(viewclass, 'permission_required')
def test_view_delete_attribute_type_permission_required(self):
""" Attribute Type Test
Ensure that `permission_required` attribute is a list
"""
module = __import__(self.delete_module, fromlist=[self.delete_view])
assert hasattr(module, self.delete_view)
viewclass = getattr(module, self.delete_view)
assert type(viewclass.permission_required) is list
def test_view_delete_attribute_exists_template_name(self):
""" Attribute exists test
Ensure that `template_name` attribute is defined as it's required.
"""
module = __import__(self.delete_module, fromlist=[self.delete_view])
assert hasattr(module, self.delete_view)
viewclass = getattr(module, self.delete_view)
assert hasattr(viewclass, 'template_name')
def test_view_delete_attribute_type_template_name(self):
""" Attribute Type Test
Ensure that `template_name` attribute is a string.
"""
module = __import__(self.delete_module, fromlist=[self.delete_view])
assert hasattr(module, self.delete_view)
viewclass = getattr(module, self.delete_view)
assert type(viewclass.template_name) is str
class DisplayView:
""" Testing of Display view """
display_module: str = None
""" Full module path to test """
display_view: str = None
""" Change Class name to test """
def test_view_display_attribute_exists_model(self):
""" Attribute exists test
Ensure that `model` attribute is defined as it's required .
"""
module = __import__(self.display_module, fromlist=[self.display_view])
assert hasattr(module, self.display_view)
viewclass = getattr(module, self.display_view)
assert hasattr(viewclass, 'model')
def test_view_display_attribute_exists_permission_required(self):
""" Attribute exists test
Ensure that `permission_required` attribute is defined as it's required.
"""
module = __import__(self.display_module, fromlist=[self.display_view])
assert hasattr(module, self.display_view)
viewclass = getattr(module, self.display_view)
assert hasattr(viewclass, 'permission_required')
def test_view_display_attribute_type_permission_required(self):
""" Attribute Type Test
Ensure that `permission_required` attribute is a list
"""
module = __import__(self.display_module, fromlist=[self.display_view])
assert hasattr(module, self.display_view)
viewclass = getattr(module, self.display_view)
assert type(viewclass.permission_required) is list
def test_view_display_attribute_exists_template_name(self):
""" Attribute exists test
Ensure that `template_name` attribute is defined as it's required.
"""
module = __import__(self.display_module, fromlist=[self.display_view])
assert hasattr(module, self.display_view)
viewclass = getattr(module, self.display_view)
assert hasattr(viewclass, 'template_name')
def test_view_display_attribute_type_template_name(self):
""" Attribute Type Test
Ensure that `template_name` attribute is a string.
"""
module = __import__(self.display_module, fromlist=[self.display_view])
assert hasattr(module, self.display_view)
viewclass = getattr(module, self.display_view)
assert type(viewclass.template_name) is str
class IndexView:
""" Testing of Display view """
index_module: str = None
""" Full module path to test """
index_view: str = None
""" Index Class name to test """
def test_view_index_attribute_exists_model(self):
""" Attribute exists test
Ensure that `model` attribute is defined as it's required .
"""
module = __import__(self.index_module, fromlist=[self.index_view])
assert hasattr(module, self.index_view)
viewclass = getattr(module, self.index_view)
assert hasattr(viewclass, 'model')
def test_view_index_attribute_exists_permission_required(self):
""" Attribute exists test
Ensure that `model` attribute is defined as it's required .
"""
module = __import__(self.index_module, fromlist=[self.index_view])
assert hasattr(module, self.index_view)
viewclass = getattr(module, self.index_view)
assert hasattr(viewclass, 'permission_required')
def test_view_index_attribute_type_permission_required(self):
""" Attribute Type Test
Ensure that `permission_required` attribute is a list
"""
module = __import__(self.index_module, fromlist=[self.index_view])
assert hasattr(module, self.index_view)
viewclass = getattr(module, self.index_view)
assert type(viewclass.permission_required) is list
def test_view_index_attribute_exists_template_name(self):
""" Attribute exists test
Ensure that `template_name` attribute is defined as it's required.
"""
module = __import__(self.index_module, fromlist=[self.index_view])
assert hasattr(module, self.index_view)
viewclass = getattr(module, self.index_view)
assert hasattr(viewclass, 'template_name')
def test_view_index_attribute_type_template_name(self):
""" Attribute Type Test
Ensure that `template_name` attribute is a string.
"""
module = __import__(self.index_module, fromlist=[self.index_view])
assert hasattr(module, self.index_view)
viewclass = getattr(module, self.index_view)
assert type(viewclass.template_name) is str
class AllViews(
AddView,
ChangeView,
DeleteView,
DisplayView,
IndexView
):
""" Abstract test class containing ALL view tests """
add_module: str = None
""" Full module path to test """
add_view: str = None
""" View Class name to test """
change_module: str = None
""" Full module path to test """
change_view: str = None
""" Change Class name to test """
delete_module: str = None
""" Full module path to test """
delete_view: str = None
""" Delete Class name to test """
display_module: str = None
""" Full module path to test """
display_view: str = None
""" Change Class name to test """
index_module: str = None
""" Full module path to test """
index_view: str = None
""" Index Class name to test """
@pytest.mark.skip(reason='write test')
def test_view_index_attribute_missing_permission_required(self):
""" Attribute missing Test
Ensure that `permission_required` attribute is not defined within the view.
this can be done by mocking the inherited class with the `permission_required` attribute
set to a value that if it changed would be considered defined in the created view.
## Why?
This attribute can be dynamically added based of of the view name along with attributes
`model._meta.model_name` and `str(__class__.__name__).lower()`.
Additional test:
- ensure that the attribute does get automagically created.
- ensure that the classes name is one of add, change, delete, display or index.
"""
@pytest.mark.skip(reason='write test')
def test_view_index_attribute_missing_template_name(self):
""" Attribute missing Test
Ensure that `template_name` attribute is not defined within the view if the value
is `form.html.j2`
this valuse is already defined in the base form
"""

View File

@ -0,0 +1,14 @@
class DoesNotExist:
"""Object does not exist
Use this class as the expected value for a test cases expected value when
the object does not exist.
"""
@property
def __name__(self):
return str('does_not_exist')

View File

@ -0,0 +1,64 @@
import django
import pytest
import unittest
import requests
from django.contrib.auth.models import ContentType
from django.shortcuts import reverse
from django.test import Client, TestCase
User = django.contrib.auth.get_user_model()
class ContentTypePermissionsAPI(TestCase):
model = ContentType
app_namespace = 'API'
url_name = '_api_v2_content_type'
@classmethod
def setUpTestData(self):
"""Setup Test
1. create a user
"""
self.url_kwargs = {}
self.url_view_kwargs = {'pk': 1}
self.view_user = User.objects.create_user(username="test_user_view", password="password")
def test_view_user_anon_denied(self):
""" Check correct permission for view
Attempt to view as anon user
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
response = client.get(url)
assert response.status_code == 401
def test_view_authenticated_user(self):
""" Check correct permission for view
Attempt to view as user who is authenticated
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
client.force_login(self.view_user)
response = client.get(url)
assert response.status_code == 200

View File

@ -0,0 +1,64 @@
import django
import pytest
import unittest
import requests
from django.contrib.auth.models import Permission
from django.shortcuts import reverse
from django.test import Client, TestCase
User = django.contrib.auth.get_user_model()
class PermissionPermissionsAPI(TestCase):
model = Permission
app_namespace = 'API'
url_name = '_api_v2_permission'
@classmethod
def setUpTestData(self):
"""Setup Test
1. create a user
"""
self.url_kwargs = {}
self.url_view_kwargs = {'pk': 1}
self.view_user = User.objects.create_user(username="test_user_view", password="password")
def test_view_user_anon_denied(self):
""" Check correct permission for view
Attempt to view as anon user
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
response = client.get(url)
assert response.status_code == 401
def test_view_authenticated_user(self):
""" Check correct permission for view
Attempt to view as user who is authenticated
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
client.force_login(self.view_user)
response = client.get(url)
assert response.status_code == 200

View File

@ -0,0 +1,88 @@
from django.conf import settings as django_settings
from django.shortcuts import reverse
from django.test import TestCase, Client
from centurion.helpers.merge_software import merge_software
import pytest
import unittest
class MergeSoftwareHelper(TestCase):
""" tests for function `merge_software` """
@classmethod
def setUpTestData(self):
self.data: dict = {
'first_list': [
{
'name': 'software_1',
'state': 'install'
},
{
'name': 'software_2',
'state': 'install'
}
],
'second_list': [
{
'name': 'software_1',
'state': 'absent'
},
{
'name': 'software_2',
'state': 'absent'
}
],
'third_list': [
{
'name': 'software_1',
'state': 'other'
},
{
'name': 'software_2',
'state': 'other'
},
{
'name': 'software_3',
'state': 'install'
}
]
}
self.software_list_one = merge_software(self.data['first_list'], self.data['second_list'])
self.software_list_two = merge_software(self.software_list_one, self.data['third_list'])
def test_merging_0_0(self):
""" ensure Second list overwrites the first app1 """
assert self.software_list_one[0]['state'] == 'absent'
def test_merging_0_1(self):
""" ensure Second list overwrites the first app2 """
assert self.software_list_one[1]['state'] == 'absent'
def test_merging_1_0(self):
""" ensure Second list overwrites the first app1 again """
assert self.software_list_two[0]['state'] == 'other'
def test_merging_1_1(self):
""" ensure Second list overwrites the first app2 again """
assert self.software_list_two[1]['state'] == 'other'
def test_merging_1_new_list_item(self):
""" ensure Second list overwrites the first app2 again """
assert len(self.software_list_two) == 3

View File

@ -0,0 +1,63 @@
import django
import pytest
import unittest
import requests
from django.shortcuts import reverse
from django.test import Client, TestCase
User = django.contrib.auth.get_user_model()
class UserPermissionsAPI(TestCase):
model = User
app_namespace = 'API'
url_name = '_api_v2_user'
@classmethod
def setUpTestData(self):
"""Setup Test
1. create a user
"""
self.url_kwargs = {}
self.view_user = User.objects.create_user(username="test_user_view", password="password")
self.url_view_kwargs = {'pk': self.view_user.pk}
def test_view_user_anon_denied(self):
""" Check correct permission for view
Attempt to view as anon user
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
response = client.get(url)
assert response.status_code == 401
def test_view_authenticated_user(self):
""" Check correct permission for view
Attempt to view as user who is authenticated
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
client.force_login(self.view_user)
response = client.get(url)
assert response.status_code == 200

View File

@ -0,0 +1,86 @@
from centurion.urls import urlpatterns
class Data:
def parse_urls(self, patterns, parent_route = None) -> list:
urls = []
root_paths = [
'access',
# 'account',
# 'api',
'config_management',
'history',
'itam',
'organization',
'settings'
]
for url in patterns:
if hasattr(url, 'pattern'):
route = None
if hasattr(url.pattern, '_route'):
if parent_route:
route = parent_route + url.pattern._route
route = str(route).replace('<int:device_id>', '1')
route = str(route).replace('<int:group_id>', '1')
route = str(route).replace('<int:operating_system_id>', '1')
route = str(route).replace('<int:organization_id>', '1')
route = str(route).replace('<int:pk>', '1')
route = str(route).replace('<int:software_id>', '1')
route = str(route).replace('<int:team_id>', '1')
if route != '' and route not in urls:
urls += [ route ]
else:
route = url.pattern._route
route = str(route).replace('<int:device_id>', '1')
route = str(route).replace('<int:group_id>', '1')
route = str(route).replace('<int:operating_system_id>', '1')
route = str(route).replace('<int:organization_id>', '1')
route = str(route).replace('<int:pk>', '1')
route = str(route).replace('<int:software_id>', '1')
route = str(route).replace('<int:team_id>', '1')
if str(url.pattern._route).replace('/', '') in root_paths:
if route != '' and route not in urls:
urls += [ route ]
if hasattr(url, 'url_patterns'):
if str(url.pattern._route).replace('/', '') in root_paths:
urls += self.parse_urls(patterns=url.url_patterns, parent_route=url.pattern._route)
return urls
def __init__(self):
urls = []
patterns = urlpatterns
urls_found = self.parse_urls(patterns=patterns)
for url in urls_found:
if url not in urls:
urls += [ url ]
self.urls = urls

View File

@ -0,0 +1,141 @@
import pytest
import re
import requests
import unittest
from django.test import LiveServerTestCase
from centurion.urls import urlpatterns
from conftest import Data
@pytest.mark.skip(reason="test server required to be setup so tests work.")
class TestRenderedTemplateLinks:
"""UI Links tests """
server_host: str = '127.0.0.1'
# server_host: str = '192.168.1.172'
server_url: str = 'http://' + server_host + ':8002/'
data = Data()
driver = None
""" Chrome webdriver """
session = None
""" Client session that is logged into the dejango site """
def setup_class(self):
""" Set up the test
1. fetch session cookie
2. login to site
3. save session for use in tests
"""
self.session = requests.Session()
# fetch the csrf token
self.session.get(
url = self.server_url + 'account/login/',
)
# login
self.client = self.session.post(
url = self.server_url + 'account/login/',
data = {
'username': 'admin',
'password': 'admin',
'csrfmiddlewaretoken': self.session.cookies._cookies[self.server_host]['/']['csrftoken'].value
}
)
@pytest.mark.parametrize(
argnames='url',
argvalues=[link for link in data.urls],
ids=[link for link in data.urls]
)
def test_ui_no_http_forbidden(self, url):
""" Test Page Links
Scrape the page for links and ensure none return HTTP/403.
Test failure denotes a link on a page that should have been filtered out by testing for user
permissions within the template.
Args:
url (str): Page to test
"""
response = self.session.get(
url = str(self.server_url + url)
)
# Failsafe to ensure no redirection and that page exists
assert len(response.history) == 0
assert response.status_code == 200
page_urls = []
page = str(response.content)
links = re.findall('href=\"([a-z\/0-9]+)\"', page)
for link in links:
page_link_response = self.session.get(
url = str(self.server_url + link)
)
# Failsafe to ensure no redirection
assert len(response.history) == 0
assert page_link_response.status_code != 403
@pytest.mark.parametrize(
argnames='url',
argvalues=[link for link in data.urls],
ids=[link for link in data.urls]
)
def test_ui_no_http_not_found(self, url):
""" Test Page Links
Scrape the page for links and ensure none return HTTP/404.
Test failure denotes a link on a page that should not exist within the template.
Args:
url (str): Page to test
"""
response = self.session.get(
url = str(self.server_url + url)
)
# Failsafe to ensure no redirection and that page exists
assert len(response.history) == 0
assert response.status_code == 200
page_urls = []
page = str(response.content)
links = re.findall('href=\"([a-z\/0-9]+)\"', page)
for link in links:
page_link_response = self.session.get(
url = str(self.server_url + link)
)
# Failsafe to ensure no redirection
assert len(response.history) == 0
assert page_link_response.status_code != 404

View File

@ -0,0 +1,18 @@
from django.test import TestCase, Client
import pytest
import unittest
import requests
@pytest.mark.skip(reason="to be written")
def test_context_processor_base_user_settings_if_authenticated_only():
""" Context Processor base to only provide `user_settings` for an authenticated user """
pass
@pytest.mark.skip(reason="to be written")
def test_context_processor_base_user_settings_is_logged_in_user():
""" Context Processor base to only provide `user_settings` for the current logged in user """
pass

View File

@ -0,0 +1,133 @@
import pytest
from django.apps import apps
from django.conf import settings
class MetaChecksPyTest:
@staticmethod
def get_models( excludes: list[ str ] ) -> list[ tuple ]:
"""Fetch models from Centurion Apps
Args:
excludes (list[ str ]): Words that may be in a models name to exclude
Returns:
list[ tuple ]: Centurion ERP Only models
"""
models: list = []
model_apps: list = []
exclude_model_apps = [
'django',
'django_celery_results',
'django_filters',
'drf_spectacular',
'drf_spectacular_sidecar',
'coresheaders',
'corsheaders',
'rest_framework',
'rest_framework_json_api',
'social_django',
]
for app in settings.INSTALLED_APPS:
app = app.split('.')[0]
if app in exclude_model_apps:
continue
model_apps += [ app ]
for model in apps.get_models():
if model._meta.app_label not in model_apps:
continue
skip = False
for exclude in excludes:
if exclude in str(model._meta.model_name):
skip = True
break
if skip:
continue
models += [ (model,) ]
return models
notes_models = get_models( [ 'base', 'history', 'note', 'ticket' ] )
@pytest.mark.xfail( reason = 'Test Checks if installed models has a notes table' )
@pytest.mark.parametrize(
argnames = [
'test_model'
],
argvalues = notes_models,
ids = [ model[0]._meta.app_label + '_' + model[0]._meta.model_name for model in notes_models ]
)
def test_model_has_notes(self, test_model):
"""Note Table check
Check if the model has a corresponding notes table that should be
called `<app_label>_<model_name>_notes`
"""
notes_model_table: str = test_model._meta.app_label + '_' + test_model._meta.model_name + '_notes'
found = False
for model in apps.get_models():
if model._meta.db_table == notes_model_table:
found = True
break
assert found
history_models = get_models( [ 'base', 'history', 'note', 'ticket' ] )
@pytest.mark.xfail( reason = 'Test Checks if installed models has a History table' )
@pytest.mark.parametrize(
argnames = [
'test_model'
],
argvalues = history_models,
ids = [ model[0]._meta.app_label + '_' + model[0]._meta.model_name for model in history_models ]
)
def test_model_has_history(self, test_model):
"""History Table check
Check if the model has a corresponding notes table that should be
called `<app_label>_<model_name>_notes`
"""
history_model_table: str = test_model._meta.app_label + '_' + test_model._meta.model_name + '_history'
found = False
for model in apps.get_models():
if model._meta.db_table == history_model_table:
found = True
break
assert found

View File

@ -0,0 +1,170 @@
import pytest
from django.conf import settings as django_settings
from django.shortcuts import reverse
from django.test import TestCase, Client
from django.conf import settings
class SettingsDefault(TestCase):
""" Test Settings file default values """
def test_setting_default_debug_off(self):
""" Ensure that debug is off within settings by default
Debug is only required during development with this setting must always remain off within the committed code.
"""
assert not settings.DEBUG
def test_setting_default_debug_off_type(self):
""" Settings attribute type check
setting `DEBUG` must be of type bool
"""
assert type(settings.DEBUG) is bool
def test_setting_default_login_required(self):
""" By default login should be required
"""
assert settings.LOGIN_REQUIRED
def test_setting_default_login_required_type(self):
""" Settings attribute type check
setting `LOGIN_REQUIRED` must be of type bool
"""
assert type(settings.LOGIN_REQUIRED) is bool
def test_setting_default_metrics_off(self):
""" Ensure that metrics is off within settings by default
Metrics is only required when user turns it on
"""
assert not settings.METRICS_ENABLED
def test_setting_default_metrics_off_type(self):
""" Settings attribute type check
setting `METRICS_ENABLED` must be of type bool
"""
assert type(settings.METRICS_ENABLED) is bool
def test_setting_default_use_tz(self):
""" Ensure that 'USE_TZ = True' is within settings
"""
assert settings.USE_TZ
def test_setting_default_use_tz_type(self):
""" Settings attribute type check
setting `USE_TZ` must be of type bool
"""
assert type(settings.USE_TZ) is bool
class SettingsValues(TestCase):
""" Test Each setting that offers different functionality """
def test_setting_value_login_required(self):
"""Some docstring defining what the test is checking."""
client = Client()
url = reverse('home')
django_settings.LOGIN_REQUIRED = True
response = client.get(url)
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_setting_value_login_required_not(self):
"""Some docstring defining what the test is checking."""
client = Client()
url = reverse('home')
django_settings.LOGIN_REQUIRED = False
response = client.get(url)
assert response.status_code == 200
def test_setting_value_metrics_off_middleware(self):
""" Metrics off check
when metrics are off, its middleware must not exist in settings
"""
assert (
'django_prometheus.middleware.PrometheusBeforeMiddleware' not in settings.MIDDLEWARE
and
'django_prometheus.middleware.PrometheusAfterMiddleware' not in settings.MIDDLEWARE
)
def test_setting_value_metrics_off_installed_apps(self):
""" Metrics off check
when metrics are off, it should not be installed.
"""
assert 'django_prometheus' not in settings.INSTALLED_APPS
@pytest.mark.skip( reason = 'figure out how to test' )
def test_setting_value_metrics_on_middleware(self):
""" Metrics off check
logic in settings adjusts middleware when `METRICS_ENABLED=True`
when metrics are off, its middleware must not exist in settings
"""
assert (
'django_prometheus.middleware.PrometheusBeforeMiddleware' in settings.MIDDLEWARE
and
'django_prometheus.middleware.PrometheusAfterMiddleware' in settings.MIDDLEWARE
)
@pytest.mark.skip( reason = 'figure out how to test' )
def test_setting_value_metrics_on_installed_apps(self):
""" Metrics off check
logic in settings adjusts installed apps when `METRICS_ENABLED=True`
when metrics are off, it should not be installed.
"""
settings.METRICS_ENABLED = True
assert 'django_prometheus' in settings.INSTALLED_APPS

View File

@ -0,0 +1,636 @@
import pytest
from django.db.models import fields
from rest_framework.exceptions import ValidationError
from access.models.tenant import Tenant as Organization
from access.tests.unit.tenancy_object.test_unit_tenancy_object_model import (
TenancyObjectInheritedCases as AccessTenancyObjectInheritedCases
)
from core.tests.unit.mixin.test_unit_history_save import (
SaveHistory,
SaveHistoryMixinInheritedCases
)
class ModelMetaTestCases:
def test_attribute_exists_ordering(self):
"""Test for existance of field in `<model>.Meta`
Attribute `ordering` must be defined in `Meta` class.
"""
assert 'ordering' in self.model._meta.original_attrs
def test_attribute_not_empty_ordering(self):
"""Test field `<model>.Meta` is not empty
Attribute `ordering` must contain values
"""
assert (
self.model._meta.original_attrs['ordering'] is not None
and len(list(self.model._meta.original_attrs['ordering'])) > 0
)
def test_attribute_type_ordering(self):
"""Test field `<model>.Meta` is not empty
Attribute `ordering` must be of type list.
"""
assert type(self.model._meta.original_attrs['ordering']) is list
def test_field_exists_verbose_name(self):
"""Test for existance of field in `<model>.Meta`
Attribute `verbose_name` must be defined in `Meta` class.
"""
assert 'verbose_name' in self.model._meta.original_attrs
def test_field_type_verbose_name(self):
"""Test field `<model>.Meta` is not empty
Attribute `verbose_name` must be of type str.
"""
assert type(self.model._meta.original_attrs['verbose_name']) is str
def test_field_exists_verbose_name_plural(self):
"""Test for existance of field in `<model>.Meta`
Attribute `verbose_name_plural` must be defined in `Meta` class.
"""
assert 'verbose_name_plural' in self.model._meta.original_attrs
def test_field_type_verbose_name_plural(self):
"""Test field `<model>.Meta` is not empty
Attribute `verbose_name_plural` must be of type str.
"""
assert type(self.model._meta.original_attrs['verbose_name_plural']) is str
class ModelFieldsTestCases:
# these tests have been migrated to class ModelFieldsTestCasesReWrite as part of #729
@pytest.mark.skip( reason = 'see test __doc__' )
def test_model_fields_parameter_mandatory_has_no_default(self):
"""Test Field called with Parameter
## Test skipped
fields dont have enough info to determine if mandatory, so this item can't be
tested.
Some fields can be set as `null=false` with `blank=false` however `default=<value>`
ensures it's populated with a desired default.
If a field is set as null=false, there must not be a default parameter
"""
fields_have_test_value: bool = True
fields_to_skip_checking: list = [
'created',
'is_global',
'modified'
]
for field in self.model._meta.fields:
if field.attname not in fields_to_skip_checking:
print(f'Checking field {field.attname} to see if mandatory')
if not getattr(field, 'null', True) and not getattr(field, 'blank', True):
if getattr(field, 'default', fields.NOT_PROVIDED) != fields.NOT_PROVIDED:
print(f' Failure on field {field.attname}')
fields_have_test_value = False
assert fields_have_test_value
def test_model_fields_parameter_has_help_text(self):
"""Test Field called with Parameter
During field creation, it should have been called with paramater `help_text`
"""
fields_have_test_value: bool = True
for field in self.model._meta.fields:
print(f'Checking field {field.attname} has attribute "help_text"')
if not hasattr(field, 'help_text'):
print(f' Failure on field {field.attname}')
fields_have_test_value = False
assert fields_have_test_value
def test_model_fields_parameter_type_help_text(self):
"""Test Field called with Parameter
During field creation, paramater `help_text` must be of type str
"""
fields_have_test_value: bool = True
for field in self.model._meta.fields:
print(f'Checking field {field.attname} is of type str')
if not type(field.help_text) is str:
print(f' Failure on field {field.attname}')
fields_have_test_value = False
assert fields_have_test_value
def test_model_fields_parameter_not_empty_help_text(self):
"""Test Field called with Parameter
During field creation, paramater `help_text` must not be `None` or empty ('')
"""
fields_have_test_value: bool = True
for field in self.model._meta.fields:
print(f'Checking field {field.attname} is not empty')
if (
(
field.help_text is None
or field.help_text == ''
)
and not str(field.attname).endswith('_ptr_id')
):
print(f' Failure on field {field.attname}')
fields_have_test_value = False
assert fields_have_test_value
def test_model_fields_parameter_has_verbose_name(self):
"""Test Field called with Parameter
During field creation, it should have been called with paramater `verbose_name`
"""
fields_have_test_value: bool = True
for field in self.model._meta.fields:
print(f'Checking field {field.attname} has attribute "verbose_name"')
if not hasattr(field, 'verbose_name'):
print(f' Failure on field {field.attname}')
fields_have_test_value = False
assert fields_have_test_value
def test_model_fields_parameter_type_verbose_name(self):
"""Test Field called with Parameter
During field creation, paramater `verbose_name` must be of type str
"""
fields_have_test_value: bool = True
for field in self.model._meta.fields:
print(f'Checking field {field.attname} is of type str')
if not type(field.verbose_name) is str:
print(f' Failure on field {field.attname}')
fields_have_test_value = False
assert fields_have_test_value
def test_model_fields_parameter_not_empty_verbose_name(self):
"""Test Field called with Parameter
During field creation, paramater `verbose_name` must not be `None` or empty ('')
"""
fields_have_test_value: bool = True
for field in self.model._meta.fields:
print(f'Checking field {field.attname} is not empty')
if (
field.verbose_name is None
or field.verbose_name == ''
):
print(f' Failure on field {field.attname}')
fields_have_test_value = False
assert fields_have_test_value
class Models(
ModelFieldsTestCases,
ModelMetaTestCases,
):
"""Test Cases for All defined models"""
pass
class TenancyObjectInheritedCases(
Models,
AccessTenancyObjectInheritedCases
):
"""Test Cases for models that inherit from
access.models.tenancy.TenancyObject"""
model = None
kwargs_item_create: dict = None
@classmethod
def setUpTestData(self):
"""Setup Test"""
if not hasattr(self, 'organization'):
self.organization = Organization.objects.create(name='test_org')
self.different_organization = Organization.objects.create(name='test_different_organization')
if self.kwargs_item_create is None:
self.kwargs_item_create: dict = {}
if 'name' in self.model().fields:
self.kwargs_item_create.update({
'name': 'one'
})
if 'organization' in self.model().fields:
self.kwargs_item_create.update({
'organization': self.organization,
})
if 'model_notes' in self.model().fields:
self.kwargs_item_create.update({
'model_notes': 'notes',
})
self.item = self.model.objects.create(
**self.kwargs_item_create,
)
def test_create_validation_exception_no_organization(self):
""" Tenancy objects must have an organization
Must not be able to create an item without an organization
"""
kwargs_item_create = self.kwargs_item_create.copy()
del kwargs_item_create['organization']
with pytest.raises(ValidationError) as err:
self.model.objects.create(
**kwargs_item_create,
)
assert err.value.get_codes()['organization'] == 'required'
class NonTenancyObjectInheritedCases(
Models,
SaveHistoryMixinInheritedCases,
):
"""Test Cases for models that don't inherit from
access.models.tenancy.TenancyObject"""
model = None
@classmethod
def setUpTestData(self):
"""Setup Test"""
self.item = self.model.objects.create(
**self.kwargs_item_create,
)
def test_class_inherits_save_history(self):
""" Class inheritence
TenancyObject must inherit SaveHistory
"""
assert issubclass(self.model, SaveHistory)
################################################################################################################
#
# PyTest Parameterized re-write
#
################################################################################################################
class ModelFieldsTestCasesReWrite:
@property
def parameterized_fields(self) -> dict:
return {
"organization": {
'field_type': fields.Field,
'field_parameter_default_exists': False,
'field_parameter_verbose_name_type': str
},
"model_notes": {
'field_type': fields.TextField,
'field_parameter_verbose_name_type': str
},
"is_global": {
'field_type': fields.BooleanField,
'field_parameter_default_exists': True,
'field_parameter_default_value': False,
'field_parameter_verbose_name_type': str
}
}
def test_model_field_type(self, parameterized, param_key_fields,
param_field_name,
param_field_type
):
"""Field Type Check
Ensure that the model field is the expected type.
"""
if param_field_type is None:
assert getattr(self.model, param_field_name) is None
else:
assert isinstance(self.model._meta.get_field(param_field_name), param_field_type)
@pytest.mark.skip( reason = 'see test __doc__' )
def test_model_fields_parameter_mandatory_has_no_default(self):
"""Test Field called with Parameter
## Test skipped
fields dont have enough info to determine if mandatory, so this item can't be
tested.
Some fields can be set as `null=false` with `blank=false` however `default=<value>`
ensures it's populated with a desired default.
If a field is set as null=false, there must not be a default parameter
"""
fields_have_test_value: bool = True
fields_to_skip_checking: list = [
'created',
'is_global',
'modified'
]
for field in self.model._meta.fields:
if field.attname not in fields_to_skip_checking:
print(f'Checking field {field.attname} to see if mandatory')
if not getattr(field, 'null', True) and not getattr(field, 'blank', True):
if getattr(field, 'default', fields.NOT_PROVIDED) != fields.NOT_PROVIDED:
print(f' Failure on field {field.attname}')
fields_have_test_value = False
assert fields_have_test_value
def test_model_fields_parameter_exist_default(self, parameterized, param_key_fields,
param_field_name,
param_field_parameter_default_exists
):
"""Test Field called with Parameter
During field creation, paramater `verbose_name` must not be `None` or empty ('')
"""
if param_field_parameter_default_exists == False:
assert self.model._meta.get_field(param_field_name).default == fields.NOT_PROVIDED
elif param_field_parameter_default_exists is None:
assert True
else:
assert self.model._meta.get_field(param_field_name).has_default() == param_field_parameter_default_exists
def test_model_fields_parameter_value_default(self, parameterized, param_key_fields,
param_field_name,
param_field_parameter_default_value
):
"""Test Field called with Parameter
During field creation, paramater `verbose_name` must not be `None` or empty ('')
"""
if param_field_parameter_default_value is None:
assert True
else:
assert getattr(self.model._meta.get_field(param_field_name), 'default') == param_field_parameter_default_value
def test_model_fields_parameter_type_help_text(self, parameterized, param_key_fields,
param_field_name,
param_field_parameter_verbose_name_type
):
"""Test Field called with Parameter
During field creation, paramater `verbose_name` must be of type str
"""
if param_field_parameter_verbose_name_type is None:
assert getattr(self.model, param_field_name) is None
else:
assert type(getattr(self.model._meta.get_field(param_field_name), 'help_text')) is param_field_parameter_verbose_name_type
def test_model_fields_parameter_not_empty_help_text(self, parameterized, param_key_fields,
param_field_name,
param_field_parameter_verbose_name_type
):
"""Test Field called with Parameter
During field creation, paramater `verbose_name` must not be `None` or empty ('')
"""
if param_field_parameter_verbose_name_type is None:
assert getattr(self.model, param_field_name) is None
else:
assert getattr(self.model._meta.get_field(param_field_name), 'help_text') != ''
def test_model_fields_parameter_type_verbose_name(self, parameterized, param_key_fields,
param_field_name,
param_field_parameter_verbose_name_type
):
"""Test Field called with Parameter
During field creation, paramater `verbose_name` must be of type str
"""
if param_field_parameter_verbose_name_type is None:
assert getattr(self.model, param_field_name) is None
else:
assert type(getattr(self.model._meta.get_field(param_field_name), 'verbose_name')) is param_field_parameter_verbose_name_type
def test_model_fields_parameter_not_empty_verbose_name(self, parameterized, param_key_fields,
param_field_name,
param_field_parameter_verbose_name_type
):
"""Test Field called with Parameter
During field creation, paramater `verbose_name` must not be `None` or empty ('')
"""
if param_field_parameter_verbose_name_type is None:
assert getattr(self.model, param_field_name) is None
else:
assert getattr(self.model._meta.get_field(param_field_name), 'verbose_name') != ''
class PyTestTenancyObjectInheritedCases(
ModelMetaTestCases,
AccessTenancyObjectInheritedCases,
ModelFieldsTestCasesReWrite,
):
parameterized_fields: dict = {
"model_notes": {
'field_type': fields.TextField,
'field_parameter_verbose_name_type': str
}
}
def test_create_validation_exception_no_organization(self):
""" Tenancy objects must have an organization
Must not be able to create an item without an organization
"""
kwargs_create_item = self.kwargs_create_item.copy()
del kwargs_create_item['organization']
with pytest.raises(ValidationError) as err:
self.model.objects.create(
**kwargs_create_item,
)
assert err.value.get_codes()['organization'] == 'required'

100
app/centurion/urls.py Normal file
View File

@ -0,0 +1,100 @@
"""
URL configuration for itsm project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_centurion.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.views.static import serve
from django.urls import include, path, re_path
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
from .views import home
from core.views import related_ticket, ticket_linked_item
from settings.views import user_settings
urlpatterns = [
path('', home.HomeView.as_view(), name='home'),
path('admin/', admin.site.urls, name='_administration'),
path('account/password_change/', auth_views.PasswordChangeView.as_view(template_name="password_change.html.j2"), name="change_password"),
path('account/settings/<int:pk>', user_settings.View.as_view(), name="_settings_user"),
path('account/settings/<int:pk>/edit', user_settings.Change.as_view(), name="_settings_user_change"),
path('account/settings/<int:user_id>/token/add', user_settings.TokenAdd.as_view(), name="_user_auth_token_add"),
path('account/settings/<int:user_id>/token/<int:pk>/delete', user_settings.TokenDelete.as_view(), name="_user_auth_token_delete"),
path("account/", include("django.contrib.auth.urls")),
path("organization/", include("access.urls")),
path("assistance/", include("assistance.urls")),
path("itam/", include("itam.urls")),
path("itim/", include("itim.urls")),
path("config_management/", include("config_management.urls")),
re_path(r'^static/(?P<path>.*)$', serve,{'document_root': settings.STATIC_ROOT}),
path('ticket/<str:ticket_type>/<int:ticket_id>/relate/add', related_ticket.Add.as_view(), name="_ticket_related_add"),
path('ticket/<str:ticket_type>/<int:ticket_id>/linked_item/add', ticket_linked_item.Add.as_view(), name="_ticket_linked_item_add"),
]
if settings.SSO_ENABLED:
urlpatterns += [
path('sso/', include('social_django.urls', namespace='social'))
]
if settings.API_ENABLED:
urlpatterns += [
path("api/", include("api.urls", namespace = 'v1')),
path('api/schema/', SpectacularAPIView.as_view(api_version='v1'), name='schema'),
path('api/swagger/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
path("api/v2/", include("api.urls_v2", namespace = 'v2')),
]
urlpatterns += [
path('api/v2/auth/', include('rest_framework.urls')),
]
if settings.DEBUG:
urlpatterns += [
path("__debug__/", include("debug_toolbar.urls"), name='_debug'),
]
# must be after above
urlpatterns += [
path("project_management/", include("project_management.urls")),
path("settings/", include("settings.urls")),
]

View File

@ -0,0 +1 @@
from .home import *

View File

@ -0,0 +1,18 @@
import requests
from django.conf import settings
from django.shortcuts import redirect, render
from django.views.generic import View
class HomeView(View):
template_name = 'home.html.j2'
def get(self, request):
if not request.user.is_authenticated and settings.LOGIN_REQUIRED:
return redirect(f"{settings.LOGIN_URL}?next={request.path}")
context = {}
return render(request, self.template_name, context)

View File

@ -0,0 +1,57 @@
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse
from api.viewsets.common import AuthUserReadOnlyModelViewSet
from centurion.serializers.content_type import (
ContentType,
ContentTypeViewSerializer
)
@extend_schema_view(
list = extend_schema(
summary = 'Fetch all content types',
description='',
responses = {
200: OpenApiResponse(description='', response=ContentTypeViewSerializer),
}
),
retrieve = extend_schema(
summary = 'Fetch a content type',
description='',
responses = {
200: OpenApiResponse(description='', response=ContentTypeViewSerializer),
}
),
)
class ViewSet(
AuthUserReadOnlyModelViewSet
):
filterset_fields = [
'app_label',
'model',
]
model = ContentType
search_fields = [
'display_name',
]
view_description = 'Centurion Content Types'
def get_serializer_class(self):
return ContentTypeViewSerializer
def get_view_name(self):
if self.detail:
return 'Content Type'
return 'Content Types'

View File

@ -0,0 +1,31 @@
from drf_spectacular.utils import extend_schema
from rest_framework.response import Response
from rest_framework.reverse import reverse
from api.viewsets.common import IndexViewset
@extend_schema(exclude = True)
class Index(IndexViewset):
allowed_methods: list = [
'GET',
'OPTIONS'
]
view_description = "Base Objects"
view_name = "Base"
def list(self, request, pk=None):
return Response(
{
"content_type": reverse('v2:_api_v2_content_type-list', request=request),
"permission": reverse('v2:_api_v2_permission-list', request=request),
"user": reverse('v2:_api_v2_user-list', request=request)
}
)

View File

@ -0,0 +1,48 @@
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse
from api.viewsets.common import AuthUserReadOnlyModelViewSet
from centurion.serializers.permission import (
Permission,
PermissionViewSerializer
)
@extend_schema_view(
list = extend_schema(
summary = 'Fetch all permissions',
description='',
responses = {
200: OpenApiResponse(description='', response=PermissionViewSerializer),
}
),
retrieve = extend_schema(
summary = 'Fetch a permission',
description='',
responses = {
200: OpenApiResponse(description='', response=PermissionViewSerializer),
}
),
)
class ViewSet(
AuthUserReadOnlyModelViewSet
):
model = Permission
view_description = 'Centurion Permissions'
def get_serializer_class(self):
return PermissionViewSerializer
def get_view_name(self):
if self.detail:
return 'Permission'
return 'Permissions'

View File

@ -0,0 +1,63 @@
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse
from api.viewsets.common import AuthUserReadOnlyModelViewSet
from centurion.serializers.user import (
User,
UserBaseSerializer
)
@extend_schema_view(
list = extend_schema(
summary = 'Fetch all users',
description='',
responses = {
200: OpenApiResponse(description='', response=UserBaseSerializer),
403: OpenApiResponse(description='User is missing view permissions'),
}
),
retrieve = extend_schema(
summary = 'Fetch a single user',
description='',
responses = {
200: OpenApiResponse(description='', response=UserBaseSerializer),
403: OpenApiResponse(description='User is missing view permissions'),
}
),
)
class ViewSet(
AuthUserReadOnlyModelViewSet
):
filterset_fields = [
'username',
'first_name',
'last_name',
'is_active'
]
model = User
search_fields = [
'username',
'first_name',
'last_name',
]
view_description = 'Centurion Users'
def get_serializer_class(self):
return UserBaseSerializer
def get_view_name(self):
if self.detail:
return 'User'
return 'Users'

16
app/centurion/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for itsm project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'centurion.settings')
application = get_wsgi_application()