🚸(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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
40
docs/env.md
40
docs/env.md
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user