diff --git a/.github/workflows/impress.yml b/.github/workflows/impress.yml
index 896f07ef..385aa633 100644
--- a/.github/workflows/impress.yml
+++ b/.github/workflows/impress.yml
@@ -206,10 +206,11 @@ jobs:
- name: Install development dependencies
run: pip install --user .[dev]
- - name: Install gettext (required to compile messages)
+ - name: Install gettext (required to compile messages) and MIME support
run: |
sudo apt-get update
- sudo apt-get install -y gettext pandoc
+ sudo apt-get install -y gettext pandoc shared-mime-info
+ sudo wget https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types -O /etc/mime.types
- name: Generate a MO file from strings extracted from the project
run: python manage.py compilemessages
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c4b6ccc3..5719eb92 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,12 +11,15 @@ and this project adheres to
## Added
-- github actions to managed Crowdin workflow
+- github actions to manage Crowdin workflow
+- πIntegrate Posthog #540
+- π·οΈ(backend) add content-type to uploaded files #552
+- β¨(frontend) export pdf docx front side #537
## Changed
- π(frontend) add abilities on doc row #581
-
+- π(frontend) improve DocsGridItem responsive padding #582
## [2.0.1] - 2025-01-17
@@ -35,6 +38,8 @@ and this project adheres to
- π(frontend) add filtering to left panel #475
- β¨(frontend) new share modal ui #489
- β¨(frontend) add favorite feature #515
+- π(documentation) Documentation about self-hosted installation #530
+- β¨(helm) helm versioning #530
## Changed
@@ -46,7 +51,7 @@ and this project adheres to
- π(frontend) update DocHeader ui #448
- π(frontend) update doc versioning ui #463
- π(frontend) update doc summary ui #473
-- π(doc) update readme.md to match V2 changes #558
+- π(doc) update readme.md to match V2 changes #558 & #572
## Fixed
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 0c73f028..20c4e9e1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,7 +2,14 @@
Thank you for taking the time to contribute! Please follow these guidelines to ensure a smooth and productive workflow. πππ
-To get started with the project, please refer to the [README.md](https://github.com/numerique-gouv/impress/blob/main/README.md) for detailed instructions.
+To get started with the project, please refer to the [README.md](https://github.com/suitenumerique/docs/blob/main/README.md) for detailed instructions.
+
+Please also check out our [dev handbook](https://suitenumerique.gitbook.io/handbook) to learn our best practices.
+
+## Help us with translations
+
+You can help us with translations on [Crowdin](https://crowdin.com/project/lasuite-docs).
+Your language is not there? Request it on our Crowdin page π.
## Creating an Issue
diff --git a/Dockerfile b/Dockerfile
index 941f0747..60f464ee 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -72,10 +72,11 @@ RUN apk add \
gettext \
gdk-pixbuf \
libffi-dev \
- pandoc \
pango \
shared-mime-info
+RUN wget https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types -O /etc/mime.types
+
# Copy entrypoint
COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint
diff --git a/README.md b/README.md
index 27b1af11..27b0e59f 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-
+
@@ -27,7 +27,7 @@ Docs is a collaborative text editor designed to address common challenges in kno
* π Simple collaborative editing without the formatting complexity of markdown
* π Offline? No problem, keep writing, your edits will get synced when back online
* π
Create clean documents with limited but beautiful formatting options and focus on content
-* π§± Built for productivity (markdown support, many block types, slash commands, markdown support, keyboard shortcuts) (page in french sorry π
).
+* π§± Built for productivity (markdown support, many block types, slash commands, keyboard shortcuts).
* β¨ Save time thanks to our AI actions (generate, sum up, correct, translate)
### Collaborate
@@ -91,7 +91,7 @@ password: impress
$ make run-with-frontend
```
-β οΈ For the frontend developper, it is often better to run the frontend in development mode locally.
+β οΈ For the frontend developer, it is often better to run the frontend in development mode locally.
To do so, install the frontend dependencies with the following command:
@@ -144,12 +144,14 @@ Want to know where the project is headed? [πΊοΈ Checkout our roadmap](https:/
## Licence π
This work is released under the MIT License (see [LICENSE](https://github.com/suitenumerique/docs/blob/main/LICENSE)).
-While Docs is public driven initiative our licence choice is an invitation for private sector actors to use, sell and contribute to the project.
+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 if you have any question related to our implementation or design decisions.
+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.
-If you intend to make pull requests see CONTRIBUTING for guidelines.
+You can help us with translations on [Crowdin](https://crowdin.com/project/lasuite-docs).
+
+If you intend to make pull requests see [CONTRIBUTING](https://github.com/suitenumerique/docs/blob/main/CONTRIBUTING.md) for guidelines.
Directory structure:
@@ -167,7 +169,15 @@ docs
## Credits β€οΈ
### Stack
-Impress is built on top of [Django Rest Framework](https://www.django-rest-framework.org/), [Next.js](https://nextjs.org/), [MinIO](https://min.io/) and [BlocNote.js](https://www.blocknotejs.org/)
+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/)
-### States β€οΈ open source
-Docs is the result of a joint effort lead by the French π«π·π₯ ([DINUM](https://www.numerique.gouv.fr/dinum/)) and German π©πͺπ₯¨ government ([ZenDiS](https://zendis.de/)). We are always looking for new public partners feel free to reach out if you are interested in using or contributing to docs.
\ No newline at end of file
+### Gov β€οΈ open source
+Docs is the result of a joint effort lead 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/).
+
+We are always looking for new public partners (we are currently onboarding the Netherlands π³π±π§), feel free to [reach out](https://matrix.to/#/#docs-official:matrix.org) if you are interested in using or contributing to Docs.
+
+
+
+
\ No newline at end of file
diff --git a/docs/assets/docs-logo.png b/docs/assets/docs-logo.png
new file mode 100644
index 00000000..fe590844
Binary files /dev/null and b/docs/assets/docs-logo.png differ
diff --git a/docs/assets/europe<3opensource.png b/docs/assets/europe<3opensource.png
new file mode 100644
index 00000000..00606b5e
Binary files /dev/null and b/docs/assets/europe<3opensource.png differ
diff --git a/docs/assets/logo-docs.png b/docs/assets/logo-docs.png
deleted file mode 100644
index 54d3aa4d..00000000
Binary files a/docs/assets/logo-docs.png and /dev/null differ
diff --git a/docs/assets/logo.png b/docs/assets/logo.png
new file mode 100644
index 00000000..949969ff
Binary files /dev/null and b/docs/assets/logo.png differ
diff --git a/docs/examples/impress.values.yaml b/docs/examples/impress.values.yaml
index e21e7ec3..bc090450 100644
--- a/docs/examples/impress.values.yaml
+++ b/docs/examples/impress.values.yaml
@@ -40,6 +40,7 @@ backend:
LOGIN_REDIRECT_URL: https://impress.127.0.0.1.nip.io
LOGIN_REDIRECT_URL_FAILURE: https://impress.127.0.0.1.nip.io
LOGOUT_REDIRECT_URL: https://impress.127.0.0.1.nip.io
+ POSTHOG_KEY: "{'id': 'posthog_key', 'host': 'https://product.impress.127.0.0.1.nip.io'}"
DB_HOST: postgresql
DB_NAME: impress
DB_USER: dinum
@@ -121,6 +122,12 @@ yProvider:
COLLABORATION_SERVER_SECRET: my-secret
Y_PROVIDER_API_KEY: my-secret
+posthog:
+ ingress:
+ enabled: false
+ ingressAssets:
+ enabled: false
+
ingress:
enabled: true
host: impress.127.0.0.1.nip.io
diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py
index d5b0d4c7..e2369f49 100644
--- a/src/backend/core/api/serializers.py
+++ b/src/backend/core/api/serializers.py
@@ -388,6 +388,7 @@ class FileUploadSerializer(serializers.Serializer):
raise serializers.ValidationError("Could not determine file extension.")
self.context["expected_extension"] = extension
+ self.context["content_type"] = magic_mime_type
return file
@@ -395,6 +396,7 @@ class FileUploadSerializer(serializers.Serializer):
"""Override validate to add the computed extension to validated_data."""
attrs["expected_extension"] = self.context["expected_extension"]
attrs["is_unsafe"] = self.context["is_unsafe"]
+ attrs["content_type"] = self.context["content_type"]
return attrs
diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py
index 0f83e1e5..a98ad4f5 100644
--- a/src/backend/core/api/viewsets.py
+++ b/src/backend/core/api/viewsets.py
@@ -20,6 +20,7 @@ from django.db.models import (
Subquery,
Value,
)
+from django.db.models.expressions import RawSQL
from django.http import Http404
import rest_framework as drf
@@ -150,29 +151,35 @@ class UserViewSet(
"""
queryset = self.queryset
- if self.action == "list":
- # Exclude all users already in the given document
- if document_id := self.request.GET.get("document_id", ""):
- queryset = queryset.exclude(documentaccess__document_id=document_id)
+ if self.action != "list":
+ return queryset
- # Filter users by email similarity
- if query := self.request.GET.get("q", ""):
- # For performance reasons we filter first by similarity, which relies on an index,
- # then only calculate precise similarity scores for sorting purposes
- queryset = queryset.filter(email__trigram_word_similar=query)
+ # Exclude all users already in the given document
+ if document_id := self.request.GET.get("document_id", ""):
+ queryset = queryset.exclude(documentaccess__document_id=document_id)
- queryset = queryset.annotate(
- similarity=TrigramSimilarity("email", query)
+ if not (query := self.request.GET.get("q", "")):
+ return queryset
+
+ # For emails, match emails by Levenstein distance to prevent typing errors
+ if "@" in query:
+ return (
+ queryset.annotate(
+ distance=RawSQL("levenshtein(email::text, %s::text)", (query,))
)
- # When the query only is on the name part, we should try to make many proposals
- # But when the query looks like an email we should only propose serious matches
- threshold = 0.6 if "@" in query else 0.1
+ .filter(distance__lte=3)
+ .order_by("distance", "email")
+ )
- queryset = queryset.filter(similarity__gt=threshold).order_by(
- "-similarity", "email"
- )
-
- return queryset
+ # Use trigram similarity for non-email-like queries
+ # For performance reasons we filter first by similarity, which relies on an
+ # index, then only calculate precise similarity scores for sorting purposes
+ return (
+ queryset.filter(email__trigram_word_similar=query)
+ .annotate(similarity=TrigramSimilarity("email", query))
+ .filter(similarity__gt=0.2)
+ .order_by("-similarity", "email")
+ )
@drf.decorators.action(
detail=False,
@@ -605,7 +612,10 @@ class DocumentViewSet(
key = f"{document.key_base}/{ATTACHMENTS_FOLDER:s}/{file_id!s}.{extension:s}"
# Prepare metadata for storage
- extra_args = {"Metadata": {"owner": str(request.user.id)}}
+ extra_args = {
+ "Metadata": {"owner": str(request.user.id)},
+ "ContentType": serializer.validated_data["content_type"],
+ }
if serializer.validated_data["is_unsafe"]:
extra_args["Metadata"]["is_unsafe"] = "true"
@@ -936,40 +946,6 @@ class TemplateViewSet(
role=models.RoleChoices.OWNER,
)
- @drf.decorators.action(
- detail=True,
- methods=["post"],
- url_path="generate-document",
- permission_classes=[permissions.AccessPermission],
- )
- # pylint: disable=unused-argument
- def generate_document(self, request, pk=None):
- """
- Generate and return a document for this template around the
- body passed as argument.
-
- 2 types of body are accepted:
- - HTML: body_type = "html"
- - Markdown: body_type = "markdown"
-
- 2 types of documents can be generated:
- - PDF: format = "pdf"
- - Docx: format = "docx"
- """
- serializer = serializers.DocumentGenerationSerializer(data=request.data)
-
- if not serializer.is_valid():
- return drf.response.Response(
- serializer.errors, status=drf.status.HTTP_400_BAD_REQUEST
- )
-
- body = serializer.validated_data["body"]
- body_type = serializer.validated_data["body_type"]
- export_format = serializer.validated_data["format"]
-
- template = self.get_object()
- return template.generate_document(body, body_type, export_format)
-
class TemplateAccessViewSet(
ResourceAccessViewsetMixin,
@@ -1124,6 +1100,7 @@ class ConfigView(drf.views.APIView):
"ENVIRONMENT",
"FRONTEND_THEME",
"MEDIA_BASE_URL",
+ "POSTHOG_KEY",
"LANGUAGES",
"LANGUAGE_CODE",
"SENTRY_DSN",
diff --git a/src/backend/core/management/__init__.py b/src/backend/core/management/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/backend/core/management/commands/__init__.py b/src/backend/core/management/commands/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/backend/core/management/commands/update_files_content_type_metadata.py b/src/backend/core/management/commands/update_files_content_type_metadata.py
new file mode 100644
index 00000000..bb2e5253
--- /dev/null
+++ b/src/backend/core/management/commands/update_files_content_type_metadata.py
@@ -0,0 +1,95 @@
+"""Management command updating the metadata for all the files in the MinIO bucket."""
+
+from django.core.files.storage import default_storage
+from django.core.management.base import BaseCommand
+
+import magic
+
+from core.models import Document
+
+# pylint: disable=too-many-locals, broad-exception-caught
+
+
+class Command(BaseCommand):
+ """Update the metadata for all the files in the MinIO bucket."""
+
+ help = __doc__
+
+ def handle(self, *args, **options):
+ """Execute management command."""
+ s3_client = default_storage.connection.meta.client
+ bucket_name = default_storage.bucket_name
+
+ mime_detector = magic.Magic(mime=True)
+
+ documents = Document.objects.all()
+ self.stdout.write(
+ f"[INFO] Found {documents.count()} documents. Starting ContentType fix..."
+ )
+
+ for doc in documents:
+ doc_id_str = str(doc.id)
+ prefix = f"{doc_id_str}/attachments/"
+ self.stdout.write(
+ f"[INFO] Processing attachments under prefix '{prefix}' ..."
+ )
+
+ continuation_token = None
+ total_updated = 0
+
+ while True:
+ list_kwargs = {"Bucket": bucket_name, "Prefix": prefix}
+ if continuation_token:
+ list_kwargs["ContinuationToken"] = continuation_token
+
+ response = s3_client.list_objects_v2(**list_kwargs)
+
+ # If no objects found under this prefix, break out of the loop
+ if "Contents" not in response:
+ break
+
+ for obj in response["Contents"]:
+ key = obj["Key"]
+
+ # Skip if it's a folder
+ if key.endswith("/"):
+ continue
+
+ try:
+ # Get existing metadata
+ head_resp = s3_client.head_object(Bucket=bucket_name, Key=key)
+
+ # Read first ~1KB for MIME detection
+ partial_obj = s3_client.get_object(
+ Bucket=bucket_name, Key=key, Range="bytes=0-1023"
+ )
+ partial_data = partial_obj["Body"].read()
+
+ # Detect MIME type
+ magic_mime_type = mime_detector.from_buffer(partial_data)
+
+ # Update ContentType
+ s3_client.copy_object(
+ Bucket=bucket_name,
+ CopySource={"Bucket": bucket_name, "Key": key},
+ Key=key,
+ ContentType=magic_mime_type,
+ Metadata=head_resp.get("Metadata", {}),
+ MetadataDirective="REPLACE",
+ )
+ total_updated += 1
+
+ except Exception as exc: # noqa
+ self.stderr.write(
+ f"[ERROR] Could not update ContentType for {key}: {exc}"
+ )
+
+ if response.get("IsTruncated"):
+ continuation_token = response.get("NextContinuationToken")
+ else:
+ break
+
+ if total_updated > 0:
+ self.stdout.write(
+ f"[INFO] -> Updated {total_updated} objects for Document {doc_id_str}."
+ )
diff --git a/src/backend/core/migrations/0013_activate_fuzzystrmatch_extension.py b/src/backend/core/migrations/0013_activate_fuzzystrmatch_extension.py
new file mode 100644
index 00000000..db7fbb3a
--- /dev/null
+++ b/src/backend/core/migrations/0013_activate_fuzzystrmatch_extension.py
@@ -0,0 +1,16 @@
+# Generated by Django 5.1.4 on 2025-01-25 08:38
+
+from django.db import migrations
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0012_make_document_creator_and_invitation_issuer_optional'),
+ ]
+
+ operations = [
+ migrations.RunSQL(
+ "CREATE EXTENSION IF NOT EXISTS fuzzystrmatch;",
+ reverse_sql="DROP EXTENSION IF EXISTS fuzzystrmatch;",
+ ),
+ ]
diff --git a/src/backend/core/models.py b/src/backend/core/models.py
index 52e66aab..1dc85739 100644
--- a/src/backend/core/models.py
+++ b/src/backend/core/models.py
@@ -5,11 +5,8 @@ Declare and configure the models for the impress core application
import hashlib
import smtplib
-import tempfile
-import textwrap
import uuid
from datetime import timedelta
-from io import BytesIO
from logging import getLogger
from django.conf import settings
@@ -21,19 +18,12 @@ from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from django.core.mail import send_mail
from django.db import models
-from django.http import FileResponse
-from django.template.base import Template as DjangoTemplate
-from django.template.context import Context
from django.template.loader import render_to_string
-from django.utils import html, timezone
+from django.utils import timezone
from django.utils.functional import cached_property, lazy
from django.utils.translation import get_language, override
from django.utils.translation import gettext_lazy as _
-import frontmatter
-import markdown
-import pypandoc
-import weasyprint
from botocore.exceptions import ClientError
from timezone_field import TimeZoneField
@@ -794,107 +784,6 @@ class Template(BaseModel):
"retrieve": can_get,
}
- def generate_pdf(self, body_html, metadata):
- """
- Generate and return a pdf document wrapped around the current template
- """
- document_html = weasyprint.HTML(
- string=DjangoTemplate(self.code).render(
- Context({"body": html.format_html(body_html), **metadata})
- )
- )
- css = weasyprint.CSS(
- string=self.css,
- font_config=weasyprint.text.fonts.FontConfiguration(),
- )
-
- pdf_content = document_html.write_pdf(stylesheets=[css], zoom=1)
- response = FileResponse(BytesIO(pdf_content), content_type="application/pdf")
- response["Content-Disposition"] = f"attachment; filename={self.title}.pdf"
-
- return response
-
- def generate_word(self, body_html, metadata):
- """
- Generate and return a docx document wrapped around the current template
- """
- template_string = DjangoTemplate(self.code).render(
- Context({"body": html.format_html(body_html), **metadata})
- )
-
- html_string = f"""
-
-
-
-
-
-
- {template_string}
-
-
- """
-
- reference_docx = "core/static/reference.docx"
- output = BytesIO()
-
- # Convert the HTML to a temporary docx file
- with tempfile.NamedTemporaryFile(suffix=".docx", prefix="docx_") as tmp_file:
- output_path = tmp_file.name
-
- pypandoc.convert_text(
- html_string,
- "docx",
- format="html",
- outputfile=output_path,
- extra_args=["--reference-doc", reference_docx],
- )
-
- # Create a BytesIO object to store the output of the temporary docx file
- with open(output_path, "rb") as f:
- output = BytesIO(f.read())
-
- # Ensure the pointer is at the beginning
- output.seek(0)
-
- response = FileResponse(
- output,
- content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
- )
- response["Content-Disposition"] = f"attachment; filename={self.title}.docx"
-
- return response
-
- def generate_document(self, body, body_type, export_format):
- """
- Generate and return a document for this template around the
- body passed as argument.
-
- 2 types of body are accepted:
- - HTML: body_type = "html"
- - Markdown: body_type = "markdown"
-
- 2 types of documents can be generated:
- - PDF: export_format = "pdf"
- - Docx: export_format = "docx"
- """
- document = frontmatter.loads(body)
- metadata = document.metadata
- strip_body = document.content.strip()
-
- if body_type == "html":
- body_html = strip_body
- else:
- body_html = (
- markdown.markdown(textwrap.dedent(strip_body)) if strip_body else ""
- )
-
- if export_format == "pdf":
- return self.generate_pdf(body_html, metadata)
-
- return self.generate_word(body_html, metadata)
-
class TemplateAccess(BaseAccess):
"""Relation model to give access to a template for a user or a team with a role."""
diff --git a/src/backend/core/static/reference.docx b/src/backend/core/static/reference.docx
deleted file mode 100644
index 2192455d..00000000
Binary files a/src/backend/core/static/reference.docx and /dev/null differ
diff --git a/src/backend/core/tests/commands/test_update_files_content_type_metadata.py b/src/backend/core/tests/commands/test_update_files_content_type_metadata.py
new file mode 100644
index 00000000..3ef78314
--- /dev/null
+++ b/src/backend/core/tests/commands/test_update_files_content_type_metadata.py
@@ -0,0 +1,50 @@
+"""
+Unit test for `update_files_content_type_metadata` command.
+"""
+
+import uuid
+
+from django.core.files.storage import default_storage
+from django.core.management import call_command
+
+import pytest
+
+from core import factories
+
+
+@pytest.mark.django_db
+def test_update_files_content_type_metadata():
+ """
+ Test that the command `update_files_content_type_metadata`
+ fixes the ContentType of attachment in the storage.
+ """
+ s3_client = default_storage.connection.meta.client
+ bucket_name = default_storage.bucket_name
+
+ # Create files with a wrong ContentType
+ keys = []
+ for _ in range(10):
+ doc_id = uuid.uuid4()
+ factories.DocumentFactory(id=doc_id)
+ key = f"{doc_id}/attachments/testfile.png"
+ keys.append(key)
+ fake_png = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR..."
+ s3_client.put_object(
+ Bucket=bucket_name,
+ Key=key,
+ Body=fake_png,
+ ContentType="text/plain",
+ Metadata={"owner": "None"},
+ )
+
+ # Call the command that fixes the ContentType
+ call_command("update_files_content_type_metadata")
+
+ for key in keys:
+ head_resp = s3_client.head_object(Bucket=bucket_name, Key=key)
+ assert head_resp["ContentType"] == "image/png", (
+ f"ContentType not fixed, got {head_resp['ContentType']!r}"
+ )
+
+ # Check that original metadata was preserved
+ assert head_resp["Metadata"].get("owner") == "None"
diff --git a/src/backend/core/tests/documents/test_api_documents_attachment_upload.py b/src/backend/core/tests/documents/test_api_documents_attachment_upload.py
index 1288f8ca..4a6564d6 100644
--- a/src/backend/core/tests/documents/test_api_documents_attachment_upload.py
+++ b/src/backend/core/tests/documents/test_api_documents_attachment_upload.py
@@ -64,12 +64,22 @@ def test_api_documents_attachment_upload_anonymous_success():
assert response.status_code == 201
pattern = re.compile(rf"^/media/{document.id!s}/attachments/(.*)\.png")
- match = pattern.search(response.json()["file"])
+ file_path = response.json()["file"]
+ match = pattern.search(file_path)
file_id = match.group(1)
# Validate that file_id is a valid UUID
uuid.UUID(file_id)
+ # Now, check the metadata of the uploaded file
+ key = file_path.replace("/media", "")
+ file_head = default_storage.connection.meta.client.head_object(
+ Bucket=default_storage.bucket_name, Key=key
+ )
+
+ assert file_head["Metadata"] == {"owner": "None"}
+ assert file_head["ContentType"] == "image/png"
+
@pytest.mark.parametrize(
"reach, role",
@@ -206,6 +216,7 @@ def test_api_documents_attachment_upload_success(via, role, mock_user_teams):
Bucket=default_storage.bucket_name, Key=key
)
assert file_head["Metadata"] == {"owner": str(user.id)}
+ assert file_head["ContentType"] == "image/png"
def test_api_documents_attachment_upload_invalid(client):
@@ -247,16 +258,18 @@ def test_api_documents_attachment_upload_size_limit_exceeded(settings):
@pytest.mark.parametrize(
- "name,content,extension",
+ "name,content,extension,content_type",
[
- ("test.exe", b"text", "exe"),
- ("test", b"text", "txt"),
- ("test.aaaaaa", b"test", "txt"),
- ("test.txt", PIXEL, "txt"),
- ("test.py", b"#!/usr/bin/python", "py"),
+ ("test.exe", b"text", "exe", "text/plain"),
+ ("test", b"text", "txt", "text/plain"),
+ ("test.aaaaaa", b"test", "txt", "text/plain"),
+ ("test.txt", PIXEL, "txt", "image/png"),
+ ("test.py", b"#!/usr/bin/python", "py", "text/plain"),
],
)
-def test_api_documents_attachment_upload_fix_extension(name, content, extension):
+def test_api_documents_attachment_upload_fix_extension(
+ name, content, extension, content_type
+):
"""
A file with no extension or a wrong extension is accepted and the extension
is corrected in storage.
@@ -287,6 +300,7 @@ def test_api_documents_attachment_upload_fix_extension(name, content, extension)
Bucket=default_storage.bucket_name, Key=key
)
assert file_head["Metadata"] == {"owner": str(user.id), "is_unsafe": "true"}
+ assert file_head["ContentType"] == content_type
def test_api_documents_attachment_upload_empty_file():
@@ -335,3 +349,4 @@ def test_api_documents_attachment_upload_unsafe():
Bucket=default_storage.bucket_name, Key=key
)
assert file_head["Metadata"] == {"owner": str(user.id), "is_unsafe": "true"}
+ assert file_head["ContentType"] == "application/octet-stream"
diff --git a/src/backend/core/tests/templates/test_api_templates_generate_document.py b/src/backend/core/tests/templates/test_api_templates_generate_document.py
deleted file mode 100644
index 0ed8f4d3..00000000
--- a/src/backend/core/tests/templates/test_api_templates_generate_document.py
+++ /dev/null
@@ -1,208 +0,0 @@
-"""
-Test users API endpoints in the impress core app.
-"""
-
-import pytest
-from rest_framework.test import APIClient
-
-from core import factories
-from core.tests.conftest import TEAM, USER, VIA
-
-pytestmark = pytest.mark.django_db
-
-
-def test_api_templates_generate_document_anonymous_public():
- """Anonymous users can generate pdf document with public templates."""
- template = factories.TemplateFactory(is_public=True)
- data = {
- "body": "# Test markdown body",
- }
-
- response = APIClient().post(
- f"/api/v1.0/templates/{template.id!s}/generate-document/",
- data,
- format="json",
- )
-
- assert response.status_code == 200
- assert response.headers["content-type"] == "application/pdf"
-
-
-def test_api_templates_generate_document_anonymous_not_public():
- """
- Anonymous users should not be allowed to generate pdf document with templates
- that are not marked as public.
- """
- template = factories.TemplateFactory(is_public=False)
- data = {
- "body": "# Test markdown body",
- }
-
- response = APIClient().post(
- f"/api/v1.0/templates/{template.id!s}/generate-document/",
- data,
- format="json",
- )
-
- assert response.status_code == 401
- assert response.json() == {
- "detail": "Authentication credentials were not provided."
- }
-
-
-def test_api_templates_generate_document_authenticated_public():
- """Authenticated users can generate pdf document with public templates."""
- user = factories.UserFactory()
-
- client = APIClient()
- client.force_login(user)
-
- template = factories.TemplateFactory(is_public=True)
- data = {"body": "# Test markdown body"}
-
- response = client.post(
- f"/api/v1.0/templates/{template.id!s}/generate-document/",
- data,
- format="json",
- )
-
- assert response.status_code == 200
- assert response.headers["content-type"] == "application/pdf"
-
-
-def test_api_templates_generate_document_authenticated_not_public():
- """
- Authenticated users should not be allowed to generate pdf document with templates
- that are not marked as public.
- """
- user = factories.UserFactory()
-
- client = APIClient()
- client.force_login(user)
-
- template = factories.TemplateFactory(is_public=False)
- data = {"body": "# Test markdown body"}
-
- response = client.post(
- f"/api/v1.0/templates/{template.id!s}/generate-document/",
- data,
- format="json",
- )
-
- assert response.status_code == 403
- assert response.json() == {
- "detail": "You do not have permission to perform this action."
- }
-
-
-@pytest.mark.parametrize("via", VIA)
-def test_api_templates_generate_document_related(via, mock_user_teams):
- """Users related to a template can generate pdf document."""
- user = factories.UserFactory()
-
- client = APIClient()
- client.force_login(user)
- access = None
- if via == USER:
- access = factories.UserTemplateAccessFactory(user=user)
- elif via == TEAM:
- mock_user_teams.return_value = ["lasuite", "unknown"]
- access = factories.TeamTemplateAccessFactory(team="lasuite")
-
- data = {"body": "# Test markdown body"}
-
- response = client.post(
- f"/api/v1.0/templates/{access.template_id!s}/generate-document/",
- data,
- format="json",
- )
-
- assert response.status_code == 200
- assert response.headers["content-type"] == "application/pdf"
-
-
-def test_api_templates_generate_document_type_html():
- """Generate pdf document with the body type html."""
- user = factories.UserFactory()
-
- client = APIClient()
- client.force_login(user)
-
- template = factories.TemplateFactory(is_public=True)
- data = {"body": "Test body
", "body_type": "html"}
-
- response = client.post(
- f"/api/v1.0/templates/{template.id!s}/generate-document/",
- data,
- format="json",
- )
-
- assert response.status_code == 200
- assert response.headers["content-type"] == "application/pdf"
-
-
-def test_api_templates_generate_document_type_markdown():
- """Generate pdf document with the body type markdown."""
- user = factories.UserFactory()
-
- client = APIClient()
- client.force_login(user)
-
- template = factories.TemplateFactory(is_public=True)
- data = {"body": "# Test markdown body", "body_type": "markdown"}
-
- response = client.post(
- f"/api/v1.0/templates/{template.id!s}/generate-document/",
- data,
- format="json",
- )
-
- assert response.status_code == 200
- assert response.headers["content-type"] == "application/pdf"
-
-
-def test_api_templates_generate_document_type_unknown():
- """Generate pdf document with the body type unknown."""
- user = factories.UserFactory()
-
- client = APIClient()
- client.force_login(user)
-
- template = factories.TemplateFactory(is_public=True)
- data = {"body": "# Test markdown body", "body_type": "unknown"}
-
- response = client.post(
- f"/api/v1.0/templates/{template.id!s}/generate-document/",
- data,
- format="json",
- )
-
- assert response.status_code == 400
- assert response.json() == {
- "body_type": [
- '"unknown" is not a valid choice.',
- ]
- }
-
-
-def test_api_templates_generate_document_export_docx():
- """Generate pdf document with the body type html."""
- user = factories.UserFactory()
-
- client = APIClient()
- client.force_login(user)
-
- template = factories.TemplateFactory(is_public=True)
- data = {"body": "Test body
", "body_type": "html", "format": "docx"}
-
- response = client.post(
- f"/api/v1.0/templates/{template.id!s}/generate-document/",
- data,
- format="json",
- )
-
- assert response.status_code == 200
- assert (
- response.headers["content-type"]
- == "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
- )
diff --git a/src/backend/core/tests/templates/test_api_templates_list.py b/src/backend/core/tests/templates/test_api_templates_list.py
index 44582b8f..11df4fa9 100644
--- a/src/backend/core/tests/templates/test_api_templates_list.py
+++ b/src/backend/core/tests/templates/test_api_templates_list.py
@@ -187,9 +187,9 @@ def test_api_templates_list_order_default():
response_template_ids = [template["id"] for template in response_data["results"]]
template_ids.reverse()
- assert (
- response_template_ids == template_ids
- ), "created_at values are not sorted from newest to oldest"
+ assert response_template_ids == template_ids, (
+ "created_at values are not sorted from newest to oldest"
+ )
def test_api_templates_list_order_param():
@@ -215,6 +215,6 @@ def test_api_templates_list_order_param():
response_template_ids = [template["id"] for template in response_data["results"]]
- assert (
- response_template_ids == templates_ids
- ), "created_at values are not sorted from oldest to newest"
+ assert response_template_ids == templates_ids, (
+ "created_at values are not sorted from oldest to newest"
+ )
diff --git a/src/backend/core/tests/test_api_config.py b/src/backend/core/tests/test_api_config.py
index a5eb151f..a625260e 100644
--- a/src/backend/core/tests/test_api_config.py
+++ b/src/backend/core/tests/test_api_config.py
@@ -20,6 +20,7 @@ pytestmark = pytest.mark.django_db
CRISP_WEBSITE_ID="123",
FRONTEND_THEME="test-theme",
MEDIA_BASE_URL="http://testserver/",
+ POSTHOG_KEY={"id": "132456", "host": "https://eu.i.posthog-test.com"},
SENTRY_DSN="https://sentry.test/123",
)
@pytest.mark.parametrize("is_authenticated", [False, True])
@@ -41,5 +42,6 @@ def test_api_config(is_authenticated):
"LANGUAGES": [["en-us", "English"], ["fr-fr", "French"], ["de-de", "German"]],
"LANGUAGE_CODE": "en-us",
"MEDIA_BASE_URL": "http://testserver/",
+ "POSTHOG_KEY": {"id": "132456", "host": "https://eu.i.posthog-test.com"},
"SENTRY_DSN": "https://sentry.test/123",
}
diff --git a/src/backend/core/tests/test_api_users.py b/src/backend/core/tests/test_api_users.py
index e739d4d1..cb3022d3 100644
--- a/src/backend/core/tests/test_api_users.py
+++ b/src/backend/core/tests/test_api_users.py
@@ -42,8 +42,9 @@ def test_api_users_list_authenticated():
def test_api_users_list_query_email():
"""
- Authenticated users should be able to list users
- and filter by email.
+ Authenticated users should be able to list users and filter by email.
+ Only results with a Levenstein distance less than 3 with the query should be returned.
+ We want to match by Levenstein distance because we want to prevent typing errors.
"""
user = factories.UserFactory()
@@ -51,9 +52,7 @@ def test_api_users_list_query_email():
client.force_login(user)
dave = factories.UserFactory(email="david.bowman@work.com")
- nicole = factories.UserFactory(email="nicole_foole@work.com")
- frank = factories.UserFactory(email="frank_poole@work.com")
- factories.UserFactory(email="heywood_floyd@work.com")
+ factories.UserFactory(email="nicole.bowman@work.com")
response = client.get(
"/api/v1.0/users/?q=david.bowman@work.com",
@@ -62,59 +61,53 @@ def test_api_users_list_query_email():
user_ids = [user["id"] for user in response.json()["results"]]
assert user_ids == [str(dave.id)]
- response = client.get("/api/v1.0/users/?q=oole")
-
+ response = client.get(
+ "/api/v1.0/users/?q=davig.bovman@worm.com",
+ )
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()["results"]]
- assert user_ids == [str(nicole.id), str(frank.id)]
+ assert user_ids == [str(dave.id)]
+
+ response = client.get(
+ "/api/v1.0/users/?q=davig.bovman@worm.cop",
+ )
+ assert response.status_code == 200
+ user_ids = [user["id"] for user in response.json()["results"]]
+ assert user_ids == []
def test_api_users_list_query_email_matching():
- """While filtering by email, results should be filtered and sorted by similarity"""
+ """While filtering by email, results should be filtered and sorted by Levenstein distance."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
- alice = factories.UserFactory(email="alice.johnson@example.gouv.fr")
- factories.UserFactory(email="jane.smith@example.gouv.fr")
- michael_wilson = factories.UserFactory(email="michael.wilson@example.gouv.fr")
- factories.UserFactory(email="david.jones@example.gouv.fr")
- michael_brown = factories.UserFactory(email="michael.brown@example.gouv.fr")
- factories.UserFactory(email="sophia.taylor@example.gouv.fr")
+ user1 = factories.UserFactory(email="alice.johnson@example.gouv.fr")
+ user2 = factories.UserFactory(email="alice.johnnson@example.gouv.fr")
+ user3 = factories.UserFactory(email="alice.kohlson@example.gouv.fr")
+ user4 = factories.UserFactory(email="alicia.johnnson@example.gouv.fr")
+ user5 = factories.UserFactory(email="alicia.johnnson@example.gov.uk")
+ factories.UserFactory(email="alice.thomson@example.gouv.fr")
response = client.get(
- "/api/v1.0/users/?q=michael.johnson@example.gouv.f",
+ "/api/v1.0/users/?q=alice.johnson@example.gouv.fr",
)
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()["results"]]
- assert user_ids == [str(michael_wilson.id)]
+ assert user_ids == [str(user1.id), str(user2.id), str(user3.id), str(user4.id)]
- response = client.get("/api/v1.0/users/?q=michael.johnson@example.gouv.fr")
+ response = client.get("/api/v1.0/users/?q=alicia.johnnson@example.gouv.fr")
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()["results"]]
- assert user_ids == [str(michael_wilson.id), str(alice.id), str(michael_brown.id)]
-
- response = client.get(
- "/api/v1.0/users/?q=ajohnson@example.gouv.f",
- )
- assert response.status_code == 200
- user_ids = [user["id"] for user in response.json()["results"]]
- assert user_ids == [str(alice.id)]
-
- response = client.get(
- "/api/v1.0/users/?q=michael.wilson@example.gouv.f",
- )
- assert response.status_code == 200
- user_ids = [user["id"] for user in response.json()["results"]]
- assert user_ids == [str(michael_wilson.id)]
+ assert user_ids == [str(user4.id), str(user2.id), str(user1.id), str(user5.id)]
def test_api_users_list_query_email_exclude_doc_user():
"""
- Authenticated users should be able to list users
- and filter by email and exclude users who have access to a document.
+ Authenticated users should be able to list users while filtering by email
+ and excluding users who have access to a document.
"""
user = factories.UserFactory()
document = factories.DocumentFactory()
@@ -122,17 +115,19 @@ def test_api_users_list_query_email_exclude_doc_user():
client = APIClient()
client.force_login(user)
- nicole = factories.UserFactory(email="nicole_foole@work.com")
- frank = factories.UserFactory(email="frank_poole@work.com")
+ nicole_fool = factories.UserFactory(email="nicole_fool@work.com")
+ nicole_pool = factories.UserFactory(email="nicole_pool@work.com")
factories.UserFactory(email="heywood_floyd@work.com")
- factories.UserDocumentAccessFactory(document=document, user=frank)
+ factories.UserDocumentAccessFactory(document=document, user=nicole_pool)
- response = client.get("/api/v1.0/users/?q=oole&document_id=" + str(document.id))
+ response = client.get(
+ "/api/v1.0/users/?q=nicole_fool@work.com&document_id=" + str(document.id)
+ )
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()["results"]]
- assert user_ids == [str(nicole.id)]
+ assert user_ids == [str(nicole_fool.id)]
def test_api_users_retrieve_me_anonymous():
diff --git a/src/backend/core/tests/test_models_templates.py b/src/backend/core/tests/test_models_templates.py
index 6e7cba2c..95f8fbde 100644
--- a/src/backend/core/tests/test_models_templates.py
+++ b/src/backend/core/tests/test_models_templates.py
@@ -2,10 +2,6 @@
Unit tests for the Template model
"""
-import os
-import time
-from unittest import mock
-
from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import ValidationError
@@ -189,31 +185,3 @@ def test_models_templates_get_abilities_preset_role(django_assert_num_queries):
"partial_update": False,
"generate_document": True,
}
-
-
-def test_models_templates__generate_word():
- """Generate word document and assert no tmp files are left in /tmp folder."""
- template = factories.TemplateFactory()
- response = template.generate_word("Test body
", {})
-
- assert response.status_code == 200
- assert len([f for f in os.listdir("/tmp") if f.startswith("docx_")]) == 0
-
-
-@mock.patch(
- "pypandoc.convert_text",
- side_effect=RuntimeError("Conversion failed"),
-)
-def test_models_templates__generate_word__raise_error(_mock_pypandoc):
- """
- Generate word document and assert no tmp files are left in /tmp folder
- even when the conversion fails.
- """
- template = factories.TemplateFactory()
-
- try:
- template.generate_word("Test body
", {})
- except RuntimeError as e:
- assert str(e) == "Conversion failed"
- time.sleep(0.5)
- assert len([f for f in os.listdir("/tmp") if f.startswith("docx_")]) == 0
diff --git a/src/backend/core/tests/test_services_collaboration_services.py b/src/backend/core/tests/test_services_collaboration_services.py
index 7d02e252..5a915170 100644
--- a/src/backend/core/tests/test_services_collaboration_services.py
+++ b/src/backend/core/tests/test_services_collaboration_services.py
@@ -55,9 +55,9 @@ def mock_reset_connections(settings):
)
yield
- assert (
- len(rsps.calls) == 1
- ), "Expected one call to reset-connections endpoint"
+ assert len(rsps.calls) == 1, (
+ "Expected one call to reset-connections endpoint"
+ )
request = rsps.calls[0].request
assert request.url == endpoint_url, f"Unexpected URL called: {request.url}"
assert (
@@ -66,9 +66,9 @@ def mock_reset_connections(settings):
), "Incorrect Authorization header"
if user_id:
- assert (
- request.headers.get("X-User-Id") == user_id
- ), "Incorrect X-User-Id header"
+ assert request.headers.get("X-User-Id") == user_id, (
+ "Incorrect X-User-Id header"
+ )
return _mock_reset_connections
diff --git a/src/backend/demo/data/template/code.txt b/src/backend/demo/data/template/code.txt
index 0ab83f60..56f6736b 100644
--- a/src/backend/demo/data/template/code.txt
+++ b/src/backend/demo/data/template/code.txt
@@ -1,10 +1,2 @@
-
-
-
-
+
+
\ No newline at end of file
diff --git a/src/backend/demo/data/template/css.txt b/src/backend/demo/data/template/css.txt
index 79a440ab..e69de29b 100644
--- a/src/backend/demo/data/template/css.txt
+++ b/src/backend/demo/data/template/css.txt
@@ -1,20 +0,0 @@
-body {
- background: white;
- font-family: arial;
-}
-.header img {
- width: 5cm;
- margin-left: -0.4cm;
-}
-.body{
- margin-top: 1.5rem;
-}
-img {
- max-width: 100%;
-}
-[custom-style="center"] {
- text-align: center;
-}
-[custom-style="right"] {
- text-align: right;
-}
diff --git a/src/backend/impress/settings.py b/src/backend/impress/settings.py
index 495ec4bc..df1cd39b 100755
--- a/src/backend/impress/settings.py
+++ b/src/backend/impress/settings.py
@@ -390,6 +390,11 @@ class Base(Configuration):
None, environ_name="FRONTEND_THEME", environ_prefix=None
)
+ # Posthog
+ POSTHOG_KEY = values.DictValue(
+ None, environ_name="POSTHOG_KEY", environ_prefix=None
+ )
+
# Crisp
CRISP_WEBSITE_ID = values.Value(
None, environ_name="CRISP_WEBSITE_ID", environ_prefix=None
diff --git a/src/backend/locale/de_DE/LC_MESSAGES/django.po b/src/backend/locale/de_DE/LC_MESSAGES/django.po
index a9db616c..31bc9062 100644
--- a/src/backend/locale/de_DE/LC_MESSAGES/django.po
+++ b/src/backend/locale/de_DE/LC_MESSAGES/django.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-15 21:00+0000\n"
-"PO-Revision-Date: 2025-01-16 19:53\n"
+"PO-Revision-Date: 2025-01-27 09:27\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
diff --git a/src/backend/locale/en_US/LC_MESSAGES/django.po b/src/backend/locale/en_US/LC_MESSAGES/django.po
index 1b2f03c3..34382019 100644
--- a/src/backend/locale/en_US/LC_MESSAGES/django.po
+++ b/src/backend/locale/en_US/LC_MESSAGES/django.po
@@ -1,9 +1,9 @@
msgid ""
msgstr ""
-"Project-Id-Version: lasuite-people\n"
+"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-12-17 15:50+0000\n"
-"PO-Revision-Date: 2024-12-17 15:53\n"
+"POT-Creation-Date: 2025-01-15 21:00+0000\n"
+"PO-Revision-Date: 2025-01-27 09:27\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -11,384 +11,342 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"X-Crowdin-Project: lasuite-people\n"
-"X-Crowdin-Project-ID: 637934\n"
+"X-Crowdin-Project: lasuite-docs\n"
+"X-Crowdin-Project-ID: 754523\n"
"X-Crowdin-Language: en\n"
"X-Crowdin-File: backend-impress.pot\n"
-"X-Crowdin-File-ID: 8\n"
+"X-Crowdin-File-ID: 18\n"
-#: core/admin.py:33
+#: build/lib/core/admin.py:33 core/admin.py:33
msgid "Personal info"
msgstr ""
-#: core/admin.py:46
+#: build/lib/core/admin.py:46 core/admin.py:46
msgid "Permissions"
msgstr ""
-#: core/admin.py:58
+#: build/lib/core/admin.py:58 core/admin.py:58
msgid "Important dates"
msgstr ""
-#: core/api/filters.py:16
+#: build/lib/core/api/filters.py:16 core/api/filters.py:16
msgid "Creator is me"
msgstr ""
-#: core/api/filters.py:19
+#: build/lib/core/api/filters.py:19 core/api/filters.py:19
msgid "Favorite"
msgstr ""
-#: core/api/filters.py:22
+#: build/lib/core/api/filters.py:22 core/api/filters.py:22
msgid "Title"
msgstr ""
-#: core/api/serializers.py:307
+#: build/lib/core/api/serializers.py:317 core/api/serializers.py:317
msgid "A new document was created on your behalf!"
msgstr ""
-#: core/api/serializers.py:311
+#: build/lib/core/api/serializers.py:321 core/api/serializers.py:321
msgid "You have been granted ownership of a new document:"
msgstr ""
-#: core/api/serializers.py:414
+#: build/lib/core/api/serializers.py:422 core/api/serializers.py:422
msgid "Body"
msgstr ""
-#: core/api/serializers.py:417
+#: build/lib/core/api/serializers.py:425 core/api/serializers.py:425
msgid "Body type"
msgstr ""
-#: core/api/serializers.py:423
+#: build/lib/core/api/serializers.py:431 core/api/serializers.py:431
msgid "Format"
msgstr ""
-#: core/authentication/backends.py:57
+#: build/lib/core/authentication/backends.py:61
+#: core/authentication/backends.py:61
msgid "Invalid response format or token verification failed"
msgstr ""
-#: core/authentication/backends.py:81
-msgid "User info contained no recognizable user identification"
-msgstr ""
-
-#: core/authentication/backends.py:88
+#: build/lib/core/authentication/backends.py:108
+#: core/authentication/backends.py:108
msgid "User account is disabled"
msgstr ""
-#: core/models.py:62 core/models.py:69
+#: build/lib/core/models.py:63 build/lib/core/models.py:70 core/models.py:63
+#: core/models.py:70
msgid "Reader"
msgstr ""
-#: core/models.py:63 core/models.py:70
+#: build/lib/core/models.py:64 build/lib/core/models.py:71 core/models.py:64
+#: core/models.py:71
msgid "Editor"
msgstr ""
-#: core/models.py:71
+#: build/lib/core/models.py:72 core/models.py:72
msgid "Administrator"
msgstr ""
-#: core/models.py:72
+#: build/lib/core/models.py:73 core/models.py:73
msgid "Owner"
msgstr ""
-#: core/models.py:83
+#: build/lib/core/models.py:84 core/models.py:84
msgid "Restricted"
msgstr ""
-#: core/models.py:87
+#: build/lib/core/models.py:88 core/models.py:88
msgid "Authenticated"
msgstr ""
-#: core/models.py:89
+#: build/lib/core/models.py:90 core/models.py:90
msgid "Public"
msgstr ""
-#: core/models.py:101
+#: build/lib/core/models.py:112 core/models.py:112
msgid "id"
msgstr ""
-#: core/models.py:102
+#: build/lib/core/models.py:113 core/models.py:113
msgid "primary key for the record as UUID"
msgstr ""
-#: core/models.py:108
+#: build/lib/core/models.py:119 core/models.py:119
msgid "created on"
msgstr ""
-#: core/models.py:109
+#: build/lib/core/models.py:120 core/models.py:120
msgid "date and time at which a record was created"
msgstr ""
-#: core/models.py:114
+#: build/lib/core/models.py:125 core/models.py:125
msgid "updated on"
msgstr ""
-#: core/models.py:115
+#: build/lib/core/models.py:126 core/models.py:126
msgid "date and time at which a record was last updated"
msgstr ""
-#: core/models.py:135
+#: build/lib/core/models.py:162 core/models.py:162
+msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
+msgstr ""
+
+#: build/lib/core/models.py:175 core/models.py:175
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
-#: core/models.py:141
+#: build/lib/core/models.py:181 core/models.py:181
msgid "sub"
msgstr ""
-#: core/models.py:143
+#: build/lib/core/models.py:183 core/models.py:183
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
-#: core/models.py:152
+#: build/lib/core/models.py:192 core/models.py:192
msgid "full name"
msgstr ""
-#: core/models.py:153
+#: build/lib/core/models.py:193 core/models.py:193
msgid "short name"
msgstr ""
-#: core/models.py:155
+#: build/lib/core/models.py:195 core/models.py:195
msgid "identity email address"
msgstr ""
-#: core/models.py:160
+#: build/lib/core/models.py:200 core/models.py:200
msgid "admin email address"
msgstr ""
-#: core/models.py:167
+#: build/lib/core/models.py:207 core/models.py:207
msgid "language"
msgstr ""
-#: core/models.py:168
+#: build/lib/core/models.py:208 core/models.py:208
msgid "The language in which the user wants to see the interface."
msgstr ""
-#: core/models.py:174
+#: build/lib/core/models.py:214 core/models.py:214
msgid "The timezone in which the user wants to see times."
msgstr ""
-#: core/models.py:177
+#: build/lib/core/models.py:217 core/models.py:217
msgid "device"
msgstr ""
-#: core/models.py:179
+#: build/lib/core/models.py:219 core/models.py:219
msgid "Whether the user is a device or a real user."
msgstr ""
-#: core/models.py:182
+#: build/lib/core/models.py:222 core/models.py:222
msgid "staff status"
msgstr ""
-#: core/models.py:184
+#: build/lib/core/models.py:224 core/models.py:224
msgid "Whether the user can log into this admin site."
msgstr ""
-#: core/models.py:187
+#: build/lib/core/models.py:227 core/models.py:227
msgid "active"
msgstr ""
-#: core/models.py:190
+#: build/lib/core/models.py:230 core/models.py:230
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
-#: core/models.py:202
+#: build/lib/core/models.py:242 core/models.py:242
msgid "user"
msgstr ""
-#: core/models.py:203
+#: build/lib/core/models.py:243 core/models.py:243
msgid "users"
msgstr ""
-#: core/models.py:342 core/models.py:718
+#: build/lib/core/models.py:382 build/lib/core/models.py:758 core/models.py:382
+#: core/models.py:758
msgid "title"
msgstr ""
-#: core/models.py:364
+#: build/lib/core/models.py:404 core/models.py:404
msgid "Document"
msgstr ""
-#: core/models.py:365
+#: build/lib/core/models.py:405 core/models.py:405
msgid "Documents"
msgstr ""
-#: core/models.py:368
+#: build/lib/core/models.py:408 core/models.py:408
msgid "Untitled Document"
msgstr ""
-#: core/models.py:593
+#: build/lib/core/models.py:633 core/models.py:633
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
-#: core/models.py:597
+#: build/lib/core/models.py:637 core/models.py:637
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
-#: core/models.py:600
+#: build/lib/core/models.py:640 core/models.py:640
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
-#: core/models.py:623
+#: build/lib/core/models.py:663 core/models.py:663
msgid "Document/user link trace"
msgstr ""
-#: core/models.py:624
+#: build/lib/core/models.py:664 core/models.py:664
msgid "Document/user link traces"
msgstr ""
-#: core/models.py:630
+#: build/lib/core/models.py:670 core/models.py:670
msgid "A link trace already exists for this document/user."
msgstr ""
-#: core/models.py:653
+#: build/lib/core/models.py:693 core/models.py:693
msgid "Document favorite"
msgstr ""
-#: core/models.py:654
+#: build/lib/core/models.py:694 core/models.py:694
msgid "Document favorites"
msgstr ""
-#: core/models.py:660
+#: build/lib/core/models.py:700 core/models.py:700
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
-#: core/models.py:682
+#: build/lib/core/models.py:722 core/models.py:722
msgid "Document/user relation"
msgstr ""
-#: core/models.py:683
+#: build/lib/core/models.py:723 core/models.py:723
msgid "Document/user relations"
msgstr ""
-#: core/models.py:689
+#: build/lib/core/models.py:729 core/models.py:729
msgid "This user is already in this document."
msgstr ""
-#: core/models.py:695
+#: build/lib/core/models.py:735 core/models.py:735
msgid "This team is already in this document."
msgstr ""
-#: core/models.py:701 core/models.py:890
+#: build/lib/core/models.py:741 build/lib/core/models.py:930 core/models.py:741
+#: core/models.py:930
msgid "Either user or team must be set, not both."
msgstr ""
-#: core/models.py:719
+#: build/lib/core/models.py:759 core/models.py:759
msgid "description"
msgstr ""
-#: core/models.py:720
+#: build/lib/core/models.py:760 core/models.py:760
msgid "code"
msgstr ""
-#: core/models.py:721
+#: build/lib/core/models.py:761 core/models.py:761
msgid "css"
msgstr ""
-#: core/models.py:723
+#: build/lib/core/models.py:763 core/models.py:763
msgid "public"
msgstr ""
-#: core/models.py:725
+#: build/lib/core/models.py:765 core/models.py:765
msgid "Whether this template is public for anyone to use."
msgstr ""
-#: core/models.py:731
+#: build/lib/core/models.py:771 core/models.py:771
msgid "Template"
msgstr ""
-#: core/models.py:732
+#: build/lib/core/models.py:772 core/models.py:772
msgid "Templates"
msgstr ""
-#: core/models.py:871
+#: build/lib/core/models.py:911 core/models.py:911
msgid "Template/user relation"
msgstr ""
-#: core/models.py:872
+#: build/lib/core/models.py:912 core/models.py:912
msgid "Template/user relations"
msgstr ""
-#: core/models.py:878
+#: build/lib/core/models.py:918 core/models.py:918
msgid "This user is already in this template."
msgstr ""
-#: core/models.py:884
+#: build/lib/core/models.py:924 core/models.py:924
msgid "This team is already in this template."
msgstr ""
-#: core/models.py:907
+#: build/lib/core/models.py:947 core/models.py:947
msgid "email address"
msgstr ""
-#: core/models.py:926
+#: build/lib/core/models.py:966 core/models.py:966
msgid "Document invitation"
msgstr ""
-#: core/models.py:927
+#: build/lib/core/models.py:967 core/models.py:967
msgid "Document invitations"
msgstr ""
-#: core/models.py:944
+#: build/lib/core/models.py:987 core/models.py:987
msgid "This email is already associated to a registered user."
msgstr ""
-#: core/templates/mail/html/hello.html:159 core/templates/mail/text/hello.txt:3
-msgid "Company logo"
-msgstr ""
-
-#: core/templates/mail/html/hello.html:188 core/templates/mail/text/hello.txt:5
-#, python-format
-msgid "Hello %(name)s"
-msgstr ""
-
-#: core/templates/mail/html/hello.html:188 core/templates/mail/text/hello.txt:5
-msgid "Hello"
-msgstr ""
-
-#: core/templates/mail/html/hello.html:189 core/templates/mail/text/hello.txt:6
-msgid "Thank you very much for your visit!"
-msgstr ""
-
-#: core/templates/mail/html/hello.html:221
-#, python-format
-msgid "This mail has been sent to %(email)s by %(name)s "
-msgstr ""
-
-#: core/templates/mail/html/invitation.html:162
-#: core/templates/mail/text/invitation.txt:3
-msgid "Logo email"
-msgstr ""
-
-#: core/templates/mail/html/invitation.html:209
-#: core/templates/mail/text/invitation.txt:10
-msgid "Open"
-msgstr ""
-
-#: core/templates/mail/html/invitation.html:226
-#: core/templates/mail/text/invitation.txt:14
-msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
-msgstr ""
-
-#: core/templates/mail/html/invitation.html:233
-#: core/templates/mail/text/invitation.txt:16
-#, python-format
-msgid " Brought to you by %(brandname)s "
-msgstr ""
-
-#: core/templates/mail/text/hello.txt:8
-#, python-format
-msgid "This mail has been sent to %(email)s by %(name)s [%(href)s]"
-msgstr ""
-
-#: impress/settings.py:236
+#: build/lib/impress/settings.py:236 impress/settings.py:236
msgid "English"
msgstr ""
-#: impress/settings.py:237
+#: build/lib/impress/settings.py:237 impress/settings.py:237
msgid "French"
msgstr ""
-#: impress/settings.py:238
+#: build/lib/impress/settings.py:238 impress/settings.py:238
msgid "German"
msgstr ""
diff --git a/src/backend/locale/fr_FR/LC_MESSAGES/django.po b/src/backend/locale/fr_FR/LC_MESSAGES/django.po
index 27272fd9..a93a965a 100644
--- a/src/backend/locale/fr_FR/LC_MESSAGES/django.po
+++ b/src/backend/locale/fr_FR/LC_MESSAGES/django.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-15 21:00+0000\n"
-"PO-Revision-Date: 2025-01-16 19:53\n"
+"PO-Revision-Date: 2025-01-27 09:27\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
diff --git a/src/backend/locale/nl_NL/LC_MESSAGES/django.po b/src/backend/locale/nl_NL/LC_MESSAGES/django.po
index f979f7bb..a0c45428 100644
--- a/src/backend/locale/nl_NL/LC_MESSAGES/django.po
+++ b/src/backend/locale/nl_NL/LC_MESSAGES/django.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-15 21:00+0000\n"
-"PO-Revision-Date: 2025-01-16 19:53\n"
+"PO-Revision-Date: 2025-01-27 09:27\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml
index 4ce29fa6..f470b9ae 100644
--- a/src/backend/pyproject.toml
+++ b/src/backend/pyproject.toml
@@ -25,7 +25,7 @@ license = { file = "LICENSE" }
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
- "boto3==1.35.90",
+ "boto3==1.36.7",
"Brotli==1.1.0",
"celery[redis]==5.4.0",
"django-configurations==2.5.1",
@@ -47,16 +47,13 @@ dependencies = [
"jsonschema==4.23.0",
"markdown==3.7",
"nested-multipart-parser==1.5.0",
- "openai==1.58.1",
- "psycopg[binary]==3.2.3",
+ "openai==1.60.2",
+ "psycopg[binary]==3.2.4",
"PyJWT==2.10.1",
- "pypandoc==1.14",
- "python-frontmatter==1.1.0",
"python-magic==0.4.27",
"requests==2.32.3",
- "sentry-sdk==2.19.2",
+ "sentry-sdk==2.20.0",
"url-normalize==1.4.3",
- "WeasyPrint>=60.2",
"whitenoise==6.8.2",
"mozilla-django-oidc==4.0.1",
]
@@ -74,16 +71,16 @@ dev = [
"freezegun==1.5.1",
"ipdb==0.13.13",
"ipython==8.31.0",
- "pyfakefs==5.7.3",
+ "pyfakefs==5.7.4",
"pylint-django==2.6.1",
- "pylint==3.3.3",
+ "pylint==3.3.4",
"pytest-cov==6.0.0",
"pytest-django==4.9.0",
"pytest==8.3.4",
"pytest-icdiff==0.9",
"pytest-xdist==3.6.1",
- "responses==0.25.3",
- "ruff==0.8.4",
+ "responses==0.25.6",
+ "ruff==0.9.3",
"types-requests==2.32.0.20241016",
]
diff --git a/src/frontend/apps/e2e/__tests__/app-impress/config.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/config.spec.ts
index cbc9a025..042c6287 100644
--- a/src/frontend/apps/e2e/__tests__/app-impress/config.spec.ts
+++ b/src/frontend/apps/e2e/__tests__/app-impress/config.spec.ts
@@ -16,6 +16,7 @@ const config = {
['de-de', 'German'],
],
LANGUAGE_CODE: 'en-us',
+ POSTHOG_KEY: {},
SENTRY_DSN: null,
};
diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts
index 8dba8dab..02048c7f 100644
--- a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts
+++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts
@@ -14,6 +14,29 @@ test.beforeEach(async ({ page }) => {
});
test.describe('Doc Editor', () => {
+ test('it saves the doc when we quit pages', async ({ page, browserName }) => {
+ // eslint-disable-next-line playwright/no-skipped-test
+ test.skip(browserName === 'webkit', 'This test is very flaky with webkit');
+
+ // Check the first doc
+ const doc = await goToGridDoc(page);
+
+ await verifyDocName(page, doc);
+
+ const editor = page.locator('.ProseMirror');
+ await editor.click();
+ await editor.fill('Hello World Doc persisted 2');
+ await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible();
+
+ await page.goto('/');
+
+ await goToGridDoc(page, {
+ title: doc,
+ });
+
+ await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible();
+ });
+
test('it check translations of the slash menu when changing language', async ({
page,
browserName,
@@ -241,29 +264,6 @@ test.describe('Doc Editor', () => {
await expect(editor.getByText('Hello World Doc persisted 1')).toBeVisible();
});
- test('it saves the doc when we quit pages', async ({ page, browserName }) => {
- // eslint-disable-next-line playwright/no-skipped-test
- test.skip(browserName === 'webkit', 'This test is very flaky with webkit');
-
- // Check the first doc
- const doc = await goToGridDoc(page);
-
- await verifyDocName(page, doc);
-
- const editor = page.locator('.ProseMirror');
- await editor.click();
- await editor.fill('Hello World Doc persisted 2');
- await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible();
-
- await page.goto('/');
-
- await goToGridDoc(page, {
- title: doc,
- });
-
- await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible();
- });
-
test('it cannot edit if viewer', async ({ page }) => {
await mockedDocument(page, {
abilities: {
diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts
index 6e62d3ad..eff9592a 100644
--- a/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts
+++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts
@@ -1,6 +1,7 @@
+import path from 'path';
+
import { expect, test } from '@playwright/test';
import cs from 'convert-stream';
-import jsdom from 'jsdom';
import pdf from 'pdf-parse';
import { createDoc, verifyDocName } from './common';
@@ -41,10 +42,8 @@ test.describe('Doc Export', () => {
).toBeVisible();
await expect(page.getByRole('button', { name: 'Download' })).toBeVisible();
});
- test('it converts the doc to pdf with a template integrated', async ({
- page,
- browserName,
- }) => {
+
+ test('it exports the doc to pdf', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
const downloadPromise = page.waitForEvent('download', (download) => {
@@ -77,10 +76,7 @@ test.describe('Doc Export', () => {
expect(pdfText).toContain('Hello World'); // This is the doc text
});
- test('it converts the doc to docx with a template integrated', async ({
- page,
- browserName,
- }) => {
+ test('it exports the doc to docx', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
const downloadPromise = page.waitForEvent('download', (download) => {
@@ -111,152 +107,75 @@ test.describe('Doc Export', () => {
expect(download.suggestedFilename()).toBe(`${randomDoc}.docx`);
});
- test('it converts the blocknote json in correct html for the export', async ({
- page,
- browserName,
- }) => {
- test.setTimeout(60000);
-
+ /**
+ * This test tell us that the export to pdf is working with images
+ * but it does not tell us if the images are beeing displayed correctly
+ * in the pdf.
+ *
+ * TODO: Check if the images are displayed correctly in the pdf
+ */
+ test('it exports the docs with images', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
- let body = '';
- await page.route('**/templates/*/generate-document/', async (route) => {
- const request = route.request();
- body = request.postDataJSON().body;
-
- await route.continue();
+ const fileChooserPromise = page.waitForEvent('filechooser');
+ const downloadPromise = page.waitForEvent('download', (download) => {
+ return download.suggestedFilename().includes(`${randomDoc}.pdf`);
});
await verifyDocName(page, randomDoc);
- await page.locator('.bn-block-outer').last().fill('Hello World');
- await page.locator('.bn-block-outer').last().click();
- await page.keyboard.press('Enter');
- await page.keyboard.press('Enter');
- await page.locator('.bn-block-outer').last().fill('Break');
- await expect(page.getByText('Break')).toBeVisible();
+ await page.locator('.ProseMirror.bn-editor').click();
+ await page.locator('.ProseMirror.bn-editor').fill('Hello World');
- // Center the text
- await page.getByText('Break').dblclick();
- await page.locator('button[data-test="alignTextCenter"]').click();
-
- // Change the background color
- await page.locator('button[data-test="colors"]').click();
- await page.locator('button[data-test="background-color-brown"]').click();
-
- // Change the text color
- await page.getByText('Break').dblclick();
- await page.locator('button[data-test="colors"]').click();
- await page.locator('button[data-test="text-color-orange"]').click();
-
- // Add a list
- await page.locator('.bn-block-outer').last().click();
await page.keyboard.press('Enter');
await page.locator('.bn-block-outer').last().fill('/');
- await page.getByText('Bullet List').click();
- await page
- .locator('.bn-block-content[data-content-type="bulletListItem"] p')
- .last()
- .fill('Test List 1');
- // eslint-disable-next-line playwright/no-wait-for-timeout
- await page.waitForTimeout(300);
- await page.keyboard.press('Enter');
- await page
- .locator('.bn-block-content[data-content-type="bulletListItem"] p')
- .last()
- .fill('Test List 2');
- await page.keyboard.press('Enter');
- await page
- .locator('.bn-block-content[data-content-type="bulletListItem"] p')
- .last()
- .fill('Test List 3');
+ await page.getByText('Resizable image with caption').click();
+ await page.getByText('Upload image').click();
- await page.keyboard.press('Enter');
- await page.keyboard.press('Backspace');
+ const fileChooser = await fileChooserPromise;
+ await fileChooser.setFiles(
+ path.join(__dirname, 'assets/logo-suite-numerique.png'),
+ );
- // Add a number list
- await page.locator('.bn-block-outer').last().click();
- await page.keyboard.press('Enter');
- await page.locator('.bn-block-outer').last().fill('/');
- await page.getByText('Numbered List').click();
- await page
- .locator('.bn-block-content[data-content-type="numberedListItem"] p')
- .last()
- .fill('Test Number 1');
- // eslint-disable-next-line playwright/no-wait-for-timeout
- await page.waitForTimeout(300);
- await page.keyboard.press('Enter');
- await page
- .locator('.bn-block-content[data-content-type="numberedListItem"] p')
- .last()
- .fill('Test Number 2');
- await page.keyboard.press('Enter');
- await page
- .locator('.bn-block-content[data-content-type="numberedListItem"] p')
- .last()
- .fill('Test Number 3');
+ const image = page.getByRole('img', { name: 'logo-suite-numerique.png' });
- // Add img
- await page.locator('.bn-block-outer').last().click();
- await page.keyboard.press('Enter');
- await page.locator('.bn-block-outer').last().fill('/');
- await page
- .getByRole('option', {
- name: 'Image',
- })
- .click();
- await page
- .getByRole('tab', {
- name: 'Embed',
- })
- .click();
- await page
- .getByPlaceholder('Enter URL')
- .fill('https://example.com/image.jpg');
- await page
- .getByRole('button', {
- name: 'Embed image',
- })
- .click();
+ await expect(image).toBeVisible();
- // Download
await page
.getByRole('button', {
name: 'download',
})
.click();
+ await page
+ .getByRole('combobox', {
+ name: 'Template',
+ })
+ .click();
+
+ await page
+ .getByRole('option', {
+ name: 'Demo Template',
+ })
+ .click({
+ delay: 100,
+ });
+
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+
await page
.getByRole('button', {
name: 'Download',
})
.click();
- // Empty paragraph should be replaced by a
- expect(body.match(/ /g)?.length).toBeGreaterThanOrEqual(2);
- expect(body).toContain('style="color: orange;"');
- expect(body).toContain('custom-style="center"');
- expect(body).toContain('style="background-color: brown;"');
+ const download = await downloadPromise;
+ expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
- const { JSDOM } = jsdom;
- const DOMParser = new JSDOM().window.DOMParser;
- const parser = new DOMParser();
- const html = parser.parseFromString(body, 'text/html');
+ const pdfBuffer = await cs.toBuffer(await download.createReadStream());
+ const pdfExport = await pdf(pdfBuffer);
+ const pdfText = pdfExport.text;
- const ulLis = html.querySelectorAll('ul li');
- expect(ulLis.length).toBe(3);
- expect(ulLis[0].textContent).toBe('Test List 1');
- expect(ulLis[1].textContent).toBe('Test List 2');
- expect(ulLis[2].textContent).toBe('Test List 3');
-
- const olLis = html.querySelectorAll('ol li');
- expect(olLis.length).toBe(3);
- expect(olLis[0].textContent).toBe('Test Number 1');
- expect(olLis[1].textContent).toBe('Test Number 2');
- expect(olLis[2].textContent).toBe('Test Number 3');
-
- const img = html.querySelectorAll('img');
- expect(img.length).toBe(1);
- expect(img[0].src).toBe('https://example.com/image.jpg');
+ expect(pdfText).toContain('Hello World');
});
});
diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts
index 8414b0c7..5e5a9dc2 100644
--- a/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts
+++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts
@@ -67,6 +67,53 @@ test.describe('Doc Visibility', () => {
test.describe('Doc Visibility: Restricted', () => {
test.use({ storageState: { cookies: [], origins: [] } });
+ test('A doc is accessible when member.', async ({ page, browserName }) => {
+ test.slow();
+ await page.goto('/');
+ await keyCloakSignIn(page, browserName);
+
+ const [docTitle] = await createDoc(page, 'Restricted auth', browserName, 1);
+
+ await verifyDocName(page, docTitle);
+
+ await page.getByRole('button', { name: 'Share' }).click();
+
+ const inputSearch = page.getByRole('combobox', {
+ name: 'Quick search input',
+ });
+
+ const otherBrowser = browsersName.find((b) => b !== browserName);
+ const username = `user@${otherBrowser}.e2e`;
+ await inputSearch.fill(username);
+ await page.getByRole('option', { name: username }).first().click();
+
+ // Choose a role
+ const container = page.getByTestId('doc-share-add-member-list');
+ await container.getByLabel('doc-role-dropdown').click();
+ await page.getByRole('button', { name: 'Administrator' }).click();
+
+ await page.getByRole('button', { name: 'Invite' }).click();
+
+ await page.locator('.c__modal__backdrop').click({
+ position: { x: 0, y: 0 },
+ });
+
+ const urlDoc = page.url();
+
+ await page
+ .getByRole('button', {
+ name: 'Logout',
+ })
+ .click();
+
+ await keyCloakSignIn(page, otherBrowser!);
+
+ await page.goto(urlDoc);
+
+ await verifyDocName(page, docTitle);
+ await expect(page.getByLabel('Share button')).toBeVisible();
+ });
+
test('A doc is not accessible when not authentified.', async ({
page,
browserName,
@@ -127,53 +174,6 @@ test.describe('Doc Visibility: Restricted', () => {
page.getByText('You do not have permission to perform this action.'),
).toBeVisible();
});
-
- test('A doc is accessible when member.', async ({ page, browserName }) => {
- test.slow();
- await page.goto('/');
- await keyCloakSignIn(page, browserName);
-
- const [docTitle] = await createDoc(page, 'Restricted auth', browserName, 1);
-
- await verifyDocName(page, docTitle);
-
- await page.getByRole('button', { name: 'Share' }).click();
-
- const inputSearch = page.getByRole('combobox', {
- name: 'Quick search input',
- });
-
- const otherBrowser = browsersName.find((b) => b !== browserName);
- const username = `user@${otherBrowser}.e2e`;
- await inputSearch.fill(username);
- await page.getByRole('option', { name: username }).click();
-
- // Choose a role
- const container = page.getByTestId('doc-share-add-member-list');
- await container.getByLabel('doc-role-dropdown').click();
- await page.getByRole('button', { name: 'Administrator' }).click();
-
- await page.getByRole('button', { name: 'Invite' }).click();
-
- await page.locator('.c__modal__backdrop').click({
- position: { x: 0, y: 0 },
- });
-
- const urlDoc = page.url();
-
- await page
- .getByRole('button', {
- name: 'Logout',
- })
- .click();
-
- await keyCloakSignIn(page, otherBrowser!);
-
- await page.goto(urlDoc);
-
- await verifyDocName(page, docTitle);
- await expect(page.getByLabel('Share button')).toBeVisible();
- });
});
test.describe('Doc Visibility: Public', () => {
diff --git a/src/frontend/apps/e2e/package.json b/src/frontend/apps/e2e/package.json
index b46958ca..b9dcad2d 100644
--- a/src/frontend/apps/e2e/package.json
+++ b/src/frontend/apps/e2e/package.json
@@ -22,7 +22,6 @@
},
"dependencies": {
"convert-stream": "1.0.2",
- "jsdom": "25.0.1",
"pdf-parse": "1.1.1"
}
}
diff --git a/src/frontend/apps/impress/.env b/src/frontend/apps/impress/.env
index 3cf0e897..e69de29b 100644
--- a/src/frontend/apps/impress/.env
+++ b/src/frontend/apps/impress/.env
@@ -1,2 +0,0 @@
-NEXT_PUBLIC_API_ORIGIN=
-NEXT_PUBLIC_SW_DEACTIVATED=
diff --git a/src/frontend/apps/impress/package.json b/src/frontend/apps/impress/package.json
index 658588e5..a4735a57 100644
--- a/src/frontend/apps/impress/package.json
+++ b/src/frontend/apps/impress/package.json
@@ -18,19 +18,24 @@
"@blocknote/core": "0.21.0",
"@blocknote/mantine": "0.21.0",
"@blocknote/react": "0.21.0",
+ "@blocknote/xl-docx-exporter": "0.21.0",
+ "@blocknote/xl-pdf-exporter": "0.21.0",
"@gouvfr-lasuite/integration": "1.0.2",
"@hocuspocus/provider": "2.15.0",
"@openfun/cunningham-react": "2.9.4",
+ "@react-pdf/renderer": "4.1.6",
"@sentry/nextjs": "8.47.0",
"@tanstack/react-query": "5.62.11",
"cmdk": "1.0.4",
"crisp-sdk-web": "1.0.25",
+ "docx": "9.1.0",
"i18next": "24.2.0",
"i18next-browser-languagedetector": "8.0.2",
"idb": "8.0.1",
"lodash": "4.17.21",
"luxon": "3.5.0",
"next": "15.1.3",
+ "posthog-js": "1.204.0",
"react": "*",
"react-aria-components": "1.5.0",
"react-dom": "*",
diff --git a/src/frontend/apps/impress/src/core/config/ConfigProvider.tsx b/src/frontend/apps/impress/src/core/config/ConfigProvider.tsx
index 57033110..8d021b14 100644
--- a/src/frontend/apps/impress/src/core/config/ConfigProvider.tsx
+++ b/src/frontend/apps/impress/src/core/config/ConfigProvider.tsx
@@ -3,7 +3,7 @@ import { PropsWithChildren, useEffect } from 'react';
import { Box } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
-import { configureCrispSession } from '@/services';
+import { PostHogProvider, configureCrispSession } from '@/services';
import { useSentryStore } from '@/stores/useSentryStore';
import { useConfig } from './api/useConfig';
@@ -45,5 +45,5 @@ export const ConfigProvider = ({ children }: PropsWithChildren) => {
);
}
- return children;
+ return {children} ;
};
diff --git a/src/frontend/apps/impress/src/core/config/api/useConfig.tsx b/src/frontend/apps/impress/src/core/config/api/useConfig.tsx
index 5cb6d3a9..7f26e9a8 100644
--- a/src/frontend/apps/impress/src/core/config/api/useConfig.tsx
+++ b/src/frontend/apps/impress/src/core/config/api/useConfig.tsx
@@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { Theme } from '@/cunningham/';
+import { PostHogConf } from '@/services';
interface ConfigResponse {
LANGUAGES: [string, string][];
@@ -11,6 +12,7 @@ interface ConfigResponse {
CRISP_WEBSITE_ID?: string;
FRONTEND_THEME?: Theme;
MEDIA_BASE_URL?: string;
+ POSTHOG_KEY?: PostHogConf;
SENTRY_DSN?: string;
}
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
index cdfdacaf..30786e3e 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
@@ -26,7 +26,7 @@ import {
} from '@/features/docs/doc-versioning';
import { useResponsiveStore } from '@/stores';
-import { ModalPDF } from './ModalExport';
+import { ModalExport } from './ModalExport';
interface DocToolBoxProps {
doc: Doc;
@@ -43,7 +43,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const colors = colorsTokens();
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
- const [isModalPDFOpen, setIsModalPDFOpen] = useState(false);
+ const [isModalExportOpen, setIsModalExportOpen] = useState(false);
const selectHistoryModal = useModal();
const modalShare = useModal();
@@ -63,7 +63,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
label: t('Export'),
icon: 'download',
callback: () => {
- setIsModalPDFOpen(true);
+ setIsModalExportOpen(true);
},
},
]
@@ -198,7 +198,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
}
onClick={() => {
- setIsModalPDFOpen(true);
+ setIsModalExportOpen(true);
}}
size={isSmallMobile ? 'small' : 'medium'}
/>
@@ -228,8 +228,8 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
{modalShare.isOpen && (
modalShare.close()} doc={doc} />
)}
- {isModalPDFOpen && (
- setIsModalPDFOpen(false)} doc={doc} />
+ {isModalExportOpen && (
+ setIsModalExportOpen(false)} doc={doc} />
)}
{isModalRemoveOpen && (
setIsModalRemoveOpen(false)} doc={doc} />
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/ModalExport.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/ModalExport.tsx
index e08ac21d..4e72a6eb 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/components/ModalExport.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/ModalExport.tsx
@@ -1,3 +1,11 @@
+import {
+ DOCXExporter,
+ docxDefaultSchemaMappings,
+} from '@blocknote/xl-docx-exporter';
+import {
+ PDFExporter,
+ pdfDefaultSchemaMappings,
+} from '@blocknote/xl-pdf-exporter';
import {
Button,
Loader,
@@ -7,91 +15,116 @@ import {
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
-import { useEffect, useMemo, useState } from 'react';
+import { pdf } from '@react-pdf/renderer';
+import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
+import { css } from 'styled-components';
import { Box, Text } from '@/components';
import { useEditorStore } from '@/features/docs/doc-editor';
import { Doc } from '@/features/docs/doc-management';
-import { useExport } from '../api/useExport';
import { TemplatesOrdering, useTemplates } from '../api/useTemplates';
-import { adaptBlockNoteHTML, downloadFile } from '../utils';
+import { downloadFile, exportResolveFileUrl } from '../utils';
-export enum DocDownloadFormat {
+enum DocDownloadFormat {
PDF = 'pdf',
DOCX = 'docx',
}
-interface ModalPDFProps {
+interface ModalExportProps {
onClose: () => void;
doc: Doc;
}
-export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
+export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
const { t } = useTranslation();
const { data: templates } = useTemplates({
ordering: TemplatesOrdering.BY_CREATED_ON_DESC,
});
const { toast } = useToastProvider();
const { editor } = useEditorStore();
- const {
- mutate: createExport,
- data: documentGenerated,
- isSuccess,
- isPending,
- error,
- } = useExport();
- const [templateIdSelected, setTemplateIdSelected] = useState();
+ const [templateSelected, setTemplateSelected] = useState('');
+ const [isExporting, setIsExporting] = useState(false);
const [format, setFormat] = useState(
DocDownloadFormat.PDF,
);
const templateOptions = useMemo(() => {
- if (!templates?.pages) {
- return [];
- }
-
- const templateOptions = templates.pages
+ const templateOptions = (templates?.pages || [])
.map((page) =>
page.results.map((template) => ({
label: template.title,
- value: template.id,
+ value: template.code,
})),
)
.flat();
- if (templateOptions.length) {
- setTemplateIdSelected(templateOptions[0].value);
- }
+ templateOptions.unshift({
+ label: t('Empty template'),
+ value: '',
+ });
return templateOptions;
- }, [templates?.pages]);
+ }, [t, templates?.pages]);
- useEffect(() => {
- if (!error) {
+ async function onSubmit() {
+ if (!editor) {
+ toast(t('The export failed'), VariantType.ERROR);
return;
}
- toast(error.message, VariantType.ERROR);
+ setIsExporting(true);
- onClose();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [error, t]);
-
- useEffect(() => {
- if (!documentGenerated || !isSuccess) {
- return;
- }
-
- // normalize title
const title = doc.title
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/\s/g, '-');
- downloadFile(documentGenerated, `${title}.${format}`);
+ const html = templateSelected;
+ let exportDocument = editor.document;
+ if (html) {
+ const blockTemplate = await editor.tryParseHTMLToBlocks(html);
+ exportDocument = [...blockTemplate, ...editor.document];
+ }
+
+ let blobExport: Blob;
+ if (format === DocDownloadFormat.PDF) {
+ const defaultExporter = new PDFExporter(
+ editor.schema,
+ pdfDefaultSchemaMappings,
+ );
+
+ const exporter = new PDFExporter(
+ editor.schema,
+ pdfDefaultSchemaMappings,
+ {
+ resolveFileUrl: async (url) =>
+ exportResolveFileUrl(url, defaultExporter.options.resolveFileUrl),
+ },
+ );
+ const pdfDocument = await exporter.toReactPDFDocument(exportDocument);
+ blobExport = await pdf(pdfDocument).toBlob();
+ } else {
+ const defaultExporter = new DOCXExporter(
+ editor.schema,
+ docxDefaultSchemaMappings,
+ );
+
+ const exporter = new DOCXExporter(
+ editor.schema,
+ docxDefaultSchemaMappings,
+ {
+ resolveFileUrl: async (url) =>
+ exportResolveFileUrl(url, defaultExporter.options.resolveFileUrl),
+ },
+ );
+
+ blobExport = await exporter.toBlob(exportDocument);
+ }
+
+ downloadFile(blobExport, `${title}.${format}`);
toast(
t('Your {{format}} was downloaded succesfully', {
@@ -100,29 +133,9 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
VariantType.SUCCESS,
);
+ setIsExporting(false);
+
onClose();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [documentGenerated, isSuccess, t]);
-
- async function onSubmit() {
- if (!templateIdSelected || !format) {
- return;
- }
-
- if (!editor) {
- toast(t('No editor found'), VariantType.ERROR);
- return;
- }
-
- let body = await editor.blocksToFullHTML(editor.document);
- body = adaptBlockNoteHTML(body);
-
- createExport({
- templateId: templateIdSelected,
- body,
- body_type: 'html',
- format,
- });
}
return (
@@ -138,6 +151,7 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
color="secondary"
fullWidth
onClick={() => onClose()}
+ disabled={isExporting}
>
{t('Cancel')}
@@ -146,7 +160,7 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
color="primary"
fullWidth
onClick={() => void onSubmit()}
- disabled={isPending || !templateIdSelected}
+ disabled={isExporting}
>
{t('Download')}
@@ -173,9 +187,9 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
clearable={false}
label={t('Template')}
options={templateOptions}
- value={templateIdSelected}
+ value={templateSelected}
onChange={(options) =>
- setTemplateIdSelected(options.target.value as string)
+ setTemplateSelected(options.target.value as string)
}
/>
{
}
/>
- {isPending && (
-
+ {isExporting && (
+
)}
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/utils.ts b/src/frontend/apps/impress/src/features/docs/doc-header/utils.ts
index de15b30e..d4a6165d 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/utils.ts
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/utils.ts
@@ -10,137 +10,23 @@ export function downloadFile(blob: Blob, filename: string) {
window.URL.revokeObjectURL(url);
}
-const convertToLi = (html: string) => {
- const parser = new DOMParser();
- const doc = parser.parseFromString(html, 'text/html');
- const divs = doc.querySelectorAll(
- 'div[data-content-type="bulletListItem"] , div[data-content-type="numberedListItem"]',
- );
+export const exportResolveFileUrl = async (
+ url: string,
+ resolveFileUrl: ((url: string) => Promise) | undefined,
+) => {
+ if (!url.includes(window.location.hostname) && resolveFileUrl) {
+ return resolveFileUrl(url);
+ }
- // Loop through each div and replace it with a li
- divs.forEach((div) => {
- // Create a new li element
- const li = document.createElement('li');
-
- // Copy the attributes from the div to the li
- for (let i = 0; i < div.attributes.length; i++) {
- li.setAttribute(div.attributes[i].name, div.attributes[i].value);
- }
-
- // Move all child elements of the div to the li
- while (div.firstChild) {
- li.appendChild(div.firstChild);
- }
-
- // Replace the div with the li in the DOM
- if (div.parentNode) {
- div.parentNode.replaceChild(li, div);
- }
- });
-
- /**
- * Convert the blocknote content to a simplified version to be
- * correctly parsed by our pdf and docx parser
- */
- const newContent: string[] = [];
- let currentList: HTMLUListElement | HTMLOListElement | null = null;
-
- // Iterate over all the children of the bn-block-group
- doc.body
- .querySelectorAll('.bn-block-group .bn-block-outer')
- .forEach((outerDiv) => {
- const blockContent = outerDiv.querySelector('.bn-block-content');
-
- if (blockContent) {
- const contentType = blockContent.getAttribute('data-content-type');
-
- if (contentType === 'bulletListItem') {
- // If a list is not started, start a new one
- if (!currentList) {
- currentList = document.createElement('ul');
- }
-
- currentList.appendChild(blockContent);
- } else if (contentType === 'numberedListItem') {
- // If a numbered list is not started, start a new one
- if (!currentList) {
- currentList = document.createElement('ol');
- }
-
- currentList.appendChild(blockContent);
- } else {
- /***
- * If there is a current list, add it to the new content
- * It means that the current list has ended
- */
- if (currentList) {
- newContent.push(currentList.outerHTML);
- }
-
- currentList = null;
- newContent.push(outerDiv.outerHTML);
- }
- } else {
- // In case there is no content-type, add the outerDiv as is
- newContent.push(outerDiv.outerHTML);
- }
+ try {
+ const response = await fetch(url, {
+ credentials: 'include',
});
- return newContent.join('');
-};
+ return response.blob();
+ } catch {
+ console.error(`Failed to fetch image: ${url}`);
+ }
-const convertToImg = (html: string) => {
- const parser = new DOMParser();
- const doc = parser.parseFromString(html, 'text/html');
- const divs = doc.querySelectorAll('div[data-content-type="image"]');
-
- // Loop through each div and replace it with a img
- divs.forEach((div) => {
- const img = document.createElement('img');
-
- // Copy the attributes from the div to the img
- for (let i = 0; i < div.attributes.length; i++) {
- img.setAttribute(div.attributes[i].name, div.attributes[i].value);
-
- if (div.attributes[i].name === 'data-url') {
- img.setAttribute('src', div.attributes[i].value);
- }
-
- if (div.attributes[i].name === 'data-preview-width') {
- img.setAttribute('width', div.attributes[i].value);
- }
- }
-
- // Move all child elements of the div to the img
- while (div.firstChild) {
- img.appendChild(div.firstChild);
- }
-
- // Replace the div with the img in the DOM
- if (div.parentNode) {
- div.parentNode.replaceChild(img, div);
- }
- });
-
- return doc.body.innerHTML;
-};
-
-export const adaptBlockNoteHTML = (html: string) => {
- html = html.replaceAll('
', ' ');
-
- // custom-style is used by pandoc to convert the style
- html = html.replaceAll(
- /data-text-alignment=\"([a-z]+)\"/g,
- 'custom-style="$1"',
- );
- html = html.replaceAll(/data-text-color=\"([a-z]+)\"/g, 'style="color: $1;"');
- html = html.replaceAll(
- /data-background-color=\"([a-z]+)\"/g,
- 'style="background-color: $1;"',
- );
-
- html = convertToLi(html);
- html = convertToImg(html);
-
- return html;
+ return url;
};
diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx
index 73236534..af8a3a93 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx
@@ -140,15 +140,14 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
return {
groupName: t('Search user result'),
elements: users,
- endActions:
- isEmail && users.length === 0
- ? [
- {
- content: ,
- onSelect: () => void onSelect(newUser),
- },
- ]
- : undefined,
+ endActions: isEmail
+ ? [
+ {
+ content: ,
+ onSelect: () => void onSelect(newUser),
+ },
+ ]
+ : undefined,
};
}, [searchUsersQuery.data, t, userQuery]);
diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItem.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItem.tsx
index 4f37068d..e23c51f0 100644
--- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItem.tsx
+++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItem.tsx
@@ -68,10 +68,14 @@ export const DocsGridItem = ({ doc }: DocsGridItemProps) => {
{showAccesses && (
docs.numerique.gouv.fr n'est pas en conformitΓ© avec le RGAA 4.1. Le site n'a pas encore Γ©tΓ© auditΓ©. ",
"you have reported to the website manager a lack of accessibility that prevents you from accessing content or one of the services of the portal and you have not received a satisfactory response.": "vous avez signalΓ© au responsable du site internet un dΓ©faut d'accessibilitΓ© qui vous empΓͺche d'accΓ©der Γ un contenu ou Γ un des services du portail et vous n'avez pas obtenu de rΓ©ponse satisfaisante."
}
- }
+ },
+ "nl": { "translation": {} }
}
diff --git a/src/frontend/apps/impress/src/services/Posthog.tsx b/src/frontend/apps/impress/src/services/Posthog.tsx
new file mode 100644
index 00000000..a8b83719
--- /dev/null
+++ b/src/frontend/apps/impress/src/services/Posthog.tsx
@@ -0,0 +1,46 @@
+import { Router } from 'next/router';
+import posthog from 'posthog-js';
+import { PostHogProvider as PHProvider } from 'posthog-js/react';
+import { PropsWithChildren, useEffect } from 'react';
+
+export interface PostHogConf {
+ id: string;
+ host: string;
+}
+
+interface PostHogProviderProps {
+ conf?: PostHogConf;
+}
+
+export function PostHogProvider({
+ children,
+ conf,
+}: PropsWithChildren) {
+ useEffect(() => {
+ if (!conf?.id || !conf?.host || posthog.__loaded) {
+ return;
+ }
+
+ posthog.init(conf.id, {
+ api_host: conf.host,
+ person_profiles: 'always',
+ loaded: (posthog) => {
+ if (process.env.NODE_ENV === 'development') {
+ posthog.debug();
+ }
+ },
+ capture_pageview: false,
+ capture_pageleave: true,
+ });
+
+ const handleRouteChange = () => posthog?.capture('$pageview');
+
+ Router.events.on('routeChangeComplete', handleRouteChange);
+
+ return () => {
+ Router.events.off('routeChangeComplete', handleRouteChange);
+ };
+ }, [conf?.host, conf?.id]);
+
+ return {children} ;
+}
diff --git a/src/frontend/apps/impress/src/services/index.ts b/src/frontend/apps/impress/src/services/index.ts
index 08bbf631..967ebd48 100644
--- a/src/frontend/apps/impress/src/services/index.ts
+++ b/src/frontend/apps/impress/src/services/index.ts
@@ -1 +1,2 @@
export * from './Crisp';
+export * from './Posthog';
diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock
index 14bdb369..e230c8c2 100644
--- a/src/frontend/yarn.lock
+++ b/src/frontend/yarn.lock
@@ -1136,6 +1136,27 @@
y-protocols "^1.0.6"
yjs "^13.6.15"
+"@blocknote/xl-docx-exporter@0.21.0":
+ version "0.21.0"
+ resolved "https://registry.yarnpkg.com/@blocknote/xl-docx-exporter/-/xl-docx-exporter-0.21.0.tgz#f7f50bac0e3110d8851b3969f704663542d28f59"
+ integrity sha512-uJiyfAXlONFu6ty6KbH7zrQUYxlZS2nya3fhCAWFTB3zEz6HiwiVE4HeC5jqDgMAw/i2zzluEGOQv3O5qW4KnQ==
+ dependencies:
+ "@blocknote/core" "^0.21.0"
+ buffer "^6.0.3"
+ docx "^9.0.2"
+ sharp "^0.33.5"
+
+"@blocknote/xl-pdf-exporter@0.21.0":
+ version "0.21.0"
+ resolved "https://registry.yarnpkg.com/@blocknote/xl-pdf-exporter/-/xl-pdf-exporter-0.21.0.tgz#6e05b20ba331cdd17162f7d42678bdfc54e9914f"
+ integrity sha512-OAw8nIuDDBXeMSdMkjBIYvsu6i3qhxo5OwqzcQzAV8wcHXvPnkcJiBE7RNTcawPbCdgBybx3i/oW9RT+dWcOrA==
+ dependencies:
+ "@blocknote/core" "^0.21.0"
+ "@blocknote/react" "^0.21.0"
+ "@react-pdf/renderer" "^4.0.0"
+ buffer "^6.0.3"
+ docx "^9.0.2"
+
"@cspotcode/source-map-support@^0.8.0":
version "0.8.1"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
@@ -3342,6 +3363,145 @@
"@react-types/shared" "^3.26.0"
"@swc/helpers" "^0.5.0"
+"@react-pdf/fns@3.0.0":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@react-pdf/fns/-/fns-3.0.0.tgz#2e0137d48b14c531b2f6a9214cb36ea2a7aea3ba"
+ integrity sha512-ICbIWR93PE6+xf2Xd/fXYO1dAuiOAJaszEuGGv3wp5lLSeeelDXlEYLh6R05okxh28YqMzc0Qd85x6n6MtaLUQ==
+ dependencies:
+ "@babel/runtime" "^7.20.13"
+
+"@react-pdf/font@^3.0.1":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@react-pdf/font/-/font-3.0.1.tgz#ea831f272e3b1418ffd2d9f70c835236e6564354"
+ integrity sha512-s+0xrQabGoYDDZwVpz8PXp1ylwabqiMhzfyetvxBqjDuQ15PuoSkmUkKUOkfDzauuAqs0MLMvt+Pcv+NioLfzw==
+ dependencies:
+ "@babel/runtime" "^7.20.13"
+ "@react-pdf/types" "^2.7.0"
+ fontkit "^2.0.2"
+ is-url "^1.2.4"
+
+"@react-pdf/image@^3.0.1":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@react-pdf/image/-/image-3.0.1.tgz#5c08a7ddf5d07c53ea3377348e7bb07d17efe7c2"
+ integrity sha512-Hd5F1LzjuzG4bL/ytaOYxwN/5ip8oFBYDHdpccOfYY87J/Ca7AL31SsuneLk9DtnwNM1BSAKXtBo/WDFY3r57A==
+ dependencies:
+ "@babel/runtime" "^7.20.13"
+ "@react-pdf/png-js" "^3.0.0"
+ jay-peg "^1.1.0"
+
+"@react-pdf/layout@^4.2.0":
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/@react-pdf/layout/-/layout-4.2.0.tgz#e2489992d4b6409a6cecf62b1b9391143ad30495"
+ integrity sha512-/0jMhDKwZH0lQs3umNsOduaPtkK0IUpaBRUEv4udHVD9lB2VzYoSNeYsCu+MJMPJyByXj70OSWV7IMjWTCKwWw==
+ dependencies:
+ "@babel/runtime" "^7.20.13"
+ "@react-pdf/fns" "3.0.0"
+ "@react-pdf/image" "^3.0.1"
+ "@react-pdf/pdfkit" "^4.0.0"
+ "@react-pdf/primitives" "^4.0.0"
+ "@react-pdf/stylesheet" "^5.2.0"
+ "@react-pdf/textkit" "^5.0.1"
+ "@react-pdf/types" "^2.7.0"
+ emoji-regex "^10.3.0"
+ queue "^6.0.1"
+ yoga-layout "^3.1.0"
+
+"@react-pdf/pdfkit@^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/@react-pdf/pdfkit/-/pdfkit-4.0.0.tgz#8dbbfc8e546ebd0b76e88bd525fd6bec43c19df4"
+ integrity sha512-HaaAoBpoRGJ6c1ZOANNQZ3q6Ehmagqa8n40x+OZ5s9HcmUviZ34SCm+QBa42s1o4299M+Lgw3UoqpW7sHv3/Hg==
+ dependencies:
+ "@babel/runtime" "^7.20.13"
+ "@react-pdf/png-js" "^3.0.0"
+ browserify-zlib "^0.2.0"
+ crypto-js "^4.2.0"
+ fontkit "^2.0.2"
+ jay-peg "^1.1.0"
+ vite-compatible-readable-stream "^3.6.1"
+
+"@react-pdf/png-js@^3.0.0":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@react-pdf/png-js/-/png-js-3.0.0.tgz#c0b7dc7c77e36f0830e9b7bccca7ddd64ada1c5e"
+ integrity sha512-eSJnEItZ37WPt6Qv5pncQDxLJRK15eaRwPT+gZoujP548CodenOVp49GST8XJvKMFt9YqIBzGBV/j9AgrOQzVA==
+ dependencies:
+ browserify-zlib "^0.2.0"
+
+"@react-pdf/primitives@^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/@react-pdf/primitives/-/primitives-4.0.0.tgz#0a710664923547c315386e82a643e58008612844"
+ integrity sha512-yp4E0rDL03NaUp/CnDBz3HQNfH2Mzdlgku57yhTMGNzetwB0NJusXcjYg5XsTGIXnR7Tv80JKI4O4ajj+oaLeQ==
+
+"@react-pdf/reconciler@^1.1.3":
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/@react-pdf/reconciler/-/reconciler-1.1.3.tgz#6fbd0f601fcb4f7ff1ff7c4dcc4df6afbccd9129"
+ integrity sha512-4vqY0klmUH32kTFvuqdAszkOpwfZYKMLO4VpJ5xZWTsoUOLQSyhC2QM2QCj9eaxpB2Nd5Kl9uW+KfyutvZnMzQ==
+ dependencies:
+ object-assign "^4.1.1"
+ scheduler "0.25.0-rc-603e6108-20241029"
+
+"@react-pdf/render@^4.0.2":
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/@react-pdf/render/-/render-4.0.2.tgz#66284862329a926589f62170b8c3adff55d76157"
+ integrity sha512-5QJB9sS0uU5ALTLxrtT073VT1imZhrzuOun+7kvo0nykeAr9I4lv0Shmy8rS4QhpmXn8ASmhd17WjCVm4DcJlw==
+ dependencies:
+ "@babel/runtime" "^7.20.13"
+ "@react-pdf/fns" "3.0.0"
+ "@react-pdf/primitives" "^4.0.0"
+ "@react-pdf/textkit" "^5.0.1"
+ "@react-pdf/types" "^2.7.0"
+ abs-svg-path "^0.1.1"
+ color-string "^1.9.1"
+ normalize-svg-path "^1.1.0"
+ parse-svg-path "^0.1.2"
+ svg-arc-to-cubic-bezier "^3.2.0"
+
+"@react-pdf/renderer@4.1.6", "@react-pdf/renderer@^4.0.0":
+ version "4.1.6"
+ resolved "https://registry.yarnpkg.com/@react-pdf/renderer/-/renderer-4.1.6.tgz#257d4871192edb4d1716bc6399b5a7cf73ee3db3"
+ integrity sha512-hfQ0PsuVqfoYxkYgmkj+HFkylbB1QTpXY1rnlgnzJlrlSoNXjzPrCa/ty8jcHOwYA2lNoazIAoDatBIsc8K5pw==
+ dependencies:
+ "@babel/runtime" "^7.20.13"
+ "@react-pdf/font" "^3.0.1"
+ "@react-pdf/layout" "^4.2.0"
+ "@react-pdf/pdfkit" "^4.0.0"
+ "@react-pdf/primitives" "^4.0.0"
+ "@react-pdf/reconciler" "^1.1.3"
+ "@react-pdf/render" "^4.0.2"
+ "@react-pdf/types" "^2.7.0"
+ events "^3.3.0"
+ object-assign "^4.1.1"
+ prop-types "^15.6.2"
+ queue "^6.0.1"
+
+"@react-pdf/stylesheet@^5.2.0":
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/@react-pdf/stylesheet/-/stylesheet-5.2.0.tgz#01ed0462c363842babf5e21de913bbfe3fff23c6"
+ integrity sha512-ST19VumM9iRG0z8EjDJnyQCG+NhPFtYUCAh5B8HY237MrsRGvMgzcwrpyyqcyuLwHHYy7S4iw8EY0mK9+Qa2XQ==
+ dependencies:
+ "@babel/runtime" "^7.20.13"
+ "@react-pdf/fns" "3.0.0"
+ "@react-pdf/types" "^2.7.0"
+ color-string "^1.9.1"
+ hsl-to-hex "^1.0.0"
+ media-engine "^1.0.3"
+ postcss-value-parser "^4.1.0"
+
+"@react-pdf/textkit@^5.0.1":
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/@react-pdf/textkit/-/textkit-5.0.1.tgz#284e55ff016f46c8bf364aa1d7bcef772c913662"
+ integrity sha512-4GdDiPA9l+If203hkh48slvRQmcmM3ecPLFTpXNMPrep/3retgvxUEXKMxI+xKclpw8tMzK/W6Z4hN9DgnxWMg==
+ dependencies:
+ "@babel/runtime" "^7.20.13"
+ "@react-pdf/fns" "3.0.0"
+ bidi-js "^1.0.2"
+ hyphen "^1.6.4"
+ unicode-properties "^1.4.1"
+
+"@react-pdf/types@^2.7.0":
+ version "2.7.0"
+ resolved "https://registry.yarnpkg.com/@react-pdf/types/-/types-2.7.0.tgz#56a0232ce420c313fe67cef193cbf57870a01477"
+ integrity sha512-7KrPPCpgRPKR+g+T127PE4bpw9Q84ZiY07EYRwXKVtTEVW9wJ5BZiF9smT9IvH19s+MQaDLmYRgjESsnqlyH0Q==
+
"@react-stately/calendar@3.5.4":
version "3.5.4"
resolved "https://registry.yarnpkg.com/@react-stately/calendar/-/calendar-3.5.4.tgz#847b2a2e5cf13a81b3344f1ef4e9a0d10138191e"
@@ -4378,7 +4538,7 @@
resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9"
integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==
-"@swc/helpers@0.5.15", "@swc/helpers@^0.5.0":
+"@swc/helpers@0.5.15", "@swc/helpers@^0.5.0", "@swc/helpers@^0.5.12":
version "0.5.15"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.15.tgz#79efab344c5819ecf83a43f3f9f811fc84b516d7"
integrity sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==
@@ -4918,7 +5078,7 @@
dependencies:
"@types/node" "*"
-"@types/node@*", "@types/node@22.10.3":
+"@types/node@*", "@types/node@22.10.3", "@types/node@^22.7.5":
version "22.10.3"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.3.tgz#cdc2a89bf6e5d5e593fad08e83f74d7348d5dd10"
integrity sha512-DifAyw4BkrufCILvD3ucnuN8eydUfc/C1GlyrnI+LK6543w5/L3VeVgf05o3B4fqSXP1dKYLOZsKfutpxPzZrw==
@@ -5347,6 +5507,11 @@ abab@^2.0.6:
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==
+abs-svg-path@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/abs-svg-path/-/abs-svg-path-0.1.1.tgz#df601c8e8d2ba10d4a76d625e236a9a39c2723bf"
+ integrity sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==
+
accepts@~1.3.8:
version "1.3.8"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
@@ -5775,11 +5940,18 @@ bare-events@^2.2.0:
resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.5.0.tgz#305b511e262ffd8b9d5616b056464f8e1b3329cc"
integrity sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==
-base64-js@^1.3.1:
+base64-js@^1.1.2, base64-js@^1.3.0, base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+bidi-js@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/bidi-js/-/bidi-js-1.0.3.tgz#6f8bcf3c877c4d9220ddf49b9bb6930c88f877d2"
+ integrity sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==
+ dependencies:
+ require-from-string "^2.0.2"
+
binary-extensions@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
@@ -5876,6 +6048,20 @@ broccoli-plugin@^4.0.7:
rimraf "^3.0.2"
symlink-or-copy "^1.3.1"
+brotli@^1.3.2:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/brotli/-/brotli-1.3.3.tgz#7365d8cc00f12cf765d2b2c898716bcf4b604d48"
+ integrity sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==
+ dependencies:
+ base64-js "^1.1.2"
+
+browserify-zlib@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f"
+ integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==
+ dependencies:
+ pako "~1.0.5"
+
browserslist@^4.24.0, browserslist@^4.24.2:
version "4.24.3"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.3.tgz#5fc2725ca8fb3c1432e13dac278c7cc103e026d2"
@@ -6147,7 +6333,7 @@ color-name@^1.0.0, color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
-color-string@^1.9.0:
+color-string@^1.9.0, color-string@^1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
@@ -6286,7 +6472,7 @@ core-js-compat@^3.38.0, core-js-compat@^3.38.1:
dependencies:
browserslist "^4.24.2"
-core-js@^3.0.0:
+core-js@^3.0.0, core-js@^3.38.1:
version "3.39.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.39.0.tgz#57f7647f4d2d030c32a72ea23a0555b2eaa30f83"
integrity sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==
@@ -6379,6 +6565,11 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
shebang-command "^2.0.0"
which "^2.0.1"
+crypto-js@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631"
+ integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
+
crypto-random-string@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
@@ -6654,6 +6845,11 @@ dezalgo@^1.0.4:
asap "^2.0.0"
wrappy "1"
+dfa@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/dfa/-/dfa-1.2.0.tgz#96ac3204e2d29c49ea5b57af8d92c2ae12790657"
+ integrity sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==
+
diff-sequences@^29.6.3:
version "29.6.3"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921"
@@ -6690,6 +6886,30 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
+docx@9.1.0:
+ version "9.1.0"
+ resolved "https://registry.yarnpkg.com/docx/-/docx-9.1.0.tgz#e089b86048df0fb777f0a957bc45c7a591b181f8"
+ integrity sha512-XOtseSTRrkKN/sV5jNBqyLazyhNpWfaUhpuKc22cs+5DavNjRQvchnohb0g0S+x/96/D06U/i0/U/Gc4E5kwuQ==
+ dependencies:
+ "@types/node" "^22.7.5"
+ hash.js "^1.1.7"
+ jszip "^3.10.1"
+ nanoid "^5.0.4"
+ xml "^1.0.1"
+ xml-js "^1.6.8"
+
+docx@^9.0.2:
+ version "9.1.1"
+ resolved "https://registry.yarnpkg.com/docx/-/docx-9.1.1.tgz#0c72d8000e9bb5bb380c5b48656a3277eb53322d"
+ integrity sha512-jz941pdz4+gMljZ1pV+95FwuWEouKi4u1Elhv3ptqeytGOSyX+u131hRYg4wgqLU+x2CbGsz9eTYgo2uMMz65Q==
+ dependencies:
+ "@types/node" "^22.7.5"
+ hash.js "^1.1.7"
+ jszip "^3.10.1"
+ nanoid "^5.0.4"
+ xml "^1.0.1"
+ xml-js "^1.6.8"
+
dom-accessibility-api@^0.5.9:
version "0.5.16"
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
@@ -6810,6 +7030,11 @@ emoji-regex-xs@^1.0.0:
resolved "https://registry.yarnpkg.com/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz#e8af22e5d9dbd7f7f22d280af3d19d2aab5b0724"
integrity sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==
+emoji-regex@^10.3.0:
+ version "10.4.0"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.4.0.tgz#03553afea80b3975749cfcb36f776ca268e413d4"
+ integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==
+
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
@@ -7349,7 +7574,7 @@ etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
-events@^3.2.0:
+events@^3.2.0, events@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
@@ -7536,6 +7761,11 @@ fetch-mock@9.11.0:
querystring "^0.2.0"
whatwg-url "^6.5.0"
+fflate@^0.4.8:
+ version "0.4.8"
+ resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae"
+ integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==
+
figlet@1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/figlet/-/figlet-1.7.0.tgz#46903a04603fd19c3e380358418bb2703587a72e"
@@ -7625,6 +7855,21 @@ flatted@^3.2.9, flatted@^3.3.1:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.2.tgz#adba1448a9841bec72b42c532ea23dbbedef1a27"
integrity sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==
+fontkit@^2.0.2:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/fontkit/-/fontkit-2.0.4.tgz#4765d664c68b49b5d6feb6bd1051ee49d8ec5ab0"
+ integrity sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==
+ dependencies:
+ "@swc/helpers" "^0.5.12"
+ brotli "^1.3.2"
+ clone "^2.1.2"
+ dfa "^1.2.0"
+ fast-deep-equal "^3.1.3"
+ restructure "^3.0.0"
+ tiny-inflate "^1.0.3"
+ unicode-properties "^1.4.0"
+ unicode-trie "^2.0.0"
+
for-each@^0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
@@ -8001,6 +8246,14 @@ has-tostringtag@^1.0.0, has-tostringtag@^1.0.2:
dependencies:
has-symbols "^1.0.3"
+hash.js@^1.1.7:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
+ integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
+ dependencies:
+ inherits "^2.0.3"
+ minimalistic-assert "^1.0.1"
+
hasown@^2.0.0, hasown@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
@@ -8284,6 +8537,18 @@ hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
dependencies:
react-is "^16.7.0"
+hsl-to-hex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/hsl-to-hex/-/hsl-to-hex-1.0.0.tgz#c58c826dc6d2f1e0a5ff1da5a7ecbf03faac1352"
+ integrity sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA==
+ dependencies:
+ hsl-to-rgb-for-reals "^1.1.0"
+
+hsl-to-rgb-for-reals@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/hsl-to-rgb-for-reals/-/hsl-to-rgb-for-reals-1.1.1.tgz#e1eb23f6b78016e3722431df68197e6dcdc016d9"
+ integrity sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==
+
html-encoding-sniffer@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9"
@@ -8389,6 +8654,11 @@ human-signals@^2.1.0:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
+hyphen@^1.6.4:
+ version "1.10.6"
+ resolved "https://registry.yarnpkg.com/hyphen/-/hyphen-1.10.6.tgz#0e779d280e696102b97d7e42f5ca5de2cc97e274"
+ integrity sha512-fXHXcGFTXOvZTSkPJuGOQf5Lv5T/R2itiiCVPg9LxAje5D00O0pP83yJShFq5V89Ly//Gt6acj7z8pbBr34stw==
+
i18next-browser-languagedetector@8.0.2:
version "8.0.2"
resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.2.tgz#037ca25c26877cad778f060a9e177054d9f8eaa3"
@@ -8470,6 +8740,11 @@ ignore@^6.0.2:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-6.0.2.tgz#77cccb72a55796af1b6d2f9eb14fa326d24f4283"
integrity sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==
+immediate@~3.0.5:
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
+ integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
+
import-fresh@^3.2.1, import-fresh@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
@@ -8794,6 +9069,11 @@ is-typed-array@^1.1.13, is-typed-array@^1.1.14, is-typed-array@^1.1.15:
dependencies:
which-typed-array "^1.1.16"
+is-url@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52"
+ integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==
+
is-valid-glob@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa"
@@ -8914,6 +9194,13 @@ jake@^10.8.5:
filelist "^1.0.4"
minimatch "^3.1.2"
+jay-peg@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/jay-peg/-/jay-peg-1.1.1.tgz#fdf410b89fa7a295bf74424ffe4c9083dbe7c363"
+ integrity sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww==
+ dependencies:
+ restructure "^3.0.0"
+
jest-changed-files@^29.7.0:
version "29.7.0"
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a"
@@ -9315,33 +9602,6 @@ js-yaml@^4.1.0:
dependencies:
argparse "^2.0.1"
-jsdom@25.0.1, jsdom@^25.0.1:
- version "25.0.1"
- resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-25.0.1.tgz#536ec685c288fc8a5773a65f82d8b44badcc73ef"
- integrity sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==
- dependencies:
- cssstyle "^4.1.0"
- data-urls "^5.0.0"
- decimal.js "^10.4.3"
- form-data "^4.0.0"
- html-encoding-sniffer "^4.0.0"
- http-proxy-agent "^7.0.2"
- https-proxy-agent "^7.0.5"
- is-potential-custom-element-name "^1.0.1"
- nwsapi "^2.2.12"
- parse5 "^7.1.2"
- rrweb-cssom "^0.7.1"
- saxes "^6.0.0"
- symbol-tree "^3.2.4"
- tough-cookie "^5.0.0"
- w3c-xmlserializer "^5.0.0"
- webidl-conversions "^7.0.0"
- whatwg-encoding "^3.1.1"
- whatwg-mimetype "^4.0.0"
- whatwg-url "^14.0.0"
- ws "^8.18.0"
- xml-name-validator "^5.0.0"
-
jsdom@^20.0.0:
version "20.0.3"
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.3.tgz#886a41ba1d4726f67a8858028c99489fed6ad4db"
@@ -9374,6 +9634,33 @@ jsdom@^20.0.0:
ws "^8.11.0"
xml-name-validator "^4.0.0"
+jsdom@^25.0.1:
+ version "25.0.1"
+ resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-25.0.1.tgz#536ec685c288fc8a5773a65f82d8b44badcc73ef"
+ integrity sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==
+ dependencies:
+ cssstyle "^4.1.0"
+ data-urls "^5.0.0"
+ decimal.js "^10.4.3"
+ form-data "^4.0.0"
+ html-encoding-sniffer "^4.0.0"
+ http-proxy-agent "^7.0.2"
+ https-proxy-agent "^7.0.5"
+ is-potential-custom-element-name "^1.0.1"
+ nwsapi "^2.2.12"
+ parse5 "^7.1.2"
+ rrweb-cssom "^0.7.1"
+ saxes "^6.0.0"
+ symbol-tree "^3.2.4"
+ tough-cookie "^5.0.0"
+ w3c-xmlserializer "^5.0.0"
+ webidl-conversions "^7.0.0"
+ whatwg-encoding "^3.1.1"
+ whatwg-mimetype "^4.0.0"
+ whatwg-url "^14.0.0"
+ ws "^8.18.0"
+ xml-name-validator "^5.0.0"
+
jsesc@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d"
@@ -9457,6 +9744,16 @@ jsonpointer@^5.0.0:
object.assign "^4.1.4"
object.values "^1.1.6"
+jszip@^3.10.1:
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2"
+ integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==
+ dependencies:
+ lie "~3.3.0"
+ pako "~1.0.2"
+ readable-stream "~2.3.6"
+ setimmediate "^1.0.5"
+
keyv@^4.5.3, keyv@^4.5.4:
version "4.5.4"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
@@ -9521,6 +9818,13 @@ lib0@^0.2.42, lib0@^0.2.47, lib0@^0.2.85, lib0@^0.2.87, lib0@^0.2.98:
dependencies:
isomorphic.js "^0.2.4"
+lie@~3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
+ integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==
+ dependencies:
+ immediate "~3.0.5"
+
lilconfig@^3.1.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.3.tgz#a1bcfd6257f9585bf5ae14ceeebb7b559025e4c4"
@@ -9886,6 +10190,11 @@ mdurl@^2.0.0:
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0"
integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==
+media-engine@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/media-engine/-/media-engine-1.0.3.tgz#be3188f6cd243ea2a40804a35de5a5b032f58dad"
+ integrity sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==
+
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@@ -10266,6 +10575,11 @@ min-indent@^1.0.0:
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
+minimalistic-assert@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
+ integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
+
minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
@@ -10344,6 +10658,11 @@ nanoid@^3.3.6, nanoid@^3.3.7:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
+nanoid@^5.0.4:
+ version "5.0.9"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.9.tgz#977dcbaac055430ce7b1e19cf0130cea91a20e50"
+ integrity sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==
+
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@@ -10440,6 +10759,13 @@ normalize-path@3.0.0, normalize-path@^3.0.0, normalize-path@~3.0.0:
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+normalize-svg-path@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz#0e614eca23c39f0cffe821d6be6cd17e569a766c"
+ integrity sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==
+ dependencies:
+ svg-arc-to-cubic-bezier "^3.0.0"
+
now-and-later@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-3.0.0.tgz#cdc045dc5b894b35793cf276cc3206077bb7302d"
@@ -10625,6 +10951,16 @@ p-try@^2.0.0:
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+pako@^0.2.5:
+ version "0.2.9"
+ resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
+ integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==
+
+pako@~1.0.2, pako@~1.0.5:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
+ integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
+
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
@@ -10642,6 +10978,11 @@ parse-json@^5.0.0, parse-json@^5.2.0:
json-parse-even-better-errors "^2.3.0"
lines-and-columns "^1.1.6"
+parse-svg-path@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/parse-svg-path/-/parse-svg-path-0.1.2.tgz#7a7ec0d1eb06fa5325c7d3e009b859a09b5d49eb"
+ integrity sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==
+
parse5-htmlparser2-tree-adapter@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz#b5a806548ed893a43e24ccb42fbb78069311e81b"
@@ -10840,7 +11181,7 @@ postcss-selector-parser@^7.0.0:
cssesc "^3.0.0"
util-deprecate "^1.0.2"
-postcss-value-parser@^4.0.2, postcss-value-parser@^4.2.0:
+postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
@@ -10921,6 +11262,21 @@ postgres-range@^1.1.1:
resolved "https://registry.yarnpkg.com/postgres-range/-/postgres-range-1.1.4.tgz#a59c5f9520909bcec5e63e8cf913a92e4c952863"
integrity sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==
+posthog-js@1.204.0:
+ version "1.204.0"
+ resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.204.0.tgz#73843af471fcc484ca1e8e1bcc927887cf81b4ba"
+ integrity sha512-wVt948wKPPztCZ3OeDq8y0dtaPbhbY8vFuEVBUNHOn7PohbTXr7HZ4CNhH8fXgFkx5COEzz/20wWJmEsSU5oCA==
+ dependencies:
+ core-js "^3.38.1"
+ fflate "^0.4.8"
+ preact "^10.19.3"
+ web-vitals "^4.2.0"
+
+preact@^10.19.3:
+ version "10.25.4"
+ resolved "https://registry.yarnpkg.com/preact/-/preact-10.25.4.tgz#c1d00bee9d7b9dcd06a2311d9951973b506ae8ac"
+ integrity sha512-jLdZDb+Q+odkHJ+MpW/9U5cODzqnB+fy2EiHSZES7ldV5LK7yjlVzTp7R8Xy6W6y75kfK8iWYtFVH7lvjwrCMA==
+
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@@ -11236,6 +11592,13 @@ queue-tick@^1.0.1:
resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.1.tgz#f6f07ac82c1fd60f82e098b417a80e52f1f4c142"
integrity sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==
+queue@^6.0.1:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/queue/-/queue-6.0.2.tgz#b91525283e2315c7553d2efa18d83e76432fed65"
+ integrity sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==
+ dependencies:
+ inherits "~2.0.3"
+
quick-temp@^0.1.8:
version "0.1.8"
resolved "https://registry.yarnpkg.com/quick-temp/-/quick-temp-0.1.8.tgz#bab02a242ab8fb0dd758a3c9776b32f9a5d94408"
@@ -11932,6 +12295,11 @@ resolve@^2.0.0-next.5:
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
+restructure@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/restructure/-/restructure-3.0.2.tgz#e6b2fad214f78edee21797fa8160fef50eb9b49a"
+ integrity sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==
+
reusify@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
@@ -12042,6 +12410,11 @@ safe-regex-test@^1.0.3, safe-regex-test@^1.1.0:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+sax@^1.2.4:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f"
+ integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==
+
saxes@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5"
@@ -12049,6 +12422,11 @@ saxes@^6.0.0:
dependencies:
xmlchars "^2.2.0"
+scheduler@0.25.0-rc-603e6108-20241029:
+ version "0.25.0-rc-603e6108-20241029"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0-rc-603e6108-20241029.tgz#684dd96647e104d23e0d29a37f18937daf82df19"
+ integrity sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==
+
scheduler@^0.23.2:
version "0.23.2"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3"
@@ -12143,6 +12521,11 @@ set-function-name@^2.0.2:
functions-have-names "^1.2.3"
has-property-descriptors "^1.0.2"
+setimmediate@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+ integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==
+
setprototypeof@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
@@ -12733,6 +13116,11 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+svg-arc-to-cubic-bezier@^3.0.0, svg-arc-to-cubic-bezier@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz#390c450035ae1c4a0104d90650304c3bc814abe6"
+ integrity sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==
+
svg-parser@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5"
@@ -12867,6 +13255,11 @@ through2@^2.0.1:
readable-stream "~2.3.6"
xtend "~4.0.1"
+tiny-inflate@^1.0.0, tiny-inflate@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4"
+ integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==
+
tippy.js@^6.3.7:
version "6.3.7"
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c"
@@ -13195,11 +13588,27 @@ unicode-match-property-value-ecmascript@^2.1.0:
resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz#a0401aee72714598f739b68b104e4fe3a0cb3c71"
integrity sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==
+unicode-properties@^1.4.0, unicode-properties@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/unicode-properties/-/unicode-properties-1.4.1.tgz#96a9cffb7e619a0dc7368c28da27e05fc8f9be5f"
+ integrity sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==
+ dependencies:
+ base64-js "^1.3.0"
+ unicode-trie "^2.0.0"
+
unicode-property-aliases-ecmascript@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd"
integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==
+unicode-trie@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/unicode-trie/-/unicode-trie-2.0.0.tgz#8fd8845696e2e14a8b67d78fa9e0dd2cad62fec8"
+ integrity sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==
+ dependencies:
+ pako "^0.2.5"
+ tiny-inflate "^1.0.0"
+
unified@^10.0.0, unified@^10.1.2:
version "10.1.2"
resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.2.tgz#b1d64e55dafe1f0b98bb6c719881103ecf6c86df"
@@ -13561,6 +13970,15 @@ vinyl@^3.0.0:
replace-ext "^2.0.0"
teex "^1.0.1"
+vite-compatible-readable-stream@^3.6.1:
+ version "3.6.1"
+ resolved "https://registry.yarnpkg.com/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz#27267aebbdc9893c0ddf65a421279cbb1e31d8cd"
+ integrity sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==
+ dependencies:
+ inherits "^2.0.3"
+ string_decoder "^1.1.1"
+ util-deprecate "^1.0.1"
+
void-elements@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
@@ -13622,6 +14040,11 @@ web-namespaces@^2.0.0:
resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692"
integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==
+web-vitals@^4.2.0:
+ version "4.2.4"
+ resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-4.2.4.tgz#1d20bc8590a37769bd0902b289550936069184b7"
+ integrity sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==
+
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
@@ -14030,6 +14453,13 @@ ws@^7.4.6:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
+xml-js@^1.6.8:
+ version "1.6.11"
+ resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9"
+ integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==
+ dependencies:
+ sax "^1.2.4"
+
xml-name-validator@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"
@@ -14040,6 +14470,11 @@ xml-name-validator@^5.0.0:
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673"
integrity sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==
+xml@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5"
+ integrity sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==
+
xmlchars@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
@@ -14114,6 +14549,11 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
+yoga-layout@^3.1.0:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/yoga-layout/-/yoga-layout-3.2.1.tgz#d2d1ba06f0e81c2eb650c3e5ad8b0b4adde1e843"
+ integrity sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==
+
zustand@5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.2.tgz#f7595ada55a565f1fd6464f002a91e701ee0cfca"
diff --git a/src/helm/env.d/dev/values.impress.yaml.gotmpl b/src/helm/env.d/dev/values.impress.yaml.gotmpl
index 016f8f0e..df9f9fce 100644
--- a/src/helm/env.d/dev/values.impress.yaml.gotmpl
+++ b/src/helm/env.d/dev/values.impress.yaml.gotmpl
@@ -148,6 +148,13 @@ ingressAdmin:
enabled: true
host: impress.127.0.0.1.nip.io
+posthog:
+ ingress:
+ enabled: false
+
+ ingressAssets:
+ enabled: false
+
ingressMedia:
enabled: true
host: impress.127.0.0.1.nip.io
diff --git a/src/helm/impress/Chart.yaml b/src/helm/impress/Chart.yaml
index da70e774..80115ecb 100644
--- a/src/helm/impress/Chart.yaml
+++ b/src/helm/impress/Chart.yaml
@@ -1,5 +1,5 @@
apiVersion: v2
type: application
name: docs
-version: 0.0.2
+version: 2.0.1-beta.8
appVersion: latest
diff --git a/src/helm/impress/README.md b/src/helm/impress/README.md
index 19b86429..999dc479 100644
--- a/src/helm/impress/README.md
+++ b/src/helm/impress/README.md
@@ -104,6 +104,11 @@
| `backend.migrate.restartPolicy` | backend migrate job restart policy | `Never` |
| `backend.createsuperuser.command` | backend migrate command | `["/bin/sh","-c","python manage.py createsuperuser --email $DJANGO_SUPERUSER_EMAIL --password $DJANGO_SUPERUSER_PASSWORD\n"]` |
| `backend.createsuperuser.restartPolicy` | backend migrate job restart policy | `Never` |
+| `backend.job` | job dedicated to run a random management command, for example after a deployment | |
+| `backend.job.name` | The name to use to describe this job | `""` |
+| `backend.job.command` | The management command to execute | `[]` |
+| `backend.job.restartPolicy` | The restart policy for the job. | `Never` |
+| `backend.job.annotations` | Annotations to add to the job [default: argocd.argoproj.io/hook: PostSync] | |
| `backend.probes.liveness.path` | Configure path for backend HTTP liveness probe | `/__heartbeat__` |
| `backend.probes.liveness.targetPort` | Configure port for backend HTTP liveness probe | `undefined` |
| `backend.probes.liveness.initialDelaySeconds` | Configure initial delay for backend liveness probe | `10` |
@@ -176,6 +181,37 @@
| `frontend.extraVolumeMounts` | Additional volumes to mount on the frontend. | `[]` |
| `frontend.extraVolumes` | Additional volumes to mount on the frontend. | `[]` |
+### posthog
+
+| Name | Description | Value |
+| -------------------------------------- | ----------------------------------------------------------- | ------------------------- |
+| `posthog.ingress.enabled` | Enable or disable the ingress resource creation | `false` |
+| `posthog.ingress.className` | Kubernetes ingress class name to use (e.g., nginx, traefik) | `nil` |
+| `posthog.ingress.host` | Primary hostname for the ingress resource | `impress.example.com` |
+| `posthog.ingress.path` | URL path prefix for the ingress routes (e.g., /) | `/` |
+| `posthog.ingress.hosts` | Additional hostnames array to be included in the ingress | `[]` |
+| `posthog.ingress.tls.enabled` | Enable or disable TLS/HTTPS for the ingress | `true` |
+| `posthog.ingress.tls.additional` | Additional TLS configurations for extra hosts/certificates | `[]` |
+| `posthog.ingress.customBackends` | Custom backend service configurations for the ingress | `[]` |
+| `posthog.ingress.annotations` | Additional Kubernetes annotations to apply to the ingress | `{}` |
+| `posthog.ingressAssets.enabled` | Enable or disable the ingress resource creation | `false` |
+| `posthog.ingressAssets.className` | Kubernetes ingress class name to use (e.g., nginx, traefik) | `nil` |
+| `posthog.ingressAssets.host` | Primary hostname for the ingress resource | `impress.example.com` |
+| `posthog.ingressAssets.paths` | URL paths prefix for the ingress routes (e.g., /static) | `["/static","/array"]` |
+| `posthog.ingressAssets.hosts` | Additional hostnames array to be included in the ingress | `[]` |
+| `posthog.ingressAssets.tls.enabled` | Enable or disable TLS/HTTPS for the ingress | `true` |
+| `posthog.ingressAssets.tls.additional` | Additional TLS configurations for extra hosts/certificates | `[]` |
+| `posthog.ingressAssets.customBackends` | Custom backend service configurations for the ingress | `[]` |
+| `posthog.ingressAssets.annotations` | Additional Kubernetes annotations to apply to the ingress | `{}` |
+| `posthog.service.type` | Service type (e.g. ExternalName, ClusterIP, LoadBalancer) | `ExternalName` |
+| `posthog.service.externalName` | External service hostname when type is ExternalName | `eu.i.posthog.com` |
+| `posthog.service.port` | Port number for the service | `443` |
+| `posthog.service.annotations` | Additional annotations to apply to the service | `{}` |
+| `posthog.assetsService.type` | Service type (e.g. ExternalName, ClusterIP, LoadBalancer) | `ExternalName` |
+| `posthog.assetsService.externalName` | External service hostname when type is ExternalName | `eu-assets.i.posthog.com` |
+| `posthog.assetsService.port` | Port number for the service | `443` |
+| `posthog.assetsService.annotations` | Additional annotations to apply to the service | `{}` |
+
### yProvider
| Name | Description | Value |
diff --git a/src/helm/impress/templates/_helpers.tpl b/src/helm/impress/templates/_helpers.tpl
index 63188f3b..b56b9892 100644
--- a/src/helm/impress/templates/_helpers.tpl
+++ b/src/helm/impress/templates/_helpers.tpl
@@ -148,6 +148,15 @@ Requires top level scope
{{ include "impress.fullname" . }}-frontend
{{- end }}
+{{/*
+Full name for the Posthog
+
+Requires top level scope
+*/}}
+{{- define "impress.posthog.fullname" -}}
+{{ include "impress.fullname" . }}-posthog
+{{- end }}
+
{{/*
Full name for the yProvider
diff --git a/src/helm/impress/templates/backend_job.yml b/src/helm/impress/templates/backend_job.yml
new file mode 100644
index 00000000..4888a904
--- /dev/null
+++ b/src/helm/impress/templates/backend_job.yml
@@ -0,0 +1,124 @@
+{{- if .Values.backend.job.command -}}
+{{- $envVars := include "impress.common.env" (list . .Values.backend) -}}
+{{- $fullName := include "impress.backend.fullname" . -}}
+{{- $component := "backend" -}}
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: {{ $fullName }}-{{ .Values.backend.job.name | default "random" | replace "_" "-" }}
+ namespace: {{ .Release.Namespace | quote }}
+ annotations:
+ argocd.argoproj.io/sync-options: Replace=true,Force=true
+ {{- with .Values.backend.job.annotations }}
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+ labels:
+ {{- include "impress.common.labels" (list . $component) | nindent 4 }}
+spec:
+ template:
+ metadata:
+ annotations:
+ {{- with .Values.backend.podAnnotations }}
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ labels:
+ {{- include "impress.common.selectorLabels" (list . $component) | nindent 8 }}
+ spec:
+ {{- if $.Values.image.credentials }}
+ imagePullSecrets:
+ - name: {{ include "impress.secret.dockerconfigjson.name" (dict "fullname" (include "impress.fullname" .) "imageCredentials" $.Values.image.credentials) }}
+ {{- end}}
+ shareProcessNamespace: {{ .Values.backend.shareProcessNamespace }}
+ containers:
+ {{- with .Values.backend.sidecars }}
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ - name: {{ .Chart.Name }}
+ image: "{{ (.Values.backend.image | default dict).repository | default .Values.image.repository }}:{{ (.Values.backend.image | default dict).tag | default .Values.image.tag }}"
+ imagePullPolicy: {{ (.Values.backend.image | default dict).pullPolicy | default .Values.image.pullPolicy }}
+ {{- with .Values.backend.job.command }}
+ command:
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ {{- with .Values.backend.args }}
+ args:
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ env:
+ {{- if $envVars}}
+ {{- $envVars | indent 12 }}
+ {{- end }}
+ {{- with .Values.backend.securityContext }}
+ securityContext:
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ {{- with .Values.backend.resources }}
+ resources:
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ volumeMounts:
+ {{- range $index, $value := .Values.mountFiles }}
+ - name: "files-{{ $index }}"
+ mountPath: {{ $value.path }}
+ subPath: content
+ {{- end }}
+ {{- range $name, $volume := .Values.backend.persistence }}
+ - name: "{{ $name }}"
+ mountPath: "{{ $volume.mountPath }}"
+ {{- end }}
+ {{- range .Values.backend.extraVolumeMounts }}
+ - name: {{ .name }}
+ mountPath: {{ .mountPath }}
+ subPath: {{ .subPath | default "" }}
+ readOnly: {{ .readOnly }}
+ {{- end }}
+ {{- with .Values.backend.nodeSelector }}
+ nodeSelector:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.backend.affinity }}
+ affinity:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.backend.tolerations }}
+ tolerations:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ restartPolicy: {{ .Values.backend.job.restartPolicy }}
+ volumes:
+ {{- range $index, $value := .Values.mountFiles }}
+ - name: "files-{{ $index }}"
+ configMap:
+ name: "{{ include "impress.fullname" $ }}-files-{{ $index }}"
+ {{- end }}
+ {{- range $name, $volume := .Values.backend.persistence }}
+ - name: "{{ $name }}"
+ {{- if eq $volume.type "emptyDir" }}
+ emptyDir: {}
+ {{- else }}
+ persistentVolumeClaim:
+ claimName: "{{ $fullName }}-{{ $name }}"
+ {{- end }}
+ {{- end }}
+ {{- range .Values.backend.extraVolumes }}
+ - name: {{ .name }}
+ {{- if .existingClaim }}
+ persistentVolumeClaim:
+ claimName: {{ .existingClaim }}
+ {{- else if .hostPath }}
+ hostPath:
+ {{ toYaml .hostPath | nindent 12 }}
+ {{- else if .csi }}
+ csi:
+ {{- toYaml .csi | nindent 12 }}
+ {{- else if .configMap }}
+ configMap:
+ {{- toYaml .configMap | nindent 12 }}
+ {{- else if .emptyDir }}
+ emptyDir:
+ {{- toYaml .emptyDir | nindent 12 }}
+ {{- else }}
+ emptyDir: {}
+ {{- end }}
+ {{- end }}
+{{- end }}
diff --git a/src/helm/impress/templates/backend_job.yaml b/src/helm/impress/templates/backend_job_migrate.yaml
similarity index 100%
rename from src/helm/impress/templates/backend_job.yaml
rename to src/helm/impress/templates/backend_job_migrate.yaml
diff --git a/src/helm/impress/templates/ingress_posthog.yaml b/src/helm/impress/templates/ingress_posthog.yaml
new file mode 100644
index 00000000..2c93725e
--- /dev/null
+++ b/src/helm/impress/templates/ingress_posthog.yaml
@@ -0,0 +1,86 @@
+{{- if .Values.posthog.ingress.enabled -}}
+{{- $fullName := include "impress.fullname" . -}}
+{{- if and .Values.posthog.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
+ {{- if not (hasKey .Values.posthog.ingress.annotations "kubernetes.io/ingress.class") }}
+ {{- $_ := set .Values.posthog.ingress.annotations "kubernetes.io/ingress.class" .Values.posthog.ingress.className}}
+ {{- end }}
+{{- end }}
+{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
+apiVersion: networking.k8s.io/v1
+{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
+apiVersion: networking.k8s.io/v1beta1
+{{- else -}}
+apiVersion: extensions/v1beta1
+{{- end }}
+kind: Ingress
+metadata:
+ name: {{ $fullName }}-posthog
+ namespace: {{ .Release.Namespace | quote }}
+ labels:
+ {{- include "impress.labels" . | nindent 4 }}
+ {{- with .Values.posthog.ingress.annotations }}
+ annotations:
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+spec:
+ {{- if and .Values.posthog.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
+ ingressClassName: {{ .Values.posthog.ingress.className }}
+ {{- end }}
+ {{- if .Values.posthog.ingress.tls.enabled }}
+ tls:
+ {{- if .Values.posthog.ingress.host }}
+ - secretName: {{ $fullName }}-posthog-tls
+ hosts:
+ - {{ .Values.posthog.ingress.host | quote }}
+ {{- end }}
+ {{- range .Values.posthog.ingress.tls.additional }}
+ - hosts:
+ {{- range .hosts }}
+ - {{ . | quote }}
+ {{- end }}
+ secretName: {{ .secretName }}
+ {{- end }}
+ {{- end }}
+ rules:
+ {{- if .Values.posthog.ingress.host }}
+ - host: {{ .Values.posthog.ingress.host | quote }}
+ http:
+ paths:
+ - path: {{ .Values.posthog.ingress.path }}
+ {{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
+ pathType: Prefix
+ {{- end }}
+ backend:
+ {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
+ service:
+ name: {{ include "impress.posthog.fullname" . }}-proxy
+ port:
+ number: {{ .Values.posthog.service.port }}
+ {{- else }}
+ serviceName: {{ include "impress.posthog.fullname" . }}-proxy
+ servicePort: {{ .Values.posthog.service.port }}
+ {{- end }}
+ {{- end }}
+ {{- range .Values.posthog.ingress.hosts }}
+ - host: {{ . | quote }}
+ http:
+ paths:
+ - path: {{ $.Values.posthog.ingress.path | quote }}
+ {{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
+ pathType: Prefix
+ {{- end }}
+ backend:
+ {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
+ service:
+ name: {{ include "impress.posthog.fullname" . }}-proxy
+ port:
+ number: {{ $.Values.posthog.service.port }}
+ {{- else }}
+ serviceName: {{ include "impress.posthog.fullname" . }}-proxy
+ servicePort: {{ $.Values.posthog.service.port }}
+ {{- end }}
+ {{- with $.Values.posthog.service.customBackends }}
+ {{- toYaml . | nindent 10 }}
+ {{- end }}
+ {{- end }}
+{{- end }}
diff --git a/src/helm/impress/templates/ingress_posthog_assets.yaml b/src/helm/impress/templates/ingress_posthog_assets.yaml
new file mode 100644
index 00000000..4a49bfa5
--- /dev/null
+++ b/src/helm/impress/templates/ingress_posthog_assets.yaml
@@ -0,0 +1,66 @@
+{{- if .Values.posthog.ingressAssets.enabled -}}
+{{- $fullName := include "impress.fullname" . -}}
+{{- if and .Values.posthog.ingressAssets.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
+ {{- if not (hasKey .Values.posthog.ingressAssets.annotations "kubernetes.io/ingress.class") }}
+ {{- $_ := set .Values.posthog.ingressAssets.annotations "kubernetes.io/ingress.class" .Values.posthog.ingressAssets.className}}
+ {{- end }}
+{{- end }}
+{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
+apiVersion: networking.k8s.io/v1
+{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
+apiVersion: networking.k8s.io/v1beta1
+{{- else -}}
+apiVersion: extensions/v1beta1
+{{- end }}
+kind: Ingress
+metadata:
+ name: {{ $fullName }}-posthog-assets
+ namespace: {{ .Release.Namespace | quote }}
+ labels:
+ {{- include "impress.labels" . | nindent 4 }}
+ {{- with .Values.posthog.ingressAssets.annotations }}
+ annotations:
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+spec:
+ {{- if and .Values.posthog.ingressAssets.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
+ ingressClassName: {{ .Values.posthog.ingressAssets.className }}
+ {{- end }}
+ {{- if .Values.posthog.ingressAssets.tls.enabled }}
+ tls:
+ {{- if .Values.posthog.ingressAssets.host }}
+ - secretName: {{ $fullName }}-posthog-tls
+ hosts:
+ - {{ .Values.posthog.ingressAssets.host | quote }}
+ {{- end }}
+ {{- range .Values.posthog.ingressAssets.tls.additional }}
+ - hosts:
+ {{- range .hosts }}
+ - {{ . | quote }}
+ {{- end }}
+ secretName: {{ .secretName }}
+ {{- end }}
+ {{- end }}
+ rules:
+ {{- if .Values.posthog.ingressAssets.host }}
+ - host: {{ .Values.posthog.ingressAssets.host | quote }}
+ http:
+ paths:
+ {{- range .Values.posthog.ingressAssets.paths }}
+ - path: {{ . | quote }}
+ {{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
+ pathType: Prefix
+ {{- end }}
+ backend:
+ {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
+ service:
+ name: {{ include "impress.posthog.fullname" $ }}-assets-proxy
+ port:
+ number: {{ $.Values.posthog.assetsService.port }}
+ {{- else }}
+ serviceName: {{ include "impress.posthog.fullname" $ }}-assets-proxy
+ servicePort: {{ $.Values.posthog.assetsService.port }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
+{{- end }}
diff --git a/src/helm/impress/templates/posthog_assets_svc.yaml b/src/helm/impress/templates/posthog_assets_svc.yaml
new file mode 100644
index 00000000..d80ea3b3
--- /dev/null
+++ b/src/helm/impress/templates/posthog_assets_svc.yaml
@@ -0,0 +1,24 @@
+{{- if .Values.posthog.ingressAssets.enabled -}}
+{{- $envVars := include "impress.common.env" (list . .Values.posthog) -}}
+{{- $fullName := include "impress.posthog.fullname" . -}}
+{{- $component := "posthog" -}}
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ $fullName }}-assets-proxy
+ namespace: {{ .Release.Namespace | quote }}
+ labels:
+ {{- include "impress.common.labels" (list . $component) | nindent 4 }}
+ annotations:
+ {{- toYaml $.Values.posthog.assetsService.annotations | nindent 4 }}
+spec:
+ type: {{ .Values.posthog.assetsService.type }}
+ externalName: {{ .Values.posthog.assetsService.externalName }}
+ ports:
+ - port: {{ .Values.posthog.assetsService.port }}
+ targetPort: {{ .Values.posthog.assetsService.targetPort }}
+ protocol: TCP
+ name: https
+ selector:
+ {{- include "impress.common.selectorLabels" (list . $component) | nindent 4 }}
+{{- end }}
diff --git a/src/helm/impress/templates/posthog_svc.yaml b/src/helm/impress/templates/posthog_svc.yaml
new file mode 100644
index 00000000..1fae600b
--- /dev/null
+++ b/src/helm/impress/templates/posthog_svc.yaml
@@ -0,0 +1,24 @@
+{{- if .Values.posthog.ingress.enabled -}}
+{{- $envVars := include "impress.common.env" (list . .Values.posthog) -}}
+{{- $fullName := include "impress.posthog.fullname" . -}}
+{{- $component := "posthog" -}}
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ $fullName }}-proxy
+ namespace: {{ .Release.Namespace | quote }}
+ labels:
+ {{- include "impress.common.labels" (list . $component) | nindent 4 }}
+ annotations:
+ {{- toYaml $.Values.posthog.service.annotations | nindent 4 }}
+spec:
+ type: {{ .Values.posthog.service.type }}
+ externalName: {{ .Values.posthog.service.externalName }}
+ ports:
+ - port: {{ .Values.posthog.service.port }}
+ targetPort: {{ .Values.posthog.service.targetPort }}
+ protocol: TCP
+ name: https
+ selector:
+ {{- include "impress.common.selectorLabels" (list . $component) | nindent 4 }}
+{{- end }}
diff --git a/src/helm/impress/values.yaml b/src/helm/impress/values.yaml
index fdaffd7a..d429937f 100644
--- a/src/helm/impress/values.yaml
+++ b/src/helm/impress/values.yaml
@@ -251,6 +251,19 @@ backend:
python manage.py createsuperuser --email $DJANGO_SUPERUSER_EMAIL --password $DJANGO_SUPERUSER_PASSWORD
restartPolicy: Never
+ ## @extra backend.job job dedicated to run a random management command, for example after a deployment
+ ## @param backend.job.name The name to use to describe this job
+ ## @param backend.job.command The management command to execute
+ ## @param backend.job.restartPolicy The restart policy for the job.
+ ## @extra backend.job.annotations Annotations to add to the job [default: argocd.argoproj.io/hook: PostSync]
+ ## @skip backend.job.annotations.argocd.argoproj.io/hook
+ job:
+ name: ""
+ command: []
+ restartPolicy: Never
+ annotations:
+ argocd.argoproj.io/hook: PostSync
+
## @param backend.probes.liveness.path [nullable] Configure path for backend HTTP liveness probe
## @param backend.probes.liveness.targetPort [nullable] Configure port for backend HTTP liveness probe
## @param backend.probes.liveness.initialDelaySeconds [nullable] Configure initial delay for backend liveness probe
@@ -390,6 +403,77 @@ frontend:
## @param frontend.extraVolumes Additional volumes to mount on the frontend.
extraVolumes: []
+## @section posthog
+
+posthog:
+
+ ## @param posthog.ingress.enabled Enable or disable the ingress resource creation
+ ## @param posthog.ingress.className Kubernetes ingress class name to use (e.g., nginx, traefik)
+ ## @param posthog.ingress.host Primary hostname for the ingress resource
+ ## @param posthog.ingress.path URL path prefix for the ingress routes (e.g., /)
+ ## @param posthog.ingress.hosts Additional hostnames array to be included in the ingress
+ ## @param posthog.ingress.tls.enabled Enable or disable TLS/HTTPS for the ingress
+ ## @param posthog.ingress.tls.additional Additional TLS configurations for extra hosts/certificates
+ ## @param posthog.ingress.customBackends Custom backend service configurations for the ingress
+ ## @param posthog.ingress.annotations Additional Kubernetes annotations to apply to the ingress
+ ingress:
+ enabled: false
+ className: null
+ host: impress.example.com
+ path: /
+ hosts: [ ]
+ tls:
+ enabled: true
+ additional: [ ]
+
+ customBackends: [ ]
+ annotations: {}
+
+ ## @param posthog.ingressAssets.enabled Enable or disable the ingress resource creation
+ ## @param posthog.ingressAssets.className Kubernetes ingress class name to use (e.g., nginx, traefik)
+ ## @param posthog.ingressAssets.host Primary hostname for the ingress resource
+ ## @param posthog.ingressAssets.paths URL paths prefix for the ingress routes (e.g., /static)
+ ## @param posthog.ingressAssets.hosts Additional hostnames array to be included in the ingress
+ ## @param posthog.ingressAssets.tls.enabled Enable or disable TLS/HTTPS for the ingress
+ ## @param posthog.ingressAssets.tls.additional Additional TLS configurations for extra hosts/certificates
+ ## @param posthog.ingressAssets.customBackends Custom backend service configurations for the ingress
+ ## @param posthog.ingressAssets.annotations Additional Kubernetes annotations to apply to the ingress
+ ingressAssets:
+ enabled: false
+ className: null
+ host: impress.example.com
+ paths:
+ - /static
+ - /array
+ hosts: [ ]
+ tls:
+ enabled: true
+ additional: [ ]
+
+ customBackends: [ ]
+ annotations: {}
+
+ ## @param posthog.service.type Service type (e.g. ExternalName, ClusterIP, LoadBalancer)
+ ## @param posthog.service.externalName External service hostname when type is ExternalName
+ ## @param posthog.service.port Port number for the service
+ ## @param posthog.service.annotations Additional annotations to apply to the service
+ service:
+ type: ExternalName
+ externalName: eu.i.posthog.com
+ port: 443
+ annotations: {}
+
+ ## @param posthog.assetsService.type Service type (e.g. ExternalName, ClusterIP, LoadBalancer)
+ ## @param posthog.assetsService.externalName External service hostname when type is ExternalName
+ ## @param posthog.assetsService.port Port number for the service
+ ## @param posthog.assetsService.annotations Additional annotations to apply to the service
+ assetsService:
+ type: ExternalName
+ externalName: eu-assets.i.posthog.com
+ port: 443
+ annotations: {}
+
+
## @section yProvider
yProvider: