focomy 0.1.106__py3-none-any.whl → 0.1.108__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/api/forms.py CHANGED
@@ -13,6 +13,7 @@ from ..rate_limit import limiter
13
13
  from ..services.entity import EntityService
14
14
  from ..services.mail import mail_service
15
15
  from ..services.theme import theme_service
16
+ from ..utils import require_feature_async
16
17
 
17
18
  router = APIRouter(prefix="/forms", tags=["forms"])
18
19
 
@@ -30,6 +31,7 @@ async def view_form(
30
31
  db: AsyncSession = Depends(get_db),
31
32
  ):
32
33
  """View a public form."""
34
+ await require_feature_async("form", db)
33
35
  entity_svc = EntityService(db)
34
36
 
35
37
  # Find form
@@ -83,6 +85,7 @@ async def submit_form(
83
85
  db: AsyncSession = Depends(get_db),
84
86
  ):
85
87
  """Submit a form."""
88
+ await require_feature_async("form", db)
86
89
  entity_svc = EntityService(db)
87
90
 
88
91
  # Find form
core/config.py CHANGED
@@ -143,7 +143,7 @@ class FeaturesConfig(BaseModel):
143
143
 
144
144
  # Phase 5: Extended features
145
145
  comment: bool = False
146
- form: bool = False
146
+ form: bool = True
147
147
  api_auth: bool = False
148
148
  oauth: bool = False
149
149
  mail: bool = False
core/engine/routes.py CHANGED
@@ -163,6 +163,13 @@ async def render_theme(
163
163
  # Build edit URL if entity provided
164
164
  if entity and content_type:
165
165
  context["edit_url"] = f"/admin/{content_type}/{entity.id}/edit"
166
+ # Add content_types for admin bar dropdown
167
+ all_ct = field_service.get_all_content_types()
168
+ context["content_types"] = {
169
+ name: ct.model_dump()
170
+ for name, ct in all_ct.items()
171
+ if ct.admin_menu # Only show types with admin_menu=true
172
+ }
166
173
  else:
167
174
  context["is_admin"] = False
168
175
 
core/main.py CHANGED
@@ -452,7 +452,6 @@ async def server_error_handler(request: Request, exc: Exception):
452
452
  from .admin import routes as admin
453
453
  from .api import auth, comments, entities, forms, media, relations, revisions, schema, search, seo
454
454
  from .engine import routes as engine
455
- from .utils import is_feature_enabled
456
455
 
457
456
  # Phase 1: Core APIs (always enabled)
458
457
  app.include_router(entities.router, prefix="/api")
@@ -462,21 +461,16 @@ app.include_router(auth.router, prefix="/api")
462
461
  app.include_router(seo.router)
463
462
  app.include_router(admin.router)
464
463
 
465
- # Phase 2: Media
466
- if is_feature_enabled("media"):
467
- app.include_router(media.router, prefix="/api")
468
-
469
- # Phase 4: Search, Revisions
470
- if is_feature_enabled("search"):
471
- app.include_router(search.router, prefix="/api")
472
- if is_feature_enabled("revision"):
473
- app.include_router(revisions.router, prefix="/api")
474
-
475
- # Phase 5: Comments, Forms
476
- if is_feature_enabled("comment"):
477
- app.include_router(comments.router, prefix="/api")
478
- if is_feature_enabled("form"):
479
- app.include_router(forms.router)
464
+ # Phase 2: Media (runtime check in endpoints)
465
+ app.include_router(media.router, prefix="/api")
466
+
467
+ # Phase 4: Search, Revisions (runtime check in endpoints)
468
+ app.include_router(search.router, prefix="/api")
469
+ app.include_router(revisions.router, prefix="/api")
470
+
471
+ # Phase 5: Comments, Forms (runtime check in endpoints)
472
+ app.include_router(comments.router, prefix="/api")
473
+ app.include_router(forms.router)
480
474
 
481
475
 
482
476
  @app.get("/api/health")
core/services/settings.py CHANGED
@@ -39,6 +39,14 @@ DEFAULT_SETTINGS = {
39
39
  "lockout_duration": 900,
40
40
  "password_min_length": 12,
41
41
  },
42
+ "features": {
43
+ "media": True,
44
+ "comment": False,
45
+ "form": True,
46
+ "wordpress_import": False,
47
+ "menu": True,
48
+ "widget": True,
49
+ },
42
50
  }
43
51
 
44
52
 
@@ -235,6 +243,7 @@ class SettingsService:
235
243
  "media": app_settings.media,
236
244
  "security": app_settings.security,
237
245
  "theme": app_settings.theme,
