arthexis 0.1.3__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.3.dist-info/METADATA +126 -0
- arthexis-0.1.3.dist-info/RECORD +73 -0
- arthexis-0.1.3.dist-info/WHEEL +5 -0
- arthexis-0.1.3.dist-info/licenses/LICENSE +21 -0
- arthexis-0.1.3.dist-info/top_level.txt +5 -0
- config/__init__.py +6 -0
- config/active_app.py +15 -0
- config/asgi.py +29 -0
- config/auth_app.py +8 -0
- config/celery.py +19 -0
- config/context_processors.py +68 -0
- config/loadenv.py +11 -0
- config/logging.py +43 -0
- config/middleware.py +25 -0
- config/offline.py +47 -0
- config/settings.py +374 -0
- config/urls.py +91 -0
- config/wsgi.py +17 -0
- core/__init__.py +0 -0
- core/admin.py +830 -0
- core/apps.py +67 -0
- core/backends.py +82 -0
- core/entity.py +97 -0
- core/environment.py +43 -0
- core/fields.py +70 -0
- core/lcd_screen.py +77 -0
- core/middleware.py +34 -0
- core/models.py +1277 -0
- core/notifications.py +95 -0
- core/release.py +451 -0
- core/system.py +111 -0
- core/tasks.py +100 -0
- core/tests.py +483 -0
- core/urls.py +11 -0
- core/user_data.py +333 -0
- core/views.py +431 -0
- nodes/__init__.py +0 -0
- nodes/actions.py +72 -0
- nodes/admin.py +347 -0
- nodes/apps.py +76 -0
- nodes/lcd.py +151 -0
- nodes/models.py +577 -0
- nodes/tasks.py +50 -0
- nodes/tests.py +1072 -0
- nodes/urls.py +13 -0
- nodes/utils.py +62 -0
- nodes/views.py +262 -0
- ocpp/__init__.py +0 -0
- ocpp/admin.py +392 -0
- ocpp/apps.py +24 -0
- ocpp/consumers.py +267 -0
- ocpp/evcs.py +911 -0
- ocpp/models.py +300 -0
- ocpp/routing.py +9 -0
- ocpp/simulator.py +357 -0
- ocpp/store.py +175 -0
- ocpp/tasks.py +27 -0
- ocpp/test_export_import.py +129 -0
- ocpp/test_rfid.py +345 -0
- ocpp/tests.py +1229 -0
- ocpp/transactions_io.py +119 -0
- ocpp/urls.py +17 -0
- ocpp/views.py +359 -0
- pages/__init__.py +0 -0
- pages/admin.py +231 -0
- pages/apps.py +10 -0
- pages/checks.py +41 -0
- pages/context_processors.py +72 -0
- pages/models.py +224 -0
- pages/tests.py +628 -0
- pages/urls.py +17 -0
- pages/utils.py +13 -0
- pages/views.py +191 -0
core/admin.py
ADDED
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
from django import forms
|
|
2
|
+
from django.contrib import admin
|
|
3
|
+
from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
|
|
4
|
+
from django.urls import path, reverse
|
|
5
|
+
from django.shortcuts import redirect, render
|
|
6
|
+
from django.http import JsonResponse
|
|
7
|
+
from django.template.response import TemplateResponse
|
|
8
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
9
|
+
from django.core.exceptions import ValidationError
|
|
10
|
+
from django.contrib import messages
|
|
11
|
+
from django.contrib.auth import get_user_model
|
|
12
|
+
from django.contrib.auth.admin import (
|
|
13
|
+
GroupAdmin as DjangoGroupAdmin,
|
|
14
|
+
UserAdmin as DjangoUserAdmin,
|
|
15
|
+
)
|
|
16
|
+
from import_export import resources, fields
|
|
17
|
+
from import_export.admin import ImportExportModelAdmin
|
|
18
|
+
from import_export.widgets import ForeignKeyWidget
|
|
19
|
+
from django.contrib.auth.models import Group
|
|
20
|
+
from django.utils.html import format_html
|
|
21
|
+
import json
|
|
22
|
+
import uuid
|
|
23
|
+
from django_object_actions import DjangoObjectActions
|
|
24
|
+
from .user_data import UserDatumAdminMixin
|
|
25
|
+
from .models import (
|
|
26
|
+
User,
|
|
27
|
+
EnergyAccount,
|
|
28
|
+
ElectricVehicle,
|
|
29
|
+
EnergyCredit,
|
|
30
|
+
Address,
|
|
31
|
+
Product,
|
|
32
|
+
Subscription,
|
|
33
|
+
Brand,
|
|
34
|
+
WMICode,
|
|
35
|
+
EVModel,
|
|
36
|
+
RFID,
|
|
37
|
+
Reference,
|
|
38
|
+
OdooProfile,
|
|
39
|
+
FediverseProfile,
|
|
40
|
+
EmailInbox,
|
|
41
|
+
Package,
|
|
42
|
+
PackageRelease,
|
|
43
|
+
ReleaseManager,
|
|
44
|
+
SecurityGroup,
|
|
45
|
+
)
|
|
46
|
+
from .user_data import UserDatumAdminMixin
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
admin.site.unregister(Group)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@admin.register(Reference)
|
|
53
|
+
class ReferenceAdmin(admin.ModelAdmin):
|
|
54
|
+
list_display = (
|
|
55
|
+
"alt_text",
|
|
56
|
+
"content_type",
|
|
57
|
+
"include_in_footer",
|
|
58
|
+
"footer_visibility",
|
|
59
|
+
"author",
|
|
60
|
+
"transaction_uuid",
|
|
61
|
+
)
|
|
62
|
+
readonly_fields = ("uses", "qr_code", "author")
|
|
63
|
+
fields = (
|
|
64
|
+
"alt_text",
|
|
65
|
+
"content_type",
|
|
66
|
+
"value",
|
|
67
|
+
"file",
|
|
68
|
+
"method",
|
|
69
|
+
"include_in_footer",
|
|
70
|
+
"footer_visibility",
|
|
71
|
+
"transaction_uuid",
|
|
72
|
+
"author",
|
|
73
|
+
"uses",
|
|
74
|
+
"qr_code",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def get_readonly_fields(self, request, obj=None):
|
|
78
|
+
ro = list(super().get_readonly_fields(request, obj))
|
|
79
|
+
if obj:
|
|
80
|
+
ro.append("transaction_uuid")
|
|
81
|
+
return ro
|
|
82
|
+
|
|
83
|
+
def get_urls(self):
|
|
84
|
+
urls = super().get_urls()
|
|
85
|
+
custom = [
|
|
86
|
+
path(
|
|
87
|
+
"bulk/",
|
|
88
|
+
self.admin_site.admin_view(csrf_exempt(self.bulk_create)),
|
|
89
|
+
name="core_reference_bulk",
|
|
90
|
+
),
|
|
91
|
+
]
|
|
92
|
+
return custom + urls
|
|
93
|
+
|
|
94
|
+
def bulk_create(self, request):
|
|
95
|
+
if request.method != "POST":
|
|
96
|
+
return JsonResponse({"error": "POST required"}, status=405)
|
|
97
|
+
try:
|
|
98
|
+
payload = json.loads(request.body or "{}")
|
|
99
|
+
except json.JSONDecodeError:
|
|
100
|
+
return JsonResponse({"error": "Invalid JSON"}, status=400)
|
|
101
|
+
refs = payload.get("references", [])
|
|
102
|
+
transaction_uuid = payload.get("transaction_uuid") or uuid.uuid4()
|
|
103
|
+
created_ids = []
|
|
104
|
+
for data in refs:
|
|
105
|
+
ref = Reference.objects.create(
|
|
106
|
+
alt_text=data.get("alt_text", ""),
|
|
107
|
+
value=data.get("value", ""),
|
|
108
|
+
transaction_uuid=transaction_uuid,
|
|
109
|
+
author=request.user if request.user.is_authenticated else None,
|
|
110
|
+
)
|
|
111
|
+
created_ids.append(ref.id)
|
|
112
|
+
return JsonResponse(
|
|
113
|
+
{"transaction_uuid": str(transaction_uuid), "ids": created_ids}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def qr_code(self, obj):
|
|
117
|
+
if obj.image:
|
|
118
|
+
return format_html(
|
|
119
|
+
'<img src="{}" alt="{}" style="height:200px;"/>',
|
|
120
|
+
obj.image.url,
|
|
121
|
+
obj.alt_text,
|
|
122
|
+
)
|
|
123
|
+
return ""
|
|
124
|
+
|
|
125
|
+
qr_code.short_description = "QR Code"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@admin.register(ReleaseManager)
|
|
129
|
+
class ReleaseManagerAdmin(admin.ModelAdmin):
|
|
130
|
+
list_display = ("user", "pypi_username", "pypi_url")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@admin.register(Package)
|
|
134
|
+
class PackageAdmin(DjangoObjectActions, admin.ModelAdmin):
|
|
135
|
+
list_display = ("name", "description", "homepage_url", "release_manager")
|
|
136
|
+
actions = ["prepare_next_release"]
|
|
137
|
+
change_actions = ["prepare_next_release_action"]
|
|
138
|
+
|
|
139
|
+
def _prepare(self, request, package):
|
|
140
|
+
from pathlib import Path
|
|
141
|
+
from packaging.version import Version
|
|
142
|
+
|
|
143
|
+
ver_file = Path("VERSION")
|
|
144
|
+
repo_version = ver_file.read_text().strip() if ver_file.exists() else "0.0.0"
|
|
145
|
+
versions = [Version(repo_version)]
|
|
146
|
+
versions += [
|
|
147
|
+
Version(r.version)
|
|
148
|
+
for r in PackageRelease.all_objects.filter(package=package)
|
|
149
|
+
]
|
|
150
|
+
highest = max(versions)
|
|
151
|
+
next_version = f"{highest.major}.{highest.minor}.{highest.micro + 1}"
|
|
152
|
+
release, _created = PackageRelease.all_objects.update_or_create(
|
|
153
|
+
package=package,
|
|
154
|
+
version=next_version,
|
|
155
|
+
defaults={
|
|
156
|
+
"release_manager": package.release_manager,
|
|
157
|
+
"is_deleted": False,
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
return redirect(
|
|
161
|
+
reverse("admin:core_packagerelease_change", args=[release.pk])
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
@admin.action(description="Prepare next Release")
|
|
165
|
+
def prepare_next_release(self, request, queryset):
|
|
166
|
+
if queryset.count() != 1:
|
|
167
|
+
self.message_user(
|
|
168
|
+
request, "Select exactly one package", messages.ERROR
|
|
169
|
+
)
|
|
170
|
+
return
|
|
171
|
+
return self._prepare(request, queryset.first())
|
|
172
|
+
|
|
173
|
+
def prepare_next_release_action(self, request, obj):
|
|
174
|
+
return self._prepare(request, obj)
|
|
175
|
+
|
|
176
|
+
prepare_next_release_action.label = "Prepare next Release"
|
|
177
|
+
prepare_next_release_action.short_description = "Prepare next release"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class SecurityGroupAdminForm(forms.ModelForm):
|
|
181
|
+
users = forms.ModelMultipleChoiceField(
|
|
182
|
+
queryset=get_user_model().objects.all(),
|
|
183
|
+
required=False,
|
|
184
|
+
widget=admin.widgets.FilteredSelectMultiple("users", False),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
class Meta:
|
|
188
|
+
model = SecurityGroup
|
|
189
|
+
fields = "__all__"
|
|
190
|
+
|
|
191
|
+
def __init__(self, *args, **kwargs):
|
|
192
|
+
super().__init__(*args, **kwargs)
|
|
193
|
+
if self.instance.pk:
|
|
194
|
+
self.fields["users"].initial = self.instance.user_set.all()
|
|
195
|
+
|
|
196
|
+
def save(self, commit=True):
|
|
197
|
+
instance = super().save(commit)
|
|
198
|
+
users = self.cleaned_data.get("users")
|
|
199
|
+
if commit:
|
|
200
|
+
instance.user_set.set(users)
|
|
201
|
+
else:
|
|
202
|
+
self.save_m2m = lambda: instance.user_set.set(users)
|
|
203
|
+
return instance
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@admin.register(SecurityGroup)
|
|
207
|
+
class SecurityGroupAdmin(DjangoGroupAdmin):
|
|
208
|
+
form = SecurityGroupAdminForm
|
|
209
|
+
fieldsets = ((None, {"fields": ("name", "parent", "users", "permissions")}),)
|
|
210
|
+
filter_horizontal = ("permissions",)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class EnergyAccountRFIDForm(forms.ModelForm):
|
|
214
|
+
"""Form for assigning existing RFIDs to an energy account."""
|
|
215
|
+
|
|
216
|
+
class Meta:
|
|
217
|
+
model = EnergyAccount.rfids.through
|
|
218
|
+
fields = ["rfid"]
|
|
219
|
+
|
|
220
|
+
def clean_rfid(self):
|
|
221
|
+
rfid = self.cleaned_data["rfid"]
|
|
222
|
+
if rfid.energy_accounts.exclude(pk=self.instance.energyaccount_id).exists():
|
|
223
|
+
raise forms.ValidationError("RFID is already assigned to another energy account")
|
|
224
|
+
return rfid
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class EnergyAccountRFIDInline(admin.TabularInline):
|
|
228
|
+
model = EnergyAccount.rfids.through
|
|
229
|
+
form = EnergyAccountRFIDForm
|
|
230
|
+
autocomplete_fields = ["rfid"]
|
|
231
|
+
extra = 0
|
|
232
|
+
verbose_name = "RFID"
|
|
233
|
+
verbose_name_plural = "RFIDs"
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class UserAdmin(DjangoUserAdmin):
|
|
237
|
+
fieldsets = DjangoUserAdmin.fieldsets + (
|
|
238
|
+
("Contact", {"fields": ("phone_number", "address", "has_charger")}),
|
|
239
|
+
)
|
|
240
|
+
add_fieldsets = DjangoUserAdmin.add_fieldsets + (
|
|
241
|
+
("Contact", {"fields": ("phone_number", "address", "has_charger")}),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@admin.register(Address)
|
|
246
|
+
class AddressAdmin(UserDatumAdminMixin, admin.ModelAdmin):
|
|
247
|
+
change_form_template = "admin/user_datum_change_form.html"
|
|
248
|
+
list_display = ("street", "number", "municipality", "state", "postal_code")
|
|
249
|
+
search_fields = ("street", "municipality", "postal_code")
|
|
250
|
+
|
|
251
|
+
def save_model(self, request, obj, form, change):
|
|
252
|
+
if "_saveacopy" in request.POST:
|
|
253
|
+
obj.pk = None
|
|
254
|
+
super().save_model(request, obj, form, False)
|
|
255
|
+
else:
|
|
256
|
+
super().save_model(request, obj, form, change)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class OdooProfileAdminForm(forms.ModelForm):
|
|
260
|
+
"""Admin form for :class:`core.models.OdooProfile` with hidden password."""
|
|
261
|
+
|
|
262
|
+
password = forms.CharField(
|
|
263
|
+
widget=forms.PasswordInput(render_value=True),
|
|
264
|
+
required=False,
|
|
265
|
+
help_text="Leave blank to keep the current password.",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
class Meta:
|
|
269
|
+
model = OdooProfile
|
|
270
|
+
fields = "__all__"
|
|
271
|
+
|
|
272
|
+
def __init__(self, *args, **kwargs):
|
|
273
|
+
super().__init__(*args, **kwargs)
|
|
274
|
+
if self.instance.pk:
|
|
275
|
+
self.fields["password"].initial = ""
|
|
276
|
+
self.initial["password"] = ""
|
|
277
|
+
else:
|
|
278
|
+
self.fields["password"].required = True
|
|
279
|
+
|
|
280
|
+
def clean_password(self):
|
|
281
|
+
pwd = self.cleaned_data.get("password")
|
|
282
|
+
if not pwd and self.instance.pk:
|
|
283
|
+
return self.instance.password
|
|
284
|
+
return pwd
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@admin.register(OdooProfile)
|
|
288
|
+
class OdooProfileAdmin(UserDatumAdminMixin, admin.ModelAdmin):
|
|
289
|
+
change_form_template = "admin/user_datum_change_form.html"
|
|
290
|
+
form = OdooProfileAdminForm
|
|
291
|
+
list_display = ("user", "host", "database", "verified_on")
|
|
292
|
+
readonly_fields = ("verified_on", "odoo_uid", "name", "email")
|
|
293
|
+
actions = ["verify_credentials"]
|
|
294
|
+
fieldsets = (
|
|
295
|
+
(None, {"fields": ("user", "host", "database", "username", "password")}),
|
|
296
|
+
("Odoo", {"fields": ("verified_on", "odoo_uid", "name", "email")}),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
@admin.action(description="Test selected credentials")
|
|
300
|
+
def verify_credentials(self, request, queryset):
|
|
301
|
+
for profile in queryset:
|
|
302
|
+
try:
|
|
303
|
+
profile.verify()
|
|
304
|
+
self.message_user(request, f"{profile.user} verified")
|
|
305
|
+
except Exception as exc: # pragma: no cover - admin feedback
|
|
306
|
+
self.message_user(
|
|
307
|
+
request, f"{profile.user}: {exc}", level=messages.ERROR
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class FediverseProfileAdminForm(forms.ModelForm):
|
|
312
|
+
"""Admin form for :class:`core.models.FediverseProfile` with hidden token."""
|
|
313
|
+
|
|
314
|
+
access_token = forms.CharField(
|
|
315
|
+
widget=forms.PasswordInput(render_value=True),
|
|
316
|
+
required=False,
|
|
317
|
+
help_text="Leave blank to keep the current token.",
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
class Meta:
|
|
321
|
+
model = FediverseProfile
|
|
322
|
+
fields = "__all__"
|
|
323
|
+
|
|
324
|
+
def __init__(self, *args, **kwargs):
|
|
325
|
+
super().__init__(*args, **kwargs)
|
|
326
|
+
if self.instance.pk:
|
|
327
|
+
self.fields["access_token"].initial = ""
|
|
328
|
+
self.initial["access_token"] = ""
|
|
329
|
+
|
|
330
|
+
def clean_access_token(self):
|
|
331
|
+
token = self.cleaned_data.get("access_token")
|
|
332
|
+
if not token and self.instance.pk:
|
|
333
|
+
return self.instance.access_token
|
|
334
|
+
return token
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@admin.register(FediverseProfile)
|
|
338
|
+
class FediverseProfileAdmin(admin.ModelAdmin):
|
|
339
|
+
form = FediverseProfileAdminForm
|
|
340
|
+
list_display = ("user", "service", "host", "handle", "verified_on")
|
|
341
|
+
readonly_fields = ("verified_on",)
|
|
342
|
+
actions = ["test_connection"]
|
|
343
|
+
fieldsets = (
|
|
344
|
+
(
|
|
345
|
+
None,
|
|
346
|
+
{
|
|
347
|
+
"fields": (
|
|
348
|
+
"user",
|
|
349
|
+
"service",
|
|
350
|
+
"host",
|
|
351
|
+
"handle",
|
|
352
|
+
"access_token",
|
|
353
|
+
"verified_on",
|
|
354
|
+
)
|
|
355
|
+
},
|
|
356
|
+
),
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
@admin.action(description="Test selected profiles")
|
|
360
|
+
def test_connection(self, request, queryset):
|
|
361
|
+
for profile in queryset:
|
|
362
|
+
try:
|
|
363
|
+
profile.test_connection()
|
|
364
|
+
self.message_user(request, f"{profile} connection successful")
|
|
365
|
+
except Exception as exc: # pragma: no cover - admin feedback
|
|
366
|
+
self.message_user(request, f"{profile}: {exc}", level=messages.ERROR)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class EmailInboxAdminForm(forms.ModelForm):
|
|
370
|
+
"""Admin form for :class:`core.models.EmailInbox` with hidden password."""
|
|
371
|
+
|
|
372
|
+
password = forms.CharField(
|
|
373
|
+
widget=forms.PasswordInput(render_value=True),
|
|
374
|
+
required=False,
|
|
375
|
+
help_text="Leave blank to keep the current password.",
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
class Meta:
|
|
379
|
+
model = EmailInbox
|
|
380
|
+
fields = "__all__"
|
|
381
|
+
|
|
382
|
+
def __init__(self, *args, **kwargs):
|
|
383
|
+
super().__init__(*args, **kwargs)
|
|
384
|
+
if self.instance.pk:
|
|
385
|
+
self.fields["password"].initial = ""
|
|
386
|
+
self.initial["password"] = ""
|
|
387
|
+
else:
|
|
388
|
+
self.fields["password"].required = True
|
|
389
|
+
|
|
390
|
+
def clean_password(self):
|
|
391
|
+
pwd = self.cleaned_data.get("password")
|
|
392
|
+
if not pwd and self.instance.pk:
|
|
393
|
+
return self.instance.password
|
|
394
|
+
return pwd
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class EmailSearchForm(forms.Form):
|
|
398
|
+
subject = forms.CharField(
|
|
399
|
+
required=False, widget=forms.TextInput(attrs={"style": "width: 40em;"})
|
|
400
|
+
)
|
|
401
|
+
from_address = forms.CharField(
|
|
402
|
+
label="From",
|
|
403
|
+
required=False,
|
|
404
|
+
widget=forms.TextInput(attrs={"style": "width: 40em;"}),
|
|
405
|
+
)
|
|
406
|
+
body = forms.CharField(
|
|
407
|
+
required=False,
|
|
408
|
+
widget=forms.Textarea(attrs={"style": "width: 40em; height: 10em;"}),
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
@admin.register(EmailInbox)
|
|
413
|
+
class EmailInboxAdmin(admin.ModelAdmin):
|
|
414
|
+
form = EmailInboxAdminForm
|
|
415
|
+
list_display = ("user", "username", "host", "protocol")
|
|
416
|
+
actions = ["test_connection", "search_inbox"]
|
|
417
|
+
fieldsets = (
|
|
418
|
+
(
|
|
419
|
+
None,
|
|
420
|
+
{
|
|
421
|
+
"fields": (
|
|
422
|
+
"user",
|
|
423
|
+
"username",
|
|
424
|
+
"host",
|
|
425
|
+
"port",
|
|
426
|
+
"password",
|
|
427
|
+
"protocol",
|
|
428
|
+
"use_ssl",
|
|
429
|
+
)
|
|
430
|
+
},
|
|
431
|
+
),
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
@admin.action(description="Test selected inboxes")
|
|
435
|
+
def test_connection(self, request, queryset):
|
|
436
|
+
for inbox in queryset:
|
|
437
|
+
try:
|
|
438
|
+
inbox.test_connection()
|
|
439
|
+
self.message_user(request, f"{inbox} connection successful")
|
|
440
|
+
except Exception as exc: # pragma: no cover - admin feedback
|
|
441
|
+
self.message_user(request, f"{inbox}: {exc}", level=messages.ERROR)
|
|
442
|
+
|
|
443
|
+
@admin.action(description="Search selected inbox")
|
|
444
|
+
def search_inbox(self, request, queryset):
|
|
445
|
+
if queryset.count() != 1:
|
|
446
|
+
self.message_user(
|
|
447
|
+
request, "Please select exactly one inbox.", level=messages.ERROR
|
|
448
|
+
)
|
|
449
|
+
return None
|
|
450
|
+
inbox = queryset.first()
|
|
451
|
+
if request.POST.get("apply"):
|
|
452
|
+
form = EmailSearchForm(request.POST)
|
|
453
|
+
if form.is_valid():
|
|
454
|
+
results = inbox.search_messages(
|
|
455
|
+
subject=form.cleaned_data["subject"],
|
|
456
|
+
from_address=form.cleaned_data["from_address"],
|
|
457
|
+
body=form.cleaned_data["body"],
|
|
458
|
+
)
|
|
459
|
+
context = {
|
|
460
|
+
"form": form,
|
|
461
|
+
"results": results,
|
|
462
|
+
"queryset": queryset,
|
|
463
|
+
"action": "search_inbox",
|
|
464
|
+
}
|
|
465
|
+
return TemplateResponse(
|
|
466
|
+
request, "admin/core/emailinbox/search.html", context
|
|
467
|
+
)
|
|
468
|
+
else:
|
|
469
|
+
form = EmailSearchForm()
|
|
470
|
+
context = {
|
|
471
|
+
"form": form,
|
|
472
|
+
"queryset": queryset,
|
|
473
|
+
"action": "search_inbox",
|
|
474
|
+
}
|
|
475
|
+
return TemplateResponse(request, "admin/core/emailinbox/search.html", context)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
class EnergyCreditInline(admin.TabularInline):
|
|
479
|
+
model = EnergyCredit
|
|
480
|
+
fields = ("amount_kw", "created_by", "created_on")
|
|
481
|
+
readonly_fields = ("created_by", "created_on")
|
|
482
|
+
extra = 0
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
@admin.register(EnergyAccount)
|
|
486
|
+
class EnergyAccountAdmin(admin.ModelAdmin):
|
|
487
|
+
change_list_template = "admin/core/energyaccount/change_list.html"
|
|
488
|
+
list_display = (
|
|
489
|
+
"name",
|
|
490
|
+
"user",
|
|
491
|
+
"credits_kw",
|
|
492
|
+
"total_kw_spent",
|
|
493
|
+
"balance_kw",
|
|
494
|
+
"service_account",
|
|
495
|
+
"authorized",
|
|
496
|
+
)
|
|
497
|
+
search_fields = (
|
|
498
|
+
"name",
|
|
499
|
+
"user__username",
|
|
500
|
+
"user__email",
|
|
501
|
+
"user__first_name",
|
|
502
|
+
"user__last_name",
|
|
503
|
+
)
|
|
504
|
+
readonly_fields = (
|
|
505
|
+
"credits_kw",
|
|
506
|
+
"total_kw_spent",
|
|
507
|
+
"balance_kw",
|
|
508
|
+
"authorized",
|
|
509
|
+
)
|
|
510
|
+
inlines = [EnergyAccountRFIDInline, EnergyCreditInline]
|
|
511
|
+
actions = ["test_authorization"]
|
|
512
|
+
fieldsets = (
|
|
513
|
+
(
|
|
514
|
+
None,
|
|
515
|
+
{
|
|
516
|
+
"fields": (
|
|
517
|
+
"name",
|
|
518
|
+
"user",
|
|
519
|
+
("service_account", "authorized"),
|
|
520
|
+
("credits_kw", "total_kw_spent", "balance_kw"),
|
|
521
|
+
)
|
|
522
|
+
},
|
|
523
|
+
),
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
def authorized(self, obj):
|
|
527
|
+
return obj.can_authorize()
|
|
528
|
+
|
|
529
|
+
authorized.boolean = True
|
|
530
|
+
authorized.short_description = "Authorized"
|
|
531
|
+
|
|
532
|
+
def test_authorization(self, request, queryset):
|
|
533
|
+
for acc in queryset:
|
|
534
|
+
if acc.can_authorize():
|
|
535
|
+
self.message_user(request, f"{acc.user} authorized")
|
|
536
|
+
else:
|
|
537
|
+
self.message_user(request, f"{acc.user} denied")
|
|
538
|
+
|
|
539
|
+
test_authorization.short_description = "Test authorization"
|
|
540
|
+
|
|
541
|
+
def save_formset(self, request, form, formset, change):
|
|
542
|
+
objs = formset.save(commit=False)
|
|
543
|
+
for obj in objs:
|
|
544
|
+
if isinstance(obj, EnergyCredit) and not obj.created_by:
|
|
545
|
+
obj.created_by = request.user
|
|
546
|
+
obj.save()
|
|
547
|
+
formset.save_m2m()
|
|
548
|
+
|
|
549
|
+
# Onboarding wizard view
|
|
550
|
+
def get_urls(self):
|
|
551
|
+
urls = super().get_urls()
|
|
552
|
+
custom = [
|
|
553
|
+
path(
|
|
554
|
+
"onboard/",
|
|
555
|
+
self.admin_site.admin_view(self.onboard_details),
|
|
556
|
+
name="core_energyaccount_onboard_details",
|
|
557
|
+
),
|
|
558
|
+
]
|
|
559
|
+
return custom + urls
|
|
560
|
+
|
|
561
|
+
def onboard_details(self, request):
|
|
562
|
+
class OnboardForm(forms.Form):
|
|
563
|
+
first_name = forms.CharField(label="First name")
|
|
564
|
+
last_name = forms.CharField(label="Last name")
|
|
565
|
+
rfid = forms.CharField(required=False, label="RFID")
|
|
566
|
+
allow_login = forms.BooleanField(
|
|
567
|
+
required=False, initial=False, label="Allow login"
|
|
568
|
+
)
|
|
569
|
+
vehicle_id = forms.CharField(required=False, label="Electric Vehicle ID")
|
|
570
|
+
|
|
571
|
+
if request.method == "POST":
|
|
572
|
+
form = OnboardForm(request.POST)
|
|
573
|
+
if form.is_valid():
|
|
574
|
+
User = get_user_model()
|
|
575
|
+
first = form.cleaned_data["first_name"]
|
|
576
|
+
last = form.cleaned_data["last_name"]
|
|
577
|
+
allow = form.cleaned_data["allow_login"]
|
|
578
|
+
username = f"{first}.{last}".lower()
|
|
579
|
+
user = User.objects.create_user(
|
|
580
|
+
username=username,
|
|
581
|
+
first_name=first,
|
|
582
|
+
last_name=last,
|
|
583
|
+
is_active=allow,
|
|
584
|
+
)
|
|
585
|
+
account = EnergyAccount.objects.create(user=user, name=username.upper())
|
|
586
|
+
rfid_val = form.cleaned_data["rfid"].upper()
|
|
587
|
+
if rfid_val:
|
|
588
|
+
tag, _ = RFID.objects.get_or_create(rfid=rfid_val)
|
|
589
|
+
account.rfids.add(tag)
|
|
590
|
+
vehicle_vin = form.cleaned_data["vehicle_id"]
|
|
591
|
+
if vehicle_vin:
|
|
592
|
+
ElectricVehicle.objects.create(account=account, vin=vehicle_vin)
|
|
593
|
+
self.message_user(request, "Customer onboarded")
|
|
594
|
+
return redirect("admin:core_energyaccount_changelist")
|
|
595
|
+
else:
|
|
596
|
+
form = OnboardForm()
|
|
597
|
+
|
|
598
|
+
context = self.admin_site.each_context(request)
|
|
599
|
+
context.update({"form": form})
|
|
600
|
+
return render(request, "core/onboard_details.html", context)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
@admin.register(ElectricVehicle)
|
|
604
|
+
class ElectricVehicleAdmin(admin.ModelAdmin):
|
|
605
|
+
list_display = ("vin", "license_plate", "brand", "model", "account")
|
|
606
|
+
fields = ("account", "vin", "license_plate", "brand", "model")
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
@admin.register(EnergyCredit)
|
|
610
|
+
class EnergyCreditAdmin(admin.ModelAdmin):
|
|
611
|
+
list_display = ("account", "amount_kw", "created_by", "created_on")
|
|
612
|
+
readonly_fields = ("created_by", "created_on")
|
|
613
|
+
|
|
614
|
+
def save_model(self, request, obj, form, change):
|
|
615
|
+
if not obj.created_by:
|
|
616
|
+
obj.created_by = request.user
|
|
617
|
+
super().save_model(request, obj, form, change)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
class WMICodeInline(admin.TabularInline):
|
|
621
|
+
model = WMICode
|
|
622
|
+
extra = 0
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
@admin.register(Brand)
|
|
626
|
+
class BrandAdmin(admin.ModelAdmin):
|
|
627
|
+
fields = ("name",)
|
|
628
|
+
list_display = ("name", "wmi_codes_display")
|
|
629
|
+
inlines = [WMICodeInline]
|
|
630
|
+
|
|
631
|
+
def wmi_codes_display(self, obj):
|
|
632
|
+
return ", ".join(obj.wmi_codes.values_list("code", flat=True))
|
|
633
|
+
|
|
634
|
+
wmi_codes_display.short_description = "WMI codes"
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
@admin.register(EVModel)
|
|
638
|
+
class EVModelAdmin(admin.ModelAdmin):
|
|
639
|
+
fields = ("brand", "name")
|
|
640
|
+
list_display = ("name", "brand")
|
|
641
|
+
list_filter = ("brand",)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
admin.site.register(Product)
|
|
645
|
+
admin.site.register(Subscription)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
class RFIDResource(resources.ModelResource):
|
|
649
|
+
reference = fields.Field(
|
|
650
|
+
column_name="reference",
|
|
651
|
+
attribute="reference",
|
|
652
|
+
widget=ForeignKeyWidget(Reference, "value"),
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
class Meta:
|
|
656
|
+
model = RFID
|
|
657
|
+
fields = (
|
|
658
|
+
"label_id",
|
|
659
|
+
"rfid",
|
|
660
|
+
"reference",
|
|
661
|
+
"allowed",
|
|
662
|
+
"color",
|
|
663
|
+
"kind",
|
|
664
|
+
"released",
|
|
665
|
+
"last_seen_on",
|
|
666
|
+
)
|
|
667
|
+
import_id_fields = ("label_id",)
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
class RFIDForm(forms.ModelForm):
|
|
671
|
+
"""RFID admin form with optional reference field."""
|
|
672
|
+
|
|
673
|
+
class Meta:
|
|
674
|
+
model = RFID
|
|
675
|
+
fields = "__all__"
|
|
676
|
+
|
|
677
|
+
def __init__(self, *args, **kwargs):
|
|
678
|
+
super().__init__(*args, **kwargs)
|
|
679
|
+
self.fields["reference"].required = False
|
|
680
|
+
rel = RFID._meta.get_field("reference").remote_field
|
|
681
|
+
widget = self.fields["reference"].widget
|
|
682
|
+
self.fields["reference"].widget = RelatedFieldWidgetWrapper(
|
|
683
|
+
widget,
|
|
684
|
+
rel,
|
|
685
|
+
admin.site,
|
|
686
|
+
can_add_related=True,
|
|
687
|
+
can_change_related=True,
|
|
688
|
+
can_view_related=True,
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
@admin.register(RFID)
|
|
693
|
+
class RFIDAdmin(ImportExportModelAdmin):
|
|
694
|
+
change_list_template = "admin/core/rfid/change_list.html"
|
|
695
|
+
resource_class = RFIDResource
|
|
696
|
+
list_display = (
|
|
697
|
+
"label_id",
|
|
698
|
+
"rfid",
|
|
699
|
+
"color",
|
|
700
|
+
"kind",
|
|
701
|
+
"released",
|
|
702
|
+
"energy_accounts_display",
|
|
703
|
+
"allowed",
|
|
704
|
+
"added_on",
|
|
705
|
+
"last_seen_on",
|
|
706
|
+
)
|
|
707
|
+
list_filter = ("color", "released", "allowed")
|
|
708
|
+
search_fields = ("label_id", "rfid")
|
|
709
|
+
autocomplete_fields = ["energy_accounts"]
|
|
710
|
+
raw_id_fields = ["reference"]
|
|
711
|
+
actions = ["scan_rfids"]
|
|
712
|
+
readonly_fields = ("added_on", "last_seen_on")
|
|
713
|
+
form = RFIDForm
|
|
714
|
+
|
|
715
|
+
def energy_accounts_display(self, obj):
|
|
716
|
+
return ", ".join(str(a) for a in obj.energy_accounts.all())
|
|
717
|
+
|
|
718
|
+
energy_accounts_display.short_description = "Energy Accounts"
|
|
719
|
+
|
|
720
|
+
def scan_rfids(self, request, queryset):
|
|
721
|
+
return redirect("admin:core_rfid_scan")
|
|
722
|
+
|
|
723
|
+
scan_rfids.short_description = "Scan new RFIDs"
|
|
724
|
+
|
|
725
|
+
def get_urls(self):
|
|
726
|
+
urls = super().get_urls()
|
|
727
|
+
custom = [
|
|
728
|
+
path(
|
|
729
|
+
"scan/",
|
|
730
|
+
self.admin_site.admin_view(csrf_exempt(self.scan_view)),
|
|
731
|
+
name="core_rfid_scan",
|
|
732
|
+
),
|
|
733
|
+
path(
|
|
734
|
+
"scan/next/",
|
|
735
|
+
self.admin_site.admin_view(csrf_exempt(self.scan_next)),
|
|
736
|
+
name="core_rfid_scan_next",
|
|
737
|
+
),
|
|
738
|
+
]
|
|
739
|
+
return custom + urls
|
|
740
|
+
|
|
741
|
+
def scan_view(self, request):
|
|
742
|
+
context = self.admin_site.each_context(request)
|
|
743
|
+
context["scan_url"] = reverse("admin:core_rfid_scan_next")
|
|
744
|
+
context["admin_change_url_template"] = reverse(
|
|
745
|
+
"admin:core_rfid_change", args=[0]
|
|
746
|
+
)
|
|
747
|
+
return render(request, "admin/core/rfid/scan.html", context)
|
|
748
|
+
|
|
749
|
+
def scan_next(self, request):
|
|
750
|
+
from ocpp.rfid.scanner import scan_sources
|
|
751
|
+
|
|
752
|
+
result = scan_sources(request)
|
|
753
|
+
status = 500 if result.get("error") else 200
|
|
754
|
+
return JsonResponse(result, status=status)
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
@admin.register(PackageRelease)
|
|
758
|
+
class PackageReleaseAdmin(DjangoObjectActions, admin.ModelAdmin):
|
|
759
|
+
list_display = (
|
|
760
|
+
"version",
|
|
761
|
+
"package",
|
|
762
|
+
"is_current",
|
|
763
|
+
"pypi_url",
|
|
764
|
+
"pr_link",
|
|
765
|
+
"revision_short",
|
|
766
|
+
"published_status",
|
|
767
|
+
)
|
|
768
|
+
list_display_links = ("version",)
|
|
769
|
+
actions = ["publish_release"]
|
|
770
|
+
change_actions = ["publish_release_action"]
|
|
771
|
+
readonly_fields = ("pypi_url", "pr_url", "is_current", "revision")
|
|
772
|
+
fields = (
|
|
773
|
+
"package",
|
|
774
|
+
"release_manager",
|
|
775
|
+
"version",
|
|
776
|
+
"revision",
|
|
777
|
+
"is_current",
|
|
778
|
+
"pypi_url",
|
|
779
|
+
"pr_url",
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
def revision_short(self, obj):
|
|
783
|
+
return obj.revision_short
|
|
784
|
+
|
|
785
|
+
revision_short.short_description = "revision"
|
|
786
|
+
|
|
787
|
+
def _publish_release(self, request, release):
|
|
788
|
+
try:
|
|
789
|
+
release.full_clean()
|
|
790
|
+
except ValidationError as exc:
|
|
791
|
+
self.message_user(request, "; ".join(exc.messages), messages.ERROR)
|
|
792
|
+
return
|
|
793
|
+
return redirect(reverse("release-progress", args=[release.pk, "publish"]))
|
|
794
|
+
|
|
795
|
+
@admin.action(description="Publish selected release(s)")
|
|
796
|
+
def publish_release(self, request, queryset):
|
|
797
|
+
if queryset.count() != 1:
|
|
798
|
+
self.message_user(
|
|
799
|
+
request, "Select exactly one release to publish", messages.ERROR
|
|
800
|
+
)
|
|
801
|
+
return
|
|
802
|
+
return self._publish_release(request, queryset.first())
|
|
803
|
+
|
|
804
|
+
def publish_release_action(self, request, obj):
|
|
805
|
+
return self._publish_release(request, obj)
|
|
806
|
+
|
|
807
|
+
publish_release_action.label = "Publish selected Release"
|
|
808
|
+
publish_release_action.short_description = "Publish this release"
|
|
809
|
+
|
|
810
|
+
@staticmethod
|
|
811
|
+
def _checkbox(value: bool) -> str:
|
|
812
|
+
return format_html(
|
|
813
|
+
'<input type="checkbox"{} disabled>', " checked" if value else ""
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
@admin.display(description="Published")
|
|
817
|
+
def published_status(self, obj):
|
|
818
|
+
return self._checkbox(obj.is_published)
|
|
819
|
+
|
|
820
|
+
@admin.display(description="Is current")
|
|
821
|
+
def is_current(self, obj):
|
|
822
|
+
return self._checkbox(obj.is_current)
|
|
823
|
+
|
|
824
|
+
def pr_link(self, obj):
|
|
825
|
+
if obj.pr_url:
|
|
826
|
+
return format_html('<a href="{0}" target="_blank">{0}</a>', obj.pr_url)
|
|
827
|
+
return ""
|
|
828
|
+
|
|
829
|
+
pr_link.short_description = "PR URL"
|
|
830
|
+
|