arthexis 0.1.10__py3-none-any.whl → 0.1.12__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.12.dist-info}/METADATA +36 -26
- arthexis-0.1.12.dist-info/RECORD +102 -0
- config/context_processors.py +1 -0
- config/settings.py +31 -5
- config/urls.py +5 -4
- core/admin.py +430 -90
- core/apps.py +48 -2
- core/backends.py +38 -0
- core/environment.py +23 -5
- core/mailer.py +3 -1
- core/models.py +303 -31
- core/reference_utils.py +20 -9
- core/release.py +4 -0
- core/sigil_builder.py +7 -2
- core/sigil_resolver.py +35 -4
- core/system.py +250 -1
- core/tasks.py +92 -40
- core/temp_passwords.py +181 -0
- core/test_system_info.py +62 -2
- core/tests.py +169 -3
- core/user_data.py +51 -8
- core/views.py +371 -20
- nodes/admin.py +453 -8
- nodes/backends.py +21 -6
- nodes/dns.py +203 -0
- nodes/feature_checks.py +133 -0
- nodes/models.py +374 -31
- nodes/reports.py +411 -0
- nodes/tests.py +677 -38
- nodes/utils.py +32 -0
- nodes/views.py +14 -0
- ocpp/admin.py +278 -15
- ocpp/consumers.py +517 -16
- ocpp/evcs_discovery.py +158 -0
- ocpp/models.py +237 -4
- ocpp/reference_utils.py +42 -0
- ocpp/simulator.py +321 -22
- ocpp/store.py +110 -2
- ocpp/test_rfid.py +169 -7
- ocpp/tests.py +819 -6
- ocpp/transactions_io.py +17 -3
- ocpp/views.py +233 -19
- pages/admin.py +144 -4
- pages/context_processors.py +21 -7
- pages/defaults.py +13 -0
- pages/forms.py +38 -0
- pages/models.py +189 -15
- pages/tests.py +281 -8
- pages/urls.py +4 -0
- pages/views.py +137 -21
- arthexis-0.1.10.dist-info/RECORD +0 -95
- {arthexis-0.1.10.dist-info → arthexis-0.1.12.dist-info}/WHEEL +0 -0
- {arthexis-0.1.10.dist-info → arthexis-0.1.12.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.10.dist-info → arthexis-0.1.12.dist-info}/top_level.txt +0 -0
pages/context_processors.py
CHANGED
|
@@ -9,11 +9,22 @@ from core.reference_utils import filter_visible_references
|
|
|
9
9
|
from .models import Module
|
|
10
10
|
|
|
11
11
|
_favicon_path = Path(settings.BASE_DIR) / "pages" / "fixtures" / "data" / "favicon.txt"
|
|
12
|
+
_control_favicon_path = (
|
|
13
|
+
Path(settings.BASE_DIR) / "pages" / "fixtures" / "data" / "favicon_control.txt"
|
|
14
|
+
)
|
|
15
|
+
|
|
12
16
|
try:
|
|
13
17
|
_DEFAULT_FAVICON = f"data:image/png;base64,{_favicon_path.read_text().strip()}"
|
|
14
18
|
except OSError:
|
|
15
19
|
_DEFAULT_FAVICON = ""
|
|
16
20
|
|
|
21
|
+
try:
|
|
22
|
+
_CONTROL_FAVICON = (
|
|
23
|
+
f"data:image/png;base64,{_control_favicon_path.read_text().strip()}"
|
|
24
|
+
)
|
|
25
|
+
except OSError:
|
|
26
|
+
_CONTROL_FAVICON = _DEFAULT_FAVICON
|
|
27
|
+
|
|
17
28
|
|
|
18
29
|
def nav_links(request):
|
|
19
30
|
"""Provide navigation links for the current site."""
|
|
@@ -39,12 +50,12 @@ def nav_links(request):
|
|
|
39
50
|
except Resolver404:
|
|
40
51
|
continue
|
|
41
52
|
view_func = match.func
|
|
42
|
-
requires_login = getattr(view_func, "login_required", False)
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
requires_login = bool(getattr(view_func, "login_required", False))
|
|
54
|
+
if not requires_login and hasattr(view_func, "login_url"):
|
|
55
|
+
requires_login = True
|
|
45
56
|
staff_only = getattr(view_func, "staff_required", False)
|
|
46
57
|
if requires_login and not request.user.is_authenticated:
|
|
47
|
-
|
|
58
|
+
setattr(landing, "requires_login", True)
|
|
48
59
|
if staff_only and not request.user.is_staff:
|
|
49
60
|
continue
|
|
50
61
|
landings.append(landing)
|
|
@@ -52,8 +63,8 @@ def nav_links(request):
|
|
|
52
63
|
app_name = getattr(module.application, "name", "").lower()
|
|
53
64
|
if app_name == "awg":
|
|
54
65
|
module.menu = "Calculate"
|
|
55
|
-
elif
|
|
56
|
-
module.menu = "
|
|
66
|
+
elif module.path.rstrip("/").lower() == "/man":
|
|
67
|
+
module.menu = "Manual"
|
|
57
68
|
module.enabled_landings = landings
|
|
58
69
|
valid_modules.append(module)
|
|
59
70
|
if request.path.startswith(module.path):
|
|
@@ -84,7 +95,10 @@ def nav_links(request):
|
|
|
84
95
|
except Exception:
|
|
85
96
|
pass
|
|
86
97
|
if not favicon_url:
|
|
87
|
-
|
|
98
|
+
if node and getattr(node.role, "name", "") == "Control":
|
|
99
|
+
favicon_url = _CONTROL_FAVICON
|
|
100
|
+
else:
|
|
101
|
+
favicon_url = _DEFAULT_FAVICON
|
|
88
102
|
|
|
89
103
|
header_refs_qs = (
|
|
90
104
|
Reference.objects.filter(show_in_header=True)
|
pages/defaults.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Default configuration for the pages application."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Dict
|
|
5
|
+
|
|
6
|
+
DEFAULT_APPLICATION_DESCRIPTIONS: Dict[str, str] = {
|
|
7
|
+
"awg": "Power, Energy and Cost calculations.",
|
|
8
|
+
"core": "Support for Business Processes and monetization.",
|
|
9
|
+
"ocpp": "Compatibility with Standards and Good Practices.",
|
|
10
|
+
"nodes": "System and Node-level operations,",
|
|
11
|
+
"pages": "User QA, Continuity Design and Chaos Testing.",
|
|
12
|
+
"teams": "Identity, Entitlements and Access Controls.",
|
|
13
|
+
}
|
pages/forms.py
CHANGED
|
@@ -9,6 +9,8 @@ from django.core.exceptions import ValidationError
|
|
|
9
9
|
from django.utils.translation import gettext_lazy as _
|
|
10
10
|
from django.views.decorators.debug import sensitive_variables
|
|
11
11
|
|
|
12
|
+
from .models import UserStory
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
class AuthenticatorLoginForm(AuthenticationForm):
|
|
14
16
|
"""Authentication form that supports password or authenticator codes."""
|
|
@@ -129,3 +131,39 @@ class AuthenticatorEnrollmentForm(forms.Form):
|
|
|
129
131
|
|
|
130
132
|
def get_verified_device(self):
|
|
131
133
|
return self.device
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class UserStoryForm(forms.ModelForm):
|
|
137
|
+
class Meta:
|
|
138
|
+
model = UserStory
|
|
139
|
+
fields = ("name", "rating", "comments", "take_screenshot", "path")
|
|
140
|
+
widgets = {
|
|
141
|
+
"path": forms.HiddenInput(),
|
|
142
|
+
"comments": forms.Textarea(attrs={"rows": 4, "maxlength": 400}),
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
def __init__(self, *args, **kwargs):
|
|
146
|
+
super().__init__(*args, **kwargs)
|
|
147
|
+
self.fields["name"].required = False
|
|
148
|
+
self.fields["name"].widget.attrs.update(
|
|
149
|
+
{
|
|
150
|
+
"maxlength": 40,
|
|
151
|
+
"placeholder": _("Name, email or pseudonym"),
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
self.fields["take_screenshot"].initial = True
|
|
155
|
+
self.fields["rating"].widget = forms.RadioSelect(
|
|
156
|
+
choices=[(i, str(i)) for i in range(1, 6)]
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def clean_comments(self):
|
|
160
|
+
comments = (self.cleaned_data.get("comments") or "").strip()
|
|
161
|
+
if len(comments) > 400:
|
|
162
|
+
raise forms.ValidationError(
|
|
163
|
+
_("Feedback must be 400 characters or fewer."), code="max_length"
|
|
164
|
+
)
|
|
165
|
+
return comments
|
|
166
|
+
|
|
167
|
+
def clean_name(self):
|
|
168
|
+
name = (self.cleaned_data.get("name") or "").strip()
|
|
169
|
+
return name[:40]
|
pages/models.py
CHANGED
|
@@ -4,11 +4,14 @@ from django.contrib.sites.models import Site
|
|
|
4
4
|
from nodes.models import NodeRole
|
|
5
5
|
from django.apps import apps as django_apps
|
|
6
6
|
from django.utils.text import slugify
|
|
7
|
-
from django.utils.translation import gettext_lazy as _
|
|
7
|
+
from django.utils.translation import gettext, gettext_lazy as _
|
|
8
8
|
from importlib import import_module
|
|
9
9
|
from django.urls import URLPattern
|
|
10
10
|
from django.conf import settings
|
|
11
11
|
from django.contrib.contenttypes.models import ContentType
|
|
12
|
+
from django.core.validators import MaxLengthValidator, MaxValueValidator, MinValueValidator
|
|
13
|
+
|
|
14
|
+
from core import github_issues
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
class ApplicationManager(models.Manager):
|
|
@@ -111,6 +114,7 @@ class Module(Entity):
|
|
|
111
114
|
return
|
|
112
115
|
patterns = getattr(urlconf, "urlpatterns", [])
|
|
113
116
|
created = False
|
|
117
|
+
normalized_module = self.path.strip("/")
|
|
114
118
|
|
|
115
119
|
def _walk(patterns, prefix=""):
|
|
116
120
|
nonlocal created
|
|
@@ -118,17 +122,34 @@ class Module(Entity):
|
|
|
118
122
|
if isinstance(pattern, URLPattern):
|
|
119
123
|
callback = pattern.callback
|
|
120
124
|
if getattr(callback, "landing", False):
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
125
|
+
pattern_path = str(pattern.pattern)
|
|
126
|
+
relative = f"{prefix}{pattern_path}"
|
|
127
|
+
if normalized_module and relative.startswith(normalized_module):
|
|
128
|
+
full_path = f"/{relative}"
|
|
129
|
+
Landing.objects.update_or_create(
|
|
130
|
+
module=self,
|
|
131
|
+
path=full_path,
|
|
132
|
+
defaults={
|
|
133
|
+
"label": getattr(
|
|
134
|
+
callback,
|
|
135
|
+
"landing_label",
|
|
136
|
+
callback.__name__.replace("_", " ").title(),
|
|
137
|
+
)
|
|
138
|
+
},
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
full_path = f"{self.path}{relative}"
|
|
142
|
+
Landing.objects.get_or_create(
|
|
143
|
+
module=self,
|
|
144
|
+
path=full_path,
|
|
145
|
+
defaults={
|
|
146
|
+
"label": getattr(
|
|
147
|
+
callback,
|
|
148
|
+
"landing_label",
|
|
149
|
+
callback.__name__.replace("_", " ").title(),
|
|
150
|
+
)
|
|
151
|
+
},
|
|
152
|
+
)
|
|
132
153
|
created = True
|
|
133
154
|
else:
|
|
134
155
|
_walk(
|
|
@@ -192,6 +213,7 @@ class Landing(Entity):
|
|
|
192
213
|
return f"{self.label} ({self.path})"
|
|
193
214
|
|
|
194
215
|
def save(self, *args, **kwargs):
|
|
216
|
+
existing = None
|
|
195
217
|
if not self.pk:
|
|
196
218
|
existing = (
|
|
197
219
|
type(self).objects.filter(module=self.module, path=self.path).first()
|
|
@@ -200,10 +222,30 @@ class Landing(Entity):
|
|
|
200
222
|
self.pk = existing.pk
|
|
201
223
|
super().save(*args, **kwargs)
|
|
202
224
|
|
|
203
|
-
def natural_key(self): # pragma: no cover - simple representation
|
|
204
|
-
return (self.module.node_role.name, self.module.path, self.path)
|
|
205
225
|
|
|
206
|
-
|
|
226
|
+
class UserManual(Entity):
|
|
227
|
+
slug = models.SlugField(unique=True)
|
|
228
|
+
title = models.CharField(max_length=200)
|
|
229
|
+
description = models.CharField(max_length=200)
|
|
230
|
+
languages = models.CharField(
|
|
231
|
+
max_length=100,
|
|
232
|
+
blank=True,
|
|
233
|
+
default="",
|
|
234
|
+
help_text="Comma-separated 2-letter language codes",
|
|
235
|
+
)
|
|
236
|
+
content_html = models.TextField()
|
|
237
|
+
content_pdf = models.TextField(help_text="Base64 encoded PDF")
|
|
238
|
+
|
|
239
|
+
class Meta:
|
|
240
|
+
db_table = "man_usermanual"
|
|
241
|
+
verbose_name = "User Manual"
|
|
242
|
+
verbose_name_plural = "User Manuals"
|
|
243
|
+
|
|
244
|
+
def __str__(self): # pragma: no cover - simple representation
|
|
245
|
+
return self.title
|
|
246
|
+
|
|
247
|
+
def natural_key(self): # pragma: no cover - simple representation
|
|
248
|
+
return (self.slug,)
|
|
207
249
|
|
|
208
250
|
|
|
209
251
|
class ViewHistory(Entity):
|
|
@@ -240,6 +282,138 @@ class Favorite(Entity):
|
|
|
240
282
|
unique_together = ("user", "content_type")
|
|
241
283
|
|
|
242
284
|
|
|
285
|
+
class UserStory(Entity):
|
|
286
|
+
path = models.CharField(max_length=500)
|
|
287
|
+
name = models.CharField(max_length=40, blank=True)
|
|
288
|
+
rating = models.PositiveSmallIntegerField(
|
|
289
|
+
validators=[MinValueValidator(1), MaxValueValidator(5)],
|
|
290
|
+
help_text=_("Rate your experience from 1 (lowest) to 5 (highest)."),
|
|
291
|
+
)
|
|
292
|
+
comments = models.TextField(
|
|
293
|
+
validators=[MaxLengthValidator(400)],
|
|
294
|
+
help_text=_("Share more about your experience."),
|
|
295
|
+
)
|
|
296
|
+
take_screenshot = models.BooleanField(
|
|
297
|
+
default=True,
|
|
298
|
+
help_text=_("Request a screenshot capture for this feedback."),
|
|
299
|
+
)
|
|
300
|
+
user = models.ForeignKey(
|
|
301
|
+
settings.AUTH_USER_MODEL,
|
|
302
|
+
on_delete=models.SET_NULL,
|
|
303
|
+
blank=True,
|
|
304
|
+
null=True,
|
|
305
|
+
related_name="user_stories",
|
|
306
|
+
)
|
|
307
|
+
owner = models.ForeignKey(
|
|
308
|
+
settings.AUTH_USER_MODEL,
|
|
309
|
+
on_delete=models.SET_NULL,
|
|
310
|
+
blank=True,
|
|
311
|
+
null=True,
|
|
312
|
+
related_name="owned_user_stories",
|
|
313
|
+
help_text=_("Internal owner for this feedback."),
|
|
314
|
+
)
|
|
315
|
+
submitted_at = models.DateTimeField(auto_now_add=True)
|
|
316
|
+
github_issue_number = models.PositiveIntegerField(
|
|
317
|
+
blank=True,
|
|
318
|
+
null=True,
|
|
319
|
+
help_text=_("Number of the GitHub issue created for this feedback."),
|
|
320
|
+
)
|
|
321
|
+
github_issue_url = models.URLField(
|
|
322
|
+
blank=True,
|
|
323
|
+
help_text=_("Link to the GitHub issue created for this feedback."),
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
class Meta:
|
|
327
|
+
ordering = ["-submitted_at"]
|
|
328
|
+
verbose_name = _("User Story")
|
|
329
|
+
verbose_name_plural = _("User Stories")
|
|
330
|
+
|
|
331
|
+
def __str__(self) -> str: # pragma: no cover - simple representation
|
|
332
|
+
display = self.name or _("Anonymous")
|
|
333
|
+
return f"{display} ({self.rating}/5)"
|
|
334
|
+
|
|
335
|
+
def get_github_issue_labels(self) -> list[str]:
|
|
336
|
+
"""Return default labels used when creating GitHub issues."""
|
|
337
|
+
|
|
338
|
+
return ["feedback"]
|
|
339
|
+
|
|
340
|
+
def get_github_issue_fingerprint(self) -> str | None:
|
|
341
|
+
"""Return a fingerprint used to avoid duplicate issue submissions."""
|
|
342
|
+
|
|
343
|
+
if self.pk:
|
|
344
|
+
return f"user-story:{self.pk}"
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
def build_github_issue_title(self) -> str:
|
|
348
|
+
"""Return the title used for GitHub issues."""
|
|
349
|
+
|
|
350
|
+
path = self.path or "/"
|
|
351
|
+
return gettext("Feedback for %(path)s (%(rating)s/5)") % {
|
|
352
|
+
"path": path,
|
|
353
|
+
"rating": self.rating,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
def build_github_issue_body(self) -> str:
|
|
357
|
+
"""Return the issue body summarising the feedback details."""
|
|
358
|
+
|
|
359
|
+
name = self.name or gettext("Anonymous")
|
|
360
|
+
path = self.path or "/"
|
|
361
|
+
screenshot_requested = gettext("Yes") if self.take_screenshot else gettext("No")
|
|
362
|
+
|
|
363
|
+
lines = [
|
|
364
|
+
f"**Path:** {path}",
|
|
365
|
+
f"**Rating:** {self.rating}/5",
|
|
366
|
+
f"**Name:** {name}",
|
|
367
|
+
f"**Screenshot requested:** {screenshot_requested}",
|
|
368
|
+
]
|
|
369
|
+
|
|
370
|
+
if self.submitted_at:
|
|
371
|
+
lines.append(f"**Submitted at:** {self.submitted_at.isoformat()}")
|
|
372
|
+
|
|
373
|
+
comment = (self.comments or "").strip()
|
|
374
|
+
if comment:
|
|
375
|
+
lines.extend(["", comment])
|
|
376
|
+
|
|
377
|
+
return "\n".join(lines).strip()
|
|
378
|
+
|
|
379
|
+
def create_github_issue(self) -> str | None:
|
|
380
|
+
"""Create a GitHub issue for this feedback and store the identifiers."""
|
|
381
|
+
|
|
382
|
+
if self.github_issue_url:
|
|
383
|
+
return self.github_issue_url
|
|
384
|
+
|
|
385
|
+
response = github_issues.create_issue(
|
|
386
|
+
self.build_github_issue_title(),
|
|
387
|
+
self.build_github_issue_body(),
|
|
388
|
+
labels=self.get_github_issue_labels(),
|
|
389
|
+
fingerprint=self.get_github_issue_fingerprint(),
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
if response is None:
|
|
393
|
+
return None
|
|
394
|
+
|
|
395
|
+
try:
|
|
396
|
+
payload = response.json()
|
|
397
|
+
except ValueError: # pragma: no cover - defensive guard
|
|
398
|
+
payload = {}
|
|
399
|
+
|
|
400
|
+
issue_url = payload.get("html_url")
|
|
401
|
+
issue_number = payload.get("number")
|
|
402
|
+
|
|
403
|
+
update_fields = []
|
|
404
|
+
if issue_url and issue_url != self.github_issue_url:
|
|
405
|
+
self.github_issue_url = issue_url
|
|
406
|
+
update_fields.append("github_issue_url")
|
|
407
|
+
if issue_number is not None and issue_number != self.github_issue_number:
|
|
408
|
+
self.github_issue_number = issue_number
|
|
409
|
+
update_fields.append("github_issue_number")
|
|
410
|
+
|
|
411
|
+
if update_fields:
|
|
412
|
+
self.save(update_fields=update_fields)
|
|
413
|
+
|
|
414
|
+
return issue_url
|
|
415
|
+
|
|
416
|
+
|
|
243
417
|
from django.db.models.signals import post_save
|
|
244
418
|
from django.dispatch import receiver
|
|
245
419
|
|