Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 2 additions & 2 deletions api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,8 @@ def test_give_today_api(self):
response = self.client.get('/apps/cms/api/give-today/')
self.assertEqual(response.status_code, 200)

def test_sticky_api(self):
response = self.client.get('/apps/cms/api/sticky/')
def test_emergency_api(self):
response = self.client.get('/apps/cms/api/emergency/')
self.assertEqual(response.status_code, 200)

def test_errata_resource_api(self):
Expand Down
4 changes: 2 additions & 2 deletions api/urls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.urls import include, path
from rest_framework import routers

from .views import AdopterViewSet, ImageViewSet, DocumentViewSet, customize_request, sticky_note, footer, schools, mapbox, flags, errata_fields, give_today, webview_settings
from .views import AdopterViewSet, ImageViewSet, DocumentViewSet, customize_request, emergency_messaging, footer, schools, mapbox, flags, errata_fields, give_today, webview_settings

router = routers.DefaultRouter()
router.register(r'images', ImageViewSet)
Expand All @@ -10,7 +10,7 @@

urlpatterns = [
path('', include(router.urls)),
path('sticky/', sticky_note, name='sticky_note'),
path('emergency/', emergency_messaging, name='emergency_messaging'),
path('footer/', footer, name='footer'),
path('schools/', schools, name='schools'),
path('mapbox/', mapbox, name='mapbox'),
Expand Down
17 changes: 5 additions & 12 deletions api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from rest_framework.parsers import JSONParser
from salesforce.models import Adopter, School, MapBoxDataset
from errata.models import ERRATA_RESOURCES
from global_settings.models import StickyNote, Footer, GiveToday
from global_settings.models import EmergencyMessaging, Footer, GiveToday
from wagtail.images.models import Image
from wagtail.documents.models import Document
from wagtail.models import Site
Expand Down Expand Up @@ -39,19 +39,12 @@ def get_queryset(self):
return Document.objects.all()


def sticky_note(request):
sticky_note = StickyNote.for_site(Site.find_for_request(request))
def emergency_messaging(request):
emergency = EmergencyMessaging.for_site(Site.find_for_request(request))

return JsonResponse({
'start': sticky_note.start,
'expires': sticky_note.expires,
'show_popup': sticky_note.show_popup,
'header': sticky_note.header,
'body': sticky_note.body,
'link_text': sticky_note.link_text,
'link': sticky_note.link,
'emergency_expires': sticky_note.emergency_expires,
'emergency_content': sticky_note.emergency_content,
'emergency_expires': emergency.emergency_expires,
'emergency_content': emergency.emergency_content,
})


Expand Down
42 changes: 42 additions & 0 deletions donations/migrations/0011_sitebanner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Generated by Django 5.1.15 on 2026-05-28 19:48

import django.db.models.deletion
import uuid
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('donations', '0010_merge_20250110_1518'),
('wagtailcore', '0095_groupsitepermission'),
('wagtailimages', '0027_image_description'),
]

operations = [
migrations.CreateModel(
name='SiteBanner',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('translation_key', models.UUIDField(default=uuid.uuid4, editable=False)),
('name', models.CharField(help_text="Admin-friendly name (e.g., 'Spring 2026 Campaign A')", max_length=255)),
('html_message', models.TextField(default='', help_text='HTML message to display in banner')),
('link_text', models.CharField(blank=True, help_text='Text for the call-to-action link', max_length=255, null=True)),
('link_url', models.URLField(blank=True, help_text='URL for the call-to-action link', null=True)),
('is_active', models.BooleanField(default=True, help_text='Enable/disable banner without deleting it')),
('start_date', models.DateTimeField(blank=True, help_text='When to start showing this banner (optional)', null=True)),
('end_date', models.DateTimeField(blank=True, help_text='When to stop showing this banner (optional)', null=True)),
('context_filter', models.CharField(choices=[('all', 'All Pages'), ('subjects', 'Subjects Pages'), ('book_details', 'Book Details Pages'), ('blog', 'Blog Pages'), ('url_pattern', 'Specific URL Pattern')], default='all', help_text='Where to display this banner', max_length=100)),
('url_pattern', models.CharField(blank=True, help_text="URL pattern for 'Specific URL Pattern' context (e.g., '/details/books/anatomy')", max_length=500, null=True)),
('locale', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='wagtailcore.locale', verbose_name='locale')),
('thumbnail', models.ForeignKey(blank=True, help_text='Optional thumbnail image for banner', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image')),
],
options={
'verbose_name': 'Site Banner',
'verbose_name_plural': 'Site Banners',
'ordering': ['-start_date'],
'abstract': False,
'unique_together': {('translation_key', 'locale')},
},
),
]
101 changes: 101 additions & 0 deletions donations/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from django.db import models
from django.core.exceptions import ValidationError
from wagtail.admin.panels import FieldPanel, MultiFieldPanel
from wagtail.models import TranslatableMixin
from openstax.functions import build_image_url

