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,805 @@
1
+ # createsonline/admin/crud.py
2
+ """
3
+ CREATESONLINE Admin Auto-CRUD System
4
+
5
+ Automatic Create, Read, Update, Delete views for all registered models.
6
+ Combines Django Admin's power with Wagtail's beautiful UI.
7
+ """
8
+ from typing import Dict, Any, List, Optional, Tuple, Type
9
+ from datetime import datetime
10
+ import inspect
11
+ import re
12
+ from sqlalchemy import inspect as sql_inspect
13
+ from sqlalchemy.orm import Session
14
+ from sqlalchemy.exc import IntegrityError
15
+
16
+
17
+ class ModelInspector:
18
+ """Inspect SQLAlchemy models to extract field information"""
19
+
20
+ @staticmethod
21
+ def get_model_fields(model_class) -> List[Dict[str, Any]]:
22
+ """
23
+ Extract field information from SQLAlchemy model
24
+
25
+ Returns:
26
+ List of field dictionaries with type, label, required, etc.
27
+ """
28
+ fields = []
29
+ mapper = sql_inspect(model_class)
30
+
31
+ for column in mapper.columns:
32
+ field_info = {
33
+ 'name': column.name,
34
+ 'label': column.name.replace('_', ' ').title(),
35
+ 'type': ModelInspector._get_field_type(column),
36
+ 'required': not column.nullable and column.default is None,
37
+ 'primary_key': column.primary_key,
38
+ 'unique': column.unique,
39
+ 'max_length': getattr(column.type, 'length', None),
40
+ 'default': column.default,
41
+ 'help_text': column.comment or '',
42
+ }
43
+ fields.append(field_info)
44
+
45
+ return fields
46
+
47
+ @staticmethod
48
+ def _get_field_type(column) -> str:
49
+ """Determine field type from SQLAlchemy column"""
50
+ from sqlalchemy import Integer, String, Boolean, DateTime, Date, Time, Text, Float, Numeric
51
+
52
+ column_type = type(column.type).__name__
53
+
54
+ # Map SQLAlchemy types to HTML input types
55
+ type_mapping = {
56
+ 'Integer': 'number',
57
+ 'String': 'text',
58
+ 'Text': 'textarea',
59
+ 'Boolean': 'checkbox',
60
+ 'DateTime': 'datetime-local',
61
+ 'Date': 'date',
62
+ 'Time': 'time',
63
+ 'Float': 'number',
64
+ 'Numeric': 'number',
65
+ }
66
+
67
+ # Special cases
68
+ if 'password' in column.name.lower():
69
+ return 'password'
70
+ elif 'email' in column.name.lower():
71
+ return 'email'
72
+ elif 'url' in column.name.lower():
73
+ return 'url'
74
+ elif hasattr(column.type, 'length') and column.type.length and column.type.length > 255:
75
+ return 'textarea'
76
+
77
+ return type_mapping.get(column_type, 'text')
78
+
79
+ @staticmethod
80
+ def get_model_name(model_class) -> str:
81
+ """Get model name"""
82
+ return model_class.__name__
83
+
84
+ @staticmethod
85
+ def get_model_verbose_name(model_class) -> str:
86
+ """Get model verbose name"""
87
+ return model_class.__name__.replace('_', ' ').title()
88
+
89
+ @staticmethod
90
+ def get_model_verbose_name_plural(model_class) -> str:
91
+ """Get model verbose name plural"""
92
+ name = ModelInspector.get_model_verbose_name(model_class)
93
+ if name.endswith('s'):
94
+ return name + 'es'
95
+ elif name.endswith('y'):
96
+ return name[:-1] + 'ies'
97
+ else:
98
+ return name + 's'
99
+
100
+
101
+ class ListView:
102
+ """List view for models - shows all records in a table"""
103
+
104
+ def __init__(self, model_class, session: Session, admin_site):
105
+ self.model_class = model_class
106
+ self.session = session
107
+ self.admin_site = admin_site
108
+ self.list_display = [] # Fields to display in list
109
+ self.list_filter = [] # Fields to filter by
110
+ self.search_fields = [] # Fields to search
111
+ self.ordering = [] # Default ordering
112
+ self.list_per_page = 25 # Items per page
113
+
114
+ async def render(self, request, page: int = 1, search: str = "", filters: Dict = None) -> str:
115
+ """Render list view"""
116
+ filters = filters or {}
117
+
118
+ # Get model info
119
+ model_name = ModelInspector.get_model_name(self.model_class)
120
+ verbose_name = ModelInspector.get_model_verbose_name(self.model_class)
121
+ verbose_name_plural = ModelInspector.get_model_verbose_name_plural(self.model_class)
122
+
123
+ # Build query
124
+ query = self.session.query(self.model_class)
125
+
126
+ # Apply search
127
+ if search and self.search_fields:
128
+ search_conditions = []
129
+ for field in self.search_fields:
130
+ if hasattr(self.model_class, field):
131
+ col = getattr(self.model_class, field)
132
+ search_conditions.append(col.like(f'%{search}%'))
133
+
134
+ if search_conditions:
135
+ from sqlalchemy import or_
136
+ query = query.filter(or_(*search_conditions))
137
+
138
+ # Apply filters
139
+ for field, value in filters.items():
140
+ if hasattr(self.model_class, field) and value:
141
+ query = query.filter(getattr(self.model_class, field) == value)
142
+
143
+ # Apply ordering
144
+ if self.ordering:
145
+ for field in self.ordering:
146
+ if field.startswith('-'):
147
+ query = query.order_by(getattr(self.model_class, field[1:]).desc())
148
+ else:
149
+ query = query.order_by(getattr(self.model_class, field))
150
+
151
+ # Get total count
152
+ total_count = query.count()
153
+
154
+ # Pagination
155
+ offset = (page - 1) * self.list_per_page
156
+ objects = query.limit(self.list_per_page).offset(offset).all()
157
+
158
+ # Calculate pagination
159
+ total_pages = (total_count + self.list_per_page - 1) // self.list_per_page
160
+
161
+ # Get fields to display
162
+ all_fields = ModelInspector.get_model_fields(self.model_class)
163
+
164
+ # Exclude password_hash and other sensitive fields
165
+ excluded_fields = ['password_hash', 'password', 'last_login']
166
+
167
+ if self.list_display:
168
+ fields = self.list_display
169
+ else:
170
+ fields = [f['name'] for f in all_fields
171
+ if not f['primary_key']
172
+ and f['name'] not in excluded_fields][:5]
173
+
174
+ # Generate HTML
175
+ return self._generate_list_html(
176
+ model_name=model_name,
177
+ verbose_name=verbose_name,
178
+ verbose_name_plural=verbose_name_plural,
179
+ objects=objects,
180
+ fields=fields,
181
+ page=page,
182
+ total_pages=total_pages,
183
+ total_count=total_count,
184
+ search=search,
185
+ filters=filters
186
+ )
187
+
188
+ def _generate_list_html(self, **kwargs) -> str:
189
+ """Generate beautiful list view HTML"""
190
+ objects = kwargs['objects']
191
+ fields = kwargs['fields']
192
+ model_name = kwargs['model_name']
193
+ verbose_name_plural = kwargs['verbose_name_plural']
194
+ page = kwargs['page']
195
+ total_pages = kwargs['total_pages']
196
+ total_count = kwargs['total_count']
197
+ search = kwargs['search']
198
+
199
+ # Generate table rows
200
+ rows_html = ""
201
+ for obj in objects:
202
+ cells = ""
203
+ for field in fields:
204
+ value = getattr(obj, field, '')
205
+ if isinstance(value, datetime):
206
+ value = value.strftime('%Y-%m-%d %H:%M')
207
+ elif value is None:
208
+ value = '-'
209
+ cells += f'<td>{value}</td>'
210
+
211
+ obj_id = getattr(obj, 'id', '')
212
+ rows_html += f"""
213
+ <tr>
214
+ {cells}
215
+ <td class="actions">
216
+ <a href="/admin/{model_name.lower()}/{obj_id}/edit" class="btn-small">Edit</a>
217
+ <a href="/admin/{model_name.lower()}/{obj_id}/delete" class="btn-small btn-danger">Delete</a>
218
+ </td>
219
+ </tr>
220
+ """
221
+
222
+ # Generate table headers
223
+ headers_html = "".join([f'<th>{field.replace("_", " ").title()}</th>' for field in fields])
224
+ headers_html += '<th>Actions</th>'
225
+
226
+ # Generate pagination
227
+ pagination_html = ""
228
+ if total_pages > 1:
229
+ pagination_html = '<div class="pagination">'
230
+ if page > 1:
231
+ pagination_html += f'<a href="?page={page-1}" class="btn-small">Previous</a>'
232
+
233
+ pagination_html += f'<span>Page {page} of {total_pages}</span>'
234
+
235
+ if page < total_pages:
236
+ pagination_html += f'<a href="?page={page+1}" class="btn-small">Next</a>'
237
+ pagination_html += '</div>'
238
+
239
+ html = f"""
240
+ <!DOCTYPE html>
241
+ <html lang="en">
242
+ <head>
243
+ <meta charset="UTF-8">
244
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
245
+ <title>{verbose_name_plural} - CREATESONLINE Admin</title>
246
+ <style>
247
+ * {{
248
+ margin: 0;
249
+ padding: 0;
250
+ box-sizing: border-box;
251
+ }}
252
+
253
+ body {{
254
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
255
+ background: #0a0a0a;
256
+ color: #ffffff;
257
+ padding: 20px;
258
+ }}
259
+
260
+ .header {{
261
+ background: #1a1a1a;
262
+ padding: 20px 30px;
263
+ border-radius: 12px;
264
+ border: 1px solid #2a2a2a;
265
+ margin-bottom: 20px;
266
+ display: flex;
267
+ justify-content: space-between;
268
+ align-items: center;
269
+ }}
270
+
271
+ h1 {{
272
+ font-size: 2em;
273
+ background: linear-gradient(135deg, #ffffff 0%, #a0a0a0 100%);
274
+ -webkit-background-clip: text;
275
+ -webkit-text-fill-color: transparent;
276
+ }}
277
+
278
+ .search-bar {{
279
+ display: flex;
280
+ gap: 10px;
281
+ }}
282
+
283
+ input[type="search"] {{
284
+ padding: 10px 15px;
285
+ background: #0a0a0a;
286
+ border: 1px solid #3a3a3a;
287
+ border-radius: 8px;
288
+ color: #ffffff;
289
+ width: 300px;
290
+ }}
291
+
292
+ .btn, .btn-small {{
293
+ padding: 10px 20px;
294
+ background: linear-gradient(135deg, #ffffff 0%, #e0e0e0 100%);
295
+ color: #0a0a0a;
296
+ border: none;
297
+ border-radius: 8px;
298
+ text-decoration: none;
299
+ cursor: pointer;
300
+ font-weight: 600;
301
+ display: inline-block;
302
+ }}
303
+
304
+ .btn-small {{
305
+ padding: 6px 12px;
306
+ font-size: 0.9em;
307
+ }}
308
+
309
+ .btn-danger {{
310
+ background: linear-gradient(135deg, #ff4444 0%, #cc0000 100%);
311
+ color: #ffffff;
312
+ }}
313
+
314
+ .content {{
315
+ background: #1a1a1a;
316
+ padding: 30px;
317
+ border-radius: 12px;
318
+ border: 1px solid #2a2a2a;
319
+ }}
320
+
321
+ table {{
322
+ width: 100%;
323
+ border-collapse: collapse;
324
+ margin-top: 20px;
325
+ }}
326
+
327
+ th {{
328
+ text-align: left;
329
+ padding: 12px;
330
+ background: #0a0a0a;
331
+ border-bottom: 2px solid #3a3a3a;
332
+ font-weight: 600;
333
+ }}
334
+
335
+ td {{
336
+ padding: 12px;
337
+ border-bottom: 1px solid #2a2a2a;
338
+ }}
339
+
340
+ tr:hover {{
341
+ background: #252525;
342
+ }}
343
+
344
+ .actions {{
345
+ display: flex;
346
+ gap: 10px;
347
+ }}
348
+
349
+ .pagination {{
350
+ margin-top: 20px;
351
+ display: flex;
352
+ gap: 15px;
353
+ align-items: center;
354
+ justify-content: center;
355
+ }}
356
+
357
+ .stats {{
358
+ color: #888;
359
+ margin-bottom: 15px;
360
+ }}
361
+ </style>
362
+ </head>
363
+ <body>
364
+ <div class="header">
365
+ <h1>{verbose_name_plural}</h1>
366
+ <div class="search-bar">
367
+ <input type="search" placeholder="Search..." value="{search}">
368
+ <a href="/admin/{model_name.lower()}/add" class="btn">Add {model_name}</a>
369
+ </div>
370
+ </div>
371
+
372
+ <div class="content">
373
+ <div class="stats">Showing {total_count} {verbose_name_plural.lower()}</div>
374
+
375
+ <table>
376
+ <thead>
377
+ <tr>{headers_html}</tr>
378
+ </thead>
379
+ <tbody>
380
+ {rows_html or '<tr><td colspan="100" style="text-align: center; padding: 40px;">No records found</td></tr>'}
381
+ </tbody>
382
+ </table>
383
+
384
+ {pagination_html}
385
+ </div>
386
+
387
+ <div style="margin-top: 20px; text-align: center;">
388
+ <a href="/admin" class="btn-small">← Back to Dashboard</a>
389
+ </div>
390
+ </body>
391
+ </html>
392
+ """
393
+ return html
394
+
395
+
396
+ class CreateView:
397
+ """Create view for models"""
398
+
399
+ def __init__(self, model_class, session: Session, admin_site):
400
+ self.model_class = model_class
401
+ self.session = session
402
+ self.admin_site = admin_site
403
+
404
+ async def render(self, request, errors: Dict = None) -> str:
405
+ """Render create form"""
406
+ errors = errors or {}
407
+ fields = ModelInspector.get_model_fields(self.model_class)
408
+
409
+ # Filter out auto-generated fields
410
+ editable_fields = [f for f in fields if not f['primary_key'] and f['name'] not in ['created_at', 'updated_at', 'date_joined']]
411
+
412
+ return self._generate_form_html(
413
+ fields=editable_fields,
414
+ errors=errors,
415
+ title=f"Add {ModelInspector.get_model_verbose_name(self.model_class)}",
416
+ action=f"/admin/{ModelInspector.get_model_name(self.model_class).lower()}/add",
417
+ method="POST"
418
+ )
419
+
420
+ async def save(self, request, data: Dict) -> Tuple[bool, Any, Dict]:
421
+ """
422
+ Save new object
423
+
424
+ Returns:
425
+ Tuple of (success, object, errors)
426
+ """
427
+ try:
428
+ # Create new instance
429
+ obj = self.model_class(**data)
430
+ self.session.add(obj)
431
+ self.session.commit()
432
+ self.session.refresh(obj)
433
+ return True, obj, {}
434
+ except IntegrityError as e:
435
+ self.session.rollback()
436
+ return False, None, {'__all__': f'Integrity error: {str(e)}'}
437
+ except ValueError as e:
438
+ self.session.rollback()
439
+ return False, None, {'__all__': str(e)}
440
+ except Exception as e:
441
+ self.session.rollback()
442
+ return False, None, {'__all__': f'Error: {str(e)}'}
443
+
444
+ def _generate_form_html(self, fields, errors, title, action, method, data=None) -> str:
445
+ """Generate beautiful form HTML"""
446
+ data = data or {}
447
+
448
+ form_fields_html = ""
449
+ for field in fields:
450
+ field_name = field['name']
451
+ field_label = field['label']
452
+ field_type = field['type']
453
+ field_value = data.get(field_name, field.get('default', ''))
454
+ field_required = 'required' if field['required'] else ''
455
+ field_error = errors.get(field_name, '')
456
+
457
+ error_html = f'<div class="error-message">{field_error}</div>' if field_error else ''
458
+
459
+ if field_type == 'textarea':
460
+ input_html = f'<textarea name="{field_name}" {field_required}>{field_value}</textarea>'
461
+ elif field_type == 'checkbox':
462
+ checked = 'checked' if field_value else ''
463
+ input_html = f'<input type="checkbox" name="{field_name}" {checked}>'
464
+ else:
465
+ input_html = f'<input type="{field_type}" name="{field_name}" value="{field_value}" {field_required}>'
466
+
467
+ form_fields_html += f"""
468
+ <div class="form-group">
469
+ <label>{field_label}{' *' if field['required'] else ''}</label>
470
+ {input_html}
471
+ {error_html}
472
+ {f'<div class="help-text">{field["help_text"]}</div>' if field.get('help_text') else ''}
473
+ </div>
474
+ """
475
+
476
+ general_error = errors.get('__all__', '')
477
+ general_error_html = f'<div class="error-message general-error">{general_error}</div>' if general_error else ''
478
+
479
+ model_name = ModelInspector.get_model_name(self.model_class).lower()
480
+
481
+ html = f"""
482
+ <!DOCTYPE html>
483
+ <html lang="en">
484
+ <head>
485
+ <meta charset="UTF-8">
486
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
487
+ <title>{title} - CREATESONLINE Admin</title>
488
+ <style>
489
+ * {{
490
+ margin: 0;
491
+ padding: 0;
492
+ box-sizing: border-box;
493
+ }}
494
+
495
+ body {{
496
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
497
+ background: #0a0a0a;
498
+ color: #ffffff;
499
+ padding: 20px;
500
+ }}
501
+
502
+ .container {{
503
+ max-width: 800px;
504
+ margin: 0 auto;
505
+ }}
506
+
507
+ h1 {{
508
+ font-size: 2.5em;
509
+ background: linear-gradient(135deg, #ffffff 0%, #a0a0a0 100%);
510
+ -webkit-background-clip: text;
511
+ -webkit-text-fill-color: transparent;
512
+ margin-bottom: 30px;
513
+ }}
514
+
515
+ .form-container {{
516
+ background: #1a1a1a;
517
+ padding: 40px;
518
+ border-radius: 12px;
519
+ border: 1px solid #2a2a2a;
520
+ }}
521
+
522
+ .form-group {{
523
+ margin-bottom: 25px;
524
+ }}
525
+
526
+ label {{
527
+ display: block;
528
+ margin-bottom: 8px;
529
+ color: #b0b0b0;
530
+ font-weight: 500;
531
+ }}
532
+
533
+ input, textarea, select {{
534
+ width: 100%;
535
+ padding: 12px 15px;
536
+ background: #0a0a0a;
537
+ border: 1px solid #3a3a3a;
538
+ border-radius: 8px;
539
+ color: #ffffff;
540
+ font-size: 1em;
541
+ font-family: inherit;
542
+ }}
543
+
544
+ input[type="checkbox"] {{
545
+ width: auto;
546
+ }}
547
+
548
+ textarea {{
549
+ min-height: 120px;
550
+ resize: vertical;
551
+ }}
552
+
553
+ input:focus, textarea:focus, select:focus {{
554
+ outline: none;
555
+ border-color: #ffffff;
556
+ }}
557
+
558
+ .error-message {{
559
+ color: #ff4444;
560
+ font-size: 0.9em;
561
+ margin-top: 5px;
562
+ }}
563
+
564
+ .general-error {{
565
+ background: #331111;
566
+ padding: 15px;
567
+ border-radius: 8px;
568
+ border: 1px solid #ff4444;
569
+ margin-bottom: 20px;
570
+ }}
571
+
572
+ .help-text {{
573
+ color: #888;
574
+ font-size: 0.85em;
575
+ margin-top: 5px;
576
+ }}
577
+
578
+ .form-actions {{
579
+ display: flex;
580
+ gap: 15px;
581
+ margin-top: 30px;
582
+ }}
583
+
584
+ .btn {{
585
+ padding: 12px 30px;
586
+ background: linear-gradient(135deg, #ffffff 0%, #e0e0e0 100%);
587
+ color: #0a0a0a;
588
+ border: none;
589
+ border-radius: 8px;
590
+ text-decoration: none;
591
+ cursor: pointer;
592
+ font-weight: 600;
593
+ font-size: 1em;
594
+ }}
595
+
596
+ .btn:hover {{
597
+ background: linear-gradient(135deg, #e0e0e0 0%, #c0c0c0 100%);
598
+ }}
599
+
600
+ .btn-secondary {{
601
+ background: #2a2a2a;
602
+ color: #ffffff;
603
+ }}
604
+
605
+ .btn-secondary:hover {{
606
+ background: #3a3a3a;
607
+ }}
608
+ </style>
609
+ </head>
610
+ <body>
611
+ <div class="container">
612
+ <h1>{title}</h1>
613
+
614
+ <div class="form-container">
615
+ {general_error_html}
616
+
617
+ <form method="{method}" action="{action}">
618
+ {form_fields_html}
619
+
620
+ <div class="form-actions">
621
+ <button type="submit" class="btn">Save</button>
622
+ <a href="/admin/{model_name}" class="btn btn-secondary">Cancel</a>
623
+ </div>
624
+ </form>
625
+ </div>
626
+ </div>
627
+ </body>
628
+ </html>
629
+ """
630
+ return html
631
+
632
+
633
+ class EditView(CreateView):
634
+ """Edit view for models - extends CreateView"""
635
+
636
+ async def render(self, request, obj_id: int, errors: Dict = None) -> str:
637
+ """Render edit form"""
638
+ errors = errors or {}
639
+ obj = self.session.query(self.model_class).get(obj_id)
640
+
641
+ if not obj:
642
+ return "Object not found"
643
+
644
+ fields = ModelInspector.get_model_fields(self.model_class)
645
+ editable_fields = [f for f in fields if not f['primary_key'] and f['name'] not in ['created_at', 'updated_at', 'date_joined']]
646
+
647
+ # Get current values
648
+ data = {f['name']: getattr(obj, f['name'], '') for f in editable_fields}
649
+
650
+ return self._generate_form_html(
651
+ fields=editable_fields,
652
+ errors=errors,
653
+ title=f"Edit {ModelInspector.get_model_verbose_name(self.model_class)}",
654
+ action=f"/admin/{ModelInspector.get_model_name(self.model_class).lower()}/{obj_id}/edit",
655
+ method="POST",
656
+ data=data
657
+ )
658
+
659
+ async def save(self, request, obj_id: int, data: Dict) -> Tuple[bool, Any, Dict]:
660
+ """Update object"""
661
+ try:
662
+ obj = self.session.query(self.model_class).get(obj_id)
663
+ if not obj:
664
+ return False, None, {'__all__': 'Object not found'}
665
+
666
+ # Update fields
667
+ for key, value in data.items():
668
+ if hasattr(obj, key):
669
+ setattr(obj, key, value)
670
+
671
+ self.session.commit()
672
+ self.session.refresh(obj)
673
+ return True, obj, {}
674
+ except IntegrityError as e:
675
+ self.session.rollback()
676
+ return False, None, {'__all__': f'Integrity error: {str(e)}'}
677
+ except ValueError as e:
678
+ self.session.rollback()
679
+ return False, None, {'__all__': str(e)}
680
+ except Exception as e:
681
+ self.session.rollback()
682
+ return False, None, {'__all__': f'Error: {str(e)}'}
683
+
684
+
685
+ class DeleteView:
686
+ """Delete view for models"""
687
+
688
+ def __init__(self, model_class, session: Session, admin_site):
689
+ self.model_class = model_class
690
+ self.session = session
691
+ self.admin_site = admin_site
692
+
693
+ async def render(self, request, obj_id: int) -> str:
694
+ """Render delete confirmation"""
695
+ obj = self.session.query(self.model_class).get(obj_id)
696
+
697
+ if not obj:
698
+ return "Object not found"
699
+
700
+ model_name = ModelInspector.get_model_name(self.model_class)
701
+ verbose_name = ModelInspector.get_model_verbose_name(self.model_class)
702
+
703
+ html = f"""
704
+ <!DOCTYPE html>
705
+ <html lang="en">
706
+ <head>
707
+ <meta charset="UTF-8">
708
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
709
+ <title>Delete {verbose_name} - CREATESONLINE Admin</title>
710
+ <style>
711
+ * {{
712
+ margin: 0;
713
+ padding: 0;
714
+ box-sizing: border-box;
715
+ }}
716
+
717
+ body {{
718
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
719
+ background: #0a0a0a;
720
+ color: #ffffff;
721
+ padding: 40px 20px;
722
+ display: flex;
723
+ align-items: center;
724
+ justify-content: center;
725
+ min-height: 100vh;
726
+ }}
727
+
728
+ .container {{
729
+ max-width: 600px;
730
+ background: #1a1a1a;
731
+ padding: 40px;
732
+ border-radius: 12px;
733
+ border: 1px solid #2a2a2a;
734
+ text-align: center;
735
+ }}
736
+
737
+ h1 {{
738
+ font-size: 2em;
739
+ background: linear-gradient(135deg, #ff4444 0%, #cc0000 100%);
740
+ -webkit-background-clip: text;
741
+ -webkit-text-fill-color: transparent;
742
+ margin-bottom: 20px;
743
+ }}
744
+
745
+ p {{
746
+ color: #b0b0b0;
747
+ margin-bottom: 30px;
748
+ font-size: 1.1em;
749
+ }}
750
+
751
+ .actions {{
752
+ display: flex;
753
+ gap: 15px;
754
+ justify-content: center;
755
+ }}
756
+
757
+ .btn {{
758
+ padding: 12px 30px;
759
+ border: none;
760
+ border-radius: 8px;
761
+ text-decoration: none;
762
+ cursor: pointer;
763
+ font-weight: 600;
764
+ font-size: 1em;
765
+ }}
766
+
767
+ .btn-danger {{
768
+ background: linear-gradient(135deg, #ff4444 0%, #cc0000 100%);
769
+ color: #ffffff;
770
+ }}
771
+
772
+ .btn-secondary {{
773
+ background: #2a2a2a;
774
+ color: #ffffff;
775
+ }}
776
+ </style>
777
+ </head>
778
+ <body>
779
+ <div class="container">
780
+ <h1>Confirm Deletion</h1>
781
+ <p>Are you sure you want to delete this {verbose_name.lower()}?</p>
782
+
783
+ <form method="POST" action="/admin/{model_name.lower()}/{obj_id}/delete" class="actions">
784
+ <button type="submit" class="btn btn-danger">Yes, Delete</button>
785
+ <a href="/admin/{model_name.lower()}" class="btn btn-secondary">Cancel</a>
786
+ </form>
787
+ </div>
788
+ </body>
789
+ </html>
790
+ """
791
+ return html
792
+
793
+ async def delete(self, request, obj_id: int) -> Tuple[bool, str]:
794
+ """Delete object"""
795
+ try:
796
+ obj = self.session.query(self.model_class).get(obj_id)
797
+ if not obj:
798
+ return False, 'Object not found'
799
+
800
+ self.session.delete(obj)
801
+ self.session.commit()
802
+ return True, 'Successfully deleted'
803
+ except Exception as e:
804
+ self.session.rollback()
805
+ return False, f'Error deleting object: {str(e)}'