246
+ "features": app_settings.features,
238
247
  }
239
248
 
240
249
  config_obj = config_map.get(category)
@@ -253,6 +262,7 @@ class SettingsService:
253
262
  "media": app_settings.media,
254
263
  "security": app_settings.security,
255
264
  "theme": app_settings.theme,
265
+ "features": app_settings.features,
256
266
  }
257
267
 
258
268
  categories = [category] if category else config_map.keys()
core/utils.py CHANGED
@@ -43,3 +43,48 @@ def require_feature(feature: str) -> None:
43
43
  """
44
44
  if not is_feature_enabled(feature):
45
45
  raise HTTPException(status_code=404, detail="Not Found")
46
+
47
+
48
+ async def is_feature_enabled_async(feature: str, db) -> bool:
49
+ """Check if a feature is enabled (async version, DB-first).
50
+
51
+ Priority: DB settings > config.yaml > False
52
+
53
+ Args:
54
+ feature: Feature name (e.g., 'form', 'comment')
55
+ db: AsyncSession database session
56
+
57
+ Returns:
58
+ True if feature is enabled, False otherwise
59
+ """
60
+ from .services.settings import SettingsService
61
+
62
+ settings_svc = SettingsService(db)
63
+ db_value = await settings_svc.get(f"features.{feature}")
64
+
65
+ if db_value is not None:
66
+ # DB value exists, use it
67
+ if isinstance(db_value, bool):
68
+ return db_value
69
+ if isinstance(db_value, str):
70
+ return db_value.lower() in ("true", "1", "yes")
71
+ return bool(db_value)
72
+
73
+ # Fallback to config.yaml
74
+ return is_feature_enabled(feature)
75
+
76
+
77
+ async def require_feature_async(feature: str, db) -> None:
78
+ """Raise 404 if feature is disabled (async version, DB-first).
79
+
80
+ Use this at the start of API endpoints for dynamic feature checking.
81
+
82
+ Args:
83
+ feature: Feature name
84
+ db: AsyncSession database session
85
+
86
+ Raises:
87
+ HTTPException: 404 if feature is disabled
88
+ """
89
+ if not await is_feature_enabled_async(feature, db):
90
+ raise HTTPException(status_code=404, detail="Not Found")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: focomy
3
- Version: 0.1.106
3
+ Version: 0.1.108
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
@@ -1,11 +1,11 @@
1
1
  core/__init__.py,sha256=SHwUSW_pu27kEARvwJFRdY2ZkVu6wzFh0eikYJXjbTY,1123
2
2
  core/cli.py,sha256=h5jnf4EJCZ1JZMnDl7W1IaL153UD3dXyKBbxhv1P-KA,35536
3
- core/config.py,sha256=kLhM1yF3rU9KtJ-DiD8pnzG_tFQsiO8G-M_ffN_yKqE,8200
3
+ core/config.py,sha256=VfRNfh5dAw9ax1MIjzaYN_KEvcNjy7X5-LxE22Rm2-I,8199
4
4
  core/database.py,sha256=wqvRuwpDxy25svag9NqreVcNA_VRDDjWsxOMUopsffs,2222
5
- core/main.py,sha256=em4na_OJu6E3y_nkeYxH_QvugiLdPzKCMbc5Jbc1y1k,16783
5
+ core/main.py,sha256=gc2s5La1qC6WCEq1Bc3UoUK1kKRzO7866-xSeYs_L4I,16647
6
6
  core/rate_limit.py,sha256=CX5UjmsU03aFWKXSKjweoHvH2xn0v4NBHNN5ynJC8LE,180
7
7
  core/relations.yaml,sha256=7GUCrphKaouEXNkyd8Ht99e6TcUPERhc4m36RGcc41U,2128
8
- core/utils.py,sha256=7myy64jI7T4WQ_C3Q7j0RPaYKQhfVzGkpNrw9c4yakc,1203
8
+ core/utils.py,sha256=-umiWA5dRuEb9cGuL8QDeZu8khvtcW3jUgb7V4AA4Sc,2521
9
9
  core/admin/__init__.py,sha256=IXrr-z-IDXmYodaZ-cVDou6wr_vsVhyWmXHdSNKkQsk,94
10
10
  core/admin/routes.py,sha256=h9yl5THrlM74ZUPiiFoAlAVN-p3kMR1h-UH_xK1_T0k,147979
11
11
  core/admin/url.py,sha256=FlusKnSz3bZgPSBmRu-dI3W-bQo7lKBDZ3zN8cFHwQc,2243
@@ -13,7 +13,7 @@ core/api/__init__.py,sha256=H1StbYGDVRS6g-Jk3UUf17ibAz1K8IUa27NfPMkaNrA,19
13
13
  core/api/auth.py,sha256=Zb37IHcUSjf8_hXiVzhoZPQw6WAiOOS_AoMqE96yat8,11565
14
14
  core/api/comments.py,sha256=Eu_0a_JqgiOP1GPsPoslxDztkCv_M324x9qAuBOWPOE,8397
15
15
  core/api/entities.py,sha256=sAS12hgjt0Zgudrq2BzfXdCU25myh-8whUxsU_z_jt8,11766
16
- core/api/forms.py,sha256=tw7oS5cqflTBBAMu5MG-GNx8ndNYxeQV1v2tOQr-Q1s,4702
16
+ core/api/forms.py,sha256=pJc0f72-F1QOTSI7aNDMaqScCmJnHUWbigvmSN4Or7k,4832
17
17
  core/api/media.py,sha256=iCsgRBS_-raYK99sksudyAINnTWFl1GhTMBaXJGWkek,6767
18
18
  core/api/relations.py,sha256=sErG5lNLBjmWZ5qSXyhGkU7F8iVZAYpFc-_5mY3ZkzU,4874
19
19
  core/api/revisions.py,sha256=8BJXtCsq8OJV1R5s33bl369lQrRDNkztWoS0tX70uAA,6960
@@ -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=CogulY71HAS2bjIPtqw2MK5og8Izns4xkzbO2Y84LbI,41905
47
+ core/engine/routes.py,sha256=LKJYCoctYk3OB9bhKutVRRaBRWNwQjS7dFxSk5cOrAI,42232
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
@@ -120,7 +120,7 @@ core/services/schedule.py,sha256=eISYvq5iHyKXdngJNk5PIjTAKeB6O0Rj2j4ktoTSQNU,813
120
120
  core/services/search.py,sha256=OSwzQkUHnOLly6yqyDNaj1-RRUNar4QPlWCLhojVtdM,14964
121
121
  core/services/sentry.py,sha256=Soilp-qbRDDcvXfeaaS9Bq_trlaRvjNAyHIgvBkplXc,6275
122
122
  core/services/seo.py,sha256=qFKKnAzZ2CspWzlHHxvfKx4JBVgNhPWxCSn-adX-2z4,15658
123
- core/services/settings.py,sha256=FZrXd8k6X_0WQOO3I20lyrksNXzr4nL_yBJm9PmMuDI,8551
123
+ core/services/settings.py,sha256=oZJx1HgFNeSicagtURXHbqq0_O3yPwvcwosuqDmZ13c,8822
124
124
  core/services/spam_filter.py,sha256=2O8YWDlZoCr7MhGzsEsx6AKm2SgBP3-kxsXaETVd-0A,11314
125
125
  core/services/storage.py,sha256=gaaVf594Ck-zkZMtdt--YcIMvWgPBs7ZGPY0FRIVIzQ,8807
126
126
  core/services/theme.py,sha256=-sXcWC0qggV9pmDimKjpmaK0Il9MNCb5urWLgYcWpYk,49304
@@ -186,7 +186,7 @@ themes/default/theme.yaml,sha256=tgcUP1YFptyXVNL2a8DBiPrP7zTjWNH62Cy9D_w6Chk,187
186
186
  themes/default/templates/404.html,sha256=6pYUz7zg5hx3nikgxiZWSkwYnv2nENCSV3KjdIF0_lE,1105
187
187
  themes/default/templates/500.html,sha256=CtU3gEsHsxAh-vbcnx5McH8V8ruKtdP8usj8hPuu8zY,1174
188
188
  themes/default/templates/archive.html,sha256=ZHBxPYewvc2TbrsB745LYO2uM5SJbTFQQR6savWUzYg,2385
189
- themes/default/templates/base.html,sha256=C2SWfi53TRwtowgLd2r4UbARG8n03nyqIVCupoSeWb8,10317
189
+ themes/default/templates/base.html,sha256=06hSyyeAHqfG2thvMMMUQnxgRTdRD4DCGKieThSzEGM,11901
190
190
  themes/default/templates/category.html,sha256=k-yN0vFoOpgxgg6DlGin5X4IzVDBG9xRZ0FOD7OJtU8,3061
191
191
  themes/default/templates/channel.html,sha256=1i1zkAWmvpcqyoEfaeQNDc2zrMao2xSXCkjRuwzxOUU,3213
192
192
  themes/default/templates/form.html,sha256=OvS2yXVu3sruDOYvEjQCEQNohLf94SMQGR1V9Ky1_Sw,8685
@@ -201,8 +201,8 @@ themes/minimal/templates/base.html,sha256=LFkx-XLDMGH7oFHHa0e6KPB0DJITOBvr6GtPkD
201
201
  themes/minimal/templates/home.html,sha256=ygYQgYj1OGCiKwmfsxwkPselVKT8vDH3jLLbfphpqKI,1577
202
202
  themes/minimal/templates/page.html,sha256=7Xcoq-ryaxlp913H2S1ishrAro2wsqqGmvsm1osXxd4,389
203
203
  themes/minimal/templates/post.html,sha256=FkTRHci8HNIIi3DU6Mb3oL0aDisGyDcsT_IUDwHmrvo,1387
204
- focomy-0.1.106.dist-info/METADATA,sha256=WuqHmxR0zsPOR2cyZW0vV6yIEBkUgIneDCm1xXgZ30U,7042
205
- focomy-0.1.106.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
206
- focomy-0.1.106.dist-info/entry_points.txt,sha256=_rF-wxGI1axY7gox3DBsTLHq-JrFKkMCjA65a6b_oqE,41
207
- focomy-0.1.106.dist-info/licenses/LICENSE,sha256=z9Z7gN7NNV7zYCaY-Knh3bv8RBCu89VueYtAlN_-lro,1063
208
- focomy-0.1.106.dist-info/RECORD,,
204
+ focomy-0.1.108.dist-info/METADATA,sha256=m0KxVaQT7D-JzBMbuE07RP9JSq416EV9dx03CucikC0,7042
205
+ focomy-0.1.108.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
206
+ focomy-0.1.108.dist-info/entry_points.txt,sha256=_rF-wxGI1axY7gox3DBsTLHq-JrFKkMCjA65a6b_oqE,41
207
+ focomy-0.1.108.dist-info/licenses/LICENSE,sha256=z9Z7gN7NNV7zYCaY-Knh3bv8RBCu89VueYtAlN_-lro,1063
208
+ focomy-0.1.108.dist-info/RECORD,,
@@ -123,7 +123,18 @@
123
123
  <div id="admin-bar">
124
124
  <a href="/" class="admin-bar-site">{{ site_name }}</a>
125
125
  <a href="/admin">ダッシュボード</a>
126
- <a href="/admin/post/new">+ 新規投稿</a>
126
+ <div class="admin-bar-dropdown">
127
+ <button class="admin-bar-dropdown-btn">+ 新規作成</button>
128
+ <div class="admin-bar-dropdown-menu">
129
+ {% if content_types %}
130
+ {% for name, ct in content_types.items()|sort(attribute='0') %}
131
+ <a href="/admin/{{ name }}/new">{{ ct.label or name }}</a>
132
+ {% endfor %}
133
+ {% else %}
134
+ <a href="/admin/post/new">投稿</a>
135
+ {% endif %}
136
+ </div>
137
+ </div>
127
138
  {% if edit_url %}
128
139
  <a href="{{ edit_url }}">編集</a>
129
140
  {% endif %}
@@ -163,6 +174,44 @@
163
174
  .admin-bar-user {
164
175
  color: #94a3b8;
165
176
  }
177
+ .admin-bar-dropdown {
178
+ position: relative;
179
+ }
180
+ .admin-bar-dropdown-btn {
181
+ background: transparent;
182
+ border: none;
183
+ color: white;
184
+ cursor: pointer;
185
+ font-size: inherit;
186
+ padding: 0;
187
+ }
188
+ .admin-bar-dropdown-btn:hover {
189
+ text-decoration: underline;
190
+ }
191
+ .admin-bar-dropdown-menu {
192
+ display: none;
193
+ position: absolute;
194
+ top: 100%;
195
+ left: 0;
196
+ background: #1e293b;
197
+ border: 1px solid #374151;
198
+ border-radius: 4px;
199
+ min-width: 150px;
200
+ padding: 0.5rem 0;
201
+ box-shadow: 0 4px 6px rgba(0,0,0,0.3);
202
+ }
203
+ .admin-bar-dropdown-menu a {
204
+ display: block;
205
+ padding: 0.5rem 1rem;
206
+ white-space: nowrap;
207
+ }
208
+ .admin-bar-dropdown-menu a:hover {
209
+ background: #374151;
210
+ text-decoration: none;
211
+ }
212
+ .admin-bar-dropdown:hover .admin-bar-dropdown-menu {
213
+ display: block;
214
+ }
166
215
  body { padding-top: 32px; }
167
216
  </style>
168
217
  {% endif %}