Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b33164574a | |||
| 98e568d63c | |||
| 97e1f7f53f | |||
| 6022809888 | |||
| d241de6af1 | |||
| ad494f5de5 | |||
| fba879e739 | |||
| cac5595a91 | |||
| 78e5c72310 | |||
| 2ab31189f4 | |||
| bb4a863f8d | |||
| 0241f67787 | |||
| 908bbb828a | |||
| c7f5dabbad | |||
| a642c6d9a2 | |||
| a6dc12d91c | |||
| 307987d94d | |||
| d7ebdbf401 | |||
| dad396273c | |||
| 555daedeba | |||
| 0d09d1df08 | |||
| a40af726b6 | |||
| f8a37e55b1 | |||
| 3baec0a863 | |||
| 5b6ed6bbf0 |
@@ -34,6 +34,32 @@ jobs:
|
||||
if: always()
|
||||
run: ~/.local/bin/gitlint --commits origin/${{ github.event.pull_request.base.ref }}..HEAD
|
||||
|
||||
check-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
contains(github.event.pull_request.labels.*.name, 'noChangeLog') == false &&
|
||||
github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Check that the CHANGELOG has been modified in the current branch
|
||||
run: git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.after }} | grep 'CHANGELOG.md'
|
||||
|
||||
lint-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Check CHANGELOG max line length
|
||||
run: |
|
||||
max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
|
||||
if [ $max_line_length -ge 80 ]; then
|
||||
echo "ERROR: CHANGELOG has lines longer than 80 characters."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
build-mails:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
@@ -183,6 +209,10 @@ jobs:
|
||||
AWS_S3_ENDPOINT_URL: http://localhost:9000
|
||||
AWS_S3_ACCESS_KEY_ID: meet
|
||||
AWS_S3_SECRET_ACCESS_KEY: password
|
||||
OIDC_RS_CLIENT_ID: meet
|
||||
OIDC_RS_CLIENT_SECRET: ThisIsAnExampleKeyForDevPurposeOnly
|
||||
OIDC_OP_INTROSPECTION_ENDPOINT: https://oidc.example.com/introspect
|
||||
OIDC_OP_URL: https://oidc.example.com
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
||||
+1
-1
@@ -9,4 +9,4 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- 🔧(backend) support `_FILE` for secret environment variables #566
|
||||
-
|
||||
|
||||
+10
@@ -90,6 +90,9 @@ services:
|
||||
- createwebhook
|
||||
extra_hosts:
|
||||
- "127.0.0.1.nip.io:host-gateway"
|
||||
networks:
|
||||
- resource-server
|
||||
- default
|
||||
|
||||
celery-dev:
|
||||
user: ${DOCKER_USER:-1000}
|
||||
@@ -145,6 +148,9 @@ services:
|
||||
- ./docker/files/etc/nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
depends_on:
|
||||
- keycloak
|
||||
networks:
|
||||
- resource-server
|
||||
- default
|
||||
|
||||
frontend:
|
||||
user: "${DOCKER_USER:-1000}"
|
||||
@@ -298,3 +304,7 @@ services:
|
||||
watch:
|
||||
- action: rebuild
|
||||
path: ./src/summary
|
||||
|
||||
networks:
|
||||
default:
|
||||
resource-server:
|
||||
|
||||
@@ -42,7 +42,12 @@ COPY ./docker/dinum-frontend/fonts/ \
|
||||
FROM nginxinc/nginx-unprivileged:alpine3.21 AS frontend-production
|
||||
|
||||
USER root
|
||||
RUN apk update && apk upgrade libssl3 libcrypto3 libxml2>=2.12.7-r2 libxslt>=1.1.39-r2 libexpat>=2.7.2-r0
|
||||
RUN apk update && apk upgrade libssl3 \
|
||||
libcrypto3 \
|
||||
libxml2>=2.12.7-r2 \
|
||||
libxslt>=1.1.39-r2 \
|
||||
libexpat>=2.7.2-r0 \
|
||||
libpng>=1.6.53-r0
|
||||
|
||||
USER nginx
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
version: '3'
|
||||
|
||||
# You can add any necessary service here that will join the same docker network
|
||||
# sharing keycloak. Services added to the 'meet_resource-server' network will be
|
||||
# able to communicate with keycloak and the backend on that network.
|
||||
services:
|
||||
# busybox service is only used for testing purposes. It provides curl to test
|
||||
# connectivity to the backend and keycloak services. Replace this with your
|
||||
# relevant application services that need to communicate with keycloak.
|
||||
busybox:
|
||||
image: alpine:latest
|
||||
privileged: true
|
||||
command: sh -c "apk add --no-cache curl && sleep infinity"
|
||||
stdin_open: true
|
||||
tty: true
|
||||
networks:
|
||||
- default
|
||||
- meet_resource-server
|
||||
|
||||
networks:
|
||||
default: {}
|
||||
meet_resource-server:
|
||||
external: true
|
||||
@@ -0,0 +1,393 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Meet External API
|
||||
version: 1.0.0
|
||||
description: |
|
||||
External API for room management with resource server authentication.
|
||||
[[description by Oauth 2.0]](https://www.oauth.com/oauth2-servers/the-resource-server/)
|
||||
|
||||
#### Authentication Flow
|
||||
|
||||
1. Authenticate with the authorization server using your credentials
|
||||
2. During authentication, request the scopes you need: `lasuite_visio` (mandatory) plus action-specific scopes
|
||||
3. Receive an access token and a refresh token that includes the requested scopes
|
||||
4. Use the access token in the `Authorization: Bearer <token>` header for all API requests
|
||||
5. When the access token expires, use the refresh token to obtain a new access token without re-authenticating
|
||||
|
||||
#### Scopes
|
||||
|
||||
* `lasuite_visio` - **Mandatory** Base scope required for any API access
|
||||
* `lasuite_visio:rooms:list` – List rooms accessible to the delegated user.
|
||||
* `lasuite_visio:rooms:retrieve` – Retrieve details of a specific room.
|
||||
* `lasuite_visio:rooms:create` – Create new rooms.
|
||||
* `lasuite_visio:rooms:update` – **Coming soon** Update existing rooms, e.g., add attendees to a room.
|
||||
* `lasuite_visio:rooms:delete` – **Coming soon** Delete rooms generated by the application.
|
||||
|
||||
#### Upcoming Features
|
||||
|
||||
* **Add attendees to a room:** You will be able to update a room to include a list of attendees, allowing them to bypass the lobby system automatically.
|
||||
* **Delete application-generated rooms:** Rooms created via the application can be deleted when no longer needed.
|
||||
|
||||
contact:
|
||||
name: API Support
|
||||
email: antoine.lebaud@mail.numerique.gouv.fr
|
||||
|
||||
servers:
|
||||
- url: https://visio-sandbox.beta.numerique.gouv.fr/external-api/v1.0
|
||||
description: Sandbox server
|
||||
|
||||
tags:
|
||||
- name: Rooms
|
||||
description: Room management operations
|
||||
|
||||
paths:
|
||||
/rooms:
|
||||
get:
|
||||
tags:
|
||||
- Rooms
|
||||
summary: List rooms
|
||||
description: |
|
||||
Returns a list of rooms accessible to the authenticated user.
|
||||
Only rooms where the delegated user has access will be returned.
|
||||
operationId: listRooms
|
||||
security:
|
||||
- BearerAuth: [rooms:list]
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
description: Page number for pagination
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
default: 1
|
||||
- name: page_size
|
||||
in: query
|
||||
description: Number of items per page
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 20
|
||||
responses:
|
||||
'200':
|
||||
description: List of accessible rooms
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
count:
|
||||
type: integer
|
||||
description: Total number of rooms
|
||||
next:
|
||||
type: string
|
||||
nullable: true
|
||||
description: URL to next page
|
||||
previous:
|
||||
type: string
|
||||
nullable: true
|
||||
description: URL to previous page
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Room'
|
||||
examples:
|
||||
roomList:
|
||||
summary: Paginated room list
|
||||
value:
|
||||
count: 2
|
||||
next: "https://visio-sandbox.beta.numerique.gouv.fr/external-api/v1.0/rooms?page=2"
|
||||
previous: null
|
||||
results:
|
||||
- id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
|
||||
slug: "aae-erez-aaz"
|
||||
access_level: "trusted"
|
||||
url: "https://visio-sandbox.beta.numerique.gouv.fr/aae-erez-aaz"
|
||||
telephony:
|
||||
enabled: true
|
||||
pin_code: "123456"
|
||||
phone_number: "+1-555-0100"
|
||||
default_country: "US"
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'403':
|
||||
$ref: '#/components/responses/ForbiddenError'
|
||||
|
||||
post:
|
||||
tags:
|
||||
- Rooms
|
||||
summary: Create a room
|
||||
description: |
|
||||
Creates a new room with secure defaults for external API usage.
|
||||
|
||||
**Restrictions:**
|
||||
- Rooms are always created with `trusted` access (no public rooms via API)
|
||||
- Room access_level can be updated from the webapp interface.
|
||||
|
||||
**Defaults:**
|
||||
- Delegated user is set as owner
|
||||
- Room slug auto-generated for uniqueness
|
||||
- Telephony PIN auto-generated when enabled
|
||||
- Creation tracked with application client_id for auditing
|
||||
operationId: createRoom
|
||||
security:
|
||||
- BearerAuth: [rooms:create]
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RoomCreate'
|
||||
examples:
|
||||
emptyBody:
|
||||
summary: No parameters (default)
|
||||
value: {}
|
||||
responses:
|
||||
'201':
|
||||
description: Room created successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Room'
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'403':
|
||||
$ref: '#/components/responses/ForbiddenError'
|
||||
|
||||
/rooms/{id}:
|
||||
get:
|
||||
tags:
|
||||
- Rooms
|
||||
summary: Retrieve a room
|
||||
description: Get detailed information about a specific room by its ID
|
||||
operationId: retrieveRoom
|
||||
security:
|
||||
- BearerAuth: [rooms:retrieve]
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: Room UUID
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
'200':
|
||||
description: Room details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Room'
|
||||
examples:
|
||||
room:
|
||||
summary: Room details
|
||||
value:
|
||||
id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
|
||||
slug: "aae-erez-aaz"
|
||||
access_level: "trusted"
|
||||
url: "https://visio-sandbox.beta.numerique.gouv.fr/aae-erez-aaz"
|
||||
telephony:
|
||||
enabled: true
|
||||
pin_code: "123456"
|
||||
phone_number: "+1-555-0100"
|
||||
default_country: "US"
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'403':
|
||||
$ref: '#/components/responses/ForbiddenError'
|
||||
'404':
|
||||
$ref: '#/components/responses/RoomNotFoundError'
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
JWT token obtained from the `/application/token` endpoint.
|
||||
Include in requests as: `Authorization: Bearer <token>`
|
||||
|
||||
schemas:
|
||||
TokenRequest:
|
||||
type: object
|
||||
required:
|
||||
- client_id
|
||||
- client_secret
|
||||
- grant_type
|
||||
- scope
|
||||
properties:
|
||||
client_id:
|
||||
type: string
|
||||
description: Application client identifier
|
||||
example: "550e8400-e29b-41d4-a716-446655440000"
|
||||
client_secret:
|
||||
type: string
|
||||
format: password
|
||||
writeOnly: true
|
||||
description: Application secret key
|
||||
example: "1234567890abcdefghijklmnopqrstuvwxyz"
|
||||
grant_type:
|
||||
type: string
|
||||
enum:
|
||||
- client_credentials
|
||||
description: OAuth2 grant type (must be 'client_credentials')
|
||||
example: "client_credentials"
|
||||
scope:
|
||||
type: string
|
||||
format: email
|
||||
description: |
|
||||
Email address of the user to delegate.
|
||||
The application will act on behalf of this user.
|
||||
Note: This parameter is named 'scope' to align with OAuth2 conventions,
|
||||
but accepts an email address to identify the user. This design allows
|
||||
for future extensibility.
|
||||
example: "user@example.com"
|
||||
|
||||
TokenResponse:
|
||||
type: object
|
||||
properties:
|
||||
access_token:
|
||||
type: string
|
||||
description: JWT access token
|
||||
example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtZWV0LWFwaSIsImF1ZCI6Im1lZXQtY2xpZW50cyIsImlhdCI6MTcwOTQ5MTIwMCwiZXhwIjoxNzA5NDk0ODAwLCJjbGllbnRfaWQiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAiLCJzY29wZSI6InJvb21zOmxpc3Qgcm9vbXM6cmV0cmlldmUgcm9vbXM6Y3JlYXRlIiwidXNlcl9pZCI6IjdiOGQ5YzQwLTNhMmItNGVkZi04NzFjLTJmM2Q0ZTVmNmE3YiIsImRlbGVnYXRlZCI6dHJ1ZX0.signature"
|
||||
token_type:
|
||||
type: string
|
||||
description: Token type (always 'Bearer')
|
||||
example: "Bearer"
|
||||
expires_in:
|
||||
type: integer
|
||||
description: Token lifetime in seconds
|
||||
example: 3600
|
||||
scope:
|
||||
type: string
|
||||
description: Space-separated list of granted permission scopes
|
||||
example: "rooms:list rooms:retrieve rooms:create"
|
||||
|
||||
RoomCreate:
|
||||
type: object
|
||||
description: Empty object - all room properties are auto-generated
|
||||
properties: {}
|
||||
|
||||
Room:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
readOnly: true
|
||||
description: Unique room identifier
|
||||
example: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
|
||||
slug:
|
||||
type: string
|
||||
readOnly: true
|
||||
description: URL-friendly room identifier (auto-generated)
|
||||
example: "aze-eere-zer"
|
||||
access_level:
|
||||
type: string
|
||||
readOnly: true
|
||||
description: Room access level (always 'trusted' for API-created rooms)
|
||||
example: "trusted"
|
||||
url:
|
||||
type: string
|
||||
format: uri
|
||||
readOnly: true
|
||||
description: Full URL to access the room
|
||||
example: "https://visio-sandbox.beta.numerique.gouv.fr/aze-eere-zer"
|
||||
telephony:
|
||||
type: object
|
||||
readOnly: true
|
||||
description: Telephony dial-in information (if enabled)
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
description: Whether telephony is available
|
||||
example: true
|
||||
pin_code:
|
||||
type: string
|
||||
description: PIN code for dial-in access
|
||||
example: "123456"
|
||||
phone_number:
|
||||
type: string
|
||||
description: Phone number to dial
|
||||
example: "+1-555-0100"
|
||||
default_country:
|
||||
type: string
|
||||
description: Default country code
|
||||
example: "US"
|
||||
|
||||
OAuthError:
|
||||
type: object
|
||||
description: OAuth2-compliant error response
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Human-readable error description
|
||||
example: "Invalid credentials"
|
||||
|
||||
Error:
|
||||
type: object
|
||||
properties:
|
||||
detail:
|
||||
type: string
|
||||
description: Error message
|
||||
example: "Invalid token."
|
||||
|
||||
ValidationError:
|
||||
type: object
|
||||
properties:
|
||||
field_name:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of validation errors for this field
|
||||
example: ["This field is required."]
|
||||
|
||||
responses:
|
||||
UnauthorizedError:
|
||||
description: The access token is expired, revoked, malformed, or invalid for other reasons. The client can obtain a new access token and try again.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
examples:
|
||||
invalidToken:
|
||||
summary: Invalid token
|
||||
value:
|
||||
error: "Invalid token."
|
||||
|
||||
ForbiddenError:
|
||||
description: Insufficient scope for this operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
examples:
|
||||
insufficientScope:
|
||||
summary: Missing required scope
|
||||
value:
|
||||
detail: "Insufficient permissions. Required scope: 'rooms:xxxx'"
|
||||
|
||||
RoomNotFoundError:
|
||||
description: Room not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
examples:
|
||||
roomNotFound:
|
||||
summary: Room does not exist
|
||||
value:
|
||||
detail: "Not found."
|
||||
|
||||
BadRequestError:
|
||||
description: Invalid request data
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
examples:
|
||||
validationError:
|
||||
summary: Field validation failed
|
||||
value:
|
||||
scope: ["Invalid email address."]
|
||||
@@ -32,6 +32,8 @@ OIDC_OP_JWKS_ENDPOINT=http://nginx:8083/realms/meet/protocol/openid-connect/cert
|
||||
OIDC_OP_AUTHORIZATION_ENDPOINT=http://localhost:8083/realms/meet/protocol/openid-connect/auth
|
||||
OIDC_OP_TOKEN_ENDPOINT=http://nginx:8083/realms/meet/protocol/openid-connect/token
|
||||
OIDC_OP_USER_ENDPOINT=http://nginx:8083/realms/meet/protocol/openid-connect/userinfo
|
||||
OIDC_OP_INTROSPECTION_ENDPOINT=http://nginx:8083/realms/meet/protocol/openid-connect/token/introspect
|
||||
OIDC_OP_URL=http://localhost:8083/realms/meet
|
||||
|
||||
OIDC_RP_CLIENT_ID=meet
|
||||
OIDC_RP_CLIENT_SECRET=ThisIsAnExampleKeyForDevPurposeOnly
|
||||
@@ -45,6 +47,9 @@ LOGOUT_REDIRECT_URL=http://localhost:3000
|
||||
OIDC_REDIRECT_ALLOWED_HOSTS=localhost:8083,localhost:3000
|
||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS={"acr_values": "eidas1"}
|
||||
|
||||
OIDC_RS_CLIENT_ID=meet
|
||||
OIDC_RS_CLIENT_SECRET=ThisIsAnExampleKeyForDevPurposeOnly
|
||||
|
||||
# Livekit Token settings
|
||||
LIVEKIT_API_SECRET=secret
|
||||
LIVEKIT_API_KEY=devkey
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
"matchManagers": ["pep621"],
|
||||
"matchPackageNames": ["redis"]
|
||||
},
|
||||
{
|
||||
"groupName": "allowed pylint versions",
|
||||
"matchManagers": ["pep621"],
|
||||
"matchPackageNames": ["pylint"],
|
||||
"allowedVersions": "<4.0.0"
|
||||
},
|
||||
{
|
||||
"enabled": false,
|
||||
"groupName": "ignored js dependencies",
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
|
||||
[project]
|
||||
name = "agents"
|
||||
version = "0.1.41"
|
||||
version = "1.0.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"livekit-agents==1.2.6",
|
||||
"livekit-plugins-deepgram==1.2.6",
|
||||
"livekit-plugins-silero==1.2.6",
|
||||
"python-dotenv==1.1.1"
|
||||
"livekit-agents==1.2.18",
|
||||
"livekit-plugins-deepgram==1.2.18",
|
||||
"livekit-plugins-silero==1.2.18",
|
||||
"python-dotenv==1.2.1"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"ruff==0.12.0",
|
||||
"ruff==0.14.4",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
||||
@@ -159,6 +159,8 @@ class RoomSerializer(serializers.ModelSerializer):
|
||||
configuration=configuration,
|
||||
is_admin_or_owner=is_admin_or_owner,
|
||||
)
|
||||
else:
|
||||
del output["pin_code"]
|
||||
|
||||
output["is_administrable"] = is_admin_or_owner
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
|
||||
import jwt
|
||||
import jwt as pyJwt
|
||||
from lasuite.oidc_resource_server.backend import ResourceServerBackend as LaSuiteBackend
|
||||
from rest_framework import authentication, exceptions
|
||||
|
||||
User = get_user_model()
|
||||
@@ -25,9 +27,11 @@ class ApplicationJWTAuthentication(authentication.BaseAuthentication):
|
||||
Returns:
|
||||
Tuple of (user, payload) if authentication successful, None otherwise
|
||||
"""
|
||||
|
||||
auth_header = authentication.get_authorization_header(request).split()
|
||||
|
||||
if not auth_header or auth_header[0].lower() != b"bearer":
|
||||
# Defer to next authentication backend
|
||||
return None
|
||||
|
||||
if len(auth_header) != 2:
|
||||
@@ -45,6 +49,8 @@ class ApplicationJWTAuthentication(authentication.BaseAuthentication):
|
||||
def authenticate_credentials(self, token):
|
||||
"""Validate JWT token and return authenticated user.
|
||||
|
||||
If token is invalid, defer to next authentication backend.
|
||||
|
||||
Args:
|
||||
token: JWT token string
|
||||
|
||||
@@ -52,29 +58,29 @@ class ApplicationJWTAuthentication(authentication.BaseAuthentication):
|
||||
Tuple of (user, payload)
|
||||
|
||||
Raises:
|
||||
AuthenticationFailed: If token is invalid, expired, or user not found
|
||||
AuthenticationFailed: If token is expired, or user not found
|
||||
"""
|
||||
# Decode and validate JWT
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
payload = pyJwt.decode(
|
||||
token,
|
||||
settings.APPLICATION_JWT_SECRET_KEY,
|
||||
algorithms=[settings.APPLICATION_JWT_ALG],
|
||||
issuer=settings.APPLICATION_JWT_ISSUER,
|
||||
audience=settings.APPLICATION_JWT_AUDIENCE,
|
||||
)
|
||||
except jwt.ExpiredSignatureError as e:
|
||||
except pyJwt.ExpiredSignatureError as e:
|
||||
logger.warning("Token expired")
|
||||
raise exceptions.AuthenticationFailed("Token expired.") from e
|
||||
except jwt.InvalidIssuerError as e:
|
||||
except pyJwt.InvalidIssuerError as e:
|
||||
logger.warning("Invalid JWT issuer: %s", e)
|
||||
raise exceptions.AuthenticationFailed("Invalid token.") from e
|
||||
except jwt.InvalidAudienceError as e:
|
||||
except pyJwt.InvalidAudienceError as e:
|
||||
logger.warning("Invalid JWT audience: %s", e)
|
||||
raise exceptions.AuthenticationFailed("Invalid token.") from e
|
||||
except jwt.InvalidTokenError as e:
|
||||
logger.warning("Invalid JWT token: %s", e)
|
||||
raise exceptions.AuthenticationFailed("Invalid token.") from e
|
||||
except pyJwt.InvalidTokenError:
|
||||
# Invalid JWT token - defer to next authentication backend
|
||||
return None
|
||||
|
||||
user_id = payload.get("user_id")
|
||||
client_id = payload.get("client_id")
|
||||
@@ -107,3 +113,55 @@ class ApplicationJWTAuthentication(authentication.BaseAuthentication):
|
||||
def authenticate_header(self, request):
|
||||
"""Return authentication scheme for WWW-Authenticate header."""
|
||||
return "Bearer"
|
||||
|
||||
|
||||
class ResourceServerBackend(LaSuiteBackend):
|
||||
"""OIDC Resource Server backend for user creation and retrieval."""
|
||||
|
||||
def get_or_create_user(self, access_token, id_token, payload):
|
||||
"""Get or create user from OIDC token claims.
|
||||
|
||||
Despite the LaSuiteBackend's method name suggesting "get_or_create",
|
||||
its implementation only performs a GET operation.
|
||||
Create new user from the sub claim.
|
||||
|
||||
Args:
|
||||
access_token: The access token string
|
||||
id_token: The ID token string (unused)
|
||||
payload: Token payload dict (unused)
|
||||
|
||||
Returns:
|
||||
User instance
|
||||
|
||||
Raises:
|
||||
SuspiciousOperation: If user info validation fails
|
||||
"""
|
||||
|
||||
sub = payload.get("sub")
|
||||
|
||||
if sub is None:
|
||||
message = "User info contained no recognizable user identification"
|
||||
logger.debug(message)
|
||||
raise SuspiciousOperation(message)
|
||||
|
||||
user = self.get_user(access_token, id_token, payload)
|
||||
|
||||
if user is None and settings.OIDC_CREATE_USER:
|
||||
user = self.create_user(sub)
|
||||
|
||||
return user
|
||||
|
||||
def create_user(self, sub):
|
||||
"""Create new user from subject claim.
|
||||
|
||||
Args:
|
||||
sub: Subject identifier from token
|
||||
|
||||
Returns:
|
||||
Newly created User instance
|
||||
"""
|
||||
user = self.UserModel(sub=sub)
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
|
||||
return user
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from rest_framework import exceptions, permissions
|
||||
|
||||
from .. import models
|
||||
@@ -55,6 +57,12 @@ class BaseScopePermission(permissions.BasePermission):
|
||||
if isinstance(token_scopes, str):
|
||||
token_scopes = token_scopes.split()
|
||||
|
||||
if settings.OIDC_RS_SCOPES_PREFIX:
|
||||
token_scopes = [
|
||||
scope.replace(f"{settings.OIDC_RS_SCOPES_PREFIX}:", "")
|
||||
for scope in token_scopes
|
||||
]
|
||||
|
||||
if required_scope not in token_scopes:
|
||||
raise exceptions.PermissionDenied(
|
||||
f"Insufficient permissions. Required scope: {required_scope}"
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.core.validators import validate_email
|
||||
|
||||
import jwt
|
||||
from lasuite.oidc_resource_server.authentication import ResourceServerAuthentication
|
||||
from rest_framework import decorators, mixins, viewsets
|
||||
from rest_framework import (
|
||||
exceptions as drf_exceptions,
|
||||
@@ -149,7 +150,10 @@ class RoomViewSet(
|
||||
- create: Create a new room owned by the user (requires 'rooms:create' scope)
|
||||
"""
|
||||
|
||||
authentication_classes = [authentication.ApplicationJWTAuthentication]
|
||||
authentication_classes = [
|
||||
authentication.ApplicationJWTAuthentication,
|
||||
ResourceServerAuthentication,
|
||||
]
|
||||
permission_classes = [
|
||||
api.permissions.IsAuthenticated & permissions.HasRequiredRoomScope
|
||||
]
|
||||
|
||||
@@ -345,7 +345,9 @@ def test_authentication_getter_existing_user_change_fields(
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
# One and only one additional update query when a field has changed
|
||||
with django_assert_num_queries(2):
|
||||
# Note: .save() triggers uniqueness validation queries for unique fields,
|
||||
# adding extra SELECT queries before the UPDATE (e.g., checking unique=True on 'sub')
|
||||
with django_assert_num_queries(3):
|
||||
authenticated_user = klass.get_or_create_user(
|
||||
access_token="test-token", id_token=None, payload=None
|
||||
)
|
||||
|
||||
@@ -32,7 +32,6 @@ def test_api_rooms_retrieve_anonymous_private_pk():
|
||||
"id": str(room.id),
|
||||
"is_administrable": False,
|
||||
"name": room.name,
|
||||
"pin_code": room.pin_code,
|
||||
"slug": room.slug,
|
||||
}
|
||||
|
||||
@@ -52,7 +51,6 @@ def test_api_rooms_retrieve_anonymous_trusted_pk():
|
||||
"id": str(room.id),
|
||||
"is_administrable": False,
|
||||
"name": room.name,
|
||||
"pin_code": room.pin_code,
|
||||
"slug": room.slug,
|
||||
}
|
||||
|
||||
@@ -71,7 +69,6 @@ def test_api_rooms_retrieve_anonymous_private_pk_no_dashes():
|
||||
"id": str(room.id),
|
||||
"is_administrable": False,
|
||||
"name": room.name,
|
||||
"pin_code": room.pin_code,
|
||||
"slug": room.slug,
|
||||
}
|
||||
|
||||
@@ -88,7 +85,6 @@ def test_api_rooms_retrieve_anonymous_private_slug():
|
||||
"id": str(room.id),
|
||||
"is_administrable": False,
|
||||
"name": room.name,
|
||||
"pin_code": room.pin_code,
|
||||
"slug": room.slug,
|
||||
}
|
||||
|
||||
@@ -105,7 +101,6 @@ def test_api_rooms_retrieve_anonymous_private_slug_not_normalized():
|
||||
"id": str(room.id),
|
||||
"is_administrable": False,
|
||||
"name": room.name,
|
||||
"pin_code": room.pin_code,
|
||||
"slug": room.slug,
|
||||
}
|
||||
|
||||
@@ -347,7 +342,6 @@ def test_api_rooms_retrieve_authenticated():
|
||||
"id": str(room.id),
|
||||
"is_administrable": False,
|
||||
"name": room.name,
|
||||
"pin_code": room.pin_code,
|
||||
"slug": room.slug,
|
||||
}
|
||||
|
||||
|
||||
@@ -10,13 +10,14 @@ from django.conf import settings
|
||||
|
||||
import jwt
|
||||
import pytest
|
||||
import responses
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core.factories import (
|
||||
RoomFactory,
|
||||
UserFactory,
|
||||
)
|
||||
from core.models import ApplicationScope, RoleChoices, Room
|
||||
from core.models import ApplicationScope, RoleChoices, Room, RoomAccessLevel, User
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -90,13 +91,29 @@ def test_api_rooms_list_with_expired_token(settings):
|
||||
assert "expired" in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_list_with_invalid_token():
|
||||
"""Listing rooms with invalid token should return 401."""
|
||||
@responses.activate
|
||||
def test_api_rooms_list_with_invalid_token(settings):
|
||||
"""Listing rooms with invalid token should return 400."""
|
||||
|
||||
settings.OIDC_OP_INTROSPECTION_ENDPOINT = "https://oidc.example.com/introspect"
|
||||
settings.OIDC_OP_URL = "https://oidc.example.com"
|
||||
|
||||
responses.add(
|
||||
responses.POST,
|
||||
"https://oidc.example.com/introspect",
|
||||
json={
|
||||
"iss": "https://oidc.example.com",
|
||||
"active": False,
|
||||
},
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION="Bearer invalid-token-123")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 401
|
||||
# Return 400 instead of 401 because ResourceServerAuthentication raises
|
||||
# SuspiciousOperation when the introspected user is not active
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_api_rooms_list_missing_scope(settings):
|
||||
@@ -332,3 +349,221 @@ def test_api_rooms_token_missing_client_id(settings):
|
||||
|
||||
assert response.status_code == 401
|
||||
assert "Invalid token claims." in str(response.data)
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_resource_server_creates_user_on_first_authentication(settings):
|
||||
"""New user should be created during first authentication.
|
||||
|
||||
Verifies that the ResourceServerBackend.get_or_create_user() creates a user
|
||||
in the database when authenticating with a token from an unknown subject (sub).
|
||||
This tests the user creation workflow during the OIDC introspection process.
|
||||
"""
|
||||
|
||||
with pytest.raises(
|
||||
User.DoesNotExist,
|
||||
match="User matching query does not exist.",
|
||||
):
|
||||
User.objects.get(sub="very-specific-sub")
|
||||
|
||||
assert (
|
||||
settings.OIDC_RS_BACKEND_CLASS
|
||||
== "core.external_api.authentication.ResourceServerBackend"
|
||||
)
|
||||
|
||||
settings.OIDC_RS_CLIENT_ID = "some_client_id"
|
||||
settings.OIDC_RS_CLIENT_SECRET = "some_client_secret"
|
||||
settings.OIDC_RS_SCOPES_PREFIX = "lasuite_meet"
|
||||
|
||||
settings.OIDC_OP_URL = "https://oidc.example.com"
|
||||
settings.OIDC_VERIFY_SSL = False
|
||||
settings.OIDC_TIMEOUT = 5
|
||||
settings.OIDC_PROXY = None
|
||||
settings.OIDC_OP_JWKS_ENDPOINT = "https://oidc.example.com/jwks"
|
||||
settings.OIDC_OP_INTROSPECTION_ENDPOINT = "https://oidc.example.com/introspect"
|
||||
|
||||
responses.add(
|
||||
responses.POST,
|
||||
"https://oidc.example.com/introspect",
|
||||
json={
|
||||
"iss": "https://oidc.example.com",
|
||||
"aud": "some_client_id", # settings.OIDC_RS_CLIENT_ID
|
||||
"sub": "very-specific-sub",
|
||||
"client_id": "some_service_provider",
|
||||
"scope": "openid lasuite_meet lasuite_meet:rooms:list",
|
||||
"active": True,
|
||||
},
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION="Bearer some_token")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
results = response.json()["results"]
|
||||
assert len(results) == 0
|
||||
|
||||
db_user = User.objects.get(sub="very-specific-sub")
|
||||
assert db_user is not None
|
||||
assert db_user.email is None
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_resource_server_skips_user_creation_when_auto_creation_disabled(settings):
|
||||
"""Verify that ResourceServerBackend respects the user auto-creation setting.
|
||||
|
||||
This ensures that the OIDC introspection process respects the configuration flag
|
||||
that controls whether new users should be automatically provisioned during
|
||||
authentication, preventing unwanted user proliferation when auto-creation is
|
||||
explicitly disabled.
|
||||
"""
|
||||
|
||||
settings.OIDC_CREATE_USER = False
|
||||
|
||||
with pytest.raises(
|
||||
User.DoesNotExist,
|
||||
match="User matching query does not exist.",
|
||||
):
|
||||
User.objects.get(sub="very-specific-sub")
|
||||
|
||||
assert (
|
||||
settings.OIDC_RS_BACKEND_CLASS
|
||||
== "core.external_api.authentication.ResourceServerBackend"
|
||||
)
|
||||
|
||||
settings.OIDC_RS_CLIENT_ID = "some_client_id"
|
||||
settings.OIDC_RS_CLIENT_SECRET = "some_client_secret"
|
||||
|
||||
settings.OIDC_OP_URL = "https://oidc.example.com"
|
||||
settings.OIDC_VERIFY_SSL = False
|
||||
settings.OIDC_TIMEOUT = 5
|
||||
settings.OIDC_PROXY = None
|
||||
settings.OIDC_OP_JWKS_ENDPOINT = "https://oidc.example.com/jwks"
|
||||
settings.OIDC_OP_INTROSPECTION_ENDPOINT = "https://oidc.example.com/introspect"
|
||||
|
||||
responses.add(
|
||||
responses.POST,
|
||||
"https://oidc.example.com/introspect",
|
||||
json={
|
||||
"iss": "https://oidc.example.com",
|
||||
"aud": "some_client_id", # settings.OIDC_RS_CLIENT_ID
|
||||
"sub": "very-specific-sub",
|
||||
"client_id": "some_service_provider",
|
||||
"scope": "openid lasuite_meet rooms:list",
|
||||
"active": True,
|
||||
},
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION="Bearer some_token")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_resource_server_authentication_successful(settings):
|
||||
"""Authenticated requests should be processed and user-specific data is returned.
|
||||
|
||||
Verifies that once a user is authenticated via OIDC token introspection,
|
||||
the API correctly identifies the user and returns only data accessible to that user
|
||||
(e.g., rooms with appropriate access levels).
|
||||
"""
|
||||
|
||||
user = UserFactory(sub="very-specific-sub")
|
||||
|
||||
other_user = UserFactory()
|
||||
|
||||
RoomFactory(access_level=RoomAccessLevel.PUBLIC)
|
||||
RoomFactory(access_level=RoomAccessLevel.TRUSTED)
|
||||
RoomFactory(access_level=RoomAccessLevel.RESTRICTED)
|
||||
room_user_accesses = RoomFactory(
|
||||
access_level=RoomAccessLevel.RESTRICTED, users=[user]
|
||||
)
|
||||
RoomFactory(access_level=RoomAccessLevel.RESTRICTED, users=[other_user])
|
||||
|
||||
assert (
|
||||
settings.OIDC_RS_BACKEND_CLASS
|
||||
== "core.external_api.authentication.ResourceServerBackend"
|
||||
)
|
||||
|
||||
settings.OIDC_RS_CLIENT_ID = "some_client_id"
|
||||
settings.OIDC_RS_CLIENT_SECRET = "some_client_secret"
|
||||
settings.OIDC_RS_SCOPES_PREFIX = "lasuite_meet"
|
||||
|
||||
settings.OIDC_OP_URL = "https://oidc.example.com"
|
||||
settings.OIDC_VERIFY_SSL = False
|
||||
settings.OIDC_TIMEOUT = 5
|
||||
settings.OIDC_PROXY = None
|
||||
settings.OIDC_OP_JWKS_ENDPOINT = "https://oidc.example.com/jwks"
|
||||
settings.OIDC_OP_INTROSPECTION_ENDPOINT = "https://oidc.example.com/introspect"
|
||||
|
||||
responses.add(
|
||||
responses.POST,
|
||||
"https://oidc.example.com/introspect",
|
||||
json={
|
||||
"iss": "https://oidc.example.com",
|
||||
"aud": "some_client_id", # settings.OIDC_RS_CLIENT_ID
|
||||
"sub": "very-specific-sub",
|
||||
"client_id": "some_service_provider",
|
||||
"scope": "openid lasuite_meet lasuite_meet:rooms:list",
|
||||
"active": True,
|
||||
},
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION="Bearer some_token")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 200
|
||||
results = response.json()["results"]
|
||||
assert len(results) == 1
|
||||
expected_ids = {
|
||||
str(room_user_accesses.id),
|
||||
}
|
||||
results_id = {result["id"] for result in results}
|
||||
assert expected_ids == results_id
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_resource_server_denies_access_with_insufficient_scopes(settings):
|
||||
"""Requests should be denied when the token lacks required scopes.
|
||||
|
||||
Verifies that the ResourceServerBackend validates token scopes during introspection
|
||||
and returns 403 Forbidden when the token is missing required scopes for the endpoint.
|
||||
"""
|
||||
|
||||
assert (
|
||||
settings.OIDC_RS_BACKEND_CLASS
|
||||
== "core.external_api.authentication.ResourceServerBackend"
|
||||
)
|
||||
|
||||
settings.OIDC_RS_CLIENT_ID = "some_client_id"
|
||||
settings.OIDC_RS_CLIENT_SECRET = "some_client_secret"
|
||||
|
||||
settings.OIDC_OP_URL = "https://oidc.example.com"
|
||||
settings.OIDC_VERIFY_SSL = False
|
||||
settings.OIDC_TIMEOUT = 5
|
||||
settings.OIDC_PROXY = None
|
||||
settings.OIDC_OP_JWKS_ENDPOINT = "https://oidc.example.com/jwks"
|
||||
settings.OIDC_OP_INTROSPECTION_ENDPOINT = "https://oidc.example.com/introspect"
|
||||
|
||||
responses.add(
|
||||
responses.POST,
|
||||
"https://oidc.example.com/introspect",
|
||||
json={
|
||||
"iss": "https://oidc.example.com",
|
||||
"aud": "some_client_id", # settings.OIDC_RS_CLIENT_ID
|
||||
"sub": "very-specific-sub",
|
||||
"client_id": "some_service_provider",
|
||||
"scope": "openid lasuite_meet", # missing rooms:list scope
|
||||
"active": True,
|
||||
},
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION="Bearer some_token")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-07-11 11:33+0000\n"
|
||||
"POT-Creation-Date: 2025-11-13 16:21+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -17,121 +17,153 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: core/admin.py:26
|
||||
#: core/admin.py:29
|
||||
msgid "Personal info"
|
||||
msgstr "Persönliche Informationen"
|
||||
|
||||
#: core/admin.py:39
|
||||
#: core/admin.py:42
|
||||
msgid "Permissions"
|
||||
msgstr "Berechtigungen"
|
||||
|
||||
#: core/admin.py:51
|
||||
#: core/admin.py:54
|
||||
msgid "Important dates"
|
||||
msgstr "Wichtige Daten"
|
||||
|
||||
#: core/admin.py:147
|
||||
#: core/admin.py:128 core/admin.py:228
|
||||
msgid "No owner"
|
||||
msgstr "Kein Eigentümer"
|
||||
|
||||
#: core/admin.py:150
|
||||
#: core/admin.py:131 core/admin.py:231
|
||||
msgid "Multiple owners"
|
||||
msgstr "Mehrere Eigentümer"
|
||||
|
||||
#: core/api/serializers.py:67
|
||||
#: core/admin.py:143
|
||||
msgid "Resend notification to external service"
|
||||
msgstr "Benachrichtigung erneut an externen Dienst senden"
|
||||
|
||||
#: core/admin.py:166
|
||||
#, python-format
|
||||
msgid "Failed to notify for recording %(id)s"
|
||||
msgstr "Benachrichtigung für Aufnahme %(id)s fehlgeschlagen"
|
||||
|
||||
#: core/admin.py:174
|
||||
#, python-format
|
||||
msgid "Failed to notify for recording %(id)s: %(error)s"
|
||||
msgstr "Benachrichtigung für Aufnahme %(id)s fehlgeschlagen: %(error)s"
|
||||
|
||||
#: core/admin.py:182
|
||||
#, python-format
|
||||
msgid "Successfully sent notifications for %(count)s recording(s)."
|
||||
msgstr "Benachrichtigungen für %(count)s Aufnahme(n) erfolgreich gesendet."
|
||||
|
||||
#: core/admin.py:190
|
||||
#, python-format
|
||||
msgid "Skipped %(count)s expired recording(s)."
|
||||
msgstr "%(count)s abgelaufene Aufnahme(n) übersprungen."
|
||||
|
||||
#: core/admin.py:294
|
||||
msgid "No scopes"
|
||||
msgstr "Keine Scopes"
|
||||
|
||||
#: core/admin.py:296
|
||||
msgid "Scopes"
|
||||
msgstr "Scopes"
|
||||
|
||||
#: core/api/serializers.py:68
|
||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||
msgstr ""
|
||||
"Sie müssen Administrator oder Eigentümer eines Raums sein, um Zugriffe "
|
||||
"hinzuzufügen."
|
||||
|
||||
#: core/models.py:31
|
||||
#: core/models.py:34
|
||||
msgid "Member"
|
||||
msgstr "Mitglied"
|
||||
|
||||
#: core/models.py:32
|
||||
#: core/models.py:35
|
||||
msgid "Administrator"
|
||||
msgstr "Administrator"
|
||||
|
||||
#: core/models.py:33
|
||||
#: core/models.py:36
|
||||
msgid "Owner"
|
||||
msgstr "Eigentümer"
|
||||
|
||||
#: core/models.py:49
|
||||
#: core/models.py:52
|
||||
msgid "Initiated"
|
||||
msgstr "Gestartet"
|
||||
|
||||
#: core/models.py:50
|
||||
#: core/models.py:53
|
||||
msgid "Active"
|
||||
msgstr "Aktiv"
|
||||
|
||||
#: core/models.py:51
|
||||
#: core/models.py:54
|
||||
msgid "Stopped"
|
||||
msgstr "Beendet"
|
||||
|
||||
#: core/models.py:52
|
||||
#: core/models.py:55
|
||||
msgid "Saved"
|
||||
msgstr "Gespeichert"
|
||||
|
||||
#: core/models.py:53
|
||||
#: core/models.py:56
|
||||
msgid "Aborted"
|
||||
msgstr "Abgebrochen"
|
||||
|
||||
#: core/models.py:54
|
||||
#: core/models.py:57
|
||||
msgid "Failed to Start"
|
||||
msgstr "Start fehlgeschlagen"
|
||||
|
||||
#: core/models.py:55
|
||||
#: core/models.py:58
|
||||
msgid "Failed to Stop"
|
||||
msgstr "Stopp fehlgeschlagen"
|
||||
|
||||
#: core/models.py:56
|
||||
#: core/models.py:59
|
||||
msgid "Notification succeeded"
|
||||
msgstr "Benachrichtigung erfolgreich"
|
||||
|
||||
#: core/models.py:83
|
||||
#: core/models.py:86
|
||||
msgid "SCREEN_RECORDING"
|
||||
msgstr "BILDSCHIRMAUFZEICHNUNG"
|
||||
|
||||
#: core/models.py:84
|
||||
#: core/models.py:87
|
||||
msgid "TRANSCRIPT"
|
||||
msgstr "TRANSKRIPT"
|
||||
|
||||
#: core/models.py:90
|
||||
#: core/models.py:93
|
||||
msgid "Public Access"
|
||||
msgstr "Öffentlicher Zugriff"
|
||||
|
||||
#: core/models.py:91
|
||||
#: core/models.py:94
|
||||
msgid "Trusted Access"
|
||||
msgstr "Vertrauenswürdiger Zugriff"
|
||||
|
||||
#: core/models.py:92
|
||||
#: core/models.py:95
|
||||
msgid "Restricted Access"
|
||||
msgstr "Eingeschränkter Zugriff"
|
||||
|
||||
#: core/models.py:104
|
||||
#: core/models.py:107
|
||||
msgid "id"
|
||||
msgstr "ID"
|
||||
|
||||
#: core/models.py:105
|
||||
#: core/models.py:108
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "Primärschlüssel des Eintrags als UUID"
|
||||
|
||||
#: core/models.py:111
|
||||
#: core/models.py:114
|
||||
msgid "created on"
|
||||
msgstr "erstellt am"
|
||||
|
||||
#: core/models.py:112
|
||||
#: core/models.py:115
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "Datum und Uhrzeit der Erstellung eines Eintrags"
|
||||
|
||||
#: core/models.py:117
|
||||
#: core/models.py:120
|
||||
msgid "updated on"
|
||||
msgstr "aktualisiert am"
|
||||
|
||||
#: core/models.py:118
|
||||
#: core/models.py:121
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "Datum und Uhrzeit der letzten Aktualisierung eines Eintrags"
|
||||
|
||||
#: core/models.py:138
|
||||
#: core/models.py:141
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
@@ -139,11 +171,11 @@ msgstr ""
|
||||
"Geben Sie einen gültigen Sub ein. Dieser Wert darf nur Buchstaben, Zahlen "
|
||||
"und die Zeichen @/./+/-/_ enthalten."
|
||||
|
||||
#: core/models.py:144
|
||||
#: core/models.py:147
|
||||
msgid "sub"
|
||||
msgstr "Sub"
|
||||
|
||||
#: core/models.py:146
|
||||
#: core/models.py:149
|
||||
msgid ""
|
||||
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
|
||||
"characters only."
|
||||
@@ -151,55 +183,55 @@ msgstr ""
|
||||
"Erforderlich. Maximal 255 Zeichen. Nur Buchstaben, Zahlen und @/./+/-/_ sind "
|
||||
"erlaubt."
|
||||
|
||||
#: core/models.py:154
|
||||
#: core/models.py:157
|
||||
msgid "identity email address"
|
||||
msgstr "Identitäts-E-Mail-Adresse"
|
||||
|
||||
#: core/models.py:159
|
||||
#: core/models.py:162
|
||||
msgid "admin email address"
|
||||
msgstr "Administrator-E-Mail-Adresse"
|
||||
|
||||
#: core/models.py:161
|
||||
#: core/models.py:164
|
||||
msgid "full name"
|
||||
msgstr "Vollständiger Name"
|
||||
|
||||
#: core/models.py:163
|
||||
#: core/models.py:166
|
||||
msgid "short name"
|
||||
msgstr "Kurzname"
|
||||
|
||||
#: core/models.py:169
|
||||
#: core/models.py:172
|
||||
msgid "language"
|
||||
msgstr "Sprache"
|
||||
|
||||
#: core/models.py:170
|
||||
#: core/models.py:173
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "Die Sprache, in der der Benutzer die Oberfläche sehen möchte."
|
||||
|
||||
#: core/models.py:176
|
||||
#: core/models.py:179
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Die Zeitzone, in der der Benutzer die Zeiten sehen möchte."
|
||||
|
||||
#: core/models.py:179
|
||||
#: core/models.py:182
|
||||
msgid "device"
|
||||
msgstr "Gerät"
|
||||
|
||||
#: core/models.py:181
|
||||
#: core/models.py:184
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Ob es sich um ein Gerät oder einen echten Benutzer handelt."
|
||||
|
||||
#: core/models.py:184
|
||||
#: core/models.py:187
|
||||
msgid "staff status"
|
||||
msgstr "Mitarbeiterstatus"
|
||||
|
||||
#: core/models.py:186
|
||||
#: core/models.py:189
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Ob der Benutzer sich bei dieser Admin-Seite anmelden kann."
|
||||
|
||||
#: core/models.py:189
|
||||
#: core/models.py:192
|
||||
msgid "active"
|
||||
msgstr "aktiv"
|
||||
|
||||
#: core/models.py:192
|
||||
#: core/models.py:195
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
@@ -207,66 +239,66 @@ msgstr ""
|
||||
"Ob dieser Benutzer als aktiv behandelt werden soll. Deaktivieren Sie dies "
|
||||
"anstelle des Löschens des Kontos."
|
||||
|
||||
#: core/models.py:205
|
||||
#: core/models.py:208
|
||||
msgid "user"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: core/models.py:206
|
||||
#: core/models.py:209
|
||||
msgid "users"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: core/models.py:265
|
||||
#: core/models.py:268
|
||||
msgid "Resource"
|
||||
msgstr "Ressource"
|
||||
|
||||
#: core/models.py:266
|
||||
#: core/models.py:269
|
||||
msgid "Resources"
|
||||
msgstr "Ressourcen"
|
||||
|
||||
#: core/models.py:320
|
||||
#: core/models.py:323
|
||||
msgid "Resource access"
|
||||
msgstr "Ressourcenzugriff"
|
||||
|
||||
#: core/models.py:321
|
||||
#: core/models.py:324
|
||||
msgid "Resource accesses"
|
||||
msgstr "Ressourcenzugriffe"
|
||||
|
||||
#: core/models.py:327
|
||||
#: core/models.py:330
|
||||
msgid "Resource access with this User and Resource already exists."
|
||||
msgstr ""
|
||||
"Ein Ressourcenzugriff mit diesem Benutzer und dieser Ressource existiert "
|
||||
"bereits."
|
||||
|
||||
#: core/models.py:383
|
||||
#: core/models.py:386
|
||||
msgid "Visio room configuration"
|
||||
msgstr "Visio-Raumkonfiguration"
|
||||
|
||||
#: core/models.py:384
|
||||
#: core/models.py:387
|
||||
msgid "Values for Visio parameters to configure the room."
|
||||
msgstr "Werte für Visio-Parameter zur Konfiguration des Raums."
|
||||
|
||||
#: core/models.py:391
|
||||
#: core/models.py:394
|
||||
msgid "Room PIN code"
|
||||
msgstr "PIN-Code für den Raum"
|
||||
|
||||
#: core/models.py:392
|
||||
#: core/models.py:395
|
||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||
msgstr ""
|
||||
"Eindeutiger n-stelliger Code, der diesen Raum im Telephonmodus identifiziert."
|
||||
|
||||
#: core/models.py:398 core/models.py:552
|
||||
#: core/models.py:401 core/models.py:555
|
||||
msgid "Room"
|
||||
msgstr "Raum"
|
||||
|
||||
#: core/models.py:399
|
||||
#: core/models.py:402
|
||||
msgid "Rooms"
|
||||
msgstr "Räume"
|
||||
|
||||
#: core/models.py:563
|
||||
#: core/models.py:566
|
||||
msgid "Worker ID"
|
||||
msgstr "Worker-ID"
|
||||
|
||||
#: core/models.py:565
|
||||
#: core/models.py:568
|
||||
msgid ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
@@ -275,42 +307,102 @@ msgstr ""
|
||||
"erhalten, auch wenn der Worker stoppt, was ein einfaches Nachverfolgen "
|
||||
"ermöglicht."
|
||||
|
||||
#: core/models.py:573
|
||||
#: core/models.py:576
|
||||
msgid "Recording mode"
|
||||
msgstr "Aufzeichnungsmodus"
|
||||
|
||||
#: core/models.py:574
|
||||
#: core/models.py:577
|
||||
msgid "Defines the mode of recording being called."
|
||||
msgstr "Definiert den aufgerufenen Aufzeichnungsmodus."
|
||||
|
||||
#: core/models.py:580
|
||||
#: core/models.py:583
|
||||
msgid "Recording"
|
||||
msgstr "Aufzeichnung"
|
||||
|
||||
#: core/models.py:581
|
||||
#: core/models.py:584
|
||||
msgid "Recordings"
|
||||
msgstr "Aufzeichnungen"
|
||||
|
||||
#: core/models.py:689
|
||||
#: core/models.py:692
|
||||
msgid "Recording/user relation"
|
||||
msgstr "Beziehung Aufzeichnung/Benutzer"
|
||||
|
||||
#: core/models.py:690
|
||||
#: core/models.py:693
|
||||
msgid "Recording/user relations"
|
||||
msgstr "Beziehungen Aufzeichnung/Benutzer"
|
||||
|
||||
#: core/models.py:696
|
||||
#: core/models.py:699
|
||||
msgid "This user is already in this recording."
|
||||
msgstr "Dieser Benutzer ist bereits Teil dieser Aufzeichnung."
|
||||
|
||||
#: core/models.py:702
|
||||
#: core/models.py:705
|
||||
msgid "This team is already in this recording."
|
||||
msgstr "Dieses Team ist bereits Teil dieser Aufzeichnung."
|
||||
|
||||
#: core/models.py:708
|
||||
#: core/models.py:711
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Entweder Benutzer oder Team muss festgelegt werden, nicht beides."
|
||||
|
||||
#: core/models.py:728
|
||||
msgid "Create rooms"
|
||||
msgstr "Räume erstellen"
|
||||
|
||||
#: core/models.py:729
|
||||
msgid "List rooms"
|
||||
msgstr "Räume auflisten"
|
||||
|
||||
#: core/models.py:730
|
||||
msgid "Retrieve room details"
|
||||
msgstr "Raumdetails abrufen"
|
||||
|
||||
#: core/models.py:731
|
||||
msgid "Update rooms"
|
||||
msgstr "Räume aktualisieren"
|
||||
|
||||
#: core/models.py:732
|
||||
msgid "Delete rooms"
|
||||
msgstr "Räume löschen"
|
||||
|
||||
#: core/models.py:745
|
||||
msgid "Application name"
|
||||
msgstr "Anwendungsname"
|
||||
|
||||
#: core/models.py:746
|
||||
msgid "Descriptive name for this application."
|
||||
msgstr "Beschreibender Name für diese Anwendung."
|
||||
|
||||
#: core/models.py:756
|
||||
msgid "Hashed on Save. Copy it now if this is a new secret."
|
||||
msgstr "Beim Speichern gehasht. Jetzt kopieren, wenn dies ein neues Geheimnis ist."
|
||||
|
||||
#: core/models.py:767
|
||||
msgid "Application"
|
||||
msgstr "Anwendung"
|
||||
|
||||
#: core/models.py:768
|
||||
msgid "Applications"
|
||||
msgstr "Anwendungen"
|
||||
|
||||
#: core/models.py:791
|
||||
msgid "Enter a valid domain"
|
||||
msgstr "Geben Sie eine gültige Domain ein"
|
||||
|
||||
#: core/models.py:794
|
||||
msgid "Domain"
|
||||
msgstr "Domain"
|
||||
|
||||
#: core/models.py:795
|
||||
msgid "Email domain this application can act on behalf of."
|
||||
msgstr "E-Mail-Domain, im Namen der diese Anwendung handeln kann."
|
||||
|
||||
#: core/models.py:807
|
||||
msgid "Application domain"
|
||||
msgstr "Anwendungsdomain"
|
||||
|
||||
#: core/models.py:808
|
||||
msgid "Application domains"
|
||||
msgstr "Anwendungsdomains"
|
||||
|
||||
#: core/recording/event/notification.py:94
|
||||
msgid "Your recording is ready"
|
||||
msgstr "Ihre Aufzeichnung ist bereit"
|
||||
@@ -401,7 +493,8 @@ msgid ""
|
||||
" Sharing the recording via link is not yet available. Only organizers can "
|
||||
"download it. "
|
||||
msgstr ""
|
||||
" Die Freigabe der Aufzeichnung per Link ist noch nicht verfügbar. Nur Organisatoren können sie herunterladen. "
|
||||
" Die Freigabe der Aufzeichnung per Link ist noch nicht verfügbar. Nur "
|
||||
"Organisatoren können sie herunterladen. "
|
||||
|
||||
#: core/templates/mail/html/screen_recording.html:206
|
||||
#: core/templates/mail/text/screen_recording.txt:11
|
||||
@@ -438,18 +531,18 @@ msgstr ""
|
||||
" Wenn Sie Fragen haben oder Unterstützung benötigen, wenden Sie sich bitte "
|
||||
"an unser Support-Team unter %(support_email)s. "
|
||||
|
||||
#: meet/settings.py:163
|
||||
#: meet/settings.py:167
|
||||
msgid "English"
|
||||
msgstr "Englisch"
|
||||
|
||||
#: meet/settings.py:164
|
||||
#: meet/settings.py:168
|
||||
msgid "French"
|
||||
msgstr "Französisch"
|
||||
|
||||
#: meet/settings.py:165
|
||||
#: meet/settings.py:169
|
||||
msgid "Dutch"
|
||||
msgstr "Niederländisch"
|
||||
|
||||
#: meet/settings.py:166
|
||||
#: meet/settings.py:170
|
||||
msgid "German"
|
||||
msgstr "Deutsch"
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-07-11 11:33+0000\n"
|
||||
"POT-Creation-Date: 2025-11-13 16:21+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -17,119 +17,151 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: core/admin.py:26
|
||||
#: core/admin.py:29
|
||||
msgid "Personal info"
|
||||
msgstr ""
|
||||
msgstr "Personal info"
|
||||
|
||||
#: core/admin.py:39
|
||||
#: core/admin.py:42
|
||||
msgid "Permissions"
|
||||
msgstr "Permissions"
|
||||
|
||||
#: core/admin.py:51
|
||||
#: core/admin.py:54
|
||||
msgid "Important dates"
|
||||
msgstr "Important dates"
|
||||
|
||||
#: core/admin.py:147
|
||||
#: core/admin.py:128 core/admin.py:228
|
||||
msgid "No owner"
|
||||
msgstr "No owner"
|
||||
|
||||
#: core/admin.py:150
|
||||
#: core/admin.py:131 core/admin.py:231
|
||||
msgid "Multiple owners"
|
||||
msgstr "Multiple owners"
|
||||
|
||||
#: core/api/serializers.py:67
|
||||
#: core/admin.py:143
|
||||
msgid "Resend notification to external service"
|
||||
msgstr "Resend notification to external service"
|
||||
|
||||
#: core/admin.py:166
|
||||
#, python-format
|
||||
msgid "Failed to notify for recording %(id)s"
|
||||
msgstr "Failed to notify for recording %(id)s"
|
||||
|
||||
#: core/admin.py:174
|
||||
#, python-format
|
||||
msgid "Failed to notify for recording %(id)s: %(error)s"
|
||||
msgstr "Failed to notify for recording %(id)s: %(error)s"
|
||||
|
||||
#: core/admin.py:182
|
||||
#, python-format
|
||||
msgid "Successfully sent notifications for %(count)s recording(s)."
|
||||
msgstr "Successfully sent notifications for %(count)s recording(s)."
|
||||
|
||||
#: core/admin.py:190
|
||||
#, python-format
|
||||
msgid "Skipped %(count)s expired recording(s)."
|
||||
msgstr "Skipped %(count)s expired recording(s)."
|
||||
|
||||
#: core/admin.py:294
|
||||
msgid "No scopes"
|
||||
msgstr "No scopes"
|
||||
|
||||
#: core/admin.py:296
|
||||
msgid "Scopes"
|
||||
msgstr "Scopes"
|
||||
|
||||
#: core/api/serializers.py:68
|
||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||
msgstr "You must be administrator or owner of a room to add accesses to it."
|
||||
|
||||
#: core/models.py:31
|
||||
#: core/models.py:34
|
||||
msgid "Member"
|
||||
msgstr "Member"
|
||||
|
||||
#: core/models.py:32
|
||||
#: core/models.py:35
|
||||
msgid "Administrator"
|
||||
msgstr "Administrator"
|
||||
|
||||
#: core/models.py:33
|
||||
#: core/models.py:36
|
||||
msgid "Owner"
|
||||
msgstr "Owner"
|
||||
|
||||
#: core/models.py:49
|
||||
#: core/models.py:52
|
||||
msgid "Initiated"
|
||||
msgstr "Initiated"
|
||||
|
||||
#: core/models.py:50
|
||||
#: core/models.py:53
|
||||
msgid "Active"
|
||||
msgstr "Active"
|
||||
|
||||
#: core/models.py:51
|
||||
#: core/models.py:54
|
||||
msgid "Stopped"
|
||||
msgstr "Stopped"
|
||||
|
||||
#: core/models.py:52
|
||||
#: core/models.py:55
|
||||
msgid "Saved"
|
||||
msgstr "Saved"
|
||||
|
||||
#: core/models.py:53
|
||||
#: core/models.py:56
|
||||
msgid "Aborted"
|
||||
msgstr "Aborted"
|
||||
|
||||
#: core/models.py:54
|
||||
#: core/models.py:57
|
||||
msgid "Failed to Start"
|
||||
msgstr "Failed to Start"
|
||||
|
||||
#: core/models.py:55
|
||||
#: core/models.py:58
|
||||
msgid "Failed to Stop"
|
||||
msgstr "Failed to Stop"
|
||||
|
||||
#: core/models.py:56
|
||||
#: core/models.py:59
|
||||
msgid "Notification succeeded"
|
||||
msgstr "Notification succeeded"
|
||||
|
||||
#: core/models.py:83
|
||||
#: core/models.py:86
|
||||
msgid "SCREEN_RECORDING"
|
||||
msgstr "SCREEN_RECORDING"
|
||||
|
||||
#: core/models.py:84
|
||||
#: core/models.py:87
|
||||
msgid "TRANSCRIPT"
|
||||
msgstr "TRANSCRIPT"
|
||||
|
||||
#: core/models.py:90
|
||||
#: core/models.py:93
|
||||
msgid "Public Access"
|
||||
msgstr "Public Access"
|
||||
|
||||
#: core/models.py:91
|
||||
#: core/models.py:94
|
||||
msgid "Trusted Access"
|
||||
msgstr "Trusted Access"
|
||||
|
||||
#: core/models.py:92
|
||||
#: core/models.py:95
|
||||
msgid "Restricted Access"
|
||||
msgstr "Restricted Access"
|
||||
|
||||
#: core/models.py:104
|
||||
#: core/models.py:107
|
||||
msgid "id"
|
||||
msgstr "id"
|
||||
|
||||
#: core/models.py:105
|
||||
#: core/models.py:108
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "primary key for the record as UUID"
|
||||
|
||||
#: core/models.py:111
|
||||
#: core/models.py:114
|
||||
msgid "created on"
|
||||
msgstr "created on"
|
||||
|
||||
#: core/models.py:112
|
||||
#: core/models.py:115
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "date and time at which a record was created"
|
||||
|
||||
#: core/models.py:117
|
||||
#: core/models.py:120
|
||||
msgid "updated on"
|
||||
msgstr "updated on"
|
||||
|
||||
#: core/models.py:118
|
||||
#: core/models.py:121
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "date and time at which a record was last updated"
|
||||
|
||||
#: core/models.py:138
|
||||
#: core/models.py:141
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
@@ -137,11 +169,11 @@ msgstr ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
|
||||
#: core/models.py:144
|
||||
#: core/models.py:147
|
||||
msgid "sub"
|
||||
msgstr "sub"
|
||||
|
||||
#: core/models.py:146
|
||||
#: core/models.py:149
|
||||
msgid ""
|
||||
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
|
||||
"characters only."
|
||||
@@ -149,55 +181,55 @@ msgstr ""
|
||||
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
|
||||
"characters only."
|
||||
|
||||
#: core/models.py:154
|
||||
#: core/models.py:157
|
||||
msgid "identity email address"
|
||||
msgstr "identity email address"
|
||||
|
||||
#: core/models.py:159
|
||||
#: core/models.py:162
|
||||
msgid "admin email address"
|
||||
msgstr "admin email address"
|
||||
|
||||
#: core/models.py:161
|
||||
#: core/models.py:164
|
||||
msgid "full name"
|
||||
msgstr "full name"
|
||||
|
||||
#: core/models.py:163
|
||||
#: core/models.py:166
|
||||
msgid "short name"
|
||||
msgstr "short name"
|
||||
|
||||
#: core/models.py:169
|
||||
#: core/models.py:172
|
||||
msgid "language"
|
||||
msgstr "language"
|
||||
|
||||
#: core/models.py:170
|
||||
#: core/models.py:173
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "The language in which the user wants to see the interface."
|
||||
|
||||
#: core/models.py:176
|
||||
#: core/models.py:179
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "The timezone in which the user wants to see times."
|
||||
|
||||
#: core/models.py:179
|
||||
#: core/models.py:182
|
||||
msgid "device"
|
||||
msgstr "device"
|
||||
|
||||
#: core/models.py:181
|
||||
#: core/models.py:184
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Whether the user is a device or a real user."
|
||||
|
||||
#: core/models.py:184
|
||||
#: core/models.py:187
|
||||
msgid "staff status"
|
||||
msgstr "staff status"
|
||||
|
||||
#: core/models.py:186
|
||||
#: core/models.py:189
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Whether the user can log into this admin site."
|
||||
|
||||
#: core/models.py:189
|
||||
#: core/models.py:192
|
||||
msgid "active"
|
||||
msgstr "active"
|
||||
|
||||
#: core/models.py:192
|
||||
#: core/models.py:195
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
@@ -205,63 +237,63 @@ msgstr ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
|
||||
#: core/models.py:205
|
||||
#: core/models.py:208
|
||||
msgid "user"
|
||||
msgstr "user"
|
||||
|
||||
#: core/models.py:206
|
||||
#: core/models.py:209
|
||||
msgid "users"
|
||||
msgstr "users"
|
||||
|
||||
#: core/models.py:265
|
||||
#: core/models.py:268
|
||||
msgid "Resource"
|
||||
msgstr "Resource"
|
||||
|
||||
#: core/models.py:266
|
||||
#: core/models.py:269
|
||||
msgid "Resources"
|
||||
msgstr "Resources"
|
||||
|
||||
#: core/models.py:320
|
||||
#: core/models.py:323
|
||||
msgid "Resource access"
|
||||
msgstr "Resource access"
|
||||
|
||||
#: core/models.py:321
|
||||
#: core/models.py:324
|
||||
msgid "Resource accesses"
|
||||
msgstr "Resource accesses"
|
||||
|
||||
#: core/models.py:327
|
||||
#: core/models.py:330
|
||||
msgid "Resource access with this User and Resource already exists."
|
||||
msgstr "Resource access with this User and Resource already exists."
|
||||
|
||||
#: core/models.py:383
|
||||
#: core/models.py:386
|
||||
msgid "Visio room configuration"
|
||||
msgstr "Visio room configuration"
|
||||
|
||||
#: core/models.py:384
|
||||
#: core/models.py:387
|
||||
msgid "Values for Visio parameters to configure the room."
|
||||
msgstr "Values for Visio parameters to configure the room."
|
||||
|
||||
#: core/models.py:391
|
||||
#: core/models.py:394
|
||||
msgid "Room PIN code"
|
||||
msgstr "Room PIN code"
|
||||
|
||||
#: core/models.py:392
|
||||
#: core/models.py:395
|
||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||
msgstr "Unique n-digit code that identifies this room in telephony mode."
|
||||
|
||||
#: core/models.py:398 core/models.py:552
|
||||
#: core/models.py:401 core/models.py:555
|
||||
msgid "Room"
|
||||
msgstr "Room"
|
||||
|
||||
#: core/models.py:399
|
||||
#: core/models.py:402
|
||||
msgid "Rooms"
|
||||
msgstr "Rooms"
|
||||
|
||||
#: core/models.py:563
|
||||
#: core/models.py:566
|
||||
msgid "Worker ID"
|
||||
msgstr "Worker ID"
|
||||
|
||||
#: core/models.py:565
|
||||
#: core/models.py:568
|
||||
msgid ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
@@ -269,42 +301,106 @@ msgstr ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
|
||||
#: core/models.py:573
|
||||
#: core/models.py:576
|
||||
msgid "Recording mode"
|
||||
msgstr "Recording mode"
|
||||
|
||||
#: core/models.py:574
|
||||
#: core/models.py:577
|
||||
msgid "Defines the mode of recording being called."
|
||||
msgstr "Defines the mode of recording being called."
|
||||
|
||||
#: core/models.py:580
|
||||
#: core/models.py:583
|
||||
msgid "Recording"
|
||||
msgstr "Recording"
|
||||
|
||||
#: core/models.py:581
|
||||
#: core/models.py:584
|
||||
msgid "Recordings"
|
||||
msgstr "Recordings"
|
||||
|
||||
#: core/models.py:689
|
||||
#: core/models.py:692
|
||||
msgid "Recording/user relation"
|
||||
msgstr "Recording/user relation"
|
||||
|
||||
#: core/models.py:690
|
||||
#: core/models.py:693
|
||||
msgid "Recording/user relations"
|
||||
msgstr "Recording/user relations"
|
||||
|
||||
#: core/models.py:696
|
||||
#: core/models.py:699
|
||||
msgid "This user is already in this recording."
|
||||
msgstr "This user is already in this recording."
|
||||
|
||||
#: core/models.py:702
|
||||
#: core/models.py:705
|
||||
msgid "This team is already in this recording."
|
||||
msgstr "This team is already in this recording."
|
||||
|
||||
#: core/models.py:708
|
||||
#: core/models.py:711
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Either user or team must be set, not both."
|
||||
|
||||
#: core/models.py:728
|
||||
#, fuzzy
|
||||
#| msgid "created on"
|
||||
msgid "Create rooms"
|
||||
msgstr "Create rooms"
|
||||
|
||||
#: core/models.py:729
|
||||
msgid "List rooms"
|
||||
msgstr "List rooms"
|
||||
|
||||
#: core/models.py:730
|
||||
msgid "Retrieve room details"
|
||||
msgstr "Retrieve room details"
|
||||
|
||||
#: core/models.py:731
|
||||
#, fuzzy
|
||||
#| msgid "updated on"
|
||||
msgid "Update rooms"
|
||||
msgstr "Update rooms"
|
||||
|
||||
#: core/models.py:732
|
||||
msgid "Delete rooms"
|
||||
msgstr "Delete rooms"
|
||||
|
||||
#: core/models.py:745
|
||||
msgid "Application name"
|
||||
msgstr "Application name"
|
||||
|
||||
#: core/models.py:746
|
||||
msgid "Descriptive name for this application."
|
||||
msgstr "Descriptive name for this application."
|
||||
|
||||
#: core/models.py:756
|
||||
msgid "Hashed on Save. Copy it now if this is a new secret."
|
||||
msgstr "Hashed on Save. Copy it now if this is a new secret."
|
||||
|
||||
#: core/models.py:767
|
||||
msgid "Application"
|
||||
msgstr "Application"
|
||||
|
||||
#: core/models.py:768
|
||||
msgid "Applications"
|
||||
msgstr "Applications"
|
||||
|
||||
#: core/models.py:791
|
||||
msgid "Enter a valid domain"
|
||||
msgstr "Enter a valid domain"
|
||||
|
||||
#: core/models.py:794
|
||||
msgid "Domain"
|
||||
msgstr "Domain"
|
||||
|
||||
#: core/models.py:795
|
||||
msgid "Email domain this application can act on behalf of."
|
||||
msgstr "Email domain this application can act on behalf of."
|
||||
|
||||
#: core/models.py:807
|
||||
msgid "Application domain"
|
||||
msgstr "Application domain"
|
||||
|
||||
#: core/models.py:808
|
||||
msgid "Application domains"
|
||||
msgstr "Application domains"
|
||||
|
||||
#: core/recording/event/notification.py:94
|
||||
msgid "Your recording is ready"
|
||||
msgstr "Your recording is ready"
|
||||
@@ -433,18 +529,18 @@ msgstr ""
|
||||
" If you have any questions or need assistance, please contact our support "
|
||||
"team at %(support_email)s. "
|
||||
|
||||
#: meet/settings.py:163
|
||||
#: meet/settings.py:167
|
||||
msgid "English"
|
||||
msgstr "English"
|
||||
|
||||
#: meet/settings.py:164
|
||||
#: meet/settings.py:168
|
||||
msgid "French"
|
||||
msgstr "French"
|
||||
|
||||
#: meet/settings.py:165
|
||||
#: meet/settings.py:169
|
||||
msgid "Dutch"
|
||||
msgstr "Dutch"
|
||||
|
||||
#: meet/settings.py:166
|
||||
#: meet/settings.py:170
|
||||
msgid "German"
|
||||
msgstr "German"
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-07-11 11:33+0000\n"
|
||||
"POT-Creation-Date: 2025-11-13 16:21+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: antoine.lebaud@mail.numerique.gouv.fr\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -17,123 +17,155 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: core/admin.py:26
|
||||
#: core/admin.py:29
|
||||
msgid "Personal info"
|
||||
msgstr "Informations personnelles"
|
||||
|
||||
#: core/admin.py:39
|
||||
#: core/admin.py:42
|
||||
msgid "Permissions"
|
||||
msgstr "Permissions"
|
||||
|
||||
#: core/admin.py:51
|
||||
#: core/admin.py:54
|
||||
msgid "Important dates"
|
||||
msgstr "Dates importantes"
|
||||
|
||||
#: core/admin.py:147
|
||||
#: core/admin.py:128 core/admin.py:228
|
||||
msgid "No owner"
|
||||
msgstr "Pas de propriétaire"
|
||||
|
||||
#: core/admin.py:150
|
||||
#: core/admin.py:131 core/admin.py:231
|
||||
msgid "Multiple owners"
|
||||
msgstr "Plusieurs propriétaires"
|
||||
|
||||
#: core/api/serializers.py:67
|
||||
#: core/admin.py:143
|
||||
msgid "Resend notification to external service"
|
||||
msgstr "Renvoyer la notification au service externe"
|
||||
|
||||
#: core/admin.py:166
|
||||
#, python-format
|
||||
msgid "Failed to notify for recording %(id)s"
|
||||
msgstr "Échec de la notification pour l’enregistrement %(id)s"
|
||||
|
||||
#: core/admin.py:174
|
||||
#, python-format
|
||||
msgid "Failed to notify for recording %(id)s: %(error)s"
|
||||
msgstr "Échec de la notification pour l’enregistrement %(id)s : %(error)s"
|
||||
|
||||
#: core/admin.py:182
|
||||
#, python-format
|
||||
msgid "Successfully sent notifications for %(count)s recording(s)."
|
||||
msgstr "Notifications envoyées avec succès pour %(count)s enregistrement(s)."
|
||||
|
||||
#: core/admin.py:190
|
||||
#, python-format
|
||||
msgid "Skipped %(count)s expired recording(s)."
|
||||
msgstr "%(count)s enregistrement(s) expiré(s) ignoré(s)."
|
||||
|
||||
#: core/admin.py:294
|
||||
msgid "No scopes"
|
||||
msgstr "Aucun scopes"
|
||||
|
||||
#: core/admin.py:296
|
||||
msgid "Scopes"
|
||||
msgstr "Scopes"
|
||||
|
||||
#: core/api/serializers.py:68
|
||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||
msgstr ""
|
||||
"Vous devez être administrateur ou propriétaire d'une salle pour y ajouter "
|
||||
"des accès."
|
||||
|
||||
#: core/models.py:31
|
||||
#: core/models.py:34
|
||||
msgid "Member"
|
||||
msgstr "Membre"
|
||||
|
||||
#: core/models.py:32
|
||||
#: core/models.py:35
|
||||
msgid "Administrator"
|
||||
msgstr "Administrateur"
|
||||
|
||||
#: core/models.py:33
|
||||
#: core/models.py:36
|
||||
msgid "Owner"
|
||||
msgstr "Propriétaire"
|
||||
|
||||
#: core/models.py:49
|
||||
#: core/models.py:52
|
||||
msgid "Initiated"
|
||||
msgstr "Initié"
|
||||
|
||||
#: core/models.py:50
|
||||
#: core/models.py:53
|
||||
msgid "Active"
|
||||
msgstr "Actif"
|
||||
|
||||
#: core/models.py:51
|
||||
#: core/models.py:54
|
||||
msgid "Stopped"
|
||||
msgstr "Arrêté"
|
||||
|
||||
#: core/models.py:52
|
||||
#: core/models.py:55
|
||||
msgid "Saved"
|
||||
msgstr "Enregistré"
|
||||
|
||||
#: core/models.py:53
|
||||
#: core/models.py:56
|
||||
msgid "Aborted"
|
||||
msgstr "Abandonné"
|
||||
|
||||
#: core/models.py:54
|
||||
#: core/models.py:57
|
||||
msgid "Failed to Start"
|
||||
msgstr "Échec au démarrage"
|
||||
|
||||
#: core/models.py:55
|
||||
#: core/models.py:58
|
||||
msgid "Failed to Stop"
|
||||
msgstr "Échec à l'arrêt"
|
||||
|
||||
#: core/models.py:56
|
||||
#: core/models.py:59
|
||||
msgid "Notification succeeded"
|
||||
msgstr "Notification réussie"
|
||||
|
||||
#: core/models.py:83
|
||||
#: core/models.py:86
|
||||
msgid "SCREEN_RECORDING"
|
||||
msgstr "ENREGISTREMENT_ÉCRAN"
|
||||
|
||||
#: core/models.py:84
|
||||
#: core/models.py:87
|
||||
msgid "TRANSCRIPT"
|
||||
msgstr "TRANSCRIPTION"
|
||||
|
||||
#: core/models.py:90
|
||||
#: core/models.py:93
|
||||
msgid "Public Access"
|
||||
msgstr "Accès public"
|
||||
|
||||
#: core/models.py:91
|
||||
#: core/models.py:94
|
||||
msgid "Trusted Access"
|
||||
msgstr "Accès de confiance"
|
||||
|
||||
#: core/models.py:92
|
||||
#: core/models.py:95
|
||||
msgid "Restricted Access"
|
||||
msgstr "Accès restreint"
|
||||
|
||||
#: core/models.py:104
|
||||
#: core/models.py:107
|
||||
msgid "id"
|
||||
msgstr "id"
|
||||
|
||||
#: core/models.py:105
|
||||
#: core/models.py:108
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "clé primaire pour l'enregistrement sous forme d'UUID"
|
||||
|
||||
#: core/models.py:111
|
||||
#: core/models.py:114
|
||||
msgid "created on"
|
||||
msgstr "créé le"
|
||||
|
||||
#: core/models.py:112
|
||||
#: core/models.py:115
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "date et heure auxquelles un enregistrement a été créé"
|
||||
|
||||
#: core/models.py:117
|
||||
#: core/models.py:120
|
||||
msgid "updated on"
|
||||
msgstr "mis à jour le"
|
||||
|
||||
#: core/models.py:118
|
||||
#: core/models.py:121
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr ""
|
||||
"date et heure auxquelles un enregistrement a été mis à jour pour la dernière "
|
||||
"fois"
|
||||
|
||||
#: core/models.py:138
|
||||
#: core/models.py:141
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
@@ -141,11 +173,11 @@ msgstr ""
|
||||
"Entrez un sub valide. Cette valeur ne peut contenir que des lettres, des "
|
||||
"chiffres et les caractères @/./+/-/_."
|
||||
|
||||
#: core/models.py:144
|
||||
#: core/models.py:147
|
||||
msgid "sub"
|
||||
msgstr "sub"
|
||||
|
||||
#: core/models.py:146
|
||||
#: core/models.py:149
|
||||
msgid ""
|
||||
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
|
||||
"characters only."
|
||||
@@ -153,55 +185,55 @@ msgstr ""
|
||||
"Obligatoire. 255 caractères ou moins. Lettres, chiffres et caractères @/./"
|
||||
"+/-/_ uniquement."
|
||||
|
||||
#: core/models.py:154
|
||||
#: core/models.py:157
|
||||
msgid "identity email address"
|
||||
msgstr "adresse e-mail d'identité"
|
||||
|
||||
#: core/models.py:159
|
||||
#: core/models.py:162
|
||||
msgid "admin email address"
|
||||
msgstr "adresse e-mail d'administrateur"
|
||||
|
||||
#: core/models.py:161
|
||||
#: core/models.py:164
|
||||
msgid "full name"
|
||||
msgstr "nom complet"
|
||||
|
||||
#: core/models.py:163
|
||||
#: core/models.py:166
|
||||
msgid "short name"
|
||||
msgstr "nom court"
|
||||
|
||||
#: core/models.py:169
|
||||
#: core/models.py:172
|
||||
msgid "language"
|
||||
msgstr "langue"
|
||||
|
||||
#: core/models.py:170
|
||||
#: core/models.py:173
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "La langue dans laquelle l'utilisateur souhaite voir l'interface."
|
||||
|
||||
#: core/models.py:176
|
||||
#: core/models.py:179
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Le fuseau horaire dans lequel l'utilisateur souhaite voir les heures."
|
||||
|
||||
#: core/models.py:179
|
||||
#: core/models.py:182
|
||||
msgid "device"
|
||||
msgstr "appareil"
|
||||
|
||||
#: core/models.py:181
|
||||
#: core/models.py:184
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Si l'utilisateur est un appareil ou un utilisateur réel."
|
||||
|
||||
#: core/models.py:184
|
||||
#: core/models.py:187
|
||||
msgid "staff status"
|
||||
msgstr "statut du personnel"
|
||||
|
||||
#: core/models.py:186
|
||||
#: core/models.py:189
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Si l'utilisateur peut se connecter à ce site d'administration."
|
||||
|
||||
#: core/models.py:189
|
||||
#: core/models.py:192
|
||||
msgid "active"
|
||||
msgstr "actif"
|
||||
|
||||
#: core/models.py:192
|
||||
#: core/models.py:195
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
@@ -209,65 +241,65 @@ msgstr ""
|
||||
"Si cet utilisateur doit être traité comme actif. Désélectionnez cette option "
|
||||
"au lieu de supprimer des comptes."
|
||||
|
||||
#: core/models.py:205
|
||||
#: core/models.py:208
|
||||
msgid "user"
|
||||
msgstr "utilisateur"
|
||||
|
||||
#: core/models.py:206
|
||||
#: core/models.py:209
|
||||
msgid "users"
|
||||
msgstr "utilisateurs"
|
||||
|
||||
#: core/models.py:265
|
||||
#: core/models.py:268
|
||||
msgid "Resource"
|
||||
msgstr "Ressource"
|
||||
|
||||
#: core/models.py:266
|
||||
#: core/models.py:269
|
||||
msgid "Resources"
|
||||
msgstr "Ressources"
|
||||
|
||||
#: core/models.py:320
|
||||
#: core/models.py:323
|
||||
msgid "Resource access"
|
||||
msgstr "Accès aux ressources"
|
||||
|
||||
#: core/models.py:321
|
||||
#: core/models.py:324
|
||||
msgid "Resource accesses"
|
||||
msgstr "Accès aux ressources"
|
||||
|
||||
#: core/models.py:327
|
||||
#: core/models.py:330
|
||||
msgid "Resource access with this User and Resource already exists."
|
||||
msgstr ""
|
||||
"L'accès à la ressource avec cet utilisateur et cette ressource existe déjà."
|
||||
|
||||
#: core/models.py:383
|
||||
#: core/models.py:386
|
||||
msgid "Visio room configuration"
|
||||
msgstr "Configuration de la salle de visioconférence"
|
||||
|
||||
#: core/models.py:384
|
||||
#: core/models.py:387
|
||||
msgid "Values for Visio parameters to configure the room."
|
||||
msgstr "Valeurs des paramètres de visioconférence pour configurer la salle."
|
||||
|
||||
#: core/models.py:391
|
||||
#: core/models.py:394
|
||||
msgid "Room PIN code"
|
||||
msgstr "Code PIN de la salle"
|
||||
|
||||
#: core/models.py:392
|
||||
#: core/models.py:395
|
||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||
msgstr ""
|
||||
"Code unique à n chiffres qui identifie cette salle en mode téléphonique."
|
||||
|
||||
#: core/models.py:398 core/models.py:552
|
||||
#: core/models.py:401 core/models.py:555
|
||||
msgid "Room"
|
||||
msgstr "Salle"
|
||||
|
||||
#: core/models.py:399
|
||||
#: core/models.py:402
|
||||
msgid "Rooms"
|
||||
msgstr "Salles"
|
||||
|
||||
#: core/models.py:563
|
||||
#: core/models.py:566
|
||||
msgid "Worker ID"
|
||||
msgstr "ID du Worker"
|
||||
|
||||
#: core/models.py:565
|
||||
#: core/models.py:568
|
||||
msgid ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
@@ -275,42 +307,102 @@ msgstr ""
|
||||
"Entrez un identifiant pour l'enregistrement du Worker. Cet identifiant est "
|
||||
"conservé même lorsque le Worker s'arrête, permettant un suivi facile."
|
||||
|
||||
#: core/models.py:573
|
||||
#: core/models.py:576
|
||||
msgid "Recording mode"
|
||||
msgstr "Mode d'enregistrement"
|
||||
|
||||
#: core/models.py:574
|
||||
#: core/models.py:577
|
||||
msgid "Defines the mode of recording being called."
|
||||
msgstr "Définit le mode d'enregistrement appelé."
|
||||
|
||||
#: core/models.py:580
|
||||
#: core/models.py:583
|
||||
msgid "Recording"
|
||||
msgstr "Enregistrement"
|
||||
|
||||
#: core/models.py:581
|
||||
#: core/models.py:584
|
||||
msgid "Recordings"
|
||||
msgstr "Enregistrements"
|
||||
|
||||
#: core/models.py:689
|
||||
#: core/models.py:692
|
||||
msgid "Recording/user relation"
|
||||
msgstr "Relation enregistrement/utilisateur"
|
||||
|
||||
#: core/models.py:690
|
||||
#: core/models.py:693
|
||||
msgid "Recording/user relations"
|
||||
msgstr "Relations enregistrement/utilisateur"
|
||||
|
||||
#: core/models.py:696
|
||||
#: core/models.py:699
|
||||
msgid "This user is already in this recording."
|
||||
msgstr "Cet utilisateur est déjà dans cet enregistrement."
|
||||
|
||||
#: core/models.py:702
|
||||
#: core/models.py:705
|
||||
msgid "This team is already in this recording."
|
||||
msgstr "Cette équipe est déjà dans cet enregistrement."
|
||||
|
||||
#: core/models.py:708
|
||||
#: core/models.py:711
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Soit l'utilisateur, soit l'équipe doit être défini, pas les deux."
|
||||
|
||||
#: core/models.py:728
|
||||
msgid "Create rooms"
|
||||
msgstr "Créer des salles"
|
||||
|
||||
#: core/models.py:729
|
||||
msgid "List rooms"
|
||||
msgstr "Lister les salles"
|
||||
|
||||
#: core/models.py:730
|
||||
msgid "Retrieve room details"
|
||||
msgstr "Afficher les détails d’une salle"
|
||||
|
||||
#: core/models.py:731
|
||||
msgid "Update rooms"
|
||||
msgstr "Mettre à jour les salles"
|
||||
|
||||
#: core/models.py:732
|
||||
msgid "Delete rooms"
|
||||
msgstr "Supprimer les salles"
|
||||
|
||||
#: core/models.py:745
|
||||
msgid "Application name"
|
||||
msgstr "Nom de l’application"
|
||||
|
||||
#: core/models.py:746
|
||||
msgid "Descriptive name for this application."
|
||||
msgstr "Nom descriptif de cette application."
|
||||
|
||||
#: core/models.py:756
|
||||
msgid "Hashed on Save. Copy it now if this is a new secret."
|
||||
msgstr "Haché lors de l’enregistrement. Copiez-le maintenant s’il s’agit d’un nouveau secret."
|
||||
|
||||
#: core/models.py:767
|
||||
msgid "Application"
|
||||
msgstr "Application"
|
||||
|
||||
#: core/models.py:768
|
||||
msgid "Applications"
|
||||
msgstr "Applications"
|
||||
|
||||
#: core/models.py:791
|
||||
msgid "Enter a valid domain"
|
||||
msgstr "Saisissez un domaine valide"
|
||||
|
||||
#: core/models.py:794
|
||||
msgid "Domain"
|
||||
msgstr "Domaine"
|
||||
|
||||
#: core/models.py:795
|
||||
msgid "Email domain this application can act on behalf of."
|
||||
msgstr "Domaine de messagerie au nom duquel cette application peut agir."
|
||||
|
||||
#: core/models.py:807
|
||||
msgid "Application domain"
|
||||
msgstr "Domaine d’application"
|
||||
|
||||
#: core/models.py:808
|
||||
msgid "Application domains"
|
||||
msgstr "Domaines d’application"
|
||||
|
||||
#: core/recording/event/notification.py:94
|
||||
msgid "Your recording is ready"
|
||||
msgstr "Votre enregistrement est prêt"
|
||||
@@ -401,7 +493,8 @@ msgid ""
|
||||
" Sharing the recording via link is not yet available. Only organizers can "
|
||||
"download it. "
|
||||
msgstr ""
|
||||
"Le partage de l'enregistrement via lien n'est pas encore disponible. Seuls les organisateurs peuvent le télécharger."
|
||||
"Le partage de l'enregistrement via lien n'est pas encore disponible. Seuls "
|
||||
"les organisateurs peuvent le télécharger."
|
||||
|
||||
#: core/templates/mail/html/screen_recording.html:206
|
||||
#: core/templates/mail/text/screen_recording.txt:11
|
||||
@@ -438,18 +531,18 @@ msgstr ""
|
||||
" Si vous avez des questions ou besoin d'assistance, veuillez contacter notre "
|
||||
"équipe d'assistance à %(support_email)s. "
|
||||
|
||||
#: meet/settings.py:163
|
||||
#: meet/settings.py:167
|
||||
msgid "English"
|
||||
msgstr "Anglais"
|
||||
|
||||
#: meet/settings.py:164
|
||||
#: meet/settings.py:168
|
||||
msgid "French"
|
||||
msgstr "Français"
|
||||
|
||||
#: meet/settings.py:165
|
||||
#: meet/settings.py:169
|
||||
msgid "Dutch"
|
||||
msgstr "Néerlandais"
|
||||
|
||||
#: meet/settings.py:166
|
||||
#: meet/settings.py:170
|
||||
msgid "German"
|
||||
msgstr "Allemand"
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-07-11 11:33+0000\n"
|
||||
"POT-Creation-Date: 2025-11-13 16:21+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -17,120 +17,152 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: core/admin.py:26
|
||||
#: core/admin.py:29
|
||||
msgid "Personal info"
|
||||
msgstr "Persoonlijke informatie"
|
||||
|
||||
#: core/admin.py:39
|
||||
#: core/admin.py:42
|
||||
msgid "Permissions"
|
||||
msgstr "Rechten"
|
||||
|
||||
#: core/admin.py:51
|
||||
#: core/admin.py:54
|
||||
msgid "Important dates"
|
||||
msgstr "Belangrijke datums"
|
||||
|
||||
#: core/admin.py:147
|
||||
#: core/admin.py:128 core/admin.py:228
|
||||
msgid "No owner"
|
||||
msgstr "Geen eigenaar"
|
||||
|
||||
#: core/admin.py:150
|
||||
#: core/admin.py:131 core/admin.py:231
|
||||
msgid "Multiple owners"
|
||||
msgstr "Meerdere eigenaren"
|
||||
|
||||
#: core/api/serializers.py:67
|
||||
#: core/admin.py:143
|
||||
msgid "Resend notification to external service"
|
||||
msgstr "Melding opnieuw verzenden naar externe dienst"
|
||||
|
||||
#: core/admin.py:166
|
||||
#, python-format
|
||||
msgid "Failed to notify for recording %(id)s"
|
||||
msgstr "Melding voor opname %(id)s mislukt"
|
||||
|
||||
#: core/admin.py:174
|
||||
#, python-format
|
||||
msgid "Failed to notify for recording %(id)s: %(error)s"
|
||||
msgstr "Melding voor opname %(id)s mislukt: %(error)s"
|
||||
|
||||
#: core/admin.py:182
|
||||
#, python-format
|
||||
msgid "Successfully sent notifications for %(count)s recording(s)."
|
||||
msgstr "Meldingen succesvol verzonden voor %(count)s opname(n)."
|
||||
|
||||
#: core/admin.py:190
|
||||
#, python-format
|
||||
msgid "Skipped %(count)s expired recording(s)."
|
||||
msgstr "%(count)s verlopen opname(n) overgeslagen."
|
||||
|
||||
#: core/admin.py:294
|
||||
msgid "No scopes"
|
||||
msgstr "Geen scopes"
|
||||
|
||||
#: core/admin.py:296
|
||||
msgid "Scopes"
|
||||
msgstr "Scopes"
|
||||
|
||||
#: core/api/serializers.py:68
|
||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||
msgstr ""
|
||||
"Je moet beheerder of eigenaar van een ruimte zijn om toegang toe te voegen."
|
||||
|
||||
#: core/models.py:31
|
||||
#: core/models.py:34
|
||||
msgid "Member"
|
||||
msgstr "Lid"
|
||||
|
||||
#: core/models.py:32
|
||||
#: core/models.py:35
|
||||
msgid "Administrator"
|
||||
msgstr "Beheerder"
|
||||
|
||||
#: core/models.py:33
|
||||
#: core/models.py:36
|
||||
msgid "Owner"
|
||||
msgstr "Eigenaar"
|
||||
|
||||
#: core/models.py:49
|
||||
#: core/models.py:52
|
||||
msgid "Initiated"
|
||||
msgstr "Gestart"
|
||||
|
||||
#: core/models.py:50
|
||||
#: core/models.py:53
|
||||
msgid "Active"
|
||||
msgstr "Actief"
|
||||
|
||||
#: core/models.py:51
|
||||
#: core/models.py:54
|
||||
msgid "Stopped"
|
||||
msgstr "Gestopt"
|
||||
|
||||
#: core/models.py:52
|
||||
#: core/models.py:55
|
||||
msgid "Saved"
|
||||
msgstr "Opgeslagen"
|
||||
|
||||
#: core/models.py:53
|
||||
#: core/models.py:56
|
||||
msgid "Aborted"
|
||||
msgstr "Afgebroken"
|
||||
|
||||
#: core/models.py:54
|
||||
#: core/models.py:57
|
||||
msgid "Failed to Start"
|
||||
msgstr "Starten mislukt"
|
||||
|
||||
#: core/models.py:55
|
||||
#: core/models.py:58
|
||||
msgid "Failed to Stop"
|
||||
msgstr "Stoppen mislukt"
|
||||
|
||||
#: core/models.py:56
|
||||
#: core/models.py:59
|
||||
msgid "Notification succeeded"
|
||||
msgstr "Notificatie geslaagd"
|
||||
|
||||
#: core/models.py:83
|
||||
#: core/models.py:86
|
||||
msgid "SCREEN_RECORDING"
|
||||
msgstr "SCHERM_OPNAME"
|
||||
|
||||
#: core/models.py:84
|
||||
#: core/models.py:87
|
||||
msgid "TRANSCRIPT"
|
||||
msgstr "TRANSCRIPT"
|
||||
|
||||
#: core/models.py:90
|
||||
#: core/models.py:93
|
||||
msgid "Public Access"
|
||||
msgstr "Openbare toegang"
|
||||
|
||||
#: core/models.py:91
|
||||
#: core/models.py:94
|
||||
msgid "Trusted Access"
|
||||
msgstr "Vertrouwde toegang"
|
||||
|
||||
#: core/models.py:92
|
||||
#: core/models.py:95
|
||||
msgid "Restricted Access"
|
||||
msgstr "Beperkte toegang"
|
||||
|
||||
#: core/models.py:104
|
||||
#: core/models.py:107
|
||||
msgid "id"
|
||||
msgstr "id"
|
||||
|
||||
#: core/models.py:105
|
||||
#: core/models.py:108
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "primaire sleutel voor het record als UUID"
|
||||
|
||||
#: core/models.py:111
|
||||
#: core/models.py:114
|
||||
msgid "created on"
|
||||
msgstr "aangemaakt op"
|
||||
|
||||
#: core/models.py:112
|
||||
#: core/models.py:115
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "datum en tijd waarop een record werd aangemaakt"
|
||||
|
||||
#: core/models.py:117
|
||||
#: core/models.py:120
|
||||
msgid "updated on"
|
||||
msgstr "bijgewerkt op"
|
||||
|
||||
#: core/models.py:118
|
||||
#: core/models.py:121
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "datum en tijd waarop een record voor het laatst werd bijgewerkt"
|
||||
|
||||
#: core/models.py:138
|
||||
#: core/models.py:141
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
@@ -138,66 +170,66 @@ msgstr ""
|
||||
"Voer een geldige sub in. Deze waarde mag alleen letters, cijfers en @/./+/-/"
|
||||
"_ tekens bevatten."
|
||||
|
||||
#: core/models.py:144
|
||||
#: core/models.py:147
|
||||
msgid "sub"
|
||||
msgstr "sub"
|
||||
|
||||
#: core/models.py:146
|
||||
#: core/models.py:149
|
||||
msgid ""
|
||||
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
|
||||
"characters only."
|
||||
msgstr ""
|
||||
"Vereist. 255 tekens of minder. Alleen letters, cijfers en @/./+/-/_ tekens."
|
||||
|
||||
#: core/models.py:154
|
||||
#: core/models.py:157
|
||||
msgid "identity email address"
|
||||
msgstr "identiteit e-mailadres"
|
||||
|
||||
#: core/models.py:159
|
||||
#: core/models.py:162
|
||||
msgid "admin email address"
|
||||
msgstr "beheerder e-mailadres"
|
||||
|
||||
#: core/models.py:161
|
||||
#: core/models.py:164
|
||||
msgid "full name"
|
||||
msgstr "volledige naam"
|
||||
|
||||
#: core/models.py:163
|
||||
#: core/models.py:166
|
||||
msgid "short name"
|
||||
msgstr "korte naam"
|
||||
|
||||
#: core/models.py:169
|
||||
#: core/models.py:172
|
||||
msgid "language"
|
||||
msgstr "taal"
|
||||
|
||||
#: core/models.py:170
|
||||
#: core/models.py:173
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "De taal waarin de gebruiker de interface wil zien."
|
||||
|
||||
#: core/models.py:176
|
||||
#: core/models.py:179
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "De tijdzone waarin de gebruiker tijden wil zien."
|
||||
|
||||
#: core/models.py:179
|
||||
#: core/models.py:182
|
||||
msgid "device"
|
||||
msgstr "apparaat"
|
||||
|
||||
#: core/models.py:181
|
||||
#: core/models.py:184
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Of de gebruiker een apparaat is of een echte gebruiker."
|
||||
|
||||
#: core/models.py:184
|
||||
#: core/models.py:187
|
||||
msgid "staff status"
|
||||
msgstr "personeelsstatus"
|
||||
|
||||
#: core/models.py:186
|
||||
#: core/models.py:189
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Of de gebruiker kan inloggen op deze beheersite."
|
||||
|
||||
#: core/models.py:189
|
||||
#: core/models.py:192
|
||||
msgid "active"
|
||||
msgstr "actief"
|
||||
|
||||
#: core/models.py:192
|
||||
#: core/models.py:195
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
@@ -205,64 +237,64 @@ msgstr ""
|
||||
"Of deze gebruiker als actief moet worden behandeld. Deselecteer dit in "
|
||||
"plaats van accounts te verwijderen."
|
||||
|
||||
#: core/models.py:205
|
||||
#: core/models.py:208
|
||||
msgid "user"
|
||||
msgstr "gebruiker"
|
||||
|
||||
#: core/models.py:206
|
||||
#: core/models.py:209
|
||||
msgid "users"
|
||||
msgstr "gebruikers"
|
||||
|
||||
#: core/models.py:265
|
||||
#: core/models.py:268
|
||||
msgid "Resource"
|
||||
msgstr "Bron"
|
||||
|
||||
#: core/models.py:266
|
||||
#: core/models.py:269
|
||||
msgid "Resources"
|
||||
msgstr "Bronnen"
|
||||
|
||||
#: core/models.py:320
|
||||
#: core/models.py:323
|
||||
msgid "Resource access"
|
||||
msgstr "Brontoegang"
|
||||
|
||||
#: core/models.py:321
|
||||
#: core/models.py:324
|
||||
msgid "Resource accesses"
|
||||
msgstr "Brontoegangsrechten"
|
||||
|
||||
#: core/models.py:327
|
||||
#: core/models.py:330
|
||||
msgid "Resource access with this User and Resource already exists."
|
||||
msgstr "Brontoegang met deze gebruiker en bron bestaat al."
|
||||
|
||||
#: core/models.py:383
|
||||
#: core/models.py:386
|
||||
msgid "Visio room configuration"
|
||||
msgstr "Visio-ruimteconfiguratie"
|
||||
|
||||
#: core/models.py:384
|
||||
#: core/models.py:387
|
||||
msgid "Values for Visio parameters to configure the room."
|
||||
msgstr "Waarden voor Visio-parameters om de ruimte te configureren."
|
||||
|
||||
#: core/models.py:391
|
||||
#: core/models.py:394
|
||||
msgid "Room PIN code"
|
||||
msgstr "Pincode van de kamer"
|
||||
|
||||
#: core/models.py:392
|
||||
#: core/models.py:395
|
||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||
msgstr ""
|
||||
"Unieke n-cijferige code die deze kamer identificeert in telefonie-modus."
|
||||
|
||||
#: core/models.py:398 core/models.py:552
|
||||
#: core/models.py:401 core/models.py:555
|
||||
msgid "Room"
|
||||
msgstr "Ruimte"
|
||||
|
||||
#: core/models.py:399
|
||||
#: core/models.py:402
|
||||
msgid "Rooms"
|
||||
msgstr "Ruimtes"
|
||||
|
||||
#: core/models.py:563
|
||||
#: core/models.py:566
|
||||
msgid "Worker ID"
|
||||
msgstr "Worker ID"
|
||||
|
||||
#: core/models.py:565
|
||||
#: core/models.py:568
|
||||
msgid ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
@@ -270,42 +302,102 @@ msgstr ""
|
||||
"Voer een identificatie in voor de worker-opname. Deze ID blijft behouden, "
|
||||
"zelfs wanneer de worker stopt, waardoor eenvoudige tracking mogelijk is."
|
||||
|
||||
#: core/models.py:573
|
||||
#: core/models.py:576
|
||||
msgid "Recording mode"
|
||||
msgstr "Opnamemodus"
|
||||
|
||||
#: core/models.py:574
|
||||
#: core/models.py:577
|
||||
msgid "Defines the mode of recording being called."
|
||||
msgstr "Definieert de modus van opname die wordt aangeroepen."
|
||||
|
||||
#: core/models.py:580
|
||||
#: core/models.py:583
|
||||
msgid "Recording"
|
||||
msgstr "Opname"
|
||||
|
||||
#: core/models.py:581
|
||||
#: core/models.py:584
|
||||
msgid "Recordings"
|
||||
msgstr "Opnames"
|
||||
|
||||
#: core/models.py:689
|
||||
#: core/models.py:692
|
||||
msgid "Recording/user relation"
|
||||
msgstr "Opname/gebruiker-relatie"
|
||||
|
||||
#: core/models.py:690
|
||||
#: core/models.py:693
|
||||
msgid "Recording/user relations"
|
||||
msgstr "Opname/gebruiker-relaties"
|
||||
|
||||
#: core/models.py:696
|
||||
#: core/models.py:699
|
||||
msgid "This user is already in this recording."
|
||||
msgstr "Deze gebruiker is al in deze opname."
|
||||
|
||||
#: core/models.py:702
|
||||
#: core/models.py:705
|
||||
msgid "This team is already in this recording."
|
||||
msgstr "Dit team is al in deze opname."
|
||||
|
||||
#: core/models.py:708
|
||||
#: core/models.py:711
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Ofwel gebruiker of team moet worden ingesteld, niet beide."
|
||||
|
||||
#: core/models.py:728
|
||||
msgid "Create rooms"
|
||||
msgstr "Ruimtes aanmaken"
|
||||
|
||||
#: core/models.py:729
|
||||
msgid "List rooms"
|
||||
msgstr "Ruimtes weergeven"
|
||||
|
||||
#: core/models.py:730
|
||||
msgid "Retrieve room details"
|
||||
msgstr "Details van een ruimte ophalen"
|
||||
|
||||
#: core/models.py:731
|
||||
msgid "Update rooms"
|
||||
msgstr "Ruimtes bijwerken"
|
||||
|
||||
#: core/models.py:732
|
||||
msgid "Delete rooms"
|
||||
msgstr "Ruimtes verwijderen"
|
||||
|
||||
#: core/models.py:745
|
||||
msgid "Application name"
|
||||
msgstr "Naam van de applicatie"
|
||||
|
||||
#: core/models.py:746
|
||||
msgid "Descriptive name for this application."
|
||||
msgstr "Beschrijvende naam voor deze applicatie."
|
||||
|
||||
#: core/models.py:756
|
||||
msgid "Hashed on Save. Copy it now if this is a new secret."
|
||||
msgstr "Wordt gehasht bij het opslaan. Kopieer het nu als dit een nieuw geheim is."
|
||||
|
||||
#: core/models.py:767
|
||||
msgid "Application"
|
||||
msgstr "Applicatie"
|
||||
|
||||
#: core/models.py:768
|
||||
msgid "Applications"
|
||||
msgstr "Applicaties"
|
||||
|
||||
#: core/models.py:791
|
||||
msgid "Enter a valid domain"
|
||||
msgstr "Voer een geldig domein in"
|
||||
|
||||
#: core/models.py:794
|
||||
msgid "Domain"
|
||||
msgstr "Domein"
|
||||
|
||||
#: core/models.py:795
|
||||
msgid "Email domain this application can act on behalf of."
|
||||
msgstr "E-maildomein namens welke deze applicatie kan handelen."
|
||||
|
||||
#: core/models.py:807
|
||||
msgid "Application domain"
|
||||
msgstr "Applicatiedomein"
|
||||
|
||||
#: core/models.py:808
|
||||
msgid "Application domains"
|
||||
msgstr "Applicatiedomeinen"
|
||||
|
||||
#: core/recording/event/notification.py:94
|
||||
msgid "Your recording is ready"
|
||||
msgstr "Je opname is klaar"
|
||||
@@ -396,7 +488,8 @@ msgid ""
|
||||
" Sharing the recording via link is not yet available. Only organizers can "
|
||||
"download it. "
|
||||
msgstr ""
|
||||
"Het delen van de opname via een link is nog niet beschikbaar. Alleen organisatoren kunnen deze downloaden."
|
||||
"Het delen van de opname via een link is nog niet beschikbaar. Alleen "
|
||||
"organisatoren kunnen deze downloaden."
|
||||
|
||||
#: core/templates/mail/html/screen_recording.html:206
|
||||
#: core/templates/mail/text/screen_recording.txt:11
|
||||
@@ -433,18 +526,18 @@ msgstr ""
|
||||
" Als je vragen hebt of hulp nodig hebt, neem dan contact op met ons support "
|
||||
"team via %(support_email)s. "
|
||||
|
||||
#: meet/settings.py:163
|
||||
#: meet/settings.py:167
|
||||
msgid "English"
|
||||
msgstr "Engels"
|
||||
|
||||
#: meet/settings.py:164
|
||||
#: meet/settings.py:168
|
||||
msgid "French"
|
||||
msgstr "Frans"
|
||||
|
||||
#: meet/settings.py:165
|
||||
#: meet/settings.py:169
|
||||
msgid "Dutch"
|
||||
msgstr "Nederlands"
|
||||
|
||||
#: meet/settings.py:166
|
||||
#: meet/settings.py:170
|
||||
msgid "German"
|
||||
msgstr "Duits"
|
||||
|
||||
@@ -10,6 +10,8 @@ For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.1/ref/settings/
|
||||
"""
|
||||
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
import json
|
||||
from os import path
|
||||
from socket import gethostbyname, gethostname
|
||||
@@ -404,6 +406,10 @@ class Base(Configuration):
|
||||
default=False,
|
||||
environ_name="OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION",
|
||||
)
|
||||
OIDC_TIMEOUT = values.IntegerValue(
|
||||
5, environ_name="OIDC_TIMEOUT", environ_prefix=None
|
||||
)
|
||||
OIDC_PROXY = values.Value(None, environ_name="OIDC_PROXY", environ_prefix=None)
|
||||
OIDC_RP_SIGN_ALGO = values.Value(
|
||||
"RS256", environ_name="OIDC_RP_SIGN_ALGO", environ_prefix=None
|
||||
)
|
||||
@@ -427,12 +433,16 @@ class Base(Configuration):
|
||||
OIDC_OP_USER_ENDPOINT = values.Value(
|
||||
None, environ_name="OIDC_OP_USER_ENDPOINT", environ_prefix=None
|
||||
)
|
||||
OIDC_OP_INTROSPECTION_ENDPOINT = values.Value(
|
||||
None, environ_name="OIDC_OP_INTROSPECTION_ENDPOINT", environ_prefix=None
|
||||
)
|
||||
OIDC_OP_USER_ENDPOINT_FORMAT = values.Value(
|
||||
"AUTO", environ_name="OIDC_OP_USER_ENDPOINT_FORMAT", environ_prefix=None
|
||||
)
|
||||
OIDC_OP_LOGOUT_ENDPOINT = values.Value(
|
||||
None, environ_name="OIDC_OP_LOGOUT_ENDPOINT", environ_prefix=None
|
||||
)
|
||||
OIDC_OP_URL = values.Value(None, environ_name="OIDC_OP_URL", environ_prefix=None)
|
||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS = values.DictValue(
|
||||
{}, environ_name="OIDC_AUTH_REQUEST_EXTRA_PARAMS", environ_prefix=None
|
||||
)
|
||||
@@ -493,6 +503,45 @@ class Base(Configuration):
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# OIDC Resource Server Backend
|
||||
OIDC_RS_BACKEND_CLASS = "core.external_api.authentication.ResourceServerBackend"
|
||||
OIDC_RS_CLIENT_ID = values.Value(
|
||||
"meet", environ_name="OIDC_RS_CLIENT_ID", environ_prefix=None
|
||||
)
|
||||
OIDC_RS_CLIENT_SECRET = SecretFileValue(
|
||||
None,
|
||||
environ_name="OIDC_RS_CLIENT_SECRET",
|
||||
environ_prefix=None,
|
||||
)
|
||||
OIDC_RS_AUDIENCE_CLAIM = values.Value(
|
||||
default="client_id", environ_name="OIDC_RS_AUDIENCE_CLAIM", environ_prefix=None
|
||||
)
|
||||
OIDC_RS_ENCRYPTION_ENCODING = values.Value(
|
||||
default="A256GCM",
|
||||
environ_name="OIDC_RS_ENCRYPTION_ENCODING",
|
||||
environ_prefix=None,
|
||||
)
|
||||
OIDC_RS_ENCRYPTION_ALGO = values.Value(
|
||||
default="RSA-OAEP", environ_name="OIDC_RS_ENCRYPTION_ALGO", environ_prefix=None
|
||||
)
|
||||
OIDC_RS_SIGNING_ALGO = values.Value(
|
||||
default="ES256", environ_name="OIDC_RS_SIGNING_ALGO", environ_prefix=None
|
||||
)
|
||||
OIDC_RS_SCOPES = values.ListValue(
|
||||
default=["lasuite_meet"],
|
||||
environ_name="OIDC_RS_SCOPES",
|
||||
environ_prefix=None,
|
||||
)
|
||||
OIDC_RS_PRIVATE_KEY_STR = SecretFileValue(
|
||||
environ_name="OIDC_RS_PRIVATE_KEY_STR", environ_prefix=None
|
||||
)
|
||||
OIDC_RS_ENCRYPTION_KEY_TYPE = values.Value(
|
||||
default="RSA", environ_name="OIDC_RS_ENCRYPTION_KEY_TYPE", environ_prefix=None
|
||||
)
|
||||
OIDC_RS_SCOPES_PREFIX = values.Value(
|
||||
default=None, environ_name="OIDC_RS_SCOPES_PREFIX", environ_prefix=None
|
||||
)
|
||||
|
||||
# Video conference configuration
|
||||
LIVEKIT_CONFIGURATION = {
|
||||
"api_key": SecretFileValue(environ_name="LIVEKIT_API_KEY", environ_prefix=None),
|
||||
|
||||
@@ -15,6 +15,7 @@ from drf_spectacular.views import (
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("", include("core.urls")),
|
||||
path("", include("lasuite.oidc_resource_server.urls")),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
|
||||
+31
-31
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "meet"
|
||||
version = "0.1.41"
|
||||
version = "1.0.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -25,38 +25,38 @@ license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"boto3==1.38.42",
|
||||
"Brotli==1.1.0",
|
||||
"brevo-python==1.1.2",
|
||||
"boto3==1.40.69",
|
||||
"Brotli==1.2.0",
|
||||
"brevo-python==1.2.0",
|
||||
"celery[redis]==5.5.3",
|
||||
"django-configurations==2.5.1",
|
||||
"django-cors-headers==4.7.0",
|
||||
"django-countries==7.6.1",
|
||||
"django-lasuite[all]==0.0.10",
|
||||
"django-cors-headers==4.9.0",
|
||||
"django-countries==8.0.0",
|
||||
"django-lasuite[all]==0.0.19",
|
||||
"django-parler==2.3",
|
||||
"redis==5.2.1",
|
||||
"django-redis==6.0.0",
|
||||
"django-storages[s3]==1.14.6",
|
||||
"django-timezone-field>=5.1",
|
||||
"django==5.2.7",
|
||||
"djangorestframework==3.16.0",
|
||||
"drf_spectacular==0.28.0",
|
||||
"django==5.2.9",
|
||||
"djangorestframework==3.16.1",
|
||||
"drf_spectacular==0.29.0",
|
||||
"dockerflow==2024.4.2",
|
||||
"easy_thumbnails==2.10",
|
||||
"easy_thumbnails==2.10.1",
|
||||
"factory_boy==3.3.3",
|
||||
"gunicorn==23.0.0",
|
||||
"jsonschema==4.24.0",
|
||||
"markdown==3.8.2",
|
||||
"nested-multipart-parser==1.5.0",
|
||||
"psycopg[binary]==3.2.9",
|
||||
"jsonschema==4.25.1",
|
||||
"markdown==3.10",
|
||||
"nested-multipart-parser==1.6.0",
|
||||
"psycopg[binary]==3.2.12",
|
||||
"PyJWT==2.10.1",
|
||||
"python-frontmatter==1.1.0",
|
||||
"requests==2.32.4",
|
||||
"sentry-sdk==2.30.0",
|
||||
"whitenoise==6.9.0",
|
||||
"requests==2.32.5",
|
||||
"sentry-sdk==2.43.0",
|
||||
"whitenoise==6.11.0",
|
||||
"mozilla-django-oidc==4.0.1",
|
||||
"livekit-api==1.0.3",
|
||||
"aiohttp==3.12.13",
|
||||
"livekit-api==1.0.7",
|
||||
"aiohttp==3.13.2",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -68,21 +68,21 @@ dependencies = [
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"django-extensions==4.1",
|
||||
"drf-spectacular-sidecar==2025.6.1",
|
||||
"freezegun==1.5.2",
|
||||
"drf-spectacular-sidecar==2025.10.1",
|
||||
"freezegun==1.5.5",
|
||||
"ipdb==0.13.13",
|
||||
"ipython==9.3.0",
|
||||
"pyfakefs==5.9.1",
|
||||
"ipython==9.7.0",
|
||||
"pyfakefs==5.10.2",
|
||||
"pylint-django==2.6.1",
|
||||
"pylint==3.3.7",
|
||||
"pytest-cov==6.2.1",
|
||||
"pylint<4.0.0",
|
||||
"pytest-cov==7.0.0",
|
||||
"pytest-django==4.11.1",
|
||||
"pytest==8.4.1",
|
||||
"pytest==9.0.0",
|
||||
"pytest-icdiff==0.9",
|
||||
"pytest-xdist==3.7.0",
|
||||
"responses==0.25.7",
|
||||
"ruff==0.12.0",
|
||||
"types-requests==2.32.4.20250611",
|
||||
"pytest-xdist==3.8.0",
|
||||
"responses==0.25.8",
|
||||
"ruff==0.14.4",
|
||||
"types-requests==2.32.4.20250913",
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
|
||||
@@ -38,7 +38,12 @@ RUN npm run build
|
||||
FROM nginxinc/nginx-unprivileged:alpine3.21 AS frontend-production
|
||||
|
||||
USER root
|
||||
RUN apk update && apk upgrade libssl3 libcrypto3 libxml2>=2.12.7-r2 libxslt>=1.1.39-r2 libexpat>=2.7.2-r0
|
||||
RUN apk update && apk upgrade libssl3 \
|
||||
libcrypto3 \
|
||||
libxml2>=2.12.7-r2 \
|
||||
libxslt>=1.1.39-r2 \
|
||||
libexpat>=2.7.2-r0 \
|
||||
libpng>=1.6.53-r0
|
||||
|
||||
USER nginx
|
||||
|
||||
|
||||
Generated
+39
-30
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"version": "0.1.41",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "meet",
|
||||
"version": "0.1.41",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@livekit/components-react": "2.9.13",
|
||||
"@livekit/components-styles": "1.1.6",
|
||||
@@ -55,7 +55,7 @@
|
||||
"postcss": "8.5.6",
|
||||
"prettier": "3.6.2",
|
||||
"typescript": "5.8.3",
|
||||
"vite": "7.0.2",
|
||||
"vite": "7.0.8",
|
||||
"vite-tsconfig-paths": "5.1.4"
|
||||
}
|
||||
},
|
||||
@@ -104,6 +104,7 @@
|
||||
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
@@ -3818,17 +3819,6 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rrweb/types": {
|
||||
"version": "2.0.0-alpha.17",
|
||||
"resolved": "https://registry.npmjs.org/@rrweb/types/-/types-2.0.0-alpha.17.tgz",
|
||||
"integrity": "sha512-AfDTVUuCyCaIG0lTSqYtrZqJX39ZEYzs4fYKnexhQ+id+kbZIpIJtaut5cto6dWZbB3SEe4fW0o90Po3LvTmfg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"rrweb-snapshot": "^2.0.0-alpha.17"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz",
|
||||
@@ -3880,6 +3870,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.81.5.tgz",
|
||||
"integrity": "sha512-lOf2KqRRiYWpQT86eeeftAGnjuTR35myTP8MXyvHa81VlomoAWNEd8x5vkcAfQefu0qtYCvyqLropFZqgI2EQw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.81.5"
|
||||
},
|
||||
@@ -3971,8 +3962,7 @@
|
||||
"version": "1.0.22",
|
||||
"resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.22.tgz",
|
||||
"integrity": "sha512-mUMZLK3NvwRLcAAT9qmcK+9p7tpU2FHdDsntR3YI4+GY88XrgG4XiE7u1Q2LAN2/FZOz/tdMDC3GQCR4T8nFuw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/dom-mediacapture-transform": {
|
||||
"version": "0.1.11",
|
||||
@@ -3988,8 +3978,7 @@
|
||||
"version": "0.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.15.tgz",
|
||||
"integrity": "sha512-omOlCPvTWyPm4ZE5bZUhlSvnHM2ZWM2U+1cPiYFL/e8aV5O9MouELp+L4dMKNTON0nTeHqEg+KWDfFQMY5Wkaw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
@@ -4016,6 +4005,7 @@
|
||||
"integrity": "sha512-B2egV9wALML1JCpv3VQoQ+yesQKAmNMBIAY7OteVrikcOcAkWm+dGL6qpeCktPjAv6N1JLnhbNiqS35UpFyBsQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
@@ -4033,6 +4023,7 @@
|
||||
"integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
@@ -4099,6 +4090,7 @@
|
||||
"integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.35.1",
|
||||
"@typescript-eslint/types": "8.35.1",
|
||||
@@ -4390,6 +4382,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -4804,6 +4797,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001646",
|
||||
"electron-to-chromium": "^1.5.4",
|
||||
@@ -5613,6 +5607,7 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
|
||||
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
@@ -6580,6 +6575,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6"
|
||||
},
|
||||
@@ -7464,6 +7460,7 @@
|
||||
"resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.15.7.tgz",
|
||||
"integrity": "sha512-19m8Q1cvRl5PslRawDUgWXeP8vL8584tX8kiZEJaPZo83U/L6VPS/O7pP06phfJaBWeeV8sAOVtEPlQiZEHtpg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@livekit/mutex": "1.1.1",
|
||||
"@livekit/protocol": "1.39.3",
|
||||
@@ -8123,6 +8120,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -8236,6 +8234,7 @@
|
||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
@@ -8389,6 +8388,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
@@ -8495,6 +8495,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
@@ -8744,7 +8745,6 @@
|
||||
"integrity": "sha512-GBg5pV8LHOTbeVmH2VHLEFR0mc2QpQMzAvcoxEGfPNWgWHc8UvKCyq7pqN1vA+fDZ+yXXbixeO0kB2pzVvFCBw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"postcss": "^8.4.38"
|
||||
}
|
||||
@@ -9248,6 +9248,7 @@
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -9366,7 +9367,8 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
@@ -9478,6 +9480,7 @@
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -9638,6 +9641,7 @@
|
||||
"resolved": "https://registry.npmjs.org/valtio/-/valtio-2.1.5.tgz",
|
||||
"integrity": "sha512-vsh1Ixu5mT0pJFZm+Jspvhga5GzHUTYv0/+Th203pLfh3/wbHwxhu/Z2OkZDXIgHfjnjBns7SN9HNcbDvPmaGw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"proxy-compare": "^3.0.1"
|
||||
},
|
||||
@@ -9733,15 +9737,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.2.tgz",
|
||||
"integrity": "sha512-hxdyZDY1CM6SNpKI4w4lcUc3Mtkd9ej4ECWVHSMrOdSinVc2zYOAppHeGc/hzmRo3pxM5blMzkuWHOJA/3NiFw==",
|
||||
"version": "7.0.8",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.8.tgz",
|
||||
"integrity": "sha512-cJBdq0/u+8rgstg9t7UkBilf8ipLmeXJO30NxD5HAHOivnj10ocV8YtR/XBvd2wQpN3TmcaxNKaHX3tN7o5F5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.6",
|
||||
"picomatch": "^4.0.2",
|
||||
"picomatch": "^4.0.3",
|
||||
"postcss": "^8.5.6",
|
||||
"rollup": "^4.40.0",
|
||||
"tinyglobby": "^0.2.14"
|
||||
@@ -9849,11 +9854,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/fdir": {
|
||||
"version": "6.4.6",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
||||
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
@@ -9864,11 +9872,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"private": true,
|
||||
"version": "0.1.41",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "panda codegen && vite",
|
||||
@@ -60,7 +60,7 @@
|
||||
"postcss": "8.5.6",
|
||||
"prettier": "3.6.2",
|
||||
"typescript": "5.8.3",
|
||||
"vite": "7.0.2",
|
||||
"vite": "7.0.8",
|
||||
"vite-tsconfig-paths": "5.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,6 @@ export const authUrl = ({
|
||||
returnTo = window.location.href,
|
||||
} = {}) => {
|
||||
return apiUrl(
|
||||
`/authenticate?silent=${encodeURIComponent(silent)}&returnTo=${encodeURIComponent(returnTo)}`
|
||||
`/authenticate/?silent=${encodeURIComponent(silent)}&returnTo=${encodeURIComponent(returnTo)}`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -71,7 +71,11 @@ export const LaterMeetingDialog = ({
|
||||
aria-label={t('copyUrl')}
|
||||
tooltip={t('copyUrl')}
|
||||
>
|
||||
{isRoomUrlCopied ? <RiCheckLine /> : <RiFileCopyLine />}
|
||||
{isRoomUrlCopied ? (
|
||||
<RiCheckLine aria-hidden="true" />
|
||||
) : (
|
||||
<RiFileCopyLine aria-hidden="true" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -103,13 +107,18 @@ export const LaterMeetingDialog = ({
|
||||
>
|
||||
{isCopied ? (
|
||||
<>
|
||||
<RiCheckLine size={18} style={{ marginRight: '8px' }} />
|
||||
<RiCheckLine
|
||||
size={18}
|
||||
style={{ marginRight: '8px' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t('copied')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RiFileCopyLine
|
||||
style={{ marginRight: '6px', minWidth: '18px' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t('copy')}
|
||||
</>
|
||||
@@ -131,7 +140,11 @@ export const LaterMeetingDialog = ({
|
||||
>
|
||||
{isCopied ? (
|
||||
<>
|
||||
<RiCheckLine size={18} style={{ marginRight: '8px' }} />
|
||||
<RiCheckLine
|
||||
size={18}
|
||||
style={{ marginRight: '8px' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t('copied')}
|
||||
</>
|
||||
) : (
|
||||
@@ -139,6 +152,7 @@ export const LaterMeetingDialog = ({
|
||||
<RiFileCopyLine
|
||||
size={18}
|
||||
style={{ marginRight: '8px', minWidth: '18px' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{isHovered ? (
|
||||
t('copy')
|
||||
@@ -173,6 +187,7 @@ export const LaterMeetingDialog = ({
|
||||
className={css({
|
||||
fill: 'primary.500',
|
||||
})}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<Text variant="sm" style={{ marginTop: '1rem' }}>
|
||||
|
||||
@@ -30,6 +30,7 @@ import { MediaDeviceErrorAlert } from './MediaDeviceErrorAlert'
|
||||
import { usePostHog } from 'posthog-js/react'
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
import { isFireFox } from '@/utils/livekit'
|
||||
import { useIsMobile } from '@/utils/useIsMobile'
|
||||
|
||||
export const Conference = ({
|
||||
roomId,
|
||||
@@ -169,6 +170,8 @@ export const Conference = ({
|
||||
kind: null,
|
||||
})
|
||||
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
/*
|
||||
* Ensure stable WebSocket connection URL. This is critical for legacy browser compatibility
|
||||
* (Firefox <124, Chrome <125, Edge <125) where HTTPS URLs in WebSocket() constructor
|
||||
@@ -238,7 +241,7 @@ export const Conference = ({
|
||||
}}
|
||||
>
|
||||
<VideoConference />
|
||||
{showInviteDialog && (
|
||||
{showInviteDialog && !isMobile && (
|
||||
<InviteDialog
|
||||
isOpen={showInviteDialog}
|
||||
onOpenChange={setShowInviteDialog}
|
||||
|
||||
@@ -115,7 +115,11 @@ export const InviteDialog = (props: Omit<DialogProps, 'title'>) => {
|
||||
aria-label={t('copyUrl')}
|
||||
tooltip={t('copyUrl')}
|
||||
>
|
||||
{isRoomUrlCopied ? <RiCheckLine /> : <RiFileCopyLine />}
|
||||
{isRoomUrlCopied ? (
|
||||
<RiCheckLine aria-hidden="true" />
|
||||
) : (
|
||||
<RiFileCopyLine aria-hidden="true" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -147,13 +151,18 @@ export const InviteDialog = (props: Omit<DialogProps, 'title'>) => {
|
||||
>
|
||||
{isCopied ? (
|
||||
<>
|
||||
<RiCheckLine size={18} style={{ marginRight: '8px' }} />
|
||||
<RiCheckLine
|
||||
size={18}
|
||||
style={{ marginRight: '8px' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t('copied')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RiFileCopyLine
|
||||
style={{ marginRight: '6px', minWidth: '18px' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t('copy')}
|
||||
</>
|
||||
|
||||
@@ -113,6 +113,7 @@ export const FullScreenShareWarning = ({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: '1rem',
|
||||
zIndex: '1000',
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
|
||||
@@ -78,12 +78,20 @@ export const Info = () => {
|
||||
>
|
||||
{isCopied ? (
|
||||
<>
|
||||
<RiCheckLine size={24} style={{ marginRight: '6px' }} />
|
||||
<RiCheckLine
|
||||
size={24}
|
||||
style={{ marginRight: '6px' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t('roomInformation.button.copied')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RiFileCopyLine size={24} style={{ marginRight: '6px' }} />
|
||||
<RiFileCopyLine
|
||||
size={24}
|
||||
style={{ marginRight: '6px' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t('roomInformation.button.copy')}
|
||||
</>
|
||||
)}
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "0.1.41",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mail_mjml",
|
||||
"version": "0.1.41",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@html-to/text-cli": "0.5.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "0.1.41",
|
||||
"version": "1.0.0",
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -24,6 +24,6 @@
|
||||
"globals": "16.3.0",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "8.35.1",
|
||||
"vite": "7.0.2"
|
||||
"vite": "7.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"sass": "1.89.2",
|
||||
"prettier": "3.6.2",
|
||||
"typescript": "5.8.3",
|
||||
"vite": "7.0.2",
|
||||
"vite": "7.0.8",
|
||||
"vite-plugin-dts": "4.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+17
-14
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "0.1.41",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sdk",
|
||||
"version": "0.1.41",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"workspaces": [
|
||||
"./library",
|
||||
@@ -30,7 +30,7 @@
|
||||
"globals": "16.3.0",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "8.35.1",
|
||||
"vite": "7.0.2"
|
||||
"vite": "7.0.8"
|
||||
}
|
||||
},
|
||||
"consumer/node_modules/@eslint/js": {
|
||||
@@ -103,7 +103,7 @@
|
||||
"prettier": "3.6.2",
|
||||
"sass": "1.89.2",
|
||||
"typescript": "5.8.3",
|
||||
"vite": "7.0.2",
|
||||
"vite": "7.0.8",
|
||||
"vite-plugin-dts": "4.5.4"
|
||||
}
|
||||
},
|
||||
@@ -6677,15 +6677,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.2.tgz",
|
||||
"integrity": "sha512-hxdyZDY1CM6SNpKI4w4lcUc3Mtkd9ej4ECWVHSMrOdSinVc2zYOAppHeGc/hzmRo3pxM5blMzkuWHOJA/3NiFw==",
|
||||
"version": "7.0.8",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.8.tgz",
|
||||
"integrity": "sha512-cJBdq0/u+8rgstg9t7UkBilf8ipLmeXJO30NxD5HAHOivnj10ocV8YtR/XBvd2wQpN3TmcaxNKaHX3tN7o5F5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.6",
|
||||
"picomatch": "^4.0.2",
|
||||
"picomatch": "^4.0.3",
|
||||
"postcss": "^8.5.6",
|
||||
"rollup": "^4.40.0",
|
||||
"tinyglobby": "^0.2.14"
|
||||
@@ -6779,11 +6779,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/fdir": {
|
||||
"version": "6.4.6",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
||||
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
@@ -6794,9 +6797,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "0.1.41",
|
||||
"version": "1.0.0",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
[project]
|
||||
name = "summary"
|
||||
version = "0.1.41"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"fastapi[standard]>=0.105.0",
|
||||
"uvicorn>=0.24.0",
|
||||
@@ -9,17 +9,17 @@ dependencies = [
|
||||
"pydantic-settings>=2.1.0",
|
||||
"celery==5.5.3",
|
||||
"redis==5.2.1",
|
||||
"minio==7.2.15",
|
||||
"minio==7.2.18",
|
||||
"mutagen==1.47.0",
|
||||
"openai==1.91.0",
|
||||
"posthog==6.0.3",
|
||||
"requests==2.32.4",
|
||||
"sentry-sdk[fastapi, celery]==2.30.0",
|
||||
"openai==2.7.1",
|
||||
"posthog==6.9.1",
|
||||
"requests==2.32.5",
|
||||
"sentry-sdk[fastapi, celery]==2.43.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"ruff==0.12.0",
|
||||
"ruff==0.14.4",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
||||
@@ -31,6 +31,7 @@ from summary.core.prompt import (
|
||||
PROMPT_SYSTEM_TLDR,
|
||||
PROMPT_USER_PART,
|
||||
)
|
||||
from summary.core.transcript_formatter import TranscriptFormatter
|
||||
|
||||
settings = get_settings()
|
||||
analytics = get_analytics()
|
||||
@@ -56,28 +57,6 @@ if settings.sentry_dsn and settings.sentry_is_enabled:
|
||||
sentry_sdk.init(dsn=settings.sentry_dsn, enable_tracing=True)
|
||||
|
||||
|
||||
DEFAULT_EMPTY_TRANSCRIPTION = """
|
||||
**Aucun contenu audio n’a été détecté dans votre transcription.**
|
||||
|
||||
|
||||
*Si vous pensez qu’il s’agit d’une erreur, n’hésitez pas à contacter
|
||||
notre support technique : visio@numerique.gouv.fr*
|
||||
|
||||
.
|
||||
|
||||
.
|
||||
|
||||
.
|
||||
|
||||
Quelques points que nous vous conseillons de vérifier :
|
||||
- Un micro était-il activé ?
|
||||
- Étiez-vous suffisamment proche ?
|
||||
- Le micro est-il de bonne qualité ?
|
||||
- L’enregistrement dure-t-il plus de 30 secondes ?
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class AudioValidationError(Exception):
|
||||
"""Custom exception for audio validation errors."""
|
||||
|
||||
@@ -166,36 +145,6 @@ def format_actions(llm_output: dict) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def format_segments(transcription_data):
|
||||
"""Format transcription segments from WhisperX into a readable conversation format.
|
||||
|
||||
Processes transcription data with segments containing speaker information and text,
|
||||
combining consecutive segments from the same speaker and formatting them as a
|
||||
conversation with speaker labels.
|
||||
"""
|
||||
formatted_output = ""
|
||||
if not transcription_data or not hasattr(transcription_data, "segments"):
|
||||
if isinstance(transcription_data, dict) and "segments" in transcription_data:
|
||||
segments = transcription_data["segments"]
|
||||
else:
|
||||
return "Error: Invalid transcription data format"
|
||||
else:
|
||||
segments = transcription_data.segments
|
||||
|
||||
previous_speaker = None
|
||||
|
||||
for segment in segments:
|
||||
speaker = segment.get("speaker", "UNKNOWN_SPEAKER")
|
||||
text = segment.get("text", "")
|
||||
if text:
|
||||
if speaker != previous_speaker:
|
||||
formatted_output += f"\n\n **{speaker}**: {text}"
|
||||
else:
|
||||
formatted_output += f" {text}"
|
||||
previous_speaker = speaker
|
||||
return formatted_output
|
||||
|
||||
|
||||
def post_with_retries(url, data):
|
||||
"""Send POST request with automatic retries."""
|
||||
session = create_retry_session()
|
||||
@@ -306,25 +255,20 @@ def process_audio_transcribe_summarize_v2(
|
||||
os.remove(temp_file_path)
|
||||
logger.debug("Temporary file removed: %s", temp_file_path)
|
||||
|
||||
formatted_transcription = (
|
||||
DEFAULT_EMPTY_TRANSCRIPTION
|
||||
if not transcription.segments
|
||||
else format_segments(transcription)
|
||||
)
|
||||
|
||||
metadata_manager.track_transcription_metadata(task_id, transcription)
|
||||
|
||||
if not room or not recording_date or not recording_time:
|
||||
title = settings.document_default_title
|
||||
else:
|
||||
title = settings.document_title_template.format(
|
||||
room=room,
|
||||
room_recording_date=recording_date,
|
||||
room_recording_time=recording_time,
|
||||
)
|
||||
formatter = TranscriptFormatter()
|
||||
|
||||
content, title = formatter.format(
|
||||
transcription,
|
||||
room=room,
|
||||
recording_date=recording_date,
|
||||
recording_time=recording_time,
|
||||
)
|
||||
|
||||
data = {
|
||||
"title": title,
|
||||
"content": formatted_transcription,
|
||||
"content": content,
|
||||
"email": email,
|
||||
"sub": sub,
|
||||
}
|
||||
@@ -356,7 +300,7 @@ def process_audio_transcribe_summarize_v2(
|
||||
):
|
||||
logger.info("Queuing summary generation task.")
|
||||
summarize_transcription.apply_async(
|
||||
args=[formatted_transcription, email, sub, title],
|
||||
args=[content, email, sub, title],
|
||||
queue=settings.summarize_queue,
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -45,6 +45,10 @@ class Settings(BaseSettings):
|
||||
llm_api_key: str
|
||||
llm_model: str
|
||||
|
||||
# Transcription processing
|
||||
hallucination_patterns: List[str] = ["Vap'n'Roll Thierry"]
|
||||
hallucination_replacement_text: str = "[Texte impossible à transcrire]"
|
||||
|
||||
# Webhook-related settings
|
||||
webhook_max_retries: int = 2
|
||||
webhook_status_forcelist: List[int] = [502, 503, 504]
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
"""Transcript formatting into readable conversation format with speaker labels."""
|
||||
|
||||
import logging
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from summary.core.config import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DEFAULT_EMPTY_TRANSCRIPTION = """
|
||||
**Aucun contenu audio n’a été détecté dans votre transcription.**
|
||||
|
||||
|
||||
*Si vous pensez qu’il s’agit d’une erreur, n’hésitez pas à contacter
|
||||
notre support technique : visio@numerique.gouv.fr*
|
||||
|
||||
.
|
||||
|
||||
.
|
||||
|
||||
.
|
||||
|
||||
Quelques points que nous vous conseillons de vérifier :
|
||||
- Un micro était-il activé ?
|
||||
- Étiez-vous suffisamment proche ?
|
||||
- Le micro est-il de bonne qualité ?
|
||||
- L’enregistrement dure-t-il plus de 30 secondes ?
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class TranscriptFormatter:
|
||||
"""Formats WhisperX transcription output into readable conversation format.
|
||||
|
||||
Handles:
|
||||
- Extracting segments from transcription objects or dictionaries
|
||||
- Combining consecutive segments from the same speaker
|
||||
- Removing hallucination patterns from content
|
||||
- Generating descriptive titles from context
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize formatter with settings."""
|
||||
self.hallucination_patterns = settings.hallucination_patterns
|
||||
self.hallucination_replacement_text = settings.hallucination_replacement_text
|
||||
self.default_title = settings.document_default_title
|
||||
self.default_empty_transcription = DEFAULT_EMPTY_TRANSCRIPTION
|
||||
|
||||
def _get_segments(self, transcription):
|
||||
"""Extract segments from transcription object or dictionary."""
|
||||
if hasattr(transcription, "segments"):
|
||||
return transcription.segments
|
||||
|
||||
if isinstance(transcription, dict):
|
||||
return transcription.get("segments", None)
|
||||
|
||||
return None
|
||||
|
||||
def format(
|
||||
self,
|
||||
transcription,
|
||||
room: Optional[str] = None,
|
||||
recording_date: Optional[str] = None,
|
||||
recording_time: Optional[str] = None,
|
||||
) -> Tuple[str, str]:
|
||||
"""Format transcription into the final document and its title."""
|
||||
segments = self._get_segments(transcription)
|
||||
|
||||
if not segments:
|
||||
content = self.default_empty_transcription
|
||||
else:
|
||||
content = self._format_speaker(segments)
|
||||
content = self._remove_hallucinations(content)
|
||||
|
||||
title = self._generate_title(room, recording_date, recording_time)
|
||||
|
||||
return content, title
|
||||
|
||||
def _remove_hallucinations(self, content: str) -> str:
|
||||
"""Remove hallucination patterns from content."""
|
||||
replacement = self.hallucination_replacement_text or ""
|
||||
|
||||
for pattern in self.hallucination_patterns:
|
||||
content = content.replace(pattern, replacement)
|
||||
return content
|
||||
|
||||
def _format_speaker(self, segments) -> str:
|
||||
"""Format segments with speaker labels, combining consecutive speakers."""
|
||||
formatted_output = ""
|
||||
previous_speaker = None
|
||||
|
||||
for segment in segments:
|
||||
speaker = segment.get("speaker", "UNKNOWN_SPEAKER")
|
||||
text = segment.get("text", "")
|
||||
if text:
|
||||
if speaker != previous_speaker:
|
||||
formatted_output += f"\n\n **{speaker}**: {text}"
|
||||
else:
|
||||
formatted_output += f" {text}"
|
||||
previous_speaker = speaker
|
||||
|
||||
return formatted_output
|
||||
|
||||
def _generate_title(
|
||||
self,
|
||||
room: Optional[str] = None,
|
||||
recording_date: Optional[str] = None,
|
||||
recording_time: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Generate title from context or return default."""
|
||||
if not room or not recording_date or not recording_time:
|
||||
return self.default_title
|
||||
|
||||
return settings.document_title_template.format(
|
||||
room=room,
|
||||
room_recording_date=recording_date,
|
||||
room_recording_time=recording_time,
|
||||
)
|
||||
Reference in New Issue
Block a user