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/apps.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CoreConfig(AppConfig):
|
|
6
|
+
default_auto_field = "django.db.models.BigAutoField"
|
|
7
|
+
name = "core"
|
|
8
|
+
verbose_name = _("Business Models")
|
|
9
|
+
|
|
10
|
+
def ready(self): # pragma: no cover - called by Django
|
|
11
|
+
from django.contrib.auth import get_user_model
|
|
12
|
+
from django.db.models.signals import post_migrate
|
|
13
|
+
from .user_data import (
|
|
14
|
+
patch_admin_user_datum,
|
|
15
|
+
patch_admin_user_data_views,
|
|
16
|
+
)
|
|
17
|
+
from .system import patch_admin_system_view
|
|
18
|
+
from .environment import patch_admin_environment_view
|
|
19
|
+
|
|
20
|
+
def create_default_arthexis(**kwargs):
|
|
21
|
+
User = get_user_model()
|
|
22
|
+
if not User.all_objects.exists():
|
|
23
|
+
User.all_objects.create_superuser(
|
|
24
|
+
pk=1,
|
|
25
|
+
username="arthexis",
|
|
26
|
+
email="arthexis@gmail.com",
|
|
27
|
+
password="arthexis",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
post_migrate.connect(create_default_arthexis, sender=self)
|
|
31
|
+
patch_admin_user_datum()
|
|
32
|
+
patch_admin_user_data_views()
|
|
33
|
+
patch_admin_system_view()
|
|
34
|
+
patch_admin_environment_view()
|
|
35
|
+
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from django.conf import settings
|
|
38
|
+
|
|
39
|
+
lock = Path(settings.BASE_DIR) / "locks" / "celery.lck"
|
|
40
|
+
|
|
41
|
+
if lock.exists():
|
|
42
|
+
|
|
43
|
+
def ensure_email_collector_task(**kwargs):
|
|
44
|
+
try: # pragma: no cover - optional dependency
|
|
45
|
+
from django_celery_beat.models import (
|
|
46
|
+
IntervalSchedule,
|
|
47
|
+
PeriodicTask,
|
|
48
|
+
)
|
|
49
|
+
from django.db.utils import OperationalError, ProgrammingError
|
|
50
|
+
except Exception: # pragma: no cover - tables or module not ready
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
schedule, _ = IntervalSchedule.objects.get_or_create(
|
|
55
|
+
every=1, period=IntervalSchedule.HOURS
|
|
56
|
+
)
|
|
57
|
+
PeriodicTask.objects.get_or_create(
|
|
58
|
+
name="poll_email_collectors",
|
|
59
|
+
defaults={
|
|
60
|
+
"interval": schedule,
|
|
61
|
+
"task": "core.tasks.poll_email_collectors",
|
|
62
|
+
},
|
|
63
|
+
)
|
|
64
|
+
except (OperationalError, ProgrammingError):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
post_migrate.connect(ensure_email_collector_task, sender=self)
|
core/backends.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Custom authentication backends for the core app."""
|
|
2
|
+
|
|
3
|
+
from django.contrib.auth import get_user_model
|
|
4
|
+
from django.contrib.auth.backends import ModelBackend
|
|
5
|
+
import ipaddress
|
|
6
|
+
|
|
7
|
+
from .models import EnergyAccount
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RFIDBackend:
|
|
11
|
+
"""Authenticate using a user's RFID."""
|
|
12
|
+
|
|
13
|
+
def authenticate(self, request, rfid=None, **kwargs):
|
|
14
|
+
if not rfid:
|
|
15
|
+
return None
|
|
16
|
+
account = (
|
|
17
|
+
EnergyAccount.objects.filter(
|
|
18
|
+
rfids__rfid=rfid.upper(), rfids__allowed=True, user__isnull=False
|
|
19
|
+
)
|
|
20
|
+
.select_related("user")
|
|
21
|
+
.first()
|
|
22
|
+
)
|
|
23
|
+
if account:
|
|
24
|
+
return account.user
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
def get_user(self, user_id):
|
|
28
|
+
User = get_user_model()
|
|
29
|
+
try:
|
|
30
|
+
return User.objects.get(pk=user_id)
|
|
31
|
+
except User.DoesNotExist:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class LocalhostAdminBackend(ModelBackend):
|
|
36
|
+
"""Allow default admin credentials only from local networks."""
|
|
37
|
+
|
|
38
|
+
_ALLOWED_NETWORKS = [
|
|
39
|
+
ipaddress.ip_network("::1/128"),
|
|
40
|
+
ipaddress.ip_network("127.0.0.0/8"),
|
|
41
|
+
ipaddress.ip_network("192.168.0.0/16"),
|
|
42
|
+
ipaddress.ip_network("172.16.0.0/12"),
|
|
43
|
+
ipaddress.ip_network("10.42.0.0/16"),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
def authenticate(self, request, username=None, password=None, **kwargs):
|
|
47
|
+
if username == "admin" and password == "admin" and request is not None:
|
|
48
|
+
forwarded = request.META.get("HTTP_X_FORWARDED_FOR")
|
|
49
|
+
if forwarded:
|
|
50
|
+
remote = forwarded.split(",")[0].strip()
|
|
51
|
+
else:
|
|
52
|
+
remote = request.META.get("REMOTE_ADDR", "")
|
|
53
|
+
try:
|
|
54
|
+
ip = ipaddress.ip_address(remote)
|
|
55
|
+
except ValueError:
|
|
56
|
+
return None
|
|
57
|
+
allowed = any(ip in net for net in self._ALLOWED_NETWORKS)
|
|
58
|
+
if not allowed:
|
|
59
|
+
return None
|
|
60
|
+
User = get_user_model()
|
|
61
|
+
user, created = User.all_objects.get_or_create(
|
|
62
|
+
username="admin",
|
|
63
|
+
defaults={
|
|
64
|
+
"is_staff": True,
|
|
65
|
+
"is_superuser": True,
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
if created:
|
|
69
|
+
user.set_password("admin")
|
|
70
|
+
user.save()
|
|
71
|
+
elif not user.check_password("admin"):
|
|
72
|
+
return None
|
|
73
|
+
return user
|
|
74
|
+
return super().authenticate(request, username, password, **kwargs)
|
|
75
|
+
|
|
76
|
+
def get_user(self, user_id):
|
|
77
|
+
User = get_user_model()
|
|
78
|
+
try:
|
|
79
|
+
return User.all_objects.get(pk=user_id)
|
|
80
|
+
except User.DoesNotExist:
|
|
81
|
+
return None
|
|
82
|
+
|
core/entity.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from django.apps import apps
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
from django.db import models
|
|
8
|
+
from django.contrib.auth.models import UserManager as DjangoUserManager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EntityQuerySet(models.QuerySet):
|
|
12
|
+
def delete(self): # pragma: no cover - delegates to instance delete
|
|
13
|
+
deleted = 0
|
|
14
|
+
for obj in self:
|
|
15
|
+
obj.delete()
|
|
16
|
+
deleted += 1
|
|
17
|
+
return deleted, {}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EntityManager(models.Manager):
|
|
21
|
+
def get_queryset(self):
|
|
22
|
+
return EntityQuerySet(self.model, using=self._db).filter(is_deleted=False)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EntityUserManager(DjangoUserManager):
|
|
26
|
+
def get_queryset(self):
|
|
27
|
+
return (
|
|
28
|
+
EntityQuerySet(self.model, using=self._db)
|
|
29
|
+
.filter(is_deleted=False)
|
|
30
|
+
.exclude(username="admin")
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Entity(models.Model):
|
|
35
|
+
"""Base model providing seed data tracking and soft deletion."""
|
|
36
|
+
|
|
37
|
+
is_seed_data = models.BooleanField(default=False, editable=False)
|
|
38
|
+
is_deleted = models.BooleanField(default=False, editable=False)
|
|
39
|
+
|
|
40
|
+
objects = EntityManager()
|
|
41
|
+
all_objects = models.Manager()
|
|
42
|
+
|
|
43
|
+
class Meta:
|
|
44
|
+
abstract = True
|
|
45
|
+
|
|
46
|
+
def clone(self):
|
|
47
|
+
"""Return an unsaved copy of this instance."""
|
|
48
|
+
new = copy.copy(self)
|
|
49
|
+
new.pk = None
|
|
50
|
+
return new
|
|
51
|
+
|
|
52
|
+
def save(self, *args, **kwargs):
|
|
53
|
+
if self.pk:
|
|
54
|
+
try:
|
|
55
|
+
old = type(self).all_objects.get(pk=self.pk)
|
|
56
|
+
except type(self).DoesNotExist:
|
|
57
|
+
pass
|
|
58
|
+
else:
|
|
59
|
+
self.is_seed_data = old.is_seed_data
|
|
60
|
+
super().save(*args, **kwargs)
|
|
61
|
+
|
|
62
|
+
def resolve_sigils(self, field: str) -> str:
|
|
63
|
+
"""Return ``field`` value with [ROOT.KEY] tokens resolved."""
|
|
64
|
+
# Find field ignoring case
|
|
65
|
+
name = field.lower()
|
|
66
|
+
fobj = next((f for f in self._meta.fields if f.name.lower() == name), None)
|
|
67
|
+
if not fobj:
|
|
68
|
+
return ""
|
|
69
|
+
value = self.__dict__.get(fobj.attname, "")
|
|
70
|
+
if value is None:
|
|
71
|
+
return ""
|
|
72
|
+
text = str(value)
|
|
73
|
+
|
|
74
|
+
pattern = re.compile(r"\[([A-Za-z0-9_]+)\.([A-Za-z0-9_]+)\]")
|
|
75
|
+
SigilRoot = apps.get_model("core", "SigilRoot")
|
|
76
|
+
|
|
77
|
+
def repl(match):
|
|
78
|
+
root_name, key = match.group(1), match.group(2)
|
|
79
|
+
try:
|
|
80
|
+
root = SigilRoot.objects.get(prefix__iexact=root_name)
|
|
81
|
+
except SigilRoot.DoesNotExist:
|
|
82
|
+
return ""
|
|
83
|
+
if root.context_type == SigilRoot.Context.CONFIG:
|
|
84
|
+
if root.prefix.upper() == "ENV":
|
|
85
|
+
return os.environ.get(key, "")
|
|
86
|
+
if root.prefix.upper() == "SYS":
|
|
87
|
+
return str(getattr(settings, key, ""))
|
|
88
|
+
return ""
|
|
89
|
+
|
|
90
|
+
return pattern.sub(repl, text)
|
|
91
|
+
|
|
92
|
+
def delete(self, using=None, keep_parents=False):
|
|
93
|
+
if self.is_seed_data:
|
|
94
|
+
self.is_deleted = True
|
|
95
|
+
self.save(update_fields=["is_deleted"])
|
|
96
|
+
else:
|
|
97
|
+
super().delete(using=using, keep_parents=keep_parents)
|
core/environment.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
from django.contrib import admin
|
|
7
|
+
from django.template.response import TemplateResponse
|
|
8
|
+
from django.urls import path
|
|
9
|
+
from django.utils.translation import gettext_lazy as _
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _environment_view(request):
|
|
13
|
+
env_vars = sorted(os.environ.items())
|
|
14
|
+
django_settings = sorted(
|
|
15
|
+
[(name, getattr(settings, name)) for name in dir(settings) if name.isupper()]
|
|
16
|
+
)
|
|
17
|
+
context = admin.site.each_context(request)
|
|
18
|
+
context.update(
|
|
19
|
+
{
|
|
20
|
+
"title": _("Environment"),
|
|
21
|
+
"env_vars": env_vars,
|
|
22
|
+
"django_settings": django_settings,
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
return TemplateResponse(request, "admin/environment.html", context)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def patch_admin_environment_view() -> None:
|
|
29
|
+
"""Add custom admin view for environment information."""
|
|
30
|
+
original_get_urls = admin.site.get_urls
|
|
31
|
+
|
|
32
|
+
def get_urls():
|
|
33
|
+
urls = original_get_urls()
|
|
34
|
+
custom = [
|
|
35
|
+
path(
|
|
36
|
+
"environment/",
|
|
37
|
+
admin.site.admin_view(_environment_view),
|
|
38
|
+
name="environment",
|
|
39
|
+
),
|
|
40
|
+
]
|
|
41
|
+
return custom + urls
|
|
42
|
+
|
|
43
|
+
admin.site.get_urls = get_urls
|
core/fields.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.db.models.fields import DeferredAttribute
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class _BaseSigilDescriptor(DeferredAttribute):
|
|
6
|
+
def __set__(self, instance, value):
|
|
7
|
+
instance.__dict__[self.field.attname] = value
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _CheckSigilDescriptor(_BaseSigilDescriptor):
|
|
11
|
+
def __get__(self, instance, cls=None):
|
|
12
|
+
value = super().__get__(instance, cls)
|
|
13
|
+
if instance is None:
|
|
14
|
+
return value
|
|
15
|
+
if getattr(instance, f"{self.field.name}_resolve_sigils", False):
|
|
16
|
+
return instance.resolve_sigils(self.field.name)
|
|
17
|
+
return value
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _AutoSigilDescriptor(_BaseSigilDescriptor):
|
|
21
|
+
def __get__(self, instance, cls=None):
|
|
22
|
+
value = super().__get__(instance, cls)
|
|
23
|
+
if instance is None:
|
|
24
|
+
return value
|
|
25
|
+
return instance.resolve_sigils(self.field.name)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _SigilBaseField:
|
|
29
|
+
def value_from_object(self, obj):
|
|
30
|
+
return obj.__dict__.get(self.attname)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SigilCheckFieldMixin(_SigilBaseField):
|
|
34
|
+
descriptor_class = _CheckSigilDescriptor
|
|
35
|
+
|
|
36
|
+
def contribute_to_class(self, cls, name, private_only=False):
|
|
37
|
+
super().contribute_to_class(cls, name, private_only=private_only)
|
|
38
|
+
extra_name = f"{name}_resolve_sigils"
|
|
39
|
+
if not any(f.name == extra_name for f in cls._meta.fields):
|
|
40
|
+
cls.add_to_class(
|
|
41
|
+
extra_name,
|
|
42
|
+
models.BooleanField(
|
|
43
|
+
default=False,
|
|
44
|
+
verbose_name="Resolve [SIGILS] in templates",
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SigilAutoFieldMixin(_SigilBaseField):
|
|
50
|
+
descriptor_class = _AutoSigilDescriptor
|
|
51
|
+
|
|
52
|
+
def contribute_to_class(self, cls, name, private_only=False):
|
|
53
|
+
super().contribute_to_class(cls, name, private_only=private_only)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SigilShortCheckField(SigilCheckFieldMixin, models.CharField):
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SigilLongCheckField(SigilCheckFieldMixin, models.TextField):
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class SigilShortAutoField(SigilAutoFieldMixin, models.CharField):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SigilLongAutoField(SigilAutoFieldMixin, models.TextField):
|
|
69
|
+
pass
|
|
70
|
+
|
core/lcd_screen.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Standalone LCD screen updater.
|
|
2
|
+
|
|
3
|
+
The script polls ``locks/lcd_screen.lck`` for up to two lines of text and
|
|
4
|
+
writes them to the attached LCD1602 display. If either line exceeds 16
|
|
5
|
+
characters the text scrolls horizontally. A third line in the lock file
|
|
6
|
+
can define the scroll speed in milliseconds per character (default 1000
|
|
7
|
+
ms).
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from nodes.lcd import CharLCD1602, LCDUnavailableError
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
LOCK_FILE = Path(__file__).resolve().parents[1] / "locks" / "lcd_screen.lck"
|
|
20
|
+
DEFAULT_SCROLL_MS = 1000
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _read_lock_file() -> tuple[str, str, int]:
|
|
24
|
+
try:
|
|
25
|
+
lines = LOCK_FILE.read_text(encoding="utf-8").splitlines()
|
|
26
|
+
except FileNotFoundError:
|
|
27
|
+
return "", "", DEFAULT_SCROLL_MS
|
|
28
|
+
line1 = lines[0][:64] if len(lines) > 0 else ""
|
|
29
|
+
line2 = lines[1][:64] if len(lines) > 1 else ""
|
|
30
|
+
try:
|
|
31
|
+
speed = int(lines[2]) if len(lines) > 2 else DEFAULT_SCROLL_MS
|
|
32
|
+
except ValueError:
|
|
33
|
+
speed = DEFAULT_SCROLL_MS
|
|
34
|
+
return line1, line2, speed
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _display(lcd: CharLCD1602, line1: str, line2: str, scroll_ms: int) -> None:
|
|
38
|
+
scroll_sec = max(scroll_ms, 0) / 1000.0
|
|
39
|
+
text1 = line1[:64]
|
|
40
|
+
text2 = line2[:64]
|
|
41
|
+
pad1 = text1 + " " * 16 if len(text1) > 16 else text1.ljust(16)
|
|
42
|
+
pad2 = text2 + " " * 16 if len(text2) > 16 else text2.ljust(16)
|
|
43
|
+
steps = max(len(pad1) - 15, len(pad2) - 15)
|
|
44
|
+
for i in range(steps):
|
|
45
|
+
segment1 = pad1[i : i + 16]
|
|
46
|
+
segment2 = pad2[i : i + 16]
|
|
47
|
+
lcd.write(0, 0, segment1.ljust(16))
|
|
48
|
+
lcd.write(0, 1, segment2.ljust(16))
|
|
49
|
+
time.sleep(scroll_sec)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def main() -> None: # pragma: no cover - hardware dependent
|
|
53
|
+
lcd = None
|
|
54
|
+
last_mtime = 0.0
|
|
55
|
+
while True:
|
|
56
|
+
try:
|
|
57
|
+
if LOCK_FILE.exists():
|
|
58
|
+
mtime = LOCK_FILE.stat().st_mtime
|
|
59
|
+
if mtime != last_mtime or lcd is None:
|
|
60
|
+
line1, line2, speed = _read_lock_file()
|
|
61
|
+
if lcd is None:
|
|
62
|
+
lcd = CharLCD1602()
|
|
63
|
+
lcd.init_lcd()
|
|
64
|
+
lcd.clear()
|
|
65
|
+
_display(lcd, line1, line2, speed)
|
|
66
|
+
last_mtime = mtime
|
|
67
|
+
except LCDUnavailableError as exc:
|
|
68
|
+
logger.warning("LCD unavailable: %s", exc)
|
|
69
|
+
lcd = None
|
|
70
|
+
except Exception as exc:
|
|
71
|
+
logger.warning("LCD update failed: %s", exc)
|
|
72
|
+
lcd = None
|
|
73
|
+
time.sleep(0.5)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__": # pragma: no cover - script entry point
|
|
77
|
+
main()
|
core/middleware.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from django.contrib.contenttypes.models import ContentType
|
|
2
|
+
from .models import AdminHistory
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AdminHistoryMiddleware:
|
|
6
|
+
"""Log recently visited admin changelists for each user."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, get_response):
|
|
9
|
+
self.get_response = get_response
|
|
10
|
+
|
|
11
|
+
def __call__(self, request):
|
|
12
|
+
response = self.get_response(request)
|
|
13
|
+
match = getattr(request, "resolver_match", None)
|
|
14
|
+
if (
|
|
15
|
+
request.user.is_authenticated
|
|
16
|
+
and request.user.is_staff
|
|
17
|
+
and request.method == "GET"
|
|
18
|
+
and match
|
|
19
|
+
and match.url_name
|
|
20
|
+
and match.url_name.endswith("_changelist")
|
|
21
|
+
and response.status_code == 200
|
|
22
|
+
):
|
|
23
|
+
parts = request.path.strip("/").split("/")
|
|
24
|
+
if len(parts) >= 3:
|
|
25
|
+
app_label, model_name = parts[1], parts[2]
|
|
26
|
+
content_type = ContentType.objects.get_by_natural_key(
|
|
27
|
+
app_label, model_name
|
|
28
|
+
)
|
|
29
|
+
AdminHistory.objects.update_or_create(
|
|
30
|
+
user=request.user,
|
|
31
|
+
url=request.get_full_path(),
|
|
32
|
+
defaults={"content_type": content_type},
|
|
33
|
+
)
|
|
34
|
+
return response
|