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.

@@ -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
- if requires_login and not request.user.is_authenticated:
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(Entity):
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
@@ -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