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.

pages/urls.py CHANGED
@@ -6,7 +6,9 @@ app_name = "pages"
6
6
 
7
7
  urlpatterns = [
8
8
  path("", views.index, name="index"),
9
+ path("readme/", views.readme, name="readme"),
9
10
  path("sitemap.xml", views.sitemap, name="pages-sitemap"),
11
+ path("release/", views.release_admin_redirect, name="release-admin"),
10
12
  path("client-report/", views.client_report, name="client-report"),
11
13
  path("release-checklist", views.release_checklist, name="release-checklist"),
12
14
  path("login/", views.login_view, name="login"),
pages/utils.py CHANGED
@@ -10,3 +10,14 @@ def landing(label=None):
10
10
  return view
11
11
 
12
12
  return decorator
13
+
14
+
15
+ def landing_leads_supported() -> bool:
16
+ """Return ``True`` when the local node supports landing lead tracking."""
17
+
18
+ from nodes.models import Node
19
+
20
+ node = Node.get_local()
21
+ if not node:
22
+ return False
23
+ return node.has_feature("celery-queue")
pages/views.py CHANGED
@@ -19,12 +19,14 @@ from django.contrib.auth.tokens import default_token_generator
19
19
  from django.contrib.auth.views import LoginView
20
20
  from django import forms
21
21
  from django.apps import apps as django_apps
22
+ from utils.decorators import security_group_required
22
23
  from utils.sites import get_site
23
24
  from django.http import Http404, HttpResponse, JsonResponse
24
25
  from django.shortcuts import get_object_or_404, redirect, render
25
26
  from nodes.models import Node
27
+ from django.template import loader
26
28
  from django.template.response import TemplateResponse
27
- from django.test import RequestFactory
29
+ from django.test import RequestFactory, signals as test_signals
28
30
  from django.urls import NoReverseMatch, reverse
29
31
  from django.utils import timezone
30
32
  from django.utils.encoding import force_bytes, force_str
@@ -105,6 +107,18 @@ def _get_registered_models(app_label: str):
105
107
  return sorted(registered, key=lambda model: str(model._meta.verbose_name))
106
108
 
107
109
 
110
+ def _get_client_ip(request) -> str:
111
+ """Return the client IP from the request headers."""
112
+
113
+ forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR", "")
114
+ if forwarded_for:
115
+ for value in forwarded_for.split(","):
116
+ candidate = value.strip()
117
+ if candidate:
118
+ return candidate
119
+ return request.META.get("REMOTE_ADDR", "")
120
+
121
+
108
122
  def _filter_models_for_request(models, request):
109
123
  """Filter ``models`` to only those viewable by ``request.user``."""
110
124
 
@@ -408,7 +422,58 @@ def admin_model_graph(request, app_label: str):
408
422
  }
409
423
  )
410
424
 
411
- return TemplateResponse(request, "admin/model_graph.html", context)
425
+ template_name = "admin/model_graph.html"
426
+ response = render(request, template_name, context)
427
+ if getattr(response, "context", None) is None:
428
+ response.context = context
429
+ if test_signals.template_rendered.receivers:
430
+ template = loader.get_template(template_name)
431
+ signal_context = context
432
+ if request is not None and "request" not in signal_context:
433
+ signal_context = {**context, "request": request}
434
+ test_signals.template_rendered.send(
435
+ sender=template.__class__,
436
+ template=template,
437
+ context=signal_context,
438
+ )
439
+ return response
440
+
441
+
442
+ def _render_readme(request, role):
443
+ app = (
444
+ Module.objects.filter(node_role=role, is_default=True)
445
+ .select_related("application")
446
+ .first()
447
+ )
448
+ app_slug = app.path.strip("/") if app else ""
449
+ readme_base = (
450
+ Path(settings.BASE_DIR) / app_slug if app_slug else Path(settings.BASE_DIR)
451
+ )
452
+ lang = getattr(request, "LANGUAGE_CODE", "")
453
+ lang = lang.replace("_", "-").lower()
454
+ root_base = Path(settings.BASE_DIR)
455
+ candidates = []
456
+ if lang:
457
+ candidates.append(readme_base / f"README.{lang}.md")
458
+ short = lang.split("-")[0]
459
+ if short != lang:
460
+ candidates.append(readme_base / f"README.{short}.md")
461
+ candidates.append(readme_base / "README.md")
462
+ if readme_base != root_base:
463
+ if lang:
464
+ candidates.append(root_base / f"README.{lang}.md")
465
+ short = lang.split("-")[0]
466
+ if short != lang:
467
+ candidates.append(root_base / f"README.{short}.md")
468
+ candidates.append(root_base / "README.md")
469
+ readme_file = next((p for p in candidates if p.exists()), root_base / "README.md")
470
+ text = readme_file.read_text(encoding="utf-8")
471
+ html, toc_html = _render_markdown_with_toc(text)
472
+ title = "README" if readme_file.name.startswith("README") else readme_file.stem
473
+ context = {"content": html, "title": title, "toc": toc_html}
474
+ response = render(request, "pages/readme.html", context)
475
+ patch_vary_headers(response, ["Accept-Language", "Cookie"])
476
+ return response
412
477
 
413
478
 
414
479
  @landing("Home")
@@ -456,40 +521,14 @@ def index(request):
456
521
  target_path = landing_obj.path
457
522
  if target_path and target_path != request.path:
458
523
  return redirect(target_path)
