Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
2 changes: 1 addition & 1 deletion ak/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def get_events(self):
return dict(sorted_events)

def get_v3_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx = super().get_v3_context_data(**kwargs)
ctx["install_card_pkg_managers"] = SharedResources.install_card_pkg_managers
ctx["install_card_system_install"] = SharedResources.install_card_system_install

Expand Down
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
47 changes: 38 additions & 9 deletions core/mixins.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from django.http import Http404
from django.urls import URLPattern, URLResolver, get_resolver
from django.urls import URLPattern, URLResolver, get_resolver, reverse_lazy
from django.views.generic import TemplateView
from waffle import flag_is_active

from core.templatetags.custom_static import large_static

class V3Mixin:

class V3Mixin(TemplateView):
"""Renders a v3 template when the 'v3' waffle flag is active.

Hooks into dispatch() to short-circuit the normal view flow (e.g.
Expand All @@ -23,20 +26,22 @@ class V3Mixin:
def dispatch(self, request, *args, **kwargs):
if self.v3_template_name and flag_is_active(request, "v3"):
self._v3_active = True
return self.render_v3_response()
return super().dispatch(request, *args, **kwargs)
Comment on lines 25 to +28

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 | 🏗️ Heavy lift

This bypasses existing render_v3_response() overrides.

Several supplied views still put real behavior in render_v3_response() instead of only template selection — see core/views.py Line 135, news/views.py Line 98, and libraries/views.py Line 462. After routing v3 requests straight through super().dispatch(...), those code paths never run, so v3 requests silently lose their redirect/cookie/context setup. Either keep the hook in V3Mixin.dispatch() or migrate the remaining overrides in the same PR.

🤖 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 `@core/mixins.py` around lines 26 - 29, The current V3Mixin.dispatch sets
self._v3_active and immediately returns super().dispatch, which skips any
render_v3_response() overrides; change dispatch so it still sets self._v3_active
= True, then call response = super().dispatch(request, *args, **kwargs), then
invoke self.render_v3_response(request, response) (or call it only if it
exists/if _v3_active) so view-specific v3 behavior (redirects/cookies/context)
runs, and finally return the (possibly modified) response; update
V3Mixin.dispatch to use this flow and reference self._v3_active,
render_v3_response, and dispatch when locating the code to change.

self._v3_active = False
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if not getattr(self, "template_name", None):
raise Http404
return super().dispatch(request, *args, **kwargs)

def get_context_data(self, **kwargs):
if getattr(self, "_v3_active", False):
context = super().get_context_data(**self.get_v3_context_data(**kwargs))
else:
context = super().get_context_data(**kwargs)
return context

def get_v3_context_data(self, **kwargs):
"""Override in subclasses to provide v3-specific context."""
return {}

def render_v3_response(self):
"""Render the v3 template through Django's standard TemplateView pipeline."""
context = self.get_context_data(**self.get_v3_context_data())
return self.render_to_response(context)
return {**kwargs}

def get_template_names(self):
if getattr(self, "_v3_active", False):
Expand All @@ -63,3 +68,27 @@ 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"):
if not getattr(self, "template_name", None):
raise Http404
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("account_signup")
context["password_reset_url"] = reverse_lazy("v3-password-reset")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return context
5 changes: 2 additions & 3 deletions core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,12 @@ class CalendarView(V3Mixin, TemplateView):
v3_template_name = "v3/calendar.html"

def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx = {}
ctx["boost_calendar"] = settings.BOOST_CALENDAR
return ctx
Comment on lines 116 to 119

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 | ⚡ Quick win

Don't bypass V3Mixin.get_context_data() here.

Starting from {} means the v3 path never merges get_v3_context_data(), so CalendarView.get_v3_context_data() is effectively dead and v3/calendar.html loses timezone and any future shared v3 keys. Keep the super().get_context_data(**kwargs) call and then add boost_calendar.

Suggested fix
     def get_context_data(self, **kwargs):
-        ctx = {}
+        ctx = super().get_context_data(**kwargs)
         ctx["boost_calendar"] = settings.BOOST_CALENDAR
         return ctx
📝 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
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx = {}
ctx["boost_calendar"] = settings.BOOST_CALENDAR
return ctx
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["boost_calendar"] = settings.BOOST_CALENDAR
return ctx
🤖 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 `@core/views.py` around lines 116 - 119, The current
CalendarView.get_context_data replaces the V3Mixin.get_context_data by starting
from {}, so CalendarView.get_v3_context_data and shared v3 keys (like timezone)
are never merged; change get_context_data to first call
super().get_context_data(**kwargs) (invoking V3Mixin.get_context_data), then set
ctx["boost_calendar"] = settings.BOOST_CALENDAR and return ctx so
v3/calendar.html receives the merged v3 context along with the boost_calendar
flag.


def get_v3_context_data(self, **kwargs):
ctx = super().get_v3_context_data(**kwargs)
print(self.request.headers)
ctx["timezone"] = "America/Chicago"
return ctx

Expand Down Expand Up @@ -502,7 +501,7 @@ class LearnPageView(V3Mixin, TemplateView):
v3_template_name = "v3/learn_page.html"

