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,552 @@
|
|
|
1
|
+
# createsonline/auth/models.py
|
|
2
|
+
"""
|
|
3
|
+
CREATESONLINE Authentication Models
|
|
4
|
+
|
|
5
|
+
User, Group, and Permission models for CREATESONLINE applications.
|
|
6
|
+
"""
|
|
7
|
+
import sqlalchemy as sa
|
|
8
|
+
from sqlalchemy.ext.declarative import declarative_base
|
|
9
|
+
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text, ForeignKey, Table
|
|
10
|
+
from sqlalchemy.orm import relationship, validates
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
import hashlib
|
|
13
|
+
import secrets
|
|
14
|
+
import hmac
|
|
15
|
+
from base64 import b64encode, b64decode
|
|
16
|
+
from typing import Optional, List
|
|
17
|
+
import re
|
|
18
|
+
|
|
19
|
+
# Create base class for CREATESONLINE models
|
|
20
|
+
Base = declarative_base()
|
|
21
|
+
|
|
22
|
+
# Pure Python password hashing functions
|
|
23
|
+
def hash_password(password: str, salt: Optional[bytes] = None) -> str:
|
|
24
|
+
"""
|
|
25
|
+
Hash password using PBKDF2 with SHA-256 (pure Python implementation)
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
password: Plain text password
|
|
29
|
+
salt: Optional salt bytes, generated if not provided
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Hashed password string in format: pbkdf2_sha256$iterations$salt$hash
|
|
33
|
+
"""
|
|
34
|
+
if salt is None:
|
|
35
|
+
salt = secrets.token_bytes(32)
|
|
36
|
+
|
|
37
|
+
iterations = 100000 # OWASP recommended minimum
|
|
38
|
+
|
|
39
|
+
# Use PBKDF2 with SHA-256
|
|
40
|
+
key = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, iterations)
|
|
41
|
+
|
|
42
|
+
# Encode salt and key as base64
|
|
43
|
+
salt_b64 = b64encode(salt).decode('ascii')
|
|
44
|
+
key_b64 = b64encode(key).decode('ascii')
|
|
45
|
+
|
|
46
|
+
return f"pbkdf2_sha256${iterations}${salt_b64}${key_b64}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def verify_password(password: str, hashed: str) -> bool:
|
|
50
|
+
"""
|
|
51
|
+
Verify password against hash
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
password: Plain text password to verify
|
|
55
|
+
hashed: Hashed password from database
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
True if password matches
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
# Parse hash format: pbkdf2_sha256$iterations$salt$hash
|
|
62
|
+
parts = hashed.split('$')
|
|
63
|
+
if len(parts) != 4 or parts[0] != 'pbkdf2_sha256':
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
iterations = int(parts[1])
|
|
67
|
+
salt = b64decode(parts[2].encode('ascii'))
|
|
68
|
+
stored_key = b64decode(parts[3].encode('ascii'))
|
|
69
|
+
|
|
70
|
+
# Hash the provided password with same salt and iterations
|
|
71
|
+
new_key = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, iterations)
|
|
72
|
+
|
|
73
|
+
# Use constant-time comparison to prevent timing attacks
|
|
74
|
+
return hmac.compare_digest(stored_key, new_key)
|
|
75
|
+
|
|
76
|
+
except (ValueError, TypeError, IndexError):
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
# Many-to-many association tables
|
|
80
|
+
user_groups = Table(
|
|
81
|
+
'createsonline_user_groups',
|
|
82
|
+
Base.metadata,
|
|
83
|
+
Column('user_id', Integer, ForeignKey('createsonline_users.id'), primary_key=True),
|
|
84
|
+
Column('group_id', Integer, ForeignKey('createsonline_groups.id'), primary_key=True)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
user_permissions = Table(
|
|
88
|
+
'createsonline_user_permissions',
|
|
89
|
+
Base.metadata,
|
|
90
|
+
Column('user_id', Integer, ForeignKey('createsonline_users.id'), primary_key=True),
|
|
91
|
+
Column('permission_id', Integer, ForeignKey('createsonline_permissions.id'), primary_key=True)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
group_permissions = Table(
|
|
95
|
+
'createsonline_group_permissions',
|
|
96
|
+
Base.metadata,
|
|
97
|
+
Column('group_id', Integer, ForeignKey('createsonline_groups.id'), primary_key=True),
|
|
98
|
+
Column('permission_id', Integer, ForeignKey('createsonline_permissions.id'), primary_key=True)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
class User(Base):
|
|
102
|
+
"""
|
|
103
|
+
CREATESONLINE User Model
|
|
104
|
+
|
|
105
|
+
Core user model for authentication and authorization.
|
|
106
|
+
Provides all essential user management features.
|
|
107
|
+
"""
|
|
108
|
+
__tablename__ = "createsonline_users"
|
|
109
|
+
|
|
110
|
+
# Primary fields
|
|
111
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
112
|
+
username = Column(String(150), unique=True, nullable=False, index=True)
|
|
113
|
+
email = Column(String(254), unique=True, nullable=False, index=True)
|
|
114
|
+
|
|
115
|
+
# Personal information
|
|
116
|
+
first_name = Column(String(150), nullable=True)
|
|
117
|
+
last_name = Column(String(150), nullable=True)
|
|
118
|
+
|
|
119
|
+
# Authentication
|
|
120
|
+
password_hash = Column(String(128), nullable=False)
|
|
121
|
+
|
|
122
|
+
# Permissions and status
|
|
123
|
+
is_active = Column(Boolean, default=True, nullable=False)
|
|
124
|
+
is_staff = Column(Boolean, default=False, nullable=False)
|
|
125
|
+
is_superuser = Column(Boolean, default=False, nullable=False)
|
|
126
|
+
|
|
127
|
+
# Timestamps
|
|
128
|
+
date_joined = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
129
|
+
last_login = Column(DateTime, nullable=True)
|
|
130
|
+
|
|
131
|
+
# Profile information
|
|
132
|
+
profile_picture = Column(String(255), nullable=True)
|
|
133
|
+
bio = Column(Text, nullable=True)
|
|
134
|
+
|
|
135
|
+
# Security
|
|
136
|
+
failed_login_attempts = Column(Integer, default=0)
|
|
137
|
+
account_locked_until = Column(DateTime, nullable=True)
|
|
138
|
+
password_reset_token = Column(String(100), nullable=True)
|
|
139
|
+
email_verification_token = Column(String(100), nullable=True)
|
|
140
|
+
email_verified = Column(Boolean, default=False)
|
|
141
|
+
|
|
142
|
+
# Relationships
|
|
143
|
+
groups = relationship(
|
|
144
|
+
"Group",
|
|
145
|
+
secondary=user_groups,
|
|
146
|
+
back_populates="users"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
user_permissions = relationship(
|
|
150
|
+
"Permission",
|
|
151
|
+
secondary=user_permissions,
|
|
152
|
+
back_populates="users"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
@validates('username')
|
|
156
|
+
def validate_username(self, key, username):
|
|
157
|
+
"""Validate username format"""
|
|
158
|
+
if not username:
|
|
159
|
+
raise ValueError("Username is required")
|
|
160
|
+
if len(username) < 3:
|
|
161
|
+
raise ValueError("Username must be at least 3 characters")
|
|
162
|
+
if not re.match(r'^[a-zA-Z0-9_]+$', username):
|
|
163
|
+
raise ValueError("Username can only contain letters, numbers, and underscores")
|
|
164
|
+
return username
|
|
165
|
+
|
|
166
|
+
@validates('email')
|
|
167
|
+
def validate_email(self, key, email):
|
|
168
|
+
"""Validate email format"""
|
|
169
|
+
if not email:
|
|
170
|
+
raise ValueError("Email is required")
|
|
171
|
+
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
|
172
|
+
if not re.match(email_pattern, email):
|
|
173
|
+
raise ValueError("Invalid email format")
|
|
174
|
+
return email.lower()
|
|
175
|
+
|
|
176
|
+
def set_password(self, password: str) -> None:
|
|
177
|
+
"""
|
|
178
|
+
Set user password with hashing
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
password: Plain text password
|
|
182
|
+
"""
|
|
183
|
+
if len(password) < 8:
|
|
184
|
+
raise ValueError("Password must be at least 8 characters")
|
|
185
|
+
|
|
186
|
+
self.password_hash = hash_password(password)
|
|
187
|
+
|
|
188
|
+
def verify_password(self, password: str) -> bool:
|
|
189
|
+
"""
|
|
190
|
+
Verify user password
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
password: Plain text password to verify
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
True if password is correct
|
|
197
|
+
"""
|
|
198
|
+
return verify_password(password, self.password_hash)
|
|
199
|
+
|
|
200
|
+
def check_password(self, password: str) -> bool:
|
|
201
|
+
"""Alias for verify_password"""
|
|
202
|
+
return self.verify_password(password)
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def full_name(self) -> str:
|
|
206
|
+
"""Get user's full name"""
|
|
207
|
+
if self.first_name and self.last_name:
|
|
208
|
+
return f"{self.first_name} {self.last_name}".strip()
|
|
209
|
+
elif self.first_name:
|
|
210
|
+
return self.first_name
|
|
211
|
+
elif self.last_name:
|
|
212
|
+
return self.last_name
|
|
213
|
+
return self.username
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def is_authenticated(self) -> bool:
|
|
217
|
+
"""Check if user is authenticated (always True for valid user objects)"""
|
|
218
|
+
return True
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def is_anonymous(self) -> bool:
|
|
222
|
+
"""Check if user is anonymous (always False for user objects)"""
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
def get_all_permissions(self) -> List[str]:
|
|
226
|
+
"""
|
|
227
|
+
Get all permissions for this user (direct + group permissions)
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
List of permission codenames
|
|
231
|
+
"""
|
|
232
|
+
permissions = set()
|
|
233
|
+
|
|
234
|
+
# Add direct permissions
|
|
235
|
+
for perm in self.user_permissions:
|
|
236
|
+
permissions.add(f"{perm.content_type}.{perm.codename}")
|
|
237
|
+
|
|
238
|
+
# Add group permissions
|
|
239
|
+
for group in self.groups:
|
|
240
|
+
for perm in group.permissions:
|
|
241
|
+
permissions.add(f"{perm.content_type}.{perm.codename}")
|
|
242
|
+
|
|
243
|
+
return list(permissions)
|
|
244
|
+
|
|
245
|
+
def has_permission(self, permission: str) -> bool:
|
|
246
|
+
"""
|
|
247
|
+
Check if user has a specific permission
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
permission: Permission string in format "app.codename"
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
True if user has permission
|
|
254
|
+
"""
|
|
255
|
+
if self.is_superuser:
|
|
256
|
+
return True
|
|
257
|
+
|
|
258
|
+
return permission in self.get_all_permissions()
|
|
259
|
+
|
|
260
|
+
def has_module_permission(self, app_label: str) -> bool:
|
|
261
|
+
"""
|
|
262
|
+
Check if user has any permission for a module/app
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
app_label: Application label
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
True if user has any permission for the app
|
|
269
|
+
"""
|
|
270
|
+
if self.is_superuser:
|
|
271
|
+
return True
|
|
272
|
+
|
|
273
|
+
permissions = self.get_all_permissions()
|
|
274
|
+
return any(perm.startswith(f"{app_label}.") for perm in permissions)
|
|
275
|
+
|
|
276
|
+
def generate_password_reset_token(self) -> str:
|
|
277
|
+
"""Generate a password reset token"""
|
|
278
|
+
token = secrets.token_urlsafe(32)
|
|
279
|
+
self.password_reset_token = token
|
|
280
|
+
return token
|
|
281
|
+
|
|
282
|
+
def generate_email_verification_token(self) -> str:
|
|
283
|
+
"""Generate an email verification token"""
|
|
284
|
+
token = secrets.token_urlsafe(32)
|
|
285
|
+
self.email_verification_token = token
|
|
286
|
+
return token
|
|
287
|
+
|
|
288
|
+
def is_account_locked(self) -> bool:
|
|
289
|
+
"""Check if account is currently locked"""
|
|
290
|
+
if self.account_locked_until is None:
|
|
291
|
+
return False
|
|
292
|
+
return datetime.utcnow() < self.account_locked_until
|
|
293
|
+
|
|
294
|
+
def lock_account(self, minutes: int = 30) -> None:
|
|
295
|
+
"""Lock account for specified minutes"""
|
|
296
|
+
self.account_locked_until = datetime.utcnow() + timedelta(minutes=minutes)
|
|
297
|
+
self.failed_login_attempts = 0
|
|
298
|
+
|
|
299
|
+
def unlock_account(self) -> None:
|
|
300
|
+
"""Unlock account"""
|
|
301
|
+
self.account_locked_until = None
|
|
302
|
+
self.failed_login_attempts = 0
|
|
303
|
+
|
|
304
|
+
def record_login_attempt(self, success: bool) -> None:
|
|
305
|
+
"""Record login attempt"""
|
|
306
|
+
if success:
|
|
307
|
+
self.failed_login_attempts = 0
|
|
308
|
+
self.last_login = datetime.utcnow()
|
|
309
|
+
self.unlock_account()
|
|
310
|
+
else:
|
|
311
|
+
self.failed_login_attempts += 1
|
|
312
|
+
if self.failed_login_attempts >= 5:
|
|
313
|
+
self.lock_account()
|
|
314
|
+
|
|
315
|
+
def __repr__(self) -> str:
|
|
316
|
+
return f"<User(id={self.id}, username='{self.username}', email='{self.email}')>"
|
|
317
|
+
|
|
318
|
+
def __str__(self) -> str:
|
|
319
|
+
return self.username
|
|
320
|
+
|
|
321
|
+
class Group(Base):
|
|
322
|
+
"""
|
|
323
|
+
CREATESONLINE Group Model
|
|
324
|
+
|
|
325
|
+
Groups are a way to categorize users and assign permissions.
|
|
326
|
+
"""
|
|
327
|
+
__tablename__ = "createsonline_groups"
|
|
328
|
+
|
|
329
|
+
# Primary fields
|
|
330
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
331
|
+
name = Column(String(150), unique=True, nullable=False, index=True)
|
|
332
|
+
description = Column(Text, nullable=True)
|
|
333
|
+
|
|
334
|
+
# Timestamps
|
|
335
|
+
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
336
|
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
337
|
+
|
|
338
|
+
# Relationships
|
|
339
|
+
users = relationship(
|
|
340
|
+
"User",
|
|
341
|
+
secondary=user_groups,
|
|
342
|
+
back_populates="groups"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
permissions = relationship(
|
|
346
|
+
"Permission",
|
|
347
|
+
secondary=group_permissions,
|
|
348
|
+
back_populates="groups"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
@validates('name')
|
|
352
|
+
def validate_name(self, key, name):
|
|
353
|
+
"""Validate group name"""
|
|
354
|
+
if not name:
|
|
355
|
+
raise ValueError("Group name is required")
|
|
356
|
+
if len(name) < 2:
|
|
357
|
+
raise ValueError("Group name must be at least 2 characters")
|
|
358
|
+
return name
|
|
359
|
+
|
|
360
|
+
def add_permission(self, permission: 'Permission') -> None:
|
|
361
|
+
"""Add permission to group"""
|
|
362
|
+
if permission not in self.permissions:
|
|
363
|
+
self.permissions.append(permission)
|
|
364
|
+
|
|
365
|
+
def remove_permission(self, permission: 'Permission') -> None:
|
|
366
|
+
"""Remove permission from group"""
|
|
367
|
+
if permission in self.permissions:
|
|
368
|
+
self.permissions.remove(permission)
|
|
369
|
+
|
|
370
|
+
def has_permission(self, permission_codename: str) -> bool:
|
|
371
|
+
"""Check if group has specific permission"""
|
|
372
|
+
return any(
|
|
373
|
+
perm.codename == permission_codename
|
|
374
|
+
for perm in self.permissions
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
def __repr__(self) -> str:
|
|
378
|
+
return f"<Group(id={self.id}, name='{self.name}')>"
|
|
379
|
+
|
|
380
|
+
def __str__(self) -> str:
|
|
381
|
+
return self.name
|
|
382
|
+
|
|
383
|
+
class Permission(Base):
|
|
384
|
+
"""
|
|
385
|
+
CREATESONLINE Permission Model
|
|
386
|
+
|
|
387
|
+
Permissions define what actions users can perform.
|
|
388
|
+
"""
|
|
389
|
+
__tablename__ = "createsonline_permissions"
|
|
390
|
+
|
|
391
|
+
# Primary fields
|
|
392
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
393
|
+
name = Column(String(255), nullable=False) # Human-readable name
|
|
394
|
+
codename = Column(String(100), nullable=False, index=True) # Machine-readable code
|
|
395
|
+
content_type = Column(String(100), nullable=False, index=True) # App/model name
|
|
396
|
+
|
|
397
|
+
# Timestamps
|
|
398
|
+
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
399
|
+
|
|
400
|
+
# Relationships
|
|
401
|
+
users = relationship(
|
|
402
|
+
"User",
|
|
403
|
+
secondary=user_permissions,
|
|
404
|
+
back_populates="user_permissions"
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
groups = relationship(
|
|
408
|
+
"Group",
|
|
409
|
+
secondary=group_permissions,
|
|
410
|
+
back_populates="permissions"
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# Unique constraint on codename + content_type
|
|
414
|
+
__table_args__ = (
|
|
415
|
+
sa.UniqueConstraint('codename', 'content_type', name='unique_permission'),
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
@validates('codename')
|
|
419
|
+
def validate_codename(self, key, codename):
|
|
420
|
+
"""Validate permission codename"""
|
|
421
|
+
if not codename:
|
|
422
|
+
raise ValueError("Permission codename is required")
|
|
423
|
+
if not re.match(r'^[a-z_]+$', codename):
|
|
424
|
+
raise ValueError("Codename can only contain lowercase letters and underscores")
|
|
425
|
+
return codename
|
|
426
|
+
|
|
427
|
+
@validates('content_type')
|
|
428
|
+
def validate_content_type(self, key, content_type):
|
|
429
|
+
"""Validate content type"""
|
|
430
|
+
if not content_type:
|
|
431
|
+
raise ValueError("Content type is required")
|
|
432
|
+
return content_type
|
|
433
|
+
|
|
434
|
+
@property
|
|
435
|
+
def natural_key(self) -> str:
|
|
436
|
+
"""Get natural key for permission"""
|
|
437
|
+
return f"{self.content_type}.{self.codename}"
|
|
438
|
+
|
|
439
|
+
def __repr__(self) -> str:
|
|
440
|
+
return f"<Permission(id={self.id}, name='{self.name}', codename='{self.codename}')>"
|
|
441
|
+
|
|
442
|
+
def __str__(self) -> str:
|
|
443
|
+
return f"{self.content_type} | {self.name}"
|
|
444
|
+
|
|
445
|
+
# Helper functions for creating default permissions
|
|
446
|
+
def create_default_permissions() -> List[Permission]:
|
|
447
|
+
"""Create default CREATESONLINE permissions"""
|
|
448
|
+
default_permissions = [
|
|
449
|
+
# User management
|
|
450
|
+
Permission(
|
|
451
|
+
name="Can add user",
|
|
452
|
+
codename="add_user",
|
|
453
|
+
content_type="auth"
|
|
454
|
+
),
|
|
455
|
+
Permission(
|
|
456
|
+
name="Can change user",
|
|
457
|
+
codename="change_user",
|
|
458
|
+
content_type="auth"
|
|
459
|
+
),
|
|
460
|
+
Permission(
|
|
461
|
+
name="Can delete user",
|
|
462
|
+
codename="delete_user",
|
|
463
|
+
content_type="auth"
|
|
464
|
+
),
|
|
465
|
+
Permission(
|
|
466
|
+
name="Can view user",
|
|
467
|
+
codename="view_user",
|
|
468
|
+
content_type="auth"
|
|
469
|
+
),
|
|
470
|
+
|
|
471
|
+
# Group management
|
|
472
|
+
Permission(
|
|
473
|
+
name="Can add group",
|
|
474
|
+
codename="add_group",
|
|
475
|
+
content_type="auth"
|
|
476
|
+
),
|
|
477
|
+
Permission(
|
|
478
|
+
name="Can change group",
|
|
479
|
+
codename="change_group",
|
|
480
|
+
content_type="auth"
|
|
481
|
+
),
|
|
482
|
+
Permission(
|
|
483
|
+
name="Can delete group",
|
|
484
|
+
codename="delete_group",
|
|
485
|
+
content_type="auth"
|
|
486
|
+
),
|
|
487
|
+
Permission(
|
|
488
|
+
name="Can view group",
|
|
489
|
+
codename="view_group",
|
|
490
|
+
content_type="auth"
|
|
491
|
+
),
|
|
492
|
+
|
|
493
|
+
# Admin access
|
|
494
|
+
Permission(
|
|
495
|
+
name="Can access admin",
|
|
496
|
+
codename="access_admin",
|
|
497
|
+
content_type="admin"
|
|
498
|
+
),
|
|
499
|
+
Permission(
|
|
500
|
+
name="Can view admin dashboard",
|
|
501
|
+
codename="view_dashboard",
|
|
502
|
+
content_type="admin"
|
|
503
|
+
),
|
|
504
|
+
|
|
505
|
+
# AI features
|
|
506
|
+
Permission(
|
|
507
|
+
name="Can use AI features",
|
|
508
|
+
codename="use_ai",
|
|
509
|
+
content_type="ai"
|
|
510
|
+
),
|
|
511
|
+
Permission(
|
|
512
|
+
name="Can manage AI models",
|
|
513
|
+
codename="manage_ai_models",
|
|
514
|
+
content_type="ai"
|
|
515
|
+
),
|
|
516
|
+
]
|
|
517
|
+
|
|
518
|
+
return default_permissions
|
|
519
|
+
|
|
520
|
+
def create_superuser(
|
|
521
|
+
username: str,
|
|
522
|
+
email: str,
|
|
523
|
+
password: str,
|
|
524
|
+
first_name: str = "",
|
|
525
|
+
last_name: str = ""
|
|
526
|
+
) -> User:
|
|
527
|
+
"""
|
|
528
|
+
Create a CREATESONLINE superuser
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
username: Username for superuser
|
|
532
|
+
email: Email for superuser
|
|
533
|
+
password: Password for superuser
|
|
534
|
+
first_name: Optional first name
|
|
535
|
+
last_name: Optional last name
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
Created User instance
|
|
539
|
+
"""
|
|
540
|
+
user = User(
|
|
541
|
+
username=username,
|
|
542
|
+
email=email,
|
|
543
|
+
first_name=first_name,
|
|
544
|
+
last_name=last_name,
|
|
545
|
+
is_staff=True,
|
|
546
|
+
is_superuser=True,
|
|
547
|
+
is_active=True,
|
|
548
|
+
email_verified=True
|
|
549
|
+
)
|
|
550
|
+
user.set_password(password)
|
|
551
|
+
|
|
552
|
+
return user
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import logging`n_logger = logging.getLogger("createsonline.cli.commands")`n# createsonline/cli/commands/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
CREATESONLINE CLI Commands
|
|
4
|
+
|
|
5
|
+
Individual command modules for the CREATESONLINE CLI.
|
|
6
|
+
FIXED: Lazy imports to prevent dependency crashes when optional packages missing
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Lazy import pattern - don't import commands eagerly
|
|
10
|
+
# This prevents ImportError if rich/typer/uvicorn are missing
|
|
11
|
+
__all__ = [
|
|
12
|
+
"serve_command",
|
|
13
|
+
"dev_command",
|
|
14
|
+
"prod_command",
|
|
15
|
+
"info_command",
|
|
16
|
+
"version_command",
|
|
17
|
+
"new_command",
|
|
18
|
+
"shell_command",
|
|
19
|
+
"createsuperuser_command",
|
|
20
|
+
"db_ai_query_command",
|
|
21
|
+
"db_rollback_command",
|
|
22
|
+
"db_audit_log_command"
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
def _lazy_import_serve():
|
|
26
|
+
"""Lazy import serve commands"""
|
|
27
|
+
try:
|
|
28
|
+
from .serve import serve_command, dev_command, prod_command
|
|
29
|
+
return serve_command, dev_command, prod_command
|
|
30
|
+
except ImportError as e:
|
|
31
|
+
def _missing_command(*args, **kwargs):
|
|
32
|
+
_logger.error(f"Command requires additional dependencies: {e}")
|
|
33
|
+
_logger.info("Install with: pip install uvicorn")
|
|
34
|
+
return
|
|
35
|
+
return _missing_command, _missing_command, _missing_command
|
|
36
|
+
|
|
37
|
+
def _lazy_import_info():
|
|
38
|
+
"""Lazy import info commands"""
|
|
39
|
+
try:
|
|
40
|
+
from .info import info_command, version_command
|
|
41
|
+
return info_command, version_command
|
|
42
|
+
except ImportError as e:
|
|
43
|
+
def _missing_command(*args, **kwargs):
|
|
44
|
+
_logger.error(f"Command requires additional dependencies: {e}")
|
|
45
|
+
_logger.info("Install with: pip install rich")
|
|
46
|
+
return
|
|
47
|
+
return _missing_command, _missing_command
|
|
48
|
+
|
|
49
|
+
def _lazy_import_project():
|
|
50
|
+
"""Lazy import project commands"""
|
|
51
|
+
try:
|
|
52
|
+
from .project import new_command
|
|
53
|
+
return new_command
|
|
54
|
+
except ImportError as e:
|
|
55
|
+
def _missing_command(*args, **kwargs):
|
|
56
|
+
_logger.error(f"Command requires additional dependencies: {e}")
|
|
57
|
+
_logger.info("Install with: pip install rich typer")
|
|
58
|
+
return
|
|
59
|
+
return _missing_command
|
|
60
|
+
|
|
61
|
+
def _lazy_import_shell():
|
|
62
|
+
"""Lazy import shell commands"""
|
|
63
|
+
try:
|
|
64
|
+
from .shell import shell_command
|
|
65
|
+
return shell_command
|
|
66
|
+
except ImportError as e:
|
|
67
|
+
def _missing_command(*args, **kwargs):
|
|
68
|
+
_logger.error(f"Command requires additional dependencies: {e}")
|
|
69
|
+
_logger.info("Install with: pip install rich")
|
|
70
|
+
return
|
|
71
|
+
return _missing_command
|
|
72
|
+
|
|
73
|
+
def _lazy_import_users():
|
|
74
|
+
"""Lazy import user commands"""
|
|
75
|
+
try:
|
|
76
|
+
from .users import createsuperuser_command
|
|
77
|
+
return createsuperuser_command
|
|
78
|
+
except ImportError as e:
|
|
79
|
+
def _missing_command(*args, **kwargs):
|
|
80
|
+
_logger.error(f"Command requires additional dependencies: {e}")
|
|
81
|
+
_logger.info("Install with: pip install rich typer")
|
|
82
|
+
return
|
|
83
|
+
return _missing_command
|
|
84
|
+
|
|
85
|
+
def _lazy_import_database():
|
|
86
|
+
"""Lazy import database commands"""
|
|
87
|
+
try:
|
|
88
|
+
from .database import db_ai_query_command, db_rollback_command, db_audit_log_command
|
|
89
|
+
return db_ai_query_command, db_rollback_command, db_audit_log_command
|
|
90
|
+
except ImportError as e:
|
|
91
|
+
def _missing_command(*args, **kwargs):
|
|
92
|
+
_logger.error(f"Database commands require additional dependencies: {e}")
|
|
93
|
+
_logger.info("Install with: pip install rich")
|
|
94
|
+
return
|
|
95
|
+
return _missing_command, _missing_command, _missing_command
|
|
96
|
+
|
|
97
|
+
# Lazy loading attributes
|
|
98
|
+
def __getattr__(name):
|
|
99
|
+
if name == "serve_command":
|
|
100
|
+
return _lazy_import_serve()[0]
|
|
101
|
+
elif name == "dev_command":
|
|
102
|
+
return _lazy_import_serve()[1]
|
|
103
|
+
elif name == "prod_command":
|
|
104
|
+
return _lazy_import_serve()[2]
|
|
105
|
+
elif name == "info_command":
|
|
106
|
+
return _lazy_import_info()[0]
|
|
107
|
+
elif name == "version_command":
|
|
108
|
+
return _lazy_import_info()[1]
|
|
109
|
+
elif name == "new_command":
|
|
110
|
+
return _lazy_import_project()
|
|
111
|
+
elif name == "shell_command":
|
|
112
|
+
return _lazy_import_shell()
|
|
113
|
+
elif name == "createsuperuser_command":
|
|
114
|
+
return _lazy_import_users()
|
|
115
|
+
elif name == "db_ai_query_command":
|
|
116
|
+
return _lazy_import_database()[0]
|
|
117
|
+
elif name == "db_rollback_command":
|
|
118
|
+
return _lazy_import_database()[1]
|
|
119
|
+
elif name == "db_audit_log_command":
|
|
120
|
+
return _lazy_import_database()[2]
|
|
121
|
+
else:
|
|
122
|
+
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|