Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
6 changes: 0 additions & 6 deletions config/v3_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
V3PasswordResetFromKeyDoneView,
V3PasswordResetFromKeyView,
V3PasswordResetView,
V3SignupView,
)

v3_urlpatterns = [
Expand All @@ -74,11 +73,6 @@
V3AllTypesCreateView.as_view(),
name="v3-news-create",
),
path(
"v3/accounts/signup/",
V3SignupView.as_view(),
name="v3-signup",
),
path(
"v3/accounts/login/",
V3LoginView.as_view(),
Expand Down
35 changes: 32 additions & 3 deletions core/mixins.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from django.http import Http404
from django.urls import URLPattern, URLResolver, get_resolver
from django.http import Http404, HttpResponseNotFound
from django.urls import URLPattern, URLResolver, get_resolver, reverse_lazy
from waffle import flag_is_active

from core.templatetags.custom_static import large_static


class V3Mixin:
"""Renders a v3 template when the 'v3' waffle flag is active.
Expand All @@ -21,7 +23,11 @@ class V3Mixin:
v3_template_name = None

def dispatch(self, request, *args, **kwargs):
if self.v3_template_name and flag_is_active(request, "v3"):
if (
self.v3_template_name
and flag_is_active(request, "v3")
and request.method == "GET"
):
self._v3_active = True
return self.render_v3_response()
self._v3_active = False
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand Down Expand Up @@ -63,3 +69,26 @@ def walk(patterns):
yield entry, view_class

yield from walk(get_resolver().url_patterns)


class V3AuthContextMixin(V3Mixin):
"""Shared context for all V3 auth pages (signup, login, password reset, etc.)."""

def dispatch(self, request, *args, **kwargs):
if not flag_is_active(request, "v3"):
return HttpResponseNotFound()
return super().dispatch(request, *args, **kwargs)

def get_v3_context_data(self, **kwargs):
context = super().get_v3_context_data(**kwargs)
context["page_title"] = getattr(self, "page_title", "Account")
context["foreground_image_url"] = large_static(
"img/v3/auth-page/auth-page-foreground.png"
)
context["background_image_url"] = large_static(
"img/v3/auth-page/auth-page-background.png"
)
context["login_url"] = reverse_lazy("v3-login")
context["signup_url"] = reverse_lazy("v3-signup")
context["password_reset_url"] = reverse_lazy("v3-password-reset")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return context
31 changes: 29 additions & 2 deletions static/css/v3/auth-page.css
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ body:has(.auth-page) .header {
}

.auth-page__illustration-foreground {
margin-top: 80px; /* This is the size of the header navbar, added so that the content will not be hidden behind it */
margin-top: 80px;
/* This is the size of the header navbar, added so that the content will not be hidden behind it */
position: absolute;
inset: 0;
width: 100%;
Expand All @@ -85,7 +86,8 @@ body:has(.auth-page) .header {
justify-content: center;
align-items: center;
padding: var(--space-xlarge);
margin-top: 80px; /* This is the size of the header navbar, added so that the content will not be hidden behind it */
margin-top: 80px;
/* This is the size of the header navbar, added so that the content will not be hidden behind it */
}

.auth-page__content-inner {
Expand Down Expand Up @@ -175,6 +177,31 @@ body:has(.auth-page) .header {
background: var(--color-surface-weak);
}

/* ── Social Card Disable pending tou ────────────────────── */
.auth-page__signup-form:not(:has(input[name="accept_terms_of_use"]:checked)) a.btn-secondary,
.auth-page__signup-form:not(:has(input[name="accept_terms_of_use"]:checked)) button.btn {
opacity: 0.5;
pointer-events: none;
cursor: default;
}

.auth-page__signup-form:has(input[name="invisible-check-protect"]:checked):not(:has(input[name="accept_terms_of_use"]:checked)) .auth-page_tou-checkbox .checkbox__box {
background-color: var(--color-surface-error-weak);
border-color: var(--color-stroke-error);
}

.auth-page__signup-form:has(input[name="invisible-check-protect"]:checked):not(:has(input[name="accept_terms_of_use"]:checked)) .auth-page_tou-checkbox .checkbox__label {
color: var(--color-text-error);
}

.auth-page__signup-form:has(input[name="invisible-check-protect"]:checked):not(:has(input[name="accept_terms_of_use"]:checked)) .auth-page__invisible-check-label {
pointer-events: none;
}

.auth-page__invisible-check-label .btn {
width: 100%;
}

/* ── Responsive (Tablet) ────────────────────────────── */
@media (max-width: 1279px) {
.auth-page__wrapper {
Expand Down
4 changes: 4 additions & 0 deletions static/css/v3/forms.css
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,10 @@
color: var(--color-text-primary, #050816);
}

.checkbox__label a {
text-decoration: underline;
}

.checkbox--disabled {
opacity: 0.5;
cursor: not-allowed;
Expand Down
47 changes: 38 additions & 9 deletions templates/v3/accounts/signup.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,16 @@
redirect_field_value (string, optional, default unset) — URL to redirect to after signup; hidden input omitted when unset
password_rules (list, optional, default unset) — rule objects for password validation checklist
{% endcomment %}
{% load static %}
{% load static socialaccount %}
{% block auth_content %}
<div class="auth-page__header">
<h1 class="auth-page__title">Create an account</h1>
<p class="auth-page__subtitle">Advance your career, learn from experts, and help shape the future of Boost and C++.</p>
</div>
<form class="auth-page__card"
<form class="auth-page__card auth-page__signup-form"
id="signup_form"
method="post"
action=""
action="/accounts/signup/"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the project has a named signup URL and detect hardcoded signup actions.
fd -i 'urls.py' | xargs rg -n "account_signup|accounts/signup|name=['\"]account_signup['\"]"
rg -n 'action="/accounts/signup/"' templates

Repository: boostorg/website-v2

Length of output: 521


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect surrounding lines in the template
echo "---- templates/v3/accounts/signup.html:25-40 ----"
nl -ba templates/v3/accounts/signup.html | sed -n '25,45p'

# 2) Find all hardcoded signup action usages
echo "---- hardcoded action=\"/accounts/signup/\" in templates ----"
rg -n 'action="/accounts/signup/"' templates || true

# 3) Confirm how account_signup is defined and referenced
echo "---- definition of name=\"account_signup\" ----"
rg -n 'name="account_signup"' config/urls.py

echo "---- usages of account_signup (templates) ----"
rg -n "{%\s*url\s+['\"]account_signup['\"]\s*%}" -S templates || true

echo "---- usages of /accounts/signup/ in templates ----"
rg -n '/accounts/signup/' -S templates || true

# 4) Check tests expectations
echo "---- tests referencing account_signup ----"
rg -n 'account_signup|/accounts/signup/' users/tests/test_urls.py

Repository: boostorg/website-v2

Length of output: 174


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- templates/v3/accounts/signup.html:25-40 ----"
cat -n templates/v3/accounts/signup.html | sed -n '25,45p'

echo "---- hardcoded action=\"/accounts/signup/\" in templates ----"
rg -n 'action="/accounts/signup/"' templates || true

echo "---- definition of name=\"account_signup\" ----"
rg -n 'name="account_signup"' config/urls.py

echo "---- usages of account_signup via {% url %} (templates) ----"
rg -n "{%[[:space:]]*url[[:space:]]+['\"]account_signup['\"][[:space:]]*%}" -S templates || true

echo "---- usages of /accounts/signup/ (templates) ----"
rg -n '/accounts/signup/' -S templates || true

echo "---- tests referencing account_signup and /accounts/signup/ ----"
rg -n 'account_signup|/accounts/signup/' users/tests/test_urls.py

Repository: boostorg/website-v2

Length of output: 1987


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- config/urls.py:120-160 ----"
sed -n '120,160p' config/urls.py | cat -n

echo "---- any existing {% url 'account_signup' %} usage in templates ----"
rg -n "{%\s*url\s+['\"]account_signup['\"]\s*%}" -S templates || true

echo "---- any existing {% url account_signup %} usage (non-quoted) in templates ----"
rg -n "{%\s*url\s+account_signup\s*%}" -S templates || true

echo "---- all hardcoded /accounts/signup/ occurrences in templates ----"
rg -n '/accounts/signup/' templates

Repository: boostorg/website-v2

Length of output: 2569


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- templates/users/includes/user_join.html ----"
cat -n templates/users/includes/user_join.html | sed -n '1,80p'

echo "---- any 'account_signup' string in templates ----"
rg -n "account_signup" templates || true

echo "---- any url tag usage referencing account_* in templates ----"
rg -n "{%[[:space:]]*url" templates | head -n 50

echo "---- all hardcoded /accounts/signup/ occurrences in repo (limit to html/templates) ----"
rg -n '/accounts/signup/' templates

echo "---- all hardcoded /accounts/signup/ occurrences in entire repo (sanity) ----"
rg -n '/accounts/signup/' . | head -n 50

Repository: boostorg/website-v2

Length of output: 843


Use URL reversal for the signup form action (and remove hardcoded signup URLs)

templates/v3/accounts/signup.html hardcodes action="/accounts/signup/", but account_signup is a named URL; use {% url 'account_signup' %} to avoid path/prefix fragility.

Suggested fix
-    action="/accounts/signup/"
+    action="{% url 'account_signup' %}"

Also consider replacing the hardcoded <a href="/accounts/signup/"> in templates/users/includes/user_join.html with {% url 'account_signup' %}.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
action="/accounts/signup/"
action="{% url 'account_signup' %}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@templates/v3/accounts/signup.html` at line 30, Replace the hardcoded form
action and links that point to "/accounts/signup/" with Django URL reversal
using the named URL account_signup; update the form tag in
templates/v3/accounts/signup.html to use {% url 'account_signup' %} for the
action and likewise replace any <a href="/accounts/signup/"> occurrences (for
example in templates/users/includes/user_join.html) with {% url 'account_signup'
%} so paths follow the named route and respect any URL prefix changes.

novalidate
{# djlint:off #}
x-data="{ hasErrors: false }"
Expand All @@ -53,17 +54,45 @@ <h1 class="auth-page__title">Create an account</h1>
{% for error in form.non_field_errors %}<p class="field__error" role="alert">{{ error }}</p>{% endfor %}
{% endif %}
{% include "v3/includes/_field_checkbox.html" with name="mailing_list" label="Also join the Boost Developers Mailing List" %}
<label class="checkbox auth-page_tou-checkbox" for="checkbox-accept_terms_of_use">
<input
class="checkbox__input"
type="checkbox"
id="checkbox-accept_terms_of_use"
name="accept_terms_of_use"
value="on"
required
>
<span class="checkbox__box" aria-hidden="true">
{% include "includes/icon.html" with icon_name="check" icon_class="checkbox__check" %}
</span>
<span class="checkbox__label">I have read and accepted the <a href="{% url 'terms-of-use' %}">Term of Use</a> for this service</span>
</label>
{% if redirect_field_value %}
<input type="hidden"
name="{{ redirect_field_name }}"
value="{{ redirect_field_value }}">
{% endif %}

<label class="auth-page__invisible-check-label" for="checkbox-create">
<input name="invisible-check-protect" class="checkbox__input" id="checkbox-create" type="checkbox">
{% include "v3/includes/_button.html" with label="Create Account" type="submit" style="primary" alpine_disabled="hasErrors" %}
</form>
<p class="auth-page__divider">OR</p>
<div class="auth-page__social-card">
{% include "v3/includes/_button.html" with label="Continue with GitHub" url="#" icon_name="github" style="secondary" %}
{% include "v3/includes/_button.html" with label="Continue with Google" url="#" icon_name="google-colored" style="secondary" %}
</label>
<p class="auth-page__divider">OR</p>
{% get_providers as socialaccount_providers %}
{% if socialaccount_providers %}
{% for provider in socialaccount_providers %}
<label class="auth-page__invisible-check-label" for="checkbox-{{provider.name}}">
{% provider_login_url provider process="login" scope=scope auth_params=auth_params as href %}
<input name="invisible-check-protect" class="checkbox__input" id="checkbox-{{provider.name}}" type="checkbox">
{% if provider.name == "GitHub" %}
{% include "v3/includes/_button.html" with label="Continue with "|add:provider.name url=href icon_name="github" style="secondary" %}
{% elif provider.name == "Google" %}
{% include "v3/includes/_button.html" with label="Continue with "|add:provider.name url=href icon_name="google-colored" style="secondary" %}
{% endif %}
</label>
{% endfor %}
{% endif %}
{% include "v3/includes/_button.html" with label="Already have an account? Sign in →" url=login_url style="primary" %}
</div>
</form>
{% endblock auth_content %}
5 changes: 5 additions & 0 deletions users/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django import forms

from allauth.account.forms import ResetPasswordKeyForm
from allauth.account.forms import SignupForm

from .models import Preferences
from news.models import NEWS_MODELS
Expand All @@ -25,6 +26,10 @@ def save(self, **kwargs):
return result


class CustomSignUpForm(SignupForm):
accept_terms_of_use = forms.BooleanField(required=True)


class PreferencesForm(forms.ModelForm):
allow_notification_own_news_approved = forms.MultipleChoiceField(
choices=NEWS_ENTRY_CHOICES,
Expand Down
54 changes: 17 additions & 37 deletions users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import auth
from django.contrib.messages.views import SuccessMessageMixin
from django.http import HttpResponseNotFound, HttpResponseRedirect
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy
from django.views.generic import DetailView, FormView
from django.views.generic.base import TemplateView
Expand All @@ -22,17 +22,18 @@
from rest_framework import generics
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated, AllowAny

from waffle import flag_is_active

from core.constants import BadgeToken
from core.mixins import V3Mixin
from core.templatetags.custom_static import large_static
from core.mixins import V3Mixin, V3AuthContextMixin
from libraries.models import CommitAuthorEmail
from .forms import (
PreferencesForm,
UserProfileForm,
UserProfilePhotoForm,
DeleteAccountForm,
CustomSignUpForm,
)
from .models import User
from .password_rules import build_password_rules
Expand Down Expand Up @@ -547,7 +548,7 @@ def form_invalid(self, form):
return res if res else super().form_invalid(form)


class CustomSignupView(ClaimExistingAccountMixin, SignupView):
class CustomSignupView(ClaimExistingAccountMixin, V3AuthContextMixin, SignupView):
"""
Override the allauth SignupView to customize behavior:

Expand All @@ -556,6 +557,18 @@ class CustomSignupView(ClaimExistingAccountMixin, SignupView):
with authors and maintainers.
"""

v3_template_name = "v3/accounts/signup.html"

def get_form_class(self):
if flag_is_active(self.request, "v3"):
return CustomSignUpForm
return super().get_form_class()

def get_v3_context_data(self, **kwargs):
context = super().get_v3_context_data(**kwargs)
context["password_rules"] = build_password_rules()
return context

def form_invalid(self, form):
"""
Override this form to catch users who were created as part of the GitHub data
Expand Down Expand Up @@ -583,39 +596,6 @@ def get_context_data(self, **kwargs):
return context


class V3AuthContextMixin(V3Mixin):
"""Shared context for all V3 auth pages (signup, login, password reset, etc.)."""

def dispatch(self, request, *args, **kwargs):
if not flag_is_active(request, "v3"):
return HttpResponseNotFound()
return super().dispatch(request, *args, **kwargs)

def get_v3_context_data(self, **kwargs):
context = super().get_v3_context_data(**kwargs)
context["page_title"] = getattr(self, "page_title", "Account")
context["foreground_image_url"] = large_static(
"img/v3/auth-page/auth-page-foreground.png"
)
context["background_image_url"] = large_static(
"img/v3/auth-page/auth-page-background.png"
)
context["login_url"] = reverse_lazy("v3-login")
context["signup_url"] = reverse_lazy("v3-signup")
context["password_reset_url"] = reverse_lazy("v3-password-reset")
return context


class V3SignupView(V3AuthContextMixin, TemplateView):
v3_template_name = "v3/accounts/signup.html"
page_title = "Create An Account"

def get_v3_context_data(self, **kwargs):
context = super().get_v3_context_data(**kwargs)
context["password_rules"] = build_password_rules()
return context


class V3LoginView(V3AuthContextMixin, TemplateView):
v3_template_name = "v3/accounts/login.html"
page_title = "Login"
Expand Down
Loading