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,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)}'
|