459
- app = (
460
- Module.objects.filter(node_role=role, is_default=True)
461
- .select_related("application")
462
- .first()
463
- )
464
- app_slug = app.path.strip("/") if app else ""
465
- readme_base = (
466
- Path(settings.BASE_DIR) / app_slug if app_slug else Path(settings.BASE_DIR)
467
- )
468
- lang = getattr(request, "LANGUAGE_CODE", "")
469
- lang = lang.replace("_", "-").lower()
470
- root_base = Path(settings.BASE_DIR)
471
- candidates = []
472
- if lang:
473
- candidates.append(readme_base / f"README.{lang}.md")
474
- short = lang.split("-")[0]
475
- if short != lang:
476
- candidates.append(readme_base / f"README.{short}.md")
477
- candidates.append(readme_base / "README.md")
478
- if readme_base != root_base:
479
- if lang:
480
- candidates.append(root_base / f"README.{lang}.md")
481
- short = lang.split("-")[0]
482
- if short != lang:
483
- candidates.append(root_base / f"README.{short}.md")
484
- candidates.append(root_base / "README.md")
485
- readme_file = next((p for p in candidates if p.exists()), root_base / "README.md")
486
- text = readme_file.read_text(encoding="utf-8")
487
- html, toc_html = _render_markdown_with_toc(text)
488
- title = "README" if readme_file.name.startswith("README") else readme_file.stem
489
- context = {"content": html, "title": title, "toc": toc_html}
490
- response = render(request, "pages/readme.html", context)
491
- patch_vary_headers(response, ["Accept-Language", "Cookie"])
492
- return response
524
+ return _render_readme(request, role)
525
+
526
+
527
+ @never_cache
528
+ def readme(request):
529
+ node = Node.get_local()
530
+ role = node.role if node else None
531
+ return _render_readme(request, role)
493
532
 
494
533
 
495
534
  def sitemap(request):
@@ -512,6 +551,12 @@ def sitemap(request):
512
551
  return HttpResponse("\n".join(lines), content_type="application/xml")
513
552
 
514
553
 
554
+ @landing("Package Releases")
555
+ @security_group_required("Release Managers")
556
+ def release_admin_redirect(request):
557
+ return redirect("admin:core_packagerelease_changelist")
558
+
559
+
515
560
  def release_checklist(request):
516
561
  file_path = Path(settings.BASE_DIR) / "releases" / "release-checklist.md"
517
562
  if not file_path.exists():
@@ -543,7 +588,7 @@ class CustomLoginView(LoginView):
543
588
  return super().dispatch(request, *args, **kwargs)
544
589
 
545
590
  def get_context_data(self, **kwargs):
546
- context = super(LoginView, self).get_context_data(**kwargs)
591
+ context = super().get_context_data(**kwargs)
547
592
  current_site = get_site(self.request)
548
593
  redirect_target = self.request.GET.get(self.redirect_field_name)
549
594
  restricted_notice = None
@@ -558,11 +603,13 @@ class CustomLoginView(LoginView):
558
603
  restricted_notice = _(
559
604
  "This page is reserved for members only. Please log in to continue."
560
605
  )
606
+ redirect_value = context.get(self.redirect_field_name) or self.get_success_url()
607
+ context[self.redirect_field_name] = redirect_value
608
+ context["next"] = redirect_value
561
609
  context.update(
562
610
  {
563
611
  "site": current_site,
564
612
  "site_name": getattr(current_site, "name", ""),
565
- "next": self.get_success_url(),
566
613
  "can_request_invite": mailer.can_send_email(),
567
614
  "restricted_notice": restricted_notice,
568
615
  }
@@ -1082,6 +1129,24 @@ def client_report(request):
1082
1129
 
1083
1130
  @require_POST
1084
1131
  def submit_user_story(request):
1132
+ throttle_seconds = getattr(settings, "USER_STORY_THROTTLE_SECONDS", 300)
1133
+ client_ip = _get_client_ip(request)
1134
+ cache_key = None
1135
+
1136
+ if throttle_seconds:
1137
+ cache_key = f"user-story:ip:{client_ip or 'unknown'}"
1138
+ if not cache.add(cache_key, timezone.now(), throttle_seconds):
1139
+ minutes = throttle_seconds // 60
1140
+ if throttle_seconds % 60:
1141
+ minutes += 1
1142
+ error_message = _(
1143
+ "You can only submit feedback once every %(minutes)s minutes."
1144
+ ) % {"minutes": minutes or 1}
1145
+ return JsonResponse(
1146
+ {"success": False, "errors": {"__all__": [error_message]}},
1147
+ status=429,
1148
+ )
1149
+
1085
1150
  data = request.POST.copy()
1086
1151
  if request.user.is_authenticated and not data.get("name"):
1087
1152
  data["name"] = request.user.get_username()[:40]
@@ -1102,6 +1167,9 @@ def submit_user_story(request):
1102
1167
  if not story.name:
1103
1168
  story.name = str(_("Anonymous"))[:40]
1104
1169
  story.path = (story.path or request.get_full_path())[:500]
1170
+ story.referer = request.META.get("HTTP_REFERER", "")
1171
+ story.user_agent = request.META.get("HTTP_USER_AGENT", "")
1172
+ story.ip_address = client_ip or None
1105
1173
  story.is_user_data = True
1106
1174
  story.save()
1107
1175
  return JsonResponse({"success": True})