def get_v3_context_data(self, **kwargs):
ctx = self.get_context_data(**kwargs)
ctx = super().get_v3_context_data(**kwargs)
ctx["learn_card_data"] = [
{
"title": "I want to learn:",
Expand Down
19 changes: 7 additions & 12 deletions libraries/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,10 @@ class LibraryListBase(BoostVersionMixin, V3Mixin, VersionAlertMixin, ListView):
v3_template_name = "v3/library_page.html"

def get_v3_context_data(self, queryset=None, **kwargs):
context = {}
queryset = self.get_queryset()
context = super().get_v3_context_data(
**kwargs, object_list=queryset, queryset=queryset
)
Comment on lines 127 to +131

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 | 🟡 Minor | ⚡ Quick win

Reuse the resolved list queryset instead of calling get_queryset() again.

ListView.get() has already populated self.object_list before context building. Re-running get_queryset() here does the same work twice, and in this class it also replays the messages.add_message(...) path when no release data exists, which can duplicate the warning on a single request. Prefer the incoming queryset or self.object_list.

Suggested fix
     def get_v3_context_data(self, queryset=None, **kwargs):
-        queryset = self.get_queryset()
+        queryset = queryset if queryset is not None else self.object_list
         context = super().get_v3_context_data(
             **kwargs, object_list=queryset, queryset=queryset
         )
📝 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
def get_v3_context_data(self, queryset=None, **kwargs):
context = {}
queryset = self.get_queryset()
context = super().get_v3_context_data(
**kwargs, object_list=queryset, queryset=queryset
)
def get_v3_context_data(self, queryset=None, **kwargs):
queryset = queryset if queryset is not None else self.object_list
context = super().get_v3_context_data(
**kwargs, object_list=queryset, queryset=queryset
)
🤖 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 `@libraries/views.py` around lines 127 - 131, The get_v3_context_data method is
re-calling get_queryset() and can duplicate work and warnings; instead use the
resolved list passed in or the already-populated self.object_list. Change
get_v3_context_data to avoid calling get_queryset() again — prefer the incoming
queryset parameter if not None, else use self.object_list — and pass that value
to super().get_v3_context_data as object_list/queryset so you don't re-run
get_queryset() or replay messages.add_message paths.

view_str = self.kwargs.get("library_view_str")

cpp_options = [("all", "All")] + list(
Expand Down Expand Up @@ -258,16 +261,6 @@ def get_v3_context_data(self, queryset=None, **kwargs):
context["library_search_query"] = self.request.GET.get("q", "")
return context

def render_v3_response(self):
"""Render the v3 template through Django's standard TemplateView pipeline."""
queryset = self.get_queryset()
# Resolve selected_version once so get_v3_context_data can reuse it.
self._selected_version = self._resolve_selected_version()
context = self.get_context_data(
**self.get_v3_context_data(queryset=queryset), object_list=queryset
)
return self.render_to_response(context)

def _resolve_selected_version(self):
version_slug = determine_selected_boost_version(
self.kwargs.get("version_slug"), self.request
Expand Down Expand Up @@ -326,8 +319,10 @@ def get_categories(self, version=None):
)

def dispatch(self, request, *args, **kwargs):
"""Set the selected version in the cookies."""
# Resolve selected_version once so get_v3_context_data can reuse it.
self._selected_version = self._resolve_selected_version()
response = super().dispatch(request, *args, **kwargs)
"""Set the selected version in the cookies."""
set_selected_boost_version(self.kwargs.get("version_slug"), response)
view = get_prioritized_library_view(request)
if request.resolver_match.view_name == "libraries":
Expand Down
6 changes: 5 additions & 1 deletion news/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ def get_v3_context_data(self, **kwargs):
"libraries": self.libary_values,
"header_text": self.header_text,
"filter_value": self.filter_value,
**kwargs,
}

def get_queryset(self):
Expand All @@ -176,7 +177,10 @@ def get_queryset(self):
return result

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
object_list = self.get_queryset()
context = super().get_context_data(
queryset=object_list, object_list=object_list, **kwargs
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
context["is_moderator"] = False

if self.request.user.is_authenticated:
Expand Down
67 changes: 65 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 @@ -152,6 +154,24 @@ body:has(.auth-page) .header {
color: var(--color-text-link-accent);
}

/* ── Sign In Link ──────────────────────────── */
.auth-page__sign-in-link {
width: 100%;
text-align: center;
color: var(--color-text-primary);
font-family: var(--font-sans);
font-size: var(--font-size-small);
font-weight: var(--font-weight-regular);
line-height: var(--line-height-tight);
letter-spacing: var(--letter-spacing-tight);
margin-bottom: var(--space-large) !important;
}

.auth-page__sign-in-link a{
text-decoration: underline;
color: var(--color-text-link-accent);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/* ── Divider ───────────────────────────────── */
.auth-page__divider {
font-family: var(--font-display);
Expand All @@ -164,6 +184,24 @@ body:has(.auth-page) .header {
margin: 0;
}

.auth-page__signup-divider {
color: var(--color-text-tertiary);
font-family: var(--font-sans);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-regular);
line-height: var(--line-height-default);
letter-spacing: var(--letter-spacing-tight);
margin: 0;
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
}

.auth-page__signup-divider hr {
flex: 1 1 0;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/* ── Social Card ───────────────────────────── */
.auth-page__social-card {
display: flex;
Expand All @@ -175,6 +213,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
51 changes: 43 additions & 8 deletions templates/v3/accounts/signup.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@
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=""
novalidate
Expand Down Expand Up @@ -53,17 +54,51 @@ <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' %}">Terms 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" %}
</label>
<div class="auth-page__signup-divider">
<hr />
<span>
OR
</span>
<hr />
</div>
{% 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 %}
</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" %}
{% include "v3/includes/_button.html" with label="Already have an account? Sign in →" url=login_url style="primary" %}
</div>
<p class="auth-page__sign-in-link">Already have an account? <a href="{{login_url}}">Sign in</a></p>
{% 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
Loading
Loading