Compare commits

..

1 Commits

Author SHA1 Message Date
Anthony LC
07a6758cdc 🔖(minor) release 1.7.0
Added:
- 📝Contributing.md
- 🌐(frontend) add localization to editor
- Public and restricted doc editable
- (frontend) Add full name if available

Changed:
- ♻️(frontend) avoid documents indexing in search engine

Fixed:
- 🐛(backend) require right to manage document
  accesses to see invitations
- 🐛(i18n) same frontend and backend language using
  shared cookies
- 🐛(frontend) add default toolbar buttons
- 🐛(frontend) throttle error correctly display

Removed:
- 🔥(helm) remove infra related codes
2024-10-24 10:54:18 +02:00
21 changed files with 115 additions and 184 deletions

View File

@@ -17,13 +17,10 @@ and this project adheres to
- 🌐(frontend) add localization to editor #368
- ✨Public and restricted doc editable #357
- ✨(frontend) Add full name if available #380
- ✨(backend) Add view accesses ability #376
## Changed
- ♻️(frontend) list accesses if user has abilities #376
- ♻️(frontend) avoid documents indexing in search engine #372
- 👔(backend) doc restricted by default #388
## Fixed

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.2 on 2024-10-25 11:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0007_fix_users_duplicate'),
]
operations = [
migrations.AlterField(
model_name='document',
name='link_reach',
field=models.CharField(choices=[('restricted', 'Restricted'), ('authenticated', 'Authenticated'), ('public', 'Public')], default='restricted', max_length=20),
),
]

View File

@@ -336,7 +336,7 @@ class Document(BaseModel):
link_reach = models.CharField(
max_length=20,
choices=LinkReachChoices.choices,
default=LinkReachChoices.RESTRICTED,
default=LinkReachChoices.AUTHENTICATED,
)
link_role = models.CharField(
max_length=20, choices=LinkRoleChoices.choices, default=LinkRoleChoices.READER
@@ -496,8 +496,7 @@ class Document(BaseModel):
# Compute version roles before adding link roles because we don't
# want anonymous users to access versions (we wouldn't know from
# which date to allow them anyway)
# Anonymous users should also not see document accesses
has_role = bool(roles)
can_get_versions = bool(roles)
# Add role provided by the document link
if self.link_reach == LinkReachChoices.PUBLIC or (
@@ -512,20 +511,19 @@ class Document(BaseModel):
can_get = bool(roles)
return {
"accesses_manage": is_owner_or_admin,
"accesses_view": has_role,
"ai_transform": is_owner_or_admin or is_editor,
"ai_translate": is_owner_or_admin or is_editor,
"attachment_upload": is_owner_or_admin or is_editor,
"destroy": RoleChoices.OWNER in roles,
"link_configuration": is_owner_or_admin,
"manage_accesses": is_owner_or_admin,
"invite_owner": RoleChoices.OWNER in roles,
"partial_update": is_owner_or_admin or is_editor,
"retrieve": can_get,
"update": is_owner_or_admin or is_editor,
"versions_destroy": is_owner_or_admin,
"versions_list": has_role,
"versions_retrieve": has_role,
"versions_list": can_get_versions,
"versions_retrieve": can_get_versions,
}
def email_invitation(self, language, email, role, sender):
@@ -681,7 +679,7 @@ class Template(BaseModel):
return {
"destroy": RoleChoices.OWNER in roles,
"generate_document": can_get,
"accesses_manage": is_owner_or_admin,
"manage_accesses": is_owner_or_admin,
"update": is_owner_or_admin or is_editor,
"partial_update": is_owner_or_admin or is_editor,
"retrieve": can_get,

View File

@@ -47,7 +47,6 @@ def test_api_documents_create_authenticated_success():
assert response.status_code == 201
document = Document.objects.get()
assert document.title == "my document"
assert document.link_reach == "restricted"
assert document.accesses.filter(role="owner", user=user).exists()

View File

@@ -21,14 +21,13 @@ def test_api_documents_retrieve_anonymous_public():
assert response.json() == {
"id": str(document.id),
"abilities": {
"accesses_manage": False,
"accesses_view": False,
"ai_transform": document.link_role == "editor",
"ai_translate": document.link_role == "editor",
"attachment_upload": document.link_role == "editor",
"destroy": False,
"invite_owner": False,
"link_configuration": False,
"manage_accesses": False,
"partial_update": document.link_role == "editor",
"retrieve": True,
"update": document.link_role == "editor",
@@ -79,14 +78,13 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
assert response.json() == {
"id": str(document.id),
"abilities": {
"accesses_manage": False,
"accesses_view": False,
"ai_transform": document.link_role == "editor",
"ai_translate": document.link_role == "editor",
"attachment_upload": document.link_role == "editor",
"link_configuration": False,
"destroy": False,
"invite_owner": False,
"manage_accesses": False,
"partial_update": document.link_role == "editor",
"retrieve": True,
"update": document.link_role == "editor",

View File

@@ -22,7 +22,7 @@ def test_api_templates_retrieve_anonymous_public():
"abilities": {
"destroy": False,
"generate_document": True,
"accesses_manage": False,
"manage_accesses": False,
"partial_update": False,
"retrieve": True,
"update": False,
@@ -68,7 +68,7 @@ def test_api_templates_retrieve_authenticated_unrelated_public():
"abilities": {
"destroy": False,
"generate_document": True,
"accesses_manage": False,
"manage_accesses": False,
"partial_update": False,
"retrieve": True,
"update": False,

View File

@@ -83,14 +83,13 @@ def test_models_documents_get_abilities_forbidden(is_authenticated, reach, role)
user = factories.UserFactory() if is_authenticated else AnonymousUser()
abilities = document.get_abilities(user)
assert abilities == {
"accesses_manage": False,
"accesses_view": False,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,
"link_configuration": False,
"destroy": False,
"invite_owner": False,
"manage_accesses": False,
"partial_update": False,
"retrieve": False,
"update": False,
@@ -117,14 +116,13 @@ def test_models_documents_get_abilities_reader(is_authenticated, reach):
user = factories.UserFactory() if is_authenticated else AnonymousUser()
abilities = document.get_abilities(user)
assert abilities == {
"accesses_manage": False,
"accesses_view": False,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,
"destroy": False,
"link_configuration": False,
"invite_owner": False,
"manage_accesses": False,
"partial_update": False,
"retrieve": True,
"update": False,
@@ -151,14 +149,13 @@ def test_models_documents_get_abilities_editor(is_authenticated, reach):
user = factories.UserFactory() if is_authenticated else AnonymousUser()
abilities = document.get_abilities(user)
assert abilities == {
"accesses_manage": False,
"accesses_view": False,
"ai_transform": True,
"ai_translate": True,
"attachment_upload": True,
"destroy": False,
"link_configuration": False,
"invite_owner": False,
"manage_accesses": False,
"partial_update": True,
"retrieve": True,
"update": True,
@@ -174,14 +171,13 @@ def test_models_documents_get_abilities_owner():
access = factories.UserDocumentAccessFactory(role="owner", user=user)
abilities = access.document.get_abilities(access.user)
assert abilities == {
"accesses_manage": True,
"accesses_view": True,
"ai_transform": True,
"ai_translate": True,
"attachment_upload": True,
"destroy": True,
"link_configuration": True,
"invite_owner": True,
"manage_accesses": True,
"partial_update": True,
"retrieve": True,
"update": True,
@@ -196,14 +192,13 @@ def test_models_documents_get_abilities_administrator():
access = factories.UserDocumentAccessFactory(role="administrator")
abilities = access.document.get_abilities(access.user)
assert abilities == {
"accesses_manage": True,
"accesses_view": True,
"ai_transform": True,
"ai_translate": True,
"attachment_upload": True,
"destroy": False,
"link_configuration": True,
"invite_owner": False,
"manage_accesses": True,
"partial_update": True,
"retrieve": True,
"update": True,
@@ -221,14 +216,13 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
abilities = access.document.get_abilities(access.user)
assert abilities == {
"accesses_manage": False,
"accesses_view": True,
"ai_transform": True,
"ai_translate": True,
"attachment_upload": True,
"destroy": False,
"link_configuration": False,
"invite_owner": False,
"manage_accesses": False,
"partial_update": True,
"retrieve": True,
"update": True,
@@ -248,14 +242,13 @@ def test_models_documents_get_abilities_reader_user(django_assert_num_queries):
abilities = access.document.get_abilities(access.user)
assert abilities == {
"accesses_manage": False,
"accesses_view": True,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,
"destroy": False,
"link_configuration": False,
"invite_owner": False,
"manage_accesses": False,
"partial_update": False,
"retrieve": True,
"update": False,
@@ -276,14 +269,13 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
abilities = access.document.get_abilities(access.user)
assert abilities == {
"accesses_manage": False,
"accesses_view": True,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,
"destroy": False,
"link_configuration": False,
"invite_owner": False,
"manage_accesses": False,
"partial_update": False,
"retrieve": True,
"update": False,

View File

@@ -62,7 +62,7 @@ def test_models_templates_get_abilities_anonymous_public():
"destroy": False,
"retrieve": True,
"update": False,
"accesses_manage": False,
"manage_accesses": False,
"partial_update": False,
"generate_document": True,
}
@@ -76,7 +76,7 @@ def test_models_templates_get_abilities_anonymous_not_public():
"destroy": False,
"retrieve": False,
"update": False,
"accesses_manage": False,
"manage_accesses": False,
"partial_update": False,
"generate_document": False,
}
@@ -90,7 +90,7 @@ def test_models_templates_get_abilities_authenticated_public():
"destroy": False,
"retrieve": True,
"update": False,
"accesses_manage": False,
"manage_accesses": False,
"partial_update": False,
"generate_document": True,
}
@@ -104,7 +104,7 @@ def test_models_templates_get_abilities_authenticated_not_public():
"destroy": False,
"retrieve": False,
"update": False,
"accesses_manage": False,
"manage_accesses": False,
"partial_update": False,
"generate_document": False,
}
@@ -119,7 +119,7 @@ def test_models_templates_get_abilities_owner():
"destroy": True,
"retrieve": True,
"update": True,
"accesses_manage": True,
"manage_accesses": True,
"partial_update": True,
"generate_document": True,
}
@@ -133,7 +133,7 @@ def test_models_templates_get_abilities_administrator():
"destroy": False,
"retrieve": True,
"update": True,
"accesses_manage": True,
"manage_accesses": True,
"partial_update": True,
"generate_document": True,
}
@@ -150,7 +150,7 @@ def test_models_templates_get_abilities_editor_user(django_assert_num_queries):
"destroy": False,
"retrieve": True,
"update": True,
"accesses_manage": False,
"manage_accesses": False,
"partial_update": True,
"generate_document": True,
}
@@ -167,7 +167,7 @@ def test_models_templates_get_abilities_reader_user(django_assert_num_queries):
"destroy": False,
"retrieve": True,
"update": False,
"accesses_manage": False,
"manage_accesses": False,
"partial_update": False,
"generate_document": True,
}
@@ -185,7 +185,7 @@ def test_models_templates_get_abilities_preset_role(django_assert_num_queries):
"destroy": False,
"retrieve": True,
"update": False,
"accesses_manage": False,
"manage_accesses": False,
"partial_update": False,
"generate_document": True,
}

View File

@@ -144,7 +144,7 @@ export const mockedDocument = async (page: Page, json: object) => {
versions_destroy: false,
versions_list: true,
versions_retrieve: true,
accesses_manage: false, // Means not admin
manage_accesses: false, // Means not admin
update: false,
partial_update: false, // Means not editor
retrieve: true,

View File

@@ -215,7 +215,7 @@ test.describe('Doc Editor', () => {
versions_destroy: false,
versions_list: true,
versions_retrieve: true,
accesses_manage: false, // Means not admin
manage_accesses: false, // Means not admin
update: false,
partial_update: false, // Means not editor
retrieve: true,

View File

@@ -303,7 +303,7 @@ test.describe('Documents Grid mobile', () => {
attachment_upload: true,
destroy: true,
link_configuration: true,
accesses_manage: true,
manage_accesses: true,
partial_update: true,
retrieve: true,
update: true,

View File

@@ -45,7 +45,7 @@ test.describe('Doc Header', () => {
versions_destroy: true,
versions_list: true,
versions_retrieve: true,
accesses_manage: true,
manage_accesses: true,
update: true,
partial_update: true,
retrieve: true,
@@ -177,13 +177,12 @@ test.describe('Doc Header', () => {
test('it checks the options available if administrator', async ({ page }) => {
await mockedDocument(page, {
abilities: {
accesses_manage: true, // Means admin
accesses_view: true,
destroy: false, // Means not owner
link_configuration: true,
versions_destroy: true,
versions_list: true,
versions_retrieve: true,
manage_accesses: true, // Means admin
update: true,
partial_update: true,
retrieve: true,
@@ -248,13 +247,12 @@ test.describe('Doc Header', () => {
test('it checks the options available if editor', async ({ page }) => {
await mockedDocument(page, {
abilities: {
accesses_manage: false, // Means not admin
accesses_view: true,
destroy: false, // Means not owner
link_configuration: false,
versions_destroy: true,
versions_list: true,
versions_retrieve: true,
manage_accesses: false, // Means not admin
update: true,
partial_update: true, // Means editor
retrieve: true,
@@ -326,13 +324,12 @@ test.describe('Doc Header', () => {
test('it checks the options available if reader', async ({ page }) => {
await mockedDocument(page, {
abilities: {
accesses_manage: false, // Means not admin
accesses_view: true,
destroy: false, // Means not owner
link_configuration: false,
versions_destroy: false,
versions_list: true,
versions_retrieve: true,
manage_accesses: false, // Means not admin
update: false,
partial_update: false, // Means not editor
retrieve: true,
@@ -492,7 +489,7 @@ test.describe('Documents Header mobile', () => {
versions_destroy: true,
versions_list: true,
versions_retrieve: true,
accesses_manage: true,
manage_accesses: true,
update: true,
partial_update: true,
retrieve: true,

View File

@@ -40,20 +40,20 @@ test.describe('Doc Visibility', () => {
name: 'Visibility',
});
await expect(selectVisibility.getByText('Restricted')).toBeVisible();
await expect(selectVisibility.getByText('Authenticated')).toBeVisible();
await expect(page.getByLabel('Read only')).toBeHidden();
await expect(page.getByLabel('Can read and edit')).toBeHidden();
await expect(page.getByLabel('Read only')).toBeVisible();
await expect(page.getByLabel('Can read and edit')).toBeVisible();
await selectVisibility.click();
await page
.getByRole('option', {
name: 'Authenticated',
name: 'Restricted',
})
.click();
await expect(page.getByLabel('Read only')).toBeVisible();
await expect(page.getByLabel('Can read and edit')).toBeVisible();
await expect(page.getByLabel('Read only')).toBeHidden();
await expect(page.getByLabel('Can read and edit')).toBeHidden();
await selectVisibility.click();
@@ -87,6 +87,26 @@ test.describe('Doc Visibility: Restricted', () => {
await expect(page.getByRole('heading', { name: docTitle })).toBeVisible();
await page.getByRole('button', { name: 'Share' }).click();
await page
.getByRole('combobox', {
name: 'Visibility',
})
.click();
await page
.getByRole('option', {
name: 'Restricted',
})
.click();
await expect(
page.getByText('The document visibility has been updated.'),
).toBeVisible();
await page.locator('.c__modal__backdrop').click({
position: { x: 0, y: 0 },
});
const urlDoc = page.url();
await page
@@ -113,6 +133,26 @@ test.describe('Doc Visibility: Restricted', () => {
await expect(page.getByRole('heading', { name: docTitle })).toBeVisible();
await page.getByRole('button', { name: 'Share' }).click();
await page
.getByRole('combobox', {
name: 'Visibility',
})
.click();
await page
.getByRole('option', {
name: 'Restricted',
})
.click();
await expect(
page.getByText('The document visibility has been updated.'),
).toBeVisible();
await page.locator('.c__modal__backdrop').click({
position: { x: 0, y: 0 },
});
const urlDoc = page.url();
await page
@@ -142,6 +182,20 @@ test.describe('Doc Visibility: Restricted', () => {
await expect(page.getByRole('heading', { name: docTitle })).toBeVisible();
await page.getByRole('button', { name: 'Share' }).click();
await page
.getByRole('combobox', {
name: 'Visibility',
})
.click();
await page
.getByRole('option', {
name: 'Restricted',
})
.click();
await expect(
page.getByText('The document visibility has been updated.'),
).toBeVisible();
const inputSearch = page.getByLabel(/Find a member to add to the document/);
@@ -335,26 +389,6 @@ test.describe('Doc Visibility: Authenticated', () => {
await expect(page.getByRole('heading', { name: docTitle })).toBeVisible();
await page.getByRole('button', { name: 'Share' }).click();
await page
.getByRole('combobox', {
name: 'Visibility',
})
.click();
await page
.getByRole('option', {
name: 'Authenticated',
})
.click();
await expect(
page.getByText('The document visibility has been updated.'),
).toBeVisible();
await page.locator('.c__modal__backdrop').click({
position: { x: 0, y: 0 },
});
const urlDoc = page.url();
await page
@@ -387,26 +421,6 @@ test.describe('Doc Visibility: Authenticated', () => {
await expect(page.getByRole('heading', { name: docTitle })).toBeVisible();
await page.getByRole('button', { name: 'Share' }).click();
await page
.getByRole('combobox', {
name: 'Visibility',
})
.click();
await page
.getByRole('option', {
name: 'Authenticated',
})
.click();
await expect(
page.getByText('The document visibility has been updated.'),
).toBeVisible();
await page.locator('.c__modal__backdrop').click({
position: { x: 0, y: 0 },
});
const urlDoc = page.url();
await page
@@ -421,20 +435,10 @@ test.describe('Doc Visibility: Authenticated', () => {
await page.goto(urlDoc);
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
await page.getByRole('button', { name: 'Share' }).click();
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
await expect(
page.getByText('Read only, you cannot edit this document'),
).toBeVisible();
const shareModal = page.getByLabel('Share modal');
await expect(
shareModal.getByRole('combobox', {
name: 'Visibility',
}),
).toHaveAttribute('disabled');
await expect(shareModal.getByText('Search by email')).toBeHidden();
await expect(shareModal.getByLabel('List members card')).toBeHidden();
});
test('It checks a authenticated doc in editable mode', async ({
@@ -453,26 +457,6 @@ test.describe('Doc Visibility: Authenticated', () => {
await expect(page.getByRole('heading', { name: docTitle })).toBeVisible();
await page.getByRole('button', { name: 'Share' }).click();
await page
.getByRole('combobox', {
name: 'Visibility',
})
.click();
await page
.getByRole('option', {
name: 'Authenticated',
})
.click();
await expect(
page.getByText('The document visibility has been updated.'),
).toBeVisible();
await page.locator('.c__modal__backdrop').click({
position: { x: 0, y: 0 },
});
const urlDoc = page.url();
await page.getByRole('button', { name: 'Share' }).click();
@@ -499,19 +483,9 @@ test.describe('Doc Visibility: Authenticated', () => {
await page.goto(urlDoc);
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
await page.getByRole('button', { name: 'Share' }).click();
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
await expect(
page.getByText('Read only, you cannot edit this document'),
).toBeHidden();
const shareModal = page.getByLabel('Share modal');
await expect(
shareModal.getByRole('combobox', {
name: 'Visibility',
}),
).toHaveAttribute('disabled');
await expect(shareModal.getByText('Search by email')).toBeHidden();
await expect(shareModal.getByLabel('List members card')).toBeHidden();
});
});

View File

@@ -5,7 +5,7 @@ export interface Template {
abilities: {
destroy: boolean;
generate_document: boolean;
accesses_manage: boolean;
manage_accesses: boolean;
retrieve: boolean;
update: boolean;
partial_update: boolean;

View File

@@ -115,7 +115,7 @@ export const ModalShare = ({ onClose, doc }: ModalShareProps) => {
</Box>
</Card>
<DocVisibility doc={doc} />
{doc.abilities.accesses_manage && (
{doc.abilities.manage_accesses && (
<AddMembers
doc={doc}
currentRole={currentDocRole(doc.abilities)}
@@ -123,12 +123,8 @@ export const ModalShare = ({ onClose, doc }: ModalShareProps) => {
)}
</Box>
<Box $minHeight="0">
{doc.abilities.accesses_view && (
<>
<InvitationList doc={doc} />
<MemberList doc={doc} />
</>
)}
<InvitationList doc={doc} />
<MemberList doc={doc} />
</Box>
</Box>
</SideModal>

View File

@@ -44,11 +44,10 @@ export interface Doc {
created_at: string;
updated_at: string;
abilities: {
accesses_manage: boolean;
accesses_view: boolean;
attachment_upload: true;
destroy: boolean;
link_configuration: boolean;
manage_accesses: boolean;
partial_update: boolean;
retrieve: boolean;
update: boolean;

View File

@@ -3,7 +3,7 @@ import { Doc, Role } from './types';
export const currentDocRole = (abilities: Doc['abilities']): Role => {
return abilities.destroy
? Role.OWNER
: abilities.accesses_manage
: abilities.manage_accesses
? Role.ADMIN
: abilities.partial_update
? Role.EDITOR

View File

@@ -112,7 +112,7 @@ export const InvitationItem = ({
}}
/>
</Box>
{doc.abilities.accesses_manage && (
{doc.abilities.manage_accesses && (
<Box $margin={isSmallMobile ? 'auto' : ''}>
<Button
color="tertiary-text"

View File

@@ -170,14 +170,14 @@ export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
doc={doc}
setSelectedUsers={setSelectedUsers}
selectedUsers={selectedUsers}
disabled={isPending || !doc.abilities.accesses_manage}
disabled={isPending || !doc.abilities.manage_accesses}
/>
</Box>
<Box $css="flex: auto;">
<ChooseRole
key={resetKey}
currentRole={currentRole}
disabled={isPending || !doc.abilities.accesses_manage}
disabled={isPending || !doc.abilities.manage_accesses}
setRole={setSelectedRole}
/>
</Box>
@@ -189,7 +189,7 @@ export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
!selectedUsers.length ||
isPending ||
!selectedRole ||
!doc.abilities.accesses_manage
!doc.abilities.manage_accesses
}
onClick={() => void handleValidate()}
style={{ height: '100%', maxHeight: '55px' }}

View File

@@ -61,7 +61,7 @@ export const MemberItem = ({
});
const isNotAllowed =
isOtherOwner || isLastOwner || !doc.abilities.accesses_manage;
isOtherOwner || isLastOwner || !doc.abilities.manage_accesses;
if (!access.user) {
return (
@@ -112,7 +112,7 @@ export const MemberItem = ({
}}
/>
</Box>
{doc.abilities.accesses_manage && (
{doc.abilities.manage_accesses && (
<Box $margin={isSmallMobile ? 'auto' : ''}>
<Button
color="tertiary-text"
@@ -136,7 +136,7 @@ export const MemberItem = ({
<TextErrors causes={errorUpdate?.cause || errorDelete?.cause} />
</Box>
)}
{(isLastOwner || isOtherOwner) && doc.abilities.accesses_manage && (
{(isLastOwner || isOtherOwner) && doc.abilities.manage_accesses && (
<Box $margin={{ top: 'tiny' }}>
<Alert
canClose={false}

View File

@@ -194,17 +194,16 @@ export class ApiPlugin implements WorkboxPlugin {
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
abilities: {
accesses_manage: true,
accesses_view: true,
attachment_upload: true,
destroy: true,
link_configuration: true,
partial_update: true,
retrieve: true,
update: true,
versions_destroy: true,
versions_list: true,
versions_retrieve: true,
manage_accesses: true,
update: true,
partial_update: true,
retrieve: true,
attachment_upload: true,
},
accesses: [
{