arthexis 0.1.10__py3-none-any.whl → 0.1.11__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.
Potentially problematic release.
This version of arthexis might be problematic. Click here for more details.
- {arthexis-0.1.10.dist-info → arthexis-0.1.11.dist-info}/METADATA +36 -26
- {arthexis-0.1.10.dist-info → arthexis-0.1.11.dist-info}/RECORD +42 -38
- config/context_processors.py +1 -0
- config/settings.py +24 -3
- config/urls.py +5 -4
- core/admin.py +184 -22
- core/apps.py +27 -2
- core/backends.py +38 -0
- core/environment.py +23 -5
- core/mailer.py +3 -1
- core/models.py +270 -31
- core/reference_utils.py +19 -8
- core/sigil_builder.py +7 -2
- core/sigil_resolver.py +35 -4
- core/system.py +247 -1
- core/temp_passwords.py +181 -0
- core/test_system_info.py +62 -2
- core/tests.py +105 -3
- core/user_data.py +51 -8
- core/views.py +245 -8
- nodes/admin.py +137 -2
- nodes/backends.py +21 -6
- nodes/dns.py +203 -0
- nodes/models.py +293 -7
- nodes/tests.py +312 -2
- nodes/views.py +14 -0
- ocpp/consumers.py +11 -8
- ocpp/models.py +3 -0
- ocpp/reference_utils.py +42 -0
- ocpp/test_rfid.py +169 -7
- ocpp/tests.py +30 -0
- ocpp/views.py +8 -0
- pages/admin.py +9 -1
- pages/context_processors.py +6 -6
- pages/defaults.py +14 -0
- pages/models.py +53 -14
- pages/tests.py +19 -4
- pages/urls.py +3 -0
- pages/views.py +86 -19
- {arthexis-0.1.10.dist-info → arthexis-0.1.11.dist-info}/WHEEL +0 -0
- {arthexis-0.1.10.dist-info → arthexis-0.1.11.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.10.dist-info → arthexis-0.1.11.dist-info}/top_level.txt +0 -0
core/admin.py
CHANGED
|
@@ -21,6 +21,8 @@ from import_export.admin import ImportExportModelAdmin
|
|
|
21
21
|
from import_export.widgets import ForeignKeyWidget
|
|
22
22
|
from django.contrib.auth.models import Group
|
|
23
23
|
from django.templatetags.static import static
|
|
24
|
+
from django.utils import timezone
|
|
25
|
+
from django.utils.dateparse import parse_datetime
|
|
24
26
|
from django.utils.html import format_html
|
|
25
27
|
from django.utils.translation import gettext_lazy as _
|
|
26
28
|
from django.forms.models import BaseInlineFormSet
|
|
@@ -51,6 +53,7 @@ from .models import (
|
|
|
51
53
|
Reference,
|
|
52
54
|
OdooProfile,
|
|
53
55
|
EmailInbox,
|
|
56
|
+
SocialProfile,
|
|
54
57
|
EmailCollector,
|
|
55
58
|
Package,
|
|
56
59
|
PackageRelease,
|
|
@@ -260,6 +263,7 @@ class ReferenceAdmin(EntityModelAdmin):
|
|
|
260
263
|
list_display = (
|
|
261
264
|
"alt_text",
|
|
262
265
|
"content_type",
|
|
266
|
+
"link",
|
|
263
267
|
"header",
|
|
264
268
|
"footer",
|
|
265
269
|
"visibility",
|
|
@@ -304,6 +308,15 @@ class ReferenceAdmin(EntityModelAdmin):
|
|
|
304
308
|
def visibility(self, obj):
|
|
305
309
|
return obj.get_footer_visibility_display()
|
|
306
310
|
|
|
311
|
+
@admin.display(description="LINK")
|
|
312
|
+
def link(self, obj):
|
|
313
|
+
if obj.value:
|
|
314
|
+
return format_html(
|
|
315
|
+
'<a href="{}" target="_blank" rel="noopener noreferrer">open</a>',
|
|
316
|
+
obj.value,
|
|
317
|
+
)
|
|
318
|
+
return ""
|
|
319
|
+
|
|
307
320
|
def get_urls(self):
|
|
308
321
|
urls = super().get_urls()
|
|
309
322
|
custom = [
|
|
@@ -532,7 +545,14 @@ class SecurityGroupAdmin(DjangoGroupAdmin):
|
|
|
532
545
|
|
|
533
546
|
|
|
534
547
|
class InviteLeadAdmin(EntityModelAdmin):
|
|
535
|
-
list_display = (
|
|
548
|
+
list_display = (
|
|
549
|
+
"email",
|
|
550
|
+
"mac_address",
|
|
551
|
+
"created_on",
|
|
552
|
+
"sent_on",
|
|
553
|
+
"sent_via_outbox",
|
|
554
|
+
"short_error",
|
|
555
|
+
)
|
|
536
556
|
search_fields = ("email", "comment")
|
|
537
557
|
readonly_fields = (
|
|
538
558
|
"created_on",
|
|
@@ -543,6 +563,7 @@ class InviteLeadAdmin(EntityModelAdmin):
|
|
|
543
563
|
"ip_address",
|
|
544
564
|
"mac_address",
|
|
545
565
|
"sent_on",
|
|
566
|
+
"sent_via_outbox",
|
|
546
567
|
"error",
|
|
547
568
|
)
|
|
548
569
|
|
|
@@ -851,6 +872,14 @@ class EmailInboxInlineForm(ProfileFormMixin, EmailInboxAdminForm):
|
|
|
851
872
|
exclude = ("user", "group")
|
|
852
873
|
|
|
853
874
|
|
|
875
|
+
class SocialProfileInlineForm(ProfileFormMixin, forms.ModelForm):
|
|
876
|
+
profile_fields = SocialProfile.profile_fields
|
|
877
|
+
|
|
878
|
+
class Meta:
|
|
879
|
+
model = SocialProfile
|
|
880
|
+
fields = ("network", "handle", "domain", "did")
|
|
881
|
+
|
|
882
|
+
|
|
854
883
|
class EmailOutboxInlineForm(ProfileFormMixin, forms.ModelForm):
|
|
855
884
|
profile_fields = EmailOutbox.profile_fields
|
|
856
885
|
password = forms.CharField(
|
|
@@ -869,6 +898,7 @@ class EmailOutboxInlineForm(ProfileFormMixin, forms.ModelForm):
|
|
|
869
898
|
"use_tls",
|
|
870
899
|
"use_ssl",
|
|
871
900
|
"from_email",
|
|
901
|
+
"is_enabled",
|
|
872
902
|
)
|
|
873
903
|
|
|
874
904
|
def __init__(self, *args, **kwargs):
|
|
@@ -998,6 +1028,22 @@ PROFILE_INLINE_CONFIG = {
|
|
|
998
1028
|
"from_email",
|
|
999
1029
|
),
|
|
1000
1030
|
},
|
|
1031
|
+
SocialProfile: {
|
|
1032
|
+
"form": SocialProfileInlineForm,
|
|
1033
|
+
"fieldsets": (
|
|
1034
|
+
(
|
|
1035
|
+
_("Configuration: Bluesky"),
|
|
1036
|
+
{
|
|
1037
|
+
"fields": ("network", "handle", "domain", "did"),
|
|
1038
|
+
"description": _(
|
|
1039
|
+
"1. Set your Bluesky handle to the domain managed by Arthexis. "
|
|
1040
|
+
"2. Publish a _atproto TXT record or /.well-known/atproto-did file pointing to the DID below. "
|
|
1041
|
+
"3. Save once Bluesky confirms the domain matches the DID."
|
|
1042
|
+
),
|
|
1043
|
+
},
|
|
1044
|
+
),
|
|
1045
|
+
),
|
|
1046
|
+
},
|
|
1001
1047
|
ReleaseManager: {
|
|
1002
1048
|
"form": ReleaseManagerInlineForm,
|
|
1003
1049
|
"fields": (
|
|
@@ -1056,6 +1102,7 @@ PROFILE_MODELS = (
|
|
|
1056
1102
|
OdooProfile,
|
|
1057
1103
|
EmailInbox,
|
|
1058
1104
|
EmailOutbox,
|
|
1105
|
+
SocialProfile,
|
|
1059
1106
|
ReleaseManager,
|
|
1060
1107
|
AssistantProfile,
|
|
1061
1108
|
)
|
|
@@ -1202,8 +1249,68 @@ class EmailCollectorInline(admin.TabularInline):
|
|
|
1202
1249
|
|
|
1203
1250
|
|
|
1204
1251
|
class EmailCollectorAdmin(EntityModelAdmin):
|
|
1205
|
-
list_display = ("inbox", "subject", "sender", "body", "fragment")
|
|
1206
|
-
search_fields = ("subject", "sender", "body", "fragment")
|
|
1252
|
+
list_display = ("name", "inbox", "subject", "sender", "body", "fragment")
|
|
1253
|
+
search_fields = ("name", "subject", "sender", "body", "fragment")
|
|
1254
|
+
actions = ["preview_messages"]
|
|
1255
|
+
|
|
1256
|
+
@admin.action(description=_("Preview matches"))
|
|
1257
|
+
def preview_messages(self, request, queryset):
|
|
1258
|
+
results = []
|
|
1259
|
+
for collector in queryset.select_related("inbox"):
|
|
1260
|
+
try:
|
|
1261
|
+
messages = collector.search_messages(limit=5)
|
|
1262
|
+
error = None
|
|
1263
|
+
except ValidationError as exc:
|
|
1264
|
+
messages = []
|
|
1265
|
+
error = str(exc)
|
|
1266
|
+
except Exception as exc: # pragma: no cover - admin feedback
|
|
1267
|
+
messages = []
|
|
1268
|
+
error = str(exc)
|
|
1269
|
+
results.append(
|
|
1270
|
+
{
|
|
1271
|
+
"collector": collector,
|
|
1272
|
+
"messages": messages,
|
|
1273
|
+
"error": error,
|
|
1274
|
+
}
|
|
1275
|
+
)
|
|
1276
|
+
context = {
|
|
1277
|
+
"title": _("Preview Email Collectors"),
|
|
1278
|
+
"results": results,
|
|
1279
|
+
"opts": self.model._meta,
|
|
1280
|
+
"queryset": queryset,
|
|
1281
|
+
}
|
|
1282
|
+
return TemplateResponse(
|
|
1283
|
+
request, "admin/core/emailcollector/preview.html", context
|
|
1284
|
+
)
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
@admin.register(SocialProfile)
|
|
1288
|
+
class SocialProfileAdmin(
|
|
1289
|
+
ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmin
|
|
1290
|
+
):
|
|
1291
|
+
list_display = ("owner", "network", "handle", "domain")
|
|
1292
|
+
list_filter = ("network",)
|
|
1293
|
+
search_fields = ("handle", "domain", "did")
|
|
1294
|
+
changelist_actions = ["my_profile"]
|
|
1295
|
+
change_actions = ["my_profile_action"]
|
|
1296
|
+
fieldsets = (
|
|
1297
|
+
(_("Owner"), {"fields": ("user", "group")}),
|
|
1298
|
+
(
|
|
1299
|
+
_("Configuration: Bluesky"),
|
|
1300
|
+
{
|
|
1301
|
+
"fields": ("network", "handle", "domain", "did"),
|
|
1302
|
+
"description": _(
|
|
1303
|
+
"Link Arthexis to Bluesky by using a verified domain handle. "
|
|
1304
|
+
"Publish a _atproto TXT record or /.well-known/atproto-did file "
|
|
1305
|
+
"that returns the DID stored here before saving."
|
|
1306
|
+
),
|
|
1307
|
+
},
|
|
1308
|
+
),
|
|
1309
|
+
)
|
|
1310
|
+
|
|
1311
|
+
@admin.display(description=_("Owner"))
|
|
1312
|
+
def owner(self, obj):
|
|
1313
|
+
return obj.owner_display()
|
|
1207
1314
|
|
|
1208
1315
|
|
|
1209
1316
|
@admin.register(OdooProfile)
|
|
@@ -1383,6 +1490,7 @@ class EmailInboxAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmi
|
|
|
1383
1490
|
subject=form.cleaned_data["subject"],
|
|
1384
1491
|
from_address=form.cleaned_data["from_address"],
|
|
1385
1492
|
body=form.cleaned_data["body"],
|
|
1493
|
+
use_regular_expressions=False,
|
|
1386
1494
|
)
|
|
1387
1495
|
context = {
|
|
1388
1496
|
"form": form,
|
|
@@ -2265,6 +2373,7 @@ class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
2265
2373
|
"package_link",
|
|
2266
2374
|
"is_current",
|
|
2267
2375
|
"pypi_url",
|
|
2376
|
+
"release_on",
|
|
2268
2377
|
"revision_short",
|
|
2269
2378
|
"published_status",
|
|
2270
2379
|
)
|
|
@@ -2272,7 +2381,7 @@ class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
2272
2381
|
actions = ["publish_release", "validate_releases"]
|
|
2273
2382
|
change_actions = ["publish_release_action"]
|
|
2274
2383
|
changelist_actions = ["refresh_from_pypi", "prepare_next_release"]
|
|
2275
|
-
readonly_fields = ("pypi_url", "is_current", "revision")
|
|
2384
|
+
readonly_fields = ("pypi_url", "release_on", "is_current", "revision")
|
|
2276
2385
|
fields = (
|
|
2277
2386
|
"package",
|
|
2278
2387
|
"release_manager",
|
|
@@ -2280,6 +2389,7 @@ class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
2280
2389
|
"revision",
|
|
2281
2390
|
"is_current",
|
|
2282
2391
|
"pypi_url",
|
|
2392
|
+
"release_on",
|
|
2283
2393
|
)
|
|
2284
2394
|
|
|
2285
2395
|
@admin.display(description="package", ordering="package")
|
|
@@ -2307,32 +2417,84 @@ class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
2307
2417
|
return
|
|
2308
2418
|
releases = resp.json().get("releases", {})
|
|
2309
2419
|
created = 0
|
|
2310
|
-
|
|
2311
|
-
|
|
2420
|
+
updated = 0
|
|
2421
|
+
restored = 0
|
|
2422
|
+
|
|
2423
|
+
for version, files in releases.items():
|
|
2424
|
+
release_on = self._release_on_from_files(files)
|
|
2425
|
+
release = PackageRelease.all_objects.filter(
|
|
2312
2426
|
package=package, version=version
|
|
2313
|
-
).
|
|
2314
|
-
if
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2427
|
+
).first()
|
|
2428
|
+
if release:
|
|
2429
|
+
update_fields = []
|
|
2430
|
+
if release.is_deleted:
|
|
2431
|
+
release.is_deleted = False
|
|
2432
|
+
update_fields.append("is_deleted")
|
|
2433
|
+
restored += 1
|
|
2434
|
+
if not release.pypi_url:
|
|
2435
|
+
release.pypi_url = (
|
|
2436
|
+
f"https://pypi.org/project/{package.name}/{version}/"
|
|
2437
|
+
)
|
|
2438
|
+
update_fields.append("pypi_url")
|
|
2439
|
+
if release_on and release.release_on != release_on:
|
|
2440
|
+
release.release_on = release_on
|
|
2441
|
+
update_fields.append("release_on")
|
|
2442
|
+
updated += 1
|
|
2443
|
+
if update_fields:
|
|
2444
|
+
release.save(update_fields=update_fields)
|
|
2445
|
+
continue
|
|
2446
|
+
PackageRelease.objects.create(
|
|
2447
|
+
package=package,
|
|
2448
|
+
release_manager=package.release_manager,
|
|
2449
|
+
version=version,
|
|
2450
|
+
revision="",
|
|
2451
|
+
pypi_url=f"https://pypi.org/project/{package.name}/{version}/",
|
|
2452
|
+
release_on=release_on,
|
|
2329
2453
|
)
|
|
2454
|
+
created += 1
|
|
2455
|
+
|
|
2456
|
+
if created or updated or restored:
|
|
2457
|
+
PackageRelease.dump_fixture()
|
|
2458
|
+
message_parts = []
|
|
2459
|
+
if created:
|
|
2460
|
+
message_parts.append(
|
|
2461
|
+
f"Created {created} release{'s' if created != 1 else ''} from PyPI"
|
|
2462
|
+
)
|
|
2463
|
+
if updated:
|
|
2464
|
+
message_parts.append(
|
|
2465
|
+
f"Updated release date for {updated} release"
|
|
2466
|
+
f"{'s' if updated != 1 else ''}"
|
|
2467
|
+
)
|
|
2468
|
+
if restored:
|
|
2469
|
+
message_parts.append(
|
|
2470
|
+
f"Restored {restored} release{'s' if restored != 1 else ''}"
|
|
2471
|
+
)
|
|
2472
|
+
self.message_user(request, "; ".join(message_parts), messages.SUCCESS)
|
|
2330
2473
|
else:
|
|
2331
2474
|
self.message_user(request, "No new releases found", messages.INFO)
|
|
2332
2475
|
|
|
2333
2476
|
refresh_from_pypi.label = "Refresh from PyPI"
|
|
2334
2477
|
refresh_from_pypi.short_description = "Refresh from PyPI"
|
|
2335
2478
|
|
|
2479
|
+
@staticmethod
|
|
2480
|
+
def _release_on_from_files(files):
|
|
2481
|
+
if not files:
|
|
2482
|
+
return None
|
|
2483
|
+
candidates = []
|
|
2484
|
+
for item in files:
|
|
2485
|
+
stamp = item.get("upload_time_iso_8601") or item.get("upload_time")
|
|
2486
|
+
if not stamp:
|
|
2487
|
+
continue
|
|
2488
|
+
when = parse_datetime(stamp)
|
|
2489
|
+
if when is None:
|
|
2490
|
+
continue
|
|
2491
|
+
if timezone.is_naive(when):
|
|
2492
|
+
when = timezone.make_aware(when, datetime.timezone.utc)
|
|
2493
|
+
candidates.append(when.astimezone(datetime.timezone.utc))
|
|
2494
|
+
if not candidates:
|
|
2495
|
+
return None
|
|
2496
|
+
return min(candidates)
|
|
2497
|
+
|
|
2336
2498
|
def prepare_next_release(self, request, queryset):
|
|
2337
2499
|
package = Package.objects.filter(is_active=True).first()
|
|
2338
2500
|
if not package:
|
core/apps.py
CHANGED
|
@@ -104,8 +104,11 @@ class CoreConfig(AppConfig):
|
|
|
104
104
|
|
|
105
105
|
lock = Path(settings.BASE_DIR) / "locks" / "celery.lck"
|
|
106
106
|
|
|
107
|
+
from django.db.backends.signals import connection_created
|
|
108
|
+
|
|
107
109
|
if lock.exists():
|
|
108
110
|
from .auto_upgrade import ensure_auto_upgrade_periodic_task
|
|
111
|
+
from django.db import DEFAULT_DB_ALIAS, connections
|
|
109
112
|
|
|
110
113
|
def ensure_email_collector_task(**kwargs):
|
|
111
114
|
try: # pragma: no cover - optional dependency
|
|
@@ -133,9 +136,31 @@ class CoreConfig(AppConfig):
|
|
|
133
136
|
|
|
134
137
|
post_migrate.connect(ensure_email_collector_task, sender=self)
|
|
135
138
|
post_migrate.connect(ensure_auto_upgrade_periodic_task, sender=self)
|
|
136
|
-
ensure_auto_upgrade_periodic_task()
|
|
137
139
|
|
|
138
|
-
|
|
140
|
+
auto_upgrade_dispatch_uid = "core.apps.ensure_auto_upgrade_periodic_task"
|
|
141
|
+
|
|
142
|
+
def ensure_auto_upgrade_on_connection(**kwargs):
|
|
143
|
+
connection = kwargs.get("connection")
|
|
144
|
+
if connection is not None and connection.alias != "default":
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
ensure_auto_upgrade_periodic_task()
|
|
149
|
+
finally:
|
|
150
|
+
connection_created.disconnect(
|
|
151
|
+
receiver=ensure_auto_upgrade_on_connection,
|
|
152
|
+
dispatch_uid=auto_upgrade_dispatch_uid,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
connection_created.connect(
|
|
156
|
+
ensure_auto_upgrade_on_connection,
|
|
157
|
+
dispatch_uid=auto_upgrade_dispatch_uid,
|
|
158
|
+
weak=False,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
default_connection = connections[DEFAULT_DB_ALIAS]
|
|
162
|
+
if default_connection.connection is not None:
|
|
163
|
+
ensure_auto_upgrade_on_connection(connection=default_connection)
|
|
139
164
|
|
|
140
165
|
def enable_sqlite_wal(**kwargs):
|
|
141
166
|
connection = kwargs.get("connection")
|
core/backends.py
CHANGED
|
@@ -12,6 +12,7 @@ from django.http.request import split_domain_port
|
|
|
12
12
|
from django_otp.plugins.otp_totp.models import TOTPDevice
|
|
13
13
|
|
|
14
14
|
from .models import EnergyAccount
|
|
15
|
+
from . import temp_passwords
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
TOTP_DEVICE_NAME = "authenticator"
|
|
@@ -196,3 +197,40 @@ class LocalhostAdminBackend(ModelBackend):
|
|
|
196
197
|
return User.all_objects.get(pk=user_id)
|
|
197
198
|
except User.DoesNotExist:
|
|
198
199
|
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class TempPasswordBackend(ModelBackend):
|
|
203
|
+
"""Authenticate using a temporary password stored in a lockfile."""
|
|
204
|
+
|
|
205
|
+
def authenticate(self, request, username=None, password=None, **kwargs):
|
|
206
|
+
if not username or not password:
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
UserModel = get_user_model()
|
|
210
|
+
manager = getattr(UserModel, "all_objects", UserModel._default_manager)
|
|
211
|
+
try:
|
|
212
|
+
user = manager.get_by_natural_key(username)
|
|
213
|
+
except UserModel.DoesNotExist:
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
entry = temp_passwords.load_temp_password(user.username)
|
|
217
|
+
if entry is None:
|
|
218
|
+
return None
|
|
219
|
+
if entry.is_expired:
|
|
220
|
+
temp_passwords.discard_temp_password(user.username)
|
|
221
|
+
return None
|
|
222
|
+
if not entry.check_password(password):
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
if not user.is_active:
|
|
226
|
+
user.is_active = True
|
|
227
|
+
user.save(update_fields=["is_active"])
|
|
228
|
+
return user
|
|
229
|
+
|
|
230
|
+
def get_user(self, user_id):
|
|
231
|
+
UserModel = get_user_model()
|
|
232
|
+
manager = getattr(UserModel, "all_objects", UserModel._default_manager)
|
|
233
|
+
try:
|
|
234
|
+
return manager.get(pk=user_id)
|
|
235
|
+
except UserModel.DoesNotExist:
|
|
236
|
+
return None
|
core/environment.py
CHANGED
|
@@ -9,22 +9,35 @@ from django.urls import path
|
|
|
9
9
|
from django.utils.translation import gettext_lazy as _
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def
|
|
13
|
-
|
|
14
|
-
django_settings = sorted(
|
|
12
|
+
def _get_django_settings():
|
|
13
|
+
return sorted(
|
|
15
14
|
[(name, getattr(settings, name)) for name in dir(settings) if name.isupper()]
|
|
16
15
|
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _environment_view(request):
|
|
19
|
+
env_vars = sorted(os.environ.items())
|
|
17
20
|
context = admin.site.each_context(request)
|
|
18
21
|
context.update(
|
|
19
22
|
{
|
|
20
|
-
"title": _("
|
|
23
|
+
"title": _("Environ"),
|
|
21
24
|
"env_vars": env_vars,
|
|
22
|
-
"django_settings": django_settings,
|
|
23
25
|
}
|
|
24
26
|
)
|
|
25
27
|
return TemplateResponse(request, "admin/environment.html", context)
|
|
26
28
|
|
|
27
29
|
|
|
30
|
+
def _config_view(request):
|
|
31
|
+
context = admin.site.each_context(request)
|
|
32
|
+
context.update(
|
|
33
|
+
{
|
|
34
|
+
"title": _("Config"),
|
|
35
|
+
"django_settings": _get_django_settings(),
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
return TemplateResponse(request, "admin/config.html", context)
|
|
39
|
+
|
|
40
|
+
|
|
28
41
|
def patch_admin_environment_view() -> None:
|
|
29
42
|
"""Add custom admin view for environment information."""
|
|
30
43
|
original_get_urls = admin.site.get_urls
|
|
@@ -37,6 +50,11 @@ def patch_admin_environment_view() -> None:
|
|
|
37
50
|
admin.site.admin_view(_environment_view),
|
|
38
51
|
name="environment",
|
|
39
52
|
),
|
|
53
|
+
path(
|
|
54
|
+
"config/",
|
|
55
|
+
admin.site.admin_view(_config_view),
|
|
56
|
+
name="config",
|
|
57
|
+
),
|
|
40
58
|
]
|
|
41
59
|
return custom + urls
|
|
42
60
|
|
core/mailer.py
CHANGED
|
@@ -61,7 +61,9 @@ def can_send_email() -> bool:
|
|
|
61
61
|
|
|
62
62
|
from nodes.models import EmailOutbox # imported lazily to avoid circular deps
|
|
63
63
|
|
|
64
|
-
has_outbox =
|
|
64
|
+
has_outbox = (
|
|
65
|
+
EmailOutbox.objects.filter(is_enabled=True).exclude(host="").exists()
|
|
66
|
+
)
|
|
65
67
|
if has_outbox:
|
|
66
68
|
return True
|
|
67
69
|
|