diff --git a/.github/workflows/impress-frontend.yml b/.github/workflows/impress-frontend.yml index f869517d..ab7fcce9 100644 --- a/.github/workflows/impress-frontend.yml +++ b/.github/workflows/impress-frontend.yml @@ -88,28 +88,6 @@ jobs: - name: Start Docker services run: make bootstrap FLUSH_ARGS='--no-input' cache= - # Tool to wait for a service to be ready - - name: Install Dockerize - run: | - curl -sSL https://github.com/jwilder/dockerize/releases/download/v0.8.0/dockerize-linux-amd64-v0.8.0.tar.gz | sudo tar -C /usr/local/bin -xzv - - - name: Wait for services to be ready - run: | - printf "Minio check...\n" - dockerize -wait tcp://localhost:9000 -timeout 20s - printf "Keyclock check...\n" - dockerize -wait tcp://localhost:8080 -timeout 20s - printf "Server collaboration check...\n" - dockerize -wait tcp://localhost:4444 -timeout 20s - printf "Ngnix check...\n" - dockerize -wait tcp://localhost:8083 -timeout 20s - printf "DRF check...\n" - dockerize -wait tcp://localhost:8071 -timeout 20s - printf "Postgres Keyclock check...\n" - dockerize -wait tcp://localhost:5433 -timeout 20s - printf "Postgres back check...\n" - dockerize -wait tcp://localhost:15432 -timeout 20s - - name: Run e2e tests run: cd src/frontend/ && yarn e2e:test --project='chromium' diff --git a/CHANGELOG.md b/CHANGELOG.md index 2576d8ba..5ffda055 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to ## [Unreleased] +## Changed + +- πŸ“(doc) minor README.md formatting and wording enhancements +- ♻️Stop setting a default title on doc creation #634 + + ## [2.2.0] - 2025-02-10 ## Added diff --git a/Makefile b/Makefile index 0e81087b..ea5fdbc5 100644 --- a/Makefile +++ b/Makefile @@ -44,7 +44,6 @@ COMPOSE_EXEC_APP = $(COMPOSE_EXEC) app-dev COMPOSE_RUN = $(COMPOSE) run --rm COMPOSE_RUN_APP = $(COMPOSE_RUN) app-dev COMPOSE_RUN_CROWDIN = $(COMPOSE_RUN) crowdin crowdin -WAIT_DB = @$(COMPOSE_RUN) dockerize -wait tcp://$(DB_HOST):$(DB_PORT) -timeout 60s # -- Backend MANAGE = $(COMPOSE_RUN_APP) python manage.py @@ -81,12 +80,12 @@ bootstrap: \ data/static \ create-env-files \ build \ - run-with-frontend \ migrate \ demo \ back-i18n-compile \ mails-install \ - mails-build + mails-build \ + run .PHONY: bootstrap # -- Docker/compose @@ -109,7 +108,7 @@ build-yjs-provider: ## build the y-provider container build-frontend: cache ?= build-frontend: ## build the frontend container - @$(COMPOSE) build frontend-dev $(cache) + @$(COMPOSE) build frontend $(cache) .PHONY: build-frontend down: ## stop and remove containers, networks, images, and volumes @@ -120,18 +119,17 @@ logs: ## display app-dev logs (follow mode) @$(COMPOSE) logs -f app-dev .PHONY: logs -run: ## start the wsgi (production) and development server +run-backend: ## Start only the backend application and all needed services @$(COMPOSE) up --force-recreate -d celery-dev @$(COMPOSE) up --force-recreate -d y-provider @$(COMPOSE) up --force-recreate -d nginx - @echo "Wait for postgresql to be up..." - @$(WAIT_DB) -.PHONY: run +.PHONY: run-backend -run-with-frontend: ## Start all the containers needed (backend to frontend) - @$(MAKE) run - @$(COMPOSE) up --force-recreate -d frontend-dev -.PHONY: run-with-frontend +run: ## start the wsgi (production) and development server +run: + @$(MAKE) run-backend + @$(COMPOSE) up --force-recreate -d frontend +.PHONY: run status: ## an alias for "docker compose ps" @$(COMPOSE) ps @@ -188,14 +186,12 @@ test-back-parallel: ## run all back-end tests in parallel makemigrations: ## run django makemigrations for the impress project. @echo "$(BOLD)Running makemigrations$(RESET)" @$(COMPOSE) up -d postgresql - @$(WAIT_DB) @$(MANAGE) makemigrations .PHONY: makemigrations migrate: ## run django migrations for the impress project. @echo "$(BOLD)Running migrations$(RESET)" @$(COMPOSE) up -d postgresql - @$(WAIT_DB) @$(MANAGE) migrate .PHONY: migrate @@ -310,16 +306,16 @@ help: .PHONY: help # Front -frontend-install: ## install the frontend locally +frontend-development-install: ## install the frontend locally cd $(PATH_FRONT_IMPRESS) && yarn -.PHONY: frontend-install +.PHONY: frontend-development-install frontend-lint: ## run the frontend linter cd $(PATH_FRONT) && yarn lint .PHONY: frontend-lint run-frontend-development: ## Run the frontend in development mode - @$(COMPOSE) stop frontend-dev + @$(COMPOSE) stop frontend cd $(PATH_FRONT_IMPRESS) && yarn dev .PHONY: run-frontend-development diff --git a/README.md b/README.md index 8d6c88c8..c024440b 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Welcome to Docs! The open source document editor where your notes can become kno ## Why use Docs ❓ + Docs is a collaborative text editor designed to address common challenges in knowledge building and sharing. ### Write @@ -33,23 +34,31 @@ Docs is a collaborative text editor designed to address common challenges in kno * ✨ Save time thanks to our AI actions (generate, sum up, correct, translate) ### Collaborate -* 🀝 Collaborate in realtime with your team mates -* πŸ”’ Granular access control to keep your information secure and shared with the right people +* 🀝 Collaborate with your team in real time +* πŸ”’ Granular access control to ensure your information is secure and only shared with the right people * πŸ“‘ Professional document exports in multiple formats (.odt, .doc, .pdf) with customizable templates -* πŸ“š Built-in wiki functionality to transform your team's collaborative work into organized knowledge `ETA 02/2025` +* πŸ“š Built-in wiki functionality to turn your team's collaborative work into organized knowledge `ETA 02/2025` ### Self-host * πŸš€ Easy to install, scalable and secure alternative to Notion, Outline or Confluence ## Getting started πŸ”§ + ### Test it + Test Docs on your browser by logging in on this [environment](https://impress-preprod.beta.numerique.gouv.fr/docs/0aa856e9-da41-4d59-b73d-a61cb2c1245f/) + ``` email: test.docs@yopmail.com password: I'd<3ToTestDocs ``` + ### Run it locally + +> ⚠️ Running Docs locally using the methods described below is for testing purposes only. It is based on building Docs using Minio as the S3 storage solution: if you want to use Minio for production deployment of Docs, you will need to comply with Minio's AGPL-3.0 licence. + **Prerequisite** + Make sure you have a recent version of Docker and [Docker Compose](https://docs.docker.com/compose/install) installed on your laptop: ```shellscript @@ -57,23 +66,22 @@ $ docker -v Docker version 20.10.2, build 2291f61 -$ docker compose -v +$ docker compose version -docker compose version 1.27.4, build 40524192 +Docker Compose version v2.32.4 ``` > ⚠️ You may need to run the following commands with sudo but this can be avoided by adding your user to the `docker` group. **Project bootstrap** + The easiest way to start working on the project is to use GNU Make: ```shellscript $ make bootstrap FLUSH_ARGS='--no-input' ``` -This command builds the `app` container, installs dependencies, performs database migrations and compile translations. It's a good idea to use this - -command each time you are pulling code from the project repository to avoid dependency-related or migration-related issues. +This command builds the `app` container, installs dependencies, performs database migrations and compile translations. It's a good idea to use this command each time you are pulling code from the project repository to avoid dependency-related or migration-related issues. Your Docker services should now be up and running πŸŽ‰ @@ -89,7 +97,7 @@ password: impress πŸ“ Note that if you need to run them afterwards, you can use the eponym Make rule: ```shellscript -$ make run-with-frontend +$ make run ``` ⚠️ For the frontend developer, it is often better to run the frontend in development mode locally. @@ -97,7 +105,7 @@ $ make run-with-frontend To do so, install the frontend dependencies with the following command: ```shellscript -$ make frontend-install +$ make frontend-development-install ``` And run the frontend locally in development mode with the following command: @@ -109,7 +117,7 @@ $ make run-frontend-development To start all the services, except the frontend container, you can use the following command: ```shellscript -$ make run +$ make run-backend ``` **Adding content** @@ -126,6 +134,7 @@ $ make help ``` **Django admin** + You can access the Django admin site at . @@ -137,17 +146,21 @@ $ make superuser ``` ## Feedback πŸ™‹β€β™‚οΈπŸ™‹β€β™€οΈ + We'd love to hear your thoughts and hear about your experiments, so come and say hi on [Matrix](https://matrix.to/#/#docs-official:matrix.org). ## Roadmap + Want to know where the project is headed? [πŸ—ΊοΈ Checkout our roadmap](https://github.com/orgs/numerique-gouv/projects/13/views/11) ## Licence πŸ“ + This work is released under the MIT License (see [LICENSE](https://github.com/suitenumerique/docs/blob/main/LICENSE)). While Docs is a public driven initiative our licence choice is an invitation for private sector actors to use, sell and contribute to the project. ## Contributing πŸ™Œ + This project is intended to be community-driven, so please, do not hesitate to [get in touch](https://matrix.to/#/#docs-official:matrix.org) if you have any question related to our implementation or design decisions. You can help us with translations on [Crowdin](https://crowdin.com/project/lasuite-docs). @@ -169,10 +182,13 @@ docs ``` ## Credits ❀️ + ### Stack -Docs is built on top of [Django Rest Framework](https://www.django-rest-framework.org/), [Next.js](https://nextjs.org/), [MinIO](https://min.io/), [BlockNote.js](https://www.blocknotejs.org/), [HocusPocus](https://tiptap.dev/docs/hocuspocus/introduction) and [Yjs](https://yjs.dev/) + +Docs is built on top of [Django Rest Framework](https://www.django-rest-framework.org/), [Next.js](https://nextjs.org/), [BlockNote.js](https://www.blocknotejs.org/), [HocusPocus](https://tiptap.dev/docs/hocuspocus/introduction) and [Yjs](https://yjs.dev/). ### Gov ❀️ open source + Docs is the result of a joint effort led by the French πŸ‡«πŸ‡·πŸ₯– ([DINUM](https://www.numerique.gouv.fr/dinum/)) and German πŸ‡©πŸ‡ͺπŸ₯¨ governments ([ZenDiS](https://zendis.de/)). We are proud sponsors of [BlockNotejs](https://www.blocknotejs.org/) and [Yjs](https://yjs.dev/). diff --git a/docker-compose.yml b/docker-compose.yml index e10221b9..84ad1b03 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,11 @@ services: postgresql: image: postgres:16 + healthcheck: + test: ["CMD-SHELL", "pg_isready"] + interval: 1s + timeout: 2s + retries: 300 env_file: - env.d/development/postgresql ports: @@ -15,7 +20,7 @@ services: - "1081:1080" minio: - # user: ${DOCKER_USER:-1000} + user: ${DOCKER_USER:-1000} image: minio/minio environment: - MINIO_ROOT_USER=impress @@ -23,6 +28,11 @@ services: ports: - '9000:9000' - '9001:9001' + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 1s + timeout: 20s + retries: 300 entrypoint: "" command: minio server --console-address :9001 /data volumes: @@ -31,7 +41,9 @@ services: createbuckets: image: minio/mc depends_on: - - minio + minio: + condition: service_healthy + restart: true entrypoint: > sh -c " /usr/bin/mc alias set impress http://minio:9000 impress password && \ @@ -59,10 +71,15 @@ services: - ./src/backend:/app - ./data/static:/data/static depends_on: - - postgresql - - mailcatcher - - redis - - createbuckets + postgresql: + condition: service_healthy + restart: true + mailcatcher: + condition: service_started + redis: + condition: service_started + createbuckets: + condition: service_started celery-dev: user: ${DOCKER_USER:-1000} @@ -93,9 +110,13 @@ services: - env.d/development/common - env.d/development/postgresql depends_on: - - postgresql - - redis - - minio + postgresql: + condition: service_healthy + restart: true + redis: + condition: service_started + minio: + condition: service_started celery: user: ${DOCKER_USER:-1000} @@ -116,11 +137,15 @@ services: volumes: - ./docker/files/etc/nginx/conf.d:/etc/nginx/conf.d:ro depends_on: - - keycloak - - app-dev - - y-provider + app-dev: + condition: service_started + y-provider: + condition: service_started + keycloak: + condition: service_healthy + restart: true - frontend-dev: + frontend: user: "${DOCKER_USER:-1000}" build: context: . @@ -135,9 +160,6 @@ services: ports: - "3000:3000" - dockerize: - image: jwilder/dockerize - crowdin: image: crowdin/cli:3.16.0 volumes: @@ -169,6 +191,11 @@ services: kc_postgresql: image: postgres:14.3 + healthcheck: + test: ["CMD-SHELL", "pg_isready"] + interval: 1s + timeout: 2s + retries: 300 ports: - "5433:5432" env_file: @@ -187,6 +214,13 @@ services: - --hostname-admin-url=http://localhost:8083/ - --hostname-strict=false - --hostname-strict-https=false + - --health-enabled=true + - --metrics-enabled=true + healthcheck: + test: ["CMD", "curl", "--head", "-fsS", "http://localhost:8080/health/ready"] + interval: 1s + timeout: 2s + retries: 300 environment: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin @@ -200,4 +234,6 @@ services: ports: - "8080:8080" depends_on: - - kc_postgresql + kc_postgresql: + condition: service_healthy + restart: true diff --git a/docker/files/etc/nginx/conf.d/default.conf b/docker/files/etc/nginx/conf.d/default.conf index 2644822c..e5967e33 100644 --- a/docker/files/etc/nginx/conf.d/default.conf +++ b/docker/files/etc/nginx/conf.d/default.conf @@ -88,5 +88,11 @@ server { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # Increase proxy buffer size to allow keycloak to send large + # header responses when a user is created. + proxy_buffer_size 128k; + proxy_buffers 4 256k; + proxy_busy_buffers_size 256k; } } diff --git a/src/backend/core/migrations/0001_initial.py b/src/backend/core/migrations/0001_initial.py index 38bdb4f3..7f4d80ab 100644 --- a/src/backend/core/migrations/0001_initial.py +++ b/src/backend/core/migrations/0001_initial.py @@ -1,166 +1,552 @@ # Generated by Django 5.0.3 on 2024-05-28 20:29 +import uuid + import django.contrib.auth.models import django.core.validators import django.db.models.deletion -import timezone_field.fields -import uuid from django.conf import settings from django.db import migrations, models +import timezone_field.fields + class Migration(migrations.Migration): - initial = True dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), + ("auth", "0012_alter_user_first_name_max_length"), ] operations = [ migrations.CreateModel( - name='Document', + name="Document", fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')), - ('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')), - ('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')), - ('title', models.CharField(max_length=255, verbose_name='title')), - ('is_public', models.BooleanField(default=False, help_text='Whether this document is public for anyone to use.', verbose_name='public')), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + help_text="primary key for the record as UUID", + primary_key=True, + serialize=False, + verbose_name="id", + ), + ), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, + help_text="date and time at which a record was created", + verbose_name="created on", + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, + help_text="date and time at which a record was last updated", + verbose_name="updated on", + ), + ), + ("title", models.CharField(max_length=255, verbose_name="title")), + ( + "is_public", + models.BooleanField( + default=False, + help_text="Whether this document is public for anyone to use.", + verbose_name="public", + ), + ), ], options={ - 'verbose_name': 'Document', - 'verbose_name_plural': 'Documents', - 'db_table': 'impress_document', - 'ordering': ('title',), + "verbose_name": "Document", + "verbose_name_plural": "Documents", + "db_table": "impress_document", + "ordering": ("title",), }, ), migrations.CreateModel( - name='Template', + name="Template", fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')), - ('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')), - ('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')), - ('title', models.CharField(max_length=255, verbose_name='title')), - ('description', models.TextField(blank=True, verbose_name='description')), - ('code', models.TextField(blank=True, verbose_name='code')), - ('css', models.TextField(blank=True, verbose_name='css')), - ('is_public', models.BooleanField(default=False, help_text='Whether this template is public for anyone to use.', verbose_name='public')), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + help_text="primary key for the record as UUID", + primary_key=True, + serialize=False, + verbose_name="id", + ), + ), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, + help_text="date and time at which a record was created", + verbose_name="created on", + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, + help_text="date and time at which a record was last updated", + verbose_name="updated on", + ), + ), + ("title", models.CharField(max_length=255, verbose_name="title")), + ( + "description", + models.TextField(blank=True, verbose_name="description"), + ), + ("code", models.TextField(blank=True, verbose_name="code")), + ("css", models.TextField(blank=True, verbose_name="css")), + ( + "is_public", + models.BooleanField( + default=False, + help_text="Whether this template is public for anyone to use.", + verbose_name="public", + ), + ), ], options={ - 'verbose_name': 'Template', - 'verbose_name_plural': 'Templates', - 'db_table': 'impress_template', - 'ordering': ('title',), + "verbose_name": "Template", + "verbose_name_plural": "Templates", + "db_table": "impress_template", + "ordering": ("title",), }, ), migrations.CreateModel( - name='User', + name="User", fields=[ - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')), - ('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')), - ('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')), - ('sub', models.CharField(blank=True, help_text='Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ characters only.', max_length=255, null=True, unique=True, validators=[django.core.validators.RegexValidator(message='Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_ characters.', regex='^[\\w.@+-]+\\Z')], verbose_name='sub')), - ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='identity email address')), - ('admin_email', models.EmailField(blank=True, max_length=254, null=True, unique=True, verbose_name='admin email address')), - ('language', models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language')), - ('timezone', timezone_field.fields.TimeZoneField(choices_display='WITH_GMT_OFFSET', default='UTC', help_text='The timezone in which the user wants to see times.', use_pytz=False)), - ('is_device', models.BooleanField(default=False, help_text='Whether the user is a device or a real user.', verbose_name='device')), - ('is_staff', models.BooleanField(default=False, help_text='Whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + help_text="primary key for the record as UUID", + primary_key=True, + serialize=False, + verbose_name="id", + ), + ), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, + help_text="date and time at which a record was created", + verbose_name="created on", + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, + help_text="date and time at which a record was last updated", + verbose_name="updated on", + ), + ), + ( + "sub", + models.CharField( + blank=True, + help_text="Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ characters only.", + max_length=255, + null=True, + unique=True, + validators=[ + django.core.validators.RegexValidator( + message="Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_ characters.", + regex="^[\\w.@+-]+\\Z", + ) + ], + verbose_name="sub", + ), + ), + ( + "email", + models.EmailField( + blank=True, + max_length=254, + null=True, + verbose_name="identity email address", + ), + ), + ( + "admin_email", + models.EmailField( + blank=True, + max_length=254, + null=True, + unique=True, + verbose_name="admin email address", + ), + ), + ( + "language", + models.CharField( + choices="(('en-us', 'English'), ('fr-fr', 'French'))", + default="en-us", + help_text="The language in which the user wants to see the interface.", + max_length=10, + verbose_name="language", + ), + ), + ( + "timezone", + timezone_field.fields.TimeZoneField( + choices_display="WITH_GMT_OFFSET", + default="UTC", + help_text="The timezone in which the user wants to see times.", + use_pytz=False, + ), + ), + ( + "is_device", + models.BooleanField( + default=False, + help_text="Whether the user is a device or a real user.", + verbose_name="device", + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), ], options={ - 'verbose_name': 'user', - 'verbose_name_plural': 'users', - 'db_table': 'impress_user', + "verbose_name": "user", + "verbose_name_plural": "users", + "db_table": "impress_user", }, managers=[ - ('objects', django.contrib.auth.models.UserManager()), + ("objects", django.contrib.auth.models.UserManager()), ], ), migrations.CreateModel( - name='DocumentAccess', + name="DocumentAccess", fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')), - ('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')), - ('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')), - ('team', models.CharField(blank=True, max_length=100)), - ('role', models.CharField(choices=[('reader', 'Reader'), ('editor', 'Editor'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='reader', max_length=20)), - ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accesses', to='core.document')), - ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + help_text="primary key for the record as UUID", + primary_key=True, + serialize=False, + verbose_name="id", + ), + ), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, + help_text="date and time at which a record was created", + verbose_name="created on", + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, + help_text="date and time at which a record was last updated", + verbose_name="updated on", + ), + ), + ("team", models.CharField(blank=True, max_length=100)), + ( + "role", + models.CharField( + choices=[ + ("reader", "Reader"), + ("editor", "Editor"), + ("administrator", "Administrator"), + ("owner", "Owner"), + ], + default="reader", + max_length=20, + ), + ), + ( + "document", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="accesses", + to="core.document", + ), + ), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'verbose_name': 'Document/user relation', - 'verbose_name_plural': 'Document/user relations', - 'db_table': 'impress_document_access', - 'ordering': ('-created_at',), + "verbose_name": "Document/user relation", + "verbose_name_plural": "Document/user relations", + "db_table": "impress_document_access", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='Invitation', + name="Invitation", fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')), - ('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')), - ('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')), - ('email', models.EmailField(max_length=254, verbose_name='email address')), - ('role', models.CharField(choices=[('reader', 'Reader'), ('editor', 'Editor'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='reader', max_length=20)), - ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to='core.document')), - ('issuer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + help_text="primary key for the record as UUID", + primary_key=True, + serialize=False, + verbose_name="id", + ), + ), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, + help_text="date and time at which a record was created", + verbose_name="created on", + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, + help_text="date and time at which a record was last updated", + verbose_name="updated on", + ), + ), + ( + "email", + models.EmailField(max_length=254, verbose_name="email address"), + ), + ( + "role", + models.CharField( + choices=[ + ("reader", "Reader"), + ("editor", "Editor"), + ("administrator", "Administrator"), + ("owner", "Owner"), + ], + default="reader", + max_length=20, + ), + ), + ( + "document", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="invitations", + to="core.document", + ), + ), + ( + "issuer", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="invitations", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'verbose_name': 'Document invitation', - 'verbose_name_plural': 'Document invitations', - 'db_table': 'impress_invitation', + "verbose_name": "Document invitation", + "verbose_name_plural": "Document invitations", + "db_table": "impress_invitation", }, ), migrations.CreateModel( - name='TemplateAccess', + name="TemplateAccess", fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')), - ('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')), - ('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')), - ('team', models.CharField(blank=True, max_length=100)), - ('role', models.CharField(choices=[('reader', 'Reader'), ('editor', 'Editor'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='reader', max_length=20)), - ('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accesses', to='core.template')), - ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + help_text="primary key for the record as UUID", + primary_key=True, + serialize=False, + verbose_name="id", + ), + ), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, + help_text="date and time at which a record was created", + verbose_name="created on", + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, + help_text="date and time at which a record was last updated", + verbose_name="updated on", + ), + ), + ("team", models.CharField(blank=True, max_length=100)), + ( + "role", + models.CharField( + choices=[ + ("reader", "Reader"), + ("editor", "Editor"), + ("administrator", "Administrator"), + ("owner", "Owner"), + ], + default="reader", + max_length=20, + ), + ), + ( + "template", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="accesses", + to="core.template", + ), + ), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'verbose_name': 'Template/user relation', - 'verbose_name_plural': 'Template/user relations', - 'db_table': 'impress_template_access', - 'ordering': ('-created_at',), + "verbose_name": "Template/user relation", + "verbose_name_plural": "Template/user relations", + "db_table": "impress_template_access", + "ordering": ("-created_at",), }, ), migrations.AddConstraint( - model_name='documentaccess', - constraint=models.UniqueConstraint(condition=models.Q(('user__isnull', False)), fields=('user', 'document'), name='unique_document_user', violation_error_message='This user is already in this document.'), + model_name="documentaccess", + constraint=models.UniqueConstraint( + condition=models.Q(("user__isnull", False)), + fields=("user", "document"), + name="unique_document_user", + violation_error_message="This user is already in this document.", + ), ), migrations.AddConstraint( - model_name='documentaccess', - constraint=models.UniqueConstraint(condition=models.Q(('team__gt', '')), fields=('team', 'document'), name='unique_document_team', violation_error_message='This team is already in this document.'), + model_name="documentaccess", + constraint=models.UniqueConstraint( + condition=models.Q(("team__gt", "")), + fields=("team", "document"), + name="unique_document_team", + violation_error_message="This team is already in this document.", + ), ), migrations.AddConstraint( - model_name='documentaccess', - constraint=models.CheckConstraint(check=models.Q(models.Q(('team', ''), ('user__isnull', False)), models.Q(('team__gt', ''), ('user__isnull', True)), _connector='OR'), name='check_document_access_either_user_or_team', violation_error_message='Either user or team must be set, not both.'), + model_name="documentaccess", + constraint=models.CheckConstraint( + check=models.Q( + models.Q(("team", ""), ("user__isnull", False)), + models.Q(("team__gt", ""), ("user__isnull", True)), + _connector="OR", + ), + name="check_document_access_either_user_or_team", + violation_error_message="Either user or team must be set, not both.", + ), ), migrations.AddConstraint( - model_name='invitation', - constraint=models.UniqueConstraint(fields=('email', 'document'), name='email_and_document_unique_together'), + model_name="invitation", + constraint=models.UniqueConstraint( + fields=("email", "document"), name="email_and_document_unique_together" + ), ), migrations.AddConstraint( - model_name='templateaccess', - constraint=models.UniqueConstraint(condition=models.Q(('user__isnull', False)), fields=('user', 'template'), name='unique_template_user', violation_error_message='This user is already in this template.'), + model_name="templateaccess", + constraint=models.UniqueConstraint( + condition=models.Q(("user__isnull", False)), + fields=("user", "template"), + name="unique_template_user", + violation_error_message="This user is already in this template.", + ), ), migrations.AddConstraint( - model_name='templateaccess', - constraint=models.UniqueConstraint(condition=models.Q(('team__gt', '')), fields=('team', 'template'), name='unique_template_team', violation_error_message='This team is already in this template.'), + model_name="templateaccess", + constraint=models.UniqueConstraint( + condition=models.Q(("team__gt", "")), + fields=("team", "template"), + name="unique_template_team", + violation_error_message="This team is already in this template.", + ), ), migrations.AddConstraint( - model_name='templateaccess', - constraint=models.CheckConstraint(check=models.Q(models.Q(('team', ''), ('user__isnull', False)), models.Q(('team__gt', ''), ('user__isnull', True)), _connector='OR'), name='check_template_access_either_user_or_team', violation_error_message='Either user or team must be set, not both.'), + model_name="templateaccess", + constraint=models.CheckConstraint( + check=models.Q( + models.Q(("team", ""), ("user__isnull", False)), + models.Q(("team__gt", ""), ("user__isnull", True)), + _connector="OR", + ), + name="check_template_access_either_user_or_team", + violation_error_message="Either user or team must be set, not both.", + ), ), ] diff --git a/src/backend/core/migrations/0002_create_pg_trgm_extension.py b/src/backend/core/migrations/0002_create_pg_trgm_extension.py index 64f58842..ff92a7d5 100644 --- a/src/backend/core/migrations/0002_create_pg_trgm_extension.py +++ b/src/backend/core/migrations/0002_create_pg_trgm_extension.py @@ -1,9 +1,9 @@ from django.db import migrations -class Migration(migrations.Migration): +class Migration(migrations.Migration): dependencies = [ - ('core', '0001_initial'), + ("core", "0001_initial"), ] operations = [ diff --git a/src/backend/core/migrations/0003_document_link_reach_document_link_role_and_more.py b/src/backend/core/migrations/0003_document_link_reach_document_link_role_and_more.py index 71a4434a..02104a11 100644 --- a/src/backend/core/migrations/0003_document_link_reach_document_link_role_and_more.py +++ b/src/backend/core/migrations/0003_document_link_reach_document_link_role_and_more.py @@ -1,52 +1,114 @@ # Generated by Django 5.1 on 2024-09-08 16:55 -import django.db.models.deletion import uuid + +import django.db.models.deletion from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('core', '0002_create_pg_trgm_extension'), + ("core", "0002_create_pg_trgm_extension"), ] operations = [ migrations.AddField( - model_name='document', - name='link_reach', - field=models.CharField(choices=[('restricted', 'Restricted'), ('authenticated', 'Authenticated'), ('public', 'Public')], default='authenticated', max_length=20), + model_name="document", + name="link_reach", + field=models.CharField( + choices=[ + ("restricted", "Restricted"), + ("authenticated", "Authenticated"), + ("public", "Public"), + ], + default="authenticated", + max_length=20, + ), ), migrations.AddField( - model_name='document', - name='link_role', - field=models.CharField(choices=[('reader', 'Reader'), ('editor', 'Editor')], default='reader', max_length=20), + model_name="document", + name="link_role", + field=models.CharField( + choices=[("reader", "Reader"), ("editor", "Editor")], + default="reader", + max_length=20, + ), ), migrations.AlterField( - model_name='document', - name='is_public', + model_name="document", + name="is_public", field=models.BooleanField(null=True), ), migrations.AlterField( - model_name='user', - name='language', - field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'), + model_name="user", + name="language", + field=models.CharField( + choices="(('en-us', 'English'), ('fr-fr', 'French'))", + default="en-us", + help_text="The language in which the user wants to see the interface.", + max_length=10, + verbose_name="language", + ), ), migrations.CreateModel( - name='LinkTrace', + name="LinkTrace", fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')), - ('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')), - ('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')), - ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='link_traces', to='core.document')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='link_traces', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + help_text="primary key for the record as UUID", + primary_key=True, + serialize=False, + verbose_name="id", + ), + ), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, + help_text="date and time at which a record was created", + verbose_name="created on", + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, + help_text="date and time at which a record was last updated", + verbose_name="updated on", + ), + ), + ( + "document", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="link_traces", + to="core.document", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="link_traces", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'verbose_name': 'Document/user link trace', - 'verbose_name_plural': 'Document/user link traces', - 'db_table': 'impress_link_trace', - 'constraints': [models.UniqueConstraint(fields=('user', 'document'), name='unique_link_trace_document_user', violation_error_message='A link trace already exists for this document/user.')], + "verbose_name": "Document/user link trace", + "verbose_name_plural": "Document/user link traces", + "db_table": "impress_link_trace", + "constraints": [ + models.UniqueConstraint( + fields=("user", "document"), + name="unique_link_trace_document_user", + violation_error_message="A link trace already exists for this document/user.", + ) + ], }, ), ] diff --git a/src/backend/core/migrations/0004_migrate_is_public_to_link_reach.py b/src/backend/core/migrations/0004_migrate_is_public_to_link_reach.py index 3e5184db..8c0b21e0 100644 --- a/src/backend/core/migrations/0004_migrate_is_public_to_link_reach.py +++ b/src/backend/core/migrations/0004_migrate_is_public_to_link_reach.py @@ -1,13 +1,14 @@ # Generated by Django 5.1 on 2024-09-08 17:04 from django.db import migrations + def migrate_is_public_to_link_reach(apps, schema_editor): """ Forward migration: Migrate 'is_public' to 'link_reach'. If is_public == True, set link_reach to 'public' """ - Document = apps.get_model('core', 'Document') - Document.objects.filter(is_public=True).update(link_reach='public') + Document = apps.get_model("core", "Document") + Document.objects.filter(is_public=True).update(link_reach="public") def reverse_migrate_link_reach_to_is_public(apps, schema_editor): @@ -16,20 +17,20 @@ def reverse_migrate_link_reach_to_is_public(apps, schema_editor): - If link_reach == 'public', set is_public to True - Else set is_public to False """ - Document = apps.get_model('core', 'Document') - Document.objects.filter(link_reach='public').update(is_public=True) - Document.objects.filter(link_reach__in=['restricted', "authenticated"]).update(is_public=False) + Document = apps.get_model("core", "Document") + Document.objects.filter(link_reach="public").update(is_public=True) + Document.objects.filter(link_reach__in=["restricted", "authenticated"]).update( + is_public=False + ) class Migration(migrations.Migration): - dependencies = [ - ('core', '0003_document_link_reach_document_link_role_and_more'), + ("core", "0003_document_link_reach_document_link_role_and_more"), ] operations = [ migrations.RunPython( - migrate_is_public_to_link_reach, - reverse_migrate_link_reach_to_is_public + migrate_is_public_to_link_reach, reverse_migrate_link_reach_to_is_public ), ] diff --git a/src/backend/core/migrations/0005_remove_document_is_public_alter_document_link_reach_and_more.py b/src/backend/core/migrations/0005_remove_document_is_public_alter_document_link_reach_and_more.py index 49c3983a..75436a68 100644 --- a/src/backend/core/migrations/0005_remove_document_is_public_alter_document_link_reach_and_more.py +++ b/src/backend/core/migrations/0005_remove_document_is_public_alter_document_link_reach_and_more.py @@ -4,15 +4,16 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('core', '0004_migrate_is_public_to_link_reach'), + ("core", "0004_migrate_is_public_to_link_reach"), ] operations = [ migrations.AlterField( - model_name='document', - name='title', - field=models.CharField(blank=True, max_length=255, null=True, verbose_name='title'), + model_name="document", + name="title", + field=models.CharField( + blank=True, max_length=255, null=True, verbose_name="title" + ), ), ] diff --git a/src/backend/core/migrations/0006_add_user_full_name_and_short_name.py b/src/backend/core/migrations/0006_add_user_full_name_and_short_name.py index dd5e3b7c..d3e8e78e 100644 --- a/src/backend/core/migrations/0006_add_user_full_name_and_short_name.py +++ b/src/backend/core/migrations/0006_add_user_full_name_and_short_name.py @@ -4,25 +4,34 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('core', '0005_remove_document_is_public_alter_document_link_reach_and_more'), + ("core", "0005_remove_document_is_public_alter_document_link_reach_and_more"), ] operations = [ migrations.AddField( - model_name='user', - name='full_name', - field=models.CharField(blank=True, max_length=100, null=True, verbose_name='full name'), + model_name="user", + name="full_name", + field=models.CharField( + blank=True, max_length=100, null=True, verbose_name="full name" + ), ), migrations.AddField( - model_name='user', - name='short_name', - field=models.CharField(blank=True, max_length=20, null=True, verbose_name='short name'), + model_name="user", + name="short_name", + field=models.CharField( + blank=True, max_length=20, null=True, verbose_name="short name" + ), ), migrations.AlterField( - model_name='user', - name='language', - field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'), + model_name="user", + name="language", + field=models.CharField( + choices="(('en-us', 'English'), ('fr-fr', 'French'))", + default="en-us", + help_text="The language in which the user wants to see the interface.", + max_length=10, + verbose_name="language", + ), ), ] diff --git a/src/backend/core/migrations/0007_fix_users_duplicate.py b/src/backend/core/migrations/0007_fix_users_duplicate.py index 50139299..3eec88fe 100644 --- a/src/backend/core/migrations/0007_fix_users_duplicate.py +++ b/src/backend/core/migrations/0007_fix_users_duplicate.py @@ -117,10 +117,10 @@ BEGIN END $$; """ -class Migration(migrations.Migration): +class Migration(migrations.Migration): dependencies = [ - ('core', '0006_add_user_full_name_and_short_name'), + ("core", "0006_add_user_full_name_and_short_name"), ] operations = [ diff --git a/src/backend/core/migrations/0008_alter_document_link_reach.py b/src/backend/core/migrations/0008_alter_document_link_reach.py index 001269af..334cdd48 100644 --- a/src/backend/core/migrations/0008_alter_document_link_reach.py +++ b/src/backend/core/migrations/0008_alter_document_link_reach.py @@ -4,15 +4,22 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('core', '0007_fix_users_duplicate'), + ("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), + model_name="document", + name="link_reach", + field=models.CharField( + choices=[ + ("restricted", "Restricted"), + ("authenticated", "Authenticated"), + ("public", "Public"), + ], + default="restricted", + max_length=20, + ), ), ] diff --git a/src/backend/core/migrations/0009_add_document_favorite.py b/src/backend/core/migrations/0009_add_document_favorite.py index 9254f9fb..4e137b83 100644 --- a/src/backend/core/migrations/0009_add_document_favorite.py +++ b/src/backend/core/migrations/0009_add_document_favorite.py @@ -1,37 +1,87 @@ # Generated by Django 5.1.2 on 2024-11-08 07:59 -import django.db.models.deletion import uuid + +import django.db.models.deletion from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('core', '0008_alter_document_link_reach'), + ("core", "0008_alter_document_link_reach"), ] operations = [ migrations.AlterField( - model_name='user', - name='language', - field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'), + model_name="user", + name="language", + field=models.CharField( + choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))", + default="en-us", + help_text="The language in which the user wants to see the interface.", + max_length=10, + verbose_name="language", + ), ), migrations.CreateModel( - name='DocumentFavorite', + name="DocumentFavorite", fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')), - ('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')), - ('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')), - ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorited_by_users', to='core.document')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorite_documents', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + help_text="primary key for the record as UUID", + primary_key=True, + serialize=False, + verbose_name="id", + ), + ), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, + help_text="date and time at which a record was created", + verbose_name="created on", + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, + help_text="date and time at which a record was last updated", + verbose_name="updated on", + ), + ), + ( + "document", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="favorited_by_users", + to="core.document", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="favorite_documents", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'verbose_name': 'Document favorite', - 'verbose_name_plural': 'Document favorites', - 'db_table': 'impress_document_favorite', - 'constraints': [models.UniqueConstraint(fields=('user', 'document'), name='unique_document_favorite_user', violation_error_message='This document is already targeted by a favorite relation instance for the same user.')], + "verbose_name": "Document favorite", + "verbose_name_plural": "Document favorites", + "db_table": "impress_document_favorite", + "constraints": [ + models.UniqueConstraint( + fields=("user", "document"), + name="unique_document_favorite_user", + violation_error_message="This document is already targeted by a favorite relation instance for the same user.", + ) + ], }, ), ] diff --git a/src/backend/core/migrations/0010_add_field_creator_to_document.py b/src/backend/core/migrations/0010_add_field_creator_to_document.py index 0fa619e2..2d8c1c1b 100644 --- a/src/backend/core/migrations/0010_add_field_creator_to_document.py +++ b/src/backend/core/migrations/0010_add_field_creator_to_document.py @@ -7,25 +7,48 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('core', '0009_add_document_favorite'), + ("core", "0009_add_document_favorite"), ] operations = [ migrations.AddField( - model_name='document', - name='creator', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.RESTRICT, related_name='documents_created', to=settings.AUTH_USER_MODEL), + model_name="document", + name="creator", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="documents_created", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( - model_name='user', - name='language', - field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'), + model_name="user", + name="language", + field=models.CharField( + choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))", + default="en-us", + help_text="The language in which the user wants to see the interface.", + max_length=10, + verbose_name="language", + ), ), migrations.AlterField( - model_name='user', - name='sub', - field=models.CharField(blank=True, help_text='Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only.', max_length=255, null=True, unique=True, validators=[django.core.validators.RegexValidator(message='Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters.', regex='^[\\w.@+-:]+\\Z')], verbose_name='sub'), + model_name="user", + name="sub", + field=models.CharField( + blank=True, + help_text="Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only.", + max_length=255, + null=True, + unique=True, + validators=[ + django.core.validators.RegexValidator( + message="Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters.", + regex="^[\\w.@+-:]+\\Z", + ) + ], + verbose_name="sub", + ), ), ] diff --git a/src/backend/core/migrations/0011_populate_creator_field_and_make_it_required.py b/src/backend/core/migrations/0011_populate_creator_field_and_make_it_required.py index 1895993d..62b1c9f2 100644 --- a/src/backend/core/migrations/0011_populate_creator_field_and_make_it_required.py +++ b/src/backend/core/migrations/0011_populate_creator_field_and_make_it_required.py @@ -3,7 +3,7 @@ import django.db.models.deletion from django.conf import settings from django.db import migrations -from django.db.models import F, ForeignKey, Subquery, OuterRef, Q +from django.db.models import F, ForeignKey, OuterRef, Q, Subquery def set_creator_from_document_access(apps, schema_editor): @@ -25,28 +25,37 @@ def set_creator_from_document_access(apps, schema_editor): DocumentAccess = apps.get_model("core", "DocumentAccess") # Update `creator` using the "owner" role - owner_subquery = DocumentAccess.objects.filter( - document=OuterRef('pk'), - user__isnull=False, - role='owner', - ).order_by('created_at').values('user_id')[:1] + owner_subquery = ( + DocumentAccess.objects.filter( + document=OuterRef("pk"), + user__isnull=False, + role="owner", + ) + .order_by("created_at") + .values("user_id")[:1] + ) - Document.objects.filter( - creator__isnull=True - ).update(creator=Subquery(owner_subquery)) + Document.objects.filter(creator__isnull=True).update( + creator=Subquery(owner_subquery) + ) class Migration(migrations.Migration): - dependencies = [ - ('core', '0010_add_field_creator_to_document'), + ("core", "0010_add_field_creator_to_document"), ] operations = [ - migrations.RunPython(set_creator_from_document_access, reverse_code=migrations.RunPython.noop), + migrations.RunPython( + set_creator_from_document_access, reverse_code=migrations.RunPython.noop + ), migrations.AlterField( - model_name='document', - name='creator', - field=ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='documents_created', to=settings.AUTH_USER_MODEL), + model_name="document", + name="creator", + field=ForeignKey( + on_delete=django.db.models.deletion.RESTRICT, + related_name="documents_created", + to=settings.AUTH_USER_MODEL, + ), ), ] diff --git a/src/backend/core/migrations/0012_make_document_creator_and_invitation_issuer_optional.py b/src/backend/core/migrations/0012_make_document_creator_and_invitation_issuer_optional.py index b0902896..f10d2bc3 100644 --- a/src/backend/core/migrations/0012_make_document_creator_and_invitation_issuer_optional.py +++ b/src/backend/core/migrations/0012_make_document_creator_and_invitation_issuer_optional.py @@ -6,25 +6,42 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('core', '0011_populate_creator_field_and_make_it_required'), + ("core", "0011_populate_creator_field_and_make_it_required"), ] operations = [ migrations.AlterField( - model_name='document', - name='creator', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.RESTRICT, related_name='documents_created', to=settings.AUTH_USER_MODEL), + model_name="document", + name="creator", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="documents_created", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( - model_name='invitation', - name='issuer', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to=settings.AUTH_USER_MODEL), + model_name="invitation", + name="issuer", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="invitations", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( - model_name='user', - name='language', - field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'), + model_name="user", + name="language", + field=models.CharField( + choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))", + default="en-us", + help_text="The language in which the user wants to see the interface.", + max_length=10, + verbose_name="language", + ), ), ] diff --git a/src/backend/core/migrations/0013_activate_fuzzystrmatch_extension.py b/src/backend/core/migrations/0013_activate_fuzzystrmatch_extension.py index db7fbb3a..01b4dc6f 100644 --- a/src/backend/core/migrations/0013_activate_fuzzystrmatch_extension.py +++ b/src/backend/core/migrations/0013_activate_fuzzystrmatch_extension.py @@ -2,10 +2,10 @@ from django.db import migrations -class Migration(migrations.Migration): +class Migration(migrations.Migration): dependencies = [ - ('core', '0012_make_document_creator_and_invitation_issuer_optional'), + ("core", "0012_make_document_creator_and_invitation_issuer_optional"), ] operations = [ diff --git a/src/backend/core/migrations/0014_add_tree_structure_to_documents.py b/src/backend/core/migrations/0014_add_tree_structure_to_documents.py index bd237f74..c99473ab 100644 --- a/src/backend/core/migrations/0014_add_tree_structure_to_documents.py +++ b/src/backend/core/migrations/0014_add_tree_structure_to_documents.py @@ -4,28 +4,29 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('core', '0013_activate_fuzzystrmatch_extension'), + ("core", "0013_activate_fuzzystrmatch_extension"), ] operations = [ migrations.AddField( - model_name='document', - name='depth', + model_name="document", + name="depth", field=models.PositiveIntegerField(default=0), preserve_default=False, ), migrations.AddField( - model_name='document', - name='numchild', + model_name="document", + name="numchild", field=models.PositiveIntegerField(default=0), ), migrations.AddField( - model_name='document', - name='path', + model_name="document", + name="path", # Allow null values pending the next datamigration to populate the field - field=models.CharField(db_collation='C', max_length=252, null=True, unique=True), + field=models.CharField( + db_collation="C", max_length=252, null=True, unique=True + ), preserve_default=False, ), ] diff --git a/src/backend/core/migrations/0015_set_path_on_existing_documents.py b/src/backend/core/migrations/0015_set_path_on_existing_documents.py index 250d8dbd..1c3081fc 100644 --- a/src/backend/core/migrations/0015_set_path_on_existing_documents.py +++ b/src/backend/core/migrations/0015_set_path_on_existing_documents.py @@ -7,9 +7,10 @@ from treebeard.numconv import NumConv ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" STEPLEN = 7 + def set_path_on_existing_documents(apps, schema_editor): """ - Updates the `path` and `depth` fields for all existing Document records + Updates the `path` and `depth` fields for all existing Document records to ensure valid materialized paths. This function assigns a unique `path` to each Document as a root node @@ -26,27 +27,25 @@ def set_path_on_existing_documents(apps, schema_editor): updates = [] for i, pk in enumerate(documents): key = numconv.int2str(i) - path = "{0}{1}".format( - ALPHABET[0] * (STEPLEN - len(key)), - key - ) + path = "{0}{1}".format(ALPHABET[0] * (STEPLEN - len(key)), key) updates.append(Document(pk=pk, path=path, depth=1)) # Bulk update using the prepared updates list - Document.objects.bulk_update(updates, ['depth', 'path']) + Document.objects.bulk_update(updates, ["depth", "path"]) class Migration(migrations.Migration): - dependencies = [ - ('core', '0014_add_tree_structure_to_documents'), + ("core", "0014_add_tree_structure_to_documents"), ] operations = [ - migrations.RunPython(set_path_on_existing_documents, reverse_code=migrations.RunPython.noop), + migrations.RunPython( + set_path_on_existing_documents, reverse_code=migrations.RunPython.noop + ), migrations.AlterField( - model_name='document', - name='path', - field=models.CharField(db_collation='C', max_length=252, unique=True), + model_name="document", + name="path", + field=models.CharField(db_collation="C", max_length=252, unique=True), ), ] diff --git a/src/backend/core/migrations/0016_add_document_excerpt.py b/src/backend/core/migrations/0016_add_document_excerpt.py index 302cd260..8a107789 100644 --- a/src/backend/core/migrations/0016_add_document_excerpt.py +++ b/src/backend/core/migrations/0016_add_document_excerpt.py @@ -4,20 +4,27 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('core', '0015_set_path_on_existing_documents'), + ("core", "0015_set_path_on_existing_documents"), ] operations = [ migrations.AddField( - model_name='document', - name='excerpt', - field=models.TextField(blank=True, max_length=300, null=True, verbose_name='excerpt'), + model_name="document", + name="excerpt", + field=models.TextField( + blank=True, max_length=300, null=True, verbose_name="excerpt" + ), ), migrations.AlterField( - model_name='user', - name='language', - field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'), + model_name="user", + name="language", + field=models.CharField( + choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))", + default="en-us", + help_text="The language in which the user wants to see the interface.", + max_length=10, + verbose_name="language", + ), ), ] diff --git a/src/backend/core/migrations/0017_add_fields_for_soft_delete.py b/src/backend/core/migrations/0017_add_fields_for_soft_delete.py index 54c9df5c..00cd8a90 100644 --- a/src/backend/core/migrations/0017_add_fields_for_soft_delete.py +++ b/src/backend/core/migrations/0017_add_fields_for_soft_delete.py @@ -4,33 +4,49 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('core', '0016_add_document_excerpt'), + ("core", "0016_add_document_excerpt"), ] operations = [ migrations.AlterModelOptions( - name='document', - options={'ordering': ('path',), 'verbose_name': 'Document', 'verbose_name_plural': 'Documents'}, + name="document", + options={ + "ordering": ("path",), + "verbose_name": "Document", + "verbose_name_plural": "Documents", + }, ), migrations.AddField( - model_name='document', - name='ancestors_deleted_at', + model_name="document", + name="ancestors_deleted_at", field=models.DateTimeField(blank=True, null=True), ), migrations.AddField( - model_name='document', - name='deleted_at', + model_name="document", + name="deleted_at", field=models.DateTimeField(blank=True, null=True), ), migrations.AlterField( - model_name='user', - name='language', - field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'), + model_name="user", + name="language", + field=models.CharField( + choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))", + default="en-us", + help_text="The language in which the user wants to see the interface.", + max_length=10, + verbose_name="language", + ), ), migrations.AddConstraint( - model_name='document', - constraint=models.CheckConstraint(condition=models.Q(('deleted_at__isnull', True), ('deleted_at', models.F('ancestors_deleted_at')), _connector='OR'), name='check_deleted_at_matches_ancestors_deleted_at_when_set'), + model_name="document", + constraint=models.CheckConstraint( + condition=models.Q( + ("deleted_at__isnull", True), + ("deleted_at", models.F("ancestors_deleted_at")), + _connector="OR", + ), + name="check_deleted_at_matches_ancestors_deleted_at_when_set", + ), ), ] diff --git a/src/backend/core/migrations/0018_update_blank_title.py b/src/backend/core/migrations/0018_update_blank_title.py new file mode 100644 index 00000000..9576f53a --- /dev/null +++ b/src/backend/core/migrations/0018_update_blank_title.py @@ -0,0 +1,24 @@ +from django.db import migrations + + +def update_titles_to_null(apps, schema_editor): + """ + If the titles are "Untitled document" or "Unbenanntes Dokument" or "Document sans titre" + we set them to Null + """ + Document = apps.get_model("core", "Document") + Document.objects.filter( + title__in=["Untitled document", "Unbenanntes Dokument", "Document sans titre"] + ).update(title=None) + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0017_add_fields_for_soft_delete"), + ] + + operations = [ + migrations.RunPython( + update_titles_to_null, reverse_code=migrations.RunPython.noop + ), + ] diff --git a/src/backend/core/tests/migrations/__init__.py b/src/backend/core/tests/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/backend/core/tests/migrations/test_0018_update_blank_title.py b/src/backend/core/tests/migrations/test_0018_update_blank_title.py new file mode 100644 index 00000000..1ac36c6e --- /dev/null +++ b/src/backend/core/tests/migrations/test_0018_update_blank_title.py @@ -0,0 +1,35 @@ +import pytest + +from core import factories + + +@pytest.mark.django_db +def test_update_blank_title_migration(migrator): + """ + Test that the migration fixes the titles of documents that are + "Untitled document", "Unbenanntes Dokument" or "Document sans titre" + """ + migrator.apply_initial_migration(("core", "0017_add_fields_for_soft_delete")) + + english_doc = factories.DocumentFactory(title="Untitled document") + german_doc = factories.DocumentFactory(title="Unbenanntes Dokument") + french_doc = factories.DocumentFactory(title="Document sans titre") + other_doc = factories.DocumentFactory(title="My document") + + assert english_doc.title == "Untitled document" + assert german_doc.title == "Unbenanntes Dokument" + assert french_doc.title == "Document sans titre" + assert other_doc.title == "My document" + + # Apply the migration + migrator.apply_tested_migration(("core", "0018_update_blank_title")) + + english_doc.refresh_from_db() + german_doc.refresh_from_db() + french_doc.refresh_from_db() + other_doc.refresh_from_db() + + assert english_doc.title == None + assert german_doc.title == None + assert french_doc.title == None + assert other_doc.title == "My document" diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index 519badd5..a7fe9a36 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -68,6 +68,7 @@ dependencies = [ [project.optional-dependencies] dev = [ "django-extensions==3.2.3", + "django-test-migrations==1.4.0", "drf-spectacular-sidecar==2024.12.1", "freezegun==1.5.1", "ipdb==0.13.13", @@ -99,7 +100,6 @@ exclude = [ "build", "venv", "__pycache__", - "*/migrations/*", ] line-length = 88 diff --git a/src/frontend/apps/e2e/__tests__/app-impress/header.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/header.spec.ts index 00a45625..7234b1fd 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/header.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/header.spec.ts @@ -88,6 +88,7 @@ test.describe('Header mobile', () => { test.describe('Header: Log out', () => { test.use({ storageState: { cookies: [], origins: [] } }); + // eslint-disable-next-line playwright/expect-expect test('checks logout button', async ({ page, browserName }) => { await page.goto('/'); await keyCloakSignIn(page, browserName); diff --git a/src/frontend/apps/impress/src/assets/icons/icon-docs.svg b/src/frontend/apps/impress/src/assets/icons/icon-docs.svg index 430eb0e0..05cf0436 100644 --- a/src/frontend/apps/impress/src/assets/icons/icon-docs.svg +++ b/src/frontend/apps/impress/src/assets/icons/icon-docs.svg @@ -1,10 +1,4 @@ - + css` user-select: none; position: absolute; top: -17px; + left: 0px; padding: 0px 6px; border-radius: 0px; white-space: nowrap; diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx index 37e2fdf9..9290bde5 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx @@ -57,16 +57,13 @@ const DocTitleInput = ({ doc }: DocTitleProps) => { const [titleDisplay, setTitleDisplay] = useState(doc.title); const { toast } = useToastProvider(); const { untitledDocument } = useTrans(); - const isUntitled = titleDisplay === untitledDocument; const { broadcast } = useBroadcastStore(); const { mutate: updateDoc } = useUpdateDoc({ listInvalideQueries: [KEY_DOC, KEY_LIST_DOC], onSuccess(data) { - if (data.title !== untitledDocument) { - toast(t('Document title updated successfully'), VariantType.SUCCESS); - } + toast(t('Document title updated successfully'), VariantType.SUCCESS); // Broadcast to every user connected to the document broadcast(`${KEY_DOC}-${data.id}`); @@ -80,8 +77,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => { // When blank we set to untitled if (!sanitizedTitle) { - sanitizedTitle = untitledDocument; - setTitleDisplay(sanitizedTitle); + setTitleDisplay(''); } // If mutation we update @@ -90,7 +86,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => { updateDoc({ id: doc.id, title: sanitizedTitle }); } }, - [doc.id, doc.title, untitledDocument, updateDoc], + [doc.id, doc.title, updateDoc], ); const handleKeyDown = (e: React.KeyboardEvent) => { @@ -111,7 +107,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => { as="span" role="textbox" contentEditable - defaultValue={isUntitled ? undefined : titleDisplay} + defaultValue={titleDisplay || undefined} onKeyDownCapture={handleKeyDown} suppressContentEditableWarning={true} aria-label="doc title input" @@ -135,7 +131,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => { outline: none; `} > - {isUntitled ? '' : titleDisplay} + {titleDisplay} diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateDoc.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateDoc.tsx index 78abddbe..46c475d4 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateDoc.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateDoc.tsx @@ -6,14 +6,9 @@ import { Doc } from '../types'; import { KEY_LIST_DOC } from './useDocs'; -export type CreateDocParam = Pick; - -export const createDoc = async ({ title }: CreateDocParam): Promise => { +export const createDoc = async (): Promise => { const response = await fetchAPI(`documents/`, { method: 'POST', - body: JSON.stringify({ - title, - }), }); if (!response.ok) { @@ -29,7 +24,7 @@ interface CreateDocProps { export function useCreateDoc({ onSuccess }: CreateDocProps) { const queryClient = useQueryClient(); - return useMutation({ + return useMutation({ mutationFn: createDoc, onSuccess: (data) => { void queryClient.resetQueries({ diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridLoader.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridLoader.tsx index 009e4395..7ac17a92 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridLoader.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridLoader.tsx @@ -1,8 +1,7 @@ import { Loader } from '@openfun/cunningham-react'; -import { createGlobalStyle, css } from 'styled-components'; +import { createGlobalStyle } from 'styled-components'; import { Box } from '@/components'; -import { HEADER_HEIGHT } from '@/features/header/conf'; const DocsGridLoaderStyle = createGlobalStyle` body, main { @@ -27,15 +26,11 @@ export const DocsGridLoader = ({ isLoading }: DocsGridLoaderProps) => { data-testid="grid-loader" $align="center" $justify="center" - $height="calc(100vh - 50px)" + $height="100%" $width="100%" - $maxWidth="960px" - $background="rgba(255, 255, 255, 0.3)" + $background="rgba(255, 255, 255, 0.5)" $zIndex={998} - $position="fixed" - $css={css` - top: ${HEADER_HEIGHT}px; - `} + $position="absolute" > diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/SimpleDocItem.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/SimpleDocItem.tsx index d6dfb3b3..b8136dc4 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/SimpleDocItem.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/SimpleDocItem.tsx @@ -4,7 +4,7 @@ import { css } from 'styled-components'; import { Box, Text } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; -import { Doc } from '@/features/docs/doc-management'; +import { Doc, useTrans } from '@/features/docs/doc-management'; import { useResponsiveStore } from '@/stores'; import PinnedDocumentIcon from '../assets/pinned-document.svg'; @@ -35,6 +35,7 @@ export const SimpleDocItem = ({ const { spacingsTokens } = useCunninghamTheme(); const { isDesktop } = useResponsiveStore(); const spacings = spacingsTokens(); + const { untitledDocument } = useTrans(); return ( @@ -61,7 +62,7 @@ export const SimpleDocItem = ({ $weight="500" $css={ItemTextCss} > - {doc.title} + {doc.title || untitledDocument} {(!isDesktop || showAccesses) && ( { $height="fit-content" $margin={{ top: 'auto' }} > - + diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeader.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeader.tsx index ccc20bea..9a184ab5 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeader.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeader.tsx @@ -31,7 +31,7 @@ export const LeftPanelHeader = ({ children }: PropsWithChildren) => { }; const createNewDoc = () => { - createDoc({ title: t('Untitled document') }); + createDoc(); }; return (