From 6ff4779cfb7bcbb43455ea5a973fd6635b92cf97 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 10 Aug 2025 16:37:16 +0930 Subject: [PATCH 01/13] chore: exclude app/artifacts dir from repo ref: #947 #774 #152 --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index a7265761..40a278c8 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ feature_flags.json coverage_*.json *-coverage.xml log/ +# Integration testing +app/artifacts/ +app/pyproject.toml From 31312584910e5da998bdb5224e327c2235e9aff9 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 10 Aug 2025 16:42:33 +0930 Subject: [PATCH 02/13] fix(access): When creating permission QuerySet prevent app crash if db not setup ref: #947 #152 --- app/access/functions/permissions.py | 92 ++++++++++++++++++----------- 1 file changed, 56 insertions(+), 36 deletions(-) diff --git a/app/access/functions/permissions.py b/app/access/functions/permissions.py index 1ba95b50..b1bf83b9 100644 --- a/app/access/functions/permissions.py +++ b/app/access/functions/permissions.py @@ -1,9 +1,10 @@ from django.apps import apps +from django.conf import settings from django.contrib.auth.models import ( ContentType, Permission ) -from django.conf import settings +from django.db.models import QuerySet def permission_queryset(): @@ -61,47 +62,66 @@ def permission_queryset(): if not settings.RUNNING_TESTS: - models = apps.get_models() + try: + # This blocks purpose is to cater for fresh install + # so that the app does not crash before the DB is setup. - for model in models: + models = apps.get_models() - if( - not str(model._meta.object_name).endswith('AuditHistory') - and not str(model._meta.model_name).lower().endswith('history') - ): - # check `endswith('history')` can be removed when the old history models are removed - continue - - content_type = ContentType.objects.get( - app_label = model._meta.app_label, - model = model._meta.model_name - ) - - permissions = Permission.objects.filter( - content_type = content_type, - ) - - for permission in permissions: + for model in models: if( - not permission.codename == 'view_' + str(model._meta.model_name) - and str(model._meta.object_name).endswith('AuditHistory') - ): - exclude_permissions += [ permission.codename ] - - elif( not str(model._meta.object_name).endswith('AuditHistory') - and str(model._meta.model_name).lower().endswith('history') + and not str(model._meta.model_name).lower().endswith('history') ): - # This `elif` can be removed when the old history models are removed + # check `endswith('history')` can be removed when the old history models are removed + continue - exclude_permissions += [ permission.codename ] + content_type = ContentType.objects.get( + app_label = model._meta.app_label, + model = model._meta.model_name + ) + + permissions = Permission.objects.filter( + content_type = content_type, + ) + + for permission in permissions: + + if( + not permission.codename == 'view_' + str(model._meta.model_name) + and str(model._meta.object_name).endswith('AuditHistory') + ): + exclude_permissions += [ permission.codename ] + + elif( + not str(model._meta.object_name).endswith('AuditHistory') + and str(model._meta.model_name).lower().endswith('history') + ): + # This `elif` can be removed when the old history models are removed + + exclude_permissions += [ permission.codename ] - return Permission.objects.select_related('content_type').filter( - content_type__app_label__in = centurion_apps, - ).exclude( - content_type__model__in = exclude_models - ).exclude( - codename__in = exclude_permissions - ) \ No newline at end of file + return Permission.objects.select_related('content_type').filter( + content_type__app_label__in = centurion_apps, + ).exclude( + content_type__model__in = exclude_models + ).exclude( + codename__in = exclude_permissions + ) + + except: + pass + + return QuerySet() + + else: + + return Permission.objects.select_related('content_type').filter( + content_type__app_label__in = centurion_apps, + ).exclude( + content_type__model__in = exclude_models + ).exclude( + codename__in = exclude_permissions + ) From 6732315b96e5827d054835780080a412b7e0daae Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 12 Aug 2025 12:50:27 +0930 Subject: [PATCH 03/13] fix: remove trailing slant from URLs ref: #947 --- app/access/urls_api.py | 14 +++++------ app/accounting/urls.py | 4 +-- app/api/urls_v2.py | 40 +++++++++++++++--------------- app/assistance/urls_api.py | 6 ++--- app/centurion/settings.py | 1 + app/centurion/urls.py | 15 +++++------ app/config_management/urls_api.py | 6 ++--- app/core/urls_api.py | 24 +++++++++--------- app/devops/urls.py | 8 +++--- app/devops/urls_public.py | 4 +-- app/itam/urls_api.py | 26 +++++++++---------- app/itim/urls_api.py | 12 ++++----- app/project_management/urls_api.py | 6 ++--- app/settings/urls_api.py | 32 ++++++++++++------------ 14 files changed, 100 insertions(+), 98 deletions(-) diff --git a/app/access/urls_api.py b/app/access/urls_api.py index bd9a96cf..6dee2800 100644 --- a/app/access/urls_api.py +++ b/app/access/urls_api.py @@ -34,17 +34,17 @@ router = DefaultRouter(trailing_slash=False) router.register('', access_v2.Index, basename = '_api_v2_access_home') router.register( - prefix = '(?P[company]+)', viewset = entity.ViewSet, + prefix = '/(?P[company]+)', viewset = entity.ViewSet, feature_flag = '2025-00008',basename = '_api_v2_company' ) router.register( - prefix=f'entity/(?P[{entity_type_names}]+)?', viewset = entity.ViewSet, + prefix=f'/entity/(?P[{entity_type_names}]+)?', viewset = entity.ViewSet, feature_flag = '2025-00002', basename = '_api_entity_sub' ) router.register( - prefix = 'entity', viewset = entity.NoDocsViewSet, + prefix = '/entity', viewset = entity.NoDocsViewSet, feature_flag = '2025-00002', basename = '_api_entity' ) @@ -54,7 +54,7 @@ router.register( # ) router.register( - prefix = 'tenant', viewset = organization.ViewSet, + prefix = '/tenant', viewset = organization.ViewSet, basename = '_api_tenant' ) @@ -64,7 +64,7 @@ router.register( # ) router.register( - prefix = 'tenant/(?P[0-9]+)/team', viewset = team_v2.ViewSet, + prefix = '/tenant/(?P[0-9]+)/team', viewset = team_v2.ViewSet, basename = '_api_v2_organization_team' ) @@ -75,13 +75,13 @@ router.register( # ) router.register( - prefix = 'access/tenant/(?P[0-9]+)/team/(?P[0-9]+)/user', + prefix = '/access/tenant/(?P[0-9]+)/team/(?P[0-9]+)/user', viewset = team_user_v2.ViewSet, basename = '_api_v2_organization_team_user' ) router.register( - prefix = 'role', viewset = role.ViewSet, + prefix = '/role', viewset = role.ViewSet, feature_flag = '2025-00003', basename = '_api_role' ) diff --git a/app/accounting/urls.py b/app/accounting/urls.py index 408c9d86..cd6b6732 100644 --- a/app/accounting/urls.py +++ b/app/accounting/urls.py @@ -42,7 +42,7 @@ asset_type_names = str(asset_type_names)[:-1] if not asset_type_names: asset_type_names = 'none' -router.register(f'asset/(?P[{asset_type_names}]+)?', asset.ViewSet, feature_flag = '2025-00004', basename='_api_asset_sub') -router.register('asset', asset.NoDocsViewSet, feature_flag = '2025-00004', basename='_api_asset') +router.register(f'/asset/(?P[{asset_type_names}]+)?', asset.ViewSet, feature_flag = '2025-00004', basename='_api_asset_sub') +router.register('/asset', asset.NoDocsViewSet, feature_flag = '2025-00004', basename='_api_asset') urlpatterns = router.urls diff --git a/app/api/urls_v2.py b/app/api/urls_v2.py index 087231b7..7e6fc46b 100644 --- a/app/api/urls_v2.py +++ b/app/api/urls_v2.py @@ -61,22 +61,22 @@ router = DefaultRouter(trailing_slash=False) router.register('', v2.Index, basename='_api_v2_home') -router.register('base', base_index_v2.Index, basename='_api_v2_base_home') -router.register('base/content_type', content_type_v2.ViewSet, basename='_api_v2_content_type') -router.register('base/permission', permission_v2.ViewSet, basename='_api_v2_permission') -router.register('base/user', user_v2.ViewSet, basename='_api_v2_user') +router.register('/base', base_index_v2.Index, basename='_api_v2_base_home') +router.register('/base/content_type', content_type_v2.ViewSet, basename='_api_v2_content_type') +router.register('/base/permission', permission_v2.ViewSet, basename='_api_v2_permission') +router.register('/base/user', user_v2.ViewSet, basename='_api_v2_user') router.register( - prefix = f'(?P[{history_app_labels}]+)/(?P[{history_type_names} \ + prefix = f'/(?P[{history_app_labels}]+)/(?P[{history_type_names} \ ]+)/(?P[0-9]+)/history', viewset = audit_history.ViewSet, basename = '_api_centurionaudit_sub' ) router.register( - prefix = f'(?P[{notes_app_labels}]+)/(?P[{notes_type_names} \ + prefix = f'/(?P[{notes_app_labels}]+)/(?P[{notes_type_names} \ ]+)/(?P[0-9]+)/notes', viewset = centurion_model_notes.ViewSet, basename = '_api_centurionmodelnote_sub' @@ -85,24 +85,24 @@ router.register( urlpatterns = [ - path('schema', SpectacularAPIView.as_view(api_version='v2'), name='schema-v2',), - path('docs', SpectacularSwaggerView.as_view(url_name='schema-v2'), name='_api_v2_docs'), + path('/schema', SpectacularAPIView.as_view(api_version='v2'), name='schema-v2',), + path('/docs', SpectacularSwaggerView.as_view(url_name='schema-v2'), name='_api_v2_docs'), ] urlpatterns += router.urls urlpatterns += [ - path(route = "access/", view = include("access.urls_api")), - path(route = "accounting/", view = include("accounting.urls")), - path(route = "assistance/", view = include("assistance.urls_api")), - path(route = "config_management/", view = include("config_management.urls_api")), - path(route = "core/", view = include("core.urls_api")), - path(route = "devops/", view = include("devops.urls")), - path(route = "hr/", view = include('human_resources.urls')), - path(route = "itam/", view = include("itam.urls_api")), - path(route = "itim/", view = include("itim.urls_api")), - path(route = "project_management/", view = include("project_management.urls_api")), - path(route = "settings/", view = include("settings.urls_api")), - path(route = 'public/', view = include('api.urls_public')), + path(route = "/access", view = include("access.urls_api")), + path(route = "/accounting", view = include("accounting.urls")), + path(route = "/assistance", view = include("assistance.urls_api")), + path(route = "/config_management", view = include("config_management.urls_api")), + path(route = "/core", view = include("core.urls_api")), + path(route = "/devops", view = include("devops.urls")), + path(route = "/hr", view = include('human_resources.urls')), + path(route = "/itam", view = include("itam.urls_api")), + path(route = "/itim", view = include("itim.urls_api")), + path(route = "/project_management", view = include("project_management.urls_api")), + path(route = "/settings", view = include("settings.urls_api")), + path(route = '/public', view = include('api.urls_public')), ] diff --git a/app/assistance/urls_api.py b/app/assistance/urls_api.py index df81aa2b..18f50b01 100644 --- a/app/assistance/urls_api.py +++ b/app/assistance/urls_api.py @@ -19,16 +19,16 @@ router.register( basename = '_api_v2_assistance_home' ) router.register( - prefix = 'knowledge_base', viewset = knowledge_base_v2.ViewSet, + prefix = '/knowledge_base', viewset = knowledge_base_v2.ViewSet, basename = '_api_knowledgebase' ) router.register( - prefix = '(?P.+)/(?P[0-9]+)/knowledge_base', + prefix = '/(?P.+)/(?P[0-9]+)/knowledge_base', viewset = model_knowledge_base_article.ViewSet, basename = '_api_v2_model_kb' ) router.register( - prefix = 'ticket/request', viewset = request_ticket_v2.ViewSet, + prefix = '/ticket/request', viewset = request_ticket_v2.ViewSet, basename = '_api_v2_ticket_request' ) diff --git a/app/centurion/settings.py b/app/centurion/settings.py index b3f9de31..280ca65e 100644 --- a/app/centurion/settings.py +++ b/app/centurion/settings.py @@ -20,6 +20,7 @@ import django.db.models.options as options options.DEFAULT_NAMES = (*options.DEFAULT_NAMES, 'sub_model_type', 'itam_sub_model_type') +APPEND_SLASH = False AUTH_USER_MODEL = 'auth.User' # Build paths inside the project like this: BASE_DIR / 'subdir'. diff --git a/app/centurion/urls.py b/app/centurion/urls.py index 4970f85e..87b26c2d 100644 --- a/app/centurion/urls.py +++ b/app/centurion/urls.py @@ -4,14 +4,14 @@ from django.contrib.auth import views as auth_views from django.views.static import serve from django.urls import include, path, re_path - +from rest_framework import urls urlpatterns = [ - path('admin/', admin.site.urls, name='_administration'), + 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/password_change', auth_views.PasswordChangeView.as_view(template_name="password_change.html.j2"), name="change_password"), - path("account/", include("django.contrib.auth.urls")), + path("account", include("django.contrib.auth.urls")), re_path(r'^static/(?P.*)$', serve,{'document_root': settings.STATIC_ROOT}), @@ -30,15 +30,16 @@ if settings.API_ENABLED: urlpatterns += [ - path("api/", include("api.urls", namespace = 'v1')), + path("api", include("api.urls", namespace = 'v1')), - path("api/v2/", include("api.urls_v2", namespace = 'v2')), + path("api/v2", include("api.urls_v2", namespace = 'v2')), ] urlpatterns += [ - path('api/v2/auth/', include('rest_framework.urls')), + path('api/v2/auth/login', auth_views.LoginView.as_view(template_name='rest_framework/login.html'), name='login'), + path('api/v2/auth/logout', auth_views.LogoutView.as_view(), name='logout'), ] diff --git a/app/config_management/urls_api.py b/app/config_management/urls_api.py index 340a6a18..865d5385 100644 --- a/app/config_management/urls_api.py +++ b/app/config_management/urls_api.py @@ -18,15 +18,15 @@ router.register( basename = '_api_v2_config_management_home' ) router.register( - prefix = 'group', viewset = config_group_v2.ViewSet, + prefix = '/group', viewset = config_group_v2.ViewSet, basename = '_api_configgroups' ) router.register( - prefix = 'group/(?P[0-9]+)/child_group', viewset = config_group_v2.ViewSet, + prefix = '/group/(?P[0-9]+)/child_group', viewset = config_group_v2.ViewSet, basename = '_api_configgroups_child' ) router.register( - prefix = 'group/(?P[0-9]+)/software', + prefix = '/group/(?P[0-9]+)/software', viewset = config_group_software_v2.ViewSet, basename = '_api_configgroupsoftware' ) diff --git a/app/core/urls_api.py b/app/core/urls_api.py index 75cc27ba..4a198f72 100644 --- a/app/core/urls_api.py +++ b/app/core/urls_api.py @@ -41,59 +41,59 @@ router: DefaultRouter = DefaultRouter(trailing_slash=False) router.register( - 'history', audit_history.NoDocsViewSet, + '/history', audit_history.NoDocsViewSet, basename = '_api_centurionaudit' ) router.register( - prefix=f'ticket', viewset = ticket.NoDocsViewSet, + prefix=f'/ticket', viewset = ticket.NoDocsViewSet, feature_flag = '2025-00006', basename = '_api_ticketbase' ) router.register( - prefix=f'ticket/(?P[{ticket_type_names}]+)', viewset = ticket.ViewSet, + prefix=f'/ticket/(?P[{ticket_type_names}]+)', viewset = ticket.ViewSet, feature_flag = '2025-00006', basename = '_api_ticketbase_sub' ) router.register( - prefix = 'ticket/(?P[0-9]+)/comment', viewset = ticket_comment.NoDocsViewSet, + prefix = '/ticket/(?P[0-9]+)/comment', viewset = ticket_comment.NoDocsViewSet, feature_flag = '2025-00006', basename = '_api_ticket_comment_base' ) router.register( - prefix = 'ticket/(?P[0-9]+)/comment/(?P[0-9]+)/threads', + prefix = '/ticket/(?P[0-9]+)/comment/(?P[0-9]+)/threads', viewset = ticket_comment.ViewSet, feature_flag = '2025-00006', basename = '_api_ticket_comment_base_thread' ) router.register( - prefix = 'ticket/(?P[0-9]+)/comments', viewset = ticket_comment_depreciated.ViewSet, + prefix = '/ticket/(?P[0-9]+)/comments', viewset = ticket_comment_depreciated.ViewSet, basename = '_api_v2_ticket_comment' ) router.register( - prefix = 'ticket/(?P[0-9]+)/comments/(?P[0-9]+)/threads', + prefix = '/ticket/(?P[0-9]+)/comments/(?P[0-9]+)/threads', viewset = ticket_comment_depreciated.ViewSet, basename = '_api_v2_ticket_comment_threads' ) router.register( - prefix = 'ticket/(?P[0-9]+)/linked_item', viewset = ticket_linked_item.ViewSet, + prefix = '/ticket/(?P[0-9]+)/linked_item', viewset = ticket_linked_item.ViewSet, basename = '_api_v2_ticket_linked_item' ) router.register( - prefix = 'ticket/(?P[0-9]+)/related_ticket', viewset = related_ticket.ViewSet, + prefix = '/ticket/(?P[0-9]+)/related_ticket', viewset = related_ticket.ViewSet, basename = '_api_v2_ticket_related' ) router.register( - prefix=f'ticket/(?P[0-9]+)/(?P[{ticket_comment_names}]+)', + prefix=f'/ticket/(?P[0-9]+)/(?P[{ticket_comment_names}]+)', viewset = ticket_comment.ViewSet, feature_flag = '2025-00006', basename = '_api_ticket_comment_base_sub' ) router.register( - prefix=f'ticket/(?P[0-9]+)/(?P[{ticket_comment_names} \ + prefix=f'/ticket/(?P[0-9]+)/(?P[{ticket_comment_names} \ ]+)/(?P[0-9]+)/threads', viewset = ticket_comment.ViewSet, feature_flag = '2025-00006', basename = '_api_ticket_comment_base_sub_thread' ) router.register( - prefix = '(?P[a-z_]+)/(?P[0-9]+)/item_ticket', + prefix = '/(?P[a-z_]+)/(?P[0-9]+)/item_ticket', viewset = ticket_linked_item.ViewSet, basename = '_api_v2_item_tickets' ) diff --git a/app/devops/urls.py b/app/devops/urls.py index c24962ac..15d5e991 100644 --- a/app/devops/urls.py +++ b/app/devops/urls.py @@ -13,21 +13,21 @@ app_name = "devops" router = DefaultRouter(trailing_slash=False) router.register( - prefix = 'feature_flag', viewset = feature_flag.ViewSet, + prefix = '/feature_flag', viewset = feature_flag.ViewSet, basename = '_api_featureflag' ) router.register( - prefix = r'git_repository(?:/(?Pgitlab|github))?', + prefix = r'/git_repository(?:/(?Pgitlab|github))?', viewset = git_repository.ViewSet, feature_flag = '2025-00001', basename = '_api_gitrepository' ) router.register( - prefix = r'(?Pgithubrepository|gitlabrepository)', + prefix = r'/(?Pgithubrepository|gitlabrepository)', viewset = git_repository.ViewSet, feature_flag = '2025-00001', basename = '_api_gitrepository_sub' ) router.register( - prefix = 'git_group', viewset = git_group.ViewSet, + prefix = '/git_group', viewset = git_group.ViewSet, feature_flag = '2025-00001', basename = '_api_gitgroup' ) diff --git a/app/devops/urls_public.py b/app/devops/urls_public.py index 08f68964..3b458aa1 100644 --- a/app/devops/urls_public.py +++ b/app/devops/urls_public.py @@ -11,8 +11,8 @@ app_name = "devops" router = SimpleRouter(trailing_slash=False) -router.register('flags', feature_flag_endpoints.Index, basename='_api_v2_flags') +router.register('/flags', feature_flag_endpoints.Index, basename='_api_v2_flags') -router.register('(?P[0-9]+)/flags/(?P[0-9]+)', public_feature_flag.ViewSet, basename='_api_checkin') +router.register('/(?P[0-9]+)/flags/(?P[0-9]+)', public_feature_flag.ViewSet, basename='_api_checkin') urlpatterns = router.urls diff --git a/app/itam/urls_api.py b/app/itam/urls_api.py index 46760edd..ffbcdd56 100644 --- a/app/itam/urls_api.py +++ b/app/itam/urls_api.py @@ -34,57 +34,57 @@ router.register( basename = '_api_v2_itam_home' ) router.register( - prefix = '(?P[itamassetbase]+)', viewset = asset.ViewSet, + prefix = '/(?P[itamassetbase]+)', viewset = asset.ViewSet, feature_flag = '2025-00007', basename = '_api_itamassetbase' ) router.register( - prefix = 'device', viewset = device.ViewSet, + prefix = '/device', viewset = device.ViewSet, basename = '_api_device' ) router.register( - prefix = 'device/(?P[0-9]+)/operating_system', + prefix = '/device/(?P[0-9]+)/operating_system', viewset = device_operating_system.ViewSet, basename = '_api_deviceoperatingsystem') router.register( - prefix = 'device/(?P[0-9]+)/software', viewset = device_software_v2.ViewSet, + prefix = '/device/(?P[0-9]+)/software', viewset = device_software_v2.ViewSet, basename = '_api_devicesoftware' ) router.register( - prefix = 'device/(?P[0-9]+)/service', viewset = service_device_v2.ViewSet, + prefix = '/device/(?P[0-9]+)/service', viewset = service_device_v2.ViewSet, basename = '_api_v2_service_device' ) router.register( - prefix = 'inventory', viewset = inventory.ViewSet, + prefix = '/inventory', viewset = inventory.ViewSet, basename = '_api_v2_inventory' ) router.register( - prefix = 'operating_system', viewset = operating_system_v2.ViewSet, + prefix = '/operating_system', viewset = operating_system_v2.ViewSet, basename = '_api_operatingsystem' ) router.register( - prefix = 'operating_system/(?P[0-9]+)/installs', + prefix = '/operating_system/(?P[0-9]+)/installs', viewset = device_operating_system.ViewSet, basename = '_api_v2_operating_system_installs' ) router.register( - prefix = 'operating_system/(?P[0-9]+)/version', + prefix = '/operating_system/(?P[0-9]+)/version', viewset = operating_system_version_v2.ViewSet, basename = '_api_operatingsystemversion' ) router.register( - prefix = 'software', viewset = software_v2.ViewSet, + prefix = '/software', viewset = software_v2.ViewSet, basename = '_api_software' ) router.register( - prefix = 'software/(?P[0-9]+)/installs', viewset = device_software_v2.ViewSet, + prefix = '/software/(?P[0-9]+)/installs', viewset = device_software_v2.ViewSet, basename = '_api_v2_software_installs' ) router.register( - prefix = 'software/(?P[0-9]+)/version', viewset = software_version_v2.ViewSet, + prefix = '/software/(?P[0-9]+)/version', viewset = software_version_v2.ViewSet, basename = '_api_softwareversion' ) router.register( - prefix = 'software/(?P[0-9]+)/feature_flag', + prefix = '/software/(?P[0-9]+)/feature_flag', viewset = software_enable_feature_flag.ViewSet, basename = '_api_softwareenablefeatureflag' ) diff --git a/app/itim/urls_api.py b/app/itim/urls_api.py index dd92e9d5..935d36d8 100644 --- a/app/itim/urls_api.py +++ b/app/itim/urls_api.py @@ -23,27 +23,27 @@ router.register( basename = '_api_v2_itim_home' ) router.register( - prefix = 'ticket/change', viewset = change.ViewSet, + prefix = '/ticket/change', viewset = change.ViewSet, basename = '_api_v2_ticket_change' ) router.register( - prefix = 'cluster', viewset = cluster_v2.ViewSet, + prefix = '/cluster', viewset = cluster_v2.ViewSet, basename = '_api_cluster' ) router.register( - prefix = 'cluster/(?P[0-9]+)/service', viewset = service_cluster.ViewSet, + prefix = '/cluster/(?P[0-9]+)/service', viewset = service_cluster.ViewSet, basename = '_api_v2_service_cluster' ) router.register( - prefix = 'ticket/incident', viewset = incident.ViewSet, + prefix = '/ticket/incident', viewset = incident.ViewSet, basename = '_api_v2_ticket_incident' ) router.register( - prefix = 'ticket/problem', viewset = problem.ViewSet, + prefix = '/ticket/problem', viewset = problem.ViewSet, basename = '_api_v2_ticket_problem' ) router.register( - prefix = 'service', viewset = service.ViewSet, + prefix = '/service', viewset = service.ViewSet, basename = '_api_service' ) diff --git a/app/project_management/urls_api.py b/app/project_management/urls_api.py index 6209e2ab..10bae4e5 100644 --- a/app/project_management/urls_api.py +++ b/app/project_management/urls_api.py @@ -20,16 +20,16 @@ router.register( basename = '_api_v2_project_management_home' ) router.register( - prefix = 'project', viewset = project.ViewSet, + prefix = '/project', viewset = project.ViewSet, basename = '_api_project' ) router.register( - prefix = 'project/(?P[0-9]+)/milestone', + prefix = '/project/(?P[0-9]+)/milestone', viewset = project_milestone.ViewSet, basename = '_api_projectmilestone' ) router.register( - prefix = 'project/(?P[0-9]+)/project_task', + prefix = '/project/(?P[0-9]+)/project_task', viewset = project_task.ViewSet, basename = '_api_v2_ticket_project_task' ) diff --git a/app/settings/urls_api.py b/app/settings/urls_api.py index 3333050f..9ddedc46 100644 --- a/app/settings/urls_api.py +++ b/app/settings/urls_api.py @@ -52,70 +52,70 @@ router.register( basename = '_api_v2_settings_home' ) router.register( - prefix = 'app_settings', viewset = app_settings.ViewSet, + prefix = '/app_settings', viewset = app_settings.ViewSet, basename = '_api_appsettings' ) router.register( - prefix = 'celery_log', viewset = celery_log_v2.ViewSet, + prefix = '/celery_log', viewset = celery_log_v2.ViewSet, basename = '_api_v2_celery_log' ) router.register( - prefix = 'cluster_type', viewset = cluster_type_v2.ViewSet, + prefix = '/cluster_type', viewset = cluster_type_v2.ViewSet, basename = '_api_clustertype' ) router.register( - prefix = 'device_model', viewset = device_model.ViewSet, + prefix = '/device_model', viewset = device_model.ViewSet, basename = '_api_devicemodel' ) router.register( - prefix = 'device_type', viewset = device_type.ViewSet, + prefix = '/device_type', viewset = device_type.ViewSet, basename = '_api_devicetype' ) router.register( - prefix = 'external_link', viewset = external_link.ViewSet, + prefix = '/external_link', viewset = external_link.ViewSet, basename = '_api_externallink' ) router.register( - prefix = 'knowledge_base_category', + prefix = '/knowledge_base_category', viewset = knowledge_base_category_v2.ViewSet, basename = '_api_knowledgebasecategory' ) router.register( - prefix = 'manufacturer', viewset = manufacturer_v2.ViewSet, + prefix = '/manufacturer', viewset = manufacturer_v2.ViewSet, basename = '_api_manufacturer' ) router.register( - prefix = 'port', viewset = port_v2.ViewSet, + prefix = '/port', viewset = port_v2.ViewSet, basename = '_api_port' ) router.register( - prefix = 'project_state', + prefix = '/project_state', viewset = project_state.ViewSet, basename = '_api_projectstate' ) router.register( - prefix = 'project_type', viewset = project_type.ViewSet, + prefix = '/project_type', viewset = project_type.ViewSet, basename = '_api_projecttype' ) router.register( - prefix = 'software_category', viewset = software_category_v2.ViewSet, + prefix = '/software_category', viewset = software_category_v2.ViewSet, basename = '_api_softwarecategory' ) router.register( - prefix = 'ticket_category', + prefix = '/ticket_category', viewset = ticket_category.ViewSet, basename = '_api_ticketcategory' ) router.register( - prefix = 'ticket_comment_category', + prefix = '/ticket_comment_category', viewset = ticket_comment_category.ViewSet, basename = '_api_ticketcommentcategory' ) router.register( - prefix = 'user_settings', viewset = user_settings.ViewSet, + prefix = '/user_settings', viewset = user_settings.ViewSet, basename = '_api_usersettings' ) router.register( - prefix = 'user_(?P[0-9]+)/token', viewset = auth_token.ViewSet, + prefix = '/user_(?P[0-9]+)/token', viewset = auth_token.ViewSet, basename = '_api_authtoken' ) From 037fbabeae2824a5f849b966019eb44816c5cb2d Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 12 Aug 2025 12:51:16 +0930 Subject: [PATCH 04/13] refactor(docker): when l;aunching gunicorn create a pid file ref: #947 --- includes/etc/supervisor/conf.source/gunicorn.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/etc/supervisor/conf.source/gunicorn.conf b/includes/etc/supervisor/conf.source/gunicorn.conf index 73bc4ab0..882e2602 100644 --- a/includes/etc/supervisor/conf.source/gunicorn.conf +++ b/includes/etc/supervisor/conf.source/gunicorn.conf @@ -7,4 +7,4 @@ autorestart=true stdout_logfile=/var/log/%(program_name)s.log stderr_logfile=/var/log/%(program_name)s.log directory=/app -command=gunicorn --config=/etc/gunicorn.conf.py centurion.wsgi:application +command=gunicorn --config=/etc/gunicorn.conf.py --pid=/run/gunicorn.pid centurion.wsgi:application From e1f8ba0c2ba4d1487ee591fa0991d62dc464b7d9 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 12 Aug 2025 12:52:44 +0930 Subject: [PATCH 05/13] chore(docker): Add optional coverage to gunicorn ref: #947 #152 --- includes/entrypoint.sh | 1 + includes/etc/gunicorn.conf.py | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/includes/entrypoint.sh b/includes/entrypoint.sh index 1d792ae3..820308f2 100755 --- a/includes/entrypoint.sh +++ b/includes/entrypoint.sh @@ -3,6 +3,7 @@ set -e mkdir -p /etc/supervisor/conf.d; +mkdir -p /var/log/nginx; if [ "$1" == "" ]; then diff --git a/includes/etc/gunicorn.conf.py b/includes/etc/gunicorn.conf.py index 2b8916a8..04a819fb 100644 --- a/includes/etc/gunicorn.conf.py +++ b/includes/etc/gunicorn.conf.py @@ -1,3 +1,4 @@ +import coverage import logging import os @@ -9,6 +10,32 @@ from prometheus_client import multiprocess, start_http_server, REGISTRY +if bool(os.environ.get("IS_TESTING")): + + def post_fork(server, worker): + + worker_cov = coverage.Coverage(data_file=f"artifacts/.coverage.{os.getpid()}") + + worker_cov.start() + + worker.worker_cov = worker_cov + + + def worker_exit(server, worker): + + if hasattr(worker, "worker_cov"): + + worker.worker_cov.stop() + + worker.worker_cov.save() + + + post_fork = post_fork + + worker_exit = worker_exit + + + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'centurion.settings') access_logfile = '-' From ecc16e6cbf10e1c09614b2b8c139a90c64d044e2 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 12 Aug 2025 13:01:58 +0930 Subject: [PATCH 06/13] test(docker): Add compose setup for integration testing ref: #947 #152 #774 --- .gitignore | 2 + makefile | 64 ++++++++++++++++++++++++++ test/docker-compose.yaml | 89 +++++++++++++++++++++++++++++++++++++ test/docker/centurion.sql | 2 + test/docker/settings.py | 79 ++++++++++++++++++++++++++++++++ test/page_speed.js | 0 test/parameterizedData.json | 0 test/setup-integration.sh | 70 +++++++++++++++++++++++++++++ 8 files changed, 306 insertions(+) create mode 100644 test/docker-compose.yaml create mode 100755 test/docker/centurion.sql create mode 100755 test/docker/settings.py mode change 100644 => 100755 test/page_speed.js mode change 100644 => 100755 test/parameterizedData.json create mode 100755 test/setup-integration.sh diff --git a/.gitignore b/.gitignore index 40a278c8..5b75fc07 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ log/ # Integration testing app/artifacts/ app/pyproject.toml +app/histogram_** +app/counter_** diff --git a/makefile b/makefile index 7f780aaa..cf3cebc8 100644 --- a/makefile +++ b/makefile @@ -69,6 +69,70 @@ lint: markdown-mkdocs-lint test: pytest --cov-report xml:artifacts/coverage_unit_functional.xml --cov-report html:artifacts/coverage/unit_functional/ --junit-xml=artifacts/unit_functional.JUnit.xml app/**/tests/unit app/**/tests/functional + + +test-integration: + export exit_code=0; + cp pyproject.toml app/; + sed -i 's|^source = \[ "./app" \]|source = [ "." ]|' app/pyproject.toml; + cd test; + if docker-compose up -d; then + + docker ps -a; + + chmod +x setup-integration.sh; + + if ./setup-integration.sh; then + + cd ..; + + ls -laR test/; + + docker exec -i centurion-erp supervisorctl stop gunicorn; + docker exec -i centurion-erp sh -c 'rm -rf /app/artifacts/* /app/artifacts/.[!.]*'; + docker exec -i centurion-erp supervisorctl start gunicorn; + sleep 30; + docker ps -a; + curl --trace-ascii - http://localhost:8003/api; + echo '--------------------------------------------------------------------'; + curl --trace-ascii - http://127.0.0.1:8003/api; + + + if [ "0${GITHUB_SHA}"!="0" ]; then + + sudo chmod 777 -R ./test + + fi; + + docker logs centurion-erp; + pytest --override-ini addopts= --no-migrations --tb=long --verbosity=2 --full-trace --showlocals --junit-xml=integration.JUnit.xml app/*/tests/integration; + docker exec -i centurion-erp supervisorctl restart gunicorn; + docker exec -i centurion-erp sh -c 'coverage combine; coverage report --skip-covered; coverage html -d artifacts/html/;'; + docker logs centurion-erp-init > ./test/volumes/log/docker-log-centurion-erp-init.log; + docker logs centurion-erp> ./test/volumes/log/docker-log-centurion-erp.log; + docker logs postgres > ./test/volumes/log/docker-log-postgres.log; + docker logs rabbitmq > ./test/volumes/log/docker-log-rabbitmq.log; + cd test; + + else + + echo 'Error: could not setup containers for testing'; + export exit_code=10; + + fi; + else + + echo 'Error: Failed to launch containers'; + export exit_code=20; + + fi; + cd test; + docker-compose down -v; + cd ..; + exit ${exit_code}; + + + test-functional: pytest --cov-report xml:artifacts/coverage_functional.xml --cov-report html:artifacts/coverage/functional/ --junit-xml=artifacts/functional.JUnit.xml app/**/tests/functional diff --git a/test/docker-compose.yaml b/test/docker-compose.yaml new file mode 100644 index 00000000..b078c969 --- /dev/null +++ b/test/docker-compose.yaml @@ -0,0 +1,89 @@ +--- + +x-app: ¢urion + image: ${CENTURION_IMAGE:-ghcr.io/nofusscomputing/centurion-erp}:${CENTURION_IMAGE_TAG:-dev} + volumes: + - ./docker/settings.py:/etc/itsm/settings.py:ro + + +services: + + + postgres: + image: ${CENTURION_POSTGRES_IMAGE:-postgres}:${CENTURION_POSTGRES_IMAGE_TAG:-13.21}-alpine # 14.18-alpine, 15.13-alpine, 16.9-alpine, 17.5-alpine + container_name: postgres + restart: always + environment: + POSTGRES_USER: admin + POSTGRES_PASSWORD: admin + expose: + - 5432 + volumes: + - ./docker/centurion.sql:/docker-entrypoint-initdb.d/centurion.sql:ro + + + rabbitmq: + image: ${CENTURION_RABBITMQ_IMAGE:-rabbitmq}:${CENTURION_RABBITMQ_IMAGE_TAG:-4.0.9}-management-alpine # 4.1.3-management-alpine + container_name: rabbitmq + environment: + - RABBITMQ_DEFAULT_USER=admin + - RABBITMQ_DEFAULT_PASS=admin + - RABBITMQ_DEFAULT_VHOST=itsm + expose: + - 5672 + ports: + # - "5672:5672" + - "15672:15672" + + + centurion-init: + <<: *centurion + container_name: centurion-erp-init + restart: "no" + entrypoint: "" + command: sh -c 'sleep 15; python manage.py migrate' + depends_on: + - postgres + - rabbitmq + + + centurion: + <<: *centurion + container_name: centurion-erp + restart: always + hostname: centurion-erp + volumes: + - ./volumes/log:/var/log:rw + - ./docker/settings.py:/etc/itsm/settings.py:ro + - ./volumes/data:/data:rw + - ./volumes/artifacts:/app/artifacts:rw + ports: + - "8003:8000" + depends_on: + - postgres + - rabbitmq + + + worker: + <<: *centurion + container_name: centurion-worker + restart: always + environment: + - IS_WORKER=true + hostname: centurion-worker + depends_on: + - postgres + - rabbitmq + - centurion + + centurion-ui: + image: ${CENTURION_UI_IMAGE:-ghcr.io/nofusscomputing/centurion-erp-ui}:${CENTURION_UI_IMAGE_TAG:-dev} + container_name: centurion-ui + restart: always + environment: + - API_URL=http://127.0.0.1:8003/api/v2 + hostname: centurion-ui + ports: + - "3000:80" + depends_on: + - centurion diff --git a/test/docker/centurion.sql b/test/docker/centurion.sql new file mode 100755 index 00000000..0caab97c --- /dev/null +++ b/test/docker/centurion.sql @@ -0,0 +1,2 @@ +CREATE DATABASE itsm; +GRANT ALL PRIVILEGES ON DATABASE itsm TO admin; diff --git a/test/docker/settings.py b/test/docker/settings.py new file mode 100755 index 00000000..cbaaa5f8 --- /dev/null +++ b/test/docker/settings.py @@ -0,0 +1,79 @@ +# ITSM Docker Settings + +# If metrics enabled, see https://nofusscomputing.com/projects/centurion_erp/administration/monitoring/#django-exporter-setup) +# to configure the database metrics. + +API_TEST = True + +AUTH_PASSWORD_VALIDATORS = [] + +CELERY_BROKER_URL = 'amqp://admin:admin@rabbitmq:5672/itsm' # 'amqp://' is the connection protocol + +CORS_ALLOW_CREDENTIALS = True + +CORS_ALLOW_METHODS = ( + "DELETE", + "GET", + "OPTIONS", + "PATCH", + "POST", + "PUT", +) + +CORS_ALLOWED_ORIGINS = [ + "http://127.0.0.1:3000", + "http://localhost:3000", + "http://127.0.0.1:8003", + "http://localhost:8003", + "http://127.0.0.1", +] + +CORS_EXPOSE_HEADERS = ['Content-Type', 'X-CSRFToken'] + +CSRF_COOKIE_SECURE = False + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'itsm', + 'USER': 'admin', + 'PASSWORD': 'admin', + 'HOST': 'postgres', + 'PORT': '5432', + } +} + +DEBUG = True + +FEATURE_FLAGGING_ENABLED = True # Turn Feature Flagging on/off + +FEATURE_FLAG_OVERRIDES = [] # Feature Flag Overrides. Takes preceedence over downloaded feature flags. + +LOG_FILES = { # Location where log files will be created + "centurion": "/var/log/centurion.log", + "weblog": "/var/log/weblog.log", + "rest_api": "/var/log/rest_api.log", + "catch_all":"/var/log/catch-all.log" +} + +METRICS_ENABLED = True + +SECRET_KEY = 'django-insecure-b*41-$afq0yl)1e#qpz^-nbt-opvjwb#avv++b9rfdxa@b55sk' + +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") + +SECURE_SSL_REDIRECT = False + +SESSION_COOKIE_SECURE = False + +SITE_URL = 'http://127.0.0.1:8003' + +TRUSTED_ORIGINS = [ + "http://127.0.0.1:3000", + "http://localhost:3000", + "http://127.0.0.1:8003", + "http://localhost:8003", + "http://127.0.0.1", +] + +USE_X_FORWARDED_HOST = True diff --git a/test/page_speed.js b/test/page_speed.js old mode 100644 new mode 100755 diff --git a/test/parameterizedData.json b/test/parameterizedData.json old mode 100644 new mode 100755 diff --git a/test/setup-integration.sh b/test/setup-integration.sh new file mode 100755 index 00000000..d1ccbf39 --- /dev/null +++ b/test/setup-integration.sh @@ -0,0 +1,70 @@ +#!/bin/sh + +set -e + +docker exec -i centurion-erp pip install -r /requirements_test.txt + +docker exec -i centurion-erp supervisorctl restart gunicorn + + +CONTAINER_NAME="centurion-erp-init" +TIMEOUT=400 +INTERVAL=5 +ELAPSED=0 +STATUS="" + +while [ "$STATUS" != "exited" ] && [ "$STATUS" != "dead" ]; do + + STATUS=$(docker inspect --format '{{.State.Status}}' "$CONTAINER_NAME" 2>/dev/null || echo "not_found") + + + if [ "$STATUS" = "not_found" ]; then + docker ps -a + echo "Container $CONTAINER_NAME was not found." + exit 2 + fi + + if [ $ELAPSED -ge $TIMEOUT ]; then + echo "Timeout reached. Container $CONTAINER_NAME still running (status: $STATUS)." + exit 3 + fi + + echo "Waiting for container $CONTAINER_NAME to complete... Current status: $STATUS" + sleep $INTERVAL + ELAPSED=$((ELAPSED + INTERVAL)) +done + +echo "Container $CONTAINER_NAME has completed." + + +CONTAINER_NAME="centurion-erp" +TIMEOUT=90 +INTERVAL=5 +ELAPSED=0 +STATUS="" + +while [ "$STATUS" != "healthy" ]; do + STATUS=$(docker inspect --format '{{.State.Health.Status}}' "$CONTAINER_NAME" 2>/dev/null || echo "none") + + if [ $ELAPSED -ge $TIMEOUT ]; then + echo "Timeout reached. Container $CONTAINER_NAME is not healthy." + exit 4 + fi + + echo "Waiting for container $CONTAINER_NAME to be healthy... Current status: $STATUS" + sleep $INTERVAL + ELAPSED=$((ELAPSED + INTERVAL)) +done + +docker exec -i centurion-erp python manage.py createsuperuser --username admin --email admin@localhost --noinput + +docker exec -i centurion-erp apk add expect + +docker exec -i centurion-erp expect -c " + spawn python manage.py changepassword admin + expect \"Password:\" + send \"admin\r\" + expect \"Password (again):\" + send \"admin\r\" + expect eof + " From 525da2fbe0a2dab5613a682c1ec013911f2ea8c1 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 12 Aug 2025 13:02:49 +0930 Subject: [PATCH 07/13] refactor(docker): update healthcheck interval=10s and start-period=30s ref: #947 #152 #774 --- dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dockerfile b/dockerfile index eb113034..7df45690 100644 --- a/dockerfile +++ b/dockerfile @@ -159,7 +159,7 @@ EXPOSE 8000 VOLUME [ "/data", "/etc/itsm" ] -HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD \ +HEALTHCHECK --interval=10s --timeout=30s --start-period=30s --retries=3 CMD \ supervisorctl status || exit 1 From bc9d6b74fd367936970487315d54a078b57a6c53 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 12 Aug 2025 13:03:45 +0930 Subject: [PATCH 08/13] feat(docker): Adjust gunicorn works=4 100reqs/max and preload app ref: #947 #152 #774 --- includes/etc/gunicorn.conf.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/includes/etc/gunicorn.conf.py b/includes/etc/gunicorn.conf.py index 04a819fb..a49b2308 100644 --- a/includes/etc/gunicorn.conf.py +++ b/includes/etc/gunicorn.conf.py @@ -48,9 +48,14 @@ forwarder_headers = "X-REAL-IP,X-FORWARDED-FOR,X-FORWARDED-PROTO" logger = logging.getLogger(__name__) -preload_app = False +max_requests = 100 +max_requests_jitter = 30 -workers = 10 +preload_app = True + +timeout = 180 + +workers = 4 def when_ready(_): @@ -73,7 +78,8 @@ def when_ready(_): proc_path = os.environ["PROMETHEUS_MULTIPROC_DIR"] - logger.info(f'Setting up prometheus metrics HTTP server on port {str(settings.METRICS_EXPORT_PORT)}.') + logger.info(f'Setting up prometheus metrics HTTP server on port \ + {str(settings.METRICS_EXPORT_PORT)}.') multiproc_folder_path = _setup_multiproc_folder() From 089480620e8e7e045d3907dadd99d4a7c9ccaef4 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 12 Aug 2025 13:15:15 +0930 Subject: [PATCH 09/13] test: Add initial integration tests ref: #947 #152 closes #774 --- .github/workflows/ci.yaml | 11 ++ .../integration/test_integration_list_urls.py | 182 ++++++++++++++++++ pyproject.toml | 1 + 3 files changed, 194 insertions(+) create mode 100644 app/centurion/tests/integration/test_integration_list_urls.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d8ac6fad..3666e8a9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -46,6 +46,17 @@ jobs: WORKFLOW_TOKEN: ${{ secrets.WORKFLOW_TOKEN }} + integration-test: + name: 'Integration Test' + uses: nofusscomputing/action_python/.github/workflows/python-integration.yaml@development + needs: + - docker + with: + PYTHON_VERSION: '3.11' + secrets: + WORKFLOW_TOKEN: ${{ secrets.WORKFLOW_TOKEN }} + + gitlab-mirror: if: ${{ github.repository == 'nofusscomputing/centurion_erp' }} runs-on: ubuntu-latest diff --git a/app/centurion/tests/integration/test_integration_list_urls.py b/app/centurion/tests/integration/test_integration_list_urls.py new file mode 100644 index 00000000..5cea20b2 --- /dev/null +++ b/app/centurion/tests/integration/test_integration_list_urls.py @@ -0,0 +1,182 @@ + +import pytest +import re +import requests + +from django.urls import get_resolver, URLPattern, URLResolver + + + +def list_urls(urlpatterns, parent_pattern=''): + + urls = [] + + for entry in urlpatterns: + + if isinstance(entry, URLPattern): + urls.append(parent_pattern + str(entry.pattern)) + + elif isinstance(entry, URLResolver): + urls.extend(list_urls(entry.url_patterns, parent_pattern + str(entry.pattern))) + + filtered = [ + re.sub(r"\^([a-z\-]+)\$$", r"\1", u).rstrip('/') for u in urls if ( + re.sub(r"\^([a-z\-]+)\$$", r"\1", u).startswith('api/') + and '(' not in re.sub(r"\^([a-z\-]+)\$$", r"\1", u).rstrip('/') + and '<' not in re.sub(r"\^([a-z\-]+)\$$", r"\1", u).rstrip('/') + and '$' not in re.sub(r"\^([a-z\-]+)\$$", r"\1", u).rstrip('/') + ) + ] + + return filtered + + +no_auth_urls = [ + 'api/v2/auth/login', + 'api/v2/docs', + 'api/v2/schema', +] + +urls_list_view_auth_required_excluded = [ + 'api/v2/auth/logout', + +] + +urls_list_view_auth_required_authenticated_excluded = [ + 'api/v2/itam/inventory', + 'api/v2/auth/logout', +] + +@pytest.mark.integration +@pytest.mark.regression +class URLChecksPyTest: + + + + @pytest.fixture(scope="class") + def auto_login_client(self): + session = requests.Session() + + login_page_url = "http://127.0.0.1:8003/api/v2/auth/login" + login_post_url = "http://127.0.0.1:8003/api/v2/auth/login" + + resp = session.get(login_page_url) + resp.raise_for_status() + # Extract CSRF token from cookies (Django sets csrftoken cookie) + csrf_token = session.cookies.get("csrftoken") + if not csrf_token: + raise RuntimeError("CSRF token cookie not found") + + login_data = { + "username": "admin", + "password": "admin", + "csrfmiddlewaretoken": csrf_token, + } + + headers = { + "Referer": login_page_url, + "X-CSRFToken": csrf_token, # Include CSRF token header + } + + resp = session.post(login_post_url, data=login_data, headers=headers, allow_redirects=True) + resp.raise_for_status() + + + class Client: + def __init__(self, session): + self._session = session + + self._unauth_session = requests.Session() + + resp = self._unauth_session.get(login_page_url) + resp.raise_for_status() + self._headers = csrf_token = { + "Referer": login_page_url, + "X-CSRFToken": self._unauth_session.cookies.get("csrftoken"), + } + + def request(self, method, url, auth = False, **kwargs): + + if auth: + session = self._session + else: + session = self._unauth_session + + return session.request(method, url, headers=self._headers, **kwargs) + + @property + def cookies(self): + return self._session.cookies + + return Client(session) + + + list_view_urls = list_urls(urlpatterns = get_resolver().url_patterns) + + + + @pytest.mark.parametrize( + argnames = "url_path", + argvalues = [ + url for url in list_view_urls if( url in no_auth_urls ) + ], + ids = [ + re.sub(r'[^\w_\-.:]', '_', url) for url in list_view_urls if( url in no_auth_urls ) + ], + ) + def test_urls_no_auth_required(self, url_path, auto_login_client): + url = f"http://127.0.0.1:8003/{url_path}" + + response = auto_login_client.request("GET", url) + + assert response.status_code == 200 + + + + @pytest.mark.permissions + @pytest.mark.parametrize( + argnames = "url_path", + argvalues = [ + url for url in list_view_urls if( + url not in no_auth_urls + and url not in urls_list_view_auth_required_excluded + ) + ], + ids = [ + re.sub(r'[^\w_\-.:]', '_', url) for url in list_view_urls if( + url not in no_auth_urls + and url not in urls_list_view_auth_required_excluded + ) + ], + ) + def test_urls_list_view_auth_required(self, url_path, auto_login_client): + url = f"http://127.0.0.1:8003/{url_path}" + + response = auto_login_client.request("GET", url) + + assert response.status_code == 401 + + + + @pytest.mark.permissions + @pytest.mark.parametrize( + argnames = "url_path", + argvalues = [ + url for url in list_view_urls if( + url not in no_auth_urls + and url not in urls_list_view_auth_required_authenticated_excluded + ) + ], + ids = [ + re.sub(r'[^\w_\-.:]', '_', url) for url in list_view_urls if( + url not in no_auth_urls + and url not in urls_list_view_auth_required_authenticated_excluded + ) + ], + ) + def test_urls_list_view_auth_required_authenticated(self, url_path, auto_login_client): + url = f"http://127.0.0.1:8003/{url_path}" + + response = auto_login_client.request(method = "GET", url = url, auth = True) + + assert response.status_code == 200 diff --git a/pyproject.toml b/pyproject.toml index e72db919..b2c51715 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1100,6 +1100,7 @@ markers = [ "audit_models: Selects Audit models.", "centurion_models: Selects Centurion models", "functional: Selects all Functional tests.", + "integration: Selects all Integration tests.", "meta_models: Selects Meta models", "mixin: Selects all mixin test cases.", "mixin_centurion: Selects all centurion mixin test cases.", From b32d7f302e6dcaf31e0b4950e9513b21f97abc90 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 12 Aug 2025 13:56:06 +0930 Subject: [PATCH 10/13] chore(tests): conduct propper'er cleanup of project objs ref: #947 --- .../test_unit_project_milestone_serializer.py | 2 +- app/tests/fixtures/model_projectmilestone.py | 8 ++++++-- app/tests/fixtures/model_projectstate.py | 9 +++++---- app/tests/fixtures/model_projecttype.py | 9 +++++---- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/app/project_management/tests/unit/project_milestone/test_unit_project_milestone_serializer.py b/app/project_management/tests/unit/project_milestone/test_unit_project_milestone_serializer.py index 82f1da29..dce0d12b 100644 --- a/app/project_management/tests/unit/project_milestone/test_unit_project_milestone_serializer.py +++ b/app/project_management/tests/unit/project_milestone/test_unit_project_milestone_serializer.py @@ -64,7 +64,7 @@ class ProjectMilestoneSerializerTestCases( def test_serializer_validation_no_name(self, - kwargs_api_create, model, model_serializer, request_user + kwargs_api_create, model, model_serializer, request_user, model_kwargs ): """Serializer Validation Check diff --git a/app/tests/fixtures/model_projectmilestone.py b/app/tests/fixtures/model_projectmilestone.py index 4fe77234..f5cc6558 100644 --- a/app/tests/fixtures/model_projectmilestone.py +++ b/app/tests/fixtures/model_projectmilestone.py @@ -75,9 +75,13 @@ def kwargs_projectmilestone(django_db_blocker, yield kwargs.copy() - # with django_db_blocker.unblock(): + with django_db_blocker.unblock(): + + for proj in project.projectmilestone_set.all(): + proj.delete() + + project.delete() - # project.delete() # milestone is cascade delete @pytest.fixture( scope = 'class') diff --git a/app/tests/fixtures/model_projectstate.py b/app/tests/fixtures/model_projectstate.py index 671dfe6b..a864ae90 100644 --- a/app/tests/fixtures/model_projectstate.py +++ b/app/tests/fixtures/model_projectstate.py @@ -48,10 +48,11 @@ def kwargs_projectstate(kwargs_centurionmodel, django_db_blocker, with django_db_blocker.unblock(): - try: - runbook.delete() - except models.deletion.ProtectedError: - pass + for proj in runbook.projectstate_set.all(): + proj.delete() + + runbook.delete() + @pytest.fixture( scope = 'class') diff --git a/app/tests/fixtures/model_projecttype.py b/app/tests/fixtures/model_projecttype.py index 8f130e01..3841ccaf 100644 --- a/app/tests/fixtures/model_projecttype.py +++ b/app/tests/fixtures/model_projecttype.py @@ -47,10 +47,11 @@ def kwargs_projecttype(kwargs_centurionmodel, django_db_blocker, with django_db_blocker.unblock(): - try: - runbook.delete() - except models.deletion.ProtectedError: - pass + for proj in runbook.projecttype_set.all(): + proj.delete() + + runbook.delete() + @pytest.fixture( scope = 'class') From 8d6d1d258d365d9009f600a25058cd3b296fb167 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 12 Aug 2025 13:57:14 +0930 Subject: [PATCH 11/13] ci(python): add app versions to inputs ref: #947 #152 --- .github/workflows/ci.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3666e8a9..1c1ae76b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -52,7 +52,9 @@ jobs: needs: - docker with: + POSTGRES_VERSIONS: "[ '13', '14', '15', '16', '17' ]" PYTHON_VERSION: '3.11' + RABBITMQ_VERSIONS: "[ '3.12', '3.13', '4.0', '4.1' ]" secrets: WORKFLOW_TOKEN: ${{ secrets.WORKFLOW_TOKEN }} From 190e4b4a98c313788769b0d3f9170a5aac28e278 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 12 Aug 2025 14:23:26 +0930 Subject: [PATCH 12/13] ci(integration): Increase wait time after gunicorn restart to 60secs ref: #947 #152 --- makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/makefile b/makefile index cf3cebc8..6a7f9cdb 100644 --- a/makefile +++ b/makefile @@ -91,7 +91,7 @@ test-integration: docker exec -i centurion-erp supervisorctl stop gunicorn; docker exec -i centurion-erp sh -c 'rm -rf /app/artifacts/* /app/artifacts/.[!.]*'; docker exec -i centurion-erp supervisorctl start gunicorn; - sleep 30; + sleep 60; docker ps -a; curl --trace-ascii - http://localhost:8003/api; echo '--------------------------------------------------------------------'; From 89970dc4e28bc60ca570cad8309cd092419c27e7 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 12 Aug 2025 16:15:44 +0930 Subject: [PATCH 13/13] docs: Add tested version badges ref: #947 #152 --- .github/workflows/ci.yaml | 4 ++-- README.md | 2 ++ docs/projects/centurion_erp/index.md | 7 +++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1c1ae76b..6e5e55d8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -52,9 +52,9 @@ jobs: needs: - docker with: - POSTGRES_VERSIONS: "[ '13', '14', '15', '16', '17' ]" + POSTGRES_VERSIONS: '[ "13", "14", "15", "16", "17" ]' PYTHON_VERSION: '3.11' - RABBITMQ_VERSIONS: "[ '3.12', '3.13', '4.0', '4.1' ]" + RABBITMQ_VERSIONS: '[ "3.12", "3.13", "4.0", "4.1" ]' secrets: WORKFLOW_TOKEN: ${{ secrets.WORKFLOW_TOKEN }} diff --git a/README.md b/README.md index 532c29f0..0fb6432e 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ [![Docker Pulls](https://img.shields.io/docker/pulls/nofusscomputing/centurion-erp?style=plastic&logo=docker&color=0db7ed)](https://hub.docker.com/r/nofusscomputing/centurion-erp) [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/centurion-erp)](https://artifacthub.io/packages/container/centurion-erp/centurion-erp) +![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Frefs%2Fheads%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_integration_postgres_versions.json&style=plastic&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iNDMyLjA3MXB0IiBoZWlnaHQ9IjQ0NS4zODNwdCIgdmlld0JveD0iMCAwIDQzMi4wNzEgNDQ1LjM4MyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgaWQ9Im9yZ2luYWwiIHN0eWxlPSJmaWxsLXJ1bGU6bm9uemVybztjbGlwLXJ1bGU6bm9uemVybztzdHJva2U6IzAwMDAwMDtzdHJva2UtbWl0ZXJsaW1pdDo0OyI%2BCgk8L2c%2BCjxnIGlkPSJMYXllcl94MDAyMF8zIiBzdHlsZT0iZmlsbC1ydWxlOm5vbnplcm87Y2xpcC1ydWxlOm5vbnplcm87ZmlsbDpub25lO3N0cm9rZTojRkZGRkZGO3N0cm9rZS13aWR0aDoxMi40NjUxO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDo0OyI%2BCjxwYXRoIHN0eWxlPSJmaWxsOiMwMDAwMDA7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjM3LjM5NTM7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7IiBkPSJNMzIzLjIwNSwzMjQuMjI3YzIuODMzLTIzLjYwMSwxLjk4NC0yNy4wNjIsMTkuNTYzLTIzLjIzOWw0LjQ2MywwLjM5MmMxMy41MTcsMC42MTUsMzEuMTk5LTIuMTc0LDQxLjU4Ny03YzIyLjM2Mi0xMC4zNzYsMzUuNjIyLTI3LjcsMTMuNTcyLTIzLjE0OGMtNTAuMjk3LDEwLjM3Ni01My43NTUtNi42NTUtNTMuNzU1LTYuNjU1YzUzLjExMS03OC44MDMsNzUuMzEzLTE3OC44MzYsNTYuMTQ5LTIwMy4zMjIgICAgQzM1Mi41MTQtNS41MzQsMjYyLjAzNiwyNi4wNDksMjYwLjUyMiwyNi44NjlsLTAuNDgyLDAuMDg5Yy05LjkzOC0yLjA2Mi0yMS4wNi0zLjI5NC0zMy41NTQtMy40OTZjLTIyLjc2MS0wLjM3NC00MC4wMzIsNS45NjctNTMuMTMzLDE1LjkwNGMwLDAtMTYxLjQwOC02Ni40OTgtMTUzLjg5OSw4My42MjhjMS41OTcsMzEuOTM2LDQ1Ljc3NywyNDEuNjU1LDk4LjQ3LDE3OC4zMSAgICBjMTkuMjU5LTIzLjE2MywzNy44NzEtNDIuNzQ4LDM3Ljg3MS00Mi43NDhjOS4yNDIsNi4xNCwyMC4zMDcsOS4yNzIsMzEuOTEyLDguMTQ3bDAuODk3LTAuNzY1Yy0wLjI4MSwyLjg3Ni0wLjE1Nyw1LjY4OSwwLjM1OSw5LjAxOWMtMTMuNTcyLDE1LjE2Ny05LjU4NCwxNy44My0zNi43MjMsMjMuNDE2Yy0yNy40NTcsNS42NTktMTEuMzI2LDE1LjczNC0wLjc5NywxOC4zNjdjMTIuNzY4LDMuMTkzLDQyLjMwNSw3LjcxNiw2Mi4yNjgtMjAuMjI0ICAgIGwtMC43OTUsMy4xODhjNS4zMjUsNC4yNiw0Ljk2NSwzMC42MTksNS43Miw0OS40NTJjMC43NTYsMTguODM0LDIuMDE3LDM2LjQwOSw1Ljg1Niw0Ni43NzFjMy44MzksMTAuMzYsOC4zNjksMzcuMDUsNDQuMDM2LDI5LjQwNmMyOS44MDktNi4zODgsNTIuNi0xNS41ODIsNTQuNjc3LTEwMS4xMDciLz4KPHBhdGggc3R5bGU9ImZpbGw6IzMzNjc5MTtzdHJva2U6bm9uZTsiIGQ9Ik00MDIuMzk1LDI3MS4yM2MtNTAuMzAyLDEwLjM3Ni01My43Ni02LjY1NS01My43Ni02LjY1NWM1My4xMTEtNzguODA4LDc1LjMxMy0xNzguODQzLDU2LjE1My0yMDMuMzI2Yy01Mi4yNy02Ni43ODUtMTQyLjc1Mi0zNS4yLTE0NC4yNjItMzQuMzhsLTAuNDg2LDAuMDg3Yy05LjkzOC0yLjA2My0yMS4wNi0zLjI5Mi0zMy41Ni0zLjQ5NmMtMjIuNzYxLTAuMzczLTQwLjAyNiw1Ljk2Ny01My4xMjcsMTUuOTAyICAgIGMwLDAtMTYxLjQxMS02Ni40OTUtMTUzLjkwNCw4My42M2MxLjU5NywzMS45MzgsNDUuNzc2LDI0MS42NTcsOTguNDcxLDE3OC4zMTJjMTkuMjYtMjMuMTYzLDM3Ljg2OS00Mi43NDgsMzcuODY5LTQyLjc0OGM5LjI0Myw2LjE0LDIwLjMwOCw5LjI3MiwzMS45MDgsOC4xNDdsMC45MDEtMC43NjVjLTAuMjgsMi44NzYtMC4xNTIsNS42ODksMC4zNjEsOS4wMTljLTEzLjU3NSwxNS4xNjctOS41ODYsMTcuODMtMzYuNzIzLDIzLjQxNiAgICBjLTI3LjQ1OSw1LjY1OS0xMS4zMjgsMTUuNzM0LTAuNzk2LDE4LjM2N2MxMi43NjgsMy4xOTMsNDIuMzA3LDcuNzE2LDYyLjI2Ni0yMC4yMjRsLTAuNzk2LDMuMTg4YzUuMzE5LDQuMjYsOS4wNTQsMjcuNzExLDguNDI4LDQ4Ljk2OWMtMC42MjYsMjEuMjU5LTEuMDQ0LDM1Ljg1NCwzLjE0Nyw0Ny4yNTRjNC4xOTEsMTEuNCw4LjM2OCwzNy4wNSw0NC4wNDIsMjkuNDA2YzI5LjgwOS02LjM4OCw0NS4yNTYtMjIuOTQyLDQ3LjQwNS01MC41NTUgICAgYzEuNTI1LTE5LjYzMSw0Ljk3Ni0xNi43MjksNS4xOTQtMzQuMjhsMi43NjgtOC4zMDljMy4xOTItMjYuNjExLDAuNTA3LTM1LjE5NiwxOC44NzItMzEuMjAzbDQuNDYzLDAuMzkyYzEzLjUxNywwLjYxNSwzMS4yMDgtMi4xNzQsNDEuNTkxLTdjMjIuMzU4LTEwLjM3NiwzNS42MTgtMjcuNywxMy41NzMtMjMuMTQ4eiIvPgo8cGF0aCBkPSJNMjE1Ljg2NiwyODYuNDg0Yy0xLjM4NSw0OS41MTYsMC4zNDgsOTkuMzc3LDUuMTkzLDExMS40OTVjNC44NDgsMTIuMTE4LDE1LjIyMywzNS42ODgsNTAuOSwyOC4wNDVjMjkuODA2LTYuMzksNDAuNjUxLTE4Ljc1Niw0NS4zNTctNDYuMDUxYzMuNDY2LTIwLjA4MiwxMC4xNDgtNzUuODU0LDExLjAwNS04Ny4yODEiLz4KPHBhdGggZD0iTTE3My4xMDQsMzguMjU2YzAsMC0xNjEuNTIxLTY2LjAxNi0xNTQuMDEyLDg0LjEwOWMxLjU5NywzMS45MzgsNDUuNzc5LDI0MS42NjQsOTguNDczLDE3OC4zMTZjMTkuMjU2LTIzLjE2NiwzNi42NzEtNDEuMzM1LDM2LjY3MS00MS4zMzUiLz4KPHBhdGggZD0iTTI2MC4zNDksMjYuMjA3Yy01LjU5MSwxLjc1Myw4OS44NDgtMzQuODg5LDE0NC4wODcsMzQuNDE3YzE5LjE1OSwyNC40ODQtMy4wNDMsMTI0LjUxOS01Ni4xNTMsMjAzLjMyOSIvPgo8cGF0aCBzdHlsZT0ic3Ryb2tlLWxpbmVqb2luOmJldmVsOyIgZD0iTTM0OC4yODIsMjYzLjk1M2MwLDAsMy40NjEsMTcuMDM2LDUzLjc2NCw2LjY1M2MyMi4wNC00LjU1Miw4Ljc3NiwxMi43NzQtMTMuNTc3LDIzLjE1NWMtMTguMzQ1LDguNTE0LTU5LjQ3NCwxMC42OTYtNjAuMTQ2LTEuMDY5Yy0xLjcyOS0zMC4zNTUsMjEuNjQ3LTIxLjEzMywxOS45Ni0yOC43MzljLTEuNTI1LTYuODUtMTEuOTc5LTEzLjU3My0xOC44OTQtMzAuMzM4ICAgIGMtNi4wMzctMTQuNjMzLTgyLjc5Ni0xMjYuODQ5LDIxLjI4Ny0xMTAuMTgzYzMuODEzLTAuNzg5LTI3LjE0Ni05OS4wMDItMTI0LjU1My0xMDAuNTk5Yy05Ny4zODUtMS41OTctOTQuMTksMTE5Ljc2Mi05NC4xOSwxMTkuNzYyIi8%2BCjxwYXRoIGQ9Ik0xODguNjA0LDI3NC4zMzRjLTEzLjU3NywxNS4xNjYtOS41ODQsMTcuODI5LTM2LjcyMywyMy40MTdjLTI3LjQ1OSw1LjY2LTExLjMyNiwxNS43MzMtMC43OTcsMTguMzY1YzEyLjc2OCwzLjE5NSw0Mi4zMDcsNy43MTgsNjIuMjY2LTIwLjIyOWM2LjA3OC04LjUwOS0wLjAzNi0yMi4wODYtOC4zODUtMjUuNTQ3Yy00LjAzNC0xLjY3MS05LjQyOC0zLjc2NS0xNi4zNjEsMy45OTR6Ii8%2BCjxwYXRoIGQ9Ik0xODcuNzE1LDI3NC4wNjljLTEuMzY4LTguOTE3LDIuOTMtMTkuNTI4LDcuNTM2LTMxLjk0MmM2LjkyMi0xOC42MjYsMjIuODkzLTM3LjI1NSwxMC4xMTctOTYuMzM5Yy05LjUyMy00NC4wMjktNzMuMzk2LTkuMTYzLTczLjQzNi0zLjE5M2MtMC4wMzksNS45NjgsMi44ODksMzAuMjYtMS4wNjcsNTguNTQ4Yy01LjE2MiwzNi45MTMsMjMuNDg4LDY4LjEzMiw1Ni40NzksNjQuOTM4Ii8%2BCjxwYXRoIHN0eWxlPSJmaWxsOiNGRkZGRkY7c3Ryb2tlLXdpZHRoOjQuMTU1O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyOyIgZD0iTTE3Mi41MTcsMTQxLjdjLTAuMjg4LDIuMDM5LDMuNzMzLDcuNDgsOC45NzYsOC4yMDdjNS4yMzQsMC43Myw5LjcxNC0zLjUyMiw5Ljk5OC01LjU1OWMwLjI4NC0yLjAzOS0zLjczMi00LjI4NS04Ljk3Ny01LjAxNWMtNS4yMzctMC43MzEtOS43MTksMC4zMzMtOS45OTYsMi4zNjd6Ii8%2BCjxwYXRoIHN0eWxlPSJmaWxsOiNGRkZGRkY7c3Ryb2tlLXdpZHRoOjIuMDc3NTtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjsiIGQ9Ik0zMzEuOTQxLDEzNy41NDNjMC4yODQsMi4wMzktMy43MzIsNy40OC04Ljk3Niw4LjIwN2MtNS4yMzgsMC43My05LjcxOC0zLjUyMi0xMC4wMDUtNS41NTljLTAuMjc3LTIuMDM5LDMuNzQtNC4yODUsOC45NzktNS4wMTVjNS4yMzktMC43Myw5LjcxOCwwLjMzMywxMC4wMDIsMi4zNjh6Ii8%2BCjxwYXRoIGQ9Ik0zNTAuNjc2LDEyMy40MzJjMC44NjMsMTUuOTk0LTMuNDQ1LDI2Ljg4OC0zLjk4OCw0My45MTRjLTAuODA0LDI0Ljc0OCwxMS43OTksNTMuMDc0LTcuMTkxLDgxLjQzNSIvPgo8cGF0aCBzdHlsZT0ic3Ryb2tlLXdpZHRoOjM7IiBkPSJNMCw2MC4yMzIiLz4KPC9nPgo8L3N2Zz4K) + ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Frefs%2Fheads%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_integration_rabbitmq_versions.json&style=plastic) ---- diff --git a/docs/projects/centurion_erp/index.md b/docs/projects/centurion_erp/index.md index 5eb82aeb..d87c2aed 100644 --- a/docs/projects/centurion_erp/index.md +++ b/docs/projects/centurion_erp/index.md @@ -10,6 +10,11 @@ about: https://gitlab.com/nofusscomputing/infrastructure/configuration-managemen ![Project Status - Active](https://img.shields.io/badge/Project%20Status-Active-green?logo=github&style=plastic) +![Docker Pulls](https://img.shields.io/docker/pulls/nofusscomputing/centurion-erp?style=plastic&logo=docker&color=0db7ed) [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/centurion-erp)](https://artifacthub.io/packages/container/centurion-erp/centurion-erp) + +![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Frefs%2Fheads%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_integration_postgres_versions.json&style=plastic&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iNDMyLjA3MXB0IiBoZWlnaHQ9IjQ0NS4zODNwdCIgdmlld0JveD0iMCAwIDQzMi4wNzEgNDQ1LjM4MyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgaWQ9Im9yZ2luYWwiIHN0eWxlPSJmaWxsLXJ1bGU6bm9uemVybztjbGlwLXJ1bGU6bm9uemVybztzdHJva2U6IzAwMDAwMDtzdHJva2UtbWl0ZXJsaW1pdDo0OyI%2BCgk8L2c%2BCjxnIGlkPSJMYXllcl94MDAyMF8zIiBzdHlsZT0iZmlsbC1ydWxlOm5vbnplcm87Y2xpcC1ydWxlOm5vbnplcm87ZmlsbDpub25lO3N0cm9rZTojRkZGRkZGO3N0cm9rZS13aWR0aDoxMi40NjUxO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDo0OyI%2BCjxwYXRoIHN0eWxlPSJmaWxsOiMwMDAwMDA7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjM3LjM5NTM7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7IiBkPSJNMzIzLjIwNSwzMjQuMjI3YzIuODMzLTIzLjYwMSwxLjk4NC0yNy4wNjIsMTkuNTYzLTIzLjIzOWw0LjQ2MywwLjM5MmMxMy41MTcsMC42MTUsMzEuMTk5LTIuMTc0LDQxLjU4Ny03YzIyLjM2Mi0xMC4zNzYsMzUuNjIyLTI3LjcsMTMuNTcyLTIzLjE0OGMtNTAuMjk3LDEwLjM3Ni01My43NTUtNi42NTUtNTMuNzU1LTYuNjU1YzUzLjExMS03OC44MDMsNzUuMzEzLTE3OC44MzYsNTYuMTQ5LTIwMy4zMjIgICAgQzM1Mi41MTQtNS41MzQsMjYyLjAzNiwyNi4wNDksMjYwLjUyMiwyNi44NjlsLTAuNDgyLDAuMDg5Yy05LjkzOC0yLjA2Mi0yMS4wNi0zLjI5NC0zMy41NTQtMy40OTZjLTIyLjc2MS0wLjM3NC00MC4wMzIsNS45NjctNTMuMTMzLDE1LjkwNGMwLDAtMTYxLjQwOC02Ni40OTgtMTUzLjg5OSw4My42MjhjMS41OTcsMzEuOTM2LDQ1Ljc3NywyNDEuNjU1LDk4LjQ3LDE3OC4zMSAgICBjMTkuMjU5LTIzLjE2MywzNy44NzEtNDIuNzQ4LDM3Ljg3MS00Mi43NDhjOS4yNDIsNi4xNCwyMC4zMDcsOS4yNzIsMzEuOTEyLDguMTQ3bDAuODk3LTAuNzY1Yy0wLjI4MSwyLjg3Ni0wLjE1Nyw1LjY4OSwwLjM1OSw5LjAxOWMtMTMuNTcyLDE1LjE2Ny05LjU4NCwxNy44My0zNi43MjMsMjMuNDE2Yy0yNy40NTcsNS42NTktMTEuMzI2LDE1LjczNC0wLjc5NywxOC4zNjdjMTIuNzY4LDMuMTkzLDQyLjMwNSw3LjcxNiw2Mi4yNjgtMjAuMjI0ICAgIGwtMC43OTUsMy4xODhjNS4zMjUsNC4yNiw0Ljk2NSwzMC42MTksNS43Miw0OS40NTJjMC43NTYsMTguODM0LDIuMDE3LDM2LjQwOSw1Ljg1Niw0Ni43NzFjMy44MzksMTAuMzYsOC4zNjksMzcuMDUsNDQuMDM2LDI5LjQwNmMyOS44MDktNi4zODgsNTIuNi0xNS41ODIsNTQuNjc3LTEwMS4xMDciLz4KPHBhdGggc3R5bGU9ImZpbGw6IzMzNjc5MTtzdHJva2U6bm9uZTsiIGQ9Ik00MDIuMzk1LDI3MS4yM2MtNTAuMzAyLDEwLjM3Ni01My43Ni02LjY1NS01My43Ni02LjY1NWM1My4xMTEtNzguODA4LDc1LjMxMy0xNzguODQzLDU2LjE1My0yMDMuMzI2Yy01Mi4yNy02Ni43ODUtMTQyLjc1Mi0zNS4yLTE0NC4yNjItMzQuMzhsLTAuNDg2LDAuMDg3Yy05LjkzOC0yLjA2My0yMS4wNi0zLjI5Mi0zMy41Ni0zLjQ5NmMtMjIuNzYxLTAuMzczLTQwLjAyNiw1Ljk2Ny01My4xMjcsMTUuOTAyICAgIGMwLDAtMTYxLjQxMS02Ni40OTUtMTUzLjkwNCw4My42M2MxLjU5NywzMS45MzgsNDUuNzc2LDI0MS42NTcsOTguNDcxLDE3OC4zMTJjMTkuMjYtMjMuMTYzLDM3Ljg2OS00Mi43NDgsMzcuODY5LTQyLjc0OGM5LjI0Myw2LjE0LDIwLjMwOCw5LjI3MiwzMS45MDgsOC4xNDdsMC45MDEtMC43NjVjLTAuMjgsMi44NzYtMC4xNTIsNS42ODksMC4zNjEsOS4wMTljLTEzLjU3NSwxNS4xNjctOS41ODYsMTcuODMtMzYuNzIzLDIzLjQxNiAgICBjLTI3LjQ1OSw1LjY1OS0xMS4zMjgsMTUuNzM0LTAuNzk2LDE4LjM2N2MxMi43NjgsMy4xOTMsNDIuMzA3LDcuNzE2LDYyLjI2Ni0yMC4yMjRsLTAuNzk2LDMuMTg4YzUuMzE5LDQuMjYsOS4wNTQsMjcuNzExLDguNDI4LDQ4Ljk2OWMtMC42MjYsMjEuMjU5LTEuMDQ0LDM1Ljg1NCwzLjE0Nyw0Ny4yNTRjNC4xOTEsMTEuNCw4LjM2OCwzNy4wNSw0NC4wNDIsMjkuNDA2YzI5LjgwOS02LjM4OCw0NS4yNTYtMjIuOTQyLDQ3LjQwNS01MC41NTUgICAgYzEuNTI1LTE5LjYzMSw0Ljk3Ni0xNi43MjksNS4xOTQtMzQuMjhsMi43NjgtOC4zMDljMy4xOTItMjYuNjExLDAuNTA3LTM1LjE5NiwxOC44NzItMzEuMjAzbDQuNDYzLDAuMzkyYzEzLjUxNywwLjYxNSwzMS4yMDgtMi4xNzQsNDEuNTkxLTdjMjIuMzU4LTEwLjM3NiwzNS42MTgtMjcuNywxMy41NzMtMjMuMTQ4eiIvPgo8cGF0aCBkPSJNMjE1Ljg2NiwyODYuNDg0Yy0xLjM4NSw0OS41MTYsMC4zNDgsOTkuMzc3LDUuMTkzLDExMS40OTVjNC44NDgsMTIuMTE4LDE1LjIyMywzNS42ODgsNTAuOSwyOC4wNDVjMjkuODA2LTYuMzksNDAuNjUxLTE4Ljc1Niw0NS4zNTctNDYuMDUxYzMuNDY2LTIwLjA4MiwxMC4xNDgtNzUuODU0LDExLjAwNS04Ny4yODEiLz4KPHBhdGggZD0iTTE3My4xMDQsMzguMjU2YzAsMC0xNjEuNTIxLTY2LjAxNi0xNTQuMDEyLDg0LjEwOWMxLjU5NywzMS45MzgsNDUuNzc5LDI0MS42NjQsOTguNDczLDE3OC4zMTZjMTkuMjU2LTIzLjE2NiwzNi42NzEtNDEuMzM1LDM2LjY3MS00MS4zMzUiLz4KPHBhdGggZD0iTTI2MC4zNDksMjYuMjA3Yy01LjU5MSwxLjc1Myw4OS44NDgtMzQuODg5LDE0NC4wODcsMzQuNDE3YzE5LjE1OSwyNC40ODQtMy4wNDMsMTI0LjUxOS01Ni4xNTMsMjAzLjMyOSIvPgo8cGF0aCBzdHlsZT0ic3Ryb2tlLWxpbmVqb2luOmJldmVsOyIgZD0iTTM0OC4yODIsMjYzLjk1M2MwLDAsMy40NjEsMTcuMDM2LDUzLjc2NCw2LjY1M2MyMi4wNC00LjU1Miw4Ljc3NiwxMi43NzQtMTMuNTc3LDIzLjE1NWMtMTguMzQ1LDguNTE0LTU5LjQ3NCwxMC42OTYtNjAuMTQ2LTEuMDY5Yy0xLjcyOS0zMC4zNTUsMjEuNjQ3LTIxLjEzMywxOS45Ni0yOC43MzljLTEuNTI1LTYuODUtMTEuOTc5LTEzLjU3My0xOC44OTQtMzAuMzM4ICAgIGMtNi4wMzctMTQuNjMzLTgyLjc5Ni0xMjYuODQ5LDIxLjI4Ny0xMTAuMTgzYzMuODEzLTAuNzg5LTI3LjE0Ni05OS4wMDItMTI0LjU1My0xMDAuNTk5Yy05Ny4zODUtMS41OTctOTQuMTksMTE5Ljc2Mi05NC4xOSwxMTkuNzYyIi8%2BCjxwYXRoIGQ9Ik0xODguNjA0LDI3NC4zMzRjLTEzLjU3NywxNS4xNjYtOS41ODQsMTcuODI5LTM2LjcyMywyMy40MTdjLTI3LjQ1OSw1LjY2LTExLjMyNiwxNS43MzMtMC43OTcsMTguMzY1YzEyLjc2OCwzLjE5NSw0Mi4zMDcsNy43MTgsNjIuMjY2LTIwLjIyOWM2LjA3OC04LjUwOS0wLjAzNi0yMi4wODYtOC4zODUtMjUuNTQ3Yy00LjAzNC0xLjY3MS05LjQyOC0zLjc2NS0xNi4zNjEsMy45OTR6Ii8%2BCjxwYXRoIGQ9Ik0xODcuNzE1LDI3NC4wNjljLTEuMzY4LTguOTE3LDIuOTMtMTkuNTI4LDcuNTM2LTMxLjk0MmM2LjkyMi0xOC42MjYsMjIuODkzLTM3LjI1NSwxMC4xMTctOTYuMzM5Yy05LjUyMy00NC4wMjktNzMuMzk2LTkuMTYzLTczLjQzNi0zLjE5M2MtMC4wMzksNS45NjgsMi44ODksMzAuMjYtMS4wNjcsNTguNTQ4Yy01LjE2MiwzNi45MTMsMjMuNDg4LDY4LjEzMiw1Ni40NzksNjQuOTM4Ii8%2BCjxwYXRoIHN0eWxlPSJmaWxsOiNGRkZGRkY7c3Ryb2tlLXdpZHRoOjQuMTU1O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyOyIgZD0iTTE3Mi41MTcsMTQxLjdjLTAuMjg4LDIuMDM5LDMuNzMzLDcuNDgsOC45NzYsOC4yMDdjNS4yMzQsMC43Myw5LjcxNC0zLjUyMiw5Ljk5OC01LjU1OWMwLjI4NC0yLjAzOS0zLjczMi00LjI4NS04Ljk3Ny01LjAxNWMtNS4yMzctMC43MzEtOS43MTksMC4zMzMtOS45OTYsMi4zNjd6Ii8%2BCjxwYXRoIHN0eWxlPSJmaWxsOiNGRkZGRkY7c3Ryb2tlLXdpZHRoOjIuMDc3NTtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjsiIGQ9Ik0zMzEuOTQxLDEzNy41NDNjMC4yODQsMi4wMzktMy43MzIsNy40OC04Ljk3Niw4LjIwN2MtNS4yMzgsMC43My05LjcxOC0zLjUyMi0xMC4wMDUtNS41NTljLTAuMjc3LTIuMDM5LDMuNzQtNC4yODUsOC45NzktNS4wMTVjNS4yMzktMC43Myw5LjcxOCwwLjMzMywxMC4wMDIsMi4zNjh6Ii8%2BCjxwYXRoIGQ9Ik0zNTAuNjc2LDEyMy40MzJjMC44NjMsMTUuOTk0LTMuNDQ1LDI2Ljg4OC0zLjk4OCw0My45MTRjLTAuODA0LDI0Ljc0OCwxMS43OTksNTMuMDc0LTcuMTkxLDgxLjQzNSIvPgo8cGF0aCBzdHlsZT0ic3Ryb2tlLXdpZHRoOjM7IiBkPSJNMCw2MC4yMzIiLz4KPC9nPgo8L3N2Zz4K) + ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Frefs%2Fheads%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_integration_rabbitmq_versions.json&style=plastic) + ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nofusscomputing/centurion_erp/ci.yaml?branch=master&style=plastic&logo=github&label=Stable%20Build&color=%23000) ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nofusscomputing/centurion_erp/ci.yaml?branch=development&style=plastic&logo=github&label=Dev%20Build&color=%23000) ![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/nofusscomputing/centurion_erp?style=plastic&logo=github&label=Open%20Issues&color=000) ![GitHub Issues or Pull Requests by label](https://img.shields.io/github/issues/nofusscomputing/centurion_erp/type%3A%3Abug?style=plastic&logo=github&label=Bug%20Fixes%20Required&color=000) @@ -20,8 +25,6 @@ about: https://gitlab.com/nofusscomputing/infrastructure/configuration-managemen ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_functional_test.json) -![Docker Pulls](https://img.shields.io/docker/pulls/nofusscomputing/centurion-erp?style=plastic&logo=docker&color=0db7ed) [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/centurion-erp)](https://artifacthub.io/packages/container/centurion-erp/centurion-erp) - Whilst there are many Enterprise Rescource Planning (ERP) applications, Centurion ERP is being developed to provide an open source option with a large emphasis on the IT Service Management (ITSM) modules. The goal is to provide a system that is not only an IT Information Library (ITIL), but that of which will connect to other ITSM systems, i.e. AWX for automation orchestration. Other common modules that form part of or are normally found within an ERP system, will be added if they relate specifically to any ITSM workflow. We welcome contributions should you desire a feature that does not yet exist.