From 8281c6159b31c05bdcec72df2fbcddd2feb08ba0 Mon Sep 17 00:00:00 2001 From: Bastien Guerry Date: Fri, 14 Feb 2025 15:04:45 +0100 Subject: [PATCH 01/11] =?UTF-8?q?=F0=9F=93=9D(doc)=20minor=20README.md=20f?= =?UTF-8?q?ormatting=20and=20wording=20enhancements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See https://github.com/suitenumerique/docs/issues/622 --- README.md | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8d6c88c8..a1bf9e6c 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 @@ -65,15 +74,14 @@ docker compose version 1.27.4, build 40524192 > ⚠️ 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 πŸŽ‰ @@ -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/). From 2709400773bf8bb0564d64d0bbe49475edfcfbb7 Mon Sep 17 00:00:00 2001 From: Bastien Guerry Date: Fri, 14 Feb 2025 17:25:13 +0100 Subject: [PATCH 02/11] =?UTF-8?q?=F0=9F=94=8A(changelog)=20add=20a=20chang?= =?UTF-8?q?elog=20entry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add "πŸ“(doc) minor README.md formatting and wording enhancements" in the unreleased section. --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2576d8ba..110208f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to ## [Unreleased] +## Changed + +- πŸ“(doc) minor README.md formatting and wording enhancements + ## [2.2.0] - 2025-02-10 ## Added From e123e919596c5a88a1928cc122079749d6977194 Mon Sep 17 00:00:00 2001 From: Manuel Raynaud Date: Fri, 14 Feb 2025 12:18:05 +0100 Subject: [PATCH 03/11] =?UTF-8?q?=F0=9F=90=9B(nginx)=20increase=20nginx=20?= =?UTF-8?q?buffer=20size=20when=20proxifying=20keycloak?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nginx is used to proxify keycloak in our development configuration. When a new user is created keycloak is send a large amount of headers in its response and the default nginx config is not enough to handle this amount of headers. We have to increase the proxy buffer size to handle them. --- docker/files/etc/nginx/conf.d/default.conf | 6 ++++++ 1 file changed, 6 insertions(+) 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; } } From 0dc8b4556cfc4d9194d291fd5c7e69fdef72fa75 Mon Sep 17 00:00:00 2001 From: Manuel Raynaud Date: Fri, 14 Feb 2025 12:42:02 +0100 Subject: [PATCH 04/11] =?UTF-8?q?=E2=99=BB=EF=B8=8F(docker)=20remove=20usa?= =?UTF-8?q?ge=20of=20dockerize?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We remove dockerize and use healthcheck on docker compose services instead. --- .github/workflows/impress-frontend.yml | 22 --------- Makefile | 5 -- docker-compose.yml | 68 ++++++++++++++++++++------ 3 files changed, 52 insertions(+), 43 deletions(-) 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/Makefile b/Makefile index 0e81087b..99666246 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 @@ -124,8 +123,6 @@ run: ## start the wsgi (production) and development server @$(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 run-with-frontend: ## Start all the containers needed (backend to frontend) @@ -188,14 +185,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 diff --git a/docker-compose.yml b/docker-compose.yml index e10221b9..7de86335 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,9 +137,13 @@ 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: user: "${DOCKER_USER:-1000}" @@ -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 From 68f3387539a482daf59e124464c8d15a8d03ae9b Mon Sep 17 00:00:00 2001 From: Manuel Raynaud Date: Fri, 14 Feb 2025 12:43:49 +0100 Subject: [PATCH 05/11] =?UTF-8?q?=E2=99=BB=EF=B8=8F(make)=20make=20run=20c?= =?UTF-8?q?ommand=20starting=20everything?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The run command is not starting the frontend application. We change the run commands. The run command is strating everything. The run-backend command is starting all services needed to use the backend application. --- Makefile | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 99666246..17690837 100644 --- a/Makefile +++ b/Makefile @@ -80,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 @@ -119,16 +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 -.PHONY: run +.PHONY: run-backend -run-with-frontend: ## Start all the containers needed (backend to frontend) - @$(MAKE) run +run: ## start the wsgi (production) and development server +run: + @$(MAKE) run-backend @$(COMPOSE) up --force-recreate -d frontend-dev -.PHONY: run-with-frontend +.PHONY: run status: ## an alias for "docker compose ps" @$(COMPOSE) ps From 3191d890f37874b33aaef4a99d4b8f228d530a59 Mon Sep 17 00:00:00 2001 From: Manuel Raynaud Date: Fri, 14 Feb 2025 13:10:35 +0100 Subject: [PATCH 06/11] =?UTF-8?q?=E2=99=BB=EF=B8=8F(docker)=20rename=20fro?= =?UTF-8?q?ntend-dev=20service=20in=20frontend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The frontend-dev service is in fact using the production image. We rename it in frontend accordingly with what it really does. We also have to change name rules in Makefile to be consistent. --- Makefile | 10 +++++----- README.md | 10 +++++----- docker-compose.yml | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index 17690837..ea5fdbc5 100644 --- a/Makefile +++ b/Makefile @@ -108,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 @@ -128,7 +128,7 @@ run-backend: ## Start only the backend application and all needed services run: ## start the wsgi (production) and development server run: @$(MAKE) run-backend - @$(COMPOSE) up --force-recreate -d frontend-dev + @$(COMPOSE) up --force-recreate -d frontend .PHONY: run status: ## an alias for "docker compose ps" @@ -306,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 a1bf9e6c..c024440b 100644 --- a/README.md +++ b/README.md @@ -66,9 +66,9 @@ $ 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. @@ -97,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. @@ -105,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: @@ -117,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** diff --git a/docker-compose.yml b/docker-compose.yml index 7de86335..84ad1b03 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -145,7 +145,7 @@ services: condition: service_healthy restart: true - frontend-dev: + frontend: user: "${DOCKER_USER:-1000}" build: context: . From d9ad397c948f8149d6039dc6c2198b61a1111bc9 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Wed, 12 Feb 2025 12:10:06 +0100 Subject: [PATCH 07/11] =?UTF-8?q?=F0=9F=A9=B9(frontend)=20minor=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix linter warning on one e2e test - improve logo svg - improve cursor - improve grid loader --- .../apps/e2e/__tests__/app-impress/header.spec.ts | 1 + .../apps/impress/src/assets/icons/icon-docs.svg | 8 +------- .../impress/src/features/docs/doc-editor/styles.tsx | 1 + .../docs/docs-grid/components/DocsGridLoader.tsx | 13 ++++--------- .../src/features/header/components/Header.tsx | 2 +- 5 files changed, 8 insertions(+), 17 deletions(-) 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/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/header/components/Header.tsx b/src/frontend/apps/impress/src/features/header/components/Header.tsx index ff37f03e..bacf39e8 100644 --- a/src/frontend/apps/impress/src/features/header/components/Header.tsx +++ b/src/frontend/apps/impress/src/features/header/components/Header.tsx @@ -50,7 +50,7 @@ export const Header = () => { $height="fit-content" $margin={{ top: 'auto' }} > - + From c369419512a308ce96877b292096af11820bcf64 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Wed, 12 Feb 2025 12:19:07 +0100 Subject: [PATCH 08/11] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20stop=20set?= =?UTF-8?q?ting=20a=20default=20title=20on=20doc=20creation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We were setting a default title to our document during creation, but we should not do that, it created lot of similar titles, lot of documents will show up during search. --- CHANGELOG.md | 2 ++ .../docs/doc-header/components/DocTitle.tsx | 14 +++++--------- .../docs/doc-management/api/useCreateDoc.tsx | 9 ++------- .../docs/docs-grid/components/SimpleDocItem.tsx | 5 +++-- .../left-panel/components/LeftPanelHeader.tsx | 2 +- 5 files changed, 13 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 110208f3..5ffda055 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to ## Changed - πŸ“(doc) minor README.md formatting and wording enhancements +- ♻️Stop setting a default title on doc creation #634 + ## [2.2.0] - 2025-02-10 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/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) && ( { }; const createNewDoc = () => { - createDoc({ title: t('Untitled document') }); + createDoc(); }; return ( From 7da7214afbcdd1872e733d0817da427991c1b716 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Wed, 12 Feb 2025 12:20:10 +0100 Subject: [PATCH 09/11] =?UTF-8?q?=E2=9E=95(backend)=20add=20django-test-mi?= =?UTF-8?q?grations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add django-test-migrations to the project. It is a tool that helps to test Django migrations. --- src/backend/pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index 519badd5..4cd076f4 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", From 6cc20aeacbcd77856298230e23a22506a7c46921 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Wed, 12 Feb 2025 12:24:29 +0100 Subject: [PATCH 10/11] =?UTF-8?q?=F0=9F=A9=B9(migration)=20add=20migration?= =?UTF-8?q?=20to=20update=20default=20titles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The frontend was setting a default titles for documents with empty titles. This migration updates the document table to set the title to null instead of the default title. We add a test to ensure that the migration works as expected. --- .../migrations/0018_update_blank_title.py | 25 +++++++++++++ src/backend/core/tests/migrations/__init__.py | 0 .../test_0018_update_blank_title.py | 35 +++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 src/backend/core/migrations/0018_update_blank_title.py create mode 100644 src/backend/core/tests/migrations/__init__.py create mode 100644 src/backend/core/tests/migrations/test_0018_update_blank_title.py 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..74e379e9 --- /dev/null +++ b/src/backend/core/migrations/0018_update_blank_title.py @@ -0,0 +1,25 @@ +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" From 15dc1e30128518c8bdfd44135b328497335037dd Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Sat, 15 Feb 2025 23:27:43 +0100 Subject: [PATCH 11/11] =?UTF-8?q?=F0=9F=A6=BA(migration)=20add=20back=20th?= =?UTF-8?q?e=20migration=20folders=20to=20linter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous commit add "core/tests/migrations". The linter could not pass on it because all the migration folders were excluded from the linter. We remove this exclusion, tests and migrations can now be linted and formatted automatically. --- src/backend/core/migrations/0001_initial.py | 580 +++++++++++++++--- .../0002_create_pg_trgm_extension.py | 4 +- ..._link_reach_document_link_role_and_more.py | 110 +++- .../0004_migrate_is_public_to_link_reach.py | 19 +- ...blic_alter_document_link_reach_and_more.py | 11 +- .../0006_add_user_full_name_and_short_name.py | 31 +- .../migrations/0007_fix_users_duplicate.py | 4 +- .../0008_alter_document_link_reach.py | 17 +- .../migrations/0009_add_document_favorite.py | 82 ++- .../0010_add_field_creator_to_document.py | 45 +- ...late_creator_field_and_make_it_required.py | 39 +- ..._creator_and_invitation_issuer_optional.py | 39 +- .../0013_activate_fuzzystrmatch_extension.py | 4 +- .../0014_add_tree_structure_to_documents.py | 19 +- .../0015_set_path_on_existing_documents.py | 23 +- .../migrations/0016_add_document_excerpt.py | 23 +- .../0017_add_fields_for_soft_delete.py | 42 +- .../migrations/0018_update_blank_title.py | 3 +- src/backend/pyproject.toml | 1 - 19 files changed, 841 insertions(+), 255 deletions(-) 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 index 74e379e9..9576f53a 100644 --- a/src/backend/core/migrations/0018_update_blank_title.py +++ b/src/backend/core/migrations/0018_update_blank_title.py @@ -19,7 +19,6 @@ class Migration(migrations.Migration): operations = [ migrations.RunPython( - update_titles_to_null, - reverse_code=migrations.RunPython.noop + update_titles_to_null, reverse_code=migrations.RunPython.noop ), ] diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index 4cd076f4..a7fe9a36 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -100,7 +100,6 @@ exclude = [ "build", "venv", "__pycache__", - "*/migrations/*", ] line-length = 88