arthexis 0.1.14__py3-none-any.whl → 0.1.16__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.14.dist-info → arthexis-0.1.16.dist-info}/METADATA +3 -2
- {arthexis-0.1.14.dist-info → arthexis-0.1.16.dist-info}/RECORD +41 -39
- config/urls.py +5 -0
- core/admin.py +200 -9
- core/admindocs.py +44 -3
- core/apps.py +1 -1
- core/backends.py +44 -8
- core/entity.py +17 -1
- core/github_issues.py +12 -7
- core/log_paths.py +24 -10
- core/mailer.py +9 -5
- core/models.py +92 -23
- core/release.py +173 -2
- core/system.py +411 -4
- core/tasks.py +5 -1
- core/test_system_info.py +16 -0
- core/tests.py +280 -0
- core/views.py +252 -38
- nodes/admin.py +25 -1
- nodes/models.py +99 -6
- nodes/rfid_sync.py +15 -0
- nodes/tests.py +142 -3
- nodes/utils.py +3 -0
- ocpp/consumers.py +38 -0
- ocpp/models.py +19 -4
- ocpp/tasks.py +156 -2
- ocpp/test_rfid.py +44 -2
- ocpp/tests.py +111 -1
- pages/admin.py +188 -5
- pages/context_processors.py +20 -1
- pages/middleware.py +4 -0
- pages/models.py +39 -1
- pages/module_defaults.py +156 -0
- pages/tasks.py +74 -0
- pages/tests.py +629 -8
- pages/urls.py +2 -0
- pages/utils.py +11 -0
- pages/views.py +106 -38
- {arthexis-0.1.14.dist-info → arthexis-0.1.16.dist-info}/WHEEL +0 -0
- {arthexis-0.1.14.dist-info → arthexis-0.1.16.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.14.dist-info → arthexis-0.1.16.dist-info}/top_level.txt +0 -0
pages/context_processors.py
CHANGED
|
@@ -50,6 +50,13 @@ def nav_links(request):
|
|
|
50
50
|
valid_modules = []
|
|
51
51
|
datasette_enabled = False
|
|
52
52
|
current_module = None
|
|
53
|
+
user = getattr(request, "user", None)
|
|
54
|
+
user_is_authenticated = getattr(user, "is_authenticated", False)
|
|
55
|
+
user_is_superuser = getattr(user, "is_superuser", False)
|
|
56
|
+
if user_is_authenticated:
|
|
57
|
+
user_group_names = set(user.groups.values_list("name", flat=True))
|
|
58
|
+
else:
|
|
59
|
+
user_group_names = set()
|
|
53
60
|
for module in modules:
|
|
54
61
|
landings = []
|
|
55
62
|
for landing in module.landings.filter(enabled=True):
|
|
@@ -62,7 +69,19 @@ def nav_links(request):
|
|
|
62
69
|
if not requires_login and hasattr(view_func, "login_url"):
|
|
63
70
|
requires_login = True
|
|
64
71
|
staff_only = getattr(view_func, "staff_required", False)
|
|
65
|
-
|
|
72
|
+
required_groups = getattr(
|
|
73
|
+
view_func, "required_security_groups", frozenset()
|
|
74
|
+
)
|
|
75
|
+
if required_groups:
|
|
76
|
+
requires_login = True
|
|
77
|
+
setattr(landing, "requires_login", True)
|
|
78
|
+
if not user_is_authenticated:
|
|
79
|
+
continue
|
|
80
|
+
if not user_is_superuser and not (
|
|
81
|
+
user_group_names & set(required_groups)
|
|
82
|
+
):
|
|
83
|
+
continue
|
|
84
|
+
elif requires_login and not user_is_authenticated:
|
|
66
85
|
setattr(landing, "requires_login", True)
|
|
67
86
|
if staff_only and not request.user.is_staff:
|
|
68
87
|
continue
|
pages/middleware.py
CHANGED
|
@@ -10,6 +10,7 @@ from django.conf import settings
|
|
|
10
10
|
from django.urls import Resolver404, resolve
|
|
11
11
|
|
|
12
12
|
from .models import Landing, LandingLead, ViewHistory
|
|
13
|
+
from .utils import landing_leads_supported
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
logger = logging.getLogger(__name__)
|
|
@@ -125,6 +126,9 @@ class ViewHistoryMiddleware:
|
|
|
125
126
|
if request.method.upper() != "GET":
|
|
126
127
|
return
|
|
127
128
|
|
|
129
|
+
if not landing_leads_supported():
|
|
130
|
+
return
|
|
131
|
+
|
|
128
132
|
referer = request.META.get("HTTP_REFERER", "") or ""
|
|
129
133
|
user_agent = request.META.get("HTTP_USER_AGENT", "") or ""
|
|
130
134
|
ip_address = self._extract_client_ip(request) or None
|
pages/models.py
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
1
4
|
from django.db import models
|
|
2
5
|
from django.db.models import Q
|
|
3
6
|
from core.entity import Entity
|
|
@@ -15,6 +18,10 @@ from django.core.validators import MaxLengthValidator, MaxValueValidator, MinVal
|
|
|
15
18
|
from django.core.exceptions import ValidationError
|
|
16
19
|
|
|
17
20
|
from core import github_issues
|
|
21
|
+
from .tasks import create_user_story_github_issue
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
18
25
|
|
|
19
26
|
|
|
20
27
|
class ApplicationManager(models.Manager):
|
|
@@ -463,7 +470,7 @@ class Favorite(Entity):
|
|
|
463
470
|
unique_together = ("user", "content_type")
|
|
464
471
|
|
|
465
472
|
|
|
466
|
-
class UserStory(
|
|
473
|
+
class UserStory(Lead):
|
|
467
474
|
path = models.CharField(max_length=500)
|
|
468
475
|
name = models.CharField(max_length=40, blank=True)
|
|
469
476
|
rating = models.PositiveSmallIntegerField(
|
|
@@ -599,6 +606,37 @@ from django.db.models.signals import post_save
|
|
|
599
606
|
from django.dispatch import receiver
|
|
600
607
|
|
|
601
608
|
|
|
609
|
+
def _celery_lock_path() -> Path:
|
|
610
|
+
return Path(settings.BASE_DIR) / "locks" / "celery.lck"
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _is_celery_enabled() -> bool:
|
|
614
|
+
return _celery_lock_path().exists()
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
@receiver(post_save, sender=UserStory)
|
|
618
|
+
def _queue_low_rating_user_story_issue(
|
|
619
|
+
sender, instance: UserStory, created: bool, raw: bool, **kwargs
|
|
620
|
+
) -> None:
|
|
621
|
+
if raw or not created:
|
|
622
|
+
return
|
|
623
|
+
if instance.rating >= 5:
|
|
624
|
+
return
|
|
625
|
+
if instance.github_issue_url:
|
|
626
|
+
return
|
|
627
|
+
if not instance.user_id:
|
|
628
|
+
return
|
|
629
|
+
if not _is_celery_enabled():
|
|
630
|
+
return
|
|
631
|
+
|
|
632
|
+
try:
|
|
633
|
+
create_user_story_github_issue.delay(instance.pk)
|
|
634
|
+
except Exception: # pragma: no cover - logging only
|
|
635
|
+
logger.exception(
|
|
636
|
+
"Failed to enqueue GitHub issue creation for user story %s", instance.pk
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
|
|
602
640
|
@receiver(post_save, sender=Module)
|
|
603
641
|
def _create_landings(
|
|
604
642
|
sender, instance, created, raw, **kwargs
|
pages/module_defaults.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Utilities to restore default navigation modules."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Iterable, Mapping
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
ModuleDefinition = Mapping[str, object]
|
|
9
|
+
LandingDefinition = tuple[str, str]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
ROLE_MODULE_DEFAULTS: Mapping[str, tuple[ModuleDefinition, ...]] = {
|
|
13
|
+
"Constellation": (
|
|
14
|
+
{
|
|
15
|
+
"application": "ocpp",
|
|
16
|
+
"path": "/ocpp/",
|
|
17
|
+
"menu": "Chargers",
|
|
18
|
+
"landings": (
|
|
19
|
+
("/ocpp/", "CPMS Online Dashboard"),
|
|
20
|
+
("/ocpp/simulator/", "Charge Point Simulator"),
|
|
21
|
+
("/ocpp/rfid/", "RFID Tag Validator"),
|
|
22
|
+
),
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"application": "awg",
|
|
26
|
+
"path": "/awg/",
|
|
27
|
+
"menu": "",
|
|
28
|
+
"landings": (
|
|
29
|
+
("/awg/", "AWG Calculator"),
|
|
30
|
+
("/awg/energy-tariff/", "Energy Tariff Calculator"),
|
|
31
|
+
),
|
|
32
|
+
},
|
|
33
|
+
),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class ReloadSummary:
|
|
39
|
+
"""Report about the changes performed while restoring defaults."""
|
|
40
|
+
|
|
41
|
+
roles_processed: int = 0
|
|
42
|
+
modules_created: int = 0
|
|
43
|
+
modules_updated: int = 0
|
|
44
|
+
landings_created: int = 0
|
|
45
|
+
landings_updated: int = 0
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def has_changes(self) -> bool:
|
|
49
|
+
return any(
|
|
50
|
+
(
|
|
51
|
+
self.modules_created,
|
|
52
|
+
self.modules_updated,
|
|
53
|
+
self.landings_created,
|
|
54
|
+
self.landings_updated,
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _manager(model, name: str):
|
|
60
|
+
manager = getattr(model, name, None)
|
|
61
|
+
if manager is not None:
|
|
62
|
+
return manager
|
|
63
|
+
return model.objects
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def reload_default_modules(Application, Module, Landing, NodeRole) -> ReloadSummary:
|
|
67
|
+
"""Ensure default navigation modules exist for the configured roles."""
|
|
68
|
+
|
|
69
|
+
summary = ReloadSummary()
|
|
70
|
+
application_manager = _manager(Application, "all_objects")
|
|
71
|
+
module_manager = _manager(Module, "all_objects")
|
|
72
|
+
landing_manager = _manager(Landing, "all_objects")
|
|
73
|
+
role_manager = _manager(NodeRole, "all_objects")
|
|
74
|
+
|
|
75
|
+
for role_name, module_definitions in ROLE_MODULE_DEFAULTS.items():
|
|
76
|
+
try:
|
|
77
|
+
role = role_manager.get(name=role_name)
|
|
78
|
+
except NodeRole.DoesNotExist:
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
summary.roles_processed += 1
|
|
82
|
+
|
|
83
|
+
for definition in module_definitions:
|
|
84
|
+
app_name: str = definition["application"] # type: ignore[assignment]
|
|
85
|
+
try:
|
|
86
|
+
application = application_manager.get(name=app_name)
|
|
87
|
+
except Application.DoesNotExist:
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
module, created = module_manager.get_or_create(
|
|
91
|
+
node_role=role,
|
|
92
|
+
path=definition["path"],
|
|
93
|
+
defaults={
|
|
94
|
+
"application": application,
|
|
95
|
+
"menu": definition["menu"],
|
|
96
|
+
"is_seed_data": True,
|
|
97
|
+
"is_deleted": False,
|
|
98
|
+
},
|
|
99
|
+
)
|
|
100
|
+
if created:
|
|
101
|
+
summary.modules_created += 1
|
|
102
|
+
|
|
103
|
+
module_updates: list[str] = []
|
|
104
|
+
if module.application_id != application.id:
|
|
105
|
+
module.application = application
|
|
106
|
+
module_updates.append("application")
|
|
107
|
+
if module.menu != definition["menu"]:
|
|
108
|
+
module.menu = definition["menu"] # type: ignore[index]
|
|
109
|
+
module_updates.append("menu")
|
|
110
|
+
if getattr(module, "is_deleted", False):
|
|
111
|
+
module.is_deleted = False
|
|
112
|
+
module_updates.append("is_deleted")
|
|
113
|
+
if not getattr(module, "is_seed_data", False):
|
|
114
|
+
module.is_seed_data = True
|
|
115
|
+
module_updates.append("is_seed_data")
|
|
116
|
+
if module_updates:
|
|
117
|
+
module.save(update_fields=module_updates)
|
|
118
|
+
if not created:
|
|
119
|
+
summary.modules_updated += 1
|
|
120
|
+
|
|
121
|
+
landings: Iterable[tuple[str, str]] = definition["landings"] # type: ignore[index]
|
|
122
|
+
for path, label in landings:
|
|
123
|
+
landing, landing_created = landing_manager.get_or_create(
|
|
124
|
+
module=module,
|
|
125
|
+
path=path,
|
|
126
|
+
defaults={
|
|
127
|
+
"label": label,
|
|
128
|
+
"description": "",
|
|
129
|
+
"enabled": True,
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
if landing_created:
|
|
133
|
+
summary.landings_created += 1
|
|
134
|
+
|
|
135
|
+
landing_updates: list[str] = []
|
|
136
|
+
if landing.label != label:
|
|
137
|
+
landing.label = label
|
|
138
|
+
landing_updates.append("label")
|
|
139
|
+
if landing.description:
|
|
140
|
+
landing.description = ""
|
|
141
|
+
landing_updates.append("description")
|
|
142
|
+
if not landing.enabled:
|
|
143
|
+
landing.enabled = True
|
|
144
|
+
landing_updates.append("enabled")
|
|
145
|
+
if getattr(landing, "is_deleted", False):
|
|
146
|
+
landing.is_deleted = False
|
|
147
|
+
landing_updates.append("is_deleted")
|
|
148
|
+
if not getattr(landing, "is_seed_data", False):
|
|
149
|
+
landing.is_seed_data = True
|
|
150
|
+
landing_updates.append("is_seed_data")
|
|
151
|
+
if landing_updates:
|
|
152
|
+
landing.save(update_fields=landing_updates)
|
|
153
|
+
if not landing_created:
|
|
154
|
+
summary.landings_updated += 1
|
|
155
|
+
|
|
156
|
+
return summary
|
pages/tasks.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Celery tasks for the pages application."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import timedelta
|
|
7
|
+
|
|
8
|
+
from celery import shared_task
|
|
9
|
+
|
|
10
|
+
from django.utils import timezone
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@shared_task
|
|
17
|
+
def create_user_story_github_issue(user_story_id: int) -> str | None:
|
|
18
|
+
"""Create a GitHub issue for the provided ``UserStory`` instance."""
|
|
19
|
+
|
|
20
|
+
from .models import UserStory
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
story = UserStory.objects.get(pk=user_story_id)
|
|
24
|
+
except UserStory.DoesNotExist: # pragma: no cover - defensive guard
|
|
25
|
+
logger.warning(
|
|
26
|
+
"User story %s no longer exists; skipping GitHub issue creation",
|
|
27
|
+
user_story_id,
|
|
28
|
+
)
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
if story.rating >= 5:
|
|
32
|
+
logger.info(
|
|
33
|
+
"Skipping GitHub issue creation for user story %s with rating %s",
|
|
34
|
+
story.pk,
|
|
35
|
+
story.rating,
|
|
36
|
+
)
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
if story.github_issue_url:
|
|
40
|
+
logger.info(
|
|
41
|
+
"GitHub issue already recorded for user story %s: %s",
|
|
42
|
+
story.pk,
|
|
43
|
+
story.github_issue_url,
|
|
44
|
+
)
|
|
45
|
+
return story.github_issue_url
|
|
46
|
+
|
|
47
|
+
issue_url = story.create_github_issue()
|
|
48
|
+
|
|
49
|
+
if issue_url:
|
|
50
|
+
logger.info(
|
|
51
|
+
"Created GitHub issue %s for user story %s", issue_url, story.pk
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
logger.info(
|
|
55
|
+
"No GitHub issue created for user story %s", story.pk
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return issue_url
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@shared_task
|
|
62
|
+
def purge_expired_landing_leads(days: int = 30) -> int:
|
|
63
|
+
"""Remove landing leads older than ``days`` days."""
|
|
64
|
+
|
|
65
|
+
from .models import LandingLead
|
|
66
|
+
|
|
67
|
+
cutoff = timezone.now() - timedelta(days=days)
|
|
68
|
+
queryset = LandingLead.objects.filter(created_on__lt=cutoff)
|
|
69
|
+
deleted, _ = queryset.delete()
|
|
70
|
+
if deleted:
|
|
71
|
+
logger.info(
|
|
72
|
+
"Purged %s landing leads older than %s days", deleted, days
|
|
73
|
+
)
|
|
74
|
+
return deleted
|