diff --git a/ak/views.py b/ak/views.py index 64a26a320..58e4db7d0 100644 --- a/ak/views.py +++ b/ak/views.py @@ -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 diff --git a/config/v3_urls.py b/config/v3_urls.py index 545b93adc..7dceef146 100644 --- a/config/v3_urls.py +++ b/config/v3_urls.py @@ -55,7 +55,6 @@ V3PasswordResetFromKeyDoneView, V3PasswordResetFromKeyView, V3PasswordResetView, - V3SignupView, ) v3_urlpatterns = [ @@ -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(), diff --git a/core/mixins.py b/core/mixins.py index ae0f9b246..f672b69e2 100644 --- a/core/mixins.py +++ b/core/mixins.py @@ -1,7 +1,9 @@ from django.http import Http404 -from django.urls import URLPattern, URLResolver, get_resolver +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. @@ -23,20 +25,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) self._v3_active = False 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): @@ -63,3 +67,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") + return context diff --git a/core/views.py b/core/views.py index b17dd28e5..b764f1b3f 100644 --- a/core/views.py +++ b/core/views.py @@ -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 def get_v3_context_data(self, **kwargs): ctx = super().get_v3_context_data(**kwargs) - print(self.request.headers) ctx["timezone"] = "America/Chicago" return ctx @@ -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:", diff --git a/libraries/views.py b/libraries/views.py index 866ff6a1d..34097d9ec 100644 --- a/libraries/views.py +++ b/libraries/views.py @@ -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 + ) view_str = self.kwargs.get("library_view_str") cpp_options = [("all", "All")] + list( @@ -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 @@ -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": diff --git a/news/views.py b/news/views.py index 05883abf2..480bbde0d 100644 --- a/news/views.py +++ b/news/views.py @@ -95,26 +95,6 @@ class EntryListView(V3Mixin, ListView): def libary_values(self): return [(x.slug, x.name) for x in Library.objects.all().order_by("name")] - def render_v3_response(self): - """Render the v3 template through Django's standard TemplateView pipeline.""" - if post_filter := self.request.GET.get("post-filter"): - match post_filter: - case "all": - return HttpResponseRedirect(reverse_lazy("news")) - case "blogpost": - return HttpResponseRedirect(reverse_lazy("news-blogpost-list")) - case "video": - return HttpResponseRedirect(reverse_lazy("news-video-list")) - case "news": - return HttpResponseRedirect(reverse_lazy("news-news-list")) - case "link": - return HttpResponseRedirect(reverse_lazy("news-link-list")) - - context = self.get_context_data( - **self.get_v3_context_data(), object_list=self.get_queryset() - ) - return self.render_to_response(context) - def get_v3_context_data(self, **kwargs): return { "filter_terms": [ @@ -154,6 +134,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): diff --git a/static/css/v3/auth-page.css b/static/css/v3/auth-page.css index fce53cccc..7375be9d1 100644 --- a/static/css/v3/auth-page.css +++ b/static/css/v3/auth-page.css @@ -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%; @@ -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 { @@ -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); +} + /* ── Divider ───────────────────────────────── */ .auth-page__divider { font-family: var(--font-display); @@ -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; +} + /* ── Social Card ───────────────────────────── */ .auth-page__social-card { display: flex; @@ -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 { diff --git a/static/css/v3/forms.css b/static/css/v3/forms.css index abe4fdbeb..53c95c8ca 100644 --- a/static/css/v3/forms.css +++ b/static/css/v3/forms.css @@ -641,6 +641,10 @@ color: var(--color-text-primary, #050816); } +.checkbox__label a { + text-decoration: underline; +} + .checkbox--disabled { opacity: 0.5; cursor: not-allowed; diff --git a/templates/v3/accounts/signup.html b/templates/v3/accounts/signup.html index b174dd783..a6320fe73 100644 --- a/templates/v3/accounts/signup.html +++ b/templates/v3/accounts/signup.html @@ -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 %}
Advance your career, learn from experts, and help shape the future of Boost and C++.
OR
- +Already have an account? Sign in
{% endblock auth_content %} diff --git a/users/forms.py b/users/forms.py index 4e1d0df8b..385c41050 100644 --- a/users/forms.py +++ b/users/forms.py @@ -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 @@ -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, diff --git a/users/views.py b/users/views.py index 3d03b8685..de76fc3a7 100644 --- a/users/views.py +++ b/users/views.py @@ -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 @@ -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 @@ -101,7 +102,7 @@ class CurrentUserProfileView( def get_v3_context_data(self, **kwargs): user = self.request.user - ctx = {} + ctx = super().get_v3_context_data(**kwargs) ctx["user_info"] = { "user_name": user.display_name, "avatar_url": user.get_avatar_url(), @@ -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: @@ -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 @@ -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" diff --git a/versions/views.py b/versions/views.py index 43c57aee4..f9243ee3d 100755 --- a/versions/views.py +++ b/versions/views.py @@ -174,7 +174,7 @@ def get_version_heading(self, obj, is_current_release): def get_v3_context_data(self, **kwargs): obj = self.object - ctx = {} + ctx = super().get_v3_context_data(**kwargs) ctx["hero_title"] = f"Latest release ({obj.display_name})" ctx["whats_new_heading"] = f"What's new in {obj.display_name}" ctx["whats_new_items"] = [