Skip to content

Commit a03fffe

Browse files
committed
Implement info pages with different access conditions
Fixes #1246
1 parent 0c60c59 commit a03fffe

File tree

16 files changed

+395
-429
lines changed

16 files changed

+395
-429
lines changed

Tekst-API/openapi.json

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2698,6 +2698,14 @@
26982698
],
26992699
"summary": "Get segment",
27002700
"operationId": "getSegment",
2701+
"security": [
2702+
{
2703+
"APIKeyCookie": []
2704+
},
2705+
{
2706+
"OAuth2PasswordBearer": []
2707+
}
2708+
],
27012709
"parameters": [
27022710
{
27032711
"name": "id",
@@ -9951,6 +9959,17 @@
99519959
"$ref": "#/components/schemas/TranslationLocaleKey",
99529960
"description": "Locale indicating the translation language of this segment"
99539961
},
9962+
"restriction": {
9963+
"type": "string",
9964+
"enum": [
9965+
"none",
9966+
"user",
9967+
"superuser"
9968+
],
9969+
"title": "Restriction",
9970+
"description": "Whether access is unrestricted or restricted to superusers or users",
9971+
"default": "none"
9972+
},
99549973
"title": {
99559974
"type": "string",
99569975
"maxLength": 32,
@@ -10044,6 +10063,17 @@
1004410063
"$ref": "#/components/schemas/TranslationLocaleKey",
1004510064
"description": "Locale indicating the translation language of this segment"
1004610065
},
10066+
"restriction": {
10067+
"type": "string",
10068+
"enum": [
10069+
"none",
10070+
"user",
10071+
"superuser"
10072+
],
10073+
"title": "Restriction",
10074+
"description": "Whether access is unrestricted or restricted to superusers or users",
10075+
"default": "none"
10076+
},
1004710077
"title": {
1004810078
"type": "string",
1004910079
"maxLength": 32,
@@ -10117,6 +10147,24 @@
1011710147
"description": "Locale indicating the translation language of this segment",
1011810148
"optionalNullable": false
1011910149
},
10150+
"restriction": {
10151+
"anyOf": [
10152+
{
10153+
"type": "string",
10154+
"enum": [
10155+
"none",
10156+
"user",
10157+
"superuser"
10158+
]
10159+
},
10160+
{
10161+
"type": "null"
10162+
}
10163+
],
10164+
"title": "Restriction",
10165+
"description": "Whether access is unrestricted or restricted to superusers or users",
10166+
"optionalNullable": false
10167+
},
1012010168
"title": {
1012110169
"anyOf": [
1012210170
{

Tekst-API/tekst/models/segment.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ class ClientSegment(ModelBase, ModelFactoryMixin):
3939
description="Locale indicating the translation language of this segment",
4040
),
4141
]
42+
restriction: Annotated[
43+
Literal["none", "user", "superuser"],
44+
Field(
45+
description=(
46+
"Whether access is unrestricted or restricted to superusers or users"
47+
),
48+
),
49+
] = "none"
4250
title: Annotated[
4351
ConStr(
4452
max_length=32,

Tekst-API/tekst/platform.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
1+
from collections.abc import Mapping
12
from datetime import UTC, datetime, timedelta
23
from os.path import realpath
34
from pathlib import Path
45

5-
from beanie.operators import LT
6+
from beanie.operators import GTE, LT, Eq, In, Or
67
from bson import json_util
78

89
from tekst import db, search
910
from tekst.auth import AccessTokenDocument, create_initial_superuser
1011
from tekst.config import TekstConfig, get_config
1112
from tekst.db import migrations
1213
from tekst.logs import log, log_op_end, log_op_start
14+
from tekst.models.common import PydanticObjectId
1315
from tekst.models.message import UserMessageDocument
1416
from tekst.models.platform import PlatformStateDocument
17+
from tekst.models.segment import ClientSegmentDocument, ClientSegmentHead
18+
from tekst.models.user import UserDocument
1519
from tekst.resources import call_resource_precompute_hooks
1620
from tekst.state import get_state, update_state
1721

@@ -119,3 +123,61 @@ async def cleanup_task(cfg: TekstConfig = get_config()) -> dict[str, float]:
119123
return {
120124
"took": round(log_op_end(op_id), 2),
121125
}
126+
127+
128+
async def _get_segment_restriction_queries(
129+
user: UserDocument | None = None,
130+
) -> tuple[Mapping]:
131+
if user is None:
132+
return (In(ClientSegmentDocument.restriction, ["none", None]),)
133+
if user.is_superuser:
134+
return tuple()
135+
return (In(ClientSegmentDocument.restriction, ["none", "user"]),)
136+
137+
138+
async def get_segment(
139+
*,
140+
segment_id: PydanticObjectId | None = None,
141+
user: UserDocument | None = None,
142+
) -> ClientSegmentDocument | None:
143+
return await ClientSegmentDocument.find_one(
144+
Eq(ClientSegmentDocument.id, segment_id),
145+
*(await _get_segment_restriction_queries(user)),
146+
)
147+
148+
149+
async def get_segments(
150+
*,
151+
system: bool | None = None,
152+
user: UserDocument | None = None,
153+
head_projection: bool = False,
154+
) -> list[ClientSegmentDocument]:
155+
system_segments_queries = (
156+
tuple()
157+
if system is None
158+
else (
159+
GTE(ClientSegmentDocument.key, "system"),
160+
LT(ClientSegmentDocument.key, "systen"),
161+
)
162+
if system
163+
else (
164+
Or(
165+
LT(ClientSegmentDocument.key, "system"),
166+
GTE(ClientSegmentDocument.key, "systen"),
167+
),
168+
)
169+
)
170+
if not head_projection:
171+
return await ClientSegmentDocument.find(
172+
*system_segments_queries,
173+
*(await _get_segment_restriction_queries(user)),
174+
).to_list()
175+
else:
176+
return (
177+
await ClientSegmentDocument.find(
178+
*system_segments_queries,
179+
*(await _get_segment_restriction_queries(user)),
180+
)
181+
.project(ClientSegmentHead)
182+
.to_list()
183+
)

Tekst-API/tekst/routers/platform.py

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
from typing import Annotated
33

44
from beanie import PydanticObjectId
5-
from beanie.operators import GTE, LT, NotIn, Or
5+
from beanie.operators import NotIn
66
from fastapi import APIRouter, Header, Path, Query, status
77
from fastapi.responses import FileResponse
88
from humps import camelize
99
from starlette.background import BackgroundTask
1010

11-
from tekst import errors, tasks
11+
from tekst import errors, platform, tasks
1212
from tekst.auth import OptionalUserDep, SuperuserDep
1313
from tekst.config import ConfigDep
1414
from tekst.models.platform import (
@@ -22,12 +22,10 @@
2222
from tekst.models.segment import (
2323
ClientSegmentCreate,
2424
ClientSegmentDocument,
25-
ClientSegmentHead,
2625
ClientSegmentRead,
2726
ClientSegmentUpdate,
2827
)
2928
from tekst.models.user import UserDocument
30-
from tekst.platform import cleanup_task
3129
from tekst.routers.texts import get_all_texts
3230
from tekst.state import get_state, update_state
3331

@@ -43,26 +41,26 @@
4341
response_model=PlatformData,
4442
status_code=status.HTTP_200_OK,
4543
)
46-
async def get_platform_data(ou: OptionalUserDep, cfg: ConfigDep) -> PlatformData:
44+
async def get_platform_data(
45+
ou: OptionalUserDep,
46+
cfg: ConfigDep,
47+
) -> PlatformData:
4748
"""Returns data about the platform and its configuration"""
4849
return PlatformData(
4950
texts=await get_all_texts(ou),
5051
state=await get_state(),
5152
security=PlatformSecurityInfo(),
5253
# find segments with keys starting with "system"
53-
system_segments=await ClientSegmentDocument.find(
54-
GTE(ClientSegmentDocument.key, "system"),
55-
LT(ClientSegmentDocument.key, "systen"),
56-
).to_list(),
54+
system_segments=await platform.get_segments(
55+
system=True,
56+
user=ou,
57+
),
5758
# find segments with keys not starting with "system"
58-
info_segments=await ClientSegmentDocument.find(
59-
Or(
60-
LT(ClientSegmentDocument.key, "system"),
61-
GTE(ClientSegmentDocument.key, "systen"),
62-
)
63-
)
64-
.project(ClientSegmentHead)
65-
.to_list(),
59+
info_segments=await platform.get_segments(
60+
system=False,
61+
user=ou,
62+
head_projection=True,
63+
),
6664
tekst=camelize(cfg.tekst),
6765
)
6866

@@ -124,9 +122,10 @@ async def update_platform_state(
124122
),
125123
)
126124
async def get_segment(
125+
ou: OptionalUserDep,
127126
segment_id: Annotated[PydanticObjectId, Path(alias="id")],
128127
) -> ClientSegmentDocument:
129-
segment = await ClientSegmentDocument.get(segment_id)
128+
segment = await platform.get_segment(segment_id=segment_id, user=ou)
130129
if not segment:
131130
raise errors.E_404_SEGMENT_NOT_FOUND
132131
return segment
@@ -353,7 +352,7 @@ async def delete_task(
353352
)
354353
async def run_platform_cleanup(su: SuperuserDep) -> tasks.TaskDocument:
355354
return await tasks.create_task(
356-
cleanup_task,
355+
platform.cleanup_task,
357356
tasks.TaskType.PLATFORM_CLEANUP,
358357
target_id=tasks.TaskType.PLATFORM_CLEANUP.value,
359358
user_id=su.id,

Tekst-API/tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ async def _insert_test_data(*collections: str) -> dict[str, list[str]]:
148148
"state",
149149
"texts",
150150
"users",
151+
"segments",
151152
]
152153
for collection in collections:
153154
test_data = get_test_data(f"collections/{collection}.json")

Tekst-API/tests/test_api_platform.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
async def test_platform_data(
1414
test_client: AsyncClient,
1515
assert_status,
16+
insert_test_data,
1617
):
18+
await insert_test_data()
1719
resp = await test_client.get("/platform")
1820
assert_status(200, resp)
1921
assert resp.json()["tekst"]["version"] == package_metadata["version"]
@@ -23,21 +25,31 @@ async def test_platform_data(
2325
async def test_web_init_data(
2426
test_client: AsyncClient,
2527
assert_status,
28+
insert_test_data,
2629
login,
2730
):
31+
await insert_test_data()
2832
# anonymous
2933
resp = await test_client.get("/platform/web-init")
3034
assert_status(200, resp)
3135
assert "platform" in resp.json()
3236
assert "user" in resp.json()
3337
assert resp.json()["user"] is None
34-
38+
assert len(resp.json()["platform"]["infoSegments"]) == 2
3539
# logged in
3640
await login()
3741
resp = await test_client.get("/platform/web-init")
3842
assert_status(200, resp)
3943
assert "user" in resp.json()
4044
assert resp.json()["user"] is not None
45+
assert len(resp.json()["platform"]["infoSegments"]) == 4
46+
# logged in as admin
47+
await login(is_superuser=True)
48+
resp = await test_client.get("/platform/web-init")
49+
assert_status(200, resp)
50+
assert "user" in resp.json()
51+
assert resp.json()["user"] is not None
52+
assert len(resp.json()["platform"]["infoSegments"]) == 6
4153

4254

4355
@pytest.mark.anyio
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
[
2+
{
3+
"_id": { "$oid": "68ecc8434d8f8e6fb2ba0cfc" },
4+
"key": "info_page",
5+
"editor_mode": "wysiwyg",
6+
"locale": "enUS",
7+
"restriction": "none",
8+
"title": "Public Info Page",
9+
"html": "<h1>A public info page</h1><p>This is a public info page</p>"
10+
},
11+
{
12+
"_id": { "$oid": "68ecc8b04d8f8e6fb2ba0cfd" },
13+
"key": "info_page",
14+
"editor_mode": "wysiwyg",
15+
"locale": "deDE",
16+
"restriction": "none",
17+
"title": "Öffentliche Info-Seite",
18+
"html": "<h1>Eine öffentliche Info-Seite</h1><p>Die ist eine öffentliche Info-Seite</p>"
19+
},
20+
{
21+
"_id": { "$oid": "68ecc8ef4d8f8e6fb2ba0cfe" },
22+
"key": "user_info_page",
23+
"editor_mode": "wysiwyg",
24+
"locale": "enUS",
25+
"restriction": "user",
26+
"title": "User Info Page",
27+
"html": "<h1>A user info page</h1><p>This is a user info page.</p><p></p>"
28+
},
29+
{
30+
"_id": { "$oid": "68ecc9404d8f8e6fb2ba0cff" },
31+
"key": "user_info_page",
32+
"editor_mode": "wysiwyg",
33+
"locale": "deDE",
34+
"restriction": "user",
35+
"title": "Benutzer*innen Info-Seite",
36+
"html": "<h1>Eine Benutzer*innen Info-Seite</h1><p>Dies ist eine Benutzer*innen Info-Seite</p><p></p>"
37+
},
38+
{
39+
"_id": { "$oid": "68eccec34d8f8e6fb2ba0d00" },
40+
"key": "admin_info_page",
41+
"editor_mode": "wysiwyg",
42+
"locale": "enUS",
43+
"restriction": "superuser",
44+
"title": "Admin info page",
45+
"html": "<h1>Admin Info Page</h1><p>This is an admin info page.</p><p></p>"
46+
},
47+
{
48+
"_id": { "$oid": "68eccf0c4d8f8e6fb2ba0d01" },
49+
"key": "admin_info_page",
50+
"editor_mode": "wysiwyg",
51+
"locale": "deDE",
52+
"restriction": "superuser",
53+
"title": "Administrator*innen Info-Seite",
54+
"html": "<h1>Administrator*innen Info-Seite</h1><p>Dies ist eine Administrator*innen Info-Seite</p>"
55+
},
56+
{
57+
"_id": { "$oid": "68ecd61d4d8f8e6fb2ba0d02" },
58+
"key": "systemSiteNotice",
59+
"editor_mode": "wysiwyg",
60+
"locale": "*",
61+
"restriction": "none",
62+
"title": "Site Notice",
63+
"html": "<h1>Site Notice</h1><p>This could be a site notice.</p>"
64+
}
65+
]

Tekst-Web/i18n/ui/deDE.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,11 @@ admin:
854854
systemSiteNotice: Impressum
855855
systemPrivacyPolicy: Datenschutzerklärung
856856
systemRegisterIntro: Einführungstext auf "Registrieren"-Seite
857+
restriction:
858+
showTo: Anzeigen für
859+
none: Alle, auch Besucher*innen
860+
user: Nur registrierte Nutzer*innen
861+
superuser: Nur Administrator*innen
857862
noSegment: Bitte wählen Sie ein Segment aus oder erstellen Sie ein neues!
858863
newSegment: Neues Segment
859864
phSelectSegment: Segment auswählen

0 commit comments

Comments
 (0)