🚸(backend) add onboarding docs for new users

Adds two methods to allow new users to start with some docs.

User._handle_onboarding_documents_access() gives READER access to
each document listed in settings.USER_ONBOARDING_DOCUMENTS.

User._duplicate_onboarding_sandbox_document() creates a local copy
of the sandbox document specified in
settings.USER_ONBOARDING_SANDBOX_DOCUMENT.
This commit is contained in:
Sylvain Boissel
2026-02-16 15:41:33 +01:00
parent 5d5ac0c1c8
commit c80e7d05bb
5 changed files with 309 additions and 21 deletions

View File

@@ -15,6 +15,7 @@ and this project adheres to
- ✨(backend) manage reconciliation requests for user accounts #1878 - ✨(backend) manage reconciliation requests for user accounts #1878
- 👷(CI) add GHCR workflow for forked repo testing #1851 - 👷(CI) add GHCR workflow for forked repo testing #1851
- ✨(backend) allow the duplication of subpages #1893 - ✨(backend) allow the duplication of subpages #1893
- ✨(backend) Onboarding docs for new users #1891
### Changed ### Changed

View File

@@ -7,7 +7,7 @@ Here we describe all environment variables that can be set for the docs applicat
These are the environment variables you can set for the `impress-backend` container. These are the environment variables you can set for the `impress-backend` container.
| Option | Description | default | | Option | Description | default |
|-------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------| | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| AI_ALLOW_REACH_FROM | Users that can use AI must be this level. options are "public", "authenticated", "restricted" | authenticated | | AI_ALLOW_REACH_FROM | Users that can use AI must be this level. options are "public", "authenticated", "restricted" | authenticated |
| AI_API_KEY | AI key to be used for AI Base url | | | AI_API_KEY | AI key to be used for AI Base url | |
| AI_BASE_URL | OpenAI compatible AI base url | | | AI_BASE_URL | OpenAI compatible AI base url | |
@@ -17,7 +17,7 @@ These are the environment variables you can set for the `impress-backend` contai
| API_USERS_LIST_LIMIT | Limit on API users | 5 | | API_USERS_LIST_LIMIT | Limit on API users | 5 |
| API_USERS_LIST_THROTTLE_RATE_BURST | Throttle rate for api on burst | 30/minute | | API_USERS_LIST_THROTTLE_RATE_BURST | Throttle rate for api on burst | 30/minute |
| API_USERS_LIST_THROTTLE_RATE_SUSTAINED | Throttle rate for api | 180/hour | | API_USERS_LIST_THROTTLE_RATE_SUSTAINED | Throttle rate for api | 180/hour |
| API_USERS_SEARCH_QUERY_MIN_LENGTH | Minimum characters to insert to search a user | 3 | | API_USERS_SEARCH_QUERY_MIN_LENGTH | Minimum characters to insert to search a user | 3 |
| AWS_S3_ACCESS_KEY_ID | Access id for s3 endpoint | | | AWS_S3_ACCESS_KEY_ID | Access id for s3 endpoint | |
| AWS_S3_ENDPOINT_URL | S3 endpoint | | | AWS_S3_ENDPOINT_URL | S3 endpoint | |
| AWS_S3_REGION_NAME | Region name for s3 endpoint | | | AWS_S3_REGION_NAME | Region name for s3 endpoint | |
@@ -35,7 +35,7 @@ These are the environment variables you can set for the `impress-backend` contai
| CONVERSION_API_SECURE | Require secure conversion api | false | | CONVERSION_API_SECURE | Require secure conversion api | false |
| CONVERSION_API_TIMEOUT | Conversion api timeout | 30 | | CONVERSION_API_TIMEOUT | Conversion api timeout | 30 |
| CONVERSION_FILE_MAX_SIZE | The file max size allowed when uploaded to convert it | 20971520 (20MB) | | CONVERSION_FILE_MAX_SIZE | The file max size allowed when uploaded to convert it | 20971520 (20MB) |
| CONVERSION_FILE_EXTENSIONS_ALLOWED | Extension list managed by the conversion service | [".docx", ".md"] | CONVERSION_FILE_EXTENSIONS_ALLOWED | Extension list managed by the conversion service | [".docx", ".md"] |
| CRISP_WEBSITE_ID | Crisp website id for support | | | CRISP_WEBSITE_ID | Crisp website id for support | |
| DB_ENGINE | Engine to use for database connections | django.db.backends.postgresql_psycopg2 | | DB_ENGINE | Engine to use for database connections | django.db.backends.postgresql_psycopg2 |
| DB_HOST | Host of the database | localhost | | DB_HOST | Host of the database | localhost |
@@ -58,22 +58,22 @@ These are the environment variables you can set for the `impress-backend` contai
| DJANGO_EMAIL_HOST_USER | User to authenticate with on the email host | | | DJANGO_EMAIL_HOST_USER | User to authenticate with on the email host | |
| DJANGO_EMAIL_LOGO_IMG | Logo for the email | | | DJANGO_EMAIL_LOGO_IMG | Logo for the email | |
| DJANGO_EMAIL_PORT | Port used to connect to email host | | | DJANGO_EMAIL_PORT | Port used to connect to email host | |
| DJANGO_EMAIL_URL_APP | Url used in the email to go to the app | | | DJANGO_EMAIL_URL_APP | Url used in the email to go to the app | |
| DJANGO_EMAIL_USE_SSL | Use ssl for email host connection | false | | DJANGO_EMAIL_USE_SSL | Use ssl for email host connection | false |
| DJANGO_EMAIL_USE_TLS | Use tls for email host connection | false | | DJANGO_EMAIL_USE_TLS | Use tls for email host connection | false |
| DJANGO_SECRET_KEY | Secret key | | | DJANGO_SECRET_KEY | Secret key | |
| DJANGO_SERVER_TO_SERVER_API_TOKENS | | [] | | DJANGO_SERVER_TO_SERVER_API_TOKENS | | [] |
| DOCSPEC_API_URL | URL to endpoint of DocSpec conversion API | | | DOCSPEC_API_URL | URL to endpoint of DocSpec conversion API | |
| DOCUMENT_IMAGE_MAX_SIZE | Maximum size of document in bytes | 10485760 | | DOCUMENT_IMAGE_MAX_SIZE | Maximum size of document in bytes | 10485760 |
| FRONTEND_CSS_URL | To add a external css file to the app | | | FRONTEND_CSS_URL | To add a external css file to the app | |
| FRONTEND_JS_URL | To add a external js file to the app | | | FRONTEND_JS_URL | To add a external js file to the app | |
| FRONTEND_HOMEPAGE_FEATURE_ENABLED | Frontend feature flag to display the homepage | false | | FRONTEND_HOMEPAGE_FEATURE_ENABLED | Frontend feature flag to display the homepage | false |
| FRONTEND_THEME | Frontend theme to use | | | FRONTEND_THEME | Frontend theme to use | |
| LANGUAGE_CODE | Default language | en-us | | LANGUAGE_CODE | Default language | en-us |
| LANGFUSE_SECRET_KEY | The Langfuse secret key used by the sdk | None | | LANGFUSE_SECRET_KEY | The Langfuse secret key used by the sdk | None |
| LANGFUSE_PUBLIC_KEY | The Langfuse public key used by the sdk | None | | LANGFUSE_PUBLIC_KEY | The Langfuse public key used by the sdk | None |
| LANGFUSE_BASE_URL | The Langfuse base url used by the sdk | None | | LANGFUSE_BASE_URL | The Langfuse base url used by the sdk | None |
| LASUITE_MARKETING_BACKEND | Backend used when SIGNUP_NEW_USER_TO_MARKETING_EMAIL is True. See https://github.com/suitenumerique/django-lasuite/blob/main/documentation/how-to-use-marketing-backend.md | lasuite.marketing.backends.dummy.DummyBackend | | LASUITE_MARKETING_BACKEND | Backend used when SIGNUP_NEW_USER_TO_MARKETING_EMAIL is True. See https://github.com/suitenumerique/django-lasuite/blob/main/documentation/how-to-use-marketing-backend.md | lasuite.marketing.backends.dummy.DummyBackend |
| LASUITE_MARKETING_PARAMETERS | The parameters to configure LASUITE_MARKETING_BACKEND. See https://github.com/suitenumerique/django-lasuite/blob/main/documentation/how-to-use-marketing-backend.md | {} | | LASUITE_MARKETING_PARAMETERS | The parameters to configure LASUITE_MARKETING_BACKEND. See https://github.com/suitenumerique/django-lasuite/blob/main/documentation/how-to-use-marketing-backend.md | {} |
| LOGGING_LEVEL_LOGGERS_APP | Application logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO | | LOGGING_LEVEL_LOGGERS_APP | Application logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO |
| LOGGING_LEVEL_LOGGERS_ROOT | Default logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO | | LOGGING_LEVEL_LOGGERS_ROOT | Default logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO |
@@ -120,11 +120,12 @@ These are the environment variables you can set for the `impress-backend` contai
| THEME_CUSTOMIZATION_FILE_PATH | Full path to the file customizing the theme. An example is provided in src/backend/impress/configuration/theme/default.json | BASE_DIR/impress/configuration/theme/default.json | | THEME_CUSTOMIZATION_FILE_PATH | Full path to the file customizing the theme. An example is provided in src/backend/impress/configuration/theme/default.json | BASE_DIR/impress/configuration/theme/default.json |
| TRASHBIN_CUTOFF_DAYS | Trashbin cutoff | 30 | | TRASHBIN_CUTOFF_DAYS | Trashbin cutoff | 30 |
| USER_OIDC_ESSENTIAL_CLAIMS | Essential claims in OIDC token | [] | | USER_OIDC_ESSENTIAL_CLAIMS | Essential claims in OIDC token | [] |
| USER_ONBOARDING_DOCUMENTS | A list of documents IDs for which a read-only access will be created for new s | [] |
| USER_ONBOARDING_SANDBOX_DOCUMENT | ID of a template sandbox document that will be duplicated for new users | |
| USER_RECONCILIATION_FORM_URL | URL of a third-party form for user reconciliation requests | | | USER_RECONCILIATION_FORM_URL | URL of a third-party form for user reconciliation requests | |
| Y_PROVIDER_API_BASE_URL | Y Provider url | | | Y_PROVIDER_API_BASE_URL | Y Provider url | |
| Y_PROVIDER_API_KEY | Y provider API key | | | Y_PROVIDER_API_KEY | Y provider API key | |
## impress-frontend image ## impress-frontend image
These are the environment variables you can set to build the `impress-frontend` image. These are the environment variables you can set to build the `impress-frontend` image.
@@ -135,29 +136,30 @@ If you want to build the Docker image, this variable is used as an argument in t
Example: Example:
``` ```bash
docker build -f src/frontend/Dockerfile --target frontend-production --build-arg PUBLISH_AS_MIT=false docs-frontend:latest docker build -f src/frontend/Dockerfile --target frontend-production --build-arg PUBLISH_AS_MIT=false docs-frontend:latest
``` ```
If you want to build the front-end application using the yarn build command, you can edit the file `src/frontend/apps/impress/.env` with the `NODE_ENV=production` environment variable and modify it. Alternatively, you can use the listed environment variables with the prefix `NEXT_PUBLIC_` (for example, `NEXT_PUBLIC_PUBLISH_AS_MIT=false`). If you want to build the front-end application using the yarn build command, you can edit the file `src/frontend/apps/impress/.env` with the `NODE_ENV=production` environment variable and modify it. Alternatively, you can use the listed environment variables with the prefix `NEXT_PUBLIC_` (for example, `NEXT_PUBLIC_PUBLISH_AS_MIT=false`).
Example: Example:
``` ```bash
cd src/frontend/apps/impress cd src/frontend/apps/impress
NODE_ENV=production NEXT_PUBLIC_PUBLISH_AS_MIT=false yarn build NODE_ENV=production NEXT_PUBLIC_PUBLISH_AS_MIT=false yarn build
``` ```
| Option | Description | default | | Option | Description | default |
| ----------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------- | | -------------- | ---------------------------------------------------------------------------------- | ------- |
| API_ORIGIN | backend domain - it uses the current domain if not initialized | | | API_ORIGIN | backend domain - it uses the current domain if not initialized | |
| SW_DEACTIVATED | To not install the service worker | | | SW_DEACTIVATED | To not install the service worker | |
| PUBLISH_AS_MIT | Removes packages whose licences are incompatible with the MIT licence (see below) | true | | PUBLISH_AS_MIT | Removes packages whose licences are incompatible with the MIT licence (see below) | true |
Packages with licences incompatible with the MIT licence: Packages with licences incompatible with the MIT licence:
* `xl-docx-exporter`: [GPL](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-docx-exporter/LICENSE),
* `xl-pdf-exporter`: [GPL](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/LICENSE), * `xl-docx-exporter`: [GPL](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-docx-exporter/LICENSE),
* `xl-multi-column`: [GPL](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-multi-column/LICENSE). * `xl-pdf-exporter`: [GPL](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/LICENSE),
* `xl-multi-column`: [GPL](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-multi-column/LICENSE).
In `.env.development`, `PUBLISH_AS_MIT` is set to `false`, allowing developers to test Docs with all its features. In `.env.development`, `PUBLISH_AS_MIT` is set to `false`, allowing developers to test Docs with all its features.

