mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-21 18:25:58 +02:00
Merge pull request #147 from cisagov/nmb/whomami
Add logged-in /whoami page
This commit is contained in:
commit
18ee041e52
11 changed files with 174 additions and 171 deletions
|
@ -26,7 +26,7 @@ services:
|
||||||
# Run Django in debug mode on local
|
# Run Django in debug mode on local
|
||||||
- DJANGO_DEBUG=True
|
- DJANGO_DEBUG=True
|
||||||
# Tell Django where it is being hosted
|
# Tell Django where it is being hosted
|
||||||
- DJANGO_BASE_URL="localhost:8080"
|
- DJANGO_BASE_URL=http://localhost:8080
|
||||||
# --- These keys are obtained from `.env` file ---
|
# --- These keys are obtained from `.env` file ---
|
||||||
# Set a private JWT signing key for Login.gov
|
# Set a private JWT signing key for Login.gov
|
||||||
- DJANGO_SECRET_LOGIN_KEY
|
- DJANGO_SECRET_LOGIN_KEY
|
||||||
|
|
|
@ -22,8 +22,7 @@ i.e.
|
||||||
|
|
||||||
@use "uswds-core" as *;
|
@use "uswds-core" as *;
|
||||||
|
|
||||||
// Test custom style
|
// Test custom style (except this has not enough contrast)
|
||||||
p {
|
//p {
|
||||||
color: color('blue-10v');
|
// color: color('blue-10v');
|
||||||
}
|
//}
|
||||||
|
|
||||||
|
|
|
@ -164,6 +164,7 @@ TEMPLATES = [
|
||||||
"django.contrib.auth.context_processors.auth",
|
"django.contrib.auth.context_processors.auth",
|
||||||
"django.contrib.messages.context_processors.messages",
|
"django.contrib.messages.context_processors.messages",
|
||||||
"registrar.context_processors.language_code",
|
"registrar.context_processors.language_code",
|
||||||
|
"registrar.context_processors.canonical_path",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -379,7 +380,7 @@ AUTHENTICATION_BACKENDS = [
|
||||||
|
|
||||||
# this is where unauthenticated requests are redirected when using
|
# this is where unauthenticated requests are redirected when using
|
||||||
# the login_required() decorator, LoginRequiredMixin, or AccessMixin
|
# the login_required() decorator, LoginRequiredMixin, or AccessMixin
|
||||||
LOGIN_URL = "openid/openid/login"
|
LOGIN_URL = "openid/login"
|
||||||
|
|
||||||
# where to go after logging out
|
# where to go after logging out
|
||||||
LOGOUT_REDIRECT_URL = "home"
|
LOGOUT_REDIRECT_URL = "home"
|
||||||
|
@ -405,10 +406,8 @@ OIDC_PROVIDERS = {
|
||||||
},
|
},
|
||||||
"client_registration": {
|
"client_registration": {
|
||||||
"client_id": "cisa_dotgov_registrar",
|
"client_id": "cisa_dotgov_registrar",
|
||||||
"redirect_uris": [f"https://{env_base_url}/openid/callback/login/"],
|
"redirect_uris": [f"{env_base_url}/openid/callback/login/"],
|
||||||
"post_logout_redirect_uris": [
|
"post_logout_redirect_uris": [f"{env_base_url}/openid/callback/logout/"],
|
||||||
f"https://{env_base_url}/openid/callback/logout/"
|
|
||||||
],
|
|
||||||
"token_endpoint_auth_method": ["private_key_jwt"],
|
"token_endpoint_auth_method": ["private_key_jwt"],
|
||||||
"sp_private_key": secret_login_key,
|
"sp_private_key": secret_login_key,
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,10 +7,11 @@ For more information see:
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
|
||||||
from registrar.views import health, index, profile
|
from registrar.views import health, index, profile, whoami
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", index.index, name="home"),
|
path("", index.index, name="home"),
|
||||||
|
path("whoami", whoami.whoami, name="whoami"),
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path("health/", health.health),
|
path("health/", health.health),
|
||||||
path("edit_profile/", profile.edit_profile, name="edit-profile"),
|
path("edit_profile/", profile.edit_profile, name="edit-profile"),
|
||||||
|
|
|
@ -11,3 +11,13 @@ def language_code(request):
|
||||||
TEMPLATES dict of our settings file).
|
TEMPLATES dict of our settings file).
|
||||||
"""
|
"""
|
||||||
return {"LANGUAGE_CODE": settings.LANGUAGE_CODE}
|
return {"LANGUAGE_CODE": settings.LANGUAGE_CODE}
|
||||||
|
|
||||||
|
|
||||||
|
def canonical_path(request):
|
||||||
|
"""Add a canonical URL to the template context.
|
||||||
|
|
||||||
|
To make a correct "rel=canonical" link in the HTML page, we need to
|
||||||
|
construct an absolute URL for the page, and we can't do that in the
|
||||||
|
template itself, so we do it here and pass the information on.
|
||||||
|
"""
|
||||||
|
return {"CANONICAL_PATH": request.build_absolute_uri(request.path)}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
<title>
|
<title>
|
||||||
{% block title %}{% endblock %}
|
{% block title %}{% endblock %}
|
||||||
{{ site.name }}
|
.gov Registrar
|
||||||
{% block extra_title %}{% endblock %}
|
{% block extra_title %}{% endblock %}
|
||||||
</title>
|
</title>
|
||||||
<meta name="description" content="{% block description %}{% endblock %}">
|
<meta name="description" content="{% block description %}{% endblock %}">
|
||||||
|
@ -16,8 +16,6 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block extra_meta %}{% endblock extra_meta %}
|
{% block extra_meta %}{% endblock extra_meta %}
|
||||||
|
|
||||||
{# TO-DO: Determine if <link rel="manifest" href="site.webmanifest"> is desirable #}
|
|
||||||
|
|
||||||
{# TO-DO: set defaults for these #}
|
{# TO-DO: set defaults for these #}
|
||||||
<link rel="shortcut icon" href="{% static 'img/favicon.png' %}">
|
<link rel="shortcut icon" href="{% static 'img/favicon.png' %}">
|
||||||
<link rel="apple-touch-icon" href="{% static 'img/touch-icon.png' %}">
|
<link rel="apple-touch-icon" href="{% static 'img/touch-icon.png' %}">
|
||||||
|
@ -28,7 +26,7 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block canonical %}
|
{% block canonical %}
|
||||||
<link rel="canonical" href="{{ current_path }}">
|
<link rel="canonical" href="{{ CANONICAL_PATH }}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -49,16 +47,12 @@
|
||||||
<script src="{% static 'js/uswds.min.js' %}" defer></script>
|
<script src="{% static 'js/uswds.min.js' %}" defer></script>
|
||||||
<a class="usa-skipnav" href="#main-content">Skip to main content</a>
|
<a class="usa-skipnav" href="#main-content">Skip to main content</a>
|
||||||
|
|
||||||
<section class="usa-banner" aria-label="Official government website">
|
<section class="usa-banner" aria-label="Official government website">
|
||||||
<div class="usa-accordion">
|
<div class="usa-accordion">
|
||||||
<header class="usa-banner__header">
|
<header class="usa-banner__header">
|
||||||
<div class="usa-banner__inner">
|
<div class="usa-banner__inner">
|
||||||
<div class="grid-col-auto">
|
<div class="grid-col-auto">
|
||||||
<img
|
<img class="usa-banner__header-flag" src="{% static 'img/us_flag_small.png' %}" alt="U.S. flag" />
|
||||||
class="usa-banner__header-flag"
|
|
||||||
src="{% static 'img/us_flag_small.png' %}"
|
|
||||||
alt="U.S. flag"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="grid-col-fill tablet:grid-col-auto">
|
<div class="grid-col-fill tablet:grid-col-auto">
|
||||||
<p class="usa-banner__header-text">
|
<p class="usa-banner__header-text">
|
||||||
|
@ -68,28 +62,17 @@
|
||||||
Here’s how you know
|
Here’s how you know
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button class="usa-accordion__button usa-banner__button" aria-expanded="false"
|
||||||
class="usa-accordion__button usa-banner__button"
|
aria-controls="gov-banner-default-default">
|
||||||
aria-expanded="false"
|
|
||||||
aria-controls="gov-banner-default-default"
|
|
||||||
>
|
|
||||||
<span class="usa-banner__button-text">Here’s how you know</span>
|
<span class="usa-banner__button-text">Here’s how you know</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div class="usa-banner__content usa-accordion__content" id="gov-banner-default-default">
|
||||||
class="usa-banner__content usa-accordion__content"
|
|
||||||
id="gov-banner-default-default"
|
|
||||||
>
|
|
||||||
<div class="grid-row grid-gap-lg">
|
<div class="grid-row grid-gap-lg">
|
||||||
<div class="usa-banner__guidance tablet:grid-col-6">
|
<div class="usa-banner__guidance tablet:grid-col-6">
|
||||||
<img
|
<img class="usa-banner__icon usa-media-block__img" src="{% static 'img/icon-dot-gov.svg' %}" role="img"
|
||||||
class="usa-banner__icon usa-media-block__img"
|
alt="" aria-hidden="true" />
|
||||||
src="{% static 'img/icon-dot-gov.svg' %}"
|
|
||||||
role="img"
|
|
||||||
alt=""
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<div class="usa-media-block__body">
|
<div class="usa-media-block__body">
|
||||||
<p>
|
<p>
|
||||||
<strong>Official websites use .gov</strong><br />A
|
<strong>Official websites use .gov</strong><br />A
|
||||||
|
@ -99,37 +82,20 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="usa-banner__guidance tablet:grid-col-6">
|
<div class="usa-banner__guidance tablet:grid-col-6">
|
||||||
<img
|
<img class="usa-banner__icon usa-media-block__img" src="{% static 'img/icon-https.svg' %}" role="img" alt=""
|
||||||
class="usa-banner__icon usa-media-block__img"
|
aria-hidden="true" />
|
||||||
src="{% static 'img/icon-https.svg' %}"
|
|
||||||
role="img"
|
|
||||||
alt=""
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<div class="usa-media-block__body">
|
<div class="usa-media-block__body">
|
||||||
<p>
|
<p>
|
||||||
<strong>Secure .gov websites use HTTPS</strong><br />A
|
<strong>Secure .gov websites use HTTPS</strong><br />A
|
||||||
<strong>lock</strong> (
|
<strong>lock</strong> (
|
||||||
<span class="icon-lock"
|
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" width="52" height="64"
|
||||||
><svg
|
viewBox="0 0 52 64" class="usa-banner__lock-image" role="img"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
aria-labelledby="banner-lock-title-default banner-lock-description-default" focusable="false">
|
||||||
width="52"
|
|
||||||
height="64"
|
|
||||||
viewBox="0 0 52 64"
|
|
||||||
class="usa-banner__lock-image"
|
|
||||||
role="img"
|
|
||||||
aria-labelledby="banner-lock-title-default banner-lock-description-default"
|
|
||||||
focusable="false"
|
|
||||||
>
|
|
||||||
<title id="banner-lock-title-default">Lock</title>
|
<title id="banner-lock-title-default">Lock</title>
|
||||||
<desc id="banner-lock-description-default">A locked padlock</desc>
|
<desc id="banner-lock-description-default">A locked padlock</desc>
|
||||||
<path
|
<path fill="#000000" fill-rule="evenodd"
|
||||||
fill="#000000"
|
d="M26 0c10.493 0 19 8.507 19 19v9h3a4 4 0 0 1 4 4v28a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V32a4 4 0 0 1 4-4h3v-9C7 8.507 15.507 0 26 0zm0 8c-5.979 0-10.843 4.77-10.996 10.712L15 19v9h22v-9c0-6.075-4.925-11-11-11z" />
|
||||||
fill-rule="evenodd"
|
</svg> </span>) or <strong>https://</strong> means you’ve safely connected to
|
||||||
d="M26 0c10.493 0 19 8.507 19 19v9h3a4 4 0 0 1 4 4v28a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V32a4 4 0 0 1 4-4h3v-9C7 8.507 15.507 0 26 0zm0 8c-5.979 0-10.843 4.77-10.996 10.712L15 19v9h22v-9c0-6.075-4.925-11-11-11z"
|
|
||||||
/>
|
|
||||||
</svg> </span
|
|
||||||
>) or <strong>https://</strong> means you’ve safely connected to
|
|
||||||
the .gov website. Share sensitive information only on official,
|
the .gov website. Share sensitive information only on official,
|
||||||
secure websites.
|
secure websites.
|
||||||
</p>
|
</p>
|
||||||
|
@ -138,19 +104,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
{% block banner %}
|
{% block banner %}
|
||||||
<header class="usa-header usa-header-extended" role="banner">
|
<header class="usa-header usa-header-basic" role="navigation">
|
||||||
|
<div class="usa-nav-container">
|
||||||
<div class="usa-navbar">
|
<div class="usa-navbar">
|
||||||
{% block logo %}
|
{% block logo %}
|
||||||
<div class="usa-logo" id="extended-logo">
|
<div class="usa-logo" id="extended-logo">
|
||||||
<em class="usa-logo-text">
|
<em class="usa-logo__text">
|
||||||
<a href="/"
|
<a href="/" title="Home" aria-label="Home">
|
||||||
title="Home"
|
{% block site_name %}Home{% endblock %}
|
||||||
aria-label="Home">
|
|
||||||
{% block site_name %}{{ site.name }}{% endblock %}
|
|
||||||
</a>
|
</a>
|
||||||
</em>
|
</em>
|
||||||
</div>
|
</div>
|
||||||
|
@ -158,8 +123,23 @@
|
||||||
<button class="usa-menu-btn">Menu</button>
|
<button class="usa-menu-btn">Menu</button>
|
||||||
</div>
|
</div>
|
||||||
{% block usa_nav %}
|
{% block usa_nav %}
|
||||||
|
<nav>
|
||||||
|
<button type="button" class="usa-nav__close">
|
||||||
|
<img src="/public/img/usa-icons/close.svg" role="img" alt="Close" />
|
||||||
|
</button>
|
||||||
|
<ul class="usa-nav__primary usa-accordion">
|
||||||
|
<li class="usa-nav__primary-item">
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
User: <a href="/whoami">{{ user.get_username }}</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="/openid/login">Sign in</a>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
{% block usa_nav_secondary %}{% endblock %}
|
{% block usa_nav_secondary %}{% endblock %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{% endblock banner %}
|
{% endblock banner %}
|
||||||
{% block usa_overlay %}<div class="usa-overlay"></div>{% endblock %}
|
{% block usa_overlay %}<div class="usa-overlay"></div>{% endblock %}
|
||||||
|
@ -168,7 +148,7 @@
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
<ul class="messages">
|
<ul class="messages">
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>
|
<li{% if message.tags %} class="{{ message.tags }}" {% endif %}>
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -193,7 +173,7 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block footer %}
|
{% block footer %}
|
||||||
<div>
|
<div>
|
||||||
<p class="copyright">© {{ now.year }} {{ site.name }}</p>
|
<p class="copyright">© {% now "Y" %} CISA .gov Registrar</p>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -208,8 +188,6 @@
|
||||||
|
|
||||||
{% block extrascript %}{% endblock %}
|
{% block extrascript %}{% endblock %}
|
||||||
|
|
||||||
{# asynchronous analytics #}
|
|
||||||
<script async id="_fed_an_ua_tag" src="https://dap.digitalgov.gov/UniversalFederatedAnalyticsM
|
|
||||||
in.js?agency={{ AGENCY }}"></script>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -3,21 +3,20 @@
|
||||||
|
|
||||||
{% block title %} Hello {% endblock %}
|
{% block title %} Hello {% endblock %}
|
||||||
{% block hero %}
|
{% block hero %}
|
||||||
<section class="usa-hero">
|
<section class="usa-hero">
|
||||||
<div class="usa-grid">
|
<div class="usa-grid">
|
||||||
<div class="usa-hero-callout usa-section-dark">
|
<div class="usa-hero-callout usa-section-dark">
|
||||||
<h2>
|
<h2>
|
||||||
<span class="usa-hero-callout-alt">Welcome to the .gov registrar</span>
|
<span class="usa-hero-callout-alt">Welcome to the .gov registrar</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<p>This is the .gov registrar.</p>
|
<p>This is the .gov registrar.</p>
|
||||||
|
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<p><b>Hello {{ user.id }}</b></p>
|
|
||||||
<p><a href="/openid/logout/">Click here to log out.</a></p>
|
<p><a href="/openid/logout/">Click here to log out.</a></p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p><a href="/openid/login/">Click here to log in.</a></p>
|
<p><a href="/openid/login/">Click here to log in.</a></p>
|
||||||
|
|
|
@ -2,17 +2,8 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block title %} Hello {% endblock %}
|
{% block title %} Hello {% endblock %}
|
||||||
{% block hero %}
|
{% block content %}
|
||||||
<section class="usa-hero">
|
<p> Hello {{ user.last_name|default:"No last name given" }}, {{ user.first_name|default:"No first name given" }} <{{ user.email }}>! </p>
|
||||||
<div class="usa-grid">
|
|
||||||
<div class="usa-hero-callout usa-section-dark">
|
<p><a href="/openid/logout">Click here to log out</a></p>
|
||||||
<h2>
|
|
||||||
<span class="usa-hero-callout-alt">This is sample content.</span>
|
|
||||||
This is only sample content.
|
|
||||||
</h2>
|
|
||||||
<p> {{ name }} You'll want to replace it with content of your own.</p>
|
|
||||||
<button class="usa-button usa-button--accent-cool">Click a usa button</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -2,14 +2,25 @@ from django.test import Client, TestCase
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
|
||||||
class HealthTest(TestCase):
|
class TestViews(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.client = Client()
|
self.client = Client()
|
||||||
|
|
||||||
def test_health_check_endpoint(self):
|
def test_health_check_endpoint(self):
|
||||||
response = self.client.get("/health/")
|
response = self.client.get("/health/")
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertContains(response, "OK", status_code=200)
|
||||||
self.assertContains(response, "OK")
|
|
||||||
|
def test_home_page(self):
|
||||||
|
"""Home page should be available without a login."""
|
||||||
|
response = self.client.get("/")
|
||||||
|
self.assertContains(response, "registrar", status_code=200)
|
||||||
|
self.assertContains(response, "log in")
|
||||||
|
|
||||||
|
def test_whoami_page_no_user(self):
|
||||||
|
"""Whoami page not accessible without a logged-in user."""
|
||||||
|
response = self.client.get("/whoami")
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertIn("?next=/whoami", response.headers["Location"])
|
||||||
|
|
||||||
|
|
||||||
class LoggedInTests(TestCase):
|
class LoggedInTests(TestCase):
|
||||||
|
@ -23,6 +34,13 @@ class LoggedInTests(TestCase):
|
||||||
)
|
)
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
def test_whoami_page(self):
|
||||||
|
"""User information appears on the whoami page."""
|
||||||
|
response = self.client.get("/whoami")
|
||||||
|
self.assertContains(response, self.user.first_name)
|
||||||
|
self.assertContains(response, self.user.last_name)
|
||||||
|
self.assertContains(response, self.user.email)
|
||||||
|
|
||||||
def test_edit_profile(self):
|
def test_edit_profile(self):
|
||||||
response = self.client.get("/edit_profile/")
|
response = self.client.get("/edit_profile/")
|
||||||
self.assertContains(response, "Display Name")
|
self.assertContains(response, "Display Name")
|
||||||
|
|
|
@ -2,5 +2,5 @@ from django.shortcuts import render
|
||||||
|
|
||||||
|
|
||||||
def index(request):
|
def index(request):
|
||||||
context = {"name": "World!"}
|
"""This page is available to anyone without logging in."""
|
||||||
return render(request, "whoami.html", context)
|
return render(request, "home.html")
|
||||||
|
|
8
src/registrar/views/whoami.py
Normal file
8
src/registrar/views/whoami.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def whoami(request):
|
||||||
|
"""This is the first page someone goes to after logging in."""
|
||||||
|
return render(request, "whoami.html")
|
Loading…
Add table
Add a link
Reference in a new issue