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 @@
-