3
app/centurion/__init__.py
Normal file
3
app/centurion/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .celery import worker as celery_app
|
||||
|
||||
__all__ = ('celery_app',)
|
16
app/centurion/asgi.py
Normal file
16
app/centurion/asgi.py
Normal 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()
|
224
app/centurion/context_processors/base.py
Normal file
224
app/centurion/context_processors/base.py
Normal 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)
|
||||
}
|
33
app/centurion/helpers/merge_software.py
Normal file
33
app/centurion/helpers/merge_software.py
Normal 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
|
38
app/centurion/middleware/timezone.py
Normal file
38
app/centurion/middleware/timezone.py
Normal 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)
|
69
app/centurion/serializers/content_type.py
Normal file
69
app/centurion/serializers/content_type.py
Normal 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',
|
||||
]
|
77
app/centurion/serializers/permission.py
Normal file
77
app/centurion/serializers/permission.py
Normal 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',
|
||||
]
|
||||
|
46
app/centurion/serializers/user.py
Normal file
46
app/centurion/serializers/user.py
Normal 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
702
app/centurion/settings.py
Normal 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
|
||||
})
|
0
app/centurion/tests/__init__.py
Normal file
0
app/centurion/tests/__init__.py
Normal file
0
app/centurion/tests/abstract/__init__.py
Normal file
0
app/centurion/tests/abstract/__init__.py
Normal file
53
app/centurion/tests/abstract/mock_view.py
Normal file
53
app/centurion/tests/abstract/mock_view.py
Normal 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
|
||||
)
|
487
app/centurion/tests/abstract/model_permissions.py
Normal file
487
app/centurion/tests/abstract/model_permissions.py
Normal 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
|
62
app/centurion/tests/abstract/models.py
Normal file
62
app/centurion/tests/abstract/models.py
Normal 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
|
||||
"""
|
626
app/centurion/tests/abstract/views.py
Normal file
626
app/centurion/tests/abstract/views.py
Normal 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
|
||||
"""
|
14
app/centurion/tests/common.py
Normal file
14
app/centurion/tests/common.py
Normal 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')
|
0
app/centurion/tests/functional/__init__.py
Normal file
0
app/centurion/tests/functional/__init__.py
Normal 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
|
@ -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
|
@ -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
|
||||
|
@ -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
|
86
app/centurion/tests/ui/conftest.py
Normal file
86
app/centurion/tests/ui/conftest.py
Normal 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
|
141
app/centurion/tests/ui/test_page_links.py
Normal file
141
app/centurion/tests/ui/test_page_links.py
Normal 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
|
||||
|
18
app/centurion/tests/unit/test_context_processor_base.py
Normal file
18
app/centurion/tests/unit/test_context_processor_base.py
Normal 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
|
133
app/centurion/tests/unit/test_preperation_work.py
Normal file
133
app/centurion/tests/unit/test_preperation_work.py
Normal 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
|
170
app/centurion/tests/unit/test_settings.py
Normal file
170
app/centurion/tests/unit/test_settings.py
Normal 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
|
636
app/centurion/tests/unit/test_unit_models.py
Normal file
636
app/centurion/tests/unit/test_unit_models.py
Normal 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
100
app/centurion/urls.py
Normal 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")),
|
||||
|
||||
]
|
1
app/centurion/views/__init__.py
Normal file
1
app/centurion/views/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .home import *
|
18
app/centurion/views/home.py
Normal file
18
app/centurion/views/home.py
Normal 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)
|
57
app/centurion/viewsets/base/content_type.py
Normal file
57
app/centurion/viewsets/base/content_type.py
Normal 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'
|
31
app/centurion/viewsets/base/index.py
Normal file
31
app/centurion/viewsets/base/index.py
Normal 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)
|
||||
}
|
||||
)
|
48
app/centurion/viewsets/base/permisson.py
Normal file
48
app/centurion/viewsets/base/permisson.py
Normal 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'
|
63
app/centurion/viewsets/base/user.py
Normal file
63
app/centurion/viewsets/base/user.py
Normal 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
16
app/centurion/wsgi.py
Normal 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()
|
Reference in New Issue
Block a user