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.

Files changed (73) hide show
  1. arthexis-0.1.3.dist-info/METADATA +126 -0
  2. arthexis-0.1.3.dist-info/RECORD +73 -0
  3. arthexis-0.1.3.dist-info/WHEEL +5 -0
  4. arthexis-0.1.3.dist-info/licenses/LICENSE +21 -0
  5. arthexis-0.1.3.dist-info/top_level.txt +5 -0
  6. config/__init__.py +6 -0
  7. config/active_app.py +15 -0
  8. config/asgi.py +29 -0
  9. config/auth_app.py +8 -0
  10. config/celery.py +19 -0
  11. config/context_processors.py +68 -0
  12. config/loadenv.py +11 -0
  13. config/logging.py +43 -0
  14. config/middleware.py +25 -0
  15. config/offline.py +47 -0
  16. config/settings.py +374 -0
  17. config/urls.py +91 -0
  18. config/wsgi.py +17 -0
  19. core/__init__.py +0 -0
  20. core/admin.py +830 -0
  21. core/apps.py +67 -0
  22. core/backends.py +82 -0
  23. core/entity.py +97 -0
  24. core/environment.py +43 -0
  25. core/fields.py +70 -0
  26. core/lcd_screen.py +77 -0
  27. core/middleware.py +34 -0
  28. core/models.py +1277 -0
  29. core/notifications.py +95 -0
  30. core/release.py +451 -0
  31. core/system.py +111 -0
  32. core/tasks.py +100 -0
  33. core/tests.py +483 -0
  34. core/urls.py +11 -0
  35. core/user_data.py +333 -0
  36. core/views.py +431 -0
  37. nodes/__init__.py +0 -0
  38. nodes/actions.py +72 -0
  39. nodes/admin.py +347 -0
  40. nodes/apps.py +76 -0
  41. nodes/lcd.py +151 -0
  42. nodes/models.py +577 -0
  43. nodes/tasks.py +50 -0
  44. nodes/tests.py +1072 -0
  45. nodes/urls.py +13 -0
  46. nodes/utils.py +62 -0
  47. nodes/views.py +262 -0
  48. ocpp/__init__.py +0 -0
  49. ocpp/admin.py +392 -0
  50. ocpp/apps.py +24 -0
  51. ocpp/consumers.py +267 -0
  52. ocpp/evcs.py +911 -0
  53. ocpp/models.py +300 -0
  54. ocpp/routing.py +9 -0
  55. ocpp/simulator.py +357 -0
  56. ocpp/store.py +175 -0
  57. ocpp/tasks.py +27 -0
  58. ocpp/test_export_import.py +129 -0
  59. ocpp/test_rfid.py +345 -0
  60. ocpp/tests.py +1229 -0
  61. ocpp/transactions_io.py +119 -0
  62. ocpp/urls.py +17 -0
  63. ocpp/views.py +359 -0
  64. pages/__init__.py +0 -0
  65. pages/admin.py +231 -0
  66. pages/apps.py +10 -0
  67. pages/checks.py +41 -0
  68. pages/context_processors.py +72 -0
  69. pages/models.py +224 -0
  70. pages/tests.py +628 -0
  71. pages/urls.py +17 -0
  72. pages/utils.py +13 -0
  73. 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
+