Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fb51a2453 | |||
| 042034a9c9 | |||
| f28081a72a | |||
| 15dc1e3012 | |||
| 6cc20aeacb | |||
| 7da7214afb | |||
| c369419512 | |||
| d9ad397c94 | |||
| 3191d890f3 | |||
| 68f3387539 | |||
| 0dc8b4556c | |||
| e123e91959 | |||
| 2709400773 | |||
| 8281c6159b |
@@ -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'
|
||||
|
||||
|
||||
@@ -9,6 +9,16 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## Changed
|
||||
|
||||
- 📝(doc) minor README.md formatting and wording enhancements
|
||||
- ♻️Stop setting a default title on doc creation #634
|
||||
|
||||
|
||||
## Fixed
|
||||
|
||||
- 🐛(backend) race condition create doc #633
|
||||
|
||||
## [2.2.0] - 2025-02-10
|
||||
|
||||
## Added
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ Welcome to Docs! The open source document editor where your notes can become kno
|
||||
<img src="/docs/assets/docs_live_collaboration_light.gif" width="100%" align="center"/>
|
||||
|
||||
## 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
|
||||
|
||||
<http://localhost:8071/admin>.
|
||||
@@ -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/).
|
||||
|
||||
+53
-17
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ from django.contrib.postgres.fields import ArrayField
|
||||
from django.contrib.postgres.search import TrigramSimilarity
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db import connection, transaction
|
||||
from django.db import models as db
|
||||
from django.db import transaction
|
||||
from django.db.models.expressions import RawSQL
|
||||
from django.db.models.functions import Left, Length
|
||||
from django.http import Http404
|
||||
@@ -573,6 +573,12 @@ class DocumentViewSet(
|
||||
@transaction.atomic
|
||||
def perform_create(self, serializer):
|
||||
"""Set the current user as creator and owner of the newly created object."""
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
f'LOCK TABLE "{models.Document._meta.db_table}" ' # noqa: SLF001
|
||||
"IN SHARE ROW EXCLUSIVE MODE;"
|
||||
)
|
||||
|
||||
obj = models.Document.add_root(
|
||||
creator=self.request.user,
|
||||
**serializer.validated_data,
|
||||
@@ -631,10 +637,17 @@ class DocumentViewSet(
|
||||
permission_classes=[],
|
||||
url_path="create-for-owner",
|
||||
)
|
||||
@transaction.atomic
|
||||
def create_for_owner(self, request):
|
||||
"""
|
||||
Create a document on behalf of a specified owner (pre-existing user or invited).
|
||||
"""
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
f'LOCK TABLE "{models.Document._meta.db_table}" ' # noqa: SLF001
|
||||
"IN SHARE ROW EXCLUSIVE MODE;"
|
||||
)
|
||||
|
||||
# Deserialize and validate the data
|
||||
serializer = serializers.ServerCreateDocumentSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
@@ -741,7 +754,11 @@ class DocumentViewSet(
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
with transaction.atomic():
|
||||
child_document = document.add_child(
|
||||
locked_parent = models.Document.objects.select_for_update().get(
|
||||
pk=document.pk
|
||||
)
|
||||
|
||||
child_document = locked_parent.add_child(
|
||||
creator=request.user,
|
||||
**serializer.validated_data,
|
||||
)
|
||||
|
||||
@@ -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.",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
+86
-24
@@ -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.",
|
||||
)
|
||||
],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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
|
||||
),
|
||||
]
|
||||
|
||||
+6
-5
@@ -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"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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.",
|
||||
)
|
||||
],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
+28
-11
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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
|
||||
),
|
||||
]
|
||||
@@ -2,6 +2,7 @@
|
||||
Tests for Documents API endpoint in impress's core app: create
|
||||
"""
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
@@ -249,3 +250,41 @@ def test_api_documents_children_create_force_id_existing():
|
||||
assert response.json() == {
|
||||
"id": ["A document with this ID already exists. You cannot override it."]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_api_documents_create_document_children_race_condition():
|
||||
"""
|
||||
It should be possible to create several documents at the same time
|
||||
without causing any race conditions or data integrity issues.
|
||||
"""
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
|
||||
factories.UserDocumentAccessFactory(user=user, document=document, role="owner")
|
||||
|
||||
def create_document():
|
||||
return client.post(
|
||||
f"/api/v1.0/documents/{document.id}/children/",
|
||||
{
|
||||
"title": "my child",
|
||||
},
|
||||
)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=2) as executor:
|
||||
future1 = executor.submit(create_document)
|
||||
future2 = executor.submit(create_document)
|
||||
|
||||
response1 = future1.result()
|
||||
response2 = future2.result()
|
||||
|
||||
assert response1.status_code == 201
|
||||
assert response2.status_code == 201
|
||||
|
||||
document.refresh_from_db()
|
||||
assert document.numchild == 2
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Tests for Documents API endpoint in impress's core app: create
|
||||
"""
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
@@ -51,6 +52,36 @@ def test_api_documents_create_authenticated_success():
|
||||
assert document.accesses.filter(role="owner", user=user).exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_api_documents_create_document_race_condition():
|
||||
"""
|
||||
It should be possible to create several documents at the same time
|
||||
without causing any race conditions or data integrity issues.
|
||||
"""
|
||||
|
||||
def create_document(title):
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
return client.post(
|
||||
"/api/v1.0/documents/",
|
||||
{
|
||||
"title": title,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=2) as executor:
|
||||
future1 = executor.submit(create_document, "my document 1")
|
||||
future2 = executor.submit(create_document, "my document 2")
|
||||
|
||||
response1 = future1.result()
|
||||
response2 = future2.result()
|
||||
|
||||
assert response1.status_code == 201
|
||||
assert response2.status_code == 201
|
||||
|
||||
|
||||
def test_api_documents_create_authenticated_title_null():
|
||||
"""It should be possible to create several documents with a null title."""
|
||||
user = factories.UserFactory()
|
||||
|
||||
@@ -4,6 +4,7 @@ Tests for Documents API endpoint in impress's core app: create
|
||||
|
||||
# pylint: disable=W0621
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.core import mail
|
||||
@@ -425,6 +426,36 @@ def test_api_documents_create_for_owner_new_user_no_sub_no_fallback_allow_duplic
|
||||
assert document.creator == user
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_api_documents_create_document_race_condition():
|
||||
"""
|
||||
It should be possible to create several documents at the same time
|
||||
without causing any race conditions or data integrity issues.
|
||||
"""
|
||||
|
||||
def create_document(title):
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
return client.post(
|
||||
"/api/v1.0/documents/",
|
||||
{
|
||||
"title": title,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=2) as executor:
|
||||
future1 = executor.submit(create_document, "my document 1")
|
||||
future2 = executor.submit(create_document, "my document 2")
|
||||
|
||||
response1 = future1.result()
|
||||
response2 = future2.result()
|
||||
|
||||
assert response1.status_code == 201
|
||||
assert response2.status_code == 201
|
||||
|
||||
|
||||
@patch.object(ServerCreateDocumentSerializer, "_send_email_notification")
|
||||
@override_settings(SERVER_TO_SERVER_API_TOKENS=["DummyToken"], LANGUAGE_CODE="de-de")
|
||||
def test_api_documents_create_for_owner_with_default_language(
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
<svg
|
||||
width="32"
|
||||
height="33"
|
||||
viewBox="0 0 32 33"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M21.6305 29.5812C22.7983 29.2538 23.9166 28.6562 24.6505 27.6003C25.3749 26.5663 25.5789 25.2547 25.5789 23.9925V5.50099C25.5789 5.17358 25.5611 4.84557 25.5216 4.52148C26.1016 4.74961 26.5486 5.12658 26.8626 5.65239C27.2331 6.25024 27.4184 7.03757 27.4184 8.01435V26.7964C27.4184 28.1184 27.0942 29.1078 26.4458 29.7646C25.7974 30.4214 24.8207 30.7498 23.5155 30.7498H16.4209C16.5889 30.7204 16.7574 30.6901 16.9262 30.659C18.4067 30.3944 19.9713 30.0354 21.6185 29.5846L21.6305 29.5812Z"
|
||||
fill="#C9191E"
|
||||
|
||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
@@ -25,6 +25,7 @@ export const cssEditor = (readonly: boolean) => css`
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
top: -17px;
|
||||
left: 0px;
|
||||
padding: 0px 6px;
|
||||
border-radius: 0px;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -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}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</>
|
||||
|
||||
@@ -6,14 +6,9 @@ import { Doc } from '../types';
|
||||
|
||||
import { KEY_LIST_DOC } from './useDocs';
|
||||
|
||||
export type CreateDocParam = Pick<Doc, 'title'>;
|
||||
|
||||
export const createDoc = async ({ title }: CreateDocParam): Promise<Doc> => {
|
||||
export const createDoc = async (): Promise<Doc> => {
|
||||
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<Doc, APIError, CreateDocParam>({
|
||||
return useMutation<Doc, APIError>({
|
||||
mutationFn: createDoc,
|
||||
onSuccess: (data) => {
|
||||
void queryClient.resetQueries({
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<Loader />
|
||||
</Box>
|
||||
|
||||
@@ -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 (
|
||||
<Box $direction="row" $gap={spacings.sm}>
|
||||
@@ -61,7 +62,7 @@ export const SimpleDocItem = ({
|
||||
$weight="500"
|
||||
$css={ItemTextCss}
|
||||
>
|
||||
{doc.title}
|
||||
{doc.title || untitledDocument}
|
||||
</Text>
|
||||
{(!isDesktop || showAccesses) && (
|
||||
<Box
|
||||
|
||||
@@ -50,7 +50,7 @@ export const Header = () => {
|
||||
$height="fit-content"
|
||||
$margin={{ top: 'auto' }}
|
||||
>
|
||||
<IconDocs aria-label={t('Docs Logo')} width={25} />
|
||||
<IconDocs aria-label={t('Docs Logo')} width={32} />
|
||||
<Title />
|
||||
</Box>
|
||||
</StyledLink>
|
||||
|
||||
@@ -31,7 +31,7 @@ export const LeftPanelHeader = ({ children }: PropsWithChildren) => {
|
||||
};
|
||||
|
||||
const createNewDoc = () => {
|
||||
createDoc({ title: t('Untitled document') });
|
||||
createDoc();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user