Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions makeabilitylab/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

# Makeability Lab Global Variables, including Makeability Lab version
ML_WEBSITE_VERSION = "2.28.0" # Keep this updated with each release and also change the short description below
ML_WEBSITE_VERSION_DESCRIPTION = "Adds five fields to the public v1 REST API so Project Sidewalk's new About page can render its team section straight from this site instead of hand-maintaining a roster (#1426). /api/v1/people/<url_name>/ gains current_school and current_department alongside the existing current_title; /api/v1/projects/<short_name>/people/ gains position_title, position_school, and position_school_abbreviated -- what the person WAS when they joined the project, not what they are now, so a 2015 stint reads 'Undergrad, UMD' even though that person may be a professor today. (Jon's own 2012 Sidewalk role now correctly reports Assistant Professor / UMD rather than Professor / UW.) The new fields are named position_* rather than a bare 'title' so they don't read as part of 'role', the editor-written free-text description sitting beside them in the same record. Which position gets picked is an explicit, documented rule on ProjectRole.position_during_role: the position containing the role's start date, else the latest one already begun (the role started in a gap between positions), else the earliest (the role predates every recorded position -- common in older imported data), and null only for someone with no Position at all; ties on start date break on pk so two positions beginning the same day can't let DB row order decide the answer from one request to the next. Both project sub-resources now prefetch person__position_set and the people list prefetches position_set, so resolving a position per row rides the prefetch instead of costing a query each -- that matters at Sidewalk's ~150-person roster, and it also fixes a pre-existing N+1 behind current_title; a regression test pins the query count as flat as the roster grows. Separately, get_school_abbreviated is tightened twice: it no longer acronyms one-word affiliations (not every school is a university, and 'Easterseals' -> 'E' (or 'Cornell' -> 'C') is worse than the name itself), and its UW/UMD special cases now match the phrase 'university of washington/maryland' rather than a bare 'washington'/'maryland' substring, which used to hand UW's initials to Washington State and Washington University in St. Louis, and UMD's to UMBC. That is a visible fix on the public People page too, not just in the API. No model changes and no migration; v1 stays additive-only, so existing consumers are unaffected."
ML_WEBSITE_VERSION = "2.28.1" # Keep this updated with each release and also change the short description below
ML_WEBSITE_VERSION_DESCRIPTION = "Adds two quick links to the admin header so a pair of easy-to-forget internal pages stop being invisible: Project People (/view-project-people/, the project-team roster, which nothing in the main site nav links to) and Activity Log (the site-wide admin action log added in #1413). The Activity Log link is wrapped in {% if user.is_superuser %} so it matches the gate on LogEntryAdmin itself -- editors and contributors never see a link to a page they cannot open, and the log's existence is not advertised to them. The roster link is labeled 'Project People' to match that page's own heading rather than inventing a second name for the same page, and both new-tab links now carry a visually-hidden '(opens in a new tab)' cue built on Django admin's own screen-reader helper class, so no new CSS ships. Also fixes a NoReverseMatch this change would otherwise have shipped: website/urls.py sets app_name = 'website', so the roster reverses as 'website:view_project_people', and the bare name raised inside the userlinks block of base_site.html -- the parent template of EVERY admin page -- which would have 500'd the whole admin for any logged-in staff user rather than merely breaking one link (and, on -test where DEBUG is True, shown a public traceback). It slipped past manual testing because hitting /view-project-people/ exercises the URL path rather than the reverse name, and an unauthenticated admin request 302s to login without ever rendering the template. A new regression test renders the admin index as both a superuser and a staff non-superuser, pinning both reverses, the superuser gate, and the new-tab cue, so a future URL-name rename fails a test instead of taking down the admin."
DATE_MAKEABILITYLAB_FORMED = datetime.date(2012, 1, 1) # Date Makeability Lab was formed
MAX_BANNERS = 7 # Maximum number of banners on a page

Expand Down
10 changes: 9 additions & 1 deletion website/templates/admin/base_site.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,15 @@
{% endblock %}

{% block userlinks %}
<a href="https://github.com/makeabilitylab/makeabilitylabwebsite" target="_blank" rel="noopener">GitHub</a> /
{% comment %}
Label matches the target page's own <h1>/<title> ("Project People Viewer")
so the link and the page it lands on agree. Both new-tab links carry a
visually-hidden warning: `.visually-hidden` is Django admin's own
screen-reader-only helper (admin/css/base.css), so no new CSS is needed.
{% endcomment %}
<a href="{% url 'website:view_project_people' %}" target="_blank" rel="noopener">Project People<span class="visually-hidden"> (opens in a new tab)</span></a> /
{% if user.is_superuser %}<a href="{% url 'admin:admin_logentry_changelist' %}">Activity Log</a> /{% endif %}
<a href="https://github.com/makeabilitylab/makeabilitylabwebsite" target="_blank" rel="noopener">GitHub<span class="visually-hidden"> (opens in a new tab)</span></a> /
{{ block.super }}
{% endblock %}

Expand Down
58 changes: 58 additions & 0 deletions website/tests/test_admin_header_links.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
Regression tests for the quick links in the admin header (``userlinks`` block
of ``website/templates/admin/base_site.html``).

Two properties matter: (1) both ``{% url %}`` names still resolve—a rename
would raise ``NoReverseMatch`` and 500 *every* admin page, not just the linked
one; and (2) the Activity Log link stays superuser-only, matching the gate on
``LogEntryAdmin`` itself.
"""

from django.contrib.auth import get_user_model
from django.urls import reverse

from website.tests.base import DatabaseTestCase

User = get_user_model()


class AdminHeaderQuickLinkTests(DatabaseTestCase):
"""The admin header renders, and the Activity Log link is superuser-only."""

def setUp(self):
self.superuser = User.objects.create_superuser(
username="root", email="root@example.com", password="pw")
self.editor = User.objects.create_user(
username="editor", email="editor@example.com", password="pw",
is_staff=True)

def test_superuser_sees_both_quick_links(self):
self.client.force_login(self.superuser)
response = self.client.get(reverse("admin:index"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, reverse("website:view_project_people"))
self.assertContains(response, reverse("admin:admin_logentry_changelist"))

def test_staff_non_superuser_does_not_see_activity_log(self):
self.client.force_login(self.editor)
response = self.client.get(reverse("admin:index"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, reverse("website:view_project_people"))
self.assertNotContains(
response, reverse("admin:admin_logentry_changelist"))

def test_project_people_target_is_publicly_reachable(self):
# The link opens in a new tab for any staff user, so it must not be
# gated behind a login the editor/contributor accounts may not pass.
response = self.client.get(reverse("website:view_project_people"))
self.assertEqual(response.status_code, 200)

def test_new_tab_links_announce_themselves(self):
# target="_blank" without a warning is a WCAG 3.2.5 nit; the cue is a
# visually-hidden span (Django admin's own screen-reader helper class).
self.client.force_login(self.superuser)
response = self.client.get(reverse("admin:index"))
self.assertContains(
response,
'<span class="visually-hidden"> (opens in a new tab)</span>',
count=2, html=False)
Loading