arthexis 0.1.21__py3-none-any.whl → 0.1.22__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/admin.py CHANGED
@@ -595,7 +595,23 @@ class ViewHistoryAdmin(EntityModelAdmin):
595
595
  )
596
596
 
597
597
  def traffic_data_view(self, request):
598
- return JsonResponse(self._build_chart_data())
598
+ return JsonResponse(
599
+ self._build_chart_data(days=self._resolve_requested_days(request))
600
+ )
601
+
602
+ def _resolve_requested_days(self, request, default: int = 30) -> int:
603
+ raw_value = request.GET.get("days")
604
+ if raw_value in (None, ""):
605
+ return default
606
+
607
+ try:
608
+ days = int(raw_value)
609
+ except (TypeError, ValueError):
610
+ return default
611
+
612
+ minimum = 1
613
+ maximum = 90
614
+ return max(minimum, min(days, maximum))
599
615
 
600
616
  def _build_chart_data(self, days: int = 30, max_pages: int = 8) -> dict:
601
617
  end_date = timezone.localdate()
@@ -689,11 +705,13 @@ class UserStoryAdmin(EntityModelAdmin):
689
705
  actions = ["create_github_issues"]
690
706
  list_display = (
691
707
  "name",
708
+ "language_code",
692
709
  "rating",
693
710
  "path",
694
711
  "status",
695
712
  "submitted_at",
696
713
  "github_issue_display",
714
+ "screenshot_display",
697
715
  "take_screenshot",
698
716
  "owner",
699
717
  "assign_to",
@@ -703,6 +721,7 @@ class UserStoryAdmin(EntityModelAdmin):
703
721
  "name",
704
722
  "comments",
705
723
  "path",
724
+ "language_code",
706
725
  "referer",
707
726
  "github_issue_url",
708
727
  "ip_address",
@@ -715,6 +734,7 @@ class UserStoryAdmin(EntityModelAdmin):
715
734
  "path",
716
735
  "user",
717
736
  "owner",
737
+ "language_code",
718
738
  "referer",
719
739
  "user_agent",
720
740
  "ip_address",
@@ -722,6 +742,7 @@ class UserStoryAdmin(EntityModelAdmin):
722
742
  "submitted_at",
723
743
  "github_issue_number",
724
744
  "github_issue_url",
745
+ "screenshot_display",
725
746
  )
726
747
  ordering = ("-submitted_at",)
727
748
  fields = (
@@ -729,7 +750,9 @@ class UserStoryAdmin(EntityModelAdmin):
729
750
  "rating",
730
751
  "comments",
731
752
  "take_screenshot",
753
+ "screenshot_display",
732
754
  "path",
755
+ "language_code",
733
756
  "user",
734
757
  "owner",
735
758
  "status",
@@ -758,6 +781,21 @@ class UserStoryAdmin(EntityModelAdmin):
758
781
  )
759
782
  if obj.github_issue_number is not None:
760
783
  return f"#{obj.github_issue_number}"
784
+ return ""
785
+
786
+ @admin.display(description=_("Screenshot"), ordering="screenshot")
787
+ def screenshot_display(self, obj):
788
+ if not obj.screenshot_id:
789
+ return ""
790
+ try:
791
+ url = reverse("admin:nodes_contentsample_change", args=[obj.screenshot_id])
792
+ except NoReverseMatch:
793
+ return obj.screenshot.path
794
+ return format_html(
795
+ '<a href="{}" target="_blank" rel="noopener noreferrer">{}</a>',
796
+ url,
797
+ _("View screenshot"),
798
+ )
761
799
  return _("Not created")
762
800
 
763
801
  @admin.action(description=_("Create GitHub issues"))
@@ -842,34 +880,93 @@ def favorite_toggle(request, ct_id):
842
880
  ct = get_object_or_404(ContentType, pk=ct_id)
843
881
  fav = Favorite.objects.filter(user=request.user, content_type=ct).first()
844
882
  next_url = request.GET.get("next")
845
- if fav:
846
- return redirect(next_url or "admin:favorite_list")
847
883
  if request.method == "POST":
884
+ if fav and request.POST.get("remove"):
885
+ fav.delete()
886
+ return redirect(next_url or "admin:index")
848
887
  label = request.POST.get("custom_label", "").strip()
849
888
  user_data = request.POST.get("user_data") == "on"
850
- Favorite.objects.create(
851
- user=request.user,
852
- content_type=ct,
853
- custom_label=label,
854
- user_data=user_data,
855
- )
889
+ priority_raw = request.POST.get("priority", "").strip()
890
+ if fav:
891
+ default_priority = fav.priority
892
+ else:
893
+ default_priority = 0
894
+ if priority_raw:
895
+ try:
896
+ priority = int(priority_raw)
897
+ except (TypeError, ValueError):
898
+ priority = default_priority
899
+ else:
900
+ priority = default_priority
901
+
902
+ if fav:
903
+ update_fields = []
904
+ if fav.custom_label != label:
905
+ fav.custom_label = label
906
+ update_fields.append("custom_label")
907
+ if fav.user_data != user_data:
908
+ fav.user_data = user_data
909
+ update_fields.append("user_data")
910
+ if fav.priority != priority:
911
+ fav.priority = priority
912
+ update_fields.append("priority")
913
+ if update_fields:
914
+ fav.save(update_fields=update_fields)
915
+ else:
916
+ Favorite.objects.create(
917
+ user=request.user,
918
+ content_type=ct,
919
+ custom_label=label,
920
+ user_data=user_data,
921
+ priority=priority,
922
+ )
856
923
  return redirect(next_url or "admin:index")
