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