""" Custom wagtail settings used by Wagtail CRX. Settings are user-configurable on a per-site basis (multisite). Global project or developer settings should be defined in coderedcms.settings.py . """ from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import gettext_lazy as _ from modelcluster.fields import ParentalKey from modelcluster.models import ClusterableModel from wagtail.admin.panels import FieldPanel from wagtail.admin.panels import HelpPanel from wagtail.admin.panels import InlinePanel from wagtail.admin.panels import MultiFieldPanel from wagtail.contrib.settings.models import BaseSiteSetting from wagtail.contrib.settings.models import register_setting from wagtail.images import get_image_model_string from wagtail.models import Orderable from coderedcms.fields import MonospaceField from coderedcms.models.snippet_models import Footer from coderedcms.models.snippet_models import Navbar from coderedcms.settings import crx_settings def maybe_register_setting(disable: bool, **kwargs): """Decorator that conditionally registers a settings class.""" def check_if_disabled(model): if not disable: register_setting(model, **kwargs) return model return check_if_disabled @maybe_register_setting(crx_settings.CRX_DISABLE_LAYOUT, icon="cr-desktop") class LayoutSettings(ClusterableModel, BaseSiteSetting): """ Branding, navbar, and theme settings. """ class Meta: verbose_name = _("CRX Settings") class SpamService(models.TextChoices): NONE = ("", _("None")) HONEYPOT = ("honeypot", _("Basic - honeypot technique")) RECAPTCHA_V3 = ( "recaptcha3", _("reCAPTCHA v3 - Invisible (requires API key)"), ) RECAPTCHA_V2 = ( "recaptcha2", _("reCAPTCHA v2 - I am not a robot (requires API key)"), ) logo = models.ForeignKey( get_image_model_string(), null=True, blank=True, on_delete=models.SET_NULL, related_name="+", verbose_name=_("Logo"), help_text=_("Brand logo used in the navbar and throughout the site"), ) favicon = models.ForeignKey( get_image_model_string(), null=True, blank=True, on_delete=models.SET_NULL, related_name="favicon", verbose_name=_("Favicon"), ) navbar_color_scheme = models.CharField( blank=True, max_length=50, choices=None, default="", verbose_name=_("Navbar color scheme"), help_text=_( "Optimizes text and other navbar elements for use with light or " "dark backgrounds." ), ) navbar_class = models.CharField( blank=True, max_length=255, default="", verbose_name=_("Navbar CSS class"), help_text=_( 'Custom classes applied to navbar e.g. "bg-light", "bg-dark", "bg-primary".' ), ) navbar_fixed = models.BooleanField( default=False, verbose_name=_("Fixed navbar"), help_text=_( "Fixed navbar will remain at the top of the page when scrolling." ), ) navbar_content_fluid = models.BooleanField( default=False, verbose_name=_("Full width navbar contents"), help_text=_("Content within the navbar will fill edge to edge."), ) navbar_collapse_mode = models.CharField( blank=True, max_length=50, choices=None, default="", verbose_name=_("Collapse navbar menu"), help_text=_( "Control on what screen sizes to show and collapse the navbar menu links." ), ) navbar_format = models.CharField( blank=True, max_length=50, choices=None, default="", verbose_name=_("Navbar format"), ) navbar_search = models.BooleanField( default=True, verbose_name=_("Search box"), help_text=_("Show search box in navbar"), ) from_email_address = models.CharField( blank=True, max_length=255, verbose_name=_("From email address"), help_text=_( "The default email address this site appears to send from. " 'For example: "sender@example.com" or ' '"Sender Name " (without quotes)' ), ) search_num_results = models.PositiveIntegerField( default=10, verbose_name=_("Number of results per page"), ) external_new_tab = models.BooleanField( default=False, verbose_name=_("Open all external links in new tab") ) spam_service = models.CharField( blank=True, max_length=10, choices=SpamService.choices, default=SpamService.HONEYPOT, verbose_name=_("Spam Protection"), help_text=_( "Choose a technique or 3rd party service to help block spam submissions." ), ) recaptcha_threshold = models.DecimalField( default=0.5, max_digits=2, decimal_places=1, verbose_name=_("reCAPTCHA Threshold"), help_text=_( "reCAPTCHA v3 returns a score (0.0 is very likely a bot, " "1.0 is very likely a good interaction). " "Reject submissions below this score (recommended 0.5)." ), ) recaptcha_public_key = models.CharField( blank=True, max_length=255, verbose_name=_("reCAPTCHA Site Key (Public)"), help_text=_( "Create this key in the Google reCAPTCHA or Google Cloud dashboard." ), ) recaptcha_secret_key = models.CharField( blank=True, max_length=255, verbose_name=_("reCAPTCHA Secret Key (Private)"), help_text=_( "Create this key in the Google reCAPTCHA or Google Cloud dashboard." ), ) google_maps_api_key = models.CharField( blank=True, max_length=255, verbose_name=_("Google Maps API Key"), help_text=_("The API Key used for Google Maps."), ) mailchimp_api_key = models.CharField( blank=True, max_length=255, verbose_name=_("Mailchimp API Key"), help_text=_("The API Key used for Mailchimp."), ) navbar_panels = [ MultiFieldPanel( [ FieldPanel("navbar_color_scheme"), FieldPanel("navbar_class"), FieldPanel("navbar_fixed"), FieldPanel("navbar_content_fluid"), FieldPanel("navbar_collapse_mode"), FieldPanel("navbar_format"), FieldPanel("navbar_search"), ], heading=_("Site Navbar Layout"), ), InlinePanel( "site_navbar", help_text=_("Choose one or more navbars for your site."), heading=_("Site Navbars"), ), ] footer_panels = [ InlinePanel( "site_footer", help_text=_("Choose one or more footers for your site."), heading=_("Site Footers"), ), ] panels = [ MultiFieldPanel( [ FieldPanel("logo"), FieldPanel("favicon"), ], heading=_("Branding"), ), MultiFieldPanel( [ FieldPanel("from_email_address"), FieldPanel("search_num_results"), FieldPanel("external_new_tab"), ], heading=_("General"), ), MultiFieldPanel( [ FieldPanel("spam_service"), FieldPanel("recaptcha_threshold"), FieldPanel("recaptcha_public_key"), FieldPanel("recaptcha_secret_key"), ], heading=_("Form Settings"), ), MultiFieldPanel( [ FieldPanel("google_maps_api_key"), FieldPanel("mailchimp_api_key"), ], heading=_("API Keys"), ), ] if not crx_settings.CRX_DISABLE_NAVBAR: panels += navbar_panels if not crx_settings.CRX_DISABLE_FOOTER: panels += footer_panels def __init__(self, *args, **kwargs): """ Inject custom choices and defaults into the form fields to enable customization of settings without causing migration issues. """ super().__init__(*args, **kwargs) # Set choices dynamically. self._meta.get_field( "navbar_collapse_mode" ).choices = crx_settings.CRX_FRONTEND_NAVBAR_COLLAPSE_MODE_CHOICES self._meta.get_field( "navbar_color_scheme" ).choices = crx_settings.CRX_FRONTEND_NAVBAR_COLOR_SCHEME_CHOICES self._meta.get_field( "navbar_format" ).choices = crx_settings.CRX_FRONTEND_NAVBAR_FORMAT_CHOICES # Set default dynamically. if not self.id: self.navbar_class = crx_settings.CRX_FRONTEND_NAVBAR_CLASS_DEFAULT self.navbar_collapse_mode = ( crx_settings.CRX_FRONTEND_NAVBAR_COLLAPSE_MODE_DEFAULT ) self.navbar_color_scheme = ( crx_settings.CRX_FRONTEND_NAVBAR_COLOR_SCHEME_DEFAULT ) self.navbar_format = crx_settings.CRX_FRONTEND_NAVBAR_FORMAT_DEFAULT def clean(self): """ Make sure reCAPTCHA keys are set if selected. """ if self.spam_service in [ self.SpamService.RECAPTCHA_V3, self.SpamService.RECAPTCHA_V2, ] and not (self.recaptcha_public_key and self.recaptcha_secret_key): raise ValidationError(_("API keys are required to use reCAPTCHA.")) return super().clean() class NavbarOrderable(Orderable, models.Model): navbar_chooser = ParentalKey( LayoutSettings, related_name="site_navbar", verbose_name=_("Site Navbars"), ) navbar = models.ForeignKey( Navbar, blank=True, null=True, on_delete=models.CASCADE, ) panels = [FieldPanel("navbar")] class FooterOrderable(Orderable, models.Model): footer_chooser = ParentalKey( LayoutSettings, related_name="site_footer", verbose_name=_("Site Footers"), ) footer = models.ForeignKey( Footer, blank=True, null=True, on_delete=models.CASCADE, ) panels = [FieldPanel("footer")] @maybe_register_setting(crx_settings.CRX_DISABLE_ANALYTICS, icon="cr-google") class AnalyticsSettings(BaseSiteSetting): """ Tracking and Google Analytics. """ class Meta: verbose_name = _("Tracking") ga_g_tracking_id = models.CharField( blank=True, max_length=255, verbose_name=_("G Tracking ID"), help_text=_('Your Google Analytics 4 tracking ID (begins with "G-")'), ) ga_track_button_clicks = models.BooleanField( default=False, verbose_name=_("Track button clicks"), help_text=_( "Track all button clicks using Google Analytics event tracking. " "Event tracking details can be specified in each button’s advanced " "settings options." ), ) gtm_id = models.CharField( blank=True, max_length=255, verbose_name=_("Google Tag Manager ID"), help_text=_('Begins with "GTM-"'), ) head_scripts = MonospaceField( blank=True, null=True, verbose_name=_(" tracking scripts"), help_text=_("Add tracking scripts between the tags."), ) body_scripts = MonospaceField( blank=True, null=True, verbose_name=_(" tracking scripts"), help_text=_("Add tracking scripts toward closing tag."), ) panels = [ HelpPanel( heading=_("Know your tracking"), content=_( "

Which tracking IDs do I need?

" "

Before adding tracking to your site, " 'read about the difference between G, UA, GTM, ' "and other tracking IDs.

" ), ), MultiFieldPanel( [ FieldPanel("ga_g_tracking_id"), FieldPanel("ga_track_button_clicks"), ], heading=_("Google Analytics"), ), MultiFieldPanel( [ FieldPanel("gtm_id"), ], heading=_("Google Tag Manager"), ), MultiFieldPanel( [ FieldPanel("head_scripts"), FieldPanel("body_scripts"), ], heading=_("Other Tracking Scripts"), ), ]