857
924
  return render(
858
925
  request,
859
926
  "admin/favorite_confirm.html",
860
- {"content_type": ct, "next": next_url},
927
+ {
928
+ "content_type": ct,
929
+ "favorite": fav,
930
+ "next": next_url,
931
+ "initial_label": fav.custom_label if fav else "",
932
+ "initial_priority": fav.priority if fav else 0,
933
+ "is_checked": fav.user_data if fav else True,
934
+ },
861
935
  )
862
936
 
863
937
 
864
938
  def favorite_list(request):
865
- favorites = Favorite.objects.filter(user=request.user).select_related(
866
- "content_type"
939
+ favorites = (
940
+ Favorite.objects.filter(user=request.user)
941
+ .select_related("content_type")
942
+ .order_by("priority", "pk")
867
943
  )
868
944
  if request.method == "POST":
869
- selected = request.POST.getlist("user_data")
945
+ selected = set(request.POST.getlist("user_data"))
870
946
  for fav in favorites:
871
- fav.user_data = str(fav.pk) in selected
872
- fav.save(update_fields=["user_data"])
947
+ update_fields = []
948
+ user_selected = str(fav.pk) in selected
949
+ if fav.user_data != user_selected:
950
+ fav.user_data = user_selected
951
+ update_fields.append("user_data")
952
+
953
+ priority_raw = request.POST.get(f"priority_{fav.pk}", "").strip()
954
+ if priority_raw:
955
+ try:
956
+ priority = int(priority_raw)
957
+ except (TypeError, ValueError):
958
+ priority = fav.priority
959
+ else:
960
+ if fav.priority != priority:
961
+ fav.priority = priority
962
+ update_fields.append("priority")
963
+ else:
964
+ if fav.priority != 0:
965
+ fav.priority = 0
966
+ update_fields.append("priority")
967
+
968
+ if update_fields:
969
+ fav.save(update_fields=update_fields)
873
970
  return redirect("admin:favorite_list")
874
971
  return render(request, "admin/favorite_list.html", {"favorites": favorites})
875
972
 
pages/apps.py CHANGED
@@ -1,13 +1,45 @@
1
+ import logging
2
+
1
3
  from django.apps import AppConfig
4
+ from django.db import DatabaseError
5
+ from django.db.backends.signals import connection_created
6
+
7
+
8
+ logger = logging.getLogger(__name__)
2
9
 
3
10
 
4
11
  class PagesConfig(AppConfig):
5
12
  default_auto_field = "django.db.models.BigAutoField"
6
13
  name = "pages"
7
14
  verbose_name = "7. Experience"
15
+ _view_history_purged = False
8
16
 
9
17
  def ready(self): # pragma: no cover - import for side effects
10
18
  from . import checks # noqa: F401
11
19
  from . import site_config
12
20
 
13
21
  site_config.ready()
22
+ connection_created.connect(
23
+ self._handle_connection_created,
24
+ dispatch_uid="pages_view_history_connection_created",
25
+ weak=False,
26
+ )
27
+
28
+ def _handle_connection_created(self, sender, connection, **kwargs):
29
+ if self._view_history_purged:
30
+ return
31
+ self._view_history_purged = True
32
+ self._purge_view_history()
33
+
34
+ def _purge_view_history(self, days: int = 15) -> None:
35
+ """Remove stale :class:`pages.models.ViewHistory` entries."""
36
+
37
+ from .models import ViewHistory
38
+
39
+ try:
40
+ deleted = ViewHistory.purge_older_than(days=days)
41
+ except DatabaseError:
42
+ logger.debug("Skipping view history purge; database unavailable", exc_info=True)
43
+ else:
44
+ if deleted:
45
+ logger.info("Purged %s view history entries older than %s days", deleted, days)
pages/forms.py CHANGED
@@ -171,15 +171,35 @@ class UserStoryForm(forms.ModelForm):
171
171
  "comments": forms.Textarea(attrs={"rows": 4, "maxlength": 400}),
172
172
  }
173
173
 
174
- def __init__(self, *args, **kwargs):
174
+ def __init__(self, *args, user=None, **kwargs):
175
+ self.user = user
175
176
  super().__init__(*args, **kwargs)
176
- self.fields["name"].required = False
177
- self.fields["name"].widget.attrs.update(
178
- {
179
- "maxlength": 40,
180
- "placeholder": _("Name, email or pseudonym"),
181
- }
182
- )
177
+
178
+ if user is not None and user.is_authenticated:
179
+ name_field = self.fields["name"]
180
+ name_field.required = False
181
+ name_field.label = _("Username")
182
+ name_field.initial = (user.get_username() or "")[:40]
183
+ name_field.widget.attrs.update(
184
+ {
185
+ "maxlength": 40,
186
+ "readonly": "readonly",
187
+ }
188
+ )
189
+ else:
190
+ self.fields["name"] = forms.EmailField(
191
+ label=_("Email address"),
192
+ max_length=40,
193
+ required=True,
194
+ widget=forms.EmailInput(
195
+ attrs={
196
+ "maxlength": 40,
197
+ "placeholder": _("name@example.com"),
198
+ "autocomplete": "email",
199
+ "inputmode": "email",
200
+ }
201
+ ),
202
+ )
183
203
  self.fields["take_screenshot"].initial = True
184
204
  self.fields["rating"].widget = forms.RadioSelect(
185
205
  choices=[(i, str(i)) for i in range(1, 6)]
@@ -194,5 +214,8 @@ class UserStoryForm(forms.ModelForm):
194
214
  return comments
195
215
 
196
216
  def clean_name(self):
217
+ if self.user is not None and self.user.is_authenticated:
218
+ return (self.user.get_username() or "")[:40]
219
+
197
220
  name = (self.cleaned_data.get("name") or "").strip()
198
221
  return name[:40]
pages/models.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import base64
2
2
  import logging
3
+ from datetime import timedelta
3
4
  from pathlib import Path
4
5
 
5
6
  from django.db import models
@@ -7,10 +8,11 @@ from django.db.models import Q
7
8
  from core.entity import Entity
8
9
  from core.models import Lead, SecurityGroup
9
10
  from django.contrib.sites.models import Site
10
- from nodes.models import NodeRole
11
+ from nodes.models import ContentSample, NodeRole
11
12
  from django.apps import apps as django_apps
13
+ from django.utils import timezone
12
14
  from django.utils.text import slugify
13
- from django.utils.translation import gettext, gettext_lazy as _
15
+ from django.utils.translation import gettext, gettext_lazy as _, get_language_info
14
16
  from importlib import import_module
15
17
  from django.urls import URLPattern
16
18
  from django.conf import settings
@@ -490,6 +492,14 @@ class ViewHistory(Entity):
490
492
  def __str__(self) -> str: # pragma: no cover - simple representation
491
493
  return f"{self.method} {self.path} ({self.status_code})"
492
494
 
495
+ @classmethod
496
+ def purge_older_than(cls, *, days: int) -> int:
497
+ """Delete history entries recorded more than ``days`` days ago."""
498
+
499
+ cutoff = timezone.now() - timedelta(days=days)
500
+ deleted, _ = cls.objects.filter(visited_at__lt=cutoff).delete()
501
+ return deleted
502
+
493
503
 
494
504
  class Favorite(Entity):
495
505
  user = models.ForeignKey(
@@ -500,9 +510,11 @@ class Favorite(Entity):
500
510
  content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
501
511
  custom_label = models.CharField(max_length=100, blank=True)
502
512
  user_data = models.BooleanField(default=False)
513
+ priority = models.IntegerField(default=0)
503
514
 
504
515
  class Meta:
505
516
  unique_together = ("user", "content_type")
517
+ ordering = ["priority", "pk"]
506
518
 
507
519
 
508
520
  class UserStory(Lead):
@@ -545,6 +557,19 @@ class UserStory(Lead):
545
557
  blank=True,
546
558
  help_text=_("Link to the GitHub issue created for this feedback."),
547
559
  )
560
+ screenshot = models.ForeignKey(
561
+ ContentSample,
562
+ on_delete=models.SET_NULL,
563
+ blank=True,
564
+ null=True,
565
+ related_name="user_stories",
566
+ help_text=_("Screenshot captured for this feedback."),
567
+ )
568
+ language_code = models.CharField(
569
+ max_length=15,
570
+ blank=True,
571
+ help_text=_("Language selected when the feedback was submitted."),
572
+ )
548
573
 
549
574
  class Meta:
550
575
  ordering = ["-submitted_at"]
@@ -590,6 +615,21 @@ class UserStory(Lead):
590
615
  f"**Screenshot requested:** {screenshot_requested}",
591
616
  ]
592
617
 
618
+ language_code = (self.language_code or "").strip()
619
+ if language_code:
620
+ normalized = language_code.replace("_", "-").lower()
621
+ try:
622
+ info = get_language_info(normalized)
623
+ except KeyError:
624
+ language_display = ""
625
+ else:
626
+ language_display = info.get("name_local") or info.get("name") or ""
627
+
628
+ if language_display:
629
+ lines.append(f"**Language:** {language_display} ({normalized})")
630
+ else:
631
+ lines.append(f"**Language:** {normalized}")
632
+
593
633
  if self.submitted_at:
594
634
  lines.append(f"**Submitted at:** {self.submitted_at.isoformat()}")
595
635