Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions openstax/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@
'rangefilter',
'reversion',
'wagtail_modeladmin',
'wagtailautocomplete',
# custom
'accounts',
'api',
Expand Down
2 changes: 2 additions & 0 deletions openstax/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.conf.urls.static import static
from django.contrib import admin
from wagtail.admin import urls as wagtailadmin_urls
from wagtailautocomplete.urls.admin import urlpatterns as autocomplete_admin_urls
from wagtail import urls as wagtail_urls
from wagtail.documents import urls as wagtaildocs_urls
from accounts import urls as accounts_urls
Expand All @@ -17,6 +18,7 @@
admin.site.site_header = 'OpenStax'

urlpatterns = [
path('admin/autocomplete/', include(autocomplete_admin_urls)),
path('admin/', include(wagtailadmin_urls)),

path('django-admin/error/', throw_error, name='throw_error'),
Expand Down
27 changes: 27 additions & 0 deletions pages/migrations/0163_rootpage_school.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 5.1.12 on 2025-12-05 18:24

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


class Migration(migrations.Migration):

dependencies = [
("pages", "0162_assignable_cta_section_footer"),
("salesforce", "0114_school_industry"),
]

operations = [
migrations.AddField(
model_name="rootpage",
name="school",
field=models.ForeignKey(
blank=True,
help_text="Link a school to this landing page. School information will be included in the API response.",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="salesforce.school",
),
),
]
32 changes: 27 additions & 5 deletions pages/models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from django import forms
from django.db import models
from django.shortcuts import render

from django.utils.functional import cached_property
from modelcluster.fields import ParentalKey
from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel, TitleFieldPanel
from wagtailautocomplete.edit_handlers import AutocompletePanel
from wagtail.admin.widgets.slug import SlugInput
from wagtail import blocks
from wagtail.fields import RichTextField, StreamField
Expand All @@ -17,7 +18,7 @@
from webinars.models import Webinar
from news.models import BlogStreamBlock # for use on the ImpactStories

from salesforce.models import PartnerTypeMapping, PartnerFieldNameMapping, PartnerCategoryMapping, Partner
from salesforce.models import PartnerTypeMapping, PartnerFieldNameMapping, PartnerCategoryMapping, Partner, School

from .custom_blocks import ImageBlock, \
APIImageChooserBlock, \
Expand Down Expand Up @@ -188,9 +189,31 @@ class RootPage(Page):
related_name='+'
)

school = models.ForeignKey(
School,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text='Link a school to this landing page. School information will be included in the API response.'
)

def get_school_data(self):
"""Return serialized school data for API"""
if self.school:
from salesforce.serializers import SchoolSerializer
serializer = SchoolSerializer(self.school)
return serializer.data
return None

@cached_property
def school_data(self):
return self.get_school_data()