View File

@@ -209,14 +209,76 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" """
If it's a new user, give its user access to the documents to which s.he was invited. If it's a new user, give its user access to the documents they were invited to.
""" """
is_adding = self._state.adding is_adding = self._state.adding
super().save(*args, **kwargs) super().save(*args, **kwargs)
if is_adding: if is_adding:
self._handle_onboarding_documents_access()
self._duplicate_onboarding_sandbox_document()
self._convert_valid_invitations() self._convert_valid_invitations()
def _handle_onboarding_documents_access(self):
"""
If the user is new and there are documents configured to be given to new users,
give access to these documents and pin them as favorites for the user.
"""
if settings.USER_ONBOARDING_DOCUMENTS:
onboarding_document_ids = set(settings.USER_ONBOARDING_DOCUMENTS)
onboarding_accesses = []
favorite_documents = []
for document_id in onboarding_document_ids:
try:
document = Document.objects.get(id=document_id)
except Document.DoesNotExist:
logger.warning(
"Onboarding document with id %s does not exist. Skipping.",
document_id,
)
continue
onboarding_accesses.append(
DocumentAccess(
user=self, document=document, role=RoleChoices.READER
)
)
favorite_documents.append(
DocumentFavorite(user=self, document_id=document_id)
)
DocumentAccess.objects.bulk_create(onboarding_accesses)
DocumentFavorite.objects.bulk_create(favorite_documents)
def _duplicate_onboarding_sandbox_document(self):
"""
If the user is new and there is a sandbox document configured,
duplicate the sandbox document for the user
"""
if settings.USER_ONBOARDING_SANDBOX_DOCUMENT:
sandbox_id = settings.USER_ONBOARDING_SANDBOX_DOCUMENT
try:
template_document = Document.objects.get(id=sandbox_id)
sandbox_document = template_document.add_sibling(
"right",
title=template_document.title,
content=template_document.content,
attachments=template_document.attachments,
duplicated_from=template_document,
creator=self,
)
DocumentAccess.objects.create(
user=self, document=sandbox_document, role=RoleChoices.OWNER
)
except Document.DoesNotExist:
logger.warning(
"Onboarding sandbox document with id %s does not exist. Skipping.",
sandbox_id,
)
def _convert_valid_invitations(self): def _convert_valid_invitations(self):
""" """
Convert valid invitations to document accesses. Convert valid invitations to document accesses.

