Akoda35 commited on
Commit
71d5253
·
verified ·
1 Parent(s): 7018d0a

Upload 22 files

Browse files
core/migrations/0001_initial.py ADDED
@@ -0,0 +1,425 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 6.0.1 on 2026-01-18 17:49
2
+
3
+ import django.contrib.auth.models
4
+ import django.contrib.auth.validators
5
+ import django.db.models.deletion
6
+ import django.utils.timezone
7
+ import uuid
8
+ from django.conf import settings
9
+ from django.db import migrations, models
10
+
11
+
12
+ class Migration(migrations.Migration):
13
+
14
+ initial = True
15
+
16
+ dependencies = [
17
+ ("auth", "0012_alter_user_first_name_max_length"),
18
+ ]
19
+
20
+ operations = [
21
+ migrations.CreateModel(
22
+ name="User",
23
+ fields=[
24
+ (
25
+ "id",
26
+ models.BigAutoField(
27
+ auto_created=True,
28
+ primary_key=True,
29
+ serialize=False,
30
+ verbose_name="ID",
31
+ ),
32
+ ),
33
+ ("password", models.CharField(max_length=128, verbose_name="password")),
34
+ (
35
+ "last_login",
36
+ models.DateTimeField(
37
+ blank=True, null=True, verbose_name="last login"
38
+ ),
39
+ ),
40
+ (
41
+ "is_superuser",
42
+ models.BooleanField(
43
+ default=False,
44
+ help_text="Designates that this user has all permissions without explicitly assigning them.",
45
+ verbose_name="superuser status",
46
+ ),
47
+ ),
48
+ (
49
+ "username",
50
+ models.CharField(
51
+ error_messages={
52
+ "unique": "A user with that username already exists."
53
+ },
54
+ help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
55
+ max_length=150,
56
+ unique=True,
57
+ validators=[
58
+ django.contrib.auth.validators.UnicodeUsernameValidator()
59
+ ],
60
+ verbose_name="username",
61
+ ),
62
+ ),
63
+ (
64
+ "first_name",
65
+ models.CharField(
66
+ blank=True, max_length=150, verbose_name="first name"
67
+ ),
68
+ ),
69
+ (
70
+ "last_name",
71
+ models.CharField(
72
+ blank=True, max_length=150, verbose_name="last name"
73
+ ),
74
+ ),
75
+ (
76
+ "is_staff",
77
+ models.BooleanField(
78
+ default=False,
79
+ help_text="Designates whether the user can log into this admin site.",
80
+ verbose_name="staff status",
81
+ ),
82
+ ),
83
+ (
84
+ "is_active",
85
+ models.BooleanField(
86
+ default=True,
87
+ help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
88
+ verbose_name="active",
89
+ ),
90
+ ),
91
+ (
92
+ "date_joined",
93
+ models.DateTimeField(
94
+ default=django.utils.timezone.now, verbose_name="date joined"
95
+ ),
96
+ ),
97
+ ("email", models.EmailField(max_length=254, unique=True)),
98
+ (
99
+ "is_verified",
100
+ models.BooleanField(
101
+ default=False,
102
+ help_text="Whether the user has verified their email via magic link",
103
+ ),
104
+ ),
105
+ (
106
+ "groups",
107
+ models.ManyToManyField(
108
+ blank=True,
109
+ help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
110
+ related_name="user_set",
111
+ related_query_name="user",
112
+ to="auth.group",
113
+ verbose_name="groups",
114
+ ),
115
+ ),
116
+ (
117
+ "user_permissions",
118
+ models.ManyToManyField(
119
+ blank=True,
120
+ help_text="Specific permissions for this user.",
121
+ related_name="user_set",
122
+ related_query_name="user",
123
+ to="auth.permission",
124
+ verbose_name="user permissions",
125
+ ),
126
+ ),
127
+ ],
128
+ options={
129
+ "verbose_name": "user",
130
+ "verbose_name_plural": "users",
131
+ "db_table": "users",
132
+ },
133
+ managers=[
134
+ ("objects", django.contrib.auth.models.UserManager()),
135
+ ],
136
+ ),
137
+ migrations.CreateModel(
138
+ name="Environment",
139
+ fields=[
140
+ (
141
+ "id",
142
+ models.UUIDField(
143
+ default=uuid.uuid4,
144
+ editable=False,
145
+ primary_key=True,
146
+ serialize=False,
147
+ ),
148
+ ),
149
+ ("name", models.CharField(max_length=100)),
150
+ ("description", models.TextField(blank=True)),
151
+ (
152
+ "path",
153
+ models.CharField(
154
+ help_text="Directory name within the environments folder",
155
+ max_length=255,
156
+ unique=True,
157
+ ),
158
+ ),
159
+ ("python_version", models.CharField(blank=True, max_length=20)),
160
+ (
161
+ "requirements",
162
+ models.TextField(
163
+ blank=True,
164
+ help_text="Installed packages in pip requirements format",
165
+ ),
166
+ ),
167
+ (
168
+ "is_default",
169
+ models.BooleanField(
170
+ default=False,
171
+ help_text="Whether this is the default environment for new scripts",
172
+ ),
173
+ ),
174
+ (
175
+ "is_active",
176
+ models.BooleanField(
177
+ default=True,
178
+ help_text="Whether this environment is available for use",
179
+ ),
180
+ ),
181
+ ("created_at", models.DateTimeField(auto_now_add=True)),
182
+ ("updated_at", models.DateTimeField(auto_now=True)),
183
+ (
184
+ "created_by",
185
+ models.ForeignKey(
186
+ blank=True,
187
+ null=True,
188
+ on_delete=django.db.models.deletion.SET_NULL,
189
+ related_name="created_environments",
190
+ to=settings.AUTH_USER_MODEL,
191
+ ),
192
+ ),
193
+ ],
194
+ options={
195
+ "verbose_name": "environment",
196
+ "verbose_name_plural": "environments",
197
+ "db_table": "environments",
198
+ "ordering": ["-is_default", "name"],
199
+ },
200
+ ),
201
+ migrations.CreateModel(
202
+ name="Script",
203
+ fields=[
204
+ (
205
+ "id",
206
+ models.UUIDField(
207
+ default=uuid.uuid4,
208
+ editable=False,
209
+ primary_key=True,
210
+ serialize=False,
211
+ ),
212
+ ),
213
+ ("name", models.CharField(max_length=200)),
214
+ ("description", models.TextField(blank=True)),
215
+ ("code", models.TextField(help_text="Python code to execute")),
216
+ (
217
+ "timeout_seconds",
218
+ models.PositiveIntegerField(
219
+ default=300,
220
+ help_text="Maximum execution time in seconds (default: 5 minutes)",
221
+ ),
222
+ ),
223
+ (
224
+ "is_enabled",
225
+ models.BooleanField(
226
+ default=True, help_text="Whether this script can be executed"
227
+ ),
228
+ ),
229
+ ("created_at", models.DateTimeField(auto_now_add=True)),
230
+ ("updated_at", models.DateTimeField(auto_now=True)),
231
+ (
232
+ "created_by",
233
+ models.ForeignKey(
234
+ blank=True,
235
+ null=True,
236
+ on_delete=django.db.models.deletion.SET_NULL,
237
+ related_name="scripts",
238
+ to=settings.AUTH_USER_MODEL,
239
+ ),
240
+ ),
241
+ (
242
+ "environment",
243
+ models.ForeignKey(
244
+ help_text="Python environment to use for execution",
245
+ on_delete=django.db.models.deletion.PROTECT,
246
+ related_name="scripts",
247
+ to="core.environment",
248
+ ),
249
+ ),
250
+ ],
251
+ options={
252
+ "verbose_name": "script",
253
+ "verbose_name_plural": "scripts",
254
+ "db_table": "scripts",
255
+ "ordering": ["-updated_at"],
256
+ },
257
+ ),
258
+ migrations.CreateModel(
259
+ name="MagicToken",
260
+ fields=[
261
+ (
262
+ "id",
263
+ models.BigAutoField(
264
+ auto_created=True,
265
+ primary_key=True,
266
+ serialize=False,
267
+ verbose_name="ID",
268
+ ),
269
+ ),
270
+ ("token", models.CharField(db_index=True, max_length=64, unique=True)),
271
+ (
272
+ "email",
273
+ models.EmailField(
274
+ help_text="Email address this token was sent to", max_length=254
275
+ ),
276
+ ),
277
+ ("created_at", models.DateTimeField(auto_now_add=True)),
278
+ ("expires_at", models.DateTimeField()),
279
+ ("used_at", models.DateTimeField(blank=True, null=True)),
280
+ (
281
+ "ip_address",
282
+ models.GenericIPAddressField(
283
+ blank=True,
284
+ help_text="IP address that requested this token",
285
+ null=True,
286
+ ),
287
+ ),
288
+ (
289
+ "user",
290
+ models.ForeignKey(
291
+ blank=True,
292
+ null=True,
293
+ on_delete=django.db.models.deletion.CASCADE,
294
+ related_name="magic_tokens",
295
+ to=settings.AUTH_USER_MODEL,
296
+ ),
297
+ ),
298
+ ],
299
+ options={
300
+ "verbose_name": "magic token",
301
+ "verbose_name_plural": "magic tokens",
302
+ "db_table": "magic_tokens",
303
+ "indexes": [
304
+ models.Index(
305
+ fields=["token", "expires_at"],
306
+ name="magic_token_token_b6602a_idx",
307
+ ),
308
+ models.Index(
309
+ fields=["email", "created_at"],
310
+ name="magic_token_email_302c69_idx",
311
+ ),
312
+ ],
313
+ },
314
+ ),
315
+ migrations.CreateModel(
316
+ name="Run",
317
+ fields=[
318
+ (
319
+ "id",
320
+ models.UUIDField(
321
+ default=uuid.uuid4,
322
+ editable=False,
323
+ primary_key=True,
324
+ serialize=False,
325
+ ),
326
+ ),
327
+ (
328
+ "status",
329
+ models.CharField(
330
+ choices=[
331
+ ("pending", "Pending"),
332
+ ("running", "Running"),
333
+ ("success", "Success"),
334
+ ("failed", "Failed"),
335
+ ("timeout", "Timeout"),
336
+ ("cancelled", "Cancelled"),
337
+ ],
338
+ db_index=True,
339
+ default="pending",
340
+ max_length=20,
341
+ ),
342
+ ),
343
+ (
344
+ "exit_code",
345
+ models.IntegerField(
346
+ blank=True,
347
+ help_text="Process exit code (0 = success)",
348
+ null=True,
349
+ ),
350
+ ),
351
+ (
352
+ "stdout",
353
+ models.TextField(
354
+ blank=True, help_text="Standard output from script execution"
355
+ ),
356
+ ),
357
+ (
358
+ "stderr",
359
+ models.TextField(
360
+ blank=True, help_text="Standard error from script execution"
361
+ ),
362
+ ),
363
+ (
364
+ "started_at",
365
+ models.DateTimeField(
366
+ blank=True, help_text="When execution started", null=True
367
+ ),
368
+ ),
369
+ (
370
+ "ended_at",
371
+ models.DateTimeField(
372
+ blank=True, help_text="When execution ended", null=True
373
+ ),
374
+ ),
375
+ (
376
+ "code_snapshot",
377
+ models.TextField(
378
+ blank=True, help_text="Copy of script code at time of execution"
379
+ ),
380
+ ),
381
+ (
382
+ "task_id",
383
+ models.CharField(
384
+ blank=True,
385
+ db_index=True,
386
+ help_text="django-q2 task ID for tracking async execution",
387
+ max_length=100,
388
+ ),
389
+ ),
390
+ ("created_at", models.DateTimeField(auto_now_add=True)),
391
+ (
392
+ "triggered_by",
393
+ models.ForeignKey(
394
+ blank=True,
395
+ null=True,
396
+ on_delete=django.db.models.deletion.SET_NULL,
397
+ related_name="triggered_runs",
398
+ to=settings.AUTH_USER_MODEL,
399
+ ),
400
+ ),
401
+ (
402
+ "script",
403
+ models.ForeignKey(
404
+ on_delete=django.db.models.deletion.CASCADE,
405
+ related_name="runs",
406
+ to="core.script",
407
+ ),
408
+ ),
409
+ ],
410
+ options={
411
+ "verbose_name": "run",
412
+ "verbose_name_plural": "runs",
413
+ "db_table": "runs",
414
+ "ordering": ["-created_at"],
415
+ "indexes": [
416
+ models.Index(
417
+ fields=["script", "-created_at"], name="runs_script__1182ba_idx"
418
+ ),
419
+ models.Index(
420
+ fields=["status", "-created_at"], name="runs_status_56f7d6_idx"
421
+ ),
422
+ ],
423
+ },
424
+ ),
425
+ ]
core/migrations/0002_add_scheduling_models.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 5.2.9 on 2026-01-19 15:56
2
+
3
+ import django.db.models.deletion
4
+ import uuid
5
+ from django.conf import settings
6
+ from django.db import migrations, models
7
+
8
+
9
+ class Migration(migrations.Migration):
10
+
11
+ dependencies = [
12
+ ("core", "0001_initial"),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.AddField(
17
+ model_name="run",
18
+ name="trigger_type",
19
+ field=models.CharField(
20
+ choices=[
21
+ ("manual", "Manual"),
22
+ ("scheduled", "Scheduled"),
23
+ ("api", "API"),
24
+ ],
25
+ default="manual",
26
+ help_text="How this run was triggered",
27
+ max_length=20,
28
+ ),
29
+ ),
30
+ migrations.CreateModel(
31
+ name="GlobalSettings",
32
+ fields=[
33
+ (
34
+ "id",
35
+ models.BigAutoField(
36
+ auto_created=True,
37
+ primary_key=True,
38
+ serialize=False,
39
+ verbose_name="ID",
40
+ ),
41
+ ),
42
+ (
43
+ "schedules_paused",
44
+ models.BooleanField(
45
+ default=False,
46
+ help_text="Global pause for all scheduled script executions",
47
+ ),
48
+ ),
49
+ (
50
+ "schedules_paused_at",
51
+ models.DateTimeField(
52
+ blank=True, help_text="When schedules were paused", null=True
53
+ ),
54
+ ),
55
+ ("updated_at", models.DateTimeField(auto_now=True)),
56
+ (
57
+ "schedules_paused_by",
58
+ models.ForeignKey(
59
+ blank=True,
60
+ null=True,
61
+ on_delete=django.db.models.deletion.SET_NULL,
62
+ related_name="+",
63
+ to=settings.AUTH_USER_MODEL,
64
+ ),
65
+ ),
66
+ ],
67
+ options={
68
+ "verbose_name": "global settings",
69
+ "verbose_name_plural": "global settings",
70
+ "db_table": "global_settings",
71
+ },
72
+ ),
73
+ migrations.CreateModel(
74
+ name="ScriptSchedule",
75
+ fields=[
76
+ (
77
+ "id",
78
+ models.UUIDField(
79
+ default=uuid.uuid4,
80
+ editable=False,
81
+ primary_key=True,
82
+ serialize=False,
83
+ ),
84
+ ),
85
+ (
86
+ "run_mode",
87
+ models.CharField(
88
+ choices=[
89
+ ("manual", "Manual"),
90
+ ("interval", "Interval"),
91
+ ("daily", "Daily"),
92
+ ],
93
+ default="manual",
94
+ max_length=20,
95
+ ),
96
+ ),
97
+ (
98
+ "interval_minutes",
99
+ models.PositiveIntegerField(
100
+ blank=True,
101
+ choices=[
102
+ (5, "Every 5 minutes"),
103
+ (10, "Every 10 minutes"),
104
+ (15, "Every 15 minutes"),
105
+ (30, "Every 30 minutes"),
106
+ (60, "Every hour"),
107
+ (120, "Every 2 hours"),
108
+ (360, "Every 6 hours"),
109
+ (720, "Every 12 hours"),
110
+ ],
111
+ help_text="Interval in minutes (for interval mode)",
112
+ null=True,
113
+ ),
114
+ ),
115
+ (
116
+ "daily_times",
117
+ models.JSONField(
118
+ blank=True,
119
+ default=list,
120
+ help_text='List of times in HH:MM format, e.g., ["09:00", "18:00"]',
121
+ ),
122
+ ),
123
+ (
124
+ "timezone",
125
+ models.CharField(
126
+ default="UTC",
127
+ help_text="Timezone for daily schedules (e.g., 'America/New_York')",
128
+ max_length=50,
129
+ ),
130
+ ),
131
+ (
132
+ "is_active",
133
+ models.BooleanField(
134
+ default=True,
135
+ help_text="Whether this schedule is active (can be paused without deleting)",
136
+ ),
137
+ ),
138
+ (
139
+ "q_schedule_ids",
140
+ models.JSONField(
141
+ blank=True,
142
+ default=list,
143
+ help_text="django-q2 Schedule object IDs",
144
+ ),
145
+ ),
146
+ (
147
+ "next_run",
148
+ models.DateTimeField(
149
+ blank=True,
150
+ help_text="Cached next scheduled run time",
151
+ null=True,
152
+ ),
153
+ ),
154
+ (
155
+ "last_scheduled_run",
156
+ models.DateTimeField(
157
+ blank=True,
158
+ help_text="When the schedule last triggered a run",
159
+ null=True,
160
+ ),
161
+ ),
162
+ ("created_at", models.DateTimeField(auto_now_add=True)),
163
+ ("updated_at", models.DateTimeField(auto_now=True)),
164
+ (
165
+ "created_by",
166
+ models.ForeignKey(
167
+ blank=True,
168
+ null=True,
169
+ on_delete=django.db.models.deletion.SET_NULL,
170
+ related_name="created_schedules",
171
+ to=settings.AUTH_USER_MODEL,
172
+ ),
173
+ ),
174
+ (
175
+ "script",
176
+ models.OneToOneField(
177
+ on_delete=django.db.models.deletion.CASCADE,
178
+ related_name="schedule",
179
+ to="core.script",
180
+ ),
181
+ ),
182
+ ],
183
+ options={
184
+ "verbose_name": "script schedule",
185
+ "verbose_name_plural": "script schedules",
186
+ "db_table": "script_schedules",
187
+ },
188
+ ),
189
+ migrations.CreateModel(
190
+ name="ScheduleHistory",
191
+ fields=[
192
+ (
193
+ "id",
194
+ models.UUIDField(
195
+ default=uuid.uuid4,
196
+ editable=False,
197
+ primary_key=True,
198
+ serialize=False,
199
+ ),
200
+ ),
201
+ (
202
+ "change_type",
203
+ models.CharField(
204
+ choices=[
205
+ ("created", "Created"),
206
+ ("updated", "Updated"),
207
+ ("enabled", "Enabled"),
208
+ ("disabled", "Disabled"),
209
+ ("deleted", "Deleted"),
210
+ ],
211
+ max_length=20,
212
+ ),
213
+ ),
214
+ (
215
+ "previous_config",
216
+ models.JSONField(
217
+ blank=True,
218
+ help_text="Previous schedule configuration",
219
+ null=True,
220
+ ),
221
+ ),
222
+ (
223
+ "new_config",
224
+ models.JSONField(
225
+ blank=True, help_text="New schedule configuration", null=True
226
+ ),
227
+ ),
228
+ ("created_at", models.DateTimeField(auto_now_add=True)),
229
+ (
230
+ "changed_by",
231
+ models.ForeignKey(
232
+ blank=True,
233
+ null=True,
234
+ on_delete=django.db.models.deletion.SET_NULL,
235
+ to=settings.AUTH_USER_MODEL,
236
+ ),
237
+ ),
238
+ (
239
+ "schedule",
240
+ models.ForeignKey(
241
+ on_delete=django.db.models.deletion.CASCADE,
242
+ related_name="history",
243
+ to="core.scriptschedule",
244
+ ),
245
+ ),
246
+ ],
247
+ options={
248
+ "verbose_name": "schedule history",
249
+ "verbose_name_plural": "schedule history entries",
250
+ "db_table": "schedule_history",
251
+ "ordering": ["-created_at"],
252
+ },
253
+ ),
254
+ ]
core/migrations/0003_add_package_operation.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 5.2.9 on 2026-01-20 07:30
2
+
3
+ import django.db.models.deletion
4
+ import uuid
5
+ from django.conf import settings
6
+ from django.db import migrations, models
7
+
8
+
9
+ class Migration(migrations.Migration):
10
+
11
+ dependencies = [
12
+ ("core", "0002_add_scheduling_models"),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.CreateModel(
17
+ name="PackageOperation",
18
+ fields=[
19
+ (
20
+ "id",
21
+ models.UUIDField(
22
+ default=uuid.uuid4,
23
+ editable=False,
24
+ primary_key=True,
25
+ serialize=False,
26
+ ),
27
+ ),
28
+ (
29
+ "operation",
30
+ models.CharField(
31
+ choices=[
32
+ ("install", "Install"),
33
+ ("uninstall", "Uninstall"),
34
+ ("bulk_install", "Bulk Install"),
35
+ ],
36
+ max_length=20,
37
+ ),
38
+ ),
39
+ (
40
+ "package_spec",
41
+ models.CharField(
42
+ help_text="Package specification (e.g., 'requests==2.31.0') or requirements content for bulk install",
43
+ max_length=500,
44
+ ),
45
+ ),
46
+ (
47
+ "status",
48
+ models.CharField(
49
+ choices=[
50
+ ("pending", "Pending"),
51
+ ("running", "Running"),
52
+ ("success", "Success"),
53
+ ("failed", "Failed"),
54
+ ],
55
+ default="pending",
56
+ max_length=20,
57
+ ),
58
+ ),
59
+ ("output", models.TextField(blank=True, help_text="pip stdout")),
60
+ ("error", models.TextField(blank=True, help_text="pip stderr")),
61
+ (
62
+ "task_id",
63
+ models.CharField(
64
+ blank=True,
65
+ help_text="django-q2 task ID for tracking",
66
+ max_length=100,
67
+ ),
68
+ ),
69
+ ("created_at", models.DateTimeField(auto_now_add=True)),
70
+ ("started_at", models.DateTimeField(blank=True, null=True)),
71
+ ("completed_at", models.DateTimeField(blank=True, null=True)),
72
+ (
73
+ "created_by",
74
+ models.ForeignKey(
75
+ blank=True,
76
+ null=True,
77
+ on_delete=django.db.models.deletion.SET_NULL,
78
+ related_name="package_operations",
79
+ to=settings.AUTH_USER_MODEL,
80
+ ),
81
+ ),
82
+ (
83
+ "environment",
84
+ models.ForeignKey(
85
+ on_delete=django.db.models.deletion.CASCADE,
86
+ related_name="package_operations",
87
+ to="core.environment",
88
+ ),
89
+ ),
90
+ ],
91
+ options={
92
+ "verbose_name": "package operation",
93
+ "verbose_name_plural": "package operations",
94
+ "db_table": "package_operations",
95
+ "ordering": ["-created_at"],
96
+ },
97
+ ),
98
+ ]
core/migrations/0004_secret.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 5.2.9 on 2026-01-20 08:26
2
+
3
+ import django.db.models.deletion
4
+ import uuid
5
+ from django.conf import settings
6
+ from django.db import migrations, models
7
+
8
+
9
+ class Migration(migrations.Migration):
10
+
11
+ dependencies = [
12
+ ("core", "0003_add_package_operation"),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.CreateModel(
17
+ name="Secret",
18
+ fields=[
19
+ (
20
+ "id",
21
+ models.UUIDField(
22
+ default=uuid.uuid4,
23
+ editable=False,
24
+ primary_key=True,
25
+ serialize=False,
26
+ ),
27
+ ),
28
+ (
29
+ "key",
30
+ models.CharField(
31
+ help_text="Environment variable name (uppercase, underscores allowed)",
32
+ max_length=100,
33
+ unique=True,
34
+ ),
35
+ ),
36
+ (
37
+ "encrypted_value",
38
+ models.TextField(help_text="Fernet-encrypted secret value"),
39
+ ),
40
+ (
41
+ "description",
42
+ models.TextField(
43
+ blank=True,
44
+ help_text="Optional description of what this secret is used for",
45
+ ),
46
+ ),
47
+ ("created_at", models.DateTimeField(auto_now_add=True)),
48
+ ("updated_at", models.DateTimeField(auto_now=True)),
49
+ (
50
+ "created_by",
51
+ models.ForeignKey(
52
+ blank=True,
53
+ null=True,
54
+ on_delete=django.db.models.deletion.SET_NULL,
55
+ related_name="created_secrets",
56
+ to=settings.AUTH_USER_MODEL,
57
+ ),
58
+ ),
59
+ ],
60
+ options={
61
+ "verbose_name": "secret",
62
+ "verbose_name_plural": "secrets",
63
+ "db_table": "secrets",
64
+ "ordering": ["key"],
65
+ },
66
+ ),
67
+ ]
core/migrations/0005_add_webhook_token.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 5.2.9 on 2026-01-20 14:08
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("core", "0004_secret"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="script",
15
+ name="webhook_token",
16
+ field=models.CharField(
17
+ blank=True,
18
+ db_index=True,
19
+ help_text="Unique token for webhook URL (auto-generated)",
20
+ max_length=64,
21
+ null=True,
22
+ unique=True,
23
+ ),
24
+ ),
25
+ ]
core/migrations/0006_notification_settings.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 5.2.9 on 2026-01-20 14:33
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("core", "0005_add_webhook_token"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="globalsettings",
15
+ name="default_notification_email",
16
+ field=models.EmailField(
17
+ blank=True,
18
+ help_text="Default email address for all notifications",
19
+ max_length=254,
20
+ ),
21
+ ),
22
+ migrations.AddField(
23
+ model_name="globalsettings",
24
+ name="email_backend",
25
+ field=models.CharField(
26
+ choices=[
27
+ ("disabled", "Disabled"),
28
+ ("smtp", "SMTP"),
29
+ ("resend", "Resend API"),
30
+ ],
31
+ default="disabled",
32
+ help_text="Email backend for notifications",
33
+ max_length=20,
34
+ ),
35
+ ),
36
+ migrations.AddField(
37
+ model_name="globalsettings",
38
+ name="resend_api_key_encrypted",
39
+ field=models.TextField(blank=True, help_text="Resend API key (encrypted)"),
40
+ ),
41
+ migrations.AddField(
42
+ model_name="globalsettings",
43
+ name="resend_from_email",
44
+ field=models.EmailField(
45
+ blank=True, help_text="From email address for Resend", max_length=254
46
+ ),
47
+ ),
48
+ migrations.AddField(
49
+ model_name="globalsettings",
50
+ name="smtp_from_email",
51
+ field=models.EmailField(
52
+ blank=True, help_text="From email address for SMTP", max_length=254
53
+ ),
54
+ ),
55
+ migrations.AddField(
56
+ model_name="globalsettings",
57
+ name="smtp_host",
58
+ field=models.CharField(
59
+ blank=True, help_text="SMTP server hostname", max_length=255
60
+ ),
61
+ ),
62
+ migrations.AddField(
63
+ model_name="globalsettings",
64
+ name="smtp_password_encrypted",
65
+ field=models.TextField(blank=True, help_text="SMTP password (encrypted)"),
66
+ ),
67
+ migrations.AddField(
68
+ model_name="globalsettings",
69
+ name="smtp_port",
70
+ field=models.PositiveIntegerField(
71
+ default=587, help_text="SMTP server port"
72
+ ),
73
+ ),
74
+ migrations.AddField(
75
+ model_name="globalsettings",
76
+ name="smtp_use_tls",
77
+ field=models.BooleanField(
78
+ default=True, help_text="Use TLS for SMTP connection"
79
+ ),
80
+ ),
81
+ migrations.AddField(
82
+ model_name="globalsettings",
83
+ name="smtp_username",
84
+ field=models.CharField(
85
+ blank=True, help_text="SMTP username", max_length=255
86
+ ),
87
+ ),
88
+ migrations.AddField(
89
+ model_name="script",
90
+ name="notify_email",
91
+ field=models.EmailField(
92
+ blank=True,
93
+ help_text="Override email for this script (uses global default if empty)",
94
+ max_length=254,
95
+ ),
96
+ ),
97
+ migrations.AddField(
98
+ model_name="script",
99
+ name="notify_on",
100
+ field=models.CharField(
101
+ choices=[
102
+ ("never", "Never"),
103
+ ("failure", "On Failure"),
104
+ ("success", "On Success"),
105
+ ("both", "On Success and Failure"),
106
+ ],
107
+ default="never",
108
+ help_text="When to send notifications for this script",
109
+ max_length=20,
110
+ ),
111
+ ),
112
+ migrations.AddField(
113
+ model_name="script",
114
+ name="notify_webhook_enabled",
115
+ field=models.BooleanField(
116
+ default=False, help_text="Enable webhook notifications for this script"
117
+ ),
118
+ ),
119
+ migrations.AddField(
120
+ model_name="script",
121
+ name="notify_webhook_url",
122
+ field=models.URLField(
123
+ blank=True,
124
+ help_text="URL to POST notification webhooks to",
125
+ max_length=500,
126
+ ),
127
+ ),
128
+ ]
core/migrations/0007_phase7_settings.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 5.2.9 on 2026-01-21 07:30
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("core", "0006_notification_settings"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="globalsettings",
15
+ name="auto_cleanup_enabled",
16
+ field=models.BooleanField(
17
+ default=False, help_text="Automatically clean up old runs daily"
18
+ ),
19
+ ),
20
+ migrations.AddField(
21
+ model_name="globalsettings",
22
+ name="date_format",
23
+ field=models.CharField(
24
+ choices=[
25
+ ("YYYY-MM-DD", "YYYY-MM-DD (ISO)"),
26
+ ("MM/DD/YYYY", "MM/DD/YYYY (US)"),
27
+ ("DD/MM/YYYY", "DD/MM/YYYY (EU)"),
28
+ ("DD.MM.YYYY", "DD.MM.YYYY"),
29
+ ],
30
+ default="YYYY-MM-DD",
31
+ help_text="Date display format",
32
+ max_length=20,
33
+ ),
34
+ ),
35
+ migrations.AddField(
36
+ model_name="globalsettings",
37
+ name="instance_name",
38
+ field=models.CharField(
39
+ blank=True,
40
+ default="PyRunner",
41
+ help_text="Instance name displayed in header and emails",
42
+ max_length=100,
43
+ ),
44
+ ),
45
+ migrations.AddField(
46
+ model_name="globalsettings",
47
+ name="last_cleanup_at",
48
+ field=models.DateTimeField(
49
+ blank=True, help_text="When the last cleanup was performed", null=True
50
+ ),
51
+ ),
52
+ migrations.AddField(
53
+ model_name="globalsettings",
54
+ name="retention_count",
55
+ field=models.PositiveIntegerField(
56
+ default=0, help_text="Keep last X runs per script (0 = unlimited)"
57
+ ),
58
+ ),
59
+ migrations.AddField(
60
+ model_name="globalsettings",
61
+ name="retention_days",
62
+ field=models.PositiveIntegerField(
63
+ default=0, help_text="Delete runs older than X days (0 = keep forever)"
64
+ ),
65
+ ),
66
+ migrations.AddField(
67
+ model_name="globalsettings",
68
+ name="time_format",
69
+ field=models.CharField(
70
+ choices=[("24h", "24-hour"), ("12h", "12-hour")],
71
+ default="24h",
72
+ help_text="Time display format",
73
+ max_length=10,
74
+ ),
75
+ ),
76
+ migrations.AddField(
77
+ model_name="globalsettings",
78
+ name="timezone",
79
+ field=models.CharField(
80
+ default="UTC",
81
+ help_text="Default timezone for the instance",
82
+ max_length=50,
83
+ ),
84
+ ),
85
+ migrations.AddField(
86
+ model_name="script",
87
+ name="retention_count_override",
88
+ field=models.PositiveIntegerField(
89
+ blank=True,
90
+ help_text="Override global retention count for this script",
91
+ null=True,
92
+ ),
93
+ ),
94
+ migrations.AddField(
95
+ model_name="script",
96
+ name="retention_days_override",
97
+ field=models.PositiveIntegerField(
98
+ blank=True,
99
+ help_text="Override global retention days for this script",
100
+ null=True,
101
+ ),
102
+ ),
103
+ ]
core/migrations/0008_worker_heartbeat.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 5.2.9 on 2026-01-21 08:02
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("core", "0007_phase7_settings"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="globalsettings",
15
+ name="worker_heartbeat_at",
16
+ field=models.DateTimeField(
17
+ blank=True, help_text="Last heartbeat from django-q workers", null=True
18
+ ),
19
+ ),
20
+ ]
core/migrations/0009_script_archive.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 5.2.9 on 2026-01-24 08:53
2
+
3
+ import django.db.models.deletion
4
+ from django.conf import settings
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ ("core", "0008_worker_heartbeat"),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.AddField(
16
+ model_name="script",
17
+ name="archived_at",
18
+ field=models.DateTimeField(
19
+ blank=True,
20
+ help_text="When this script was archived (null = not archived)",
21
+ null=True,
22
+ ),
23
+ ),
24
+ migrations.AddField(
25
+ model_name="script",
26
+ name="archived_by",
27
+ field=models.ForeignKey(
28
+ blank=True,
29
+ null=True,
30
+ on_delete=django.db.models.deletion.SET_NULL,
31
+ related_name="archived_scripts",
32
+ to=settings.AUTH_USER_MODEL,
33
+ ),
34
+ ),
35
+ ]
core/migrations/0010_globalsettings_setup_completed.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 5.2.9 on 2026-01-24 10:46
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("core", "0009_script_archive"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="globalsettings",
15
+ name="setup_completed",
16
+ field=models.BooleanField(
17
+ default=False, help_text="Whether initial setup has been completed"
18
+ ),
19
+ ),
20
+ migrations.AddField(
21
+ model_name="globalsettings",
22
+ name="setup_completed_at",
23
+ field=models.DateTimeField(
24
+ blank=True, help_text="When the initial setup was completed", null=True
25
+ ),
26
+ ),
27
+ ]
core/migrations/0011_registration_and_invites.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated migration for registration control and user invites
2
+
3
+ from django.conf import settings
4
+ from django.db import migrations, models
5
+ import django.db.models.deletion
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ ("core", "0010_globalsettings_setup_completed"),
12
+ ]
13
+
14
+ operations = [
15
+ # Add allow_registration to GlobalSettings
16
+ migrations.AddField(
17
+ model_name="globalsettings",
18
+ name="allow_registration",
19
+ field=models.BooleanField(
20
+ default=True,
21
+ help_text="Allow new users to register without an invite (auto-disabled after first user)",
22
+ ),
23
+ ),
24
+ # Create UserInvite model
25
+ migrations.CreateModel(
26
+ name="UserInvite",
27
+ fields=[
28
+ (
29
+ "id",
30
+ models.BigAutoField(
31
+ auto_created=True,
32
+ primary_key=True,
33
+ serialize=False,
34
+ verbose_name="ID",
35
+ ),
36
+ ),
37
+ (
38
+ "email",
39
+ models.EmailField(
40
+ help_text="Email address of the invited user",
41
+ max_length=254,
42
+ unique=True,
43
+ ),
44
+ ),
45
+ (
46
+ "token",
47
+ models.CharField(db_index=True, max_length=64, unique=True),
48
+ ),
49
+ ("created_at", models.DateTimeField(auto_now_add=True)),
50
+ ("expires_at", models.DateTimeField()),
51
+ ("used_at", models.DateTimeField(blank=True, null=True)),
52
+ (
53
+ "created_by",
54
+ models.ForeignKey(
55
+ on_delete=django.db.models.deletion.CASCADE,
56
+ related_name="sent_invites",
57
+ to=settings.AUTH_USER_MODEL,
58
+ ),
59
+ ),
60
+ (
61
+ "used_by",
62
+ models.ForeignKey(
63
+ blank=True,
64
+ null=True,
65
+ on_delete=django.db.models.deletion.SET_NULL,
66
+ related_name="received_invite",
67
+ to=settings.AUTH_USER_MODEL,
68
+ ),
69
+ ),
70
+ ],
71
+ options={
72
+ "verbose_name": "user invite",
73
+ "verbose_name_plural": "user invites",
74
+ "db_table": "user_invites",
75
+ },
76
+ ),
77
+ migrations.AddIndex(
78
+ model_name="userinvite",
79
+ index=models.Index(fields=["token"], name="user_invite_token_idx"),
80
+ ),
81
+ migrations.AddIndex(
82
+ model_name="userinvite",
83
+ index=models.Index(fields=["email"], name="user_invite_email_idx"),
84
+ ),
85
+ ]
core/migrations/0012_password_reset_token.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 5.2.9 on 2026-01-24 12:17
2
+
3
+ import django.db.models.deletion
4
+ from django.conf import settings
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ ("core", "0011_registration_and_invites"),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.CreateModel(
16
+ name="PasswordResetToken",
17
+ fields=[
18
+ (
19
+ "id",
20
+ models.BigAutoField(
21
+ auto_created=True,
22
+ primary_key=True,
23
+ serialize=False,
24
+ verbose_name="ID",
25
+ ),
26
+ ),
27
+ ("token", models.CharField(db_index=True, max_length=64, unique=True)),
28
+ ("created_at", models.DateTimeField(auto_now_add=True)),
29
+ ("expires_at", models.DateTimeField()),
30
+ ("used_at", models.DateTimeField(blank=True, null=True)),
31
+ ],
32
+ options={
33
+ "verbose_name": "password reset token",
34
+ "verbose_name_plural": "password reset tokens",
35
+ "db_table": "password_reset_tokens",
36
+ },
37
+ ),
38
+ migrations.RenameIndex(
39
+ model_name="userinvite",
40
+ new_name="user_invite_token_e617ee_idx",
41
+ old_name="user_invite_token_idx",
42
+ ),
43
+ migrations.RenameIndex(
44
+ model_name="userinvite",
45
+ new_name="user_invite_email_cf6347_idx",
46
+ old_name="user_invite_email_idx",
47
+ ),
48
+ migrations.AddField(
49
+ model_name="passwordresettoken",
50
+ name="user",
51
+ field=models.ForeignKey(
52
+ on_delete=django.db.models.deletion.CASCADE,
53
+ related_name="password_reset_tokens",
54
+ to=settings.AUTH_USER_MODEL,
55
+ ),
56
+ ),
57
+ migrations.AddIndex(
58
+ model_name="passwordresettoken",
59
+ index=models.Index(
60
+ fields=["token", "expires_at"], name="password_re_token_9adc88_idx"
61
+ ),
62
+ ),
63
+ ]
core/migrations/0013_add_tags.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 5.2.9 on 2026-01-24 13:07
2
+
3
+ import django.db.models.deletion
4
+ import uuid
5
+ from django.conf import settings
6
+ from django.db import migrations, models
7
+
8
+
9
+ class Migration(migrations.Migration):
10
+
11
+ dependencies = [
12
+ ("core", "0012_password_reset_token"),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.CreateModel(
17
+ name="Tag",
18
+ fields=[
19
+ (
20
+ "id",
21
+ models.UUIDField(
22
+ default=uuid.uuid4,
23
+ editable=False,
24
+ primary_key=True,
25
+ serialize=False,
26
+ ),
27
+ ),
28
+ (
29
+ "name",
30
+ models.CharField(
31
+ help_text="Tag name (must be unique)",
32
+ max_length=50,
33
+ unique=True,
34
+ ),
35
+ ),
36
+ (
37
+ "color",
38
+ models.CharField(
39
+ choices=[
40
+ ("gray", "Gray"),
41
+ ("red", "Red"),
42
+ ("orange", "Orange"),
43
+ ("yellow", "Yellow"),
44
+ ("green", "Green"),
45
+ ("blue", "Blue"),
46
+ ("purple", "Purple"),
47
+ ("pink", "Pink"),
48
+ ],
49
+ default="gray",
50
+ help_text="Tag color for visual distinction",
51
+ max_length=20,
52
+ ),
53
+ ),
54
+ ("created_at", models.DateTimeField(auto_now_add=True)),
55
+ (
56
+ "created_by",
57
+ models.ForeignKey(
58
+ blank=True,
59
+ null=True,
60
+ on_delete=django.db.models.deletion.SET_NULL,
61
+ related_name="created_tags",
62
+ to=settings.AUTH_USER_MODEL,
63
+ ),
64
+ ),
65
+ ],
66
+ options={
67
+ "verbose_name": "tag",
68
+ "verbose_name_plural": "tags",
69
+ "db_table": "tags",
70
+ "ordering": ["name"],
71
+ },
72
+ ),
73
+ migrations.AddField(
74
+ model_name="script",
75
+ name="tags",
76
+ field=models.ManyToManyField(
77
+ blank=True,
78
+ help_text="Tags for organizing and filtering scripts",
79
+ related_name="scripts",
80
+ to="core.tag",
81
+ ),
82
+ ),
83
+ ]
core/migrations/0014_worker_settings.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 5.2.9 on 2026-01-24 14:37
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("core", "0013_add_tags"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="globalsettings",
15
+ name="q_queue_limit",
16
+ field=models.PositiveIntegerField(
17
+ default=20, help_text="Maximum number of tasks in the queue"
18
+ ),
19
+ ),
20
+ migrations.AddField(
21
+ model_name="globalsettings",
22
+ name="q_retry",
23
+ field=models.PositiveIntegerField(
24
+ default=660, help_text="Seconds before a task is retried after timeout"
25
+ ),
26
+ ),
27
+ migrations.AddField(
28
+ model_name="globalsettings",
29
+ name="q_timeout",
30
+ field=models.PositiveIntegerField(
31
+ default=600, help_text="Task timeout in seconds (0 for no timeout)"
32
+ ),
33
+ ),
34
+ migrations.AddField(
35
+ model_name="globalsettings",
36
+ name="q_workers",
37
+ field=models.PositiveIntegerField(
38
+ default=2, help_text="Number of worker processes for task queue"
39
+ ),
40
+ ),
41
+ migrations.AddField(
42
+ model_name="globalsettings",
43
+ name="worker_settings_updated_at",
44
+ field=models.DateTimeField(
45
+ blank=True,
46
+ help_text="When worker settings were last updated (requires restart)",
47
+ null=True,
48
+ ),
49
+ ),
50
+ ]
core/migrations/0015_datastore.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 5.2.9 on 2026-01-24 17:54
2
+
3
+ import django.db.models.deletion
4
+ import uuid
5
+ from django.conf import settings
6
+ from django.db import migrations, models
7
+
8
+
9
+ class Migration(migrations.Migration):
10
+
11
+ dependencies = [
12
+ ("core", "0014_worker_settings"),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.CreateModel(
17
+ name="DataStore",
18
+ fields=[
19
+ (
20
+ "id",
21
+ models.UUIDField(
22
+ default=uuid.uuid4,
23
+ editable=False,
24
+ primary_key=True,
25
+ serialize=False,
26
+ ),
27
+ ),
28
+ (
29
+ "name",
30
+ models.CharField(
31
+ help_text="Unique name for this data store (used in scripts)",
32
+ max_length=100,
33
+ unique=True,
34
+ ),
35
+ ),
36
+ (
37
+ "description",
38
+ models.TextField(
39
+ blank=True,
40
+ help_text="Optional description of what this data store is used for",
41
+ ),
42
+ ),
43
+ ("created_at", models.DateTimeField(auto_now_add=True)),
44
+ ("updated_at", models.DateTimeField(auto_now=True)),
45
+ (
46
+ "created_by",
47
+ models.ForeignKey(
48
+ blank=True,
49
+ null=True,
50
+ on_delete=django.db.models.deletion.SET_NULL,
51
+ related_name="created_datastores",
52
+ to=settings.AUTH_USER_MODEL,
53
+ ),
54
+ ),
55
+ ],
56
+ options={
57
+ "verbose_name": "data store",
58
+ "verbose_name_plural": "data stores",
59
+ "db_table": "datastores",
60
+ "ordering": ["name"],
61
+ },
62
+ ),
63
+ migrations.CreateModel(
64
+ name="DataStoreEntry",
65
+ fields=[
66
+ (
67
+ "id",
68
+ models.UUIDField(
69
+ default=uuid.uuid4,
70
+ editable=False,
71
+ primary_key=True,
72
+ serialize=False,
73
+ ),
74
+ ),
75
+ (
76
+ "key",
77
+ models.CharField(
78
+ help_text="Unique key within this data store", max_length=255
79
+ ),
80
+ ),
81
+ ("value_json", models.TextField(help_text="JSON-encoded value")),
82
+ ("created_at", models.DateTimeField(auto_now_add=True)),
83
+ ("updated_at", models.DateTimeField(auto_now=True)),
84
+ (
85
+ "datastore",
86
+ models.ForeignKey(
87
+ on_delete=django.db.models.deletion.CASCADE,
88
+ related_name="entries",
89
+ to="core.datastore",
90
+ ),
91
+ ),
92
+ ],
93
+ options={
94
+ "verbose_name": "data store entry",
95
+ "verbose_name_plural": "data store entries",
96
+ "db_table": "datastore_entries",
97
+ "ordering": ["key"],
98
+ "indexes": [
99
+ models.Index(
100
+ fields=["datastore", "key"],
101
+ name="datastore_e_datasto_6e5232_idx",
102
+ )
103
+ ],
104
+ "unique_together": {("datastore", "key")},
105
+ },
106
+ ),
107
+ ]
core/migrations/0016_globalsettings_admin_url_slug.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.db import migrations, models
2
+
3
+
4
+ class Migration(migrations.Migration):
5
+ dependencies = [
6
+ ('core', '0015_datastore'),
7
+ ]
8
+
9
+ operations = [
10
+ migrations.AddField(
11
+ model_name='globalsettings',
12
+ name='admin_url_slug',
13
+ field=models.CharField(
14
+ default='django-admin',
15
+ help_text='URL path for Django admin interface (requires restart)',
16
+ max_length=100,
17
+ ),
18
+ ),
19
+ ]
core/migrations/0017_datastore_api_token.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated manually for DataStore API Token
2
+
3
+ import django.db.models.deletion
4
+ import uuid
5
+ from django.conf import settings
6
+ from django.db import migrations, models
7
+
8
+
9
+ class Migration(migrations.Migration):
10
+
11
+ dependencies = [
12
+ ("core", "0016_globalsettings_admin_url_slug"),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.CreateModel(
17
+ name="DataStoreAPIToken",
18
+ fields=[
19
+ (
20
+ "id",
21
+ models.UUIDField(
22
+ default=uuid.uuid4,
23
+ editable=False,
24
+ primary_key=True,
25
+ serialize=False,
26
+ ),
27
+ ),
28
+ (
29
+ "token",
30
+ models.CharField(
31
+ db_index=True,
32
+ help_text="API token value (auto-generated)",
33
+ max_length=64,
34
+ unique=True,
35
+ ),
36
+ ),
37
+ (
38
+ "name",
39
+ models.CharField(
40
+ help_text="Friendly name for this token",
41
+ max_length=100,
42
+ ),
43
+ ),
44
+ ("created_at", models.DateTimeField(auto_now_add=True)),
45
+ (
46
+ "last_used_at",
47
+ models.DateTimeField(
48
+ blank=True,
49
+ help_text="Last time this token was used",
50
+ null=True,
51
+ ),
52
+ ),
53
+ (
54
+ "expires_at",
55
+ models.DateTimeField(
56
+ blank=True,
57
+ help_text="Optional expiration date. Leave empty for no expiration.",
58
+ null=True,
59
+ ),
60
+ ),
61
+ (
62
+ "is_active",
63
+ models.BooleanField(
64
+ default=True,
65
+ help_text="Inactive tokens cannot be used for API access",
66
+ ),
67
+ ),
68
+ (
69
+ "datastore",
70
+ models.ForeignKey(
71
+ blank=True,
72
+ help_text="If set, token only grants access to this datastore. Leave empty for global access.",
73
+ null=True,
74
+ on_delete=django.db.models.deletion.CASCADE,
75
+ related_name="api_tokens",
76
+ to="core.datastore",
77
+ ),
78
+ ),
79
+ (
80
+ "created_by",
81
+ models.ForeignKey(
82
+ blank=True,
83
+ null=True,
84
+ on_delete=django.db.models.deletion.SET_NULL,
85
+ related_name="created_api_tokens",
86
+ to=settings.AUTH_USER_MODEL,
87
+ ),
88
+ ),
89
+ ],
90
+ options={
91
+ "verbose_name": "API token",
92
+ "verbose_name_plural": "API tokens",
93
+ "db_table": "datastore_api_tokens",
94
+ "ordering": ["-created_at"],
95
+ },
96
+ ),
97
+ ]
core/migrations/0018_add_weekly_monthly_schedule.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated manually for weekly/monthly scheduling
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("core", "0017_datastore_api_token"),
10
+ ]
11
+
12
+ operations = [
13
+ # Add weekly_days field
14
+ migrations.AddField(
15
+ model_name="scriptschedule",
16
+ name="weekly_days",
17
+ field=models.JSONField(
18
+ blank=True,
19
+ default=list,
20
+ help_text="Days of week [0-6] where 0=Monday, e.g., [0, 2, 4] for Mon/Wed/Fri",
21
+ ),
22
+ ),
23
+ # Add weekly_times field
24
+ migrations.AddField(
25
+ model_name="scriptschedule",
26
+ name="weekly_times",
27
+ field=models.JSONField(
28
+ blank=True,
29
+ default=list,
30
+ help_text="List of times in HH:MM format for weekly mode",
31
+ ),
32
+ ),
33
+ # Add monthly_days field
34
+ migrations.AddField(
35
+ model_name="scriptschedule",
36
+ name="monthly_days",
37
+ field=models.JSONField(
38
+ blank=True,
39
+ default=list,
40
+ help_text="Days of month [1-31], e.g., [1, 15] for 1st and 15th",
41
+ ),
42
+ ),
43
+ # Add monthly_times field
44
+ migrations.AddField(
45
+ model_name="scriptschedule",
46
+ name="monthly_times",
47
+ field=models.JSONField(
48
+ blank=True,
49
+ default=list,
50
+ help_text="List of times in HH:MM format for monthly mode",
51
+ ),
52
+ ),
53
+ # Update run_mode choices to include weekly and monthly
54
+ migrations.AlterField(
55
+ model_name="scriptschedule",
56
+ name="run_mode",
57
+ field=models.CharField(
58
+ choices=[
59
+ ("manual", "Manual"),
60
+ ("interval", "Interval"),
61
+ ("daily", "Daily"),
62
+ ("weekly", "Weekly"),
63
+ ("monthly", "Monthly"),
64
+ ],
65
+ default="manual",
66
+ max_length=20,
67
+ ),
68
+ ),
69
+ ]
core/migrations/0019_s3_storage_settings.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated migration for S3 storage settings
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("core", "0018_add_weekly_monthly_schedule"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="globalsettings",
15
+ name="s3_enabled",
16
+ field=models.BooleanField(
17
+ default=False,
18
+ help_text="Enable S3-compatible storage for backups",
19
+ ),
20
+ ),
21
+ migrations.AddField(
22
+ model_name="globalsettings",
23
+ name="s3_endpoint_url",
24
+ field=models.CharField(
25
+ blank=True,
26
+ max_length=500,
27
+ help_text="S3 endpoint URL (leave empty for AWS S3)",
28
+ ),
29
+ ),
30
+ migrations.AddField(
31
+ model_name="globalsettings",
32
+ name="s3_region",
33
+ field=models.CharField(
34
+ blank=True,
35
+ default="us-east-1",
36
+ max_length=50,
37
+ help_text="S3 region",
38
+ ),
39
+ ),
40
+ migrations.AddField(
41
+ model_name="globalsettings",
42
+ name="s3_bucket_name",
43
+ field=models.CharField(
44
+ blank=True,
45
+ max_length=255,
46
+ help_text="S3 bucket name",
47
+ ),
48
+ ),
49
+ migrations.AddField(
50
+ model_name="globalsettings",
51
+ name="s3_access_key_encrypted",
52
+ field=models.TextField(
53
+ blank=True,
54
+ help_text="S3 access key (encrypted)",
55
+ ),
56
+ ),
57
+ migrations.AddField(
58
+ model_name="globalsettings",
59
+ name="s3_secret_key_encrypted",
60
+ field=models.TextField(
61
+ blank=True,
62
+ help_text="S3 secret key (encrypted)",
63
+ ),
64
+ ),
65
+ migrations.AddField(
66
+ model_name="globalsettings",
67
+ name="s3_use_ssl",
68
+ field=models.BooleanField(
69
+ default=True,
70
+ help_text="Use SSL/TLS for S3 connections",
71
+ ),
72
+ ),
73
+ migrations.AddField(
74
+ model_name="globalsettings",
75
+ name="s3_path_style",
76
+ field=models.BooleanField(
77
+ default=False,
78
+ help_text="Use path-style addressing (required for MinIO)",
79
+ ),
80
+ ),
81
+ migrations.AddField(
82
+ model_name="globalsettings",
83
+ name="s3_last_tested_at",
84
+ field=models.DateTimeField(
85
+ blank=True,
86
+ null=True,
87
+ help_text="When S3 connection was last successfully tested",
88
+ ),
89
+ ),
90
+ ]
core/migrations/0020_s3_backup_schedule.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated migration for S3 scheduled backup settings
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("core", "0019_s3_storage_settings"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="globalsettings",
15
+ name="s3_backup_enabled",
16
+ field=models.BooleanField(
17
+ default=False,
18
+ help_text="Enable scheduled backups to S3",
19
+ ),
20
+ ),
21
+ migrations.AddField(
22
+ model_name="globalsettings",
23
+ name="s3_backup_schedule",
24
+ field=models.CharField(
25
+ choices=[
26
+ ("disabled", "Disabled"),
27
+ ("daily", "Daily"),
28
+ ("weekly", "Weekly"),
29
+ ],
30
+ default="disabled",
31
+ max_length=20,
32
+ help_text="Backup schedule frequency",
33
+ ),
34
+ ),
35
+ migrations.AddField(
36
+ model_name="globalsettings",
37
+ name="s3_backup_time",
38
+ field=models.TimeField(
39
+ default="02:00",
40
+ help_text="Time of day to run backup (in instance timezone)",
41
+ ),
42
+ ),
43
+ migrations.AddField(
44
+ model_name="globalsettings",
45
+ name="s3_backup_day",
46
+ field=models.PositiveSmallIntegerField(
47
+ default=0,
48
+ help_text="Day of week for weekly backups (0=Monday, 6=Sunday)",
49
+ ),
50
+ ),
51
+ migrations.AddField(
52
+ model_name="globalsettings",
53
+ name="s3_backup_prefix",
54
+ field=models.CharField(
55
+ blank=True,
56
+ default="pyrunner-backups/",
57
+ max_length=255,
58
+ help_text="S3 key prefix for backup files",
59
+ ),
60
+ ),
61
+ migrations.AddField(
62
+ model_name="globalsettings",
63
+ name="s3_backup_retention_count",
64
+ field=models.PositiveIntegerField(
65
+ default=7,
66
+ help_text="Number of backups to keep in S3 (0 = keep all)",
67
+ ),
68
+ ),
69
+ migrations.AddField(
70
+ model_name="globalsettings",
71
+ name="s3_backup_include_runs",
72
+ field=models.BooleanField(
73
+ default=False,
74
+ help_text="Include run history in scheduled backups",
75
+ ),
76
+ ),
77
+ migrations.AddField(
78
+ model_name="globalsettings",
79
+ name="s3_backup_max_runs",
80
+ field=models.PositiveIntegerField(
81
+ default=1000,
82
+ help_text="Maximum runs to include in backup",
83
+ ),
84
+ ),
85
+ migrations.AddField(
86
+ model_name="globalsettings",
87
+ name="s3_backup_include_datastores",
88
+ field=models.BooleanField(
89
+ default=True,
90
+ help_text="Include datastores in scheduled backups",
91
+ ),
92
+ ),
93
+ migrations.AddField(
94
+ model_name="globalsettings",
95
+ name="s3_backup_last_run_at",
96
+ field=models.DateTimeField(
97
+ blank=True,
98
+ null=True,
99
+ help_text="When the last scheduled backup ran",
100
+ ),
101
+ ),
102
+ migrations.AddField(
103
+ model_name="globalsettings",
104
+ name="s3_backup_last_status",
105
+ field=models.CharField(
106
+ blank=True,
107
+ default="",
108
+ max_length=20,
109
+ help_text="Status of last backup (success/failed)",
110
+ ),
111
+ ),
112
+ migrations.AddField(
113
+ model_name="globalsettings",
114
+ name="s3_backup_last_error",
115
+ field=models.TextField(
116
+ blank=True,
117
+ default="",
118
+ help_text="Error message from last failed backup",
119
+ ),
120
+ ),
121
+ migrations.AddField(
122
+ model_name="globalsettings",
123
+ name="s3_backup_last_size",
124
+ field=models.PositiveIntegerField(
125
+ default=0,
126
+ help_text="Size of last backup in bytes",
127
+ ),
128
+ ),
129
+ ]
core/migrations/0021_field_updates.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 6.0.1 on 2026-04-15 12:47
2
+
3
+ import core.models.environment
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ("core", "0020_s3_backup_schedule"),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AlterField(
15
+ model_name="environment",
16
+ name="path",
17
+ field=models.CharField(
18
+ help_text="Directory name within the environments folder",
19
+ max_length=255,
20
+ unique=True,
21
+ validators=[core.models.environment.validate_environment_path],
22
+ ),
23
+ ),
24
+ migrations.AlterField(
25
+ model_name="script",
26
+ name="timeout_seconds",
27
+ field=models.PositiveIntegerField(
28
+ default=3600,
29
+ help_text="Maximum execution time in seconds (default: 1 hour, max: 24 hours)",
30
+ ),
31
+ ),
32
+ migrations.AlterField(
33
+ model_name="scriptschedule",
34
+ name="timezone",
35
+ field=models.CharField(
36
+ default="UTC",
37
+ help_text="Timezone for scheduled runs (e.g., 'America/New_York')",
38
+ max_length=50,
39
+ ),
40
+ ),
41
+ ]
core/migrations/__init__.py ADDED
File without changes