Skip to content

Commit 104b799

Browse files
Merge pull request #60 from avalyset/fix/third-party-defaults
Make Gravatar opt-in and pin the Tailwind CDN version
2 parents 6b6275d + 8c51db5 commit 104b799

12 files changed

Lines changed: 78 additions & 21 deletions

File tree

‎.env.dev.example‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ VISUALIZER_RESULTS_DIR=./results
4444
# VISUALIZER_MAX_TREE_DEPTH=8
4545
# VISUALIZER_MAX_FILE_SIZE_MB=100
4646

47+
# --- Gravatar avatars -----------------------------------------------------------
48+
# Off by default: the user's initial is shown. Set to "true" to load avatars
49+
# from gravatar.com (the browser sends the MD5 hash of the user's email there).
50+
# SIMPLEAUDIT_GRAVATAR=true
51+
4752
# --- Chat (Open WebUI) ----------------------------------------------------------
4853
# On by default in dev; no line needed. To force it off:
4954
# SIMPLEAUDIT_CHAT=off (env)

‎.env.example‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ SIMPLEAUDIT_STUDIO_URL=http://localhost:8000
123123
# management) on or off. ON by default (preserves existing behavior). Set to
124124
# "off" to 404 the /otlp/* and /api/otlp/* routes and hide the OTLP button.
125125
#SIMPLEAUDIT_OTLP=off
126+
# Optional: show Gravatar avatars. OFF by default (the user's initial is shown).
127+
# Set to "true" to load them from gravatar.com; the browser then sends the MD5
128+
# hash of the user's email there.
129+
#SIMPLEAUDIT_GRAVATAR=true
126130

127131
# --- Optional observability -----------------------------------------------------
128132

‎config/settings.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,10 @@ def _csrf_trusted_origins() -> list[str]:
314314
WORKOS_API_KEY = os.environ.get("WORKOS_API_KEY", "")
315315
WORKOS_ENABLED = bool(WORKOS_CLIENT_ID and WORKOS_API_KEY)
316316

317+
# Gravatar avatars: off by default, since loading one sends the MD5 hash of the
318+
# user's email to gravatar.com. When off, templates show the user's initial.
319+
SIMPLEAUDIT_GRAVATAR = env_bool("SIMPLEAUDIT_GRAVATAR", False)
320+
317321
# Demo mode: prefill the login form with demo credentials and show a hint banner
318322
# (public demos / HF Spaces).
319323
DEMO_MODE = env_bool("DEMO_MODE", False)

