Skip to content
Merged
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
1 change: 1 addition & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3560,6 +3560,7 @@ path = [
"theming/scss/components/_toc.scss",
"theming/scss/components/_section_card.scss",
"theming/scss/components/_field_help.scss",
"theming/scss/components/_user_menu.scss",
"base/templates/components/**",
"base/templates/base/toc/**",
"base/static/css/about.css",
Expand Down
2 changes: 1 addition & 1 deletion base/static/css/bootstrap.min.css

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions base/styleguide.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import re
from dataclasses import dataclass
from pathlib import Path
from types import SimpleNamespace

from django.template.loader import render_to_string
from django.views.generic import TemplateView
Expand Down Expand Up @@ -224,6 +225,11 @@ def render_entries(request, context, entries=ENTRIES):
)


#: The signed-in user the user menu's entry shows, whoever reads the page
#: (pk 0: no account has it, so its links lead to nobody's dashboard).
EXAMPLE_USER = SimpleNamespace(pk=0, name="Ada Lovelace", is_authenticated=True)


class StyleguideView(TemplateView):
"""The catalogue: public, ``noindex``, linked from no menu.

Expand All @@ -241,5 +247,6 @@ def get_context_data(self, **kwargs):
context["token_modes"] = TOKEN_MODES
context["stock_tokens"] = context["token_mode"] == "bootstrap"
context["tokens"] = TOKENS
context["example_user"] = EXAMPLE_USER
context["entries"] = list(render_entries(self.request, context))
return context
24 changes: 2 additions & 22 deletions base/templates/base/_header.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% load static i18n fontawesome_5 django_bootstrap5 instance_theming %}
{% load instance_theming %}
<button class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
Expand Down Expand Up @@ -178,25 +178,5 @@
</div>
</li>
</ul>
</div>
<div class="ms-auto">
{% if request.user.is_authenticated %}
<!-- Profile Link -->
<a class="btn btn-link text-white p-0"
href="{% url 'login:settings' user.pk %} ">
{% fa5_icon 'user' 'fas' %} {{ user }}
</a>
{# URL provided by django-allauth/account/urls.py #}
<a href="{% url 'account_logout' %}" class="btn btn-danger btn-sm m-1">
{% fa5_icon 'arrow-right' 'fas' %} {% translate "Sign Out" %}</a>
{% else %}
{% if ACCOUNT_ALLOW_REGISTRATION %}
{# URL provided by django-allauth/account/urls.py #}
<a href="{% url 'account_signup' %}"
class="btn btn-info btn-sm m-1">{% translate "Sign Up" %}</a>
{% endif %}
{# URL provided by django-allauth/account/urls.py #}
<a class="btn btn-info btn-sm m-1"
href="{% url 'account_login' %}">{% translate "Sign In" %}</a>
{% endif %}
{% include "components/user_menu.html" with user=request.user %}
</div>
84 changes: 84 additions & 0 deletions base/templates/components/user_menu.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
{% load i18n fontawesome_5 %}
{% comment %}
user_menu (#2644): the navbar's account corner. Signed in, one menu under the
user's name (Dashboard, Settings, Sign out); signed out, Sign in as a nav link
and Sign up as the bar's one light button. Styles:
theming/scss/components/_user_menu.scss; catalogue entry:
styleguide/entries/user_menu.html.

user the signed-in user (anything with pk, name and is_authenticated), or
anyone else for the signed-out corner
id the menu toggle's id, "navbarDropdownUser" unless given (a second
corner on one page, the catalogue's, needs its own)

Placed last inside the navbar's .navbar-collapse. Below lg the collapsed menu
shows the same items as its own section instead of a menu in a menu.
{% endcomment %}
<ul class="navbar-nav ms-lg-auto align-items-lg-center user-menu">
{% if user.is_authenticated %}
{% url 'login:profile' user.pk as dashboard_url %}
{% url 'login:settings' user.pk as settings_url %}
{% url 'account_logout' as logout_url %}
{% firstof id "navbarDropdownUser" as toggle_id %}
<li class="nav-item dropdown d-none d-lg-block">
<a class="nav-link dropdown-toggle"
href="#"
id="{{ toggle_id }}"
role="button"
data-bs-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">{% fa5_icon 'user' 'fas' %} {{ user.name }}</a>
<ul class="dropdown-menu dropdown-menu-end"
aria-labelledby="{{ toggle_id }}">
<li>
<a class="dropdown-item"
href="{{ dashboard_url }}">{% translate "Dashboard" %}</a>
</li>
<li>
<a class="dropdown-item"
href="{{ settings_url }}">{% translate "Settings" %}</a>
</li>
<li>
<hr class="dropdown-divider" />
</li>
<li>
<form method="post" action="{{ logout_url }}">
{% csrf_token %}
<button type="submit"
class="dropdown-item">{% translate "Sign out" %}</button>
</form>
</li>
</ul>
</li>
<li class="nav-item d-lg-none">
<span class="navbar-text user-menu__name">{% fa5_icon 'user' 'fas' %} {{ user.name }}</span>
</li>
<li class="nav-item d-lg-none">
<a class="nav-link"
href="{{ dashboard_url }}">{% translate "Dashboard" %}</a>
</li>
<li class="nav-item d-lg-none">
<a class="nav-link"
href="{{ settings_url }}">{% translate "Settings" %}</a>
</li>
<li class="nav-item d-lg-none">
<form method="post" action="{{ logout_url }}">
{% csrf_token %}
<button type="submit"
class="nav-link">{% translate "Sign out" %}</button>
</form>
</li>
{% else %}
{# URLs provided by django-allauth/account/urls.py #}
<li class="nav-item">
<a class="nav-link"
href="{% url 'account_login' %}">{% translate "Sign in" %}</a>
</li>
{% if ACCOUNT_ALLOW_REGISTRATION %}
<li class="nav-item">
<a class="btn btn-on-strong btn-sm user-menu__sign-up"
href="{% url 'account_signup' %}">{% translate "Sign up" %}</a>
</li>
{% endif %}
{% endif %}
</ul>
15 changes: 15 additions & 0 deletions base/templates/styleguide/entries/user_menu.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{# use: The navbar's account corner, once per page, last in the navbar's collapse; signed in a menu under the user's name, signed out Sign in and the bar's one Sign up. Never anywhere else. #}
<div class="d-flex flex-column gap-3">
<nav class="navbar navbar-expand-lg navbar-dark bg-primary index-navbar"
aria-label="Signed in">
<div class="container-fluid">
{% include "components/user_menu.html" with user=example_user id="catalogueUserMenu" %}
</div>
</nav>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary index-navbar"
aria-label="Signed out">
<div class="container-fluid">
{% include "components/user_menu.html" with user=None %}
</div>
</nav>
</div>
10 changes: 6 additions & 4 deletions base/tests/page_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ class at all, is caught instead of drifting the page back. `lint` reads one
black or light grey), which is never an action's colour;
- a native `confirm(` or `alert(` in an inline script or event handler.

It reads the page's own content, `#main-content`, for the first three: the
navbar is the shell's, and the user menu (#2644) moves its last coloured
buttons. A page without `#main-content` is refused rather than passed. An
It reads the page's own content, `#main-content`, and the navbar above it
for the first three; the navbar has carried no coloured button since the user
menu (#2644). A page without `#main-content` is refused rather than passed. An
htmx fragment has no shell, so `fragment=True` reads all of it. Inline
scripts are read wherever they are on the page.

Expand All @@ -32,7 +32,7 @@ class at all, is caught instead of drifting the page back. `lint` reads one
import re
from html.parser import HTMLParser

from modelview.tests.html import element_markup
from modelview.tests.html import element_markup, elements_with_class

#: The four roles (spec #2632, "button"), as classes. `btn-outline-secondary`
#: is Bootstrap's own name for the secondary role and looks the same;
Expand Down Expand Up @@ -139,6 +139,8 @@ def lint(html: str, fragment: bool = False) -> list[str]:
content = html if fragment else element_markup(html, "main-content")
if not content:
raise ValueError("no #main-content: not a page, or a shell without one")
if not fragment:
content += "".join(elements_with_class(html, "index-navbar"))
for tag, attrs in _parse(content).elements:
classes = set((attrs.get("class") or "").split())
statuses = sorted(c for c in classes if STATUS_BUTTON.match(c))
Expand Down
2 changes: 1 addition & 1 deletion base/tests/page_texts/moved_styleguide.txt

Large diffs are not rendered by default.

28 changes: 8 additions & 20 deletions base/tests/test_page_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
def page(body: str) -> str:
"""A whole page whose own content is `body`."""
return (
'<html><body><nav class="navbar"><a class="btn btn-info">Sign Up</a></nav>'
'<html><body><nav class="navbar index-navbar">'
'<a class="btn btn-on-strong btn-sm" href="#">Sign up</a></nav>'
f'<main id="main-content">{body}</main></body></html>'
)

Expand Down Expand Up @@ -168,9 +169,12 @@ def test_hx_confirm_and_data_confirm_ask_the_house_dialog(self):
[],
)

def test_the_navbar_is_not_the_pages_own_content(self):
# The navbar's Sign Up is cyan until the user menu (#2644).
self.assertEqual(lint(page("")), [])
def test_the_navbar_is_read_too(self):
html = page("").replace("btn-on-strong", "btn-info")

problems = lint(html)
self.assertEqual(len(problems), 1)
self.assertIn("btn-info", problems[0])


@dataclass
Expand Down Expand Up @@ -439,13 +443,6 @@ def test_the_footer_is_the_blue_band_not_the_secondary_colour(self):
#: grey buttons are swapped by script (#2648), out of this scan's sight.
GONE_COLOURS = status_button()

#: Templates that still carry a status colour as an action, and the ticket
#: that removes it. Nothing else may.
STATUS_COLOURS_LEFT = {
# Sign Up and Sign In are cyan until the navbar's user menu.
"base/templates/base/_header.html": "#2644",
}

#: Template directories this scan does not read: the React apps' own (out of
#: scope, spec #2632) and anything installed.
NOT_TEMPLATES = ("node_modules", "site-packages", "/frontend/", "/static/")
Expand All @@ -461,8 +458,6 @@ def test_no_template_uses_a_status_colour_on_a_button(self):
relative = path.relative_to(root).as_posix()
if any(part in "/" + relative for part in NOT_TEMPLATES):
continue
if relative in STATUS_COLOURS_LEFT:
continue
for value in re.findall(r'class="([^"]*)"', path.read_text("utf-8")):
tokens = " ".join(re.split(r"{%.*?%}|\s+", value))
if any(GONE_COLOURS.match(token) for token in tokens.split()):
Expand All @@ -482,13 +477,6 @@ def test_no_theme_rule_extends_one_onto_a_button(self):
]
self.assertEqual(offenders, [])

def test_the_templates_left_still_need_their_ticket(self):
# When the user menu lands, its ticket deletes the entry above.
root = Path(settings.BASE_DIR)
for template in STATUS_COLOURS_LEFT:
with self.subTest(template=template):
self.assertRegex((root / template).read_text("utf-8"), r"\bbtn-info\b")


class CatalogueEntryTest(SimpleTestCase):
def test_the_button_entry_shows_every_role_size_and_state(self):
Expand Down
Loading
Loading