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,594 @@
1
+ # createsonline/admin/user_forms.py
2
+ """
3
+ Custom User forms for Add/Edit with password generation and validation
4
+ """
5
+ from typing import Dict, Any, Tuple
6
+ from sqlalchemy.orm import Session
7
+ from createsonline.auth.models import User, hash_password, verify_password
8
+ import secrets
9
+ import string
10
+
11
+
12
+ class UserCreateForm:
13
+ """Custom form for creating users"""
14
+
15
+ def __init__(self, session: Session, admin_site):
16
+ self.session = session
17
+ self.admin_site = admin_site
18
+
19
+ async def render(self, request, errors: Dict = None, data: Dict = None) -> str:
20
+ """Render user creation form"""
21
+ errors = errors or {}
22
+ data = data or {}
23
+
24
+ return self._generate_form_html(errors, data, is_edit=False)
25
+
26
+ async def save(self, request, data: Dict) -> Tuple[bool, Any, Dict]:
27
+ """
28
+ Save new user with validation
29
+
30
+ Returns:
31
+ Tuple of (success, user_object, errors)
32
+ """
33
+ errors = {}
34
+
35
+ # Validate required fields
36
+ if not data.get('username'):
37
+ errors['username'] = 'Username is required'
38
+
39
+ if not data.get('email'):
40
+ errors['email'] = 'Email is required'
41
+
42
+ # Check if username already exists
43
+ if data.get('username'):
44
+ existing_user = self.session.query(User).filter_by(username=data['username']).first()
45
+ if existing_user:
46
+ errors['username'] = f'Username "{data["username"]}" is already taken'
47
+
48
+ # Check if email already exists
49
+ if data.get('email'):
50
+ existing_email = self.session.query(User).filter_by(email=data['email']).first()
51
+ if existing_email:
52
+ errors['email'] = f'Email "{data["email"]}" is already in use'
53
+
54
+ # Handle password
55
+ password = None
56
+ if data.get('use_generated_password'):
57
+ # Generate random password
58
+ password = self._generate_password()
59
+ generated_password_display = password # Store for display
60
+ elif data.get('password') and data.get('password_confirm'):
61
+ # User provided password
62
+ if data['password'] != data['password_confirm']:
63
+ errors['password_confirm'] = 'Passwords do not match'
64
+ else:
65
+ password = data['password']
66
+ generated_password_display = None
67
+ else:
68
+ errors['password'] = 'Please either generate a password or enter one manually'
69
+
70
+ if errors:
71
+ return False, None, errors
72
+
73
+ try:
74
+ # Create user
75
+ user = User(
76
+ username=data['username'],
77
+ email=data['email'],
78
+ first_name=data.get('first_name', ''),
79
+ last_name=data.get('last_name', ''),
80
+ password_hash=hash_password(password),
81
+ is_active=data.get('is_active', False) == 'on',
82
+ is_staff=data.get('is_staff', False) == 'on',
83
+ is_superuser=data.get('is_superuser', False) == 'on'
84
+ )
85
+
86
+ self.session.add(user)
87
+ self.session.commit()
88
+ self.session.refresh(user)
89
+
90
+ # Store generated password for display if applicable
91
+ if generated_password_display:
92
+ user._generated_password = generated_password_display
93
+
94
+ return True, user, {}
95
+
96
+ except Exception as e:
97
+ self.session.rollback()
98
+ return False, None, {'__all__': f'Error creating user: {str(e)}'}
99
+
100
+ def _generate_password(self, length: int = 16) -> str:
101
+ """Generate a secure random password"""
102
+ alphabet = string.ascii_letters + string.digits + '!@#$%^&*'
103
+ password = ''.join(secrets.choice(alphabet) for _ in range(length))
104
+ return password
105
+
106
+ def _generate_form_html(self, errors: Dict, data: Dict, is_edit: bool = False) -> str:
107
+ """Generate form HTML"""
108
+
109
+ # Build error messages
110
+ def get_error(field):
111
+ return f'<div class="error-message">{errors.get(field)}</div>' if errors.get(field) else ''
112
+
113
+ general_error = errors.get('__all__', '')
114
+ general_error_html = f'<div class="error-message general-error">{general_error}</div>' if general_error else ''
115
+
116
+ # Password section for create form
117
+ password_section = f"""
118
+ <div class="form-section">
119
+ <h3>Password</h3>
120
+
121
+ <div class="password-options">
122
+ <label class="radio-option">
123
+ <input type="radio" name="password_method" value="generate" checked onchange="togglePasswordFields()">
124
+ Generate secure password automatically
125
+ </label>
126
+
127
+ <label class="radio-option">
128
+ <input type="radio" name="password_method" value="manual" onchange="togglePasswordFields()">
129
+ Enter password manually
130
+ </label>
131
+ </div>
132
+
133
+ <div id="manual-password-fields" style="display: none;">
134
+ <div class="form-group">
135
+ <label>Password *</label>
136
+ <input type="password" name="password" id="password" value="">
137
+ {get_error('password')}
138
+ </div>
139
+
140
+ <div class="form-group">
141
+ <label>Confirm Password *</label>
142
+ <input type="password" name="password_confirm" id="password_confirm" value="">
143
+ {get_error('password_confirm')}
144
+ </div>
145
+ </div>
146
+
147
+ <input type="hidden" name="use_generated_password" id="use_generated_password" value="on">
148
+ </div>
149
+ """ if not is_edit else ""
150
+
151
+ # Edit password section
152
+ edit_password_section = f"""
153
+ <div class="form-section">
154
+ <h3>Change Password (Optional)</h3>
155
+
156
+ <div class="form-group">
157
+ <label>Current Password</label>
158
+ <input type="password" name="current_password" value="">
159
+ <div class="help-text">Leave blank to keep current password</div>
160
+ {get_error('current_password')}
161
+ </div>
162
+
163
+ <div class="form-group">
164
+ <label>New Password</label>
165
+ <input type="password" name="new_password" value="">
166
+ {get_error('new_password')}
167
+ </div>
168
+
169
+ <div class="form-group">
170
+ <label>Confirm New Password</label>
171
+ <input type="password" name="new_password_confirm" value="">
172
+ {get_error('new_password_confirm')}
173
+ </div>
174
+ </div>
175
+ """ if is_edit else ""
176
+
177
+ action_url = "/admin/user/add" if not is_edit else f"/admin/user/{data.get('id', '')}/edit"
178
+ title = "Add User" if not is_edit else f"Edit User: {data.get('username', '')}"
179
+
180
+ html = f"""<!DOCTYPE html>
181
+ <html lang="en">
182
+ <head>
183
+ <meta charset="UTF-8">
184
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
185
+ <title>{title} - CREATESONLINE Admin</title>
186
+ <style>
187
+ * {{
188
+ margin: 0;
189
+ padding: 0;
190
+ box-sizing: border-box;
191
+ }}
192
+
193
+ body {{
194
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
195
+ background: #fafafa;
196
+ color: #1a1a1a;
197
+ padding: 20px;
198
+ }}
199
+
200
+ .container {{
201
+ max-width: 800px;
202
+ margin: 0 auto;
203
+ }}
204
+
205
+ .header {{
206
+ background: #000000;
207
+ padding: 20px 40px;
208
+ border-radius: 12px;
209
+ margin-bottom: 30px;
210
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
211
+ }}
212
+
213
+ h1 {{
214
+ font-size: 2em;
215
+ color: #ffffff;
216
+ }}
217
+
218
+ .breadcrumb {{
219
+ margin-top: 15px;
220
+ color: rgba(255, 255, 255, 0.7);
221
+ }}
222
+
223
+ .breadcrumb a {{
224
+ color: #fff;
225
+ text-decoration: none;
226
+ transition: color 0.2s;
227
+ }}
228
+
229
+ .breadcrumb a:hover {{
230
+ color: rgba(255, 255, 255, 0.9);
231
+ }}
232
+
233
+ .form-container {{
234
+ background: #ffffff;
235
+ padding: 40px;
236
+ border-radius: 12px;
237
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
238
+ border: 1px solid #e5e5e5;
239
+ }}
240
+
241
+ .form-section {{
242
+ margin-bottom: 35px;
243
+ padding-bottom: 35px;
244
+ border-bottom: 2px solid #f0f0f0;
245
+ }}
246
+
247
+ .form-section:last-of-type {{
248
+ border-bottom: none;
249
+ }}
250
+
251
+ h3 {{
252
+ font-size: 1.3em;
253
+ margin-bottom: 20px;
254
+ color: #1a1a1a;
255
+ font-weight: 600;
256
+ }}
257
+
258
+ .form-group {{
259
+ margin-bottom: 20px;
260
+ }}
261
+
262
+ label {{
263
+ display: block;
264
+ margin-bottom: 8px;
265
+ font-weight: 500;
266
+ color: #666;
267
+ }}
268
+
269
+ input[type="text"],
270
+ input[type="email"],
271
+ input[type="password"] {{
272
+ width: 100%;
273
+ padding: 12px 15px;
274
+ background: #fafafa;
275
+ border: 2px solid #e5e5e5;
276
+ border-radius: 8px;
277
+ color: #1a1a1a;
278
+ font-size: 1em;
279
+ transition: all 0.2s;
280
+ }}
281
+
282
+ input[type="text"]:focus,
283
+ input[type="email"]:focus,
284
+ input[type="password"]:focus {{
285
+ outline: none;
286
+ border-color: #000000;
287
+ background: #ffffff;
288
+ }}
289
+
290
+ .checkbox-group {{
291
+ display: flex;
292
+ gap: 30px;
293
+ margin-top: 15px;
294
+ }}
295
+
296
+ .checkbox-label {{
297
+ display: flex;
298
+ align-items: center;
299
+ gap: 10px;
300
+ cursor: pointer;
301
+ }}
302
+
303
+ input[type="checkbox"] {{
304
+ width: 20px;
305
+ height: 20px;
306
+ cursor: pointer;
307
+ accent-color: #ffffff;
308
+ }}
309
+
310
+ .password-options {{
311
+ margin-bottom: 20px;
312
+ }}
313
+
314
+ .radio-option {{
315
+ display: block;
316
+ padding: 15px;
317
+ background: #fafafa;
318
+ border: 2px solid #e5e5e5;
319
+ border-radius: 8px;
320
+ margin-bottom: 10px;
321
+ cursor: pointer;
322
+ transition: all 0.2s;
323
+ }}
324
+
325
+ .radio-option:hover {{
326
+ border-color: #000000;
327
+ background: #ffffff;
328
+ }}
329
+
330
+ .radio-option input[type="radio"] {{
331
+ margin-right: 10px;
332
+ accent-color: #000000;
333
+ }}
334
+
335
+ .error-message {{
336
+ color: #dc2626;
337
+ font-size: 0.9em;
338
+ margin-top: 5px;
339
+ }}
340
+
341
+ .general-error {{
342
+ background: rgba(220, 38, 38, 0.1);
343
+ padding: 15px;
344
+ border-radius: 8px;
345
+ border: 2px solid #dc2626;
346
+ margin-bottom: 20px;
347
+ }}
348
+
349
+ .help-text {{
350
+ color: #888;
351
+ font-size: 0.9em;
352
+ margin-top: 5px;
353
+ }}
354
+
355
+ .form-actions {{
356
+ display: flex;
357
+ gap: 15px;
358
+ margin-top: 30px;
359
+ }}
360
+
361
+ .btn {{
362
+ padding: 12px 30px;
363
+ border: none;
364
+ border-radius: 8px;
365
+ font-weight: 600;
366
+ cursor: pointer;
367
+ transition: all 0.3s;
368
+ text-decoration: none;
369
+ display: inline-block;
370
+ }}
371
+
372
+ .btn-primary {{
373
+ background: #000000;
374
+ color: #ffffff;
375
+ }}
376
+
377
+ .btn-primary:hover {{
378
+ background: #333333;
379
+ transform: translateY(-2px);
380
+ }}
381
+
382
+ .btn-secondary {{
383
+ background: #e5e5e5;
384
+ color: #1a1a1a;
385
+ }}
386
+
387
+ .btn-secondary:hover {{
388
+ background: #d0d0d0;
389
+ }}
390
+ </style>
391
+ </head>
392
+ <body>
393
+ <div class="container">
394
+ <div class="header">
395
+ <h1>{title}</h1>
396
+ <div class="breadcrumb">
397
+ <a href="/admin">Admin</a> /
398
+ <a href="/admin/user">Users</a> /
399
+ {title}
400
+ </div>
401
+ </div>
402
+
403
+ {general_error_html}
404
+
405
+ <form method="POST" action="{action_url}" class="form-container">
406
+ <div class="form-section">
407
+ <h3>Basic Information</h3>
408
+
409
+ <div class="form-group">
410
+ <label>Username *</label>
411
+ <input type="text" name="username" value="{data.get('username', '')}" required>
412
+ {get_error('username')}
413
+ <div class="help-text">Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.</div>
414
+ </div>
415
+
416
+ <div class="form-group">
417
+ <label>Email *</label>
418
+ <input type="email" name="email" value="{data.get('email', '')}" required>
419
+ {get_error('email')}
420
+ </div>
421
+
422
+ <div class="form-group">
423
+ <label>First Name</label>
424
+ <input type="text" name="first_name" value="{data.get('first_name', '')}">
425
+ {get_error('first_name')}
426
+ </div>
427
+
428
+ <div class="form-group">
429
+ <label>Last Name</label>
430
+ <input type="text" name="last_name" value="{data.get('last_name', '')}">
431
+ {get_error('last_name')}
432
+ </div>
433
+ </div>
434
+
435
+ {password_section}
436
+ {edit_password_section}
437
+
438
+ <div class="form-section">
439
+ <h3>Permissions</h3>
440
+
441
+ <div class="checkbox-group">
442
+ <label class="checkbox-label">
443
+ <input type="checkbox" name="is_active" {"checked" if data.get('is_active', True) else ""}>
444
+ Is Active
445
+ </label>
446
+
447
+ <label class="checkbox-label">
448
+ <input type="checkbox" name="is_staff" {"checked" if data.get('is_staff', False) else ""}>
449
+ Is Staff
450
+ </label>
451
+
452
+ <label class="checkbox-label">
453
+ <input type="checkbox" name="is_superuser" {"checked" if data.get('is_superuser', False) else ""}>
454
+ Is Superuser
455
+ </label>
456
+ </div>
457
+
458
+ <div class="help-text" style="margin-top: 15px;">
459
+ <strong>Active:</strong> User can login<br>
460
+ <strong>Staff:</strong> User can access admin<br>
461
+ <strong>Superuser:</strong> User has all permissions
462
+ </div>
463
+ </div>
464
+
465
+ <div class="form-actions">
466
+ <button type="submit" class="btn btn-primary">Save User</button>
467
+ <a href="/admin/user" class="btn btn-secondary">Cancel</a>
468
+ </div>
469
+ </form>
470
+ </div>
471
+
472
+ <script>
473
+ function togglePasswordFields() {{
474
+ const method = document.querySelector('input[name="password_method"]:checked').value;
475
+ const manualFields = document.getElementById('manual-password-fields');
476
+ const useGenerated = document.getElementById('use_generated_password');
477
+
478
+ if (method === 'generate') {{
479
+ manualFields.style.display = 'none';
480
+ useGenerated.value = 'on';
481
+ document.getElementById('password').value = '';
482
+ document.getElementById('password_confirm').value = '';
483
+ }} else {{
484
+ manualFields.style.display = 'block';
485
+ useGenerated.value = '';
486
+ }}
487
+ }}
488
+ </script>
489
+ </body>
490
+ </html>
491
+ """
492
+ return html
493
+
494
+
495
+ class UserEditForm:
496
+ """Custom form for editing users"""
497
+
498
+ def __init__(self, user_id: int, session: Session, admin_site):
499
+ self.user_id = user_id
500
+ self.session = session
501
+ self.admin_site = admin_site
502
+
503
+ async def render(self, request, errors: Dict = None) -> str:
504
+ """Render user edit form"""
505
+ errors = errors or {}
506
+
507
+ # Get user
508
+ user = self.session.query(User).filter_by(id=self.user_id).first()
509
+ if not user:
510
+ return "<h1>User not found</h1>"
511
+
512
+ data = {
513
+ 'id': user.id,
514
+ 'username': user.username,
515
+ 'email': user.email,
516
+ 'first_name': user.first_name or '',
517
+ 'last_name': user.last_name or '',
518
+ 'is_active': user.is_active,
519
+ 'is_staff': user.is_staff,
520
+ 'is_superuser': user.is_superuser
521
+ }
522
+
523
+ form = UserCreateForm(self.session, self.admin_site)
524
+ return form._generate_form_html(errors, data, is_edit=True)
525
+
526
+ async def save(self, request, data: Dict) -> Tuple[bool, Any, Dict]:
527
+ """
528
+ Update user with validation
529
+
530
+ Returns:
531
+ Tuple of (success, user_object, errors)
532
+ """
533
+ errors = {}
534
+
535
+ # Get user
536
+ user = self.session.query(User).filter_by(id=self.user_id).first()
537
+ if not user:
538
+ return False, None, {'__all__': 'User not found'}
539
+
540
+ # Validate required fields
541
+ if not data.get('username'):
542
+ errors['username'] = 'Username is required'
543
+
544
+ if not data.get('email'):
545
+ errors['email'] = 'Email is required'
546
+
547
+ # Check if username already exists (excluding current user)
548
+ if data.get('username') and data['username'] != user.username:
549
+ existing_user = self.session.query(User).filter_by(username=data['username']).first()
550
+ if existing_user:
551
+ errors['username'] = f'Username "{data["username"]}" is already taken'
552
+
553
+ # Check if email already exists (excluding current user)
554
+ if data.get('email') and data['email'] != user.email:
555
+ existing_email = self.session.query(User).filter_by(email=data['email']).first()
556
+ if existing_email:
557
+ errors['email'] = f'Email "{data["email"]}" is already in use'
558
+
559
+ # Handle password change
560
+ if data.get('current_password') or data.get('new_password'):
561
+ if not data.get('current_password'):
562
+ errors['current_password'] = 'Current password is required to change password'
563
+ elif not verify_password(data['current_password'], user.password_hash):
564
+ errors['current_password'] = 'Current password is incorrect'
565
+ elif not data.get('new_password'):
566
+ errors['new_password'] = 'New password is required'
567
+ elif data.get('new_password') != data.get('new_password_confirm'):
568
+ errors['new_password_confirm'] = 'New passwords do not match'
569
+
570
+ if errors:
571
+ return False, None, errors
572
+
573
+ try:
574
+ # Update user fields
575
+ user.username = data['username']
576
+ user.email = data['email']
577
+ user.first_name = data.get('first_name', '')
578
+ user.last_name = data.get('last_name', '')
579
+ user.is_active = data.get('is_active') == 'on'
580
+ user.is_staff = data.get('is_staff') == 'on'
581
+ user.is_superuser = data.get('is_superuser') == 'on'
582
+
583
+ # Update password if changed
584
+ if data.get('new_password') and data.get('current_password'):
585
+ user.password_hash = hash_password(data['new_password'])
586
+
587
+ self.session.commit()
588
+ self.session.refresh(user)
589
+
590
+ return True, user, {}
591
+
592
+ except Exception as e:
593
+ self.session.rollback()
594
+ return False, None, {'__all__': f'Error updating user: {str(e)}'}