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
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()
|