‎docs/deployment.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ override them when you deliberately need to.
187187
| `BOOTSTRAP_USERNAME`, `BOOTSTRAP_EMAIL`, `BOOTSTRAP_PASSWORD`, `BOOTSTRAP_PROJECT_NAME` | First admin user and workspace. |
188188
| `DEMO_MODE`, `DEMO_USERNAME`, `DEMO_PASSWORD` | Prefilled demo login and the HF iframe cookie/CSRF policy. |
189189
| `WORKOS_CLIENT_ID`, `WORKOS_API_KEY` | Optional passwordless email sign-in. |
190+
| `SIMPLEAUDIT_GRAVATAR` | `true` = show Gravatar avatars (the browser sends the MD5 hash of the user's email to gravatar.com). Default off: the user's initial is shown. |
190191
| `SENTRY_DSN`, `SENTRY_ENVIRONMENT` | Optional error tracking. |
191192
| `LOG_LEVEL`, `PORT` | Logging level; web port for the CLI. |
192193
| any name in a connection's `secret_reference` | Model API key read at execution time. |

‎infra/context_processors.py‎

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,31 @@
22

33
import hashlib
44

5+
from django.conf import settings
56

6-
def gravatar_url(request):
7-
"""Expose the current user's Gravatar URL to templates.
87

9-
The avatar is derived from the user's email (MD5 hash), so no storage or
10-
upload is needed. Returns ``None`` for anonymous users or users without
11-
an email, letting templates fall back to initials.
8+
def gravatar_for(user) -> str | None:
9+
"""The user's Gravatar URL, or ``None`` so templates fall back to initials.
10+
11+
The URL carries the MD5 hash of the user's email, and the browser sends it
12+
to gravatar.com when it loads the image. Off unless ``SIMPLEAUDIT_GRAVATAR``
13+
is set. Also ``None`` for anonymous users and users without an email.
1214
"""
13-
user = getattr(request, "user", None)
15+
if not settings.SIMPLEAUDIT_GRAVATAR:
16+
return None
1417
if user is None or not user.is_authenticated or not user.email:
15-
return {"gravatar_url": None}
18+
return None
1619
email = user.email.strip().lower()
1720
md5_hash = hashlib.md5(email.encode("utf-8")).hexdigest()
18-
return {"gravatar_url": f"https://www.gravatar.com/avatar/{md5_hash}?d=identicon&s=128"}
21+
return f"https://www.gravatar.com/avatar/{md5_hash}?d=identicon&s=128"
22+
23+
24+
def gravatar_url(request):
25+
"""Expose the current user's Gravatar URL (or ``None``) and the setting."""
26+
return {
27+
"gravatar_url": gravatar_for(getattr(request, "user", None)),
28+
"gravatar_enabled": settings.SIMPLEAUDIT_GRAVATAR,
29+
}
1930

2031

2132
def admin_status(request):

‎infra/tests/test_gravatar.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Gravatar avatars are opt-in: off by default, no gravatar.com URL is rendered."""
2+
from django.contrib.auth.models import AnonymousUser
3+
from django.test import Client, TestCase, override_settings
4+
5+
from infra.context_processors import gravatar_for
6+
from infra.tests.factories import UserFactory
7+
8+
9+
class GravatarSettingTest(TestCase):
10+
def setUp(self):
11+
self.user = UserFactory(username="jane", email="Jane@X.com")
12+
self.client = Client(SERVER_NAME="localhost")
13+
self.client.force_login(self.user)
14+
15+
def test_off_by_default(self):
16+
self.assertIsNone(gravatar_for(self.user))
17+
content = self.client.get("/profile/").content.decode()
18+
self.assertNotIn("gravatar.com/avatar/", content)
19+
20+
def test_off_shows_no_gravatar_text_on_the_profile(self):
21+
content = self.client.get("/profile/").content.decode()
22+
self.assertNotIn("Add an email address to get a Gravatar avatar.", content)
23+
self.assertNotIn("Update image on Gravatar", content)
24+
25+
@override_settings(SIMPLEAUDIT_GRAVATAR=True)
26+
def test_on_renders_the_gravatar_url(self):
27+
# MD5 of the trimmed, lower-cased email "jane@x.com".
28+
url = "https://www.gravatar.com/avatar/8e747d19e83ff063fbb509e348a296d4"
29+
self.assertTrue(gravatar_for(self.user).startswith(url))
30+
content = self.client.get("/profile/").content.decode()
31+
self.assertIn(url, content)
32+
33+
@override_settings(SIMPLEAUDIT_GRAVATAR=True)
34+
def test_on_still_none_without_a_signed_in_user_with_email(self):
35+
self.assertIsNone(gravatar_for(AnonymousUser()))
36+
self.assertIsNone(gravatar_for(UserFactory(username="noemail", email="")))

‎infra/visualizer.py‎

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -517,16 +517,10 @@ def _inject_sidebar(html: str, request) -> str:
517517

518518
# render_to_string with a plain context does not run context processors, so
519519
# pass the values the partial needs explicitly (user, gravatar).
520+
from infra.context_processors import gravatar_for
521+
520522
user = getattr(request, "user", None)
521-
gravatar = None
522-
if user is not None and user.is_authenticated and user.email:
523-
import hashlib
524-
525-
gravatar = (
526-
"https://www.gravatar.com/avatar/"
527-
+ hashlib.md5(user.email.strip().lower().encode("utf-8")).hexdigest()
528-
+ "?d=identicon&s=128"
529-
)
523+
gravatar = gravatar_for(user)
530524
sidebar = render_to_string(
531525
"partials/visualizer_sidebar.html",
532526
{"request": request, "user": user, "gravatar_url": gravatar},

‎static/scenario_viewer.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<title>SimpleAudit Scenario Viewer</title>
88
<link rel="icon" type="image/svg+xml" sizes="32x32" href="/static/favicon-32x32.svg">
99
<link rel="icon" type="image/svg+xml" sizes="16x16" href="/static/favicon-16x16.svg">
10-
<script src="https://cdn.tailwindcss.com"></script>
10+
<script src="https://cdn.tailwindcss.com/3.4.17"></script>
1111
<script>
1212
tailwind.config = {
1313
theme: {

‎static/visualizer.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<script>window.__VISUALIZER_API_BASE = '{{ visualizer_api_base|default:"" }}';</script>
99
<link rel="icon" type="image/svg+xml" sizes="32x32" href="/static/favicon-32x32.svg">
1010
<link rel="icon" type="image/svg+xml" sizes="16x16" href="/static/favicon-16x16.svg">
11-
<script src="https://cdn.tailwindcss.com"></script>
11+
<script src="https://cdn.tailwindcss.com/3.4.17"></script>
1212
<script>
1313
tailwind.config = {
1414
theme: {

‎templates/base.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
{% include "seo/head.html" with seo_title=page_title seo_description=page_description seo_robots=page_robots|default:"noindex,nofollow" seo_canonical=page_canonical seo_jsonld=page_jsonld og_image_url=og_image_url og_image_width=og_image_width og_image_height=og_image_height og_image_alt=og_image_alt %}
1818
<link rel="icon" type="image/svg+xml" sizes="32x32" href="{% static 'favicon-32x32.svg' %}">
1919
<link rel="icon" type="image/svg+xml" sizes="16x16" href="{% static 'favicon-16x16.svg' %}">
20-
<script src="https://cdn.tailwindcss.com"></script>
20+
<script src="https://cdn.tailwindcss.com/3.4.17"></script>
2121
<script>
2222
tailwind.config = {
2323
theme: {

0 commit comments

Comments
 (0)