arthexis 0.1.16__py3-none-any.whl → 0.1.18__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.16.dist-info → arthexis-0.1.18.dist-info}/METADATA +1 -1
- {arthexis-0.1.16.dist-info → arthexis-0.1.18.dist-info}/RECORD +26 -25
- config/middleware.py +47 -1
- config/settings.py +4 -0
- config/urls.py +5 -0
- core/admin.py +69 -9
- core/backends.py +2 -0
- core/changelog.py +66 -5
- core/models.py +88 -7
- core/release.py +55 -2
- core/system.py +1 -1
- core/tasks.py +0 -6
- core/tests.py +131 -0
- core/views.py +112 -24
- ocpp/admin.py +92 -10
- ocpp/consumers.py +63 -19
- ocpp/test_rfid.py +118 -3
- ocpp/tests.py +225 -0
- ocpp/views.py +46 -7
- pages/admin.py +87 -5
- pages/apps.py +3 -0
- pages/site_config.py +137 -0
- pages/tests.py +206 -2
- {arthexis-0.1.16.dist-info → arthexis-0.1.18.dist-info}/WHEEL +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.18.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.18.dist-info}/top_level.txt +0 -0
pages/admin.py
CHANGED
|
@@ -6,12 +6,13 @@ from django.contrib.sites.admin import SiteAdmin as DjangoSiteAdmin
|
|
|
6
6
|
from django.contrib.sites.models import Site
|
|
7
7
|
from django import forms
|
|
8
8
|
from django.shortcuts import redirect, render, get_object_or_404
|
|
9
|
-
from django.urls import path, reverse
|
|
9
|
+
from django.urls import NoReverseMatch, path, reverse
|
|
10
10
|
from django.utils.html import format_html
|
|
11
11
|
from django.template.response import TemplateResponse
|
|
12
12
|
from django.http import JsonResponse
|
|
13
13
|
from django.utils import timezone
|
|
14
14
|
from django.db.models import Count
|
|
15
|
+
from django.core.exceptions import FieldError
|
|
15
16
|
from django.db.models.functions import TruncDate
|
|
16
17
|
from datetime import datetime, time, timedelta
|
|
17
18
|
import ipaddress
|
|
@@ -25,6 +26,7 @@ from nodes.utils import capture_screenshot, save_screenshot
|
|
|
25
26
|
|
|
26
27
|
from .forms import UserManualAdminForm
|
|
27
28
|
from .module_defaults import reload_default_modules as restore_default_modules
|
|
29
|
+
from .site_config import ensure_site_fields
|
|
28
30
|
from .utils import landing_leads_supported
|
|
29
31
|
|
|
30
32
|
from .models import (
|
|
@@ -41,6 +43,7 @@ from .models import (
|
|
|
41
43
|
UserStory,
|
|
42
44
|
)
|
|
43
45
|
from django.contrib.contenttypes.models import ContentType
|
|
46
|
+
from core.models import ReleaseManager
|
|
44
47
|
from core.user_data import EntityModelAdmin
|
|
45
48
|
|
|
46
49
|
|
|
@@ -73,12 +76,47 @@ class SiteForm(forms.ModelForm):
|
|
|
73
76
|
fields = "__all__"
|
|
74
77
|
|
|
75
78
|
|
|
79
|
+
ensure_site_fields()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class _BooleanAttributeListFilter(admin.SimpleListFilter):
|
|
83
|
+
"""Filter helper for boolean attributes on :class:`~django.contrib.sites.models.Site`."""
|
|
84
|
+
|
|
85
|
+
field_name: str
|
|
86
|
+
|
|
87
|
+
def lookups(self, request, model_admin): # pragma: no cover - admin UI
|
|
88
|
+
return (("1", _("Yes")), ("0", _("No")))
|
|
89
|
+
|
|
90
|
+
def queryset(self, request, queryset):
|
|
91
|
+
value = self.value()
|
|
92
|
+
if value not in {"0", "1"}:
|
|
93
|
+
return queryset
|
|
94
|
+
expected = value == "1"
|
|
95
|
+
try:
|
|
96
|
+
return queryset.filter(**{self.field_name: expected})
|
|
97
|
+
except FieldError: # pragma: no cover - defensive when fields missing
|
|
98
|
+
return queryset
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ManagedSiteListFilter(_BooleanAttributeListFilter):
|
|
102
|
+
title = _("Managed by local NGINX")
|
|
103
|
+
parameter_name = "managed"
|
|
104
|
+
field_name = "managed"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class RequireHttpsListFilter(_BooleanAttributeListFilter):
|
|
108
|
+
title = _("Require HTTPS")
|
|
109
|
+
parameter_name = "require_https"
|
|
110
|
+
field_name = "require_https"
|
|
111
|
+
|
|
112
|
+
|
|
76
113
|
class SiteAdmin(DjangoSiteAdmin):
|
|
77
114
|
form = SiteForm
|
|
78
115
|
inlines = [SiteBadgeInline]
|
|
79
116
|
change_list_template = "admin/sites/site/change_list.html"
|
|
80
|
-
fields = ("domain", "name")
|
|
81
|
-
list_display = ("domain", "name")
|
|
117
|
+
fields = ("domain", "name", "managed", "require_https")
|
|
118
|
+
list_display = ("domain", "name", "managed", "require_https")
|
|
119
|
+
list_filter = (ManagedSiteListFilter, RequireHttpsListFilter)
|
|
82
120
|
actions = ["capture_screenshot"]
|
|
83
121
|
|
|
84
122
|
@admin.action(description="Capture screenshot")
|
|
@@ -110,6 +148,27 @@ class SiteAdmin(DjangoSiteAdmin):
|
|
|
110
148
|
messages.INFO,
|
|
111
149
|
)
|
|
112
150
|
|
|
151
|
+
def save_model(self, request, obj, form, change):
|
|
152
|
+
super().save_model(request, obj, form, change)
|
|
153
|
+
if {"managed", "require_https"} & set(form.changed_data or []):
|
|
154
|
+
self.message_user(
|
|
155
|
+
request,
|
|
156
|
+
_(
|
|
157
|
+
"Managed NGINX configuration staged. Run network-setup.sh to apply changes."
|
|
158
|
+
),
|
|
159
|
+
messages.INFO,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def delete_model(self, request, obj):
|
|
163
|
+
super().delete_model(request, obj)
|
|
164
|
+
self.message_user(
|
|
165
|
+
request,
|
|
166
|
+
_(
|
|
167
|
+
"Managed NGINX configuration staged. Run network-setup.sh to apply changes."
|
|
168
|
+
),
|
|
169
|
+
messages.INFO,
|
|
170
|
+
)
|
|
171
|
+
|
|
113
172
|
def _reload_site_fixtures(self, request):
|
|
114
173
|
fixtures_dir = Path(settings.BASE_DIR) / "core" / "fixtures"
|
|
115
174
|
fixture_paths = sorted(fixtures_dir.glob("references__00_site_*.json"))
|
|
@@ -708,10 +767,33 @@ class UserStoryAdmin(EntityModelAdmin):
|
|
|
708
767
|
issue_url = story.create_github_issue()
|
|
709
768
|
except Exception as exc: # pragma: no cover - network/runtime errors
|
|
710
769
|
logger.exception("Failed to create GitHub issue for UserStory %s", story.pk)
|
|
770
|
+
message = _("Unable to create a GitHub issue for %(story)s: %(error)s") % {
|
|
771
|
+
"story": story,
|
|
772
|
+
"error": exc,
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (
|
|
776
|
+
isinstance(exc, RuntimeError)
|
|
777
|
+
and "GitHub token is not configured" in str(exc)
|
|
778
|
+
):
|
|
779
|
+
try:
|
|
780
|
+
opts = ReleaseManager._meta
|
|
781
|
+
config_url = reverse(
|
|
782
|
+
f"{self.admin_site.name}:{opts.app_label}_{opts.model_name}_changelist"
|
|
783
|
+
)
|
|
784
|
+
except NoReverseMatch: # pragma: no cover - defensive guard
|
|
785
|
+
config_url = None
|
|
786
|
+
if config_url:
|
|
787
|
+
message = format_html(
|
|
788
|
+
"{} <a href=\"{}\">{}</a>",
|
|
789
|
+
message,
|
|
790
|
+
config_url,
|
|
791
|
+
_("Configure GitHub credentials."),
|
|
792
|
+
)
|
|
793
|
+
|
|
711
794
|
self.message_user(
|
|
712
795
|
request,
|
|
713
|
-
|
|
714
|
-
% {"story": story, "error": exc},
|
|
796
|
+
message,
|
|
715
797
|
messages.ERROR,
|
|
716
798
|
)
|
|
717
799
|
continue
|
pages/apps.py
CHANGED
pages/site_config.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Customizations for :mod:`django.contrib.sites`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from django.apps import apps
|
|
10
|
+
from django.conf import settings
|
|
11
|
+
from django.contrib.sites.models import Site
|
|
12
|
+
from django.db import DatabaseError, models
|
|
13
|
+
from django.db.models.signals import post_delete, post_migrate, post_save
|
|
14
|
+
from django.dispatch import receiver
|
|
15
|
+
from django.utils.translation import gettext_lazy as _
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_FIELD_DEFINITIONS: tuple[tuple[str, models.Field], ...] = (
|
|
22
|
+
(
|
|
23
|
+
"managed",
|
|
24
|
+
models.BooleanField(
|
|
25
|
+
default=False,
|
|
26
|
+
db_default=False,
|
|
27
|
+
verbose_name=_("Managed by local NGINX"),
|
|
28
|
+
help_text=_(
|
|
29
|
+
"Include this site when staging the local NGINX configuration."
|
|
30
|
+
),
|
|
31
|
+
),
|
|
32
|
+
),
|
|
33
|
+
(
|
|
34
|
+
"require_https",
|
|
35
|
+
models.BooleanField(
|
|
36
|
+
default=False,
|
|
37
|
+
db_default=False,
|
|
38
|
+
verbose_name=_("Require HTTPS"),
|
|
39
|
+
help_text=_(
|
|
40
|
+
"Redirect HTTP traffic to HTTPS when the staged NGINX configuration is applied."
|
|
41
|
+
),
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _sites_config_path() -> Path:
|
|
48
|
+
return Path(settings.BASE_DIR) / "scripts" / "generated" / "nginx-sites.json"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _ensure_directories(path: Path) -> bool:
|
|
52
|
+
try:
|
|
53
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
except OSError as exc: # pragma: no cover - filesystem errors
|
|
55
|
+
logger.warning("Unable to create directory for %s: %s", path, exc)
|
|
56
|
+
return False
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def update_local_nginx_scripts() -> None:
|
|
61
|
+
"""Serialize managed site configuration for the network setup script."""
|
|
62
|
+
|
|
63
|
+
SiteModel = apps.get_model("sites", "Site")
|
|
64
|
+
data: list[dict[str, object]] = []
|
|
65
|
+
seen_domains: set[str] = set()
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
sites = list(
|
|
69
|
+
SiteModel.objects.filter(managed=True)
|
|
70
|
+
.only("domain", "require_https")
|
|
71
|
+
.order_by("domain")
|
|
72
|
+
)
|
|
73
|
+
except DatabaseError: # pragma: no cover - database not ready
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
for site in sites:
|
|
77
|
+
domain = (site.domain or "").strip()
|
|
78
|
+
if not domain:
|
|
79
|
+
continue
|
|
80
|
+
if domain.lower() in seen_domains:
|
|
81
|
+
continue
|
|
82
|
+
seen_domains.add(domain.lower())
|
|
83
|
+
data.append({"domain": domain, "require_https": bool(site.require_https)})
|
|
84
|
+
|
|
85
|
+
output_path = _sites_config_path()
|
|
86
|
+
if not _ensure_directories(output_path):
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
if data:
|
|
90
|
+
try:
|
|
91
|
+
output_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
92
|
+
except OSError as exc: # pragma: no cover - filesystem errors
|
|
93
|
+
logger.warning("Failed to write managed site configuration: %s", exc)
|
|
94
|
+
else:
|
|
95
|
+
try:
|
|
96
|
+
output_path.unlink()
|
|
97
|
+
except FileNotFoundError:
|
|
98
|
+
pass
|
|
99
|
+
except OSError as exc: # pragma: no cover - filesystem errors
|
|
100
|
+
logger.warning("Failed to remove managed site configuration: %s", exc)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _install_fields() -> None:
|
|
104
|
+
for name, field in _FIELD_DEFINITIONS:
|
|
105
|
+
if hasattr(Site, name):
|
|
106
|
+
continue
|
|
107
|
+
Site.add_to_class(name, field.clone())
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def ensure_site_fields() -> None:
|
|
111
|
+
"""Ensure the custom ``Site`` fields are installed."""
|
|
112
|
+
|
|
113
|
+
_install_fields()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@receiver(post_save, sender=Site, dispatch_uid="pages_site_save_update_nginx")
|
|
117
|
+
def _site_saved(sender, **kwargs) -> None: # pragma: no cover - signal wrapper
|
|
118
|
+
update_local_nginx_scripts()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@receiver(post_delete, sender=Site, dispatch_uid="pages_site_delete_update_nginx")
|
|
122
|
+
def _site_deleted(sender, **kwargs) -> None: # pragma: no cover - signal wrapper
|
|
123
|
+
update_local_nginx_scripts()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _run_post_migrate_update(**kwargs) -> None: # pragma: no cover - signal wrapper
|
|
127
|
+
update_local_nginx_scripts()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def ready() -> None:
|
|
131
|
+
"""Apply customizations and connect signal handlers."""
|
|
132
|
+
|
|
133
|
+
ensure_site_fields()
|
|
134
|
+
post_migrate.connect(
|
|
135
|
+
_run_post_migrate_update,
|
|
136
|
+
dispatch_uid="pages_site_post_migrate_update",
|
|
137
|
+
)
|
pages/tests.py
CHANGED
|
@@ -8,18 +8,19 @@ django.setup()
|
|
|
8
8
|
|
|
9
9
|
from django.test import Client, RequestFactory, TestCase, SimpleTestCase, override_settings
|
|
10
10
|
from django.test.utils import CaptureQueriesContext
|
|
11
|
-
from django.urls import reverse
|
|
11
|
+
from django.urls import NoReverseMatch, reverse
|
|
12
12
|
from django.templatetags.static import static
|
|
13
13
|
from urllib.parse import quote
|
|
14
14
|
from django.contrib.auth import get_user_model
|
|
15
15
|
from django.contrib.sites.models import Site
|
|
16
|
-
from django.contrib import admin
|
|
16
|
+
from django.contrib import admin, messages
|
|
17
17
|
from django.contrib.messages.storage.fallback import FallbackStorage
|
|
18
18
|
from django.core.exceptions import DisallowedHost
|
|
19
19
|
from django.core.cache import cache
|
|
20
20
|
from django.db import connection
|
|
21
21
|
import socket
|
|
22
22
|
from django.db import connection
|
|
23
|
+
from pages import site_config
|
|
23
24
|
from pages.models import (
|
|
24
25
|
Application,
|
|
25
26
|
Landing,
|
|
@@ -47,6 +48,7 @@ from pages.screenshot_specs import (
|
|
|
47
48
|
)
|
|
48
49
|
from pages.context_processors import nav_links
|
|
49
50
|
from django.apps import apps as django_apps
|
|
51
|
+
from config.middleware import SiteHttpsRedirectMiddleware
|
|
50
52
|
from core import mailer
|
|
51
53
|
from core.admin import ProfileAdminMixin
|
|
52
54
|
from core.models import (
|
|
@@ -60,8 +62,10 @@ from core.models import (
|
|
|
60
62
|
Todo,
|
|
61
63
|
TOTPDeviceSettings,
|
|
62
64
|
)
|
|
65
|
+
from ocpp.models import Charger
|
|
63
66
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
64
67
|
import base64
|
|
68
|
+
import json
|
|
65
69
|
import tempfile
|
|
66
70
|
import shutil
|
|
67
71
|
from io import StringIO
|
|
@@ -72,6 +76,7 @@ from types import SimpleNamespace
|
|
|
72
76
|
from django.core.management import call_command
|
|
73
77
|
import re
|
|
74
78
|
from django.contrib.contenttypes.models import ContentType
|
|
79
|
+
from django.http import HttpResponse
|
|
75
80
|
from datetime import (
|
|
76
81
|
date,
|
|
77
82
|
datetime,
|
|
@@ -685,6 +690,22 @@ class AdminDashboardAppListTests(TestCase):
|
|
|
685
690
|
resp = self.client.get(reverse("admin:index"))
|
|
686
691
|
self.assertContains(resp, "5. Horologia MODELS")
|
|
687
692
|
|
|
693
|
+
def test_dashboard_handles_missing_last_net_message_url(self):
|
|
694
|
+
from pages.templatetags import admin_extras
|
|
695
|
+
|
|
696
|
+
real_reverse = admin_extras.reverse
|
|
697
|
+
|
|
698
|
+
def fake_reverse(name, *args, **kwargs):
|
|
699
|
+
if name == "last-net-message":
|
|
700
|
+
raise NoReverseMatch("missing")
|
|
701
|
+
return real_reverse(name, *args, **kwargs)
|
|
702
|
+
|
|
703
|
+
with patch("pages.templatetags.admin_extras.reverse", side_effect=fake_reverse):
|
|
704
|
+
resp = self.client.get(reverse("admin:index"))
|
|
705
|
+
|
|
706
|
+
self.assertEqual(resp.status_code, 200)
|
|
707
|
+
self.assertNotIn(b"last-net-message", resp.content)
|
|
708
|
+
|
|
688
709
|
|
|
689
710
|
class AdminSidebarTests(TestCase):
|
|
690
711
|
def setUp(self):
|
|
@@ -1156,6 +1177,125 @@ class AdminModelStatusTests(TestCase):
|
|
|
1156
1177
|
self.assertContains(resp, 'class="model-status missing"', count=1)
|
|
1157
1178
|
|
|
1158
1179
|
|
|
1180
|
+
class _FakeQuerySet(list):
|
|
1181
|
+
def only(self, *args, **kwargs):
|
|
1182
|
+
return self
|
|
1183
|
+
|
|
1184
|
+
def order_by(self, *args, **kwargs):
|
|
1185
|
+
return self
|
|
1186
|
+
|
|
1187
|
+
|
|
1188
|
+
class SiteConfigurationStagingTests(SimpleTestCase):
|
|
1189
|
+
def setUp(self):
|
|
1190
|
+
self.tmpdir = tempfile.mkdtemp()
|
|
1191
|
+
self.addCleanup(shutil.rmtree, self.tmpdir)
|
|
1192
|
+
self.config_path = Path(self.tmpdir) / "nginx-sites.json"
|
|
1193
|
+
self._path_patcher = patch(
|
|
1194
|
+
"pages.site_config._sites_config_path", side_effect=lambda: self.config_path
|
|
1195
|
+
)
|
|
1196
|
+
self._path_patcher.start()
|
|
1197
|
+
self.addCleanup(self._path_patcher.stop)
|
|
1198
|
+
self._model_patcher = patch("pages.site_config.apps.get_model")
|
|
1199
|
+
self.mock_get_model = self._model_patcher.start()
|
|
1200
|
+
self.addCleanup(self._model_patcher.stop)
|
|
1201
|
+
|
|
1202
|
+
def _read_config(self):
|
|
1203
|
+
if not self.config_path.exists():
|
|
1204
|
+
return None
|
|
1205
|
+
return json.loads(self.config_path.read_text(encoding="utf-8"))
|
|
1206
|
+
|
|
1207
|
+
def _set_sites(self, sites):
|
|
1208
|
+
queryset = _FakeQuerySet(sites)
|
|
1209
|
+
|
|
1210
|
+
class _Manager:
|
|
1211
|
+
@staticmethod
|
|
1212
|
+
def filter(**kwargs):
|
|
1213
|
+
return queryset
|
|
1214
|
+
|
|
1215
|
+
self.mock_get_model.return_value = SimpleNamespace(objects=_Manager())
|
|
1216
|
+
|
|
1217
|
+
def test_managed_site_persists_configuration(self):
|
|
1218
|
+
self._set_sites([SimpleNamespace(domain="example.com", require_https=True)])
|
|
1219
|
+
site_config.update_local_nginx_scripts()
|
|
1220
|
+
config = self._read_config()
|
|
1221
|
+
self.assertEqual(
|
|
1222
|
+
config,
|
|
1223
|
+
[
|
|
1224
|
+
{
|
|
1225
|
+
"domain": "example.com",
|
|
1226
|
+
"require_https": True,
|
|
1227
|
+
}
|
|
1228
|
+
],
|
|
1229
|
+
)
|
|
1230
|
+
|
|
1231
|
+
def test_disabling_managed_site_removes_entry(self):
|
|
1232
|
+
primary = SimpleNamespace(domain="primary.test", require_https=False)
|
|
1233
|
+
secondary = SimpleNamespace(domain="secondary.test", require_https=False)
|
|
1234
|
+
self._set_sites([primary, secondary])
|
|
1235
|
+
site_config.update_local_nginx_scripts()
|
|
1236
|
+
config = self._read_config()
|
|
1237
|
+
self.assertEqual(
|
|
1238
|
+
[entry["domain"] for entry in config],
|
|
1239
|
+
["primary.test", "secondary.test"],
|
|
1240
|
+
)
|
|
1241
|
+
|
|
1242
|
+
self._set_sites([secondary])
|
|
1243
|
+
site_config.update_local_nginx_scripts()
|
|
1244
|
+
config = self._read_config()
|
|
1245
|
+
self.assertEqual(config, [{"domain": "secondary.test", "require_https": False}])
|
|
1246
|
+
|
|
1247
|
+
self._set_sites([])
|
|
1248
|
+
site_config.update_local_nginx_scripts()
|
|
1249
|
+
self.assertIsNone(self._read_config())
|
|
1250
|
+
|
|
1251
|
+
def test_require_https_toggle_updates_configuration(self):
|
|
1252
|
+
site = SimpleNamespace(domain="secure.example", require_https=False)
|
|
1253
|
+
self._set_sites([site])
|
|
1254
|
+
site_config.update_local_nginx_scripts()
|
|
1255
|
+
config = self._read_config()
|
|
1256
|
+
self.assertEqual(config, [{"domain": "secure.example", "require_https": False}])
|
|
1257
|
+
|
|
1258
|
+
site.require_https = True
|
|
1259
|
+
self._set_sites([site])
|
|
1260
|
+
site_config.update_local_nginx_scripts()
|
|
1261
|
+
config = self._read_config()
|
|
1262
|
+
self.assertEqual(config, [{"domain": "secure.example", "require_https": True}])
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
class SiteRequireHttpsMiddlewareTests(SimpleTestCase):
|
|
1266
|
+
def setUp(self):
|
|
1267
|
+
self.factory = RequestFactory()
|
|
1268
|
+
self.middleware = SiteHttpsRedirectMiddleware(lambda request: HttpResponse("ok"))
|
|
1269
|
+
self.secure_site = SimpleNamespace(domain="secure.test", require_https=True)
|
|
1270
|
+
|
|
1271
|
+
def test_http_request_redirects_to_https(self):
|
|
1272
|
+
request = self.factory.get("/", HTTP_HOST="secure.test")
|
|
1273
|
+
request.site = self.secure_site
|
|
1274
|
+
response = self.middleware(request)
|
|
1275
|
+
self.assertEqual(response.status_code, 301)
|
|
1276
|
+
self.assertTrue(response["Location"].startswith("https://secure.test"))
|
|
1277
|
+
|
|
1278
|
+
def test_secure_request_not_redirected(self):
|
|
1279
|
+
request = self.factory.get("/", HTTP_HOST="secure.test", secure=True)
|
|
1280
|
+
request.site = self.secure_site
|
|
1281
|
+
response = self.middleware(request)
|
|
1282
|
+
self.assertEqual(response.status_code, 200)
|
|
1283
|
+
|
|
1284
|
+
def test_forwarded_proto_respected(self):
|
|
1285
|
+
request = self.factory.get(
|
|
1286
|
+
"/", HTTP_HOST="secure.test", HTTP_X_FORWARDED_PROTO="https"
|
|
1287
|
+
)
|
|
1288
|
+
request.site = self.secure_site
|
|
1289
|
+
response = self.middleware(request)
|
|
1290
|
+
self.assertEqual(response.status_code, 200)
|
|
1291
|
+
|
|
1292
|
+
self.secure_site.require_https = False
|
|
1293
|
+
request = self.factory.get("/", HTTP_HOST="secure.test")
|
|
1294
|
+
request.site = self.secure_site
|
|
1295
|
+
response = self.middleware(request)
|
|
1296
|
+
self.assertEqual(response.status_code, 200)
|
|
1297
|
+
|
|
1298
|
+
|
|
1159
1299
|
class SiteAdminRegisterCurrentTests(TestCase):
|
|
1160
1300
|
def setUp(self):
|
|
1161
1301
|
self.client = Client()
|
|
@@ -2406,6 +2546,27 @@ class FavoriteTests(TestCase):
|
|
|
2406
2546
|
self.assertContains(resp, f'title="{badge_label}"')
|
|
2407
2547
|
self.assertContains(resp, f'aria-label="{badge_label}"')
|
|
2408
2548
|
|
|
2549
|
+
def test_dashboard_shows_charge_point_availability_badge(self):
|
|
2550
|
+
Charger.objects.create(charger_id="CP-001", last_status="Available")
|
|
2551
|
+
Charger.objects.create(
|
|
2552
|
+
charger_id="CP-001", connector_id=1, last_status="Available"
|
|
2553
|
+
)
|
|
2554
|
+
Charger.objects.create(
|
|
2555
|
+
charger_id="CP-002", connector_id=1, last_status="Unavailable"
|
|
2556
|
+
)
|
|
2557
|
+
|
|
2558
|
+
resp = self.client.get(reverse("admin:index"))
|
|
2559
|
+
|
|
2560
|
+
expected = "2 / 1"
|
|
2561
|
+
badge_label = gettext(
|
|
2562
|
+
"%(total)s chargers reporting Available status, including %(with_cp)s with a CP number"
|
|
2563
|
+
) % {"total": 2, "with_cp": 1}
|
|
2564
|
+
|
|
2565
|
+
self.assertContains(resp, expected)
|
|
2566
|
+
self.assertContains(resp, 'class="charger-availability-badge"')
|
|
2567
|
+
self.assertContains(resp, f'title="{badge_label}"')
|
|
2568
|
+
self.assertContains(resp, f'aria-label="{badge_label}"')
|
|
2569
|
+
|
|
2409
2570
|
def test_nav_sidebar_hides_dashboard_badges(self):
|
|
2410
2571
|
InviteLead.objects.create(email="open@example.com")
|
|
2411
2572
|
RFID.objects.create(rfid="RFID0003", released=True, allowed=True)
|
|
@@ -2568,6 +2729,15 @@ class FavoriteTests(TestCase):
|
|
|
2568
2729
|
resp, '<div class="todo-details">More info</div>', html=True
|
|
2569
2730
|
)
|
|
2570
2731
|
|
|
2732
|
+
def test_dashboard_hides_completed_todos(self):
|
|
2733
|
+
todo = Todo.objects.create(request="Completed task")
|
|
2734
|
+
Todo.objects.filter(pk=todo.pk).update(done_on=timezone.now())
|
|
2735
|
+
|
|
2736
|
+
resp = self.client.get(reverse("admin:index"))
|
|
2737
|
+
|
|
2738
|
+
self.assertNotContains(resp, todo.request)
|
|
2739
|
+
self.assertNotContains(resp, "Completed")
|
|
2740
|
+
|
|
2571
2741
|
def test_dashboard_shows_todos_when_node_unknown(self):
|
|
2572
2742
|
Todo.objects.create(request="Check fallback")
|
|
2573
2743
|
from nodes.models import Node
|
|
@@ -3134,6 +3304,40 @@ class UserStoryAdminActionTests(TestCase):
|
|
|
3134
3304
|
|
|
3135
3305
|
mock_create_issue.assert_not_called()
|
|
3136
3306
|
|
|
3307
|
+
def test_create_github_issues_action_links_to_credentials_when_missing(self):
|
|
3308
|
+
request = self._build_request()
|
|
3309
|
+
queryset = UserStory.objects.filter(pk=self.story.pk)
|
|
3310
|
+
|
|
3311
|
+
mock_url = "/admin/core/releasemanager/"
|
|
3312
|
+
with (
|
|
3313
|
+
patch(
|
|
3314
|
+
"pages.admin.reverse", return_value=mock_url
|
|
3315
|
+
) as mock_reverse,
|
|
3316
|
+
patch.object(
|
|
3317
|
+
UserStory,
|
|
3318
|
+
"create_github_issue",
|
|
3319
|
+
side_effect=RuntimeError("GitHub token is not configured"),
|
|
3320
|
+
),
|
|
3321
|
+
):
|
|
3322
|
+
self.admin.create_github_issues(request, queryset)
|
|
3323
|
+
|
|
3324
|
+
messages_list = list(request._messages)
|
|
3325
|
+
self.assertTrue(messages_list)
|
|
3326
|
+
|
|
3327
|
+
opts = ReleaseManager._meta
|
|
3328
|
+
mock_reverse.assert_called_once_with(
|
|
3329
|
+
f"{self.admin.admin_site.name}:{opts.app_label}_{opts.model_name}_changelist"
|
|
3330
|
+
)
|
|
3331
|
+
self.assertTrue(
|
|
3332
|
+
any(mock_url in message.message for message in messages_list),
|
|
3333
|
+
)
|
|
3334
|
+
self.assertTrue(
|
|
3335
|
+
any("Configure GitHub credentials" in message.message for message in messages_list),
|
|
3336
|
+
)
|
|
3337
|
+
self.assertTrue(
|
|
3338
|
+
any(message.level == messages.ERROR for message in messages_list),
|
|
3339
|
+
)
|
|
3340
|
+
|
|
3137
3341
|
|
|
3138
3342
|
class ClientReportLiveUpdateTests(TestCase):
|
|
3139
3343
|
def setUp(self):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|