View File

@@ -2,7 +2,11 @@
Unit tests for the User model Unit tests for the User model
""" """
import uuid
from unittest.mock import patch
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.test.utils import override_settings
import pytest import pytest
@@ -74,3 +78,217 @@ def test_modes_users_convert_valid_invitations():
id=invitation_other_document.id id=invitation_other_document.id
).exists() ).exists()
assert models.Invitation.objects.filter(id=other_email_invitation.id).exists() assert models.Invitation.objects.filter(id=other_email_invitation.id).exists()
@override_settings(USER_ONBOARDING_DOCUMENTS=[])
def test_models_users_handle_onboarding_documents_access_empty_setting():
"""
When USER_ONBOARDING_DOCUMENTS is empty, no accesses should be created.
"""
user = factories.UserFactory()
assert models.DocumentAccess.objects.filter(user=user).count() == 0
def test_models_users_handle_onboarding_documents_access_with_single_document():
"""
When USER_ONBOARDING_DOCUMENTS has a valid document ID,
an access should be created for the new user with the READER role.
The document should be pinned as a favorite for the user.
"""
document = factories.DocumentFactory()
with override_settings(USER_ONBOARDING_DOCUMENTS=[str(document.id)]):
user = factories.UserFactory()
assert (
models.DocumentAccess.objects.filter(user=user, document=document).count() == 1
)
access = models.DocumentAccess.objects.get(user=user, document=document)
assert access.role == models.RoleChoices.READER
user_favorites = models.DocumentFavorite.objects.filter(user=user)
assert user_favorites.count() == 1
assert user_favorites.filter(document=document).exists()
def test_models_users_handle_onboarding_documents_access_with_multiple_documents():
"""
When USER_ONBOARDING_DOCUMENTS has multiple valid document IDs,
accesses should be created for all documents.
All accesses should have the READER role.
All documents should be pinned as favorites for the user.
"""
document1 = factories.DocumentFactory(title="Document 1")
document2 = factories.DocumentFactory(title="Document 2")
document3 = factories.DocumentFactory(title="Document 3")
with override_settings(
USER_ONBOARDING_DOCUMENTS=[
str(document1.id),
str(document2.id),
str(document3.id),
]
):
user = factories.UserFactory()
user_accesses = models.DocumentAccess.objects.filter(user=user)
assert user_accesses.count() == 3
assert models.DocumentAccess.objects.filter(user=user, document=document1).exists()
assert models.DocumentAccess.objects.filter(user=user, document=document2).exists()
assert models.DocumentAccess.objects.filter(user=user, document=document3).exists()
for access in user_accesses:
assert access.role == models.RoleChoices.READER
user_favorites = models.DocumentFavorite.objects.filter(user=user)
assert user_favorites.count() == 3
assert user_favorites.filter(document=document1).exists()
assert user_favorites.filter(document=document2).exists()
assert user_favorites.filter(document=document3).exists()
def test_models_users_handle_onboarding_documents_access_with_invalid_document_id():
"""
When USER_ONBOARDING_DOCUMENTS has an invalid document ID,
it should be skipped and logged, but not raise an exception.
"""
invalid_id = uuid.uuid4()
with override_settings(USER_ONBOARDING_DOCUMENTS=[str(invalid_id)]):
with patch("core.models.logger") as mock_logger:
user = factories.UserFactory()
mock_logger.warning.assert_called_once()
call_args = mock_logger.warning.call_args
assert "Onboarding document with id" in call_args[0][0]
assert models.DocumentAccess.objects.filter(user=user).count() == 0
def test_models_users_handle_onboarding_documents_access_duplicate_prevention():
"""
If the same document is listed multiple times in USER_ONBOARDING_DOCUMENTS,
it should only create one access (or handle duplicates gracefully).
"""
document = factories.DocumentFactory()
with override_settings(
USER_ONBOARDING_DOCUMENTS=[str(document.id), str(document.id)]
):
user = factories.UserFactory()
user_accesses = models.DocumentAccess.objects.filter(user=user, document=document)
assert user_accesses.count() >= 1
@override_settings(USER_ONBOARDING_SANDBOX_DOCUMENT=None)
def test_models_users_duplicate_onboarding_sandbox_document_no_setting():
"""
When USER_ONBOARDING_SANDBOX_DOCUMENT is not set, no sandbox document should be created.
"""
user = factories.UserFactory()
assert (
models.Document.objects.filter(creator=user, title__icontains="Sandbox").count()
== 0
)
initial_accesses = models.DocumentAccess.objects.filter(user=user).count()
assert initial_accesses == 0
def test_models_users_duplicate_onboarding_sandbox_document_creates_sandbox():
"""
When USER_ONBOARDING_SANDBOX_DOCUMENT is set with a valid template document,
a new sandbox document should be created for the user with OWNER access.
"""
template_document = factories.DocumentFactory(title="Getting started with Docs")
with override_settings(USER_ONBOARDING_SANDBOX_DOCUMENT=str(template_document.id)):
user = factories.UserFactory()
sandbox_docs = models.Document.objects.filter(
creator=user, title="Getting started with Docs"
)
assert sandbox_docs.count() == 1
sandbox_doc = sandbox_docs.first()
assert sandbox_doc.creator == user
assert sandbox_doc.duplicated_from == template_document
access = models.DocumentAccess.objects.get(user=user, document=sandbox_doc)
assert access.role == models.RoleChoices.OWNER
def test_models_users_duplicate_onboarding_sandbox_document_with_invalid_template_id():
"""
When USER_ONBOARDING_SANDBOX_DOCUMENT has an invalid document ID,
it should be skipped and logged, but not raise an exception.
"""
invalid_id = uuid.uuid4()
with override_settings(USER_ONBOARDING_SANDBOX_DOCUMENT=str(invalid_id)):
with patch("core.models.logger") as mock_logger:
user = factories.UserFactory()
mock_logger.warning.assert_called_once()
call_args = mock_logger.warning.call_args
assert "Onboarding sandbox document with id" in call_args[0][0]
sandbox_docs = models.Document.objects.filter(creator=user)
assert sandbox_docs.count() == 0
def test_models_users_duplicate_onboarding_sandbox_document_creates_unique_sandbox_per_user():
"""
Each new user should get their own independent sandbox document.
"""
template_document = factories.DocumentFactory(title="Getting started with Docs")
with override_settings(USER_ONBOARDING_SANDBOX_DOCUMENT=str(template_document.id)):
user1 = factories.UserFactory()
user2 = factories.UserFactory()
sandbox_docs_user1 = models.Document.objects.filter(
creator=user1, title="Getting started with Docs"
)
sandbox_docs_user2 = models.Document.objects.filter(
creator=user2, title="Getting started with Docs"
)
assert sandbox_docs_user1.count() == 1
assert sandbox_docs_user2.count() == 1
assert sandbox_docs_user1.first().id != sandbox_docs_user2.first().id
def test_models_users_duplicate_onboarding_sandbox_document_integration_with_other_methods():
"""
Verify that sandbox creation works alongside other onboarding methods.
"""
template_document = factories.DocumentFactory(title="Getting started with Docs")
onboarding_doc = factories.DocumentFactory(title="Onboarding Document")
with override_settings(
USER_ONBOARDING_SANDBOX_DOCUMENT=str(template_document.id),
USER_ONBOARDING_DOCUMENTS=[str(onboarding_doc.id)],
):
user = factories.UserFactory()
sandbox_doc = models.Document.objects.filter(
creator=user, title="Getting started with Docs"
).first()
user_accesses = models.DocumentAccess.objects.filter(user=user)
assert user_accesses.count() == 2
sandbox_access = user_accesses.get(document=sandbox_doc)
onboarding_access = user_accesses.get(document=onboarding_doc)
assert sandbox_access.role == models.RoleChoices.OWNER
assert onboarding_access.role == models.RoleChoices.READER

View File

@@ -884,7 +884,12 @@ class Base(Configuration):
USER_RECONCILIATION_FORM_URL = values.Value( USER_RECONCILIATION_FORM_URL = values.Value(
None, environ_name="USER_RECONCILIATION_FORM_URL", environ_prefix=None None, environ_name="USER_RECONCILIATION_FORM_URL", environ_prefix=None
) )
USER_ONBOARDING_DOCUMENTS = values.ListValue(
[], environ_name="USER_ONBOARDING_DOCUMENTS", environ_prefix=None
)
USER_ONBOARDING_SANDBOX_DOCUMENT = values.Value(
None, environ_name="USER_ONBOARDING_SANDBOX_DOCUMENT", environ_prefix=None
)
# Marketing and communication settings # Marketing and communication settings
SIGNUP_NEW_USER_TO_MARKETING_EMAIL = values.BooleanValue( SIGNUP_NEW_USER_TO_MARKETING_EMAIL = values.BooleanValue(
False, False,