platzky 1.0.1__py3-none-any.whl → 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- platzky/admin/admin.py +25 -3
- platzky/admin/fake_login.py +3 -2
- platzky/blog/blog.py +126 -37
- platzky/blog/comment_form.py +10 -0
- platzky/config.py +1 -1
- platzky/db/db.py +61 -10
- platzky/db/db_loader.py +5 -2
- platzky/db/github_json_db.py +42 -4
- platzky/db/google_json_db.py +63 -7
- platzky/db/graph_ql_db.py +216 -31
- platzky/db/json_db.py +172 -38
- platzky/db/json_file_db.py +46 -6
- platzky/db/mongodb_db.py +134 -6
- platzky/engine.py +9 -7
- platzky/models.py +160 -24
- platzky/platzky.py +169 -23
- platzky/plugin/plugin.py +39 -3
- platzky/plugin/plugin_loader.py +108 -12
- platzky/seo/seo.py +51 -16
- {platzky-1.0.1.dist-info → platzky-1.2.0.dist-info}/METADATA +1 -1
- {platzky-1.0.1.dist-info → platzky-1.2.0.dist-info}/RECORD +23 -23
- {platzky-1.0.1.dist-info → platzky-1.2.0.dist-info}/WHEEL +0 -0
- {platzky-1.0.1.dist-info → platzky-1.2.0.dist-info}/licenses/LICENSE +0 -0
platzky/db/graph_ql_db.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
"""GraphQL-based database implementation for CMS integration."""
|
|
2
2
|
|
|
3
|
+
# TODO: Rename file, extract to another library, remove gql and aiohttp from dependencies
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
3
6
|
|
|
4
7
|
from gql import Client, gql
|
|
5
8
|
from gql.transport.aiohttp import AIOHTTPTransport
|
|
@@ -7,29 +10,60 @@ from gql.transport.exceptions import TransportQueryError
|
|
|
7
10
|
from pydantic import Field
|
|
8
11
|
|
|
9
12
|
from platzky.db.db import DB, DBConfig
|
|
10
|
-
from platzky.models import Post
|
|
13
|
+
from platzky.models import MenuItem, Page, Post
|
|
14
|
+
|
|
11
15
|
|
|
16
|
+
def db_config_type() -> type["GraphQlDbConfig"]:
|
|
17
|
+
"""Return the configuration class for GraphQL database.
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
Returns:
|
|
20
|
+
GraphQlDbConfig class
|
|
21
|
+
"""
|
|
14
22
|
return GraphQlDbConfig
|
|
15
23
|
|
|
16
24
|
|
|
17
25
|
class GraphQlDbConfig(DBConfig):
|
|
26
|
+
"""Configuration for GraphQL database connection."""
|
|
27
|
+
|
|
18
28
|
endpoint: str = Field(alias="CMS_ENDPOINT")
|
|
19
29
|
token: str = Field(alias="CMS_TOKEN")
|
|
20
30
|
|
|
21
31
|
|
|
22
|
-
def get_db(config: GraphQlDbConfig):
|
|
32
|
+
def get_db(config: GraphQlDbConfig) -> "GraphQL":
|
|
33
|
+
"""Get a GraphQL database instance from configuration.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
config: GraphQL database configuration
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Configured GraphQL database instance
|
|
40
|
+
"""
|
|
23
41
|
return GraphQL(config.endpoint, config.token)
|
|
24
42
|
|
|
25
43
|
|
|
26
|
-
def db_from_config(config: GraphQlDbConfig):
|
|
44
|
+
def db_from_config(config: GraphQlDbConfig) -> "GraphQL":
|
|
45
|
+
"""Create a GraphQL database instance from configuration.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
config: GraphQL database configuration
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Configured GraphQL database instance
|
|
52
|
+
"""
|
|
27
53
|
return GraphQL(config.endpoint, config.token)
|
|
28
54
|
|
|
29
55
|
|
|
30
|
-
def
|
|
31
|
-
comment,
|
|
32
|
-
):
|
|
56
|
+
def _standardize_comment(
|
|
57
|
+
comment: dict[str, Any],
|
|
58
|
+
) -> dict[str, Any]:
|
|
59
|
+
"""Standardize comment data structure from GraphQL response.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
comment: Raw comment data from GraphQL response
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Standardized comment dictionary
|
|
66
|
+
"""
|
|
33
67
|
return {
|
|
34
68
|
"author": comment["author"],
|
|
35
69
|
"comment": comment["comment"],
|
|
@@ -37,14 +71,22 @@ def _standarize_comment(
|
|
|
37
71
|
}
|
|
38
72
|
|
|
39
73
|
|
|
40
|
-
def
|
|
74
|
+
def _standardize_post(post: dict[str, Any]) -> dict[str, Any]:
|
|
75
|
+
"""Standardize post data structure from GraphQL response.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
post: Raw post data from GraphQL response
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Standardized post dictionary
|
|
82
|
+
"""
|
|
41
83
|
return {
|
|
42
84
|
"author": post["author"]["name"],
|
|
43
85
|
"slug": post["slug"],
|
|
44
86
|
"title": post["title"],
|
|
45
87
|
"excerpt": post["excerpt"],
|
|
46
88
|
"contentInMarkdown": post["contentInRichText"]["html"],
|
|
47
|
-
"comments": [
|
|
89
|
+
"comments": [_standardize_comment(comment) for comment in post["comments"]],
|
|
48
90
|
"tags": post["tags"],
|
|
49
91
|
"language": post["language"],
|
|
50
92
|
"coverImage": {
|
|
@@ -54,8 +96,72 @@ def _standarize_post(post):
|
|
|
54
96
|
}
|
|
55
97
|
|
|
56
98
|
|
|
99
|
+
def _standardize_page(page: dict[str, Any]) -> dict[str, Any]:
|
|
100
|
+
"""Standardize page data structure from GraphQL response.
|
|
101
|
+
|
|
102
|
+
Pages have fewer required fields than posts in the GraphQL schema.
|
|
103
|
+
This function provides sensible defaults for missing Post fields.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
page: Raw page data from GraphQL response
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Standardized page dictionary compatible with Page model
|
|
110
|
+
"""
|
|
111
|
+
return {
|
|
112
|
+
"author": page.get("author", ""),
|
|
113
|
+
"slug": page.get("slug", ""),
|
|
114
|
+
"title": page["title"],
|
|
115
|
+
"excerpt": page.get("excerpt", ""),
|
|
116
|
+
"contentInMarkdown": page["contentInMarkdown"],
|
|
117
|
+
"comments": [],
|
|
118
|
+
"tags": page.get("tags", []),
|
|
119
|
+
"language": page.get("language", "en"),
|
|
120
|
+
"coverImage": {
|
|
121
|
+
"url": page.get("coverImage", {}).get("url", ""),
|
|
122
|
+
},
|
|
123
|
+
"date": page.get("date", "1970-01-01"),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _standardize_post_by_tag(post: dict[str, Any]) -> dict[str, Any]:
|
|
128
|
+
"""Standardize post data from get_posts_by_tag GraphQL response.
|
|
129
|
+
|
|
130
|
+
Posts returned by tag query have fewer fields than full posts.
|
|
131
|
+
This function provides sensible defaults for missing Post fields.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
post: Raw post data from GraphQL get_posts_by_tag response
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Standardized post dictionary compatible with Post model
|
|
138
|
+
"""
|
|
139
|
+
return {
|
|
140
|
+
"author": post.get("author", ""),
|
|
141
|
+
"slug": post["slug"],
|
|
142
|
+
"title": post["title"],
|
|
143
|
+
"excerpt": post["excerpt"],
|
|
144
|
+
"contentInMarkdown": post.get("contentInMarkdown", ""),
|
|
145
|
+
"comments": [],
|
|
146
|
+
"tags": post["tags"],
|
|
147
|
+
"language": post.get("language", "en"),
|
|
148
|
+
"coverImage": {
|
|
149
|
+
"url": post["coverImage"]["image"]["url"],
|
|
150
|
+
},
|
|
151
|
+
"date": post["date"],
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
57
155
|
class GraphQL(DB):
|
|
58
|
-
|
|
156
|
+
"""GraphQL database implementation for CMS integration."""
|
|
157
|
+
|
|
158
|
+
def __init__(self, endpoint: str, token: str) -> None:
|
|
159
|
+
"""Initialize GraphQL database connection.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
endpoint: GraphQL API endpoint URL
|
|
163
|
+
token: Authentication token for the API
|
|
164
|
+
"""
|
|
59
165
|
self.module_name = "graph_ql_db"
|
|
60
166
|
self.db_name = "GraphQLDb"
|
|
61
167
|
full_token = "bearer " + token
|
|
@@ -63,7 +169,15 @@ class GraphQL(DB):
|
|
|
63
169
|
self.client = Client(transport=transport)
|
|
64
170
|
super().__init__()
|
|
65
171
|
|
|
66
|
-
def get_all_posts(self, lang):
|
|
172
|
+
def get_all_posts(self, lang: str) -> list[Post]:
|
|
173
|
+
"""Retrieve all published posts for a specific language.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
lang: Language code (e.g., 'en', 'pl')
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
List of Post objects
|
|
180
|
+
"""
|
|
67
181
|
all_posts = gql(
|
|
68
182
|
"""
|
|
69
183
|
query MyQuery($lang: Lang!) {
|
|
@@ -98,9 +212,17 @@ class GraphQL(DB):
|
|
|
98
212
|
)
|
|
99
213
|
raw_ql_posts = self.client.execute(all_posts, variable_values={"lang": lang})["posts"]
|
|
100
214
|
|
|
101
|
-
return [Post.model_validate(
|
|
215
|
+
return [Post.model_validate(_standardize_post(post)) for post in raw_ql_posts]
|
|
216
|
+
|
|
217
|
+
def get_menu_items_in_lang(self, lang: str) -> list[MenuItem]:
|
|
218
|
+
"""Retrieve menu items for a specific language.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
lang: Language code (e.g., 'en', 'pl')
|
|
102
222
|
|
|
103
|
-
|
|
223
|
+
Returns:
|
|
224
|
+
List of MenuItem objects
|
|
225
|
+
"""
|
|
104
226
|
menu_items = []
|
|
105
227
|
try:
|
|
106
228
|
menu_items_with_lang = gql(
|
|
@@ -132,9 +254,17 @@ class GraphQL(DB):
|
|
|
132
254
|
)
|
|
133
255
|
menu_items = self.client.execute(menu_items_without_lang)
|
|
134
256
|
|
|
135
|
-
return menu_items["menuItems"]
|
|
257
|
+
return [MenuItem.model_validate(item) for item in menu_items["menuItems"]]
|
|
258
|
+
|
|
259
|
+
def get_post(self, slug: str) -> Post:
|
|
260
|
+
"""Retrieve a single post by its slug.
|
|
136
261
|
|
|
137
|
-
|
|
262
|
+
Args:
|
|
263
|
+
slug: URL-friendly identifier for the post
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Post object
|
|
267
|
+
"""
|
|
138
268
|
post = gql(
|
|
139
269
|
"""
|
|
140
270
|
query MyQuery($slug: String!) {
|
|
@@ -169,14 +299,23 @@ class GraphQL(DB):
|
|
|
169
299
|
)
|
|
170
300
|
|
|
171
301
|
post_raw = self.client.execute(post, variable_values={"slug": slug})["post"]
|
|
172
|
-
return Post.model_validate(
|
|
302
|
+
return Post.model_validate(_standardize_post(post_raw))
|
|
173
303
|
|
|
174
|
-
# TODO Cleanup page logic of internationalization (now it depends on translation of slugs)
|
|
175
|
-
def get_page(self, slug):
|
|
176
|
-
|
|
304
|
+
# TODO: Cleanup page logic of internationalization (now it depends on translation of slugs)
|
|
305
|
+
def get_page(self, slug: str) -> Page:
|
|
306
|
+
"""Retrieve a page by its slug.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
slug: URL-friendly identifier for the page
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Page object
|
|
313
|
+
"""
|
|
314
|
+
page_query = gql(
|
|
177
315
|
"""
|
|
178
316
|
query MyQuery ($slug: String!){
|
|
179
317
|
page(where: {slug: $slug}, stage: PUBLISHED) {
|
|
318
|
+
slug
|
|
180
319
|
title
|
|
181
320
|
contentInMarkdown
|
|
182
321
|
coverImage
|
|
@@ -187,9 +326,19 @@ class GraphQL(DB):
|
|
|
187
326
|
}
|
|
188
327
|
"""
|
|
189
328
|
)
|
|
190
|
-
|
|
329
|
+
page_raw = self.client.execute(page_query, variable_values={"slug": slug})["page"]
|
|
330
|
+
return Page.model_validate(_standardize_page(page_raw))
|
|
331
|
+
|
|
332
|
+
def get_posts_by_tag(self, tag: str, lang: str) -> list[Post]:
|
|
333
|
+
"""Retrieve posts filtered by tag and language.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
tag: Tag name to filter by
|
|
337
|
+
lang: Language code (e.g., 'en', 'pl')
|
|
191
338
|
|
|
192
|
-
|
|
339
|
+
Returns:
|
|
340
|
+
List of Post objects
|
|
341
|
+
"""
|
|
193
342
|
post = gql(
|
|
194
343
|
"""
|
|
195
344
|
query MyQuery ($tag: String!, $lang: Lang!){
|
|
@@ -209,9 +358,17 @@ class GraphQL(DB):
|
|
|
209
358
|
}
|
|
210
359
|
"""
|
|
211
360
|
)
|
|
212
|
-
|
|
361
|
+
raw_posts = self.client.execute(post, variable_values={"tag": tag, "lang": lang})["posts"]
|
|
362
|
+
return [Post.model_validate(_standardize_post_by_tag(p)) for p in raw_posts]
|
|
363
|
+
|
|
364
|
+
def add_comment(self, author_name: str, comment: str, post_slug: str) -> None:
|
|
365
|
+
"""Add a new comment to a post.
|
|
213
366
|
|
|
214
|
-
|
|
367
|
+
Args:
|
|
368
|
+
author_name: Name of the comment author
|
|
369
|
+
comment: Comment text content
|
|
370
|
+
post_slug: URL-friendly identifier of the post
|
|
371
|
+
"""
|
|
215
372
|
add_comment = gql(
|
|
216
373
|
"""
|
|
217
374
|
mutation MyMutation($author: String!, $comment: String!, $slug: String!) {
|
|
@@ -236,10 +393,20 @@ class GraphQL(DB):
|
|
|
236
393
|
},
|
|
237
394
|
)
|
|
238
395
|
|
|
239
|
-
def get_font(self):
|
|
240
|
-
|
|
396
|
+
def get_font(self) -> str:
|
|
397
|
+
"""Get the font configuration for the application.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
Empty string (not implemented in GraphQL backend)
|
|
401
|
+
"""
|
|
402
|
+
return ""
|
|
403
|
+
|
|
404
|
+
def get_logo_url(self) -> str:
|
|
405
|
+
"""Retrieve the URL of the application logo.
|
|
241
406
|
|
|
242
|
-
|
|
407
|
+
Returns:
|
|
408
|
+
Logo image URL or empty string if not found
|
|
409
|
+
"""
|
|
243
410
|
logo = gql(
|
|
244
411
|
"""
|
|
245
412
|
query myquery {
|
|
@@ -259,7 +426,15 @@ class GraphQL(DB):
|
|
|
259
426
|
except IndexError:
|
|
260
427
|
return ""
|
|
261
428
|
|
|
262
|
-
def get_app_description(self, lang):
|
|
429
|
+
def get_app_description(self, lang: str) -> str:
|
|
430
|
+
"""Retrieve the application description for a specific language.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
lang: Language code (e.g., 'en', 'pl')
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
Application description text or empty string if not found
|
|
437
|
+
"""
|
|
263
438
|
description_query = gql(
|
|
264
439
|
"""
|
|
265
440
|
query myquery($lang: Lang!) {
|
|
@@ -272,9 +447,14 @@ class GraphQL(DB):
|
|
|
272
447
|
|
|
273
448
|
return self.client.execute(description_query, variable_values={"lang": lang})[
|
|
274
449
|
"applicationSetups"
|
|
275
|
-
][0].get("applicationDescription",
|
|
450
|
+
][0].get("applicationDescription", "")
|
|
276
451
|
|
|
277
|
-
def get_favicon_url(self):
|
|
452
|
+
def get_favicon_url(self) -> str:
|
|
453
|
+
"""Retrieve the URL of the application favicon.
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
Favicon URL
|
|
457
|
+
"""
|
|
278
458
|
favicon = gql(
|
|
279
459
|
"""
|
|
280
460
|
query myquery {
|
|
@@ -295,7 +475,12 @@ class GraphQL(DB):
|
|
|
295
475
|
def get_secondary_color(self) -> str:
|
|
296
476
|
return "navy" # Default color as string
|
|
297
477
|
|
|
298
|
-
def get_plugins_data(self):
|
|
478
|
+
def get_plugins_data(self) -> list[dict[str, Any]]:
|
|
479
|
+
"""Retrieve configuration data for all plugins.
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
List of plugin configuration dictionaries
|
|
483
|
+
"""
|
|
299
484
|
plugins_data = gql(
|
|
300
485
|
"""
|
|
301
486
|
query MyQuery {
|
platzky/db/json_db.py
CHANGED
|
@@ -1,46 +1,92 @@
|
|
|
1
|
+
"""In-memory JSON database implementation."""
|
|
2
|
+
|
|
1
3
|
import datetime
|
|
2
|
-
from typing import Any
|
|
4
|
+
from typing import Any
|
|
3
5
|
|
|
4
6
|
from pydantic import Field
|
|
5
7
|
|
|
6
8
|
from platzky.db.db import DB, DBConfig
|
|
7
|
-
from platzky.models import MenuItem, Post
|
|
9
|
+
from platzky.models import MenuItem, Page, Post
|
|
10
|
+
|
|
8
11
|
|
|
12
|
+
def db_config_type() -> type["JsonDbConfig"]:
|
|
13
|
+
"""Return the configuration class for JSON database.
|
|
9
14
|
|
|
10
|
-
|
|
15
|
+
Returns:
|
|
16
|
+
JsonDbConfig class
|
|
17
|
+
"""
|
|
11
18
|
return JsonDbConfig
|
|
12
19
|
|
|
13
20
|
|
|
14
21
|
class JsonDbConfig(DBConfig):
|
|
15
|
-
|
|
22
|
+
"""Configuration for in-memory JSON database."""
|
|
23
|
+
|
|
24
|
+
data: dict[str, Any] = Field(alias="DATA")
|
|
25
|
+
|
|
16
26
|
|
|
27
|
+
def get_db(config: dict[str, Any]) -> "Json":
|
|
28
|
+
"""Get a JSON database instance from raw configuration.
|
|
17
29
|
|
|
18
|
-
|
|
30
|
+
Args:
|
|
31
|
+
config: Raw configuration dictionary
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Configured JSON database instance
|
|
35
|
+
"""
|
|
19
36
|
json_db_config = JsonDbConfig.model_validate(config)
|
|
20
37
|
return Json(json_db_config.data)
|
|
21
38
|
|
|
22
39
|
|
|
23
|
-
def db_from_config(config: JsonDbConfig):
|
|
24
|
-
|
|
40
|
+
def db_from_config(config: JsonDbConfig) -> "Json":
|
|
41
|
+
"""Create a JSON database instance from configuration.
|
|
25
42
|
|
|
43
|
+
Args:
|
|
44
|
+
config: JSON database configuration
|
|
26
45
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
46
|
+
Returns:
|
|
47
|
+
Configured JSON database instance
|
|
48
|
+
"""
|
|
49
|
+
return Json(config.data)
|
|
30
50
|
|
|
31
51
|
|
|
52
|
+
# TODO: Make all language-specific methods available without language parameter.
|
|
53
|
+
# This will allow a default language and if there is one language,
|
|
54
|
+
# there will be no need to pass it to the method or in db.
|
|
32
55
|
class Json(DB):
|
|
33
|
-
|
|
56
|
+
"""In-memory JSON database implementation."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, data: dict[str, Any]):
|
|
59
|
+
"""Initialize JSON database with data dictionary.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
data: Dictionary containing all database content
|
|
63
|
+
"""
|
|
34
64
|
super().__init__()
|
|
35
|
-
self.data:
|
|
65
|
+
self.data: dict[str, Any] = data
|
|
36
66
|
self.module_name = "json_db"
|
|
37
67
|
self.db_name = "JsonDb"
|
|
38
68
|
|
|
39
|
-
def get_app_description(self, lang):
|
|
69
|
+
def get_app_description(self, lang: str) -> str:
|
|
70
|
+
"""Retrieve the application description for a specific language.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
lang: Language code (e.g., 'en', 'pl')
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Application description text or empty string if not found
|
|
77
|
+
"""
|
|
40
78
|
description = self._get_site_content().get("app_description", {})
|
|
41
|
-
return description.get(lang,
|
|
79
|
+
return description.get(lang, "")
|
|
80
|
+
|
|
81
|
+
def get_all_posts(self, lang: str) -> list[Post]:
|
|
82
|
+
"""Retrieve all posts for a specific language.
|
|
42
83
|
|
|
43
|
-
|
|
84
|
+
Args:
|
|
85
|
+
lang: Language code (e.g., 'en', 'pl')
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
List of Post objects
|
|
89
|
+
"""
|
|
44
90
|
return [
|
|
45
91
|
Post.model_validate(post)
|
|
46
92
|
for post in self._get_site_content().get("posts", ())
|
|
@@ -48,7 +94,17 @@ class Json(DB):
|
|
|
48
94
|
]
|
|
49
95
|
|
|
50
96
|
def get_post(self, slug: str) -> Post:
|
|
51
|
-
"""Returns a post matching the given slug.
|
|
97
|
+
"""Returns a post matching the given slug.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
slug: URL-friendly identifier for the post
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Post object
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
ValueError: If posts data is missing or post not found
|
|
107
|
+
"""
|
|
52
108
|
all_posts = self._get_site_content().get("posts")
|
|
53
109
|
if all_posts is None:
|
|
54
110
|
raise ValueError("Posts data is missing")
|
|
@@ -57,54 +113,127 @@ class Json(DB):
|
|
|
57
113
|
raise ValueError(f"Post with slug {slug} not found")
|
|
58
114
|
return Post.model_validate(wanted_post)
|
|
59
115
|
|
|
60
|
-
# TODO:
|
|
61
|
-
def get_page(self, slug):
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
116
|
+
# TODO: Add test for non-existing page
|
|
117
|
+
def get_page(self, slug: str) -> Page:
|
|
118
|
+
"""Retrieve a page by its slug.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
slug: URL-friendly identifier for the page
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Page object
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
ValueError: If pages data is missing or page not found
|
|
128
|
+
"""
|
|
129
|
+
pages = self._get_site_content().get("pages")
|
|
130
|
+
if pages is None:
|
|
131
|
+
raise ValueError("Pages data is missing")
|
|
132
|
+
list_of_pages = (page for page in pages if page["slug"] == slug)
|
|
133
|
+
wanted_page = next(list_of_pages, None)
|
|
134
|
+
if wanted_page is None:
|
|
135
|
+
raise ValueError(f"Page with slug {slug} not found")
|
|
136
|
+
page = Page.model_validate(wanted_page)
|
|
66
137
|
return page
|
|
67
138
|
|
|
68
|
-
def get_menu_items_in_lang(self, lang) -> list[MenuItem]:
|
|
139
|
+
def get_menu_items_in_lang(self, lang: str) -> list[MenuItem]:
|
|
140
|
+
"""Retrieve menu items for a specific language.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
lang: Language code (e.g., 'en', 'pl')
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
List of MenuItem objects
|
|
147
|
+
"""
|
|
69
148
|
menu_items_raw = self._get_site_content().get("menu_items", {})
|
|
70
149
|
items_in_lang = menu_items_raw.get(lang, {})
|
|
71
150
|
|
|
72
151
|
menu_items_list = [MenuItem.model_validate(x) for x in items_in_lang]
|
|
73
152
|
return menu_items_list
|
|
74
153
|
|
|
75
|
-
def get_posts_by_tag(self, tag, lang):
|
|
76
|
-
|
|
77
|
-
|
|
154
|
+
def get_posts_by_tag(self, tag: str, lang: str) -> list[Post]:
|
|
155
|
+
"""Retrieve posts filtered by tag and language.
|
|
156
|
+
|
|
157
|
+
Returns a list of posts, unlike generators which can only be iterated once.
|
|
158
|
+
"""
|
|
159
|
+
return [
|
|
160
|
+
Post.model_validate(post)
|
|
78
161
|
for post in self._get_site_content()["posts"]
|
|
79
162
|
if tag in post["tags"] and post["language"] == lang
|
|
80
|
-
|
|
163
|
+
]
|
|
81
164
|
|
|
82
|
-
def _get_site_content(self):
|
|
165
|
+
def _get_site_content(self) -> dict[str, Any]:
|
|
166
|
+
"""Get the site content dictionary from data.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Site content dictionary
|
|
170
|
+
|
|
171
|
+
Raises:
|
|
172
|
+
ValueError: If site content is not found
|
|
173
|
+
"""
|
|
83
174
|
content = self.data.get("site_content")
|
|
84
175
|
if content is None:
|
|
85
|
-
raise
|
|
176
|
+
raise ValueError("Content should not be None")
|
|
86
177
|
return content
|
|
87
178
|
|
|
88
|
-
def get_logo_url(self):
|
|
179
|
+
def get_logo_url(self) -> str:
|
|
180
|
+
"""Retrieve the URL of the application logo.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Logo image URL or empty string if not found
|
|
184
|
+
"""
|
|
89
185
|
return self._get_site_content().get("logo_url", "")
|
|
90
186
|
|
|
91
|
-
def get_favicon_url(self):
|
|
187
|
+
def get_favicon_url(self) -> str:
|
|
188
|
+
"""Retrieve the URL of the application favicon.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Favicon URL or empty string if not found
|
|
192
|
+
"""
|
|
92
193
|
return self._get_site_content().get("favicon_url", "")
|
|
93
194
|
|
|
94
195
|
def get_font(self) -> str:
|
|
196
|
+
"""Get the font configuration for the application.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Font name or empty string if not configured
|
|
200
|
+
"""
|
|
95
201
|
return self._get_site_content().get("font", "")
|
|
96
202
|
|
|
97
|
-
def get_primary_color(self):
|
|
203
|
+
def get_primary_color(self) -> str:
|
|
204
|
+
"""Retrieve the primary color for the application theme.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Primary color value, defaults to 'white'
|
|
208
|
+
"""
|
|
98
209
|
return self._get_site_content().get("primary_color", "white")
|
|
99
210
|
|
|
100
|
-
def get_secondary_color(self):
|
|
211
|
+
def get_secondary_color(self) -> str:
|
|
212
|
+
"""Retrieve the secondary color for the application theme.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Secondary color value, defaults to 'navy'
|
|
216
|
+
"""
|
|
101
217
|
return self._get_site_content().get("secondary_color", "navy")
|
|
102
218
|
|
|
103
|
-
def add_comment(self, author_name, comment, post_slug):
|
|
104
|
-
comment
|
|
219
|
+
def add_comment(self, author_name: str, comment: str, post_slug: str) -> None:
|
|
220
|
+
"""Add a new comment to a post.
|
|
221
|
+
|
|
222
|
+
Store dates in UTC with timezone info for consistency with MongoDB backend.
|
|
223
|
+
This ensures accurate time delta calculations regardless of server timezone.
|
|
224
|
+
Legacy dates without timezone info are still supported for backward compatibility.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
author_name: Name of the comment author
|
|
228
|
+
comment: Comment text content
|
|
229
|
+
post_slug: URL-friendly identifier of the post
|
|
230
|
+
"""
|
|
231
|
+
now_utc = datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds")
|
|
232
|
+
|
|
233
|
+
comment_data = {
|
|
105
234
|
"author": str(author_name),
|
|
106
235
|
"comment": str(comment),
|
|
107
|
-
"date":
|
|
236
|
+
"date": now_utc,
|
|
108
237
|
}
|
|
109
238
|
|
|
110
239
|
post_index = next(
|
|
@@ -112,9 +241,14 @@ class Json(DB):
|
|
|
112
241
|
for i in range(len(self._get_site_content()["posts"]))
|
|
113
242
|
if self._get_site_content()["posts"][i]["slug"] == post_slug
|
|
114
243
|
)
|
|
115
|
-
self._get_site_content()["posts"][post_index]["comments"].append(
|
|
244
|
+
self._get_site_content()["posts"][post_index]["comments"].append(comment_data)
|
|
245
|
+
|
|
246
|
+
def get_plugins_data(self) -> list[dict[str, Any]]:
|
|
247
|
+
"""Retrieve configuration data for all plugins.
|
|
116
248
|
|
|
117
|
-
|
|
249
|
+
Returns:
|
|
250
|
+
List of plugin configuration dictionaries
|
|
251
|
+
"""
|
|
118
252
|
return self.data.get("plugins", [])
|
|
119
253
|
|
|
120
254
|
def health_check(self) -> None:
|