createsonline 0.1.26__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.
- createsonline/__init__.py +46 -0
- createsonline/admin/__init__.py +7 -0
- createsonline/admin/content.py +526 -0
- createsonline/admin/crud.py +805 -0
- createsonline/admin/field_builder.py +559 -0
- createsonline/admin/integration.py +482 -0
- createsonline/admin/interface.py +2562 -0
- createsonline/admin/model_creator.py +513 -0
- createsonline/admin/model_manager.py +388 -0
- createsonline/admin/modern_dashboard.py +498 -0
- createsonline/admin/permissions.py +264 -0
- createsonline/admin/user_forms.py +594 -0
- createsonline/ai/__init__.py +202 -0
- createsonline/ai/fields.py +1226 -0
- createsonline/ai/orm.py +325 -0
- createsonline/ai/services.py +1244 -0
- createsonline/app.py +506 -0
- createsonline/auth/__init__.py +8 -0
- createsonline/auth/management.py +228 -0
- createsonline/auth/models.py +552 -0
- createsonline/cli/__init__.py +5 -0
- createsonline/cli/commands/__init__.py +122 -0
- createsonline/cli/commands/database.py +416 -0
- createsonline/cli/commands/info.py +173 -0
- createsonline/cli/commands/initdb.py +218 -0
- createsonline/cli/commands/project.py +545 -0
- createsonline/cli/commands/serve.py +173 -0
- createsonline/cli/commands/shell.py +93 -0
- createsonline/cli/commands/users.py +148 -0
- createsonline/cli/main.py +2041 -0
- createsonline/cli/manage.py +274 -0
- createsonline/config/__init__.py +9 -0
- createsonline/config/app.py +2577 -0
- createsonline/config/database.py +179 -0
- createsonline/config/docs.py +384 -0
- createsonline/config/errors.py +160 -0
- createsonline/config/orm.py +43 -0
- createsonline/config/request.py +93 -0
- createsonline/config/settings.py +176 -0
- createsonline/data/__init__.py +23 -0
- createsonline/data/dataframe.py +925 -0
- createsonline/data/io.py +453 -0
- createsonline/data/series.py +557 -0
- createsonline/database/__init__.py +60 -0
- createsonline/database/abstraction.py +440 -0
- createsonline/database/assistant.py +585 -0
- createsonline/database/fields.py +442 -0
- createsonline/database/migrations.py +132 -0
- createsonline/database/models.py +604 -0
- createsonline/database.py +438 -0
- createsonline/http/__init__.py +28 -0
- createsonline/http/client.py +535 -0
- createsonline/ml/__init__.py +55 -0
- createsonline/ml/classification.py +552 -0
- createsonline/ml/clustering.py +680 -0
- createsonline/ml/metrics.py +542 -0
- createsonline/ml/neural.py +560 -0
- createsonline/ml/preprocessing.py +784 -0
- createsonline/ml/regression.py +501 -0
- createsonline/performance/__init__.py +19 -0
- createsonline/performance/cache.py +444 -0
- createsonline/performance/compression.py +335 -0
- createsonline/performance/core.py +419 -0
- createsonline/project_init.py +789 -0
- createsonline/routing.py +528 -0
- createsonline/security/__init__.py +34 -0
- createsonline/security/core.py +811 -0
- createsonline/security/encryption.py +349 -0
- createsonline/server.py +295 -0
- createsonline/static/css/admin.css +263 -0
- createsonline/static/css/common.css +358 -0
- createsonline/static/css/dashboard.css +89 -0
- createsonline/static/favicon.ico +0 -0
- createsonline/static/icons/icon-128x128.png +0 -0
- createsonline/static/icons/icon-128x128.webp +0 -0
- createsonline/static/icons/icon-16x16.png +0 -0
- createsonline/static/icons/icon-16x16.webp +0 -0
- createsonline/static/icons/icon-180x180.png +0 -0
- createsonline/static/icons/icon-180x180.webp +0 -0
- createsonline/static/icons/icon-192x192.png +0 -0
- createsonline/static/icons/icon-192x192.webp +0 -0
- createsonline/static/icons/icon-256x256.png +0 -0
- createsonline/static/icons/icon-256x256.webp +0 -0
- createsonline/static/icons/icon-32x32.png +0 -0
- createsonline/static/icons/icon-32x32.webp +0 -0
- createsonline/static/icons/icon-384x384.png +0 -0
- createsonline/static/icons/icon-384x384.webp +0 -0
- createsonline/static/icons/icon-48x48.png +0 -0
- createsonline/static/icons/icon-48x48.webp +0 -0
- createsonline/static/icons/icon-512x512.png +0 -0
- createsonline/static/icons/icon-512x512.webp +0 -0
- createsonline/static/icons/icon-64x64.png +0 -0
- createsonline/static/icons/icon-64x64.webp +0 -0
- createsonline/static/image/android-chrome-192x192.png +0 -0
- createsonline/static/image/android-chrome-512x512.png +0 -0
- createsonline/static/image/apple-touch-icon.png +0 -0
- createsonline/static/image/favicon-16x16.png +0 -0
- createsonline/static/image/favicon-32x32.png +0 -0
- createsonline/static/image/favicon.ico +0 -0
- createsonline/static/image/favicon.svg +17 -0
- createsonline/static/image/icon-128x128.png +0 -0
- createsonline/static/image/icon-128x128.webp +0 -0
- createsonline/static/image/icon-16x16.png +0 -0
- createsonline/static/image/icon-16x16.webp +0 -0
- createsonline/static/image/icon-180x180.png +0 -0
- createsonline/static/image/icon-180x180.webp +0 -0
- createsonline/static/image/icon-192x192.png +0 -0
- createsonline/static/image/icon-192x192.webp +0 -0
- createsonline/static/image/icon-256x256.png +0 -0
- createsonline/static/image/icon-256x256.webp +0 -0
- createsonline/static/image/icon-32x32.png +0 -0
- createsonline/static/image/icon-32x32.webp +0 -0
- createsonline/static/image/icon-384x384.png +0 -0
- createsonline/static/image/icon-384x384.webp +0 -0
- createsonline/static/image/icon-48x48.png +0 -0
- createsonline/static/image/icon-48x48.webp +0 -0
- createsonline/static/image/icon-512x512.png +0 -0
- createsonline/static/image/icon-512x512.webp +0 -0
- createsonline/static/image/icon-64x64.png +0 -0
- createsonline/static/image/icon-64x64.webp +0 -0
- createsonline/static/image/logo-header-h100.png +0 -0
- createsonline/static/image/logo-header-h100.webp +0 -0
- createsonline/static/image/logo-header-h200@2x.png +0 -0
- createsonline/static/image/logo-header-h200@2x.webp +0 -0
- createsonline/static/image/logo.png +0 -0
- createsonline/static/js/admin.js +274 -0
- createsonline/static/site.webmanifest +35 -0
- createsonline/static/templates/admin/base.html +87 -0
- createsonline/static/templates/admin/dashboard.html +217 -0
- createsonline/static/templates/admin/model_form.html +270 -0
- createsonline/static/templates/admin/model_list.html +202 -0
- createsonline/static/test_script.js +15 -0
- createsonline/static/test_styles.css +59 -0
- createsonline/static_files.py +365 -0
- createsonline/templates/404.html +100 -0
- createsonline/templates/admin_login.html +169 -0
- createsonline/templates/base.html +102 -0
- createsonline/templates/index.html +151 -0
- createsonline/templates.py +205 -0
- createsonline/testing.py +322 -0
- createsonline/utils.py +448 -0
- createsonline/validation/__init__.py +49 -0
- createsonline/validation/fields.py +598 -0
- createsonline/validation/models.py +504 -0
- createsonline/validation/validators.py +561 -0
- createsonline/views.py +184 -0
- createsonline-0.1.26.dist-info/METADATA +46 -0
- createsonline-0.1.26.dist-info/RECORD +152 -0
- createsonline-0.1.26.dist-info/WHEEL +5 -0
- createsonline-0.1.26.dist-info/entry_points.txt +2 -0
- createsonline-0.1.26.dist-info/licenses/LICENSE +21 -0
- createsonline-0.1.26.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# createsonline/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
CREATESONLINE - The AI-Native Web Framework
|
|
4
|
+
|
|
5
|
+
Build Intelligence Into Everything
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
def _get_version():
|
|
11
|
+
"""Get version from VERSION file"""
|
|
12
|
+
version_file = Path(__file__).parent.parent / "VERSION"
|
|
13
|
+
if version_file.exists():
|
|
14
|
+
return version_file.read_text().strip()
|
|
15
|
+
return '0.1.26'
|
|
16
|
+
|
|
17
|
+
__version__ = _get_version()
|
|
18
|
+
__framework_name__ = 'CREATESONLINE'
|
|
19
|
+
__tagline__ = 'Build Intelligence Into Everything'
|
|
20
|
+
__author__ = 'Ahmed Hassan'
|
|
21
|
+
__license__ = 'MIT'
|
|
22
|
+
|
|
23
|
+
# Import main API
|
|
24
|
+
from .app import CreatesonlineInternalApp, create_app
|
|
25
|
+
from .server import run_server
|
|
26
|
+
from .static_files import StaticFileHandler, serve_static, serve_template
|
|
27
|
+
from .templates import render_template, render_to_response, TemplateEngine
|
|
28
|
+
from .project_init import ProjectInitializer, auto_discover_routes, init_project_if_needed
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
'create_app',
|
|
32
|
+
'CreatesonlineInternalApp',
|
|
33
|
+
'run_server',
|
|
34
|
+
'StaticFileHandler',
|
|
35
|
+
'serve_static',
|
|
36
|
+
'serve_template',
|
|
37
|
+
'render_template',
|
|
38
|
+
'render_to_response',
|
|
39
|
+
'TemplateEngine',
|
|
40
|
+
'ProjectInitializer',
|
|
41
|
+
'auto_discover_routes',
|
|
42
|
+
'init_project_if_needed',
|
|
43
|
+
'__version__',
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
# createsonline/admin/content.py
|
|
2
|
+
"""
|
|
3
|
+
CREATESONLINE Content Management
|
|
4
|
+
|
|
5
|
+
Rich content features inspired by Wagtail:
|
|
6
|
+
- Rich text editor
|
|
7
|
+
- Image/file upload
|
|
8
|
+
- Content versioning (draft/published)
|
|
9
|
+
- Content scheduling
|
|
10
|
+
"""
|
|
11
|
+
from typing import Dict, Any, Optional, List
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean, ForeignKey, JSON
|
|
14
|
+
from sqlalchemy.orm import relationship
|
|
15
|
+
|
|
16
|
+
# Import Base from auth models to avoid foreign key issues
|
|
17
|
+
try:
|
|
18
|
+
from createsonline.auth.models import Base
|
|
19
|
+
except ImportError:
|
|
20
|
+
from sqlalchemy.ext.declarative import declarative_base
|
|
21
|
+
Base = declarative_base()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ========================================
|
|
25
|
+
# Content Management Models
|
|
26
|
+
# ========================================
|
|
27
|
+
|
|
28
|
+
class ContentVersion(Base):
|
|
29
|
+
"""Track content versions for any model"""
|
|
30
|
+
__tablename__ = "createsonline_content_versions"
|
|
31
|
+
|
|
32
|
+
id = Column(Integer, primary_key=True)
|
|
33
|
+
content_type = Column(String(100), nullable=False) # Model name
|
|
34
|
+
object_id = Column(Integer, nullable=False)
|
|
35
|
+
|
|
36
|
+
# Version info
|
|
37
|
+
version_number = Column(Integer, nullable=False)
|
|
38
|
+
data = Column(JSON, nullable=False) # Serialized content
|
|
39
|
+
|
|
40
|
+
# Status
|
|
41
|
+
status = Column(String(20), default='draft') # draft, published, archived
|
|
42
|
+
|
|
43
|
+
# User who created this version
|
|
44
|
+
created_by = Column(Integer, ForeignKey('createsonline_users.id'), nullable=True)
|
|
45
|
+
created_at = Column(DateTime, default=datetime.utcnow)
|
|
46
|
+
|
|
47
|
+
# Publishing
|
|
48
|
+
published_at = Column(DateTime, nullable=True)
|
|
49
|
+
published_by = Column(Integer, ForeignKey('createsonline_users.id'), nullable=True)
|
|
50
|
+
|
|
51
|
+
# Comment/notes
|
|
52
|
+
comment = Column(Text, nullable=True)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ScheduledContent(Base):
|
|
56
|
+
"""Schedule content for future publishing"""
|
|
57
|
+
__tablename__ = "createsonline_scheduled_content"
|
|
58
|
+
|
|
59
|
+
id = Column(Integer, primary_key=True)
|
|
60
|
+
content_type = Column(String(100), nullable=False)
|
|
61
|
+
object_id = Column(Integer, nullable=False)
|
|
62
|
+
|
|
63
|
+
# Scheduling
|
|
64
|
+
publish_at = Column(DateTime, nullable=False)
|
|
65
|
+
unpublish_at = Column(DateTime, nullable=True)
|
|
66
|
+
|
|
67
|
+
# Status
|
|
68
|
+
status = Column(String(20), default='pending') # pending, published, cancelled
|
|
69
|
+
|
|
70
|
+
# User who scheduled
|
|
71
|
+
created_by = Column(Integer, ForeignKey('createsonline_users.id'))
|
|
72
|
+
created_at = Column(DateTime, default=datetime.utcnow)
|
|
73
|
+
|
|
74
|
+
# Execution
|
|
75
|
+
executed_at = Column(DateTime, nullable=True)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class MediaFile(Base):
|
|
79
|
+
"""Media file storage"""
|
|
80
|
+
__tablename__ = "createsonline_media_files"
|
|
81
|
+
|
|
82
|
+
id = Column(Integer, primary_key=True)
|
|
83
|
+
|
|
84
|
+
# File info
|
|
85
|
+
filename = Column(String(255), nullable=False)
|
|
86
|
+
original_filename = Column(String(255), nullable=False)
|
|
87
|
+
file_path = Column(String(500), nullable=False)
|
|
88
|
+
file_type = Column(String(100), nullable=False) # image, document, video, etc.
|
|
89
|
+
mime_type = Column(String(100), nullable=False)
|
|
90
|
+
file_size = Column(Integer, nullable=False) # bytes
|
|
91
|
+
|
|
92
|
+
# Image-specific
|
|
93
|
+
width = Column(Integer, nullable=True)
|
|
94
|
+
height = Column(Integer, nullable=True)
|
|
95
|
+
|
|
96
|
+
# Metadata
|
|
97
|
+
title = Column(String(255), nullable=True)
|
|
98
|
+
alt_text = Column(String(255), nullable=True)
|
|
99
|
+
description = Column(Text, nullable=True)
|
|
100
|
+
tags = Column(JSON, nullable=True) # List of tags
|
|
101
|
+
|
|
102
|
+
# Upload info
|
|
103
|
+
uploaded_by = Column(Integer, ForeignKey('createsonline_users.id'))
|
|
104
|
+
uploaded_at = Column(DateTime, default=datetime.utcnow)
|
|
105
|
+
|
|
106
|
+
# Usage tracking
|
|
107
|
+
usage_count = Column(Integer, default=0)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ========================================
|
|
111
|
+
# Rich Text Field
|
|
112
|
+
# ========================================
|
|
113
|
+
|
|
114
|
+
class RichTextField:
|
|
115
|
+
"""
|
|
116
|
+
Rich text field for models
|
|
117
|
+
|
|
118
|
+
Provides:
|
|
119
|
+
- HTML editor
|
|
120
|
+
- Markdown support
|
|
121
|
+
- Media embedding
|
|
122
|
+
- Link management
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def __init__(self, allow_html: bool = True, allow_markdown: bool = True):
|
|
126
|
+
self.allow_html = allow_html
|
|
127
|
+
self.allow_markdown = allow_markdown
|
|
128
|
+
|
|
129
|
+
def render_editor(self, field_name: str, value: str = "", field_id: str = None) -> str:
|
|
130
|
+
"""Render rich text editor HTML"""
|
|
131
|
+
field_id = field_id or field_name
|
|
132
|
+
|
|
133
|
+
# Simple rich text editor with toolbar
|
|
134
|
+
html = f"""
|
|
135
|
+
<div class="rich-text-editor">
|
|
136
|
+
<div class="editor-toolbar">
|
|
137
|
+
<button type="button" class="editor-btn" data-action="bold" title="Bold">
|
|
138
|
+
<strong>B</strong>
|
|
139
|
+
</button>
|
|
140
|
+
<button type="button" class="editor-btn" data-action="italic" title="Italic">
|
|
141
|
+
<em>I</em>
|
|
142
|
+
</button>
|
|
143
|
+
<button type="button" class="editor-btn" data-action="underline" title="Underline">
|
|
144
|
+
<u>U</u>
|
|
145
|
+
</button>
|
|
146
|
+
<span class="toolbar-separator"></span>
|
|
147
|
+
<button type="button" class="editor-btn" data-action="h1" title="Heading 1">H1</button>
|
|
148
|
+
<button type="button" class="editor-btn" data-action="h2" title="Heading 2">H2</button>
|
|
149
|
+
<button type="button" class="editor-btn" data-action="h3" title="Heading 3">H3</button>
|
|
150
|
+
<span class="toolbar-separator"></span>
|
|
151
|
+
<button type="button" class="editor-btn" data-action="ul" title="Bullet List">• List</button>
|
|
152
|
+
<button type="button" class="editor-btn" data-action="ol" title="Numbered List">1. List</button>
|
|
153
|
+
<span class="toolbar-separator"></span>
|
|
154
|
+
<button type="button" class="editor-btn" data-action="link" title="Insert Link">🔗</button>
|
|
155
|
+
<button type="button" class="editor-btn" data-action="image" title="Insert Image">🖼️</button>
|
|
156
|
+
<span class="toolbar-separator"></span>
|
|
157
|
+
<button type="button" class="editor-btn" data-action="code" title="Code Block"></></button>
|
|
158
|
+
<button type="button" class="editor-btn" data-action="quote" title="Quote">" "</button>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="editor-content" contenteditable="true" id="{field_id}_editor">{value}</div>
|
|
161
|
+
<textarea name="{field_name}" id="{field_id}" style="display: none;">{value}</textarea>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<style>
|
|
165
|
+
.rich-text-editor {{
|
|
166
|
+
border: 1px solid #3a3a3a;
|
|
167
|
+
border-radius: 8px;
|
|
168
|
+
overflow: hidden;
|
|
169
|
+
}}
|
|
170
|
+
|
|
171
|
+
.editor-toolbar {{
|
|
172
|
+
background: #0a0a0a;
|
|
173
|
+
padding: 8px;
|
|
174
|
+
border-bottom: 1px solid #3a3a3a;
|
|
175
|
+
display: flex;
|
|
176
|
+
gap: 4px;
|
|
177
|
+
align-items: center;
|
|
178
|
+
}}
|
|
179
|
+
|
|
180
|
+
.editor-btn {{
|
|
181
|
+
padding: 6px 10px;
|
|
182
|
+
background: #2a2a2a;
|
|
183
|
+
color: #ffffff;
|
|
184
|
+
border: 1px solid #3a3a3a;
|
|
185
|
+
border-radius: 4px;
|
|
186
|
+
cursor: pointer;
|
|
187
|
+
font-size: 0.9em;
|
|
188
|
+
}}
|
|
189
|
+
|
|
190
|
+
.editor-btn:hover {{
|
|
191
|
+
background: #3a3a3a;
|
|
192
|
+
}}
|
|
193
|
+
|
|
194
|
+
.editor-btn:active {{
|
|
195
|
+
background: #4a4a4a;
|
|
196
|
+
}}
|
|
197
|
+
|
|
198
|
+
.toolbar-separator {{
|
|
199
|
+
width: 1px;
|
|
200
|
+
height: 20px;
|
|
201
|
+
background: #3a3a3a;
|
|
202
|
+
margin: 0 4px;
|
|
203
|
+
}}
|
|
204
|
+
|
|
205
|
+
.editor-content {{
|
|
206
|
+
min-height: 200px;
|
|
207
|
+
padding: 15px;
|
|
208
|
+
background: #1a1a1a;
|
|
209
|
+
color: #ffffff;
|
|
210
|
+
outline: none;
|
|
211
|
+
}}
|
|
212
|
+
|
|
213
|
+
.editor-content:focus {{
|
|
214
|
+
background: #1a1a1a;
|
|
215
|
+
}}
|
|
216
|
+
|
|
217
|
+
/* Content styles */
|
|
218
|
+
.editor-content h1 {{
|
|
219
|
+
font-size: 2em;
|
|
220
|
+
margin: 0.5em 0;
|
|
221
|
+
}}
|
|
222
|
+
|
|
223
|
+
.editor-content h2 {{
|
|
224
|
+
font-size: 1.5em;
|
|
225
|
+
margin: 0.5em 0;
|
|
226
|
+
}}
|
|
227
|
+
|
|
228
|
+
.editor-content h3 {{
|
|
229
|
+
font-size: 1.25em;
|
|
230
|
+
margin: 0.5em 0;
|
|
231
|
+
}}
|
|
232
|
+
|
|
233
|
+
.editor-content ul, .editor-content ol {{
|
|
234
|
+
margin: 0.5em 0;
|
|
235
|
+
padding-left: 2em;
|
|
236
|
+
}}
|
|
237
|
+
|
|
238
|
+
.editor-content blockquote {{
|
|
239
|
+
border-left: 3px solid #3a3a3a;
|
|
240
|
+
padding-left: 1em;
|
|
241
|
+
margin: 1em 0;
|
|
242
|
+
color: #b0b0b0;
|
|
243
|
+
}}
|
|
244
|
+
|
|
245
|
+
.editor-content code {{
|
|
246
|
+
background: #0a0a0a;
|
|
247
|
+
padding: 2px 6px;
|
|
248
|
+
border-radius: 3px;
|
|
249
|
+
font-family: 'Courier New', monospace;
|
|
250
|
+
}}
|
|
251
|
+
|
|
252
|
+
.editor-content pre {{
|
|
253
|
+
background: #0a0a0a;
|
|
254
|
+
padding: 15px;
|
|
255
|
+
border-radius: 8px;
|
|
256
|
+
overflow-x: auto;
|
|
257
|
+
}}
|
|
258
|
+
|
|
259
|
+
.editor-content a {{
|
|
260
|
+
color: #ffffff;
|
|
261
|
+
text-decoration: underline;
|
|
262
|
+
}}
|
|
263
|
+
|
|
264
|
+
.editor-content img {{
|
|
265
|
+
max-width: 100%;
|
|
266
|
+
height: auto;
|
|
267
|
+
border-radius: 8px;
|
|
268
|
+
}}
|
|
269
|
+
</style>
|
|
270
|
+
|
|
271
|
+
<script>
|
|
272
|
+
(function() {{
|
|
273
|
+
const editor = document.getElementById('{field_id}_editor');
|
|
274
|
+
const textarea = document.getElementById('{field_id}');
|
|
275
|
+
|
|
276
|
+
// Sync editor content to textarea
|
|
277
|
+
editor.addEventListener('input', function() {{
|
|
278
|
+
textarea.value = editor.innerHTML;
|
|
279
|
+
}});
|
|
280
|
+
|
|
281
|
+
// Toolbar actions
|
|
282
|
+
document.querySelectorAll('.editor-btn').forEach(btn => {{
|
|
283
|
+
btn.addEventListener('click', function(e) {{
|
|
284
|
+
e.preventDefault();
|
|
285
|
+
const action = this.dataset.action;
|
|
286
|
+
|
|
287
|
+
switch(action) {{
|
|
288
|
+
case 'bold':
|
|
289
|
+
document.execCommand('bold');
|
|
290
|
+
break;
|
|
291
|
+
case 'italic':
|
|
292
|
+
document.execCommand('italic');
|
|
293
|
+
break;
|
|
294
|
+
case 'underline':
|
|
295
|
+
document.execCommand('underline');
|
|
296
|
+
break;
|
|
297
|
+
case 'h1':
|
|
298
|
+
document.execCommand('formatBlock', false, 'h1');
|
|
299
|
+
break;
|
|
300
|
+
case 'h2':
|
|
301
|
+
document.execCommand('formatBlock', false, 'h2');
|
|
302
|
+
break;
|
|
303
|
+
case 'h3':
|
|
304
|
+
document.execCommand('formatBlock', false, 'h3');
|
|
305
|
+
break;
|
|
306
|
+
case 'ul':
|
|
307
|
+
document.execCommand('insertUnorderedList');
|
|
308
|
+
break;
|
|
309
|
+
case 'ol':
|
|
310
|
+
document.execCommand('insertOrderedList');
|
|
311
|
+
break;
|
|
312
|
+
case 'link':
|
|
313
|
+
const url = prompt('Enter URL:');
|
|
314
|
+
if (url) document.execCommand('createLink', false, url);
|
|
315
|
+
break;
|
|
316
|
+
case 'image':
|
|
317
|
+
const imgUrl = prompt('Enter image URL:');
|
|
318
|
+
if (imgUrl) document.execCommand('insertImage', false, imgUrl);
|
|
319
|
+
break;
|
|
320
|
+
case 'code':
|
|
321
|
+
document.execCommand('formatBlock', false, 'pre');
|
|
322
|
+
break;
|
|
323
|
+
case 'quote':
|
|
324
|
+
document.execCommand('formatBlock', false, 'blockquote');
|
|
325
|
+
break;
|
|
326
|
+
}}
|
|
327
|
+
|
|
328
|
+
editor.focus();
|
|
329
|
+
}});
|
|
330
|
+
}});
|
|
331
|
+
}})();
|
|
332
|
+
</script>
|
|
333
|
+
"""
|
|
334
|
+
return html
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# ========================================
|
|
338
|
+
# Image Upload Field
|
|
339
|
+
# ========================================
|
|
340
|
+
|
|
341
|
+
class ImageField:
|
|
342
|
+
"""
|
|
343
|
+
Image upload field
|
|
344
|
+
|
|
345
|
+
Provides:
|
|
346
|
+
- File upload
|
|
347
|
+
- Image preview
|
|
348
|
+
- Validation
|
|
349
|
+
- Resize options
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
def __init__(
|
|
353
|
+
self,
|
|
354
|
+
max_size_mb: float = 5.0,
|
|
355
|
+
allowed_formats: List[str] = None,
|
|
356
|
+
max_width: Optional[int] = None,
|
|
357
|
+
max_height: Optional[int] = None
|
|
358
|
+
):
|
|
359
|
+
self.max_size_mb = max_size_mb
|
|
360
|
+
self.allowed_formats = allowed_formats or ['jpg', 'jpeg', 'png', 'gif', 'webp']
|
|
361
|
+
self.max_width = max_width
|
|
362
|
+
self.max_height = max_height
|
|
363
|
+
|
|
364
|
+
def render_uploader(self, field_name: str, current_value: str = "", field_id: str = None) -> str:
|
|
365
|
+
"""Render image upload field"""
|
|
366
|
+
field_id = field_id or field_name
|
|
367
|
+
|
|
368
|
+
preview_html = ""
|
|
369
|
+
if current_value:
|
|
370
|
+
preview_html = f'<img src="{current_value}" alt="Current image" class="image-preview">'
|
|
371
|
+
|
|
372
|
+
html = f"""
|
|
373
|
+
<div class="image-upload-field">
|
|
374
|
+
<div class="upload-area" id="{field_id}_upload_area">
|
|
375
|
+
{preview_html}
|
|
376
|
+
<div class="upload-prompt">
|
|
377
|
+
<span class="upload-icon">📁</span>
|
|
378
|
+
<p>Click to upload or drag and drop</p>
|
|
379
|
+
<p class="upload-hint">
|
|
380
|
+
Max {self.max_size_mb}MB • {', '.join(self.allowed_formats).upper()}
|
|
381
|
+
</p>
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
<input type="file" name="{field_name}" id="{field_id}" accept="image/*" style="display: none;">
|
|
385
|
+
<input type="hidden" name="{field_name}_url" id="{field_id}_url" value="{current_value}">
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
<style>
|
|
389
|
+
.image-upload-field {{
|
|
390
|
+
border: 2px dashed #3a3a3a;
|
|
391
|
+
border-radius: 8px;
|
|
392
|
+
padding: 20px;
|
|
393
|
+
text-align: center;
|
|
394
|
+
cursor: pointer;
|
|
395
|
+
transition: all 0.3s;
|
|
396
|
+
}}
|
|
397
|
+
|
|
398
|
+
.image-upload-field:hover {{
|
|
399
|
+
border-color: #ffffff;
|
|
400
|
+
background: #1a1a1a;
|
|
401
|
+
}}
|
|
402
|
+
|
|
403
|
+
.upload-area {{
|
|
404
|
+
position: relative;
|
|
405
|
+
}}
|
|
406
|
+
|
|
407
|
+
.image-preview {{
|
|
408
|
+
max-width: 100%;
|
|
409
|
+
max-height: 300px;
|
|
410
|
+
border-radius: 8px;
|
|
411
|
+
margin-bottom: 15px;
|
|
412
|
+
}}
|
|
413
|
+
|
|
414
|
+
.upload-icon {{
|
|
415
|
+
font-size: 3em;
|
|
416
|
+
display: block;
|
|
417
|
+
margin-bottom: 10px;
|
|
418
|
+
}}
|
|
419
|
+
|
|
420
|
+
.upload-prompt p {{
|
|
421
|
+
color: #b0b0b0;
|
|
422
|
+
margin: 5px 0;
|
|
423
|
+
}}
|
|
424
|
+
|
|
425
|
+
.upload-hint {{
|
|
426
|
+
font-size: 0.85em;
|
|
427
|
+
color: #888;
|
|
428
|
+
}}
|
|
429
|
+
</style>
|
|
430
|
+
|
|
431
|
+
<script>
|
|
432
|
+
(function() {{
|
|
433
|
+
const uploadArea = document.getElementById('{field_id}_upload_area');
|
|
434
|
+
const fileInput = document.getElementById('{field_id}');
|
|
435
|
+
const urlInput = document.getElementById('{field_id}_url');
|
|
436
|
+
|
|
437
|
+
uploadArea.addEventListener('click', () => fileInput.click());
|
|
438
|
+
|
|
439
|
+
fileInput.addEventListener('change', function(e) {{
|
|
440
|
+
const file = e.target.files[0];
|
|
441
|
+
if (file) {{
|
|
442
|
+
// Validate file size
|
|
443
|
+
if (file.size > {self.max_size_mb} * 1024 * 1024) {{
|
|
444
|
+
alert('File too large! Max {self.max_size_mb}MB');
|
|
445
|
+
return;
|
|
446
|
+
}}
|
|
447
|
+
|
|
448
|
+
// Validate file type
|
|
449
|
+
const ext = file.name.split('.').pop().toLowerCase();
|
|
450
|
+
const allowed = {self.allowed_formats};
|
|
451
|
+
if (!allowed.includes(ext)) {{
|
|
452
|
+
alert('Invalid file type! Allowed: ' + allowed.join(', '));
|
|
453
|
+
return;
|
|
454
|
+
}}
|
|
455
|
+
|
|
456
|
+
// Show preview
|
|
457
|
+
const reader = new FileReader();
|
|
458
|
+
reader.onload = function(e) {{
|
|
459
|
+
uploadArea.innerHTML = '<img src="' + e.target.result + '" class="image-preview">';
|
|
460
|
+
urlInput.value = e.target.result;
|
|
461
|
+
}};
|
|
462
|
+
reader.readAsDataURL(file);
|
|
463
|
+
}}
|
|
464
|
+
}});
|
|
465
|
+
|
|
466
|
+
// Drag and drop
|
|
467
|
+
uploadArea.addEventListener('dragover', (e) => {{
|
|
468
|
+
e.preventDefault();
|
|
469
|
+
uploadArea.style.borderColor = '#ffffff';
|
|
470
|
+
}});
|
|
471
|
+
|
|
472
|
+
uploadArea.addEventListener('dragleave', () => {{
|
|
473
|
+
uploadArea.style.borderColor = '#3a3a3a';
|
|
474
|
+
}});
|
|
475
|
+
|
|
476
|
+
uploadArea.addEventListener('drop', (e) => {{
|
|
477
|
+
e.preventDefault();
|
|
478
|
+
uploadArea.style.borderColor = '#3a3a3a';
|
|
479
|
+
|
|
480
|
+
const file = e.dataTransfer.files[0];
|
|
481
|
+
if (file) {{
|
|
482
|
+
fileInput.files = e.dataTransfer.files;
|
|
483
|
+
fileInput.dispatchEvent(new Event('change'));
|
|
484
|
+
}}
|
|
485
|
+
}});
|
|
486
|
+
}})();
|
|
487
|
+
</script>
|
|
488
|
+
"""
|
|
489
|
+
return html
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
# ========================================
|
|
493
|
+
# Content Status Mixin
|
|
494
|
+
# ========================================
|
|
495
|
+
|
|
496
|
+
class ContentStatusMixin:
|
|
497
|
+
"""Mixin to add content status to models"""
|
|
498
|
+
|
|
499
|
+
status = Column(String(20), default='draft') # draft, published, archived
|
|
500
|
+
published_at = Column(DateTime, nullable=True)
|
|
501
|
+
created_at = Column(DateTime, default=datetime.utcnow)
|
|
502
|
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
503
|
+
|
|
504
|
+
def publish(self):
|
|
505
|
+
"""Publish content"""
|
|
506
|
+
self.status = 'published'
|
|
507
|
+
self.published_at = datetime.utcnow()
|
|
508
|
+
|
|
509
|
+
def unpublish(self):
|
|
510
|
+
"""Unpublish content"""
|
|
511
|
+
self.status = 'draft'
|
|
512
|
+
self.published_at = None
|
|
513
|
+
|
|
514
|
+
def archive(self):
|
|
515
|
+
"""Archive content"""
|
|
516
|
+
self.status = 'archived'
|
|
517
|
+
|
|
518
|
+
@property
|
|
519
|
+
def is_published(self) -> bool:
|
|
520
|
+
"""Check if content is published"""
|
|
521
|
+
return self.status == 'published'
|
|
522
|
+
|
|
523
|
+
@property
|
|
524
|
+
def is_draft(self) -> bool:
|
|
525
|
+
"""Check if content is draft"""
|
|
526
|
+
return self.status == 'draft'
|