-
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 5 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,35 @@ | ||
| 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 correct permissions. Safe to rerun." | ||
|
|
||
| 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( | ||
|
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: command enforces only a subset of the documented permission matrix The command removes
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. So you want to fix the docstring then? If this is compliant, to permissions.md, then we are good. You can fix docstring.
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 2c0a3cb826076edc8485a29ca41aa76e7a4610f0. The |
||
| 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,46 @@ | ||
| # 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 | ||
|
|
||
| model: type[Model] | None = getattr(getattr(view, "queryset", None), "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', () => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,6 +47,7 @@ | |
| </table> | ||
| </div> | ||
| <div class="bg-base-300 p-2 flex items-center gap-3"> | ||
| <a href="{% url 'archive:scan' %}" class="btn btn-primary rounded-full">Add Record</a> | ||
|
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. Change to "Scan New Record" maybe?
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 — Done in commit 1e6f185. Button text updated from "Add Record" to "Scan New Record".
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. No, that's not a good idea. Revert to Add Record. This is an Add Record View, which happens to also have Scan and import file capabilities. Technically is should only have to let you select the path of the file, so add Record is fine. What would make sense, though, would be to separate the flow: first scan/import file above, then add the recrod details. This view shold be completely re-written: the original deisgn was workflow oriented, without giving the user options, step by step, wizard like. But that will be for another issue.
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. |
||
| <div class="grow"></div> | ||
| <span class="text-sm text-base-content/60"> | ||
| <span id="record-count-badge"></span> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| """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 |
||
|
|
||
| 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 — Bug (Low): GET /logout/ silently leaves the user logged in
The custom
logout_viewonly callslogout()on POST. A GET request redirects to login but the user's session remains active. Any bookmark, prefetch, or direct link to/logout/will silently fail to log the user out.The test
test_logout_get_redirects_without_errorpasses because it only checks the redirect status code, not whether the session was terminated.Since the template was changed to
<form method="post">, the GET path is no longer reachable from the UI. But the endpoint is still accessible. Consider either callinglogout()unconditionally, or returning 405 for GET.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.
Agreed, and fix tests to catch this.
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 168becdab5e0427a07f1e481216b109c3bb329b0.
logout()is now called unconditionally on all request methods. The testtest_logout_get_redirects_without_errornow also asserts_auth_user_idis absent from the session after a GET.