diff --git a/.env.example b/.env.example index 46dda2ee..b5348fef 100644 --- a/.env.example +++ b/.env.example @@ -26,7 +26,7 @@ DJANGO_DB_LOG_HANDLER=console #PHOTO_ROOT_PATH= # Should the database be migrated before start (entrypoint.sh - docker setup). Will be migrated anyway if $SITE_ROOT=api. Comment out for False DJANGO_MIGRATE=True -# Should the modules be searched for scheduled tasks. Comment out for false + PROJECT_NAME=dev # set up you main domain #DOMAIN=dev-openimis.org @@ -51,6 +51,7 @@ PASSWORD_LOWERCASE=1 # Minimum number of lowercase letters PASSWORD_DIGITS=1 # Minimum number of digits PASSWORD_SYMBOLS=1 # Minimum number of symbols PASSWORD_SPACES=1 # Maximum number of spaces allowed + # Define the trusted origins for CSRF protection, separated by commas CSRF_TRUSTED_ORIGINS=http://localhost:3000,http://localhost:8000 @@ -60,8 +61,7 @@ RATELIMIT_KEY=ip # Key to identify the client; 'ip' means it will use the clien RATELIMIT_RATE=150/m # Rate limit (150 requests per minute) RATELIMIT_METHOD=ALL # HTTP methods to rate limit; 'ALL' means all methods RATELIMIT_GROUP=graphql # Group name for the rate limit -RATELIMIT_SKIP_TIMEOUT=False # Whether to skip rate limiting during c - +RATELIMIT_SKIP_TIMEOUT=False # Whether to skip rate limiting OPENSEARCH_ADMIN=admin OPENSEARCH_PASSWORD=B9wc9VrqX7pY \ No newline at end of file diff --git a/.github/workflows/ci_module.yml b/.github/workflows/ci_module.yml index 95d80084..e1e94ada 100755 --- a/.github/workflows/ci_module.yml +++ b/.github/workflows/ci_module.yml @@ -344,6 +344,25 @@ jobs: OPEN_SEARCH_HTTP_PORT: 9200 OPENSEARCH_HOSTS: "localhost:9200" OPENSEARCH_DSL_AUTOSYNC: False + - name: Generate Coverage Report + if: always() + working-directory: ./openimis/openIMIS + run: | + python -m coverage xml -o coverage.xml + # Only leave actual code part, skip absuolute directory like + # /home/runner/work/openimis-be-invoice_py/openimis-be-invoice_py/current-module/ + # It's required by circle-ci setup + mkdir coverage + sed -E 's|/home/runner/work/(.*?)/(.*?)/current-module/|./current-module/|' coverage.xml >> coverage/coverage1.xml + sed -E 's|/home/runner/work/(.*?)/(.*?)/current-module/|./|' coverage.xml >> coverage/coverage2.xml + + cat coverage.xml + - name: Coverage results + if: success() || failure() + uses: actions/upload-artifact@v4 + with: + name: coverage.xml + path: ./openimis/openIMIS/coverage flake-8-linter: name: Run flake8 check runs-on: ubuntu-24.04 diff --git a/.gitignore b/.gitignore index da2798b2..662822c9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,28 @@ -**/.idea -**/venv -**/.venv -**/.env -**/__pycache__ -modules-requirements.txt -modules-tests.sh -**/.coverage -**/coverage.xml -**/coverage -**/*.log -**/*.log.* -**/*.mo -openIMIS/locale/en/LC_MESSAGES/django.mo -**/staticfiles -extracted_translations_fe -script/config.py -**/src/* -**/images/insurees -openimis-dev.json -# Except for the runConfigurations folder -!.idea/runConfigurations +**/.vscode +**/.idea +**/venv +**/.venv +**/.env +**/__pycache__ +modules-requirements.txt +modules-tests.sh +**/.coverage +**/coverage.xml +**/coverage +**/*.log +**/*.log.* +**/*.mo +openIMIS/file_storage +openIMIS/locale/en/LC_MESSAGES/django.mo +**/staticfiles +extracted_translations_fe +script/config.py +**/src/* +**/images/insurees +openimis-dev.json + +# Except for the runConfigurations folder +!.idea/runConfigurations -# Ensure all files in runConfigurations are included -!.idea/runConfigurations/* +# Ensure all files in runConfigurations are included +!.idea/runConfigurations/* diff --git a/README.md b/README.md index fc883838..871befe0 100644 --- a/README.md +++ b/README.md @@ -89,11 +89,10 @@ When programming for openIMIS backend, you are highly encouraged to use the feat At this stage, you may (depends on the database you connect to) need to: -- apply django migrations, from `openimis-be_py/openIMIS`: `python manage.py migrate` +- apply django migrations, from `openimis-be_py/openIMIS`: `python manage.py migrate`. See [PostgresQL section](#postgresql) if you are using postgresql for dev DB. - create a superuser for django admin console, from - `openimis-be_py/openIMIS`: `python manage.py createsuperuser` (will - not prompt for a password) and then `python manage.py changepassword -` + `openimis-be_py/openIMIS`: `python manage.py createsuperuser` (will + not prompt for a password) and then `python manage.py changepassword ` ### To edit (modify) an existing openIMIS module (e.g. `openimis-be-claim`) @@ -220,6 +219,68 @@ The configuration for connection to the database is identical for developers and - default 'options' in openIMIS are `{'driver': 'ODBC Driver 17 for SQL Server','unicode_results': True}` If you need to provide other options, use the `DB_OPTIONS` entry in the `.env` file (be complete: the new json string will entirely replace the default one) + +### PostgresQL + +**Requirement:** `postgres-json-schema`. +Follow the README at https://github.com/gavinwahl/postgres-json-schema to install. + +To use postgresql as the dev database, specify `DB_ENGINE` in `.env` alongside other DB config vars: +``` +DB_ENGINE=django.db.backends.postgresql +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=imis +DB_USER=postgres +``` + +Create the database, named `imis` using the example `DB_NAME` above: +```bash +psql postgres -c 'create database imis' +``` + +Before applying django migrations, the database needs to be prepared using scripts from https://github.com/openimis/database_postgresql: + +```bash +git clone https://github.com/openimis/database_postgresql +cd database_postgresql + +# generate concatenated .sql for easy execution +bash concatenate_files.sh + +# prepare the database - replace fullDemoDatabase.sql with EmptyDatabase.sql if you don't need demo data +psql -d imis -a -f output/fullDemoDatabase.sql +``` + +From here on django's `python manage.py migrate` should execute succesfully. + +Repeat the same steps for `test_imis`, which is used for unit tests: + +```bash +psql postgres -c 'create database test_imis' +psql -d test_imis -a -f output/fullDemoDatabase.sql +``` + + +## OpenSearch + +### OpenSearch - Adding Environmental Variables to Your Build +To configure environmental variables for your build, include the following: +* `OPENSEARCH_HOST` - For the non-dockerized instance in a local context, set it to 0.0.0.0:9200. +For the dockerized instance, use opensearch:9200. +* `OPENSEARCH_ADMIN` This variable is used for the admin username. (default value: admin) +* `OPENSEARCH_PASSWORD` This variable is used for the admin password. (default value: admin) + +### OpenSearch - How to initialize data after deployment +* If you have initialized the application but still have some data to be transferred, you can effortlessly +achieve this by using the commands available in the business module: `python manage.py add__data_to_opensearch`. +This command loads existing data into OpenSearch. + +### OpenSearch - more details +* For more comprehensive details on OpenSearch configuration, please consult the [resource](https://github.com/openimis/openimis-be-opensearch_reports_py/tree/develop) +provided in the README section. + + ## Developer tools ### To create backend module skeleton in single command @@ -324,6 +385,7 @@ module skeleton in single command` section to extract frontend translations of all modules present in `openimis.json`. - those translations will be copied into 'extracted_translations_fe' folder in assembly backend module + ### JWT Security Configuration To enhance JWT token security, you can configure the system to use RSA keys for signing and verifying tokens. @@ -344,7 +406,6 @@ To enhance JWT token security, you can configure the system to use RSA keys for Note: If RSA keys are not provided, the system defaults to HS256. Using RS256 with RSA keys is recommended for enhanced security. - ## CSRF Setup Guide CSRF (Cross-Site Request Forgery) protection ensures that unauthorized commands are not performed on behalf of authenticated users without their consent. It achieves this by including a unique token in each form submission or AJAX request, which is then validated by the server. @@ -405,6 +466,7 @@ USER_AGENT_CSRF_BYPASS = [ ] ``` + ## Custom exception handler for new modules REST-based modules If the module you want to add to the openIMIS uses its own REST exception handler you have to register diff --git a/openIMIS/openIMIS/ExceptionHandlerDispatcher.py b/openIMIS/openIMIS/ExceptionHandlerDispatcher.py index 4dde35df..fabad6e9 100644 --- a/openIMIS/openIMIS/ExceptionHandlerDispatcher.py +++ b/openIMIS/openIMIS/ExceptionHandlerDispatcher.py @@ -1,7 +1,6 @@ from .ExceptionHandlerRegistry import ExceptionHandlerRegistry from rest_framework.views import exception_handler -from rest_framework import exceptions, status - +from rest_framework import exceptions, status, views def dispatcher(exc, context): """ @@ -10,6 +9,10 @@ def dispatcher(exc, context): module_name = _extract_module_name(context['request']) handler = ExceptionHandlerRegistry.get_exception_handler(module_name) + response = _process_exception_handler(exc, context) + if response: + return response + if handler is None: # Fallback to default DRF exception handler if no handler is defined for the module handler = exception_handler @@ -21,9 +24,16 @@ def dispatcher(exc, context): return response +def _process_exception_handler(exc, context): + response = views.exception_handler(exc, context) + + if isinstance(exc, (exceptions.AuthenticationFailed, exceptions.NotAuthenticated)): + response.status_code = status.HTTP_401_UNAUTHORIZED + return response + + def _extract_module_name(request): """ Extracts the module name from the request URL. """ return request.path.split('/')[2] - diff --git a/openIMIS/openIMIS/settings/base.py b/openIMIS/openIMIS/settings/base.py index 88608979..41465bab 100644 --- a/openIMIS/openIMIS/settings/base.py +++ b/openIMIS/openIMIS/settings/base.py @@ -177,9 +177,23 @@ def SITE_URL(): # https://docs.djangoproject.com/en/2.1/howto/static-files/ STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") -STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" STATIC_URL = "/%sstatic/" % SITE_ROOT() +MEDIA_URL = "/file_storage/" +MEDIA_ROOT = os.path.join(BASE_DIR, "file_storage/") + +if not os.path.exists(MEDIA_ROOT): + os.makedirs(MEDIA_ROOT) + +STORAGES = { + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + }, + 'staticfiles': { + 'BACKEND': "whitenoise.storage.CompressedManifestStaticFilesStorage", + }, +} + ASGI_APPLICATION = "openIMIS.asgi.application" diff --git a/openIMIS/openIMIS/settings/common.py b/openIMIS/openIMIS/settings/common.py index 402f2625..16710900 100644 --- a/openIMIS/openIMIS/settings/common.py +++ b/openIMIS/openIMIS/settings/common.py @@ -1,10 +1,11 @@ import os - +import sys # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + MODE = os.environ.get("MODE", 'prod').lower() -if MODE == "dev": +if MODE == "dev" or "test" in sys.argv: DEBUG = True else: - DEBUG = os.environ.get("DEBUG", "false").lower() == "true" + DEBUG = os.environ.get("DEBUG", "false").lower() == "true" \ No newline at end of file