Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to
### Added

- ✨(frontend) add customization for translations #857
- ✨(frontend) Duplicate a doc #1078
- 📝(project) add troubleshoot doc #1066
- 📝(project) add system-requirement doc #1066
- 🔧(front) configure x-frame-options to DENY in nginx conf #1084
Expand Down
4 changes: 3 additions & 1 deletion src/backend/core/api/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -954,6 +954,8 @@ def duplicate(self, request, *args, **kwargs):
)
serializer.is_valid(raise_exception=True)
with_accesses = serializer.validated_data.get("with_accesses", False)
roles = set(document.get_roles(request.user))
is_owner_or_admin = bool(roles.intersection(set(models.PRIVILEGED_ROLES)))

base64_yjs_content = document.content

Expand Down Expand Up @@ -985,7 +987,7 @@ def duplicate(self, request, *args, **kwargs):
]

# If accesses should be duplicated, add other users' accesses as per original document
if with_accesses:
if with_accesses and is_owner_or_admin:
original_accesses = models.DocumentAccess.objects.filter(
document=document
).exclude(user=request.user)
Expand Down
2 changes: 1 addition & 1 deletion src/backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -842,7 +842,7 @@ def get_abilities(self, user, ancestors_links=None):
"cors_proxy": can_get,
"descendants": can_get,
"destroy": is_owner,
"duplicate": can_get,
"duplicate": can_get and user.is_authenticated,
"favorite": can_get and user.is_authenticated,
"link_configuration": is_owner_or_admin,
"invite_owner": is_owner,
Expand Down
52 changes: 48 additions & 4 deletions src/backend/core/tests/documents/test_api_documents_duplicate.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def test_api_documents_duplicate_forbidden():
def test_api_documents_duplicate_anonymous():
"""Anonymous users should not be able to duplicate documents even with read access."""

document = factories.DocumentFactory(link_reach="public")
document = factories.DocumentFactory(link_reach="public", link_role="reader")

response = APIClient().post(f"/api/v1.0/documents/{document.id!s}/duplicate/")

Expand Down Expand Up @@ -168,14 +168,17 @@ def test_api_documents_duplicate_success(index):
assert response.status_code == 403


def test_api_documents_duplicate_with_accesses():
"""Accesses should be duplicated if the user requests it specifically."""
@pytest.mark.parametrize("role", ["owner", "administrator"])
def test_api_documents_duplicate_with_accesses_admin(role):
"""
Accesses should be duplicated if the user requests it specifically and is owner or admin.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)

document = factories.DocumentFactory(
users=[user],
users=[(user, role)],
title="document with accesses",
)
user_access = factories.UserDocumentAccessFactory(document=document)
Expand Down Expand Up @@ -205,3 +208,44 @@ def test_api_documents_duplicate_with_accesses():
assert duplicated_accesses.get(user=user).role == "owner"
assert duplicated_accesses.get(user=user_access.user).role == user_access.role
assert duplicated_accesses.get(team=team_access.team).role == team_access.role


@pytest.mark.parametrize("role", ["editor", "reader"])
def test_api_documents_duplicate_with_accesses_non_admin(role):
"""
Accesses should not be duplicated if the user requests it specifically and is not owner
or admin.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)

document = factories.DocumentFactory(
users=[(user, role)],
title="document with accesses",
)
factories.UserDocumentAccessFactory(document=document)
factories.TeamDocumentAccessFactory(document=document)

# Duplicate the document via the API endpoint requesting to duplicate accesses
response = client.post(
f"/api/v1.0/documents/{document.id!s}/duplicate/",
{"with_accesses": True},
format="json",
)

assert response.status_code == 201

duplicated_document = models.Document.objects.get(id=response.json()["id"])
assert duplicated_document.title == "Copy of document with accesses"
assert duplicated_document.content == document.content
assert duplicated_document.link_reach == document.link_reach
assert duplicated_document.link_role == document.link_role
assert duplicated_document.creator == user
assert duplicated_document.duplicated_from == document
assert duplicated_document.attachments == []