COLOR_SCHEME_CHOICES = (
('red', 'Red'),
Expand Down Expand Up @@ -77,3 +80,101 @@ class Fundraiser(models.Model):
goal_time = models.DateTimeField(blank=True, null=True)


CONTEXT_FILTER_CHOICES = (
('all', 'All Pages'),
('subjects', 'Subjects Pages'),
('book_details', 'Book Details Pages'),
('blog', 'Blog Pages'),
('url_pattern', 'Specific URL Pattern'),
)


class SiteBanner(TranslatableMixin, models.Model):
name = models.CharField(
max_length=255,
help_text="Admin-friendly name (e.g., 'Spring 2026 Campaign A')"
)

html_message = models.TextField(
default='',
help_text="HTML message to display in banner"
)
link_text = models.CharField(
max_length=255,
null=True,
blank=True,
help_text="Text for the call-to-action link"
)
link_url = models.URLField(
null=True,
blank=True,
help_text="URL for the call-to-action link"
)
thumbnail = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text="Optional thumbnail image for banner"
)

is_active = models.BooleanField(
default=True,
help_text="Enable/disable banner without deleting it"
)
start_date = models.DateTimeField(
null=True,
blank=True,
help_text="When to start showing this banner (optional)"
)
end_date = models.DateTimeField(
null=True,
blank=True,
help_text="When to stop showing this banner (optional)"
)

context_filter = models.CharField(
max_length=100,
default='all',
choices=CONTEXT_FILTER_CHOICES,
help_text="Where to display this banner"
)
url_pattern = models.CharField(
max_length=500,
blank=True,
null=True,
help_text="URL pattern for 'Specific URL Pattern' context (e.g., '/details/books/anatomy')"
)

def get_banner_thumbnail(self):
return build_image_url(self.thumbnail)

banner_thumbnail = property(get_banner_thumbnail)

panels = [
FieldPanel('name'),
FieldPanel('is_active'),
MultiFieldPanel([
FieldPanel('start_date'),
FieldPanel('end_date'),
], heading='Campaign Schedule'),
MultiFieldPanel([
FieldPanel('context_filter'),
FieldPanel('url_pattern'),
], heading='Targeting'),
MultiFieldPanel([
FieldPanel('html_message'),
FieldPanel('link_text'),
FieldPanel('link_url'),
FieldPanel('thumbnail'),
], heading='Content'),
]

def __str__(self):
return self.name

class Meta(TranslatableMixin.Meta):
verbose_name = 'Site Banner'
verbose_name_plural = 'Site Banners'
ordering = ['-start_date']
19 changes: 18 additions & 1 deletion donations/serializers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .models import ThankYouNote, DonationPopup, Fundraiser
from .models import ThankYouNote, DonationPopup, Fundraiser, SiteBanner
from rest_framework import serializers


Expand Down Expand Up @@ -52,6 +52,23 @@ class Meta:
'hide_donation_popup')


class SiteBannerSerializer(serializers.ModelSerializer):
class Meta:
model = SiteBanner
fields = ('id',
'name',
'html_message',
'link_text',
'link_url',
'banner_thumbnail',
'is_active',
'start_date',
'end_date',
'context_filter',
'url_pattern')
read_only_fields = fields


class FundraiserSerializer(serializers.ModelSerializer):
class Meta:
model = Fundraiser
Expand Down
1 change: 1 addition & 0 deletions donations/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
router = routers.SimpleRouter()
router.register(r'donation-popup', views.DonationPopupViewSet, basename='DonationPopup')
router.register(r'fundraiser', views.FundraiserViewSet, basename='Fundraiser')
router.register(r'sitebanner', views.SiteBannerViewSet, basename='SiteBanner')

urlpatterns = [
path('', include(router.urls)),
Expand Down
19 changes: 16 additions & 3 deletions donations/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from rest_framework import viewsets
from .models import ThankYouNote, DonationPopup, Fundraiser
from .serializers import ThankYouNoteSerializer, DonationPopupSerializer, FundraiserSerializer
from .models import ThankYouNote, DonationPopup, Fundraiser, SiteBanner
from .serializers import ThankYouNoteSerializer, DonationPopupSerializer, FundraiserSerializer, SiteBannerSerializer
from rest_framework.decorators import action
from django.db.models import Q
from django.utils import timezone
from django.http import JsonResponse

Expand Down Expand Up @@ -41,4 +42,16 @@ class DonationPopupViewSet(viewsets.ModelViewSet):
class FundraiserViewSet(viewsets.ModelViewSet):
serializer_class = FundraiserSerializer
queryset = Fundraiser.objects.all()
http_method_names = ['get']
http_method_names = ['get']


class SiteBannerViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = SiteBannerSerializer

def get_queryset(self):
now = timezone.now()
return SiteBanner.objects.filter(is_active=True).filter(
Q(start_date__isnull=True) | Q(start_date__lte=now)
).filter(
Q(end_date__isnull=True) | Q(end_date__gte=now)
)
Comment thread
TomWoodward marked this conversation as resolved.
19 changes: 10 additions & 9 deletions donations/wagtail_hooks.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from wagtail_modeladmin.options import ModelAdmin, ModelAdminGroup, modeladmin_register
from .models import DonationPopup, Fundraiser
from global_settings.models import GiveToday, StickyNote
from .models import DonationPopup, Fundraiser, SiteBanner
from global_settings.models import GiveToday


class DonationPopupAdmin(ModelAdmin):
Expand All @@ -27,19 +27,20 @@ class GiveTodayAdmin(ModelAdmin):
search_fields = ('give_link_text',)


class StickyNoteAdmin(ModelAdmin):
model = StickyNote
menu_icon = 'doc-empty'
menu_label = 'Sticky Note'
list_display = ('header', 'start', 'expires', 'show_popup')
search_fields = ('header', 'body',)
class SiteBannerAdmin(ModelAdmin):
model = SiteBanner
menu_icon = 'doc-full-inverse'
menu_label = 'Site Banners'
list_display = ('name', 'is_active', 'start_date', 'end_date', 'context_filter')
search_fields = ('name', 'html_message',)
list_filter = ('is_active', 'context_filter',)


class SiteMessagingModalsGroup(ModelAdminGroup):
menu_label = 'Site Messaging'
menu_icon = 'doc-full-inverse'
menu_order = 600
items = (DonationPopupAdmin, FundraiserAdmin, GiveTodayAdmin, StickyNoteAdmin,)
items = (DonationPopupAdmin, FundraiserAdmin, GiveTodayAdmin, SiteBannerAdmin,)
Comment thread
TomWoodward marked this conversation as resolved.


modeladmin_register(SiteMessagingModalsGroup)
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 5.1.15 on 2026-05-28 19:48

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('global_settings', '0021_auto_20201002_1012'),
('wagtailcore', '0095_groupsitepermission'),
]

operations = [
migrations.CreateModel(
name='EmergencyMessaging',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('emergency_expires', models.DateTimeField(blank=True, help_text='When active, displays emergency banner instead of regular banners', null=True)),
('emergency_content', models.CharField(blank=True, help_text='Emergency message to display', max_length=500)),
('site', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, to='wagtailcore.site')),
],
options={
'verbose_name': 'Emergency Messaging',
},
),
migrations.DeleteModel(
name='StickyNote',
),
Comment thread
TomWoodward marked this conversation as resolved.
]
32 changes: 21 additions & 11 deletions global_settings/models.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
from django.db import models
from wagtail.admin.panels import FieldPanel, MultiFieldPanel
from wagtail.contrib.settings.models import BaseSiteSetting, register_setting


class StickyNote(BaseSiteSetting):
start = models.DateTimeField(null=True, help_text="Set the start date to override the content of the Give Sticky. Set the header and body below to change.")
expires = models.DateTimeField(null=True, help_text="Set the date to expire overriding the content of the Give Sticky.")
show_popup = models.BooleanField(default=False, help_text="Replaces the top banner with a popup, start and expire dates still control timing.")
header = models.TextField(max_length=255)
body = models.TextField()
link_text = models.CharField(max_length=255)
link = models.URLField()
emergency_expires = models.DateTimeField(null=True, blank=True, help_text="When active, the Sticky Note will not be displayed until the emergency expires.")
emergency_content = models.CharField(max_length=255)
@register_setting(icon='warning')
class EmergencyMessaging(BaseSiteSetting):
emergency_expires = models.DateTimeField(
null=True,
blank=True,
help_text="When active, displays emergency banner instead of regular banners"
)
emergency_content = models.CharField(
max_length=500,
blank=True,
help_text="Emergency message to display"
)

panels = [
MultiFieldPanel([
FieldPanel('emergency_expires'),
FieldPanel('emergency_content'),
], heading='Emergency Override'),
]

class Meta:
verbose_name = 'Sticky Note'
verbose_name = 'Emergency Messaging'


@register_setting(icon='collapse-down')
Expand Down
Loading
Loading