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.
Files changed (152) hide show
  1. createsonline/__init__.py +46 -0
  2. createsonline/admin/__init__.py +7 -0
  3. createsonline/admin/content.py +526 -0
  4. createsonline/admin/crud.py +805 -0
  5. createsonline/admin/field_builder.py +559 -0
  6. createsonline/admin/integration.py +482 -0
  7. createsonline/admin/interface.py +2562 -0
  8. createsonline/admin/model_creator.py +513 -0
  9. createsonline/admin/model_manager.py +388 -0
  10. createsonline/admin/modern_dashboard.py +498 -0
  11. createsonline/admin/permissions.py +264 -0
  12. createsonline/admin/user_forms.py +594 -0
  13. createsonline/ai/__init__.py +202 -0
  14. createsonline/ai/fields.py +1226 -0
  15. createsonline/ai/orm.py +325 -0
  16. createsonline/ai/services.py +1244 -0
  17. createsonline/app.py +506 -0
  18. createsonline/auth/__init__.py +8 -0
  19. createsonline/auth/management.py +228 -0
  20. createsonline/auth/models.py +552 -0
  21. createsonline/cli/__init__.py +5 -0
  22. createsonline/cli/commands/__init__.py +122 -0
  23. createsonline/cli/commands/database.py +416 -0
  24. createsonline/cli/commands/info.py +173 -0
  25. createsonline/cli/commands/initdb.py +218 -0
  26. createsonline/cli/commands/project.py +545 -0
  27. createsonline/cli/commands/serve.py +173 -0
  28. createsonline/cli/commands/shell.py +93 -0
  29. createsonline/cli/commands/users.py +148 -0
  30. createsonline/cli/main.py +2041 -0
  31. createsonline/cli/manage.py +274 -0
  32. createsonline/config/__init__.py +9 -0
  33. createsonline/config/app.py +2577 -0
  34. createsonline/config/database.py +179 -0
  35. createsonline/config/docs.py +384 -0
  36. createsonline/config/errors.py +160 -0
  37. createsonline/config/orm.py +43 -0
  38. createsonline/config/request.py +93 -0
  39. createsonline/config/settings.py +176 -0
  40. createsonline/data/__init__.py +23 -0
  41. createsonline/data/dataframe.py +925 -0
  42. createsonline/data/io.py +453 -0
  43. createsonline/data/series.py +557 -0
  44. createsonline/database/__init__.py +60 -0
  45. createsonline/database/abstraction.py +440 -0
  46. createsonline/database/assistant.py +585 -0
  47. createsonline/database/fields.py +442 -0
  48. createsonline/database/migrations.py +132 -0
  49. createsonline/database/models.py +604 -0
  50. createsonline/database.py +438 -0
  51. createsonline/http/__init__.py +28 -0
  52. createsonline/http/client.py +535 -0
  53. createsonline/ml/__init__.py +55 -0
  54. createsonline/ml/classification.py +552 -0
  55. createsonline/ml/clustering.py +680 -0
  56. createsonline/ml/metrics.py +542 -0
  57. createsonline/ml/neural.py +560 -0
  58. createsonline/ml/preprocessing.py +784 -0
  59. createsonline/ml/regression.py +501 -0
  60. createsonline/performance/__init__.py +19 -0
  61. createsonline/performance/cache.py +444 -0
  62. createsonline/performance/compression.py +335 -0
  63. createsonline/performance/core.py +419 -0
  64. createsonline/project_init.py +789 -0
  65. createsonline/routing.py +528 -0
  66. createsonline/security/__init__.py +34 -0
  67. createsonline/security/core.py +811 -0
  68. createsonline/security/encryption.py +349 -0
  69. createsonline/server.py +295 -0
  70. createsonline/static/css/admin.css +263 -0
  71. createsonline/static/css/common.css +358 -0
  72. createsonline/static/css/dashboard.css +89 -0
  73. createsonline/static/favicon.ico +0 -0
  74. createsonline/static/icons/icon-128x128.png +0 -0
  75. createsonline/static/icons/icon-128x128.webp +0 -0
  76. createsonline/static/icons/icon-16x16.png +0 -0
  77. createsonline/static/icons/icon-16x16.webp +0 -0
  78. createsonline/static/icons/icon-180x180.png +0 -0
  79. createsonline/static/icons/icon-180x180.webp +0 -0
  80. createsonline/static/icons/icon-192x192.png +0 -0
  81. createsonline/static/icons/icon-192x192.webp +0 -0
  82. createsonline/static/icons/icon-256x256.png +0 -0
  83. createsonline/static/icons/icon-256x256.webp +0 -0
  84. createsonline/static/icons/icon-32x32.png +0 -0
  85. createsonline/static/icons/icon-32x32.webp +0 -0
  86. createsonline/static/icons/icon-384x384.png +0 -0
  87. createsonline/static/icons/icon-384x384.webp +0 -0
  88. createsonline/static/icons/icon-48x48.png +0 -0
  89. createsonline/static/icons/icon-48x48.webp +0 -0
  90. createsonline/static/icons/icon-512x512.png +0 -0
  91. createsonline/static/icons/icon-512x512.webp +0 -0
  92. createsonline/static/icons/icon-64x64.png +0 -0
  93. createsonline/static/icons/icon-64x64.webp +0 -0
  94. createsonline/static/image/android-chrome-192x192.png +0 -0
  95. createsonline/static/image/android-chrome-512x512.png +0 -0
  96. createsonline/static/image/apple-touch-icon.png +0 -0
  97. createsonline/static/image/favicon-16x16.png +0 -0
  98. createsonline/static/image/favicon-32x32.png +0 -0
  99. createsonline/static/image/favicon.ico +0 -0
  100. createsonline/static/image/favicon.svg +17 -0
  101. createsonline/static/image/icon-128x128.png +0 -0
  102. createsonline/static/image/icon-128x128.webp +0 -0
  103. createsonline/static/image/icon-16x16.png +0 -0
  104. createsonline/static/image/icon-16x16.webp +0 -0
  105. createsonline/static/image/icon-180x180.png +0 -0
  106. createsonline/static/image/icon-180x180.webp +0 -0
  107. createsonline/static/image/icon-192x192.png +0 -0
  108. createsonline/static/image/icon-192x192.webp +0 -0
  109. createsonline/static/image/icon-256x256.png +0 -0
  110. createsonline/static/image/icon-256x256.webp +0 -0
  111. createsonline/static/image/icon-32x32.png +0 -0
  112. createsonline/static/image/icon-32x32.webp +0 -0
  113. createsonline/static/image/icon-384x384.png +0 -0
  114. createsonline/static/image/icon-384x384.webp +0 -0
  115. createsonline/static/image/icon-48x48.png +0 -0
  116. createsonline/static/image/icon-48x48.webp +0 -0
  117. createsonline/static/image/icon-512x512.png +0 -0
  118. createsonline/static/image/icon-512x512.webp +0 -0
  119. createsonline/static/image/icon-64x64.png +0 -0
  120. createsonline/static/image/icon-64x64.webp +0 -0
  121. createsonline/static/image/logo-header-h100.png +0 -0
  122. createsonline/static/image/logo-header-h100.webp +0 -0
  123. createsonline/static/image/logo-header-h200@2x.png +0 -0
  124. createsonline/static/image/logo-header-h200@2x.webp +0 -0
  125. createsonline/static/image/logo.png +0 -0
  126. createsonline/static/js/admin.js +274 -0
  127. createsonline/static/site.webmanifest +35 -0
  128. createsonline/static/templates/admin/base.html +87 -0
  129. createsonline/static/templates/admin/dashboard.html +217 -0
  130. createsonline/static/templates/admin/model_form.html +270 -0
  131. createsonline/static/templates/admin/model_list.html +202 -0
  132. createsonline/static/test_script.js +15 -0
  133. createsonline/static/test_styles.css +59 -0
  134. createsonline/static_files.py +365 -0
  135. createsonline/templates/404.html +100 -0
  136. createsonline/templates/admin_login.html +169 -0
  137. createsonline/templates/base.html +102 -0
  138. createsonline/templates/index.html +151 -0
  139. createsonline/templates.py +205 -0
  140. createsonline/testing.py +322 -0
  141. createsonline/utils.py +448 -0
  142. createsonline/validation/__init__.py +49 -0
  143. createsonline/validation/fields.py +598 -0
  144. createsonline/validation/models.py +504 -0
  145. createsonline/validation/validators.py +561 -0
  146. createsonline/views.py +184 -0
  147. createsonline-0.1.26.dist-info/METADATA +46 -0
  148. createsonline-0.1.26.dist-info/RECORD +152 -0
  149. createsonline-0.1.26.dist-info/WHEEL +5 -0
  150. createsonline-0.1.26.dist-info/entry_points.txt +2 -0
  151. createsonline-0.1.26.dist-info/licenses/LICENSE +21 -0
  152. 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,7 @@
1
+ # createsonline/admin/__init__.py
2
+ from .interface import AdminSite, ModelAdmin
3
+
4
+ # Create default admin site
5
+ admin_site = AdminSite(name='admin')
6
+
7
+ __all__ = ['AdminSite', 'ModelAdmin', 'admin_site']
@@ -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">&lt;/&gt;</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'