api_fields = [
APIField('layout'),
APIField('body'),
APIField('school_data'),
APIField('slug'),
APIField('seo_title'),
APIField('search_description'),
Expand All @@ -199,6 +222,7 @@ class RootPage(Page):
content_panels = [
TitleFieldPanel('title', help_text="For CMS use only. Use 'Promote' tab above to edit SEO information."),
FieldPanel('layout'),
AutocompletePanel('school'),
FieldPanel('body'),
]

Expand All @@ -211,9 +235,7 @@ class RootPage(Page):

template = 'page.html'
max_count = 1
# TODO: we are allowing this to be built as a child of the homepage. Not ideal.
# Once the home page is released, use something to migrate homepage children to root page and remove this parent type.
parent_page_types = ['wagtailcore.Page', 'pages.HomePage']
parent_page_types = ['wagtailcore.Page']

def __str__(self):
return self.path
Expand Down
161 changes: 161 additions & 0 deletions pages/static/pages/conditional-school-field.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
(function() {
// Get jQuery - try django.jQuery first (Django admin), then fall back to regular jQuery
var getJQuery = function() {
if (typeof django !== 'undefined' && django.jQuery) {
return django.jQuery;
} else if (typeof jQuery !== 'undefined') {
return jQuery;
} else if (typeof $ !== 'undefined') {
return $;
}
return null;
};

// Wait for jQuery to be available
var init = function() {
var $ = getJQuery();
if (!$) {
// jQuery not ready yet, try again
setTimeout(init, 50);
return;
}

$(document).ready(function(){
function checkAndToggleSchoolField() {
// Find the layout StreamField container
// Wagtail StreamFields have a data-contentpath attribute
var $layoutField = $('[data-contentpath="layout"]');

if ($layoutField.length === 0) {
return;
}

// Check if there's a block with type "landing" (displayed as "Landing Page")
var hasLandingPage = false;

// Method 1: Check for the block type label "Landing Page" in the layout field
var $landingPageLabel = $layoutField.find('.c-sf-block_type').filter(function() {
return $(this).text().trim() === 'Landing Page';
});

if ($landingPageLabel.length > 0) {
hasLandingPage = true;
} else {
// Method 2: Check hidden inputs for block type value "landing"
$layoutField.find('input[type="hidden"][name*="type"], input[type="hidden"][name*="block_type"]').each(function() {
if ($(this).val() === 'landing') {
hasLandingPage = true;
return false; // break
}
});

// Method 3: Check StreamField value directly via JSON data
if (!hasLandingPage) {
var $hiddenInput = $layoutField.find('input[type="hidden"][name*="layout"]');
if ($hiddenInput.length > 0) {
try {
var streamValue = JSON.parse($hiddenInput.val() || '[]');
if (Array.isArray(streamValue) && streamValue.length > 0) {
hasLandingPage = streamValue[0].type === 'landing';
}
} catch (e) {
// JSON parse failed, continue with other methods
}
}
}
}

// Find the School field panel
// Try multiple selectors based on Wagtail panel structure
var $schoolPanel = null;

// Try by ID first (based on image description showing panel IDs)
var schoolPanelIds = [
'#panel-child-content-school',
'#panel-child-content-school-section',
'[id*="panel"][id*="school"]'
];

for (var i = 0; i < schoolPanelIds.length; i++) {
var $found = $(schoolPanelIds[i]).closest('.w-panel, section.w-panel');
if ($found.length > 0) {
$schoolPanel = $found;
break;
}
}

// If not found by ID, try finding by label/heading text "School"
// Scope search to the form container to avoid false matches
if (!$schoolPanel || $schoolPanel.length === 0) {
var $formContainer = $('.w-form-width, .w-form, [class*="wagtail"]').first();
if ($formContainer.length === 0) {
$formContainer = $('form').first();
}

$formContainer.find('label, .w-panel_heading, h2, [data-panel-heading-text]').each(function() {
var $el = $(this);
var text = $el.text().trim();
if (text.toLowerCase() === 'school') {
$schoolPanel = $el.closest('.w-panel, section.w-panel');
if ($schoolPanel.length > 0) {
return false; // break
}
}
});
}

// Last resort: try finding by data-contentpath
if (!$schoolPanel || $schoolPanel.length === 0) {
$schoolPanel = $('[data-contentpath="school"]').closest('.w-panel, section.w-panel');
}

// Show or hide the School panel
if ($schoolPanel && $schoolPanel.length > 0) {
if (hasLandingPage) {
$schoolPanel.show();
} else {
$schoolPanel.hide();
}
}
}

// Check on page load
checkAndToggleSchoolField();

// Also check after a delay to ensure StreamField is fully initialized
setTimeout(checkAndToggleSchoolField, 500);

// Listen for StreamField changes (when blocks are added/removed/changed)
// Wagtail triggers custom events on StreamField changes
$(document).on('wagtail:stream-field-block-added wagtail:stream-field-block-removed wagtail:stream-field-block-moved', function() {
setTimeout(checkAndToggleSchoolField, 100);
});

// Also listen for any changes in the layout field container
var $layoutField = $('[data-contentpath="layout"]');
if ($layoutField.length > 0) {
// Use MutationObserver to watch for DOM changes in the layout field
var observer = new MutationObserver(function(mutations) {
checkAndToggleSchoolField();
});

observer.observe($layoutField[0], {
childList: true,
subtree: true,
attributes: true,
characterData: true
});
}

// Fallback: Also check periodically (with debouncing)
var checkTimeout;
$(document).on('change click input', '[data-contentpath="layout"]', function() {
clearTimeout(checkTimeout);
checkTimeout = setTimeout(checkAndToggleSchoolField, 200);
});
});
};

// Start initialization
init();
})();
15 changes: 15 additions & 0 deletions pages/wagtail_hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.templatetags.static import static
from django.utils.html import format_html
from wagtail import hooks
from pages.models import RootPage

@hooks.register('insert_editor_js')
def conditional_school_field_js(request):
"""Inject JavaScript to conditionally show school field only for RootPage edit views"""
# Only inject JS if editing a RootPage instance
if hasattr(request, 'instance') and isinstance(getattr(request, 'instance', None), RootPage):
Copy link

Choose a reason for hiding this comment

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

Bug: JS injection check relies on non-existent request attribute

The insert_editor_js hook checks for request.instance to determine whether to inject the JavaScript, but Django request objects do not have an instance attribute. Wagtail does not attach the page instance being edited to the request object. This means the condition hasattr(request, 'instance') will always be False, and the conditional school field JavaScript will never be loaded when editing RootPage instances.

Fix in Cursor Fix in Web

return format_html(
'<script src="{}"></script>',
static('pages/conditional-school-field.js')
)
return ''
1 change: 1 addition & 0 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ ua-parser==1.0.1
unicodecsv==0.14.1
vcrpy==7.0.0
wagtail==7.1.1
wagtail-autocomplete==0.12.0
wagtail-modeladmin==2.2.0
whitenoise==6.9.0
19 changes: 15 additions & 4 deletions salesforce/management/commands/update_schools.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def handle(self, *args, **options):
fetch_results = sf.bulk.Account.query("SELECT Name, Id, Phone, " \
"Website, " \
"Type, " \
"Industry, " \
"School_Location__c, " \
"Students_Current_Year__c, " \
"Total_School_Enrollment__c, " \
Expand All @@ -23,21 +24,26 @@ def handle(self, *args, **options):
"BillingCountry, " \
"BillingLatitude, " \
"BillingLongitude " \
"FROM Account", lazy_operation=True)
"FROM Account WHERE Industry = 'HE' OR Industry = 'K12'", lazy_operation=True)
sf_schools = []
for list_results in fetch_results:
sf_schools.extend(list_results)

updated_schools = 0
created_schools = 0
seen_salesforce_ids = set()

for sf_school in sf_schools:
salesforce_id = sf_school['Id']
seen_salesforce_ids.add(salesforce_id)

school, created = School.objects.update_or_create(
salesforce_id=sf_school['Id'],
salesforce_id=salesforce_id,
defaults={'name': sf_school['Name'],
'phone': sf_school['Phone'],
'website': sf_school['Website'],
'type': sf_school['Type'],
'industry': sf_school['Industry'],
'location': sf_school['School_Location__c'],
'current_year_students': sf_school['Students_Current_Year__c'],
'total_school_enrollment': sf_school['Total_School_Enrollment__c'],
Expand All @@ -51,13 +57,18 @@ def handle(self, *args, **options):
},
)

school.save()
if created:
created_schools = created_schools + 1
else:
updated_schools = updated_schools + 1

# Delete schools that no longer exist in Salesforce
deleted_schools = School.objects.exclude(
salesforce_id__in=seen_salesforce_ids
).delete()[0]

invalidate_cloudfront_caches('salesforce/schools')
response = self.style.SUCCESS(
"Successfully updated {} schools, created {} schools.".format(updated_schools, created_schools))
"Successfully updated {} schools, created {} schools, deleted {} schools.".format(
updated_schools, created_schools, deleted_schools))
self.stdout.write(response)
18 changes: 18 additions & 0 deletions salesforce/migrations/0114_school_industry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.1.12 on 2025-12-05 17:28

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("salesforce", "0113_school_created_school_updated"),
]

operations = [
migrations.AddField(
model_name="school",
name="industry",
field=models.CharField(blank=True, max_length=255, null=True),
),
]
Loading
Loading