-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/55 order of items on sidebar incorrect #69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
aa24529
5373f3e
236e533
01db1cc
5da1a12
168becd
de1d552
c191375
2c0a3cb
fb4c52a
1e6f185
9652ecc
87ec389
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| from django.contrib.auth.models import Group, Permission | ||
| from django.contrib.contenttypes.models import ContentType | ||
| from django.core.management.base import BaseCommand | ||
|
|
||
| from archive.models import Encounter, Record, Subject | ||
|
|
||
| class Command(BaseCommand): | ||
| help = ( | ||
| "Create or update the Curator group with required add/change permissions. " | ||
| "Safe to rerun: removes delete_* and auth.* permissions on each run, " | ||
| "but does not remove other manually granted permissions. " | ||
| "See docs/permissions.md for the full policy." | ||
| ) | ||
|
|
||
| def handle(self, *args, **options): | ||
| group, _ = Group.objects.get_or_create(name="Curator") | ||
| subject_ct = ContentType.objects.get_for_model(Subject) | ||
| encounter_ct = ContentType.objects.get_for_model(Encounter) | ||
| record_ct = ContentType.objects.get_for_model(Record) | ||
|
|
||
| subject_add = Permission.objects.get(codename="add_subject", content_type=subject_ct) | ||
| subject_change = Permission.objects.get(codename="change_subject", content_type=subject_ct) | ||
| encounter_add = Permission.objects.get(codename="add_encounter", content_type=encounter_ct) | ||
| encounter_change = Permission.objects.get(codename="change_encounter", content_type=encounter_ct) | ||
|
|
||
| required_perms = [subject_add, subject_change, encounter_add, encounter_change] | ||
| for perm in required_perms: | ||
| group.permissions.add(perm) | ||
|
|
||
| forbidden_perms = Permission.objects.filter( | ||
| content_type__in=[subject_ct, encounter_ct, record_ct], | ||
| codename__startswith="delete_", | ||
| ) | ||
| auth_management_perms = Permission.objects.filter(content_type__app_label="auth") | ||
| forbidden_perms = forbidden_perms | auth_management_perms | ||
|
|
||
| for perm in forbidden_perms: | ||
| group.permissions.remove(perm) | ||
|
|
||
| self.stdout.write(self.style.SUCCESS("Curator group permissions updated.")) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| # pyright: reportIncompatibleMethodOverride=false | ||
|
|
||
| from typing import Any | ||
|
|
||
| from django.db.models import Model | ||
| from rest_framework.permissions import SAFE_METHODS, BasePermission | ||
| from rest_framework.request import Request | ||
|
|
||
|
|
||
| class CuratorOrSuperuserEditPermission(BasePermission): | ||
| """Require model add/change perms for writes and auth for reads.""" | ||
|
|
||
| def has_permission(self, request: Request, view: Any): # pyright: ignore[reportIncompatibleMethodOverride] | ||
| if not request.user or not request.user.is_authenticated: | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. claude-sonnet-4.6 — Note:
This does not affect current consumers (
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, if fix is easy, apply.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. claude-sonnet-4.6 — Fixed in commit de1d5529f2a1a3ed0ffe4b2b7cd90a6cd9ee3ee7. The permission class now falls back to calling |
||
| return False | ||
| if request.user.is_superuser: | ||
| return True | ||
| if request.method in SAFE_METHODS: | ||
| return True | ||
| if request.method == "DELETE": | ||
| return False | ||
|
|
||
| qs = getattr(view, "queryset", None) | ||
| if qs is None and hasattr(view, "get_queryset"): | ||
| try: | ||
| qs = view.get_queryset() | ||
| except Exception: | ||
| qs = None | ||
| model: type[Model] | None = getattr(qs, "model", None) | ||
| if model is None: | ||
| return False | ||
|
|
||
| app_label = model._meta.app_label | ||
| model_name = model._meta.model_name | ||
| if model_name is None: | ||
| return False | ||
| if request.method == "POST": | ||
| return request.user.has_perm(f"{app_label}.add_{model_name}") | ||
| if request.method in {"PUT", "PATCH"}: | ||
| return request.user.has_perm(f"{app_label}.change_{model_name}") | ||
| return False | ||
|
|
||
|
|
||
| class RecordPermission(BasePermission): | ||
| """Allow authenticated users to read/create/update records.""" | ||
|
|
||
| def has_permission(self, request: Request, view: Any): # pyright: ignore[reportIncompatibleMethodOverride] | ||
| if not request.user or not request.user.is_authenticated: | ||
| return False | ||
| if request.method == "DELETE": | ||
| return bool(request.user.is_superuser) | ||
| return True | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,21 +6,24 @@ | |
| <div class="card shadow-sm bg-base-100 h-full grow"> | ||
| <div class="p-4 flex space-between align-center border-b-2 border-primary"> | ||
| <div class="flex grow items-end gap-2"> | ||
| <h1 class="text-3xl font-bold leading-none">BFD9000</h1> | ||
| <a href="{% url 'archive:index' %}" class="text-3xl font-bold leading-none">BFD9000</a> | ||
| <span class="font-medium text-base-content/70" style="align-self:flex-end;font-size:0.65rem;line-height:1;">v{{ app_version|default:"nover" }}</span> | ||
| </div> | ||
| <span class="my-auto text-sm text-nowrap mr-4">Logged in as: <strong>{{ user.username }}</strong></span> | ||
| <a href="{% url 'logout' %}" class="btn btn-sm grow-0">Logout</a> | ||
| <form method="post" action="{% url 'logout' %}" class="m-0"> | ||
| {% csrf_token %} | ||
| <button type="submit" class="btn btn-sm grow-0">Logout</button> | ||
| </form> | ||
| </div> | ||
|
|
||
| <div class="flex-[1_1_0] flex"> | ||
| <ul class="menu bg-base-300 w-36"> | ||
| <li> | ||
| <a href="{% url 'archive:index' %}" data-nav="home"> | ||
| <a href="{% url 'archive:subjects' %}" data-nav="subjects"> | ||
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" d="m2.25 12 8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" /> | ||
| <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" /> | ||
| </svg> | ||
| Home | ||
| Subjects | ||
| </a> | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. claude-sonnet-4.6 — Intentional or accidental: "Scan" sidebar entry removed The If this removal is intentional (scan is only accessible via Records), that should be noted. If accidental, the item needs to be restored.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Its intentional. Make a note of it.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. claude-sonnet-4.6 — Noted in commit c191375d1c7a1310742a5ec160b073b85d7c5a47. Added an HTML comment in the sidebar template confirming the Scan nav item removal is intentional and that scanning is accessible via the "Add Record" button in the Records view. |
||
| </li> | ||
| <li> | ||
|
|
@@ -39,22 +42,6 @@ <h1 class="text-3xl font-bold leading-none">BFD9000</h1> | |
| Records | ||
| </a> | ||
| </li> | ||
| <li> | ||
| <a href="{% url 'archive:subjects' %}" data-nav="subjects"> | ||
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" /> | ||
| </svg> | ||
| Subjects | ||
| </a> | ||
| </li> | ||
| <li> | ||
| <a href="{% url 'archive:scan' %}" data-nav="scan"> | ||
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" d="M6.429 9.75 2.25 12l4.179 2.25m0-4.5 5.571 3 5.571-3m-11.142 0L2.25 7.5 12 2.25l9.75 5.25-4.179 2.25m0 0L21.75 12l-4.179 2.25m0 0 4.179 2.25L12 21.75 2.25 16.5l4.179-2.25m11.142 0-5.571 3-5.571-3" /> | ||
| </svg> | ||
| Scan | ||
| </a> | ||
| </li> | ||
|
Comment on lines
43
to
-57
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removing scan as a sidebar item makes sense but I think the distinction between "creating a new subject / encounter in a form" and "actually having to scan it in" was valuable. Currently, scanning remains at I propose two changes (I'll put review comments):
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also if its under
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @aspiringLich : why not just make this the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. claude-sonnet-4.6 — Implemented in commit 1e6f185. The scan page has been moved from |
||
| <script> | ||
| // Highlight active navigation item based on current path, after stripping script_name prefix | ||
| document.addEventListener('DOMContentLoaded', () => { | ||
|
|
@@ -79,6 +66,8 @@ <h1 class="text-3xl font-bold leading-none">BFD9000</h1> | |
| } | ||
| }); | ||
| </script> | ||
| <!-- Scan nav item intentionally omitted: scanning is accessible via | ||
| the "Scan New Record" button on the Records view (/records/create/). --> | ||
| </ul> | ||
| <div class="flex-[1_1_0] flex flex-col overflow-y-auto"> | ||
| {% block content %}{% endblock %} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| """Tests for project auth endpoints.""" | ||
|
|
||
| from django.contrib.auth.models import User | ||
| from django.test import TestCase | ||
| from django.urls import reverse | ||
|
|
||
|
|
||
| class AuthViewTests(TestCase): | ||
| """Validate login/logout view behavior.""" | ||
|
|
||
| def test_logout_get_redirects_without_error(self): | ||
| user = User.objects.create_user(username="testuser", password="testpassword") | ||
| self.client.force_login(user) | ||
|
|
||
| response = self.client.get(reverse("logout")) | ||
| self.assertEqual(response.status_code, 302) | ||
| self.assertEqual(response.url, reverse("login")) | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. claude-sonnet-4.6 — Test gap: GET logout test does not assert session state
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, assert the session state.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. claude-sonnet-4.6 — Covered in commit 168becdab5e0427a07f1e481216b109c3bb329b0 (same as comment 1). The test now calls |
||
| self.assertNotIn('_auth_user_id', self.client.session) | ||
|
|
||
| def test_logout_post_logs_user_out(self): | ||
| user = User.objects.create_user(username="testuser", password="testpassword") | ||
| self.client.force_login(user) | ||
|
|
||
| response = self.client.post(reverse("logout")) | ||
| self.assertEqual(response.status_code, 302) | ||
| self.assertEqual(response.url, reverse("login")) | ||
| self.assertNotIn("_auth_user_id", self.client.session) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| """UI permission tests for subject and encounter create buttons.""" | ||
|
|
||
| from django.contrib.auth.models import Permission, User | ||
| from django.test import override_settings | ||
| from django.urls import reverse | ||
|
|
||
| from .base import CleanupTestCase | ||
|
|
||
|
|
||
| @override_settings( | ||
| STORAGES={ | ||
| "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"}, | ||
| "staticfiles": {"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"}, | ||
| } | ||
| ) | ||
| class PermissionUiTests(CleanupTestCase): | ||
| """Verify template button visibility follows Django permissions.""" | ||
|
|
||
| def test_regular_user_does_not_see_create_buttons(self): | ||
| user = User.objects.create_user(username="regular", password="testpassword") | ||
| self.client.force_login(user) | ||
|
|
||
| subjects_response = self.client.get(reverse("archive:subjects")) | ||
| encounters_response = self.client.get(reverse("archive:encounters")) | ||
|
|
||
| self.assertEqual(subjects_response.status_code, 200) | ||
| self.assertEqual(encounters_response.status_code, 200) | ||
| self.assertNotContains(subjects_response, "New Subject") | ||
| self.assertNotContains(encounters_response, "New Encounter") | ||
|
|
||
| def test_curator_like_user_sees_create_buttons(self): | ||
| user = User.objects.create_user(username="curator", password="testpassword") | ||
| add_subject = Permission.objects.get(codename="add_subject") | ||
| add_encounter = Permission.objects.get(codename="add_encounter") | ||
| user.user_permissions.add(add_subject, add_encounter) | ||
|
|
||
| self.client.force_login(user) | ||
|
|
||
| subjects_response = self.client.get(reverse("archive:subjects")) | ||
| encounters_response = self.client.get(reverse("archive:encounters")) | ||
|
|
||
| self.assertEqual(subjects_response.status_code, 200) | ||
| self.assertEqual(encounters_response.status_code, 200) | ||
| self.assertContains(subjects_response, "New Subject") | ||
| self.assertContains(encounters_response, "New Encounter") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
claude-sonnet-4.6 — Note: command enforces only a subset of the documented permission matrix
The command removes
delete_*andauth.*permissions, but does not enforce the full documented matrix on reruns. For example, if someone manually grants the Curator groupadd_recordvia the admin, rerunning this command will not remove it (since only delete/auth perms are inforbidden_perms). According topermissions.md, Curators can create/update records, so this is currently correct behavior—but the command's docstring says "Safe to rerun" without clarifying that it only enforces the no-delete, no-auth-management constraints, not the complete matrix.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So you want to fix the docstring then? If this is compliant, to permissions.md, then we are good. You can fix docstring.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
claude-sonnet-4.6 — Fixed in commit 2c0a3cb826076edc8485a29ca41aa76e7a4610f0. The
helptext now explicitly states that reruns enforce only the no-delete/no-auth-management constraints and do not remove other manually granted permissions, with a pointer todocs/permissions.md.