focomy 0.1.114__py3-none-any.whl → 0.1.116__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.
- core/admin/routes.py +62 -0
- core/engine/routes.py +53 -0
- core/services/channel.py +58 -0
- core/templates/admin/entity_form.html +29 -0
- {focomy-0.1.114.dist-info → focomy-0.1.116.dist-info}/METADATA +1 -1
- {focomy-0.1.114.dist-info → focomy-0.1.116.dist-info}/RECORD +9 -8
- {focomy-0.1.114.dist-info → focomy-0.1.116.dist-info}/WHEEL +0 -0
- {focomy-0.1.114.dist-info → focomy-0.1.116.dist-info}/entry_points.txt +0 -0
- {focomy-0.1.114.dist-info → focomy-0.1.116.dist-info}/licenses/LICENSE +0 -0
core/admin/routes.py
CHANGED
|
@@ -2020,6 +2020,38 @@ async def preview_render(
|
|
|
2020
2020
|
return {"html": html}
|
|
2021
2021
|
|
|
2022
2022
|
|
|
2023
|
+
@router.post("/api/preview/token")
|
|
2024
|
+
async def create_preview_token(
|
|
2025
|
+
request: Request,
|
|
2026
|
+
db: AsyncSession = Depends(get_db),
|
|
2027
|
+
current_user: Entity = Depends(require_admin_api),
|
|
2028
|
+
):
|
|
2029
|
+
"""Create a preview token for an entity."""
|
|
2030
|
+
from ..services.preview import get_preview_service
|
|
2031
|
+
|
|
2032
|
+
try:
|
|
2033
|
+
body = await request.json()
|
|
2034
|
+
except Exception:
|
|
2035
|
+
raise HTTPException(status_code=400, detail="Invalid JSON")
|
|
2036
|
+
|
|
2037
|
+
entity_id = body.get("entity_id")
|
|
2038
|
+
if not entity_id:
|
|
2039
|
+
raise HTTPException(status_code=400, detail="entity_id required")
|
|
2040
|
+
|
|
2041
|
+
# Verify entity exists
|
|
2042
|
+
entity_svc = EntityService(db)
|
|
2043
|
+
entity = await entity_svc.get(entity_id)
|
|
2044
|
+
if not entity:
|
|
2045
|
+
raise HTTPException(status_code=404, detail="Entity not found")
|
|
2046
|
+
|
|
2047
|
+
# Create preview token
|
|
2048
|
+
preview_svc = get_preview_service(db)
|
|
2049
|
+
token = await preview_svc.create_token(entity_id, current_user.id)
|
|
2050
|
+
preview_url = preview_svc.get_preview_url(token)
|
|
2051
|
+
|
|
2052
|
+
return {"token": token, "url": preview_url}
|
|
2053
|
+
|
|
2054
|
+
|
|
2023
2055
|
@router.get("/system", response_class=HTMLResponse)
|
|
2024
2056
|
async def system_info(
|
|
2025
2057
|
request: Request,
|
|
@@ -4188,6 +4220,16 @@ async def entity_create(
|
|
|
4188
4220
|
if rel_ids:
|
|
4189
4221
|
await relation_svc.sync(entity.id, rel_ids, rel.type)
|
|
4190
4222
|
|
|
4223
|
+
# Auto-assign posts channel for post type if not specified
|
|
4224
|
+
if type_name == "post":
|
|
4225
|
+
rel_channel_values = form_data.getlist("rel_post_channel")
|
|
4226
|
+
channel_ids = [v for v in rel_channel_values if v]
|
|
4227
|
+
if not channel_ids:
|
|
4228
|
+
from ..services.channel import get_or_create_posts_channel
|
|
4229
|
+
|
|
4230
|
+
posts_channel_id = await get_or_create_posts_channel(db)
|
|
4231
|
+
await relation_svc.sync(entity.id, [posts_channel_id], "post_channel")
|
|
4232
|
+
|
|
4191
4233
|
# Log create action
|
|
4192
4234
|
audit_svc = AuditService(db)
|
|
4193
4235
|
await audit_svc.log_create(
|
|
@@ -4495,6 +4537,16 @@ async def entity_delete(
|
|
|
4495
4537
|
entity_data = entity_svc.serialize(entity)
|
|
4496
4538
|
user_data = entity_svc.serialize(current_user)
|
|
4497
4539
|
|
|
4540
|
+
# Check if channel is protected from deletion
|
|
4541
|
+
if type_name == "channel":
|
|
4542
|
+
from ..services.channel import is_protected_channel
|
|
4543
|
+
|
|
4544
|
+
if is_protected_channel(entity_data.get("slug", "")):
|
|
4545
|
+
raise HTTPException(
|
|
4546
|
+
status_code=400,
|
|
4547
|
+
detail="postsチャンネルは削除できません",
|
|
4548
|
+
)
|
|
4549
|
+
|
|
4498
4550
|
await entity_svc.delete(entity_id, user_id=user_data.get("id"))
|
|
4499
4551
|
|
|
4500
4552
|
# Log delete action
|
|
@@ -4538,6 +4590,16 @@ async def entity_delete_post(
|
|
|
4538
4590
|
entity_data = entity_svc.serialize(entity)
|
|
4539
4591
|
user_data = entity_svc.serialize(current_user)
|
|
4540
4592
|
|
|
4593
|
+
# Check if channel is protected from deletion
|
|
4594
|
+
if type_name == "channel":
|
|
4595
|
+
from ..services.channel import is_protected_channel
|
|
4596
|
+
|
|
4597
|
+
if is_protected_channel(entity_data.get("slug", "")):
|
|
4598
|
+
raise HTTPException(
|
|
4599
|
+
status_code=400,
|
|
4600
|
+
detail="postsチャンネルは削除できません",
|
|
4601
|
+
)
|
|
4602
|
+
|
|
4541
4603
|
await entity_svc.delete(entity_id, user_id=user_data.get("id"))
|
|
4542
4604
|
|
|
4543
4605
|
# Log delete action
|
core/engine/routes.py
CHANGED
|
@@ -261,6 +261,59 @@ async def sitemap_xml(
|
|
|
261
261
|
return Response(content=xml_content, media_type="application/xml")
|
|
262
262
|
|
|
263
263
|
|
|
264
|
+
# === Preview Routes ===
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@router.get("/preview/{token}", response_class=HTMLResponse)
|
|
268
|
+
async def preview_entity(
|
|
269
|
+
token: str,
|
|
270
|
+
request: Request,
|
|
271
|
+
db: AsyncSession = Depends(get_db),
|
|
272
|
+
):
|
|
273
|
+
"""Preview an entity with a valid token (shows draft/unpublished content)."""
|
|
274
|
+
from ..services.preview import get_preview_service
|
|
275
|
+
|
|
276
|
+
preview_svc = get_preview_service(db)
|
|
277
|
+
entity = await preview_svc.get_preview_entity(token)
|
|
278
|
+
|
|
279
|
+
if not entity:
|
|
280
|
+
raise HTTPException(status_code=404, detail="Preview not found or expired")
|
|
281
|
+
|
|
282
|
+
entity_svc = EntityService(db)
|
|
283
|
+
entity_data = entity_svc.serialize(entity)
|
|
284
|
+
|
|
285
|
+
# Get content type info
|
|
286
|
+
content_type = entity.type
|
|
287
|
+
ct = field_service.get_content_type(content_type)
|
|
288
|
+
|
|
289
|
+
# Get site URL and contexts
|
|
290
|
+
site_url = str(request.base_url).rstrip("/")
|
|
291
|
+
menus_ctx = await get_menus_context(db)
|
|
292
|
+
widgets_ctx = await get_widgets_context(db)
|
|
293
|
+
seo_ctx = await get_seo_settings(db, site_url)
|
|
294
|
+
|
|
295
|
+
# Add preview flag to context
|
|
296
|
+
context = {
|
|
297
|
+
"post": entity_data,
|
|
298
|
+
"entity": entity_data,
|
|
299
|
+
"content": entity_data,
|
|
300
|
+
"is_preview": True,
|
|
301
|
+
"content_type": ct,
|
|
302
|
+
**menus_ctx,
|
|
303
|
+
**widgets_ctx,
|
|
304
|
+
**seo_ctx,
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
# Determine template
|
|
308
|
+
template = "post.html"
|
|
309
|
+
if ct and ct.template:
|
|
310
|
+
template = ct.template
|
|
311
|
+
|
|
312
|
+
html = await render_theme(db, template, context, request=request)
|
|
313
|
+
|
|
314
|
+
return HTMLResponse(content=html)
|
|
315
|
+
|
|
316
|
+
|
|
264
317
|
async def get_menus_context(db: AsyncSession) -> dict:
|
|
265
318
|
"""Get menus context for templates."""
|
|
266
319
|
menu_svc = MenuService(db)
|
core/services/channel.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Channel service - Default channel management."""
|
|
2
|
+
|
|
3
|
+
import structlog
|
|
4
|
+
|
|
5
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
6
|
+
|
|
7
|
+
from .entity import EntityService
|
|
8
|
+
|
|
9
|
+
logger = structlog.get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
DEFAULT_CHANNEL_SLUG = "posts"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def get_or_create_posts_channel(db: AsyncSession) -> str:
|
|
15
|
+
"""Get or create the default 'posts' channel.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Channel entity ID
|
|
19
|
+
"""
|
|
20
|
+
entity_svc = EntityService(db)
|
|
21
|
+
|
|
22
|
+
# Try to find existing posts channel
|
|
23
|
+
entities = await entity_svc.find(
|
|
24
|
+
"channel",
|
|
25
|
+
filters={"slug": DEFAULT_CHANNEL_SLUG},
|
|
26
|
+
limit=1,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if entities:
|
|
30
|
+
channel_id = entities[0].id
|
|
31
|
+
logger.debug("found_posts_channel", channel_id=channel_id)
|
|
32
|
+
return channel_id
|
|
33
|
+
|
|
34
|
+
# Create posts channel
|
|
35
|
+
entity = await entity_svc.create(
|
|
36
|
+
"channel",
|
|
37
|
+
{
|
|
38
|
+
"title": "Posts",
|
|
39
|
+
"slug": DEFAULT_CHANNEL_SLUG,
|
|
40
|
+
"description": "Default channel for posts",
|
|
41
|
+
"sort_order": 0,
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
logger.info("created_posts_channel", channel_id=entity.id)
|
|
46
|
+
return entity.id
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_protected_channel(slug: str) -> bool:
|
|
50
|
+
"""Check if a channel is protected from deletion.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
slug: Channel slug
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
True if protected
|
|
57
|
+
"""
|
|
58
|
+
return slug == DEFAULT_CHANNEL_SLUG
|
|
@@ -388,6 +388,7 @@
|
|
|
388
388
|
<a href="{{ cancel_url }}" class="btn btn-secondary">Cancel</a>
|
|
389
389
|
{% if entity %}
|
|
390
390
|
<button type="button" class="btn btn-secondary" onclick="toggleRevisions()">History</button>
|
|
391
|
+
<button type="button" class="btn btn-secondary" onclick="openPreview()">Preview</button>
|
|
391
392
|
{% endif %}
|
|
392
393
|
<button type="submit" class="btn btn-primary">
|
|
393
394
|
{% if entity %}Update{% else %}Create{% endif %}
|
|
@@ -1239,6 +1240,34 @@ function toggleRevisions() {
|
|
|
1239
1240
|
}
|
|
1240
1241
|
}
|
|
1241
1242
|
|
|
1243
|
+
// Preview in new tab
|
|
1244
|
+
async function openPreview() {
|
|
1245
|
+
const entityId = '{{ entity.id if entity else "" }}';
|
|
1246
|
+
if (!entityId) {
|
|
1247
|
+
alert('Please save the content first before previewing.');
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
try {
|
|
1252
|
+
const response = await fetch('/admin/api/preview/token', {
|
|
1253
|
+
method: 'POST',
|
|
1254
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1255
|
+
body: JSON.stringify({ entity_id: entityId })
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
if (!response.ok) {
|
|
1259
|
+
const error = await response.json();
|
|
1260
|
+
alert('Preview error: ' + (error.detail || 'Unknown error'));
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
const data = await response.json();
|
|
1265
|
+
window.open(data.url, '_blank');
|
|
1266
|
+
} catch (e) {
|
|
1267
|
+
alert('Preview error: ' + e.message);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1242
1271
|
async function loadRevisions() {
|
|
1243
1272
|
const entityId = '{{ entity.id }}';
|
|
1244
1273
|
const list = document.getElementById('revisions-list');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: focomy
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.116
|
|
4
4
|
Summary: The Most Beautiful CMS - A metadata-driven, zero-duplicate-code content management system
|
|
5
5
|
Project-URL: Homepage, https://github.com/focomy/focomy
|
|
6
6
|
Project-URL: Documentation, https://focomy.dev/docs
|
|
@@ -7,7 +7,7 @@ core/rate_limit.py,sha256=CX5UjmsU03aFWKXSKjweoHvH2xn0v4NBHNN5ynJC8LE,180
|
|
|
7
7
|
core/relations.yaml,sha256=7GUCrphKaouEXNkyd8Ht99e6TcUPERhc4m36RGcc41U,2128
|
|
8
8
|
core/utils.py,sha256=Rqs1WStB0JjTb8-750jL-xJ_kaPH3ddupvqt46BXIBc,2754
|
|
9
9
|
core/admin/__init__.py,sha256=IXrr-z-IDXmYodaZ-cVDou6wr_vsVhyWmXHdSNKkQsk,94
|
|
10
|
-
core/admin/routes.py,sha256=
|
|
10
|
+
core/admin/routes.py,sha256=U6NCzZNjOwdRzU-pzktVbPWYUTNExjNnaMfgzW0X_Hw,150612
|
|
11
11
|
core/admin/url.py,sha256=FlusKnSz3bZgPSBmRu-dI3W-bQo7lKBDZ3zN8cFHwQc,2243
|
|
12
12
|
core/api/__init__.py,sha256=H1StbYGDVRS6g-Jk3UUf17ibAz1K8IUa27NfPMkaNrA,19
|
|
13
13
|
core/api/auth.py,sha256=Zb37IHcUSjf8_hXiVzhoZPQw6WAiOOS_AoMqE96yat8,11565
|
|
@@ -44,7 +44,7 @@ core/content_types/user.yaml,sha256=y3SwqzIc9_6C7R1GULk7AwYJPxcTT38ZmZe4_wekfyU,
|
|
|
44
44
|
core/content_types/widget.yaml,sha256=Jotbts5QQtHaF2bJWQL3rkEoCkp_aq_A3gN-58eJwv8,1454
|
|
45
45
|
core/content_types/workflow_history.yaml,sha256=3wi58LNLYbk7t6Z2QDRi9whQSedJCXKVKuyBhixNUK0,518
|
|
46
46
|
core/engine/__init__.py,sha256=ycR0Kdn6buwdCH6QFG8bV69wFciFSKEg9Ro26cHpa2U,83
|
|
47
|
-
core/engine/routes.py,sha256=
|
|
47
|
+
core/engine/routes.py,sha256=W09MJIEddsQbsmeyGd2P9nBZUDwDlTpOY4O-DvUd0_4,43849
|
|
48
48
|
core/migrations/env.py,sha256=1dLI8qcGojLDR_--MdgwP5q-V0p2Z-32klSPjokXx4M,1389
|
|
49
49
|
core/migrations/script.py.mako,sha256=LyYLSC7HzBBGwHZ8s2SguBPMXsWCph0FJp49kPsGhU8,590
|
|
50
50
|
core/migrations/versions/2038bdf6693b_add_import_jobs_table.py,sha256=v8lPC5WmwpUfHUG_YgQn6jepPtfKWFn0JIj9XvD9224,2325
|
|
@@ -83,6 +83,7 @@ core/services/auth.py,sha256=tb_ixUqD0UG2wStilExFxfqTfmW86Rv63svisPDBKCA,19094
|
|
|
83
83
|
core/services/block_converter.py,sha256=31iP5TP4eXhar_wF3ojDZd0vmQe9rg5p4QAhDXzyJpg,28110
|
|
84
84
|
core/services/bulk.py,sha256=xdaLyP7s3mTBV_7t2F5VT5mePNqOvba4MNcnipdyqno,7436
|
|
85
85
|
core/services/cache.py,sha256=utvAHobZGbF4zaWXPwcB5NIZmClYGT1XXuQjB2C3FFQ,12723
|
|
86
|
+
core/services/channel.py,sha256=s2puzRqSCRLzSvWuKU6q5B1nTN_Ot1dNbo_1JDOUimI,1324
|
|
86
87
|
core/services/cleanup.py,sha256=kgZCheADrJY21Qn4NAlnh3Li7vEksNDXv8lcot1p6Ew,6686
|
|
87
88
|
core/services/comment.py,sha256=SOfsFW46vhjw-C050fLim9vXifUogdpYKsEF7tghtz4,8705
|
|
88
89
|
core/services/config_priority.py,sha256=F4OzreN9WHE69rCTWx_UwgN2DDFj3rU1VK-PTFbaddw,10832
|
|
@@ -153,7 +154,7 @@ core/templates/admin/base.html,sha256=yWQNumbtcLIE0iffIoDB2xTF-9W5bt2mdsMYqA5QOa
|
|
|
153
154
|
core/templates/admin/comments.html,sha256=AEzCMYu1-EdFC1yUa9oIObtRS-U9RlMTe_5P3O5alb8,10012
|
|
154
155
|
core/templates/admin/customize.html,sha256=EwncOC0AgDCSZRum5PZzF-KDYNjrhPish57KMuff4sE,12260
|
|
155
156
|
core/templates/admin/dashboard.html,sha256=WzURfANu1k9AIjdP82fwJE1o3sRhNVr77iUCRfUoBm8,3439
|
|
156
|
-
core/templates/admin/entity_form.html,sha256=
|
|
157
|
+
core/templates/admin/entity_form.html,sha256=lX2l7iS6psm29wh1tHcK-4tArhG65gJxmCDInUSqV0c,61161
|
|
157
158
|
core/templates/admin/entity_list.html,sha256=DxaM2c_1UGdPpQHTIe7LrFxjC11f8hXDy-9C-T8LO6o,7966
|
|
158
159
|
core/templates/admin/forgot_password.html,sha256=j2r9N5G8T5zKODQu7IVc0NdRvSVuyXBA9FCLn7egp1Q,2320
|
|
159
160
|
core/templates/admin/import.html,sha256=2b2Bu9Do1pQBUgsseowt19oaf0vEfGzTOCJ8Rr-Nphc,48385
|
|
@@ -201,8 +202,8 @@ themes/minimal/templates/base.html,sha256=LFkx-XLDMGH7oFHHa0e6KPB0DJITOBvr6GtPkD
|
|
|
201
202
|
themes/minimal/templates/home.html,sha256=ygYQgYj1OGCiKwmfsxwkPselVKT8vDH3jLLbfphpqKI,1577
|
|
202
203
|
themes/minimal/templates/page.html,sha256=7Xcoq-ryaxlp913H2S1ishrAro2wsqqGmvsm1osXxd4,389
|
|
203
204
|
themes/minimal/templates/post.html,sha256=FkTRHci8HNIIi3DU6Mb3oL0aDisGyDcsT_IUDwHmrvo,1387
|
|
204
|
-
focomy-0.1.
|
|
205
|
-
focomy-0.1.
|
|
206
|
-
focomy-0.1.
|
|
207
|
-
focomy-0.1.
|
|
208
|
-
focomy-0.1.
|
|
205
|
+
focomy-0.1.116.dist-info/METADATA,sha256=V_aSIaMDxBQFDEfNqMY9QVREcNFBp1686Z8lAIRp6sc,7042
|
|
206
|
+
focomy-0.1.116.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
207
|
+
focomy-0.1.116.dist-info/entry_points.txt,sha256=_rF-wxGI1axY7gox3DBsTLHq-JrFKkMCjA65a6b_oqE,41
|
|
208
|
+
focomy-0.1.116.dist-info/licenses/LICENSE,sha256=z9Z7gN7NNV7zYCaY-Knh3bv8RBCu89VueYtAlN_-lro,1063
|
|
209
|
+
focomy-0.1.116.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|