mirror of
https://github.com/suitenumerique/docs.git
synced 2026-09-27 12:04:59 +02:00
🚩(backend) add DUPLICATE_CHILDREN_FEATURE_ENABLED flag
We want to introduce a feature flag that allows duplicating documents along with their children when enabled. We will be able to control not only frontend side but also backend behavior regarding document with children duplication.
This commit is contained in:
@@ -86,6 +86,7 @@ These are the environment variables you can set for the `impress-backend` contai
|
||||
| DOCUMENT_IMAGE_MAX_SIZE | Maximum size of document in bytes | 10485760 |
|
||||
| DOCUMENT_ALL_ENDPOINT_ENABLED | Enable or not the endpoint /api/v1.0/documents/all/ | true |
|
||||
| DOCUMENT_NB_ACCESSES_CACHE_TIMEOUT | Time, in seconds, the number of accesses for a document stay in cache. | 600 |
|
||||
| DUPLICATE_CHILDREN_FEATURE_ENABLED | Allow duplicating a document together with its children | true |
|
||||
| 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_HOMEPAGE_FEATURE_ENABLED | Frontend feature flag to display the homepage | false |
|
||||
|
||||
@@ -1429,7 +1429,10 @@ class DocumentViewSet(
|
||||
if `with_descendants` is set to true.
|
||||
"""
|
||||
with_accesses = serializer.validated_data.get("with_accesses", False)
|
||||
with_descendants = serializer.validated_data.get("with_descendants", False)
|
||||
with_descendants = (
|
||||
serializer.validated_data.get("with_descendants", False)
|
||||
and settings.DUPLICATE_CHILDREN_FEATURE_ENABLED
|
||||
)
|
||||
|
||||
user_role = document_to_duplicate.get_role(user)
|
||||
is_owner_or_admin = user_role in models.PRIVILEGED_ROLES
|
||||
@@ -2977,6 +2980,7 @@ class ConfigView(drf.views.APIView):
|
||||
"CONVERSION_FILE_MAX_SIZE",
|
||||
"CONVERSION_UPLOAD_ENABLED",
|
||||
"DOCUMENT_IMAGE_MAX_SIZE",
|
||||
"DUPLICATE_CHILDREN_FEATURE_ENABLED",
|
||||
"ENVIRONMENT",
|
||||
"FRONTEND_CSS_URL",
|
||||
"FRONTEND_HOMEPAGE_FEATURE_ENABLED",
|
||||
|
||||
@@ -810,6 +810,55 @@ def test_api_documents_duplicate_without_descendants_should_not_duplicate_childr
|
||||
assert duplicated_root.get_children().count() == 0
|
||||
|
||||
|
||||
def test_api_documents_duplicate_with_descendants_disabled_by_feature_flag(settings):
|
||||
"""
|
||||
When DUPLICATE_CHILDREN_FEATURE_ENABLED is off, requesting with_descendants=True
|
||||
should be ignored server-side and children should not be duplicated, regardless
|
||||
of what the client sends.
|
||||
"""
|
||||
settings.DUPLICATE_CHILDREN_FEATURE_ENABLED = False
|
||||
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Create document tree
|
||||
root = factories.DocumentFactory(
|
||||
users=[(user, "owner")],
|
||||
title="Root",
|
||||
)
|
||||
# child
|
||||
factories.DocumentFactory(
|
||||
parent=root,
|
||||
title="Child",
|
||||
)
|
||||
|
||||
initial_count = models.Document.objects.count()
|
||||
assert initial_count == 2
|
||||
|
||||
# Duplicate requesting descendants while the feature is disabled
|
||||
with mock.patch("core.api.viewsets.posthog_capture") as mock_capture:
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{root.id!s}/duplicate/",
|
||||
{"with_descendants": True},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
duplicated_root = models.Document.objects.get(id=response.json()["id"])
|
||||
|
||||
mock_capture.assert_called_once_with(
|
||||
"doc_duplicated",
|
||||
user,
|
||||
{"duplicated_from": str(root.id)},
|
||||
document=duplicated_root,
|
||||
)
|
||||
|
||||
# Only root should be duplicated, not children
|
||||
assert models.Document.objects.count() == 3
|
||||
assert duplicated_root.get_children().count() == 0
|
||||
|
||||
|
||||
def test_api_documents_duplicate_with_descendants_preserves_link_configuration():
|
||||
"""
|
||||
Duplicating with descendants should preserve link configuration (link_reach, link_role)
|
||||
|
||||
@@ -29,6 +29,7 @@ pytestmark = pytest.mark.django_db
|
||||
COLLABORATION_WS_URL="http://testcollab/",
|
||||
COLLABORATION_WS_INACTIVITY_TIMEOUT=300,
|
||||
CONVERSION_UPLOAD_ENABLED=False,
|
||||
DUPLICATE_CHILDREN_FEATURE_ENABLED=False,
|
||||
FRONTEND_CSS_URL="http://testcss/",
|
||||
FRONTEND_JS_URL="http://testjs/",
|
||||
FRONTEND_THEME="test-theme",
|
||||
@@ -64,6 +65,7 @@ def test_api_config(is_authenticated):
|
||||
"CONVERSION_FILE_MAX_SIZE": 20971520,
|
||||
"CONVERSION_UPLOAD_ENABLED": False,
|
||||
"DOCUMENT_IMAGE_MAX_SIZE": 10485760,
|
||||
"DUPLICATE_CHILDREN_FEATURE_ENABLED": False,
|
||||
"ENVIRONMENT": "test",
|
||||
"FRONTEND_CSS_URL": "http://testcss/",
|
||||
"FRONTEND_HOMEPAGE_FEATURE_ENABLED": True,
|
||||
|
||||
@@ -211,6 +211,13 @@ class Base(Configuration):
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Allow duplicating a document together with its children
|
||||
DUPLICATE_CHILDREN_FEATURE_ENABLED = values.BooleanValue(
|
||||
default=True,
|
||||
environ_name="DUPLICATE_CHILDREN_FEATURE_ENABLED",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
DATA_UPLOAD_MAX_MEMORY_SIZE = values.IntegerValue(20 * MB) # 20 MB
|
||||
|
||||
REACTIONS_MAX_PER_COMMENT = values.IntegerValue(
|
||||
|
||||
@@ -26,6 +26,7 @@ export const CONFIG = {
|
||||
CONVERSION_FILE_EXTENSIONS_ALLOWED: ['.docx', '.md'],
|
||||
CONVERSION_FILE_MAX_SIZE: 20971520,
|
||||
DOCUMENT_IMAGE_MAX_SIZE: 10485760,
|
||||
DUPLICATE_CHILDREN_FEATURE_ENABLED: true,
|
||||
ENVIRONMENT: 'development',
|
||||
FRONTEND_CSS_URL: null,
|
||||
FRONTEND_JS_URL: null,
|
||||
|
||||
@@ -56,6 +56,7 @@ export interface ConfigResponse {
|
||||
CONVERSION_FILE_MAX_SIZE: number;
|
||||
CONVERSION_UPLOAD_ENABLED?: boolean;
|
||||
DOCUMENT_IMAGE_MAX_SIZE?: number;
|
||||
DUPLICATE_CHILDREN_FEATURE_ENABLED?: boolean;
|
||||
ENVIRONMENT: string;
|
||||
FRONTEND_CSS_URL?: string;
|
||||
FRONTEND_HOMEPAGE_FEATURE_ENABLED?: boolean;
|
||||
|
||||
+5
-1
@@ -15,6 +15,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box } from '@/components/Box';
|
||||
import { Text } from '@/components/Text';
|
||||
import { useConfig } from '@/core/config/api';
|
||||
import { useEditorStore } from '@/docs/doc-editor/stores/useEditorStore';
|
||||
import { getWordCount } from '@/docs/doc-editor/utils';
|
||||
import { printDocumentWithStyles } from '@/docs/doc-export/utils_print';
|
||||
@@ -158,8 +159,11 @@ const DocToolBoxComponent = ({
|
||||
const [isModalMoveOpen, setIsModalMoveOpen] = useState(false);
|
||||
const { onClick: onButtonClick, ...buttonPropsLeft } = buttonProps || {};
|
||||
const { isFeatureFlagActivated } = useAnalytics();
|
||||
const { data: conf } = useConfig();
|
||||
const duplicateWithChildrenAllowed = !!(
|
||||
isFeatureFlagActivated(DUPLICATE_WITH_CHILDREN_FEATURE_FLAG) && doc.numchild
|
||||
isFeatureFlagActivated(DUPLICATE_WITH_CHILDREN_FEATURE_FLAG) &&
|
||||
conf?.DUPLICATE_CHILDREN_FEATURE_ENABLED &&
|
||||
doc.numchild
|
||||
);
|
||||
|
||||
const editor = useEditorStore((state) => state.editor);
|
||||
|
||||
+39
-3
@@ -27,6 +27,11 @@ vi.mock('@/libs/Analytics', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const useConfigMock = vi.fn();
|
||||
vi.mock('@/core/config/api', () => ({
|
||||
useConfig: () => useConfigMock(),
|
||||
}));
|
||||
|
||||
const duplicateDocMock = vi.fn();
|
||||
vi.mock('@/docs/doc-management/components/ConfirmationDuplicateModal', () => ({
|
||||
useDuplicatedDoc: () => ({
|
||||
@@ -101,8 +106,11 @@ const openDuplicateOption = async () => {
|
||||
};
|
||||
|
||||
describe('<DocToolBox /> - duplicate with children', () => {
|
||||
test('opens the confirmation modal when the feature flag is active and the doc has children', async () => {
|
||||
test('opens the confirmation modal when the posthog flag and the backend setting are both active and the doc has children', async () => {
|
||||
isFeatureFlagActivatedMock.mockReturnValue(true);
|
||||
useConfigMock.mockReturnValue({
|
||||
data: { DUPLICATE_CHILDREN_FEATURE_ENABLED: true },
|
||||
});
|
||||
const doc = createDoc({ numchild: 3 });
|
||||
|
||||
render(<DocToolBox doc={doc} isCurrentDoc />, { wrapper: AppWrapper });
|
||||
@@ -117,8 +125,11 @@ describe('<DocToolBox /> - duplicate with children', () => {
|
||||
expect(duplicateDocMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('duplicates directly when the feature flag is inactive, even if the doc has children', async () => {
|
||||
test('duplicates directly when the posthog flag is inactive, even if the backend setting is active and the doc has children', async () => {
|
||||
isFeatureFlagActivatedMock.mockReturnValue(false);
|
||||
useConfigMock.mockReturnValue({
|
||||
data: { DUPLICATE_CHILDREN_FEATURE_ENABLED: true },
|
||||
});
|
||||
const doc = createDoc({ numchild: 3 });
|
||||
|
||||
render(<DocToolBox doc={doc} isCurrentDoc />, { wrapper: AppWrapper });
|
||||
@@ -136,8 +147,33 @@ describe('<DocToolBox /> - duplicate with children', () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('duplicates directly when the feature flag is active but the doc has no children', async () => {
|
||||
test('duplicates directly when the backend setting is inactive, even if the posthog flag is active and the doc has children', async () => {
|
||||
isFeatureFlagActivatedMock.mockReturnValue(true);
|
||||
useConfigMock.mockReturnValue({
|
||||
data: { DUPLICATE_CHILDREN_FEATURE_ENABLED: false },
|
||||
});
|
||||
const doc = createDoc({ numchild: 3 });
|
||||
|
||||
render(<DocToolBox doc={doc} isCurrentDoc />, { wrapper: AppWrapper });
|
||||
|
||||
await openDuplicateOption();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(duplicateDocMock).toHaveBeenCalledWith({
|
||||
docId: doc.id,
|
||||
canSave: doc.abilities.partial_update,
|
||||
});
|
||||
});
|
||||
expect(
|
||||
screen.queryByTestId('confirmation-duplicate-modal'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('duplicates directly when both flags are active but the doc has no children', async () => {
|
||||
isFeatureFlagActivatedMock.mockReturnValue(true);
|
||||
useConfigMock.mockReturnValue({
|
||||
data: { DUPLICATE_CHILDREN_FEATURE_ENABLED: true },
|
||||
});
|
||||
const doc = createDoc({ numchild: 0 });
|
||||
|
||||
render(<DocToolBox doc={doc} isCurrentDoc />, { wrapper: AppWrapper });
|
||||
|
||||
Reference in New Issue
Block a user