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
pages/admin.py ADDED
@@ -0,0 +1,231 @@
1
+ from django.contrib import admin, messages
2
+ from django.contrib.sites.admin import SiteAdmin as DjangoSiteAdmin
3
+ from django.contrib.sites.models import Site
4
+ from django import forms
5
+ from django.db import models
6
+ from app.widgets import CopyColorWidget
7
+ from django.shortcuts import redirect, render, get_object_or_404
8
+ from django.urls import path, reverse
9
+ from django.utils.html import format_html
10
+ import ipaddress
11
+ from django.apps import apps as django_apps
12
+ from django.conf import settings
13
+
14
+ from nodes.models import Node
15
+ from nodes.utils import capture_screenshot, save_screenshot
16
+
17
+ from .models import SiteBadge, Application, SiteProxy, Module, Landing, Favorite
18
+ from django.contrib.contenttypes.models import ContentType
19
+
20
+
21
+ def get_local_app_choices():
22
+ choices = []
23
+ for app_label in getattr(settings, "LOCAL_APPS", []):
24
+ try:
25
+ config = django_apps.get_app_config(app_label)
26
+ except LookupError:
27
+ continue
28
+ choices.append((config.label, config.verbose_name))
29
+ return choices
30
+
31
+
32
+ class SiteBadgeInline(admin.StackedInline):
33
+ model = SiteBadge
34
+ can_delete = False
35
+ extra = 0
36
+ formfield_overrides = {models.CharField: {"widget": CopyColorWidget}}
37
+ fields = ("badge_color", "favicon", "landing_override")
38
+
39
+
40
+ class SiteForm(forms.ModelForm):
41
+ name = forms.CharField(required=False)
42
+
43
+ class Meta:
44
+ model = Site
45
+ fields = "__all__"
46
+
47
+
48
+ class SiteAdmin(DjangoSiteAdmin):
49
+ form = SiteForm
50
+ inlines = [SiteBadgeInline]
51
+ change_list_template = "admin/sites/site/change_list.html"
52
+ fields = ("domain", "name")
53
+ list_display = ("domain", "name")
54
+ actions = ["capture_screenshot"]
55
+
56
+ @admin.action(description="Capture screenshot")
57
+ def capture_screenshot(self, request, queryset):
58
+ node = Node.get_local()
59
+ for site in queryset:
60
+ url = f"http://{site.domain}/"
61
+ try:
62
+ path = capture_screenshot(url)
63
+ screenshot = save_screenshot(path, node=node, method="ADMIN")
64
+ except Exception as exc: # pragma: no cover - browser issues
65
+ self.message_user(request, f"{site.domain}: {exc}", messages.ERROR)
66
+ continue
67
+ if screenshot:
68
+ link = reverse(
69
+ "admin:nodes_contentsample_change", args=[screenshot.pk]
70
+ )
71
+ self.message_user(
72
+ request,
73
+ format_html(
74
+ 'Screenshot for {} saved. <a href="{}">View</a>',
75
+ site.domain,
76
+ link,
77
+ ),
78
+ messages.SUCCESS,
79
+ )
80
+ else:
81
+ self.message_user(
82
+ request,
83
+ f"{site.domain}: duplicate screenshot; not saved",
84
+ messages.INFO,
85
+ )
86
+
87
+ def get_urls(self):
88
+ urls = super().get_urls()
89
+ custom = [
90
+ path(
91
+ "register-current/",
92
+ self.admin_site.admin_view(self.register_current),
93
+ name="pages_siteproxy_register_current",
94
+ )
95
+ ]
96
+ return custom + urls
97
+
98
+ def register_current(self, request):
99
+ domain = request.get_host().split(":")[0]
100
+ try:
101
+ ipaddress.ip_address(domain)
102
+ except ValueError:
103
+ name = domain
104
+ else:
105
+ name = ""
106
+ site, created = Site.objects.get_or_create(
107
+ domain=domain, defaults={"name": name}
108
+ )
109
+ if created:
110
+ self.message_user(request, "Current domain registered", messages.SUCCESS)
111
+ else:
112
+ self.message_user(
113
+ request, "Current domain already registered", messages.INFO
114
+ )
115
+ return redirect("..")
116
+
117
+
118
+ admin.site.unregister(Site)
119
+ admin.site.register(SiteProxy, SiteAdmin)
120
+
121
+
122
+ class ApplicationForm(forms.ModelForm):
123
+ name = forms.ChoiceField(choices=[])
124
+
125
+ class Meta:
126
+ model = Application
127
+ fields = "__all__"
128
+
129
+ def __init__(self, *args, **kwargs):
130
+ super().__init__(*args, **kwargs)
131
+ self.fields["name"].choices = get_local_app_choices()
132
+
133
+
134
+ class ApplicationModuleInline(admin.TabularInline):
135
+ model = Module
136
+ fk_name = "application"
137
+ extra = 0
138
+
139
+
140
+ @admin.register(Application)
141
+ class ApplicationAdmin(admin.ModelAdmin):
142
+ form = ApplicationForm
143
+ list_display = ("name", "app_verbose_name", "installed")
144
+ readonly_fields = ("installed",)
145
+ inlines = [ApplicationModuleInline]
146
+
147
+ @admin.display(description="Verbose name")
148
+ def app_verbose_name(self, obj):
149
+ return obj.verbose_name
150
+
151
+ @admin.display(boolean=True)
152
+ def installed(self, obj):
153
+ return obj.installed
154
+
155
+
156
+ class LandingInline(admin.TabularInline):
157
+ model = Landing
158
+ extra = 0
159
+ fields = ("path", "label", "enabled", "description")
160
+
161
+
162
+ @admin.register(Module)
163
+ class ModuleAdmin(admin.ModelAdmin):
164
+ list_display = ("application", "node_role", "path", "menu", "is_default")
165
+ list_filter = ("node_role", "application")
166
+ fields = ("node_role", "application", "path", "menu", "is_default", "favicon")
167
+ inlines = [LandingInline]
168
+
169
+
170
+ def favorite_toggle(request, ct_id):
171
+ ct = get_object_or_404(ContentType, pk=ct_id)
172
+ fav = Favorite.objects.filter(user=request.user, content_type=ct).first()
173
+ if fav:
174
+ return redirect("admin:favorite_list")
175
+ if request.method == "POST":
176
+ label = request.POST.get("custom_label", "").strip()
177
+ user_data = request.POST.get("user_data") == "on"
178
+ Favorite.objects.create(
179
+ user=request.user,
180
+ content_type=ct,
181
+ custom_label=label,
182
+ user_data=user_data,
183
+ )
184
+ return redirect("admin:index")
185
+ return render(request, "admin/favorite_confirm.html", {"content_type": ct})
186
+
187
+
188
+ def favorite_list(request):
189
+ favorites = Favorite.objects.filter(user=request.user).select_related("content_type")
190
+ if request.method == "POST":
191
+ selected = request.POST.getlist("user_data")
192
+ for fav in favorites:
193
+ fav.user_data = str(fav.pk) in selected
194
+ fav.save(update_fields=["user_data"])
195
+ return redirect("admin:favorite_list")
196
+ return render(request, "admin/favorite_list.html", {"favorites": favorites})
197
+
198
+
199
+ def favorite_delete(request, pk):
200
+ fav = get_object_or_404(Favorite, pk=pk, user=request.user)
201
+ fav.delete()
202
+ return redirect("admin:favorite_list")
203
+
204
+
205
+ def favorite_clear(request):
206
+ Favorite.objects.filter(user=request.user).delete()
207
+ return redirect("admin:favorite_list")
208
+
209
+
210
+ def get_admin_urls(urls):
211
+ def get_urls():
212
+ my_urls = [
213
+ path("favorites/<int:ct_id>/", admin.site.admin_view(favorite_toggle), name="favorite_toggle"),
214
+ path("favorites/", admin.site.admin_view(favorite_list), name="favorite_list"),
215
+ path(
216
+ "favorites/delete/<int:pk>/",
217
+ admin.site.admin_view(favorite_delete),
218
+ name="favorite_delete",
219
+ ),
220
+ path(
221
+ "favorites/clear/",
222
+ admin.site.admin_view(favorite_clear),
223
+ name="favorite_clear",
224
+ ),
225
+ ]
226
+ return my_urls + urls
227
+
228
+ return get_urls
229
+
230
+
231
+ admin.site.get_urls = get_admin_urls(admin.site.get_urls())
pages/apps.py ADDED
@@ -0,0 +1,10 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class PagesConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "pages"
7
+ verbose_name = "Web Experience"
8
+
9
+ def ready(self): # pragma: no cover - import for side effects
10
+ from . import checks # noqa: F401
pages/checks.py ADDED
@@ -0,0 +1,41 @@
1
+ import inspect
2
+
3
+ from django.core.checks import Warning, register
4
+ from django.urls.resolvers import URLPattern, URLResolver
5
+
6
+ from config import urls as project_urls
7
+
8
+
9
+ def _collect_checks(resolver: URLResolver, errors: list, prefix: str = ""):
10
+ for pattern in resolver.url_patterns:
11
+ if isinstance(pattern, URLResolver):
12
+ _collect_checks(pattern, errors, prefix + pattern.pattern._route)
13
+ elif isinstance(pattern, URLPattern):
14
+ view = pattern.callback
15
+ if getattr(view, "landing", False):
16
+ sig = inspect.signature(view)
17
+ params = list(sig.parameters.values())
18
+ if params and params[0].name == "request":
19
+ params = params[1:]
20
+ has_required = any(
21
+ p.default is inspect._empty
22
+ and p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)
23
+ for p in params
24
+ )
25
+ if has_required:
26
+ errors.append(
27
+ Warning(
28
+ f'Landing view "{view.__module__}.{view.__name__}" requires URL parameters and cannot be a landing page.',
29
+ id="pages.W001",
30
+ )
31
+ )
32
+
33
+
34
+ @register()
35
+ def landing_views_have_no_args(app_configs, **kwargs):
36
+ errors: list = []
37
+ for p in project_urls.urlpatterns:
38
+ if isinstance(p, URLResolver):
39
+ _collect_checks(p, errors, p.pattern._route)
40
+ return errors
41
+
@@ -0,0 +1,72 @@
1
+ from utils.sites import get_site
2
+ from django.urls import Resolver404, resolve
3
+ from django.conf import settings
4
+ from pathlib import Path
5
+ from nodes.models import Node
6
+ from .models import Module
7
+
8
+ _favicon_path = (
9
+ Path(settings.BASE_DIR) / "pages" / "fixtures" / "data" / "favicon.txt"
10
+ )
11
+ try:
12
+ _DEFAULT_FAVICON = f"data:image/png;base64,{_favicon_path.read_text().strip()}"
13
+ except OSError:
14
+ _DEFAULT_FAVICON = ""
15
+
16
+
17
+ def nav_links(request):
18
+ """Provide navigation links for the current site."""
19
+ site = get_site(request)
20
+ node = Node.get_local()
21
+ role = node.role if node else None
22
+ if role:
23
+ modules = (
24
+ Module.objects.filter(node_role=role)
25
+ .select_related("application")
26
+ .prefetch_related("landings")
27
+ )
28
+ else:
29
+ modules = []
30
+
31
+ valid_modules = []
32
+ current_module = None
33
+ for module in modules:
34
+ landings = []
35
+ for landing in module.landings.filter(enabled=True):
36
+ try:
37
+ match = resolve(landing.path)
38
+ except Resolver404:
39
+ continue
40
+ view_func = match.func
41
+ requires_login = getattr(view_func, "login_required", False) or hasattr(
42
+ view_func, "login_url"
43
+ )
44
+ staff_only = getattr(view_func, "staff_required", False)
45
+ if requires_login and not request.user.is_authenticated:
46
+ continue
47
+ if staff_only and not request.user.is_staff:
48
+ continue
49
+ landings.append(landing)
50
+ if landings:
51
+ module.enabled_landings = landings
52
+ valid_modules.append(module)
53
+ if request.path.startswith(module.path):
54
+ if current_module is None or len(module.path) > len(current_module.path):
55
+ current_module = module
56
+
57
+ valid_modules.sort(key=lambda m: m.menu_label.lower())
58
+
59
+ if current_module and current_module.favicon:
60
+ favicon_url = current_module.favicon.url
61
+ else:
62
+ favicon_url = None
63
+ if site:
64
+ try:
65
+ if site.badge.favicon:
66
+ favicon_url = site.badge.favicon.url
67
+ except Exception:
68
+ pass
69
+ if not favicon_url:
70
+ favicon_url = _DEFAULT_FAVICON
71
+
72
+ return {"nav_modules": valid_modules, "favicon_url": favicon_url}
pages/models.py ADDED
@@ -0,0 +1,224 @@
1
+ from django.db import models
2
+ from core.entity import Entity
3
+ from django.contrib.sites.models import Site
4
+ from nodes.models import NodeRole
5
+ from django.apps import apps as django_apps
6
+ from django.utils.text import slugify
7
+ from django.utils.translation import gettext_lazy as _
8
+ from importlib import import_module
9
+ from django.urls import URLPattern
10
+ from django.conf import settings
11
+ from django.contrib.contenttypes.models import ContentType
12
+
13
+
14
+ class ApplicationManager(models.Manager):
15
+ def get_by_natural_key(self, name: str):
16
+ return self.get(name=name)
17
+
18
+
19
+ class Application(Entity):
20
+ name = models.CharField(max_length=100, unique=True)
21
+ description = models.TextField(blank=True)
22
+
23
+ objects = ApplicationManager()
24
+
25
+ def natural_key(self): # pragma: no cover - simple representation
26
+ return (self.name,)
27
+
28
+ def __str__(self) -> str: # pragma: no cover - simple representation
29
+ return self.name
30
+
31
+ @property
32
+ def installed(self) -> bool:
33
+ return django_apps.is_installed(self.name)
34
+
35
+ @property
36
+ def verbose_name(self) -> str:
37
+ try:
38
+ return django_apps.get_app_config(self.name).verbose_name
39
+ except LookupError:
40
+ return self.name
41
+
42
+
43
+ class ModuleManager(models.Manager):
44
+ def get_by_natural_key(self, role: str, path: str):
45
+ return self.get(node_role__name=role, path=path)
46
+
47
+
48
+ class Module(Entity):
49
+ node_role = models.ForeignKey(
50
+ NodeRole, on_delete=models.CASCADE, related_name="modules",
51
+ )
52
+ application = models.ForeignKey(
53
+ Application, on_delete=models.CASCADE, related_name="modules",
54
+ )
55
+ path = models.CharField(
56
+ max_length=100,
57
+ help_text="Base path for the app, starting with /",
58
+ blank=True,
59
+ )
60
+ menu = models.CharField(
61
+ max_length=100,
62
+ blank=True,
63
+ help_text="Text used for the navbar pill; defaults to the application name.",
64
+ )
65
+ is_default = models.BooleanField(default=False)
66
+ favicon = models.ImageField(upload_to="modules/favicons/", blank=True)
67
+
68
+ objects = ModuleManager()
69
+
70
+ class Meta:
71
+ verbose_name = _("Module")
72
+ verbose_name_plural = _("Modules")
73
+ unique_together = ("node_role", "path")
74
+
75
+ def natural_key(self): # pragma: no cover - simple representation
76
+ role_name = None
77
+ if getattr(self, "node_role_id", None):
78
+ role_name = self.node_role.name
79
+ return (role_name, self.path)
80
+
81
+ natural_key.dependencies = ["nodes.NodeRole"]
82
+
83
+ def __str__(self) -> str: # pragma: no cover - simple representation
84
+ return f"{self.application.name} ({self.path})"
85
+
86
+ @property
87
+ def menu_label(self) -> str:
88
+ return self.menu or self.application.name
89
+
90
+ def save(self, *args, **kwargs):
91
+ if not self.path:
92
+ self.path = f"/{slugify(self.application.name)}/"
93
+ super().save(*args, **kwargs)
94
+
95
+ def create_landings(self):
96
+ try:
97
+ urlconf = import_module(f"{self.application.name}.urls")
98
+ except Exception:
99
+ try:
100
+ urlconf = import_module(f"{self.application.name.lower()}.urls")
101
+ except Exception:
102
+ Landing.objects.get_or_create(
103
+ module=self,
104
+ path=self.path,
105
+ defaults={"label": self.application.name},
106
+ )
107
+ return
108
+ patterns = getattr(urlconf, "urlpatterns", [])
109
+ created = False
110
+
111
+ def _walk(patterns, prefix=""):
112
+ nonlocal created
113
+ for pattern in patterns:
114
+ if isinstance(pattern, URLPattern):
115
+ callback = pattern.callback
116
+ if getattr(callback, "landing", False):
117
+ Landing.objects.get_or_create(
118
+ module=self,
119
+ path=f"{self.path}{prefix}{str(pattern.pattern)}",
120
+ defaults={
121
+ "label": getattr(
122
+ callback,
123
+ "landing_label",
124
+ callback.__name__.replace("_", " ").title(),
125
+ )
126
+ },
127
+ )
128
+ created = True
129
+ else:
130
+ _walk(pattern.url_patterns, prefix=f"{prefix}{str(pattern.pattern)}")
131
+
132
+ _walk(patterns)
133
+
134
+ if not created:
135
+ Landing.objects.get_or_create(
136
+ module=self, path=self.path, defaults={"label": self.application.name}
137
+ )
138
+
139
+
140
+ class SiteBadge(Entity):
141
+ site = models.OneToOneField(Site, on_delete=models.CASCADE, related_name="badge")
142
+ badge_color = models.CharField(max_length=7, default="#28a745")
143
+ favicon = models.ImageField(upload_to="sites/favicons/", blank=True)
144
+ landing_override = models.ForeignKey('Landing', null=True, blank=True, on_delete=models.SET_NULL)
145
+
146
+ def __str__(self) -> str: # pragma: no cover - simple representation
147
+ return f"Badge for {self.site.domain}"
148
+
149
+ class Meta:
150
+ verbose_name = "Site Badge"
151
+ verbose_name_plural = "Site Badges"
152
+
153
+
154
+ class SiteProxy(Site):
155
+ class Meta:
156
+ proxy = True
157
+ app_label = "pages"
158
+ verbose_name = "Site"
159
+ verbose_name_plural = "Sites"
160
+
161
+
162
+ class LandingManager(models.Manager):
163
+ def get_by_natural_key(self, role: str, module_path: str, path: str):
164
+ return self.get(
165
+ module__node_role__name=role, module__path=module_path, path=path
166
+ )
167
+
168
+
169
+ class Landing(Entity):
170
+ module = models.ForeignKey(
171
+ Module, on_delete=models.CASCADE, related_name="landings"
172
+ )
173
+ path = models.CharField(max_length=200)
174
+ label = models.CharField(max_length=100)
175
+ enabled = models.BooleanField(default=True)
176
+ description = models.TextField(blank=True)
177
+
178
+ objects = LandingManager()
179
+
180
+ class Meta:
181
+ unique_together = ("module", "path")
182
+
183
+ def __str__(self) -> str: # pragma: no cover - simple representation
184
+ return f"{self.label} ({self.path})"
185
+
186
+ def save(self, *args, **kwargs):
187
+ if not self.pk:
188
+ existing = type(self).objects.filter(
189
+ module=self.module, path=self.path
190
+ ).first()
191
+ if existing:
192
+ self.pk = existing.pk
193
+ super().save(*args, **kwargs)
194
+
195
+ def natural_key(self): # pragma: no cover - simple representation
196
+ return (self.module.node_role.name, self.module.path, self.path)
197
+
198
+ natural_key.dependencies = ["nodes.NodeRole", "pages.Module"]
199
+
200
+
201
+ class Favorite(Entity):
202
+ user = models.ForeignKey(
203
+ settings.AUTH_USER_MODEL,
204
+ on_delete=models.CASCADE,
205
+ related_name="favorites",
206
+ )
207
+ content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
208
+ custom_label = models.CharField(max_length=100, blank=True)
209
+ user_data = models.BooleanField(default=False)
210
+
211
+ class Meta:
212
+ unique_together = ("user", "content_type")
213
+
214
+
215
+ from django.db.models.signals import post_save
216
+ from django.dispatch import receiver
217
+
218
+
219
+ @receiver(post_save, sender=Module)
220
+ def _create_landings(
221
+ sender, instance, created, raw, **kwargs
222
+ ): # pragma: no cover - simple handler
223
+ if created and not raw:
224
+ instance.create_landings()