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.

Files changed (54) hide show
  1. {arthexis-0.1.10.dist-info → arthexis-0.1.12.dist-info}/METADATA +36 -26
  2. arthexis-0.1.12.dist-info/RECORD +102 -0
  3. config/context_processors.py +1 -0
  4. config/settings.py +31 -5
  5. config/urls.py +5 -4
  6. core/admin.py +430 -90
  7. core/apps.py +48 -2
  8. core/backends.py +38 -0
  9. core/environment.py +23 -5
  10. core/mailer.py +3 -1
  11. core/models.py +303 -31
  12. core/reference_utils.py +20 -9
  13. core/release.py +4 -0
  14. core/sigil_builder.py +7 -2
  15. core/sigil_resolver.py +35 -4
  16. core/system.py +250 -1
  17. core/tasks.py +92 -40
  18. core/temp_passwords.py +181 -0
  19. core/test_system_info.py +62 -2
  20. core/tests.py +169 -3
  21. core/user_data.py +51 -8
  22. core/views.py +371 -20
  23. nodes/admin.py +453 -8
  24. nodes/backends.py +21 -6
  25. nodes/dns.py +203 -0
  26. nodes/feature_checks.py +133 -0
  27. nodes/models.py +374 -31
  28. nodes/reports.py +411 -0
  29. nodes/tests.py +677 -38
  30. nodes/utils.py +32 -0
  31. nodes/views.py +14 -0
  32. ocpp/admin.py +278 -15
  33. ocpp/consumers.py +517 -16
  34. ocpp/evcs_discovery.py +158 -0
  35. ocpp/models.py +237 -4
  36. ocpp/reference_utils.py +42 -0
  37. ocpp/simulator.py +321 -22
  38. ocpp/store.py +110 -2
  39. ocpp/test_rfid.py +169 -7
  40. ocpp/tests.py +819 -6
  41. ocpp/transactions_io.py +17 -3
  42. ocpp/views.py +233 -19
  43. pages/admin.py +144 -4
  44. pages/context_processors.py +21 -7
  45. pages/defaults.py +13 -0
  46. pages/forms.py +38 -0
  47. pages/models.py +189 -15
  48. pages/tests.py +281 -8
  49. pages/urls.py +4 -0
  50. pages/views.py +137 -21
  51. arthexis-0.1.10.dist-info/RECORD +0 -95
  52. {arthexis-0.1.10.dist-info → arthexis-0.1.12.dist-info}/WHEEL +0 -0
  53. {arthexis-0.1.10.dist-info → arthexis-0.1.12.dist-info}/licenses/LICENSE +0 -0
  54. {arthexis-0.1.10.dist-info → arthexis-0.1.12.dist-info}/top_level.txt +0 -0
@@ -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) or hasattr(
43
- view_func, "login_url"
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
- continue
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 app_name == "man":
56
- module.menu = "Manuals"
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
- favicon_url = _DEFAULT_FAVICON
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
- Landing.objects.get_or_create(
122
- module=self,
123
- path=f"{self.path}{prefix}{str(pattern.pattern)}",
124
- defaults={
125
- "label": getattr(
126
- callback,
127
- "landing_label",
128
- callback.__name__.replace("_", " ").title(),
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
- natural_key.dependencies = ["nodes.NodeRole", "pages.Module"]
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