# Check that accesses were duplicated and the user who did the duplicate is forced as owner
duplicated_accesses = duplicated_document.accesses
assert duplicated_accesses.count() == 1
assert duplicated_accesses.get(user=user).role == "owner"
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def test_api_documents_retrieve_anonymous_public_standalone():
"cors_proxy": True,
"descendants": True,
"destroy": False,
"duplicate": True,
"duplicate": False,
# Anonymous user can't favorite a document even with read access
"favorite": False,
"invite_owner": False,
Expand Down Expand Up @@ -105,7 +105,7 @@ def test_api_documents_retrieve_anonymous_public_parent():
"descendants": True,
"cors_proxy": True,
"destroy": False,
"duplicate": True,
"duplicate": False,
# Anonymous user can't favorite a document even with read access
"favorite": False,
"invite_owner": False,
Expand Down
4 changes: 2 additions & 2 deletions src/backend/core/tests/test_models_documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ def test_models_documents_get_abilities_reader(
"descendants": True,
"cors_proxy": True,
"destroy": False,
"duplicate": True,
"duplicate": is_authenticated,
"favorite": is_authenticated,
"invite_owner": False,
"link_configuration": False,
Expand Down Expand Up @@ -285,7 +285,7 @@ def test_models_documents_get_abilities_editor(
"descendants": True,
"cors_proxy": True,
"destroy": False,
"duplicate": True,
"duplicate": is_authenticated,
"favorite": is_authenticated,
"invite_owner": False,
"link_configuration": False,
Expand Down
31 changes: 30 additions & 1 deletion src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ test.describe('Doc Header', () => {
});

test('it pins a document', async ({ page, browserName }) => {
const [docTitle] = await createDoc(page, `Favorite doc`, browserName);
const [docTitle] = await createDoc(page, `Pin doc`, browserName);

await page.getByLabel('Open the document options').click();

Expand Down Expand Up @@ -456,6 +456,35 @@ test.describe('Doc Header', () => {
await expect(row.getByLabel('Pin document icon')).toBeHidden();
await expect(leftPanelFavorites.getByText(docTitle)).toBeHidden();
});

test('it duplicates a document', async ({ page, browserName }) => {
const [docTitle] = await createDoc(page, `Duplicate doc`, browserName);

const editor = page.locator('.ProseMirror');
await editor.click();
await editor.fill('Hello Duplicated World');

await page.getByLabel('Open the document options').click();

await page.getByRole('menuitem', { name: 'Duplicate' }).click();
await expect(
page.getByText('Document duplicated successfully!'),
).toBeVisible();

await page.goto('/');

const duplicateTitle = 'Copy of ' + docTitle;

const row = await getGridRow(page, duplicateTitle);

await expect(row.getByText(duplicateTitle)).toBeVisible();

await row.getByText(`more_horiz`).click();
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
const duplicateDuplicateTitle = 'Copy of ' + duplicateTitle;
await page.getByText(duplicateDuplicateTitle).click();
await expect(page.getByText('Hello Duplicated World')).toBeVisible();
});
});

test.describe('Documents Header mobile', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const originalEnv = process.env.NEXT_PUBLIC_PUBLISH_AS_MIT;

jest.mock('@/features/docs/doc-export/utils', () => ({
anything: true,
}));
jest.mock('@/features/docs/doc-export/components/ModalExport', () => ({
ModalExport: () => <span>ModalExport</span>,
}));

describe('useModuleExport', () => {
afterAll(() => {
process.env.NEXT_PUBLIC_PUBLISH_AS_MIT = originalEnv;
});

afterEach(() => {
jest.clearAllMocks();
jest.resetModules();
});

it('should return undefined when NEXT_PUBLIC_PUBLISH_AS_MIT is true', async () => {
process.env.NEXT_PUBLIC_PUBLISH_AS_MIT = 'true';
const Export = await import('@/features/docs/doc-export/');

expect(Export.default).toBeUndefined();
});

it('should load modules when NEXT_PUBLIC_PUBLISH_AS_MIT is false', async () => {
process.env.NEXT_PUBLIC_PUBLISH_AS_MIT = 'false';
const Export = await import('@/features/docs/doc-export/');

expect(Export.default).toHaveProperty('ModalExport');
});
});
19 changes: 18 additions & 1 deletion src/frontend/apps/impress/src/features/docs/doc-export/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
/**
* To import Export modules you must import from the index file.
* This is to ensure that the Export modules are only loaded when
* the application is not published as MIT.
*/
export * from './api';
export * from './components';
export * from './utils';

import * as ModalExport from './components/ModalExport';

let modulesExport = undefined;
if (process.env.NEXT_PUBLIC_PUBLISH_AS_MIT === 'false') {
modulesExport = {
...ModalExport,
};
}

type ModulesExport = typeof modulesExport;

export default modulesExport as ModulesExport;

This file was deleted.

This file was deleted.

Loading
Loading