arthexis 0.1.12__py3-none-any.whl → 0.1.14__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 (107) hide show
  1. {arthexis-0.1.12.dist-info → arthexis-0.1.14.dist-info}/METADATA +222 -221
  2. arthexis-0.1.14.dist-info/RECORD +109 -0
  3. {arthexis-0.1.12.dist-info → arthexis-0.1.14.dist-info}/licenses/LICENSE +674 -674
  4. config/__init__.py +5 -5
  5. config/active_app.py +15 -15
  6. config/asgi.py +43 -29
  7. config/auth_app.py +7 -7
  8. config/celery.py +32 -25
  9. config/context_processors.py +67 -69
  10. config/horologia_app.py +7 -7
  11. config/loadenv.py +11 -11
  12. config/logging.py +59 -48
  13. config/middleware.py +25 -25
  14. config/offline.py +49 -49
  15. config/settings.py +691 -716
  16. config/settings_helpers.py +109 -0
  17. config/urls.py +171 -166
  18. config/wsgi.py +17 -17
  19. core/admin.py +3771 -2772
  20. core/admin_history.py +50 -50
  21. core/admindocs.py +151 -151
  22. core/apps.py +356 -272
  23. core/auto_upgrade.py +57 -57
  24. core/backends.py +265 -236
  25. core/changelog.py +342 -0
  26. core/entity.py +133 -133
  27. core/environment.py +61 -61
  28. core/fields.py +168 -168
  29. core/form_fields.py +75 -0
  30. core/github_helper.py +188 -25
  31. core/github_issues.py +178 -172
  32. core/github_repos.py +72 -0
  33. core/lcd_screen.py +78 -78
  34. core/liveupdate.py +25 -25
  35. core/log_paths.py +100 -100
  36. core/mailer.py +85 -85
  37. core/middleware.py +91 -91
  38. core/models.py +3609 -2672
  39. core/notifications.py +105 -105
  40. core/public_wifi.py +267 -227
  41. core/reference_utils.py +108 -108
  42. core/release.py +721 -350
  43. core/rfid_import_export.py +113 -0
  44. core/sigil_builder.py +149 -149
  45. core/sigil_context.py +20 -20
  46. core/sigil_resolver.py +315 -315
  47. core/system.py +752 -493
  48. core/tasks.py +408 -394
  49. core/temp_passwords.py +181 -181
  50. core/test_system_info.py +186 -139
  51. core/tests.py +2095 -1511
  52. core/tests_liveupdate.py +17 -17
  53. core/urls.py +11 -11
  54. core/user_data.py +641 -633
  55. core/views.py +2175 -1382
  56. core/widgets.py +213 -51
  57. core/workgroup_urls.py +17 -17
  58. core/workgroup_views.py +94 -94
  59. nodes/admin.py +1720 -898
  60. nodes/apps.py +87 -70
  61. nodes/backends.py +160 -160
  62. nodes/dns.py +203 -203
  63. nodes/feature_checks.py +133 -133
  64. nodes/lcd.py +165 -165
  65. nodes/models.py +1737 -1416
  66. nodes/reports.py +411 -411
  67. nodes/rfid_sync.py +195 -0
  68. nodes/signals.py +18 -0
  69. nodes/tasks.py +46 -46
  70. nodes/tests.py +3810 -2497
  71. nodes/urls.py +15 -13
  72. nodes/utils.py +121 -105
  73. nodes/views.py +683 -451
  74. ocpp/admin.py +948 -804
  75. ocpp/apps.py +25 -25
  76. ocpp/consumers.py +1565 -1342
  77. ocpp/evcs.py +844 -931
  78. ocpp/evcs_discovery.py +158 -158
  79. ocpp/models.py +917 -915
  80. ocpp/reference_utils.py +42 -42
  81. ocpp/routing.py +11 -9
  82. ocpp/simulator.py +745 -724
  83. ocpp/status_display.py +26 -0
  84. ocpp/store.py +601 -541
  85. ocpp/tasks.py +31 -31
  86. ocpp/test_export_import.py +130 -130
  87. ocpp/test_rfid.py +913 -702
  88. ocpp/tests.py +4445 -3485
  89. ocpp/transactions_io.py +189 -179
  90. ocpp/urls.py +50 -50
  91. ocpp/views.py +1479 -1151
  92. pages/admin.py +708 -536
  93. pages/apps.py +10 -10
  94. pages/checks.py +40 -40
  95. pages/context_processors.py +127 -119
  96. pages/defaults.py +13 -13
  97. pages/forms.py +198 -169
  98. pages/middleware.py +205 -153
  99. pages/models.py +607 -426
  100. pages/tests.py +2612 -2083
  101. pages/urls.py +25 -25
  102. pages/utils.py +12 -12
  103. pages/views.py +1165 -1120
  104. arthexis-0.1.12.dist-info/RECORD +0 -102
  105. nodes/actions.py +0 -70
  106. {arthexis-0.1.12.dist-info → arthexis-0.1.14.dist-info}/WHEEL +0 -0
  107. {arthexis-0.1.12.dist-info → arthexis-0.1.14.dist-info}/top_level.txt +0 -0
pages/forms.py CHANGED
@@ -1,169 +1,198 @@
1
- """Forms for the pages app."""
2
-
3
- from __future__ import annotations
4
-
5
- from django import forms
6
- from django.contrib.auth import authenticate
7
- from django.contrib.auth.forms import AuthenticationForm
8
- from django.core.exceptions import ValidationError
9
- from django.utils.translation import gettext_lazy as _
10
- from django.views.decorators.debug import sensitive_variables
11
-
12
- from .models import UserStory
13
-
14
-
15
- class AuthenticatorLoginForm(AuthenticationForm):
16
- """Authentication form that supports password or authenticator codes."""
17
-
18
- otp_token = forms.CharField(
19
- label=_("Authenticator code"),
20
- required=False,
21
- widget=forms.TextInput(
22
- attrs={
23
- "autocomplete": "one-time-code",
24
- "inputmode": "numeric",
25
- "pattern": "[0-9]*",
26
- }
27
- ),
28
- )
29
- auth_method = forms.CharField(required=False, widget=forms.HiddenInput(), initial="password")
30
-
31
- error_messages = {
32
- **AuthenticationForm.error_messages,
33
- "invalid_token": _("The authenticator code is invalid or has expired."),
34
- "token_required": _("Enter the code from your authenticator app."),
35
- "password_required": _("Enter your password."),
36
- }
37
-
38
- def __init__(self, request=None, *args, **kwargs):
39
- super().__init__(request=request, *args, **kwargs)
40
- self.fields["password"].required = False
41
- self.fields["otp_token"].strip = True
42
- self.fields["auth_method"].initial = "password"
43
- self.verified_device = None
44
-
45
- def get_invalid_token_error(self) -> ValidationError:
46
- return ValidationError(self.error_messages["invalid_token"], code="invalid_token")
47
-
48
- def get_token_required_error(self) -> ValidationError:
49
- return ValidationError(self.error_messages["token_required"], code="token_required")
50
-
51
- def get_password_required_error(self) -> ValidationError:
52
- return ValidationError(self.error_messages["password_required"], code="password_required")
53
-
54
- @sensitive_variables()
55
- def clean(self):
56
- username = self.cleaned_data.get("username")
57
- method = (self.cleaned_data.get("auth_method") or "password").lower()
58
- if method not in {"password", "otp"}:
59
- method = "password"
60
- self.cleaned_data["auth_method"] = method
61
-
62
- if username is not None:
63
- if method == "otp":
64
- token = (self.cleaned_data.get("otp_token") or "").strip().replace(" ", "")
65
- if not token:
66
- raise self.get_token_required_error()
67
- self.user_cache = authenticate(
68
- self.request,
69
- username=username,
70
- otp_token=token,
71
- )
72
- if self.user_cache is None:
73
- raise self.get_invalid_token_error()
74
- self.cleaned_data["otp_token"] = token
75
- self.verified_device = getattr(self.user_cache, "otp_device", None)
76
- else:
77
- password = self.cleaned_data.get("password")
78
- if not password:
79
- raise self.get_password_required_error()
80
- self.user_cache = authenticate(
81
- self.request, username=username, password=password
82
- )
83
- if self.user_cache is None:
84
- raise self.get_invalid_login_error()
85
- self.confirm_login_allowed(self.user_cache)
86
-
87
- return self.cleaned_data
88
-
89
- def get_verified_device(self):
90
- return self.verified_device
91
-
92
-
93
- class AuthenticatorEnrollmentForm(forms.Form):
94
- """Form used to confirm a pending authenticator enrollment."""
95
-
96
- token = forms.CharField(
97
- label=_("Authenticator code"),
98
- min_length=6,
99
- max_length=8,
100
- widget=forms.TextInput(
101
- attrs={
102
- "autocomplete": "one-time-code",
103
- "inputmode": "numeric",
104
- "pattern": "[0-9]*",
105
- }
106
- ),
107
- )
108
-
109
- error_messages = {
110
- "invalid_token": _("The provided code is invalid or has expired."),
111
- "missing_device": _("Generate a new authenticator secret before confirming it."),
112
- }
113
-
114
- def __init__(self, *args, device=None, **kwargs):
115
- self.device = device
116
- super().__init__(*args, **kwargs)
117
-
118
- def clean_token(self):
119
- token = (self.cleaned_data.get("token") or "").strip().replace(" ", "")
120
- if not token:
121
- raise forms.ValidationError(self.error_messages["invalid_token"], code="invalid_token")
122
- if self.device is None:
123
- raise forms.ValidationError(self.error_messages["missing_device"], code="missing_device")
124
- try:
125
- verified = self.device.verify_token(token)
126
- except Exception:
127
- verified = False
128
- if not verified:
129
- raise forms.ValidationError(self.error_messages["invalid_token"], code="invalid_token")
130
- return token
131
-
132
- def get_verified_device(self):
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]
1
+ """Forms for the pages app."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from django import forms
6
+ from django.contrib.auth import authenticate
7
+ from django.contrib.auth.forms import AuthenticationForm
8
+ from django.core.exceptions import ValidationError
9
+ from django.utils.translation import gettext_lazy as _
10
+ from django.views.decorators.debug import sensitive_variables
11
+
12
+ from core.form_fields import Base64FileField
13
+
14
+ from .models import UserManual, UserStory
15
+
16
+
17
+ class AuthenticatorLoginForm(AuthenticationForm):
18
+ """Authentication form that supports password or authenticator codes."""
19
+
20
+ otp_token = forms.CharField(
21
+ label=_("Authenticator code"),
22
+ required=False,
23
+ widget=forms.TextInput(
24
+ attrs={
25
+ "autocomplete": "one-time-code",
26
+ "inputmode": "numeric",
27
+ "pattern": "[0-9]*",
28
+ }
29
+ ),
30
+ )
31
+ auth_method = forms.CharField(required=False, widget=forms.HiddenInput(), initial="password")
32
+
33
+ error_messages = {
34
+ **AuthenticationForm.error_messages,
35
+ "invalid_token": _("The authenticator code is invalid or has expired."),
36
+ "token_required": _("Enter the code from your authenticator app."),
37
+ "password_required": _("Enter your password."),
38
+ }
39
+
40
+ def __init__(self, request=None, *args, **kwargs):
41
+ super().__init__(request=request, *args, **kwargs)
42
+ self.fields["password"].required = False
43
+ self.fields["otp_token"].strip = True
44
+ self.fields["auth_method"].initial = "password"
45
+ self.verified_device = None
46
+
47
+ def get_invalid_token_error(self) -> ValidationError:
48
+ return ValidationError(self.error_messages["invalid_token"], code="invalid_token")
49
+
50
+ def get_token_required_error(self) -> ValidationError:
51
+ return ValidationError(self.error_messages["token_required"], code="token_required")
52
+
53
+ def get_password_required_error(self) -> ValidationError:
54
+ return ValidationError(self.error_messages["password_required"], code="password_required")
55
+
56
+ @sensitive_variables()
57
+ def clean(self):
58
+ username = self.cleaned_data.get("username")
59
+ method = (self.cleaned_data.get("auth_method") or "password").lower()
60
+ if method not in {"password", "otp"}:
61
+ method = "password"
62
+ self.cleaned_data["auth_method"] = method
63
+
64
+ if username is not None:
65
+ if method == "otp":
66
+ token = (self.cleaned_data.get("otp_token") or "").strip().replace(" ", "")
67
+ if not token:
68
+ raise self.get_token_required_error()
69
+ self.user_cache = authenticate(
70
+ self.request,
71
+ username=username,
72
+ otp_token=token,
73
+ )
74
+ if self.user_cache is None:
75
+ raise self.get_invalid_token_error()
76
+ self.cleaned_data["otp_token"] = token
77
+ self.verified_device = getattr(self.user_cache, "otp_device", None)
78
+ else:
79
+ password = self.cleaned_data.get("password")
80
+ if not password:
81
+ raise self.get_password_required_error()
82
+ self.user_cache = authenticate(
83
+ self.request, username=username, password=password
84
+ )
85
+ if self.user_cache is None:
86
+ raise self.get_invalid_login_error()
87
+ self.confirm_login_allowed(self.user_cache)
88
+
89
+ return self.cleaned_data
90
+
91
+ def get_verified_device(self):
92
+ return self.verified_device
93
+
94
+
95
+ class AuthenticatorEnrollmentForm(forms.Form):
96
+ """Form used to confirm a pending authenticator enrollment."""
97
+
98
+ token = forms.CharField(
99
+ label=_("Authenticator code"),
100
+ min_length=6,
101
+ max_length=8,
102
+ widget=forms.TextInput(
103
+ attrs={
104
+ "autocomplete": "one-time-code",
105
+ "inputmode": "numeric",
106
+ "pattern": "[0-9]*",
107
+ }
108
+ ),
109
+ )
110
+
111
+ error_messages = {
112
+ "invalid_token": _("The provided code is invalid or has expired."),
113
+ "missing_device": _("Generate a new authenticator secret before confirming it."),
114
+ }
115
+
116
+ def __init__(self, *args, device=None, **kwargs):
117
+ self.device = device
118
+ super().__init__(*args, **kwargs)
119
+
120
+ def clean_token(self):
121
+ token = (self.cleaned_data.get("token") or "").strip().replace(" ", "")
122
+ if not token:
123
+ raise forms.ValidationError(self.error_messages["invalid_token"], code="invalid_token")
124
+ if self.device is None:
125
+ raise forms.ValidationError(self.error_messages["missing_device"], code="missing_device")
126
+ try:
127
+ verified = self.device.verify_token(token)
128
+ except Exception:
129
+ verified = False
130
+ if not verified:
131
+ raise forms.ValidationError(self.error_messages["invalid_token"], code="invalid_token")
132
+ return token
133
+
134
+ def get_verified_device(self):
135
+ return self.device
136
+
137
+
138
+ _manual_pdf_field = UserManual._meta.get_field("content_pdf")
139
+
140
+
141
+ class UserManualAdminForm(forms.ModelForm):
142
+ content_pdf = Base64FileField(
143
+ label=_manual_pdf_field.verbose_name,
144
+ help_text=_manual_pdf_field.help_text,
145
+ required=not _manual_pdf_field.blank,
146
+ content_type="application/pdf",
147
+ download_name="manual.pdf",
148
+ )
149
+
150
+ class Meta:
151
+ model = UserManual
152
+ fields = "__all__"
153
+
154
+ def __init__(self, *args, **kwargs):
155
+ super().__init__(*args, **kwargs)
156
+ instance = getattr(self, "instance", None)
157
+ slug = getattr(instance, "slug", "")
158
+ if slug:
159
+ self.fields["content_pdf"].widget.download_name = f"{slug}.pdf"
160
+ self.fields["content_pdf"].widget.attrs.setdefault(
161
+ "accept", "application/pdf"
162
+ )
163
+
164
+
165
+ class UserStoryForm(forms.ModelForm):
166
+ class Meta:
167
+ model = UserStory
168
+ fields = ("name", "rating", "comments", "take_screenshot", "path")
169
+ widgets = {
170
+ "path": forms.HiddenInput(),
171
+ "comments": forms.Textarea(attrs={"rows": 4, "maxlength": 400}),
172
+ }
173
+
174
+ def __init__(self, *args, **kwargs):
175
+ super().__init__(*args, **kwargs)
176
+ self.fields["name"].required = False
177
+ self.fields["name"].widget.attrs.update(
178
+ {
179
+ "maxlength": 40,
180
+ "placeholder": _("Name, email or pseudonym"),
181
+ }
182
+ )
183
+ self.fields["take_screenshot"].initial = True
184
+ self.fields["rating"].widget = forms.RadioSelect(
185
+ choices=[(i, str(i)) for i in range(1, 6)]
186
+ )
187
+
188
+ def clean_comments(self):
189
+ comments = (self.cleaned_data.get("comments") or "").strip()
190
+ if len(comments) > 400:
191
+ raise forms.ValidationError(
192
+ _("Feedback must be 400 characters or fewer."), code="max_length"
193
+ )
194
+ return comments
195
+
196
+ def clean_name(self):
197
+ name = (self.cleaned_data.get("name") or "").strip()
198
+ return name[:40]