diff --git a/allies/models.py b/allies/models.py index d51265407..eda56683e 100644 --- a/allies/models.py +++ b/allies/models.py @@ -4,7 +4,6 @@ MultiFieldPanel) from wagtail.fields import RichTextField from wagtail.models import Page -from wagtail.images.edit_handlers import ImageChooserPanel from openstax.functions import build_image_url from snippets.models import Subject diff --git a/api/tests.py b/api/tests.py index 349eb527d..4aefd9370 100644 --- a/api/tests.py +++ b/api/tests.py @@ -1,5 +1,5 @@ import json -from django.test import TestCase, Client +from django.test import TestCase from wagtail.test.utils import WagtailTestUtils from wagtail.images.tests.utils import Image, get_test_image_file @@ -7,7 +7,7 @@ from api.models import FeatureFlag, WebviewSettings -from shared.test_utilities import assertPathDoesNotRedirectToTrailingSlash, mock_user_login +from shared.test_utilities import mock_user_login class PagesAPI(TestCase, WagtailTestUtils): def setUp(self): @@ -18,7 +18,7 @@ def test_api_v2_pages_urls(self): response = self.client.get('/apps/cms/api/v2/pages/') self.assertEqual(response.status_code, 200) - response = self.client.get('/apps/cms/api/v2/pages') + response = self.client.get('/apps/cms/api/v2/pages', follow=True) self.assertEqual(response.status_code, 200) @@ -160,7 +160,7 @@ def test_sticky_api(self): self.assertEqual(response.status_code, 200) def test_errata_resource_api(self): - response = self.client.get('/apps/cms/api/errata-fields?field=resources') + response = self.client.get('/apps/cms/api/errata-fields/?field=resources') self.assertNotIn('content', 'OpenStax Concept Coach') self.assertNotIn('content', 'Rover by OpenStax') self.assertEqual(response.status_code, 200) diff --git a/books/models.py b/books/models.py index 091e6e8c0..283580903 100644 --- a/books/models.py +++ b/books/models.py @@ -1,34 +1,24 @@ import re import html -import json -import urllib -import ssl from sentry_sdk import capture_exception from django.conf import settings from django.db import models -from django.forms import ValidationError from django.utils.html import format_html, mark_safe -from django.contrib.postgres.fields import ArrayField from modelcluster.fields import ParentalKey -from modelcluster.models import ClusterableModel from wagtail.admin.panels import (FieldPanel, - InlinePanel, - PageChooserPanel, - StreamFieldPanel) + InlinePanel, + PageChooserPanel) +from wagtail.admin.widgets.slug import SlugInput from wagtail import blocks from wagtail.fields import RichTextField, StreamField from wagtail.models import Orderable, Page -from wagtail.documents.edit_handlers import DocumentChooserPanel from wagtail.snippets.blocks import SnippetChooserBlock -from wagtail.images.edit_handlers import ImageChooserPanel -from wagtail.snippets.edit_handlers import SnippetChooserPanel from wagtail.admin.panels import TabbedInterface, ObjectList from wagtail.api import APIField -from wagtail.snippets.models import register_snippet from wagtail.models import Site -from openstax.functions import build_document_url, build_image_url +from openstax.functions import build_document_url from books.constants import BOOK_STATES, BOOK_COVER_TEXT_COLOR, COVER_COLORS, CC_NC_SA_LICENSE_NAME, CC_BY_LICENSE_NAME, \ CC_BY_LICENSE_URL, CC_NC_SA_LICENSE_URL, CC_NC_SA_LICENSE_VERSION, CC_BY_LICENSE_VERSION, K12_CATEGORIES import snippets.models as snippets @@ -65,6 +55,7 @@ class VideoFacultyResource(models.Model): FieldPanel('video_file'), ] + class OrientationFacultyResource(models.Model): resource_heading = models.CharField(max_length=255, null=True) resource_description = RichTextField(blank=True, null=True) @@ -134,6 +125,7 @@ def get_document_title(self): FieldPanel('featured'), ] + class FacultyResources(models.Model): resource = models.ForeignKey( snippets.FacultyResource, @@ -145,25 +137,31 @@ class FacultyResources(models.Model): def get_resource_heading(self): return self.resource.heading + resource_heading = property(get_resource_heading) def get_resource_description(self): return self.resource.description + resource_description = property(get_resource_description) def get_resource_unlocked(self): return self.resource.unlocked_resource + resource_unlocked = property(get_resource_unlocked) def get_resource_icon(self): return self.resource.resource_icon + resource_icon = property(get_resource_icon) def get_resource_creator_fest_resource(self): return self.resource.creator_fest_resource + creator_fest_resource = property(get_resource_creator_fest_resource) - link_external = models.URLField("External link", default='', blank=True, help_text="Provide an external URL starting with https:// (or fill out either one of the following two).") + link_external = models.URLField("External link", default='', blank=True, + help_text="Provide an external URL starting with https:// (or fill out either one of the following two).") link_page = models.ForeignKey( 'wagtailcore.Page', null=True, @@ -183,14 +181,17 @@ def get_resource_creator_fest_resource(self): def get_link_document(self): return build_document_url(self.link_document.url) + link_document_url = property(get_link_document) def get_document_title(self): return self.link_document.title + link_document_title = property(get_document_title) link_text = models.CharField(max_length=255, null=True, blank=True, help_text="Call to Action Text") - coming_soon_text = models.CharField(max_length=255, null=True, blank=True, help_text="If there is text in this field a coming soon banner will be added with this description.") + coming_soon_text = models.CharField(max_length=255, null=True, blank=True, + help_text="If there is text in this field a coming soon banner will be added with this description.") video_reference_number = models.IntegerField(blank=True, null=True) updated = models.DateTimeField(blank=True, null=True, help_text='Late date resource was updated') featured = models.BooleanField(default=False, help_text="Add to featured bar on resource page") @@ -200,6 +201,7 @@ def get_document_title(self): def get_resource_category(self): return self.resource.resource_category + resource_category = property(get_resource_category) api_fields = [ @@ -250,21 +252,26 @@ class StudentResources(models.Model): def get_resource_heading(self): return self.resource.heading + resource_heading = property(get_resource_heading) def get_resource_description(self): return self.resource.description + resource_description = property(get_resource_description) def get_resource_unlocked(self): return self.resource.unlocked_resource + resource_unlocked = property(get_resource_unlocked) - + def get_resource_icon(self): return self.resource.resource_icon + resource_icon = property(get_resource_icon) - link_external = models.URLField("External link", default='', blank=True, help_text="Provide an external URL starting with http:// (or fill out either one of the following two).") + link_external = models.URLField("External link", default='', blank=True, + help_text="Provide an external URL starting with http:// (or fill out either one of the following two).") link_page = models.ForeignKey( 'wagtailcore.Page', null=True, @@ -284,22 +291,25 @@ def get_resource_icon(self): def get_link_document(self): return build_document_url(self.link_document.url) + link_document_url = property(get_link_document) def get_document_title(self): return self.link_document.title + link_document_title = property(get_document_title) link_text = models.CharField(max_length=255, null=True, blank=True, help_text="Call to Action Text") - coming_soon_text = models.CharField(max_length=255, null=True, blank=True, help_text="If there is text in this field a coming soon banner will be added with this description.") + coming_soon_text = models.CharField(max_length=255, null=True, blank=True, + help_text="If there is text in this field a coming soon banner will be added with this description.") updated = models.DateTimeField(blank=True, null=True, help_text='Late date resource was updated') print_link = models.URLField(blank=True, null=True, help_text="Link for Buy Print link on resource") display_on_k12 = models.BooleanField(default=False, help_text="Display resource on K12 subject pages") def get_resource_category(self): return self.resource.resource_category - resource_category = property(get_resource_category) + resource_category = property(get_resource_category) api_fields = [ APIField('resource_heading'), @@ -333,9 +343,12 @@ def get_resource_category(self): class Authors(models.Model): name = models.CharField(max_length=255, help_text="Full name of the author.") - university = models.CharField(max_length=255, null=True, blank=True, help_text="Name of the university/institution the author is associated with.") - country = models.CharField(max_length=255, null=True, blank=True, help_text="Country of the university/institution.") - senior_author = models.BooleanField(default=False, help_text="Whether the author is a senior author. (Senior authors are shown before non-senior authors.)") + university = models.CharField(max_length=255, null=True, blank=True, + help_text="Name of the university/institution the author is associated with.") + country = models.CharField(max_length=255, null=True, blank=True, + help_text="Country of the university/institution.") + senior_author = models.BooleanField(default=False, + help_text="Whether the author is a senior author. (Senior authors are shown before non-senior authors.)") display_at_top = models.BooleanField(default=False, help_text="Whether display the author on top.") book = ParentalKey( 'books.Book', related_name='book_contributing_authors', null=True, blank=True) @@ -358,14 +371,16 @@ class Authors(models.Model): class AuthorBlock(blocks.StructBlock): - name = blocks.CharBlock(required=True, help_text="Full name of the author.") - university = blocks.CharBlock(required=False, help_text="Name of the university/institution the author is associated with.") - country = blocks.CharBlock(required=False, help_text="Country of the university/institution.") - senior_author = blocks.BooleanBlock(required=False, help_text="Whether the author is a senior author. (Senior authors are shown before non-senior authors.)") - display_at_top = blocks.BooleanBlock(required=False, help_text="Whether display the author on top.") + name = blocks.CharBlock(required=True, help_text="Full name of the author.") + university = blocks.CharBlock(required=False, + help_text="Name of the university/institution the author is associated with.") + country = blocks.CharBlock(required=False, help_text="Country of the university/institution.") + senior_author = blocks.BooleanBlock(required=False, + help_text="Whether the author is a senior author. (Senior authors are shown before non-senior authors.)") + display_at_top = blocks.BooleanBlock(required=False, help_text="Whether display the author on top.") - class Meta: - icon = 'user' + class Meta: + icon = 'user' class SubjectBooks(models.Model): @@ -373,18 +388,22 @@ class SubjectBooks(models.Model): def get_subject_name(self): return self.subject.name + subject_name = property(get_subject_name) def get_subject_page_content(self): return self.subject.page_content + subject_page_content = property(get_subject_page_content) def get_subject_page_title(self): return self.subject.seo_title + subject_seo_title = property(get_subject_page_title) def get_subject_meta(self): return self.subject.search_description + subject_search_description = property(get_subject_meta) api_fields = [ @@ -393,15 +412,19 @@ def get_subject_meta(self): APIField('subject_search_description') ] + class K12SubjectBooks(models.Model): - subject = models.ForeignKey(snippets.K12Subject, on_delete=models.SET_NULL, null=True, related_name='k12subjects_subject') + subject = models.ForeignKey(snippets.K12Subject, on_delete=models.SET_NULL, null=True, + related_name='k12subjects_subject') def get_subject_name(self): return self.subject.name + subject_name = property(get_subject_name) def get_subject_category(self): return self.subject.subject_category + subject_category = property(get_subject_category) api_fields = [ @@ -411,14 +434,17 @@ def get_subject_category(self): class BookCategory(models.Model): - category = models.ForeignKey(snippets.SubjectCategory, on_delete=models.SET_NULL, null=True, related_name='subjects_subjectcategory') + category = models.ForeignKey(snippets.SubjectCategory, on_delete=models.SET_NULL, null=True, + related_name='subjects_subjectcategory') def get_subject_name(self): return self.category.subject_name + subject_name = property(get_subject_name) def get_subject_category(self): return self.category.subject_category if self.category is not None else '' + subject_category = property(get_subject_category) api_fields = [ @@ -472,18 +498,23 @@ class Meta: class BookFacultyResources(Orderable, FacultyResources): book_faculty_resource = ParentalKey('books.Book', related_name='book_faculty_resources') + class VideoFacultyResources(Orderable, VideoFacultyResource): book_video_faculty_resource = ParentalKey('books.Book', related_name='book_video_faculty_resources') + class OrientationFacultyResources(Orderable, OrientationFacultyResource): book_orientation_faculty_resource = ParentalKey('books.Book', related_name='book_orientation_faculty_resources') + class BookStudentResources(Orderable, StudentResources): book_student_resource = ParentalKey('books.Book', related_name='book_student_resources') + class BookSubjects(Orderable, SubjectBooks): book_subject = ParentalKey('books.Book', related_name='book_subjects') + class K12BookSubjects(Orderable, K12SubjectBooks): k12book_subject = ParentalKey('books.Book', related_name='k12book_subjects') @@ -499,7 +530,8 @@ class Book(Page): ) created = models.DateTimeField(auto_now_add=True) - book_state = models.CharField(max_length=255, choices=BOOK_STATES, default='live', help_text='The state of the book.') + book_state = models.CharField(max_length=255, choices=BOOK_STATES, default='live', + help_text='The state of the book.') cnx_id = models.CharField( max_length=255, help_text="collection.xml UUID. Should be same as book UUID", blank=True, null=True) @@ -509,7 +541,7 @@ class Book(Page): salesforce_abbreviation = models.CharField(max_length=255, blank=True, null=True) salesforce_name = models.CharField(max_length=255, blank=True, null=True) salesforce_book_id = models.CharField(max_length=255, blank=True, null=True, - help_text='No tracking and not included on adoption and interest forms if left blank)') + help_text='No tracking and not included on adoption and interest forms if left blank)') updated = models.DateTimeField(blank=True, null=True, help_text='Late date web content was updated') is_ap = models.BooleanField(default=False, help_text='Whether this book is an AP (Advanced Placement) book.') description = RichTextField( @@ -522,11 +554,13 @@ class Book(Page): related_name='+', help_text='The book cover to be shown on the website.' ) + def get_cover_url(self): if self.cover: return build_document_url(self.cover.url) else: return '' + cover_url = property(get_cover_url) title_image = models.ForeignKey( @@ -536,27 +570,35 @@ def get_cover_url(self): related_name='+', help_text='The svg for title image to be shown on the website.' ) + def get_title_image_url(self): return build_document_url(self.title_image.url) + title_image_url = property(get_title_image_url) - cover_color = models.CharField(max_length=255, choices=COVER_COLORS, default='blue', help_text='The color of the cover.') - book_cover_text_color = models.CharField(max_length=255, choices=BOOK_COVER_TEXT_COLOR, default='yellow', help_text="Use by the Unified team - this will not change the text color on the book cover.") + cover_color = models.CharField(max_length=255, choices=COVER_COLORS, default='blue', + help_text='The color of the cover.') + book_cover_text_color = models.CharField(max_length=255, choices=BOOK_COVER_TEXT_COLOR, default='yellow', + help_text="Use by the Unified team - this will not change the text color on the book cover.") reverse_gradient = models.BooleanField(default=False) publish_date = models.DateField(null=True, help_text='Date the book is published on.') authors = StreamField([ ('author', AuthorBlock()), ], null=True, use_json_field=True) - print_isbn_13 = models.CharField(max_length=255, blank=True, null=True, help_text='ISBN 13 for print version (hardcover).') - print_softcover_isbn_13 = models.CharField(max_length=255, blank=True, null=True, help_text='ISBN 13 for print version (softcover).') + print_isbn_13 = models.CharField(max_length=255, blank=True, null=True, + help_text='ISBN 13 for print version (hardcover).') + print_softcover_isbn_13 = models.CharField(max_length=255, blank=True, null=True, + help_text='ISBN 13 for print version (softcover).') digital_isbn_13 = models.CharField(max_length=255, blank=True, null=True, help_text='ISBN 13 for digital version.') ibook_isbn_13 = models.CharField(max_length=255, blank=True, null=True, help_text='ISBN 13 for iBook version.') - ibook_volume_2_isbn_13 = models.CharField(max_length=255, blank=True, null=True, help_text='ISBN 13 for iBook v2 version.') + ibook_volume_2_isbn_13 = models.CharField(max_length=255, blank=True, null=True, + help_text='ISBN 13 for iBook v2 version.') license_text = models.TextField( blank=True, null=True, help_text="Overrides default license text.") license_name = models.CharField( - max_length=255, blank=True, null=True, choices=licenses,default=CC_BY_LICENSE_NAME, help_text="Name of the license.") + max_length=255, blank=True, null=True, choices=licenses, default=CC_BY_LICENSE_NAME, + help_text="Name of the license.") license_version = models.CharField( max_length=255, blank=True, null=True, editable=False, help_text="Version of the license.") license_url = models.CharField( @@ -570,11 +612,13 @@ def get_title_image_url(self): related_name='+', help_text="High quality PDF document of the book." ) + def get_high_res_pdf_url(self): if self.high_resolution_pdf: return build_document_url(self.high_resolution_pdf.url) else: return None + high_resolution_pdf_url = property(get_high_res_pdf_url) low_resolution_pdf = models.ForeignKey( @@ -585,16 +629,22 @@ def get_high_res_pdf_url(self): related_name='+', help_text="Low quality PDF document of the book." ) + def get_low_res_pdf_url(self): if self.low_resolution_pdf: return build_document_url(self.low_resolution_pdf.url) else: return None + low_resolution_pdf_url = property(get_low_res_pdf_url) - free_stuff_instructor = StreamField(SharedContentBlock(), null=True, blank=True, help_text="Snippet to show texts for free instructor resources.", use_json_field=True) - free_stuff_student = StreamField(SharedContentBlock(), null=True, blank=True, help_text="Snipped to show texts for free student resources.", use_json_field=True) - community_resource_heading = models.CharField(max_length=255, blank=True, null=True, help_text="Snipped to show texts for community resources.") + free_stuff_instructor = StreamField(SharedContentBlock(), null=True, blank=True, + help_text="Snippet to show texts for free instructor resources.", + use_json_field=True) + free_stuff_student = StreamField(SharedContentBlock(), null=True, blank=True, + help_text="Snipped to show texts for free student resources.", use_json_field=True) + community_resource_heading = models.CharField(max_length=255, blank=True, null=True, + help_text="Snipped to show texts for community resources.") community_resource_logo = models.ForeignKey( 'wagtaildocs.Document', null=True, @@ -622,6 +672,7 @@ def get_community_resource_logo_url(self): related_name='+', help_text='Document of the community resource feature.' ) + def get_community_resource_feature_link_url(self): return build_document_url(self.community_resource_feature_link.url) @@ -633,9 +684,12 @@ def get_community_resource_feature_link_url(self): ibook_link_volume_2 = models.URLField(blank=True, help_text="Link to secondary iBook") webview_link = models.URLField(blank=True, help_text="Link to CNX Webview book") webview_rex_link = models.URLField(blank=True, help_text="Link to REX Webview book") - rex_callout_title = models.CharField(max_length=255, blank=True, null=True, help_text='Title of the REX callout', default="Recommended") - rex_callout_blurb = models.CharField(max_length=255, blank=True, null=True, help_text='Additional text for the REX callout.') - enable_study_edge = models.BooleanField(default=False, help_text="This will cause the link to the Study Edge app appear on the book details page.") + rex_callout_title = models.CharField(max_length=255, blank=True, null=True, help_text='Title of the REX callout', + default="Recommended") + rex_callout_blurb = models.CharField(max_length=255, blank=True, null=True, + help_text='Additional text for the REX callout.') + enable_study_edge = models.BooleanField(default=False, + help_text="This will cause the link to the Study Edge app appear on the book details page.") bookshare_link = models.URLField(blank=True, help_text="Link to Bookshare resources") amazon_coming_soon = models.BooleanField(default=False, verbose_name="Individual Print Coming Soon") amazon_link = models.URLField(blank=True, verbose_name="Individual Print Link") @@ -643,23 +697,37 @@ def get_community_resource_feature_link_url(self): kindle_link = models.URLField(blank=True, help_text="Link to Kindle version") chegg_link = models.URLField(blank=True, null=True, help_text="Link to Chegg e-reader") chegg_link_text = models.CharField(max_length=255, blank=True, null=True, help_text='Text for Chegg link.') - bookstore_coming_soon = models.BooleanField(default=False, help_text='Whether this book is coming to bookstore soon.') - bookstore_content = StreamField(SharedContentBlock(), null=True, blank=True, help_text='Bookstore content.', use_json_field=True) + bookstore_coming_soon = models.BooleanField(default=False, + help_text='Whether this book is coming to bookstore soon.') + bookstore_content = StreamField(SharedContentBlock(), null=True, blank=True, help_text='Bookstore content.', + use_json_field=True) comp_copy_available = models.BooleanField(default=True, help_text='Whether free compy available for teachers.') - comp_copy_content = StreamField(SharedContentBlock(), null=True, blank=True, help_text='Content of the free copy.', use_json_field=True) + comp_copy_content = StreamField(SharedContentBlock(), null=True, blank=True, help_text='Content of the free copy.', + use_json_field=True) tutor_marketing_book = models.BooleanField(default=False, help_text='Whether this is a Tutor marketing book.') assignable_book = models.BooleanField(default=False, help_text='Whether this is an Assignable book.') - partner_list_label = models.CharField(max_length=255, null=True, blank=True, help_text="Controls the heading text on the book detail page for partners. This will update ALL books to use this value!") - partner_page_link_text = models.CharField(max_length=255, null=True, blank=True, help_text="Link to partners page on top right of list.") - featured_resources_header = models.CharField(max_length=255, null=True, blank=True, help_text="Featured resource header on instructor resources tab.") - customization_form_heading = models.CharField(max_length=255, null=True, blank=True, help_text="Heading for the CE customization form. This will update ALL books to use this value!", default="Customization Form") - customization_form_subheading = models.CharField(max_length=255, null=True, blank=True, help_text="Subheading for the CE customization form. This will update ALL books to use this value!", default="Please select the modules (up to 10), that you want to customize with Google Docs.") - customization_form_disclaimer = RichTextField(blank=True, help_text="This will update ALL books to use this value!", default="

Disclaimer

The following features and functionality are not available to teachers and students using Google Docs customized content:

") - customization_form_next_steps = RichTextField(blank=True, help_text="This will update ALL books to use this value!", default="

Next Steps

  1. Within two business days, you will receive an email for each module that you have requested access to customize.
  2. The link provided in the email will be your own copy of the Google Doc that OpenStax generated for you.
  3. Once you have accessessed the document you can make the changes you desire and share with your students. We recommend using the "Publish to the Web" functionality under the file menu for sharing with students.
") + partner_list_label = models.CharField(max_length=255, null=True, blank=True, + help_text="Controls the heading text on the book detail page for partners. This will update ALL books to use this value!") + partner_page_link_text = models.CharField(max_length=255, null=True, blank=True, + help_text="Link to partners page on top right of list.") + featured_resources_header = models.CharField(max_length=255, null=True, blank=True, + help_text="Featured resource header on instructor resources tab.") + customization_form_heading = models.CharField(max_length=255, null=True, blank=True, + help_text="Heading for the CE customization form. This will update ALL books to use this value!", + default="Customization Form") + customization_form_subheading = models.CharField(max_length=255, null=True, blank=True, + help_text="Subheading for the CE customization form. This will update ALL books to use this value!", + default="Please select the modules (up to 10), that you want to customize with Google Docs.") + customization_form_disclaimer = RichTextField(blank=True, help_text="This will update ALL books to use this value!", + default="

Disclaimer

The following features and functionality are not available to teachers and students using Google Docs customized content:

") + customization_form_next_steps = RichTextField(blank=True, help_text="This will update ALL books to use this value!", + default="

Next Steps

  1. Within two business days, you will receive an email for each module that you have requested access to customize.
  2. The link provided in the email will be your own copy of the Google Doc that OpenStax generated for you.
  3. Once you have accessessed the document you can make the changes you desire and share with your students. We recommend using the "Publish to the Web" functionality under the file menu for sharing with students.
") adoptions = models.IntegerField(blank=True, null=True) savings = models.IntegerField(blank=True, null=True) - support_statement = models.TextField(blank=True, null=True, default="With philanthropic support, this book is used in classrooms, saving students dollars this school year. Learn more about our impact and how you can help.", help_text="Updating this statement updates it for all book pages.") - + support_statement = models.TextField(blank=True, null=True, + default="With philanthropic support, this book is used in classrooms, saving students dollars this school year. Learn more about our impact and how you can help.", + help_text="Updating this statement updates it for all book pages.") + promote_snippet = StreamField(PromoteSnippetBlock(), null=True, blank=True, use_json_field=True) videos = StreamField([ @@ -684,7 +752,8 @@ def get_community_resource_feature_link_url(self): ]))) ], null=True, blank=True, use_json_field=True) - last_updated_pdf = models.DateTimeField(blank=True, null=True, help_text="Last time PDF was revised.", verbose_name='PDF Content Revision Date') + last_updated_pdf = models.DateTimeField(blank=True, null=True, help_text="Last time PDF was revised.", + verbose_name='PDF Content Revision Date') book_detail_panel = Page.content_panels + [ FieldPanel('book_state'), @@ -911,14 +980,14 @@ def book_urls(self): book_urls = [] for field in self.api_fields: try: - url = re.findall('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', getattr(self, field)) + url = re.findall('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', + getattr(self, field)) if url: book_urls.append(url) except(TypeError, AttributeError): pass return book_urls - def save(self, *args, **kwargs): if self.cnx_id: self.webview_link = '{}contents/{}'.format(settings.CNX_URL, self.cnx_id) @@ -933,11 +1002,14 @@ def save(self, *args, **kwargs): if self.customization_form_heading: Book.objects.filter(locale=self.locale).update(customization_form_heading=self.customization_form_heading) if self.customization_form_subheading: - Book.objects.filter(locale=self.locale).update(customization_form_subheading=self.customization_form_subheading) + Book.objects.filter(locale=self.locale).update( + customization_form_subheading=self.customization_form_subheading) if self.customization_form_disclaimer: - Book.objects.filter(locale=self.locale).update(customization_form_disclaimer=self.customization_form_disclaimer) + Book.objects.filter(locale=self.locale).update( + customization_form_disclaimer=self.customization_form_disclaimer) if self.customization_form_next_steps: - Book.objects.filter(locale=self.locale).update(customization_form_next_steps=self.customization_form_next_steps) + Book.objects.filter(locale=self.locale).update( + customization_form_next_steps=self.customization_form_next_steps) if self.support_statement: Book.objects.filter(locale=self.locale).update(support_statement=self.support_statement) @@ -959,7 +1031,6 @@ def save(self, *args, **kwargs): return super(Book, self).save(*args, **kwargs) - def get_url_parts(self, *args, **kwargs): # This overrides the "Live" link in admin to take you to proper FE page url_parts = super(Book, self).get_url_parts(*args, **kwargs) @@ -972,7 +1043,6 @@ def get_url_parts(self, *args, **kwargs): return (site_id, root_url, page_path) - def get_sitemap_urls(self, request=None): return [ { @@ -1000,7 +1070,8 @@ class BookIndex(Page): dev_standard_3_description = RichTextField() dev_standard_4_heading = models.CharField( max_length=255, blank=True, null=True) - dev_standard_4_description = models.TextField(help_text="Keep in place to populate with Salesforce data. id=adoption_number for classrooms and id=savings for savings number.") + dev_standard_4_description = models.TextField( + help_text="Keep in place to populate with Salesforce data. id=adoption_number for classrooms and id=savings for savings number.") subject_list_heading = models.CharField( max_length=255, blank=True, null=True) promote_image = models.ForeignKey( @@ -1019,7 +1090,7 @@ class BookIndex(Page): @property def books(self): - books = Book.objects.live().filter(locale=self.locale).exclude(book_state='unlisted').order_by('title') + books = Book.objects.live().filter(locale=self.locale).exclude(book_state='unlisted').order_by('title') book_data = [] for book in books: has_faculty_resources = BookFacultyResources.objects.filter(book_faculty_resource=book).exists() @@ -1076,7 +1147,7 @@ def books(self): ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') diff --git a/errata/models.py b/errata/models.py index c1f12cf09..c59b2c9ce 100644 --- a/errata/models.py +++ b/errata/models.py @@ -276,7 +276,7 @@ def save(self, *args, **kwargs): @hooks.register('register_admin_menu_item') def register_errata_menu_item(): - return MenuItem('Errata', '/django-admin/errata/errata', classnames='icon icon-form', order=10000) + return MenuItem('Errata', '/django-admin/errata/errata', classname='icon icon-form', order=10000) def __str__(self): return self.book.book_title diff --git a/extraadminfilters/filters.py b/extraadminfilters/filters.py index ebb78cc0a..c0a6d8b1a 100644 --- a/extraadminfilters/filters.py +++ b/extraadminfilters/filters.py @@ -1,6 +1,4 @@ from django.contrib.admin.filters import FieldListFilter -from django.db.models.fields import IntegerField, AutoField -from django.db.models.fields.related import OneToOneField, ForeignKey, ManyToOneRel class MultipleSelectFieldListFilter(FieldListFilter): diff --git a/global_settings/wagtail_hooks.py b/global_settings/wagtail_hooks.py index cff2ebd74..904a10250 100644 --- a/global_settings/wagtail_hooks.py +++ b/global_settings/wagtail_hooks.py @@ -35,9 +35,9 @@ def register_strikethrough_feature(features): @hooks.register('register_settings_menu_item') def register_500_menu_item(): - return MenuItem('Generate 500', reverse('throw_error'), classnames='icon icon-warning', order=10000) + return MenuItem('Generate 500', reverse('throw_error'), classname='icon icon-warning', order=10000) @hooks.register('register_settings_menu_item') def register_clear_cache_menu_item(): - return MenuItem('Clear Cloudfront Cache', reverse('clear_entire_cache'), classnames='icon icon-bin', order=11000) + return MenuItem('Clear Cloudfront Cache', reverse('clear_entire_cache'), classname='icon icon-bin', order=11000) diff --git a/locked-requirements.txt b/locked-requirements.txt deleted file mode 100644 index 34148b120..000000000 --- a/locked-requirements.txt +++ /dev/null @@ -1,242 +0,0 @@ -appdirs==1.4.4 -chardet==4.0.0 -django-admin-rangefilter==0.8.4 -django-crontab==0.7.1 - Django==4.1.7 - asgiref==3.6.0 - sqlparse==0.4.3 -django_debug_toolbar==3.8.1 - Django==4.1.7 - asgiref==3.6.0 - sqlparse==0.4.3 - sqlparse==0.4.3 -django-extensions==3.2.1 - Django==4.1.7 - asgiref==3.6.0 - sqlparse==0.4.3 -django-import-export==2.8.0 - diff-match-patch==20200713 - Django==4.1.7 - asgiref==3.6.0 - sqlparse==0.4.3 - tablib==3.3.0 -django-libsass==0.9 - django-compressor==4.3.1 - django-appconf==1.0.5 - Django==4.1.7 - asgiref==3.6.0 - sqlparse==0.4.3 - rcssmin==1.1.1 - rjsmin==1.2.1 - libsass==0.22.0 -django-rest-auth==0.9.5 - Django==4.1.7 - asgiref==3.6.0 - sqlparse==0.4.3 - djangorestframework==3.14.0 - Django==4.1.7 - asgiref==3.6.0 - sqlparse==0.4.3 - pytz==2022.7.1 - six==1.16.0 -django-reversion==5.0.0 - Django==4.1.7 - asgiref==3.6.0 - sqlparse==0.4.3 -django-ses==3.0.1 - boto3==1.26.56 - botocore==1.29.56 - jmespath==1.0.1 - python-dateutil==2.8.2 - six==1.16.0 - urllib3==1.26.14 - jmespath==1.0.1 - s3transfer==0.6.0 - botocore==1.29.56 - jmespath==1.0.1 - python-dateutil==2.8.2 - six==1.16.0 - urllib3==1.26.14 - Django==4.1.7 - asgiref==3.6.0 - sqlparse==0.4.3 - pytz==2022.7.1 -django-storages==1.12.3 - Django==4.1.7 - asgiref==3.6.0 - sqlparse==0.4.3 -future==0.18.2 -html2text==2020.1.16 -jsonfield==3.1.0 - Django==4.1.7 - asgiref==3.6.0 - sqlparse==0.4.3 -mapbox==0.18.1 - boto3==1.26.56 - botocore==1.29.56 - jmespath==1.0.1 - python-dateutil==2.8.2 - six==1.16.0 - urllib3==1.26.14 - jmespath==1.0.1 - s3transfer==0.6.0 - botocore==1.29.56 - jmespath==1.0.1 - python-dateutil==2.8.2 - six==1.16.0 - urllib3==1.26.14 - CacheControl==0.12.11 - msgpack==1.0.4 - requests==2.28.2 - certifi==2022.12.7 - charset-normalizer==3.0.1 - idna==3.4 - urllib3==1.26.14 - iso3166==2.1.1 - polyline==2.0.0 - python-dateutil==2.8.2 - six==1.16.0 - requests==2.28.2 - certifi==2022.12.7 - charset-normalizer==3.0.1 - idna==3.4 - urllib3==1.26.14 - uritemplate==4.1.1 -MarkupPy==1.14 -odfpy==1.4.1 - defusedxml==0.7.1 -openpyxl==3.0.10 - et-xmlfile==1.1.0 -pip==22.3.1 -pipdeptree==2.7.0 -psycopg2==2.9.5 -pycryptodome==3.14.1 -PyJWE==1.0.0 - cryptography==39.0.0 - cffi==1.15.1 - pycparser==2.21 -sentry-sdk==1.15.0 - certifi==2022.12.7 - urllib3==1.26.14 -setuptools==65.5.1 -simple-salesforce==1.11.6 - Authlib==1.2.0 - cryptography==39.0.0 - cffi==1.15.1 - pycparser==2.21 - requests==2.28.2 - certifi==2022.12.7 - charset-normalizer==3.0.1 - idna==3.4 - urllib3==1.26.14 - zeep==4.2.1 - attrs==22.2.0 - isodate==0.6.1 - six==1.16.0 - lxml==4.9.2 - platformdirs==2.6.2 - pytz==2022.7.1 - requests==2.28.2 - certifi==2022.12.7 - charset-normalizer==3.0.1 - idna==3.4 - urllib3==1.26.14 - requests-file==1.5.1 - requests==2.28.2 - certifi==2022.12.7 - charset-normalizer==3.0.1 - idna==3.4 - urllib3==1.26.14 - six==1.16.0 - requests-toolbelt==0.10.1 - requests==2.28.2 - certifi==2022.12.7 - charset-normalizer==3.0.1 - idna==3.4 - urllib3==1.26.14 -social-auth-app-django==5.0.0 - social-auth-core==4.3.0 - cryptography==39.0.0 - cffi==1.15.1 - pycparser==2.21 - defusedxml==0.7.1 - oauthlib==3.2.2 - PyJWT==2.6.0 - python3-openid==3.2.0 - defusedxml==0.7.1 - requests==2.28.2 - certifi==2022.12.7 - charset-normalizer==3.0.1 - idna==3.4 - urllib3==1.26.14 - requests-oauthlib==1.3.1 - oauthlib==3.2.2 - requests==2.28.2 - certifi==2022.12.7 - charset-normalizer==3.0.1 - idna==3.4 - urllib3==1.26.14 -ua-parser==0.16.1 -unicodecsv==0.14.1 -Unidecode==1.3.4 -vcrpy==4.1.1 - PyYAML==6.0 - six==1.16.0 - wrapt==1.14.1 - yarl==1.8.2 - idna==3.4 - multidict==6.0.4 -wagtail==4.0.4 - anyascii==0.3.1 - beautifulsoup4==4.9.3 - soupsieve==2.3.2.post1 - Django==4.1.7 - asgiref==3.6.0 - sqlparse==0.4.3 - django-filter==21.1 - Django==4.1.7 - asgiref==3.6.0 - sqlparse==0.4.3 - django-modelcluster==6.0 - Django==4.1.7 - asgiref==3.6.0 - sqlparse==0.4.3 - pytz==2022.7.1 - django-permissionedforms==0.1 - Django==4.1.7 - asgiref==3.6.0 - sqlparse==0.4.3 - django-taggit==3.1.0 - Django==4.1.7 - asgiref==3.6.0 - sqlparse==0.4.3 - django-treebeard==4.6.0 - Django==4.1.7 - asgiref==3.6.0 - sqlparse==0.4.3 - djangorestframework==3.14.0 - Django==4.1.7 - asgiref==3.6.0 - sqlparse==0.4.3 - pytz==2022.7.1 - draftjs-exporter==2.1.7 - html5lib==1.1 - six==1.16.0 - webencodings==0.5.1 - l18n==2021.3 - pytz==2022.7.1 - six==1.16.0 - Pillow==9.4.0 - requests==2.28.2 - certifi==2022.12.7 - charset-normalizer==3.0.1 - idna==3.4 - urllib3==1.26.14 - tablib==3.3.0 - telepath==0.3 - Willow==1.4.1 - XlsxWriter==3.0.7 -Wand==0.6.7 -whitenoise==6.1.0 -xlrd==2.0.1 -xlwt==1.3.0 diff --git a/news/models.py b/news/models.py index 3a875822f..a222ba9ff 100644 --- a/news/models.py +++ b/news/models.py @@ -1,5 +1,3 @@ -import json - from bs4 import BeautifulSoup from django.db import models @@ -7,9 +5,8 @@ from wagtail.models import Page, Orderable from wagtail.fields import RichTextField, StreamField -from wagtail.admin.panels import FieldPanel, StreamFieldPanel, InlinePanel -from wagtail.images.edit_handlers import ImageChooserPanel -from wagtail.documents.edit_handlers import DocumentChooserPanel +from wagtail.admin.panels import FieldPanel, InlinePanel +from wagtail.admin.widgets.slug import SlugInput from wagtail.embeds.blocks import EmbedBlock from wagtail.search import index from wagtail import blocks @@ -17,8 +14,6 @@ from wagtail.images.blocks import ImageChooserBlock from wagtail.documents.blocks import DocumentChooserBlock from wagtail.snippets.blocks import SnippetChooserBlock -from wagtail.snippets.edit_handlers import SnippetChooserPanel -from wagtail.snippets.models import register_snippet from wagtail.api import APIField from wagtail.images.api.fields import ImageRenditionField from wagtail.models import Site @@ -171,7 +166,7 @@ class NewsIndex(Page): ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -385,7 +380,7 @@ def blog_collections(self): ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -592,7 +587,7 @@ def releases(self): ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -681,7 +676,7 @@ def get_sitemap_urls(self, request=None): ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') diff --git a/openstax/middleware.py b/openstax/middleware.py index b1cfb9b2e..1e8306a20 100644 --- a/openstax/middleware.py +++ b/openstax/middleware.py @@ -1,19 +1,17 @@ -from django.http import HttpResponsePermanentRedirect +from django.http import HttpResponsePermanentRedirect, HttpResponse from django.core.handlers.base import BaseHandler from django.middleware.common import CommonMiddleware +from django.utils.http import escape_leading_slashes from django.conf import settings + from ua_parser import user_agent_parser -from wagtail.models import Page -from django.shortcuts import get_object_or_404 -from django.template.response import TemplateResponse -from django.http import HttpResponse from urllib.parse import unquote from api.models import FeatureFlag from books.models import Book, BookIndex from openstax.functions import build_image_url from news.models import NewsArticle -from pages.models import HomePage, Supporters, GeneralPage, PrivacyPolicy, K12Subject, Subject, Subjects +from pages.models import HomePage, Supporters, PrivacyPolicy, K12Subject, Subject, Subjects class HttpSmartRedirectResponse(HttpResponsePermanentRedirect): @@ -22,41 +20,53 @@ class HttpSmartRedirectResponse(HttpResponsePermanentRedirect): class CommonMiddlewareAppendSlashWithoutRedirect(CommonMiddleware): """ This class converts HttpSmartRedirectResponse to the common response - of Django view, without redirect. + of Django view, without redirect. This is necessary to match status_codes + for urls like /url?q=1 and /url/?q=1. If you don't use it, you will have 302 + code always on pages without slash. """ response_redirect_class = HttpSmartRedirectResponse - def __init__(self, *args, **kwargs): - # create django request resolver - self.handler = BaseHandler() +def __init__(self, *args, **kwargs): + # create django request resolver + self.handler = BaseHandler() + + # prevent recursive includes + old = settings.MIDDLEWARE + name = self.__module__ + '.' + self.__class__.__name__ + settings.MIDDLEWARE = [i for i in settings.MIDDLEWARE if i != name] - # prevent recursive includes - old = settings.MIDDLEWARE - name = self.__module__ + '.' + self.__class__.__name__ - settings.MIDDLEWARE = [i for i in settings.MIDDLEWARE if i != name] + self.handler.load_middleware() - self.handler.load_middleware() + settings.MIDDLEWARE = old + super(CommonMiddlewareAppendSlashWithoutRedirect, self).__init__(*args, **kwargs) - settings.MIDDLEWARE = old - super(CommonMiddlewareAppendSlashWithoutRedirect, self).__init__(*args, **kwargs) +def get_full_path_with_slash(self, request): + """ Return the full path of the request with a trailing slash appended + without Exception in Debug mode + """ + new_path = request.get_full_path(force_append_slash=True) + # Prevent construction of scheme relative urls. + new_path = escape_leading_slashes(new_path) + return new_path - def process_response(self, request, response): - response = super(CommonMiddlewareAppendSlashWithoutRedirect, self).process_response(request, response) +def process_response(self, request, response): + response = super(CommonMiddlewareAppendSlashWithoutRedirect, self).process_response(request, response) - if isinstance(response, HttpSmartRedirectResponse): - if not request.path.endswith('/'): - request.path = request.path + '/' - # we don't need query string in path_info because it's in request.GET already - request.path_info = request.path - response = self.handler.get_response(request) + if isinstance(response, HttpSmartRedirectResponse): + if not request.path.endswith('/'): + request.path = request.path + '/' + # we don't need query string in path_info because it's in request.GET already + request.path_info = request.path + response = self.handler.get_response(request) - return response + return response class CommonMiddlewareOpenGraphRedirect(CommonMiddleware): OG_USER_AGENTS = [ 'twitterbot', 'facebookbot', + 'facebookexternalhit/1.1', 'pinterestbot', 'slackbot-linkexpanding', ] @@ -163,4 +173,3 @@ def page_by_slug(self, page_slug): - diff --git a/openstax/settings/base.py b/openstax/settings/base.py index a3a198d4a..eca84ebf6 100644 --- a/openstax/settings/base.py +++ b/openstax/settings/base.py @@ -25,7 +25,7 @@ # These should both be set to true. The openstax.middleware will handle resolving the URL # without a redirect if needed. APPEND_SLASH = True -WAGTAIL_APPEND_SLASH=True +WAGTAIL_APPEND_SLASH = True # urls.W002 warns about slashes at the start of URLs. But we need those so # we don't have to have slashes at the end of URLs. So ignore. @@ -152,7 +152,6 @@ AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', - 'oxauth.backend.OpenStaxAccountsBackend', ) TEMPLATES = [ @@ -203,6 +202,7 @@ 'import_export', 'rangefilter', 'reversion', + 'wagtail_modeladmin', # custom 'accounts', 'api', @@ -239,7 +239,6 @@ 'wagtail.sites', 'wagtail.api.v2', 'wagtail.contrib.settings', - 'wagtail.contrib.modeladmin' ] CRONJOBS = [ @@ -352,7 +351,7 @@ ALLOWED_HOSTS = json.loads(os.getenv('ALLOWED_HOSTS', '[]')) -CNX_URL = os.getenv('CNX_URL') +CNX_URL = os.getenv('CNX_URL', 'https://openstax.org') # used in page.models to retrieve book information CNX_ARCHIVE_URL = 'https://archive.cnx.org' diff --git a/openstax/tests.py b/openstax/tests.py index 3050dbafd..a0aeb08ba 100644 --- a/openstax/tests.py +++ b/openstax/tests.py @@ -134,7 +134,7 @@ def test_book_link_preview(self): ) book_index.add_child(instance=book) self.client = Client(HTTP_USER_AGENT='Slackbot-LinkExpanding 1.0 (+https://api.slack.com/robots)') - response = self.client.get('/details/books/biology-2e') + response = self.client.get('/details/books/biology-2e/') self.assertContains(response, 'og:image') def test_blog_link_preview(self): @@ -185,6 +185,6 @@ def test_blog_link_preview(self): )) self.news_index.add_child(instance=self.article) self.client = Client(HTTP_USER_AGENT='facebookexternalhit/1.1') - response = self.client.get('/blog/article-1') + response = self.client.get('/blog/article-1/') self.assertContains(response, 'og:image') diff --git a/openstax/urls.py b/openstax/urls.py index d1a15114d..782c3aa61 100644 --- a/openstax/urls.py +++ b/openstax/urls.py @@ -2,13 +2,11 @@ from django.urls import include, path, re_path from django.conf.urls.static import static from django.contrib import admin -from django.views.generic.base import RedirectView from wagtail.admin import urls as wagtailadmin_urls from wagtail import urls as wagtail_urls from wagtail.documents import urls as wagtaildocs_urls from wagtail.images.views.serve import ServeView from accounts import urls as accounts_urls -from oxauth import views as oxauth_views from .api import api_router from news.search import search @@ -17,14 +15,10 @@ from api import urls as api_urls from global_settings.views import throw_error, clear_entire_cache from wagtail.contrib.sitemaps.views import sitemap -#from wagtailimportexport import urls as wagtailimportexport_urls admin.site.site_header = 'OpenStax' urlpatterns = [ - #path('admin/login/', oxauth_views.login), - #path('admin/logout/', oxauth_views.logout), - path('oxauth/', include('oxauth.urls')), # new auth package path('admin/', include(wagtailadmin_urls)), path('django-admin/error/', throw_error, name='throw_error'), @@ -47,7 +41,6 @@ path('blog-feed/atom/', AtomBlogFeed()), path('errata/', include('errata.urls')), path('apps/cms/api/errata/', include('errata.urls')), - #path('apps/cms/api/events/', include('events.urls')), path('apps/cms/api/webinars/', include('webinars.urls')), path('apps/cms/api/donations/', include('donations.urls')), path('apps/cms/api/oxmenus/', include('oxmenus.urls')), @@ -55,7 +48,6 @@ # route everything to /api/spike also... path('apps/cms/api/spike/', include(wagtail_urls)), path('sitemap.xml', sitemap), - #path(r'', include(wagtailimportexport_urls)), # For anything not caught by a more specific rule above, hand over to Wagtail's serving mechanism path('', include(wagtail_urls)), diff --git a/oxauth/admin.py b/oxauth/admin.py deleted file mode 100644 index 8584e1b42..000000000 --- a/oxauth/admin.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.contrib import admin -from .models import OpenStaxUserProfile - -class OpenStaxUserProfileAdmin(admin.ModelAdmin): - list_display = ['user', 'openstax_accounts_uuid'] - search_fields = ['user', 'openstax_accounts_uuid' ] - raw_id_fields = ['user'] - - def has_add_permission(self, request): - return False - -admin.site.register(OpenStaxUserProfile, OpenStaxUserProfileAdmin) diff --git a/oxauth/backend.py b/oxauth/backend.py deleted file mode 100644 index 67d694cd8..000000000 --- a/oxauth/backend.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.contrib.auth.backends import BaseBackend -from django.conf import settings -from .functions import decrypt_cookie -from .models import OpenStaxUserProfile -from django.contrib.auth.models import User - -class OpenStaxAccountsBackend(BaseBackend): - def authenticate(self, request): - decrypted_cookie = decrypt_cookie(request.COOKIES.get(settings.SSO_COOKIE_NAME)).payload_dict['sub'] - - openstax_user = OpenStaxUserProfile.objects.get(openstax_accounts_uuid=decrypted_cookie['uuid']) - return openstax_user.user - - def get_user(self, user_id): - return User.objects.get(pk=user_id) - - def has_perm(self, user_obj, perm, obj=None): - return user_obj.is_superuser == True diff --git a/oxauth/functions.py b/oxauth/functions.py index 4fe8cbd19..296ac539f 100644 --- a/oxauth/functions.py +++ b/oxauth/functions.py @@ -11,10 +11,10 @@ def decrypt_cookie(cookie): strategy = Strategy2( signature_public_key=settings.SIGNATURE_PUBLIC_KEY, - signature_algorithm = settings.SIGNATURE_ALGORITHM, - encryption_private_key = settings.ENCRYPTION_PRIVATE_KEY, - encryption_method = 'A256GCM', - encryption_algorithm = 'dir' + signature_algorithm=settings.SIGNATURE_ALGORITHM, + encryption_private_key=settings.ENCRYPTION_PRIVATE_KEY, + encryption_method='A256GCM', + encryption_algorithm='dir' ) payload = strategy.decrypt(cookie) @@ -125,4 +125,3 @@ def retrieve_user_data(url=None): return user_data else: return {} - diff --git a/oxauth/models.py b/oxauth/models.py index 8bd006edc..92c0f249d 100644 --- a/oxauth/models.py +++ b/oxauth/models.py @@ -1,6 +1,7 @@ from django.contrib.auth.models import User from django.db import models +# TODO: This can be removed, it's not being used but will do in another PR because it causing deployment issues. class OpenStaxUserProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) openstax_accounts_id = models.IntegerField() @@ -8,6 +9,3 @@ class OpenStaxUserProfile(models.Model): def __str__(self): return self.user.username - - class Meta: - verbose_name = 'OpenStax User Profile' diff --git a/oxauth/templates/wagtailusers/users/edit.html b/oxauth/templates/wagtailusers/users/edit.html index e66d553b5..72306b600 100644 --- a/oxauth/templates/wagtailusers/users/edit.html +++ b/oxauth/templates/wagtailusers/users/edit.html @@ -1,5 +1,7 @@ {% extends "wagtailusers/users/edit.html" %} -{% block extra_fields %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.is_staff %} -{% endblock extra_fields %} +
  • + {% block extra_fields %} + {% include "wagtailadmin/shared/field.html" with field=form.is_staff %} + {% endblock extra_fields %} +
  • diff --git a/oxauth/tests.py b/oxauth/tests.py index 8086dd152..25f1cb175 100644 --- a/oxauth/tests.py +++ b/oxauth/tests.py @@ -1,15 +1,7 @@ -import json -from urllib.parse import urlencode -from urllib.request import urlopen - from django.test import TestCase, Client -from django.conf import settings -from django.urls import reverse from shared.test_utilities import RequestMock -from oxauth.models import OpenStaxUserProfile -from oxauth.functions import get_token, decrypt_cookie, get_logged_in_user_id, get_logged_in_user_uuid -from oxauth.views import login, logout +from oxauth.functions import decrypt_cookie, get_logged_in_user_id, get_logged_in_user_uuid from http import cookies @@ -36,10 +28,3 @@ def test_can_get_logged_in_user_uuid(self): uuid = get_logged_in_user_uuid(request, bypass_sso_cookie_check=False) self.assertEqual(uuid, '467cea6c-8159-40b1-90f1-e9b0dc26344c') - def creates_openstax_profile_on_login(self): - biscuits = cookies.SimpleCookie() - biscuits['oxa'] = self.sso_cookie - self.client.cookies = biscuits - response = self.client.get('/admin') - profile = OpenStaxUserProfile.objects.last('id') - self.assertEqual(profile.openstax_accounts_uuid, '467cea6c-8159-40b1-90f1-e9b0dc26344c') diff --git a/oxauth/urls.py b/oxauth/urls.py deleted file mode 100644 index 479c263b7..000000000 --- a/oxauth/urls.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.urls import include, path -from .views import logout, login - -urlpatterns = [ - path("login/", login, name="login"), - path("logout/", logout, name="logout"), -] diff --git a/oxauth/views.py b/oxauth/views.py deleted file mode 100644 index a25916745..000000000 --- a/oxauth/views.py +++ /dev/null @@ -1,81 +0,0 @@ -import urllib.parse -from django.shortcuts import redirect -from django.http import request, HttpResponse -from django.conf import settings -from django.contrib.auth import login as auth_login -from django.contrib.auth import logout as auth_logout -from django.contrib.auth.models import User, Permission -from django.urls import reverse -from .functions import decrypt_cookie -from .models import OpenStaxUserProfile - -def create_sso_profile(decrypted_cookie): - user, _ = User.objects.get_or_create( - username=decrypted_cookie['name'].replace(" ", ""), - first_name=decrypted_cookie['first_name'], - last_name=decrypted_cookie['last_name'] - ) - openstax_user, _ = OpenStaxUserProfile.objects.get_or_create( - openstax_accounts_id=decrypted_cookie['id'], - openstax_accounts_uuid=decrypted_cookie['uuid'], - defaults={'user': user} - ) - openstax_user.save() - - # Assign/remove admin permissions if they are an admin in the Accounts admin SSO cookie - if decrypted_cookie['is_administrator'] == True: - user.is_superuser = True - user.is_staff = True - permission = Permission.objects.get(codename='access_admin') - user.user_permissions.add(permission) - user.save() - elif decrypted_cookie['is_administrator'] == False: # to handle when no longer an admin in accounts - user.is_superuser = False - user.is_staff = False - permission = Permission.objects.get(codename='access_admin') - user.user_permissions.remove(permission) - user.save() - - return user - -def login(request): - # to allow using django auth login from django-admin - if request.user.is_authenticated: - return redirect(reverse('wagtailadmin_explore_root')) - - try: - decrypted_cookie = decrypt_cookie(request.COOKIES.get(settings.SSO_COOKIE_NAME)).payload_dict['sub'] - user = create_sso_profile(decrypted_cookie) - except AttributeError: - return HttpResponse( - 'Unauthorized. Please check your login/admin status on OpenStax Accounts', - status=401 - ) - - # we only authenticate admins for the CMS, so everyone else gets a 401 - if user is not None and user.is_superuser: - auth_login(request, user, 'oxauth.backend.OpenStaxAccountsBackend') - return redirect(reverse('wagtailadmin_explore_root')) - else: - return HttpResponse( - 'Unauthorized. Please check your login/admin status on OpenStax Accounts', - status=401 - ) - - -def logout(request): - url = settings.ACCOUNTS_URL + '/logout/' - - next = request.GET.get("next", None) - if next: - url += "/logout/?r={}".format(urllib.parse.quote(next)) - - # logout of the django auth system - auth_logout(request) - - # logout and redirect (or return to accounts login page if no `r` param) - return redirect(url) diff --git a/oxmenus/migrations/0002_alter_menus_options_alter_menus_menu.py b/oxmenus/migrations/0002_alter_menus_options_alter_menus_menu.py new file mode 100644 index 000000000..813110ba5 --- /dev/null +++ b/oxmenus/migrations/0002_alter_menus_options_alter_menus_menu.py @@ -0,0 +1,47 @@ +# Generated by Django 4.2.8 on 2024-01-03 22:26 + +from django.db import migrations +import wagtail.blocks +import wagtail.fields + + +class Migration(migrations.Migration): + dependencies = [ + ("oxmenus", "0001_initial"), + ] + + operations = [ + migrations.AlterModelOptions( + name="menus", + options={"verbose_name": "Menu", "verbose_name_plural": "Menus"}, + ), + migrations.AlterField( + model_name="menus", + name="menu", + field=wagtail.fields.StreamField( + [ + ( + "menu_block", + wagtail.blocks.StructBlock( + [ + ( + "menu_items", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ("label", wagtail.blocks.CharBlock(max_length=255)), + ("partial_url", wagtail.blocks.CharBlock(max_length=255)), + ], + required=True, + ) + ), + ) + ], + required=True, + ), + ) + ], + use_json_field=True, + ), + ), + ] diff --git a/oxmenus/models.py b/oxmenus/models.py index ddbcc456e..77d0db0e1 100644 --- a/oxmenus/models.py +++ b/oxmenus/models.py @@ -1,7 +1,7 @@ from django.db import models from wagtail import blocks -from wagtail.fields import RichTextField, StreamField -from wagtail.admin.edit_handlers import FieldPanel +from wagtail.fields import StreamField +from wagtail.admin.panels import FieldPanel from wagtail.api import APIField class MenuItemBlock(blocks.StructBlock): @@ -34,7 +34,11 @@ def menu_block_json(self): def __str__(self): return self.name - content_panels = [ + class Meta: + verbose_name = "Menu" + verbose_name_plural = "Menus" + + panels = [ FieldPanel('name'), FieldPanel('menu') ] @@ -42,4 +46,4 @@ def __str__(self): api_fields = [ APIField('name'), APIField('menu') - ] \ No newline at end of file + ] diff --git a/oxmenus/wagtail_hooks.py b/oxmenus/wagtail_hooks.py index 4bc0006b1..d78f90d34 100644 --- a/oxmenus/wagtail_hooks.py +++ b/oxmenus/wagtail_hooks.py @@ -1,10 +1,10 @@ -from wagtail.contrib.modeladmin.options import ModelAdmin, ModelAdminGroup, modeladmin_register +from wagtail_modeladmin.options import ModelAdmin, modeladmin_register from .models import Menus class OXMenusAdmin(ModelAdmin): model = Menus - menu_icon = 'media' + menu_icon = 'grip' menu_label = 'OX Menu' menu_order = 5000 list_display = ('name',) diff --git a/pages/custom_fields.py b/pages/custom_fields.py index 868d65f49..dd04fec24 100644 --- a/pages/custom_fields.py +++ b/pages/custom_fields.py @@ -2,8 +2,7 @@ from wagtail import blocks from wagtail.fields import RichTextField, StreamField -from wagtail.admin.panels import FieldPanel, StreamFieldPanel -from wagtail.images.edit_handlers import ImageChooserPanel +from wagtail.admin.panels import FieldPanel from .custom_blocks import APIImageChooserBlock from openstax.functions import build_image_url diff --git a/pages/migrations/0083_alter_k12mainpage_testimonials.py b/pages/migrations/0083_alter_k12mainpage_testimonials.py new file mode 100644 index 000000000..ded5f38d4 --- /dev/null +++ b/pages/migrations/0083_alter_k12mainpage_testimonials.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.8 on 2024-01-03 22:26 + +from django.db import migrations +import pages.custom_blocks +import wagtail.blocks +import wagtail.fields + + +class Migration(migrations.Migration): + dependencies = [ + ("pages", "0082_remove_webinarpage_description_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="k12mainpage", + name="testimonials", + field=wagtail.fields.StreamField( + [ + ( + "testimonials", + wagtail.blocks.StructBlock( + [ + ("author_icon", pages.custom_blocks.APIImageChooserBlock(required=False)), + ("author_name", wagtail.blocks.CharBlock(required=True)), + ("author_title", wagtail.blocks.CharBlock(required=True)), + ("testimonial", wagtail.blocks.RichTextBlock(required=True)), + ] + ), + ) + ], + use_json_field=True, + ), + ), + ] diff --git a/pages/models.py b/pages/models.py index 0d9f449ac..bb1d48242 100644 --- a/pages/models.py +++ b/pages/models.py @@ -2,7 +2,8 @@ from django.db import models from modelcluster.fields import ParentalKey -from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel +from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel, TitleFieldPanel +from wagtail.admin.widgets.slug import SlugInput from wagtail import blocks from wagtail.fields import RichTextField, StreamField from wagtail.models import Orderable, Page @@ -13,7 +14,7 @@ from openstax.functions import build_image_url, build_document_url from books.models import Book, SubjectBooks, BookFacultyResources, BookStudentResources from webinars.models import Webinar -from news.models import BlogStreamBlock # for use on the ImpactStories +from news.models import BlogStreamBlock # for use on the ImpactStories from salesforce.models import PartnerTypeMapping, PartnerFieldNameMapping, PartnerCategoryMapping, Partner @@ -46,8 +47,10 @@ class AboutUsPage(Page): on_delete=models.SET_NULL, related_name='+' ) + def get_who_image(self): return build_image_url(self.who_image) + who_image_url = property(get_who_image) what_heading = models.CharField(max_length=255) what_paragraph = models.TextField() @@ -68,8 +71,10 @@ def get_who_image(self): on_delete=models.SET_NULL, related_name='+' ) + def get_where_map(self): return build_image_url(self.where_map) + where_map_alt = models.CharField(max_length=255, blank=True, null=True) where_map_url = property(get_where_map) promote_image = models.ForeignKey( @@ -100,7 +105,7 @@ def get_where_map(self): ] content_panels = [ - FieldPanel('title', classname="full title"), + TitleFieldPanel('title', classname="full title"), FieldPanel('who_heading'), FieldPanel('who_paragraph'), FieldPanel('who_image'), @@ -114,7 +119,7 @@ def get_where_map(self): ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -141,8 +146,10 @@ class TeamPage(Page): on_delete=models.SET_NULL, related_name='+' ) + def get_header_image(self): return build_image_url(self.header_image) + header_image_url = property(get_header_image) team_header = models.CharField(max_length=255, null=True, blank=True) @@ -155,7 +162,7 @@ def get_header_image(self): ) content_panels = [ - FieldPanel('title', classname="full title"), + TitleFieldPanel('title', classname="full title"), FieldPanel('header'), FieldPanel('subheader'), FieldPanel('header_image'), @@ -164,7 +171,7 @@ def get_header_image(self): ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -218,7 +225,7 @@ class HomePage(Page): features_tab1_features = StreamField( blocks.StreamBlock([ ('feature_text', blocks.CharBlock()) - ], max_num=4),use_json_field=True + ], max_num=4), use_json_field=True ) features_tab1_explore_text = models.CharField(default='', blank=True, max_length=255) features_tab1_explore_url = models.URLField(blank=True, default='') @@ -227,7 +234,7 @@ class HomePage(Page): features_tab2_features = StreamField( blocks.StreamBlock([ ('feature_text', blocks.CharBlock()) - ], max_num=4),use_json_field=True + ], max_num=4), use_json_field=True ) features_tab2_explore_text = models.CharField(default='', blank=True, max_length=255) features_tab2_explore_url = models.URLField(blank=True, default='') @@ -371,9 +378,8 @@ class HomePage(Page): class Meta: verbose_name = "Home Page" - content_panels = [ - FieldPanel('title', classname="full title"), + TitleFieldPanel('title', classname="full title"), FieldPanel('banner_headline'), FieldPanel('banner_description'), FieldPanel('banner_get_started_text'), @@ -426,7 +432,7 @@ class Meta: ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -499,9 +505,7 @@ class Meta: verbose_name = "homepage" - class K12MainPage(Page): - banner_headline = models.CharField(default='', blank=True, max_length=255) banner_description = models.TextField(default='', blank=True) banner_right_image = models.ForeignKey( @@ -517,63 +521,63 @@ class K12MainPage(Page): ], use_json_field=True) highlights_header = RichTextField(default='', blank=True) highlights = StreamField( - blocks.StreamBlock([ - ('highlight', blocks.ListBlock(blocks.StructBlock([ - ('highlight_subheader', blocks.TextBlock(required=False)), - ('highlight_text', blocks.CharBlock(Required=False)), - ])))], max_num=3),use_json_field=True) + blocks.StreamBlock([ + ('highlight', blocks.ListBlock(blocks.StructBlock([ + ('highlight_subheader', blocks.TextBlock(required=False)), + ('highlight_text', blocks.CharBlock(Required=False)), + ])))], max_num=3), use_json_field=True) highlights_icon = models.ForeignKey( - 'wagtailimages.Image', - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name='+' - ) + 'wagtailimages.Image', + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name='+' + ) stats_grid = StreamField( - blocks.StreamBlock([ - ('stat', blocks.ListBlock(blocks.StructBlock([ - ('bold_stat_text', blocks.TextBlock(required=False)), - ('normal_stat_text', blocks.CharBlock(required=False)), - ])))], max_num=3), use_json_field=True) + blocks.StreamBlock([ + ('stat', blocks.ListBlock(blocks.StructBlock([ + ('bold_stat_text', blocks.TextBlock(required=False)), + ('normal_stat_text', blocks.CharBlock(required=False)), + ])))], max_num=3), use_json_field=True) stats_image1 = models.ForeignKey( - 'wagtailimages.Image', - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name='+' - ) + 'wagtailimages.Image', + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name='+' + ) stats_image2 = models.ForeignKey( - 'wagtailimages.Image', - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name='+' - ) + 'wagtailimages.Image', + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name='+' + ) stats_image3 = models.ForeignKey( - 'wagtailimages.Image', - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name='+' - ) + 'wagtailimages.Image', + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name='+' + ) subject_library_header = models.CharField(default='', blank=True, max_length=255) subject_library_description = models.TextField(default='', blank=True) testimonials_header = models.CharField(default='', blank=True, max_length=255) testimonials_description = models.TextField(default='', blank=True) testimonials = StreamField([ ('testimonials', TestimonialBlock()), - ]) + ], use_json_field=True) faq_header = models.CharField(default='', blank=True, max_length=255) faqs = StreamField([ ('faq', FAQBlock()), ], use_json_field=True) rfi_image = models.ForeignKey( - 'wagtailimages.Image', - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name='+' - ) + 'wagtailimages.Image', + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name='+' + ) rfi_header = models.CharField(default='', blank=True, max_length=255) rfi_description = models.TextField(default='', blank=True) sticky_header = models.CharField(default='', blank=True, max_length=255) @@ -587,7 +591,6 @@ def get_sitemap_urls(self, request=None): } ] - promote_image = models.ForeignKey( 'wagtailimages.Image', null=True, @@ -646,9 +649,8 @@ def k12library(self): class Meta: verbose_name = "K12 Main Page" - content_panels = [ - FieldPanel('title', classname="full title"), + TitleFieldPanel('title', classname="full title"), FieldPanel('banner_headline'), FieldPanel('banner_description'), FieldPanel('banner_right_image'), @@ -675,26 +677,22 @@ class Meta: ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') ] - template = 'page.html' parent_page_types = ['pages.HomePage'] subpage_types = ['pages.K12Subject'] max_count = 1 - - class Meta: verbose_name = "K12 Main Page" - class ContactUs(Page): tagline = models.CharField(max_length=255) mailing_header = models.CharField(max_length=255) @@ -709,7 +707,7 @@ class ContactUs(Page): ) content_panels = [ - FieldPanel('title', classname="full title"), + TitleFieldPanel('title', classname="full title"), FieldPanel('tagline'), FieldPanel('mailing_header'), FieldPanel('mailing_address'), @@ -717,7 +715,7 @@ class ContactUs(Page): ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -791,12 +789,12 @@ def get_url_parts(self, *args, **kwargs): ] content_panels = [ - FieldPanel('title'), + TitleFieldPanel('title'), FieldPanel('body'), ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -851,7 +849,7 @@ class Supporters(Page): ] content_panels = [ - FieldPanel('title', classname="full title"), + TitleFieldPanel('title', classname="full title"), FieldPanel('banner_heading'), FieldPanel('banner_description'), FieldPanel('banner_image'), @@ -860,7 +858,7 @@ class Supporters(Page): ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -881,8 +879,10 @@ class MapPage(Page): on_delete=models.SET_NULL, related_name='+' ) + def get_map_image(self): return build_image_url(self.map_image) + map_image_url = property(get_map_image) section_1_cards = StreamField([ ('card', blocks.StructBlock([ @@ -903,8 +903,10 @@ def get_map_image(self): on_delete=models.SET_NULL, related_name='+' ) + def get_section_2_image_1(self): return build_image_url(self.section_2_image_1) + section_2_image_1_url = property(get_section_2_image_1) section_2_header_2 = models.CharField(max_length=255) section_2_blurb_2 = models.TextField() @@ -917,8 +919,10 @@ def get_section_2_image_1(self): on_delete=models.SET_NULL, related_name='+' ) + def get_section_2_image_2(self): return build_image_url(self.section_2_image_2) + section_2_image_2_url = property(get_section_2_image_2) section_3_heading = models.CharField(max_length=255) section_3_blurb = models.TextField() @@ -959,7 +963,7 @@ def get_section_2_image_2(self): ] content_panels = [ - FieldPanel('title', classname='full title'), + TitleFieldPanel('title', classname='full title'), FieldPanel('header_text'), FieldPanel('map_image'), FieldPanel('section_1_cards'), @@ -980,7 +984,7 @@ def get_section_2_image_2(self): ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -1035,7 +1039,7 @@ class Give(Page): ] content_panels = [ - FieldPanel('title', classname='full title'), + TitleFieldPanel('title', classname='full title'), FieldPanel('intro_heading'), FieldPanel('intro_description'), FieldPanel('other_payment_methods_heading'), @@ -1052,7 +1056,7 @@ class Give(Page): ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -1086,13 +1090,13 @@ class TermsOfService(Page): ] content_panels = [ - FieldPanel('title', classname='full title'), + TitleFieldPanel('title', classname='full title'), FieldPanel('intro_heading'), FieldPanel('terms_of_service_content'), ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -1129,14 +1133,14 @@ class FAQ(Page): ] content_panels = [ - FieldPanel('title', classname="full title"), + TitleFieldPanel('title', classname="full title"), FieldPanel('intro_heading'), FieldPanel('intro_description'), FieldPanel('questions'), ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -1167,12 +1171,12 @@ class GiveForm(Page): ] content_panels = [ - FieldPanel('title', classname="full title"), + TitleFieldPanel('title', classname="full title"), FieldPanel('page_description'), ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -1206,13 +1210,13 @@ class Accessibility(Page): ] content_panels = [ - FieldPanel('title', classname='full title'), + TitleFieldPanel('title', classname='full title'), FieldPanel('intro_heading'), FieldPanel('accessibility_content'), ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -1246,13 +1250,13 @@ class Licensing(Page): ] content_panels = [ - FieldPanel('title', classname='full title'), + TitleFieldPanel('title', classname='full title'), FieldPanel('intro_heading'), FieldPanel('licensing_content'), ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -1313,7 +1317,7 @@ class Technology(Page): ] content_panels = [ - FieldPanel('title', classname="full title"), + TitleFieldPanel('title', classname="full title"), FieldPanel('intro_heading'), FieldPanel('intro_description'), FieldPanel('banner_cta'), @@ -1332,7 +1336,7 @@ class Technology(Page): ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -1347,12 +1351,13 @@ class Technology(Page): class ErrataList(Page): correction_schedule = RichTextField() - deprecated_errata_message = RichTextField(help_text="Errata message for deprecated books, controlled via the book state field.") - new_edition_errata_message = RichTextField(help_text="Errata message for books with new editions, controlled via the book state field.") + deprecated_errata_message = RichTextField( + help_text="Errata message for deprecated books, controlled via the book state field.") + new_edition_errata_message = RichTextField( + help_text="Errata message for books with new editions, controlled via the book state field.") about_header = models.CharField(max_length=255, help_text="About our correction schedule") about_text = RichTextField(help_text="Errata received from March through...\" the stuff that will show on the page") - about_popup = RichTextField(help_text= "Instructor and student resources...\" the stuff that will be in the popup") - + about_popup = RichTextField(help_text="Instructor and student resources...\" the stuff that will be in the popup") promote_image = models.ForeignKey( 'wagtailimages.Image', @@ -1375,7 +1380,7 @@ class ErrataList(Page): ] content_panels = [ - FieldPanel('title', classname="full title"), + TitleFieldPanel('title', classname="full title"), FieldPanel('correction_schedule'), FieldPanel('deprecated_errata_message'), FieldPanel('new_edition_errata_message'), @@ -1385,7 +1390,7 @@ class ErrataList(Page): ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -1422,13 +1427,13 @@ class PrivacyPolicy(Page): ] content_panels = [ - FieldPanel('title', classname='full title'), + TitleFieldPanel('title', classname='full title'), FieldPanel('intro_heading'), FieldPanel('privacy_content'), ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -1475,7 +1480,7 @@ class PrintOrder(Page): ] content_panels = [ - FieldPanel('title', classname='full title'), + TitleFieldPanel('title', classname='full title'), FieldPanel('intro_heading'), FieldPanel('intro_description'), FieldPanel('featured_provider_intro_blurb'), @@ -1485,7 +1490,7 @@ class PrintOrder(Page): ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -1588,7 +1593,7 @@ class LearningResearchPage(Page): ) content_panels = [ - FieldPanel('title', classname='full title', help_text="Internal name for page."), + TitleFieldPanel('title', classname='full title', help_text="Internal name for page."), FieldPanel('mission_body'), FieldPanel('banner_header'), FieldPanel('banner_body'), @@ -1606,7 +1611,7 @@ class LearningResearchPage(Page): ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -1661,13 +1666,13 @@ class Careers(Page): ] content_panels = [ - FieldPanel('title', classname='full title'), + TitleFieldPanel('title', classname='full title'), FieldPanel('intro_heading'), FieldPanel('careers_content'), ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -1696,7 +1701,7 @@ class ImpactStory(Page): content_panels = Page.content_panels + [ FieldPanel('date'), - FieldPanel('title'), + TitleFieldPanel('title'), FieldPanel('heading'), FieldPanel('subheading'), FieldPanel('author'), @@ -1725,72 +1730,72 @@ class ImpactStory(Page): class Impact(Page): improving_access = StreamField( blocks.StreamBlock([ - ('content', blocks.StructBlock([ - ('image', ImageBlock()), - ('heading', blocks.CharBlock()), - ('description', blocks.RichTextBlock()), - ('button_text', blocks.CharBlock()), - ('button_href', blocks.URLBlock()) - ]))], max_num=1), use_json_field=True) + ('content', blocks.StructBlock([ + ('image', ImageBlock()), + ('heading', blocks.CharBlock()), + ('description', blocks.RichTextBlock()), + ('button_text', blocks.CharBlock()), + ('button_href', blocks.URLBlock()) + ]))], max_num=1), use_json_field=True) reach = StreamField( blocks.StreamBlock([ - ('content', blocks.StructBlock([ - ('image', ImageBlock()), - ('heading', blocks.CharBlock()), - ('description', blocks.RichTextBlock()), - ('cards', blocks.ListBlock(blocks.StructBlock([ - ('icon', APIImageChooserBlock(required=False)), - ('description', blocks.CharBlock()), - ('link_text', blocks.CharBlock(required=False)), - ('link_href', blocks.URLBlock(required=False)) - ]))) - ]))], max_num=1), use_json_field=True) + ('content', blocks.StructBlock([ + ('image', ImageBlock()), + ('heading', blocks.CharBlock()), + ('description', blocks.RichTextBlock()), + ('cards', blocks.ListBlock(blocks.StructBlock([ + ('icon', APIImageChooserBlock(required=False)), + ('description', blocks.CharBlock()), + ('link_text', blocks.CharBlock(required=False)), + ('link_href', blocks.URLBlock(required=False)) + ]))) + ]))], max_num=1), use_json_field=True) quote = StreamField( blocks.StreamBlock([ - ('content', blocks.StructBlock([ - ('image', ImageBlock()), - ('quote', blocks.RichTextBlock()) - ]))], max_num=1), use_json_field=True) + ('content', blocks.StructBlock([ + ('image', ImageBlock()), + ('quote', blocks.RichTextBlock()) + ]))], max_num=1), use_json_field=True) making_a_difference = StreamField( blocks.StreamBlock([ - ('content', blocks.StructBlock([ - ('heading', blocks.CharBlock()), - ('description', blocks.RichTextBlock()), - ('stories', blocks.ListBlock(StoryBlock())) - ]))], max_num=1), use_json_field=True) + ('content', blocks.StructBlock([ + ('heading', blocks.CharBlock()), + ('description', blocks.RichTextBlock()), + ('stories', blocks.ListBlock(StoryBlock())) + ]))], max_num=1), use_json_field=True) disruption = StreamField( blocks.StreamBlock([ - ('content', blocks.StructBlock([ - ('heading', blocks.CharBlock()), - ('description', blocks.TextBlock()), - ('graph', blocks.StructBlock([ - ('image', ImageBlock(required=False)), - ('image_alt_text', blocks.CharBlock(required=False)), - ])) - ]))], max_num=1), use_json_field=True) + ('content', blocks.StructBlock([ + ('heading', blocks.CharBlock()), + ('description', blocks.TextBlock()), + ('graph', blocks.StructBlock([ + ('image', ImageBlock(required=False)), + ('image_alt_text', blocks.CharBlock(required=False)), + ])) + ]))], max_num=1), use_json_field=True) supporter_community = StreamField( blocks.StreamBlock([ - ('content', blocks.StructBlock([ - ('heading', blocks.CharBlock()), - ('image', ImageBlock()), - ('quote', blocks.RichTextBlock()), - ('link_text', blocks.CharBlock()), - ('link_href', blocks.URLBlock()) - ]))], max_num=1), use_json_field=True) + ('content', blocks.StructBlock([ + ('heading', blocks.CharBlock()), + ('image', ImageBlock()), + ('quote', blocks.RichTextBlock()), + ('link_text', blocks.CharBlock()), + ('link_href', blocks.URLBlock()) + ]))], max_num=1), use_json_field=True) giving = StreamField( blocks.StreamBlock([ - ('content', blocks.StructBlock([ - ('heading', blocks.CharBlock()), - ('description', blocks.TextBlock()), - ('link_text', blocks.CharBlock()), - ('link_href', blocks.URLBlock()), - ('nonprofit_statement', blocks.TextBlock()), - ('annual_report_link_text', blocks.CharBlock()), - ('annual_report_link_href', blocks.URLBlock()), - ]))], max_num=1), use_json_field=True) + ('content', blocks.StructBlock([ + ('heading', blocks.CharBlock()), + ('description', blocks.TextBlock()), + ('link_text', blocks.CharBlock()), + ('link_href', blocks.URLBlock()), + ('nonprofit_statement', blocks.TextBlock()), + ('annual_report_link_text', blocks.CharBlock()), + ('annual_report_link_href', blocks.URLBlock()), + ]))], max_num=1), use_json_field=True) content_panels = [ - FieldPanel('title', classname='full title', help_text="Internal name for page."), + TitleFieldPanel('title', classname='full title', help_text="Internal name for page."), FieldPanel('improving_access'), FieldPanel('reach'), FieldPanel('quote'), @@ -1847,7 +1852,7 @@ class InstitutionalPartnership(Page): application_link = models.URLField(blank=True, null=True) content_panels = [ - FieldPanel('title'), + TitleFieldPanel('title'), FieldPanel('heading_image'), FieldPanel('heading_year'), FieldPanel('heading'), @@ -1982,7 +1987,7 @@ class InstitutionalPartnerProgramPage(Page): section_9_contact_html = RichTextField() content_panels = [ - FieldPanel('title', classname='full title', help_text="Internal name for page."), + TitleFieldPanel('title', classname='full title', help_text="Internal name for page."), FieldPanel('section_1_heading'), FieldPanel('section_1_description'), FieldPanel('section_1_link_text'), @@ -2123,7 +2128,7 @@ class CreatorFestPage(Page): ], null=True, use_json_field=True) content_panels = [ - FieldPanel('title', classname='full title', help_text="Internal name for page."), + TitleFieldPanel('title', classname='full title', help_text="Internal name for page."), FieldPanel('banner_headline'), FieldPanel('banner_content'), FieldPanel('banner_image'), @@ -2152,8 +2157,10 @@ class CreatorFestPage(Page): class PartnersPage(Page): heading = models.CharField(max_length=255) description = RichTextField() - partner_landing_page_link = models.CharField(max_length=255, null=True, blank=True, help_text="Link text to partner landing page.") - partner_request_info_link = models.CharField(max_length=255, null=True, blank=True, help_text="Forstack form link text") + partner_landing_page_link = models.CharField(max_length=255, null=True, blank=True, + help_text="Link text to partner landing page.") + partner_request_info_link = models.CharField(max_length=255, null=True, blank=True, + help_text="Forstack form link text") partner_full_partner_heading = models.CharField(max_length=255, null=True, blank=True) partner_full_partner_description = models.TextField(null=True, blank=True) partner_ally_heading = models.CharField(max_length=255, null=True, blank=True) @@ -2163,7 +2170,8 @@ class PartnersPage(Page): def category_mapping(): field_mappings = PartnerCategoryMapping.objects.all() mapping_dict = {} - field_name_mappings = PartnerFieldNameMapping.objects.values_list('salesforce_name', flat=True).filter(hidden=False) + field_name_mappings = PartnerFieldNameMapping.objects.values_list('salesforce_name', flat=True).filter( + hidden=False) field_name_mappings = list(field_name_mappings) for field in field_mappings: @@ -2194,7 +2202,7 @@ def partner_type_choices(): return partner_types_array content_panels = [ - FieldPanel('title', classname='full title', help_text="Internal name for page."), + TitleFieldPanel('title', classname='full title', help_text="Internal name for page."), FieldPanel('heading'), FieldPanel('description'), FieldPanel('partner_landing_page_link'), @@ -2226,11 +2234,12 @@ def partner_type_choices(): parent_page_type = ['pages.HomePage'] template = 'page.html' + class WebinarPage(Page): heading = models.CharField(max_length=255) content_panels = [ - FieldPanel('title', classname='full title', help_text="Internal name for page."), + TitleFieldPanel('title', classname='full title', help_text="Internal name for page."), FieldPanel('heading'), ] @@ -2271,6 +2280,7 @@ def get_api_representation(self, value, context=None): 'logo': None } + class MathQuizPage(Page): heading = models.CharField(max_length=255) description = models.TextField() @@ -2281,12 +2291,12 @@ class MathQuizPage(Page): ('description', blocks.TextBlock()), ('partners', blocks.ListBlock(blocks.StructBlock([ ('partner', PartnerChooserBlock()), - ]))), + ]))), ]))) ], use_json_field=True) content_panels = [ - FieldPanel('title', classname='full title', help_text="Internal name for page."), + TitleFieldPanel('title', classname='full title', help_text="Internal name for page."), FieldPanel('heading'), FieldPanel('description'), FieldPanel('results') @@ -2327,8 +2337,8 @@ class LLPHPage(Page): def get_book_cover(self): return build_document_url(self.book_cover.url) - book_cover_url = property(get_book_cover) + book_cover_url = property(get_book_cover) info_link_slug = models.CharField(max_length=255, default="/details/books/life-liberty-and-pursuit-happiness") info_link_text = models.CharField(max_length=255, default="Not an educator? Take a look at the book here.") @@ -2336,7 +2346,7 @@ def get_book_cover(self): book_description = RichTextField() content_panels = [ - FieldPanel('title', classname='full title', help_text="Internal name for page."), + TitleFieldPanel('title', classname='full title', help_text="Internal name for page."), FieldPanel('heading'), FieldPanel('subheading'), FieldPanel('hero_background'), @@ -2381,16 +2391,16 @@ class TutorMarketing(Page): header_cta_button_link = models.URLField() quote = RichTextField() - #features + # features features_header = models.CharField(max_length=255) features_cards = StreamField([ ('cards', CardImageBlock()), ], use_json_field=True) - #availble books + # availble books available_books_header = models.CharField(max_length=255) - #cost + # cost cost_header = models.CharField(max_length=255) cost_description = models.TextField() cost_cards = StreamField([ @@ -2398,7 +2408,7 @@ class TutorMarketing(Page): ], use_json_field=True) cost_institution_message = models.CharField(max_length=255) - #feedback + # feedback feedback_media = StreamField( blocks.StreamBlock([ ('image', ImageBlock()), @@ -2410,10 +2420,10 @@ class TutorMarketing(Page): feedback_occupation = models.CharField(max_length=255) feedback_organization = models.CharField(max_length=255) - #webinars + # webinars webinars_header = models.CharField(max_length=255) - #faq + # faq faq_header = models.CharField(max_length=255) faqs = StreamField([ ('faq', FAQBlock()), @@ -2492,7 +2502,7 @@ def webinars(self): ] content_panels = [ - FieldPanel('title', classname="full title"), + TitleFieldPanel('title', classname="full title"), FieldPanel('header'), FieldPanel('description'), FieldPanel('header_cta_button_text'), @@ -2520,7 +2530,7 @@ def webinars(self): ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -2623,7 +2633,7 @@ def get_sitemap_urls(self, request=None): ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -2820,7 +2830,7 @@ def subjects(self): ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -2858,7 +2868,7 @@ class FormHeadings(Page): ] content_panels = [ - FieldPanel('title'), + TitleFieldPanel('title'), FieldPanel('adoption_intro_heading'), FieldPanel('adoption_intro_description'), FieldPanel('interest_intro_heading'), @@ -2866,7 +2876,7 @@ class FormHeadings(Page): ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -2885,15 +2895,18 @@ class K12Subject(Page): about_books_heading = models.TextField(default='About the Books') about_books_text = models.CharField(default='FIND SUPPLEMENTAL RESOURCES', blank=True, max_length=255) adoption_heading = models.TextField(default='Using an OpenStax resource in your classroom? Let us know!') - adoption_text = RichTextField(default="

    Help us continue to make high-quality educational materials accessible by letting us know you've adopted! Our future grant funding is based on educator adoptions and the number of students we impact.

    ") + adoption_text = RichTextField( + default="

    Help us continue to make high-quality educational materials accessible by letting us know you've adopted! Our future grant funding is based on educator adoptions and the number of students we impact.

    ") adoption_link_text = models.CharField(default='Report Your Adoption', max_length=255) adoption_link = models.URLField(blank=True, default='/adoption') - quote_heading = models.TextField(default='What Our Teachers Say', blank=True,) + quote_heading = models.TextField(default='What Our Teachers Say', blank=True, ) quote_text = models.CharField(default='', blank=True, max_length=255) resources_heading = models.TextField(default='Supplemental Resources') - blogs_heading = models.TextField(default='Blogs for High School Teachers', blank=True,) + blogs_heading = models.TextField(default='Blogs for High School Teachers', blank=True, ) rfi_heading = models.TextField(default="Don't see what you're looking for?") - rfi_text = models.CharField(default="We're here to answer any questions you may have. Complete the form to get in contact with a member of our team.", max_length=255) + rfi_text = models.CharField( + default="We're here to answer any questions you may have. Complete the form to get in contact with a member of our team.", + max_length=255) promote_image = models.ForeignKey( 'wagtailimages.Image', @@ -2923,59 +2936,59 @@ def subject_category(self): @property def books(self): - books = Book.objects.order_by('path') - book_data = [] - for book in books: - k12subjects=[] - for subject in book.k12book_subjects.all(): - k12subjects.append(subject.subject_name) - subjects=[] - for subject in book.book_subjects.all(): - subjects.append(subject.subject_name) - if book.k12book_subjects is not None \ - and self.title in k12subjects \ - and book.book_state not in ['retired', 'draft']: - book_data.append({ - 'id': book.id, - 'slug': 'books/{}'.format(book.slug), - 'title': book.title, - 'description': book.description, - 'cover_url': book.cover_url, - 'is_ap': book.is_ap, - 'is_hs': 'High School' in subjects, - 'cover_color': book.cover_color, - 'high_resolution_pdf_url': book.high_resolution_pdf_url, - 'low_resolution_pdf_url': book.low_resolution_pdf_url, - 'ibook_link': book.ibook_link, - 'ibook_link_volume_2': book.ibook_link_volume_2, - 'webview_link': book.webview_link, - 'webview_rex_link': book.webview_rex_link, - 'bookshare_link': book.bookshare_link, - 'kindle_link': book.kindle_link, - 'amazon_coming_soon': book.amazon_coming_soon, - 'amazon_link': book.amazon_link, - 'bookstore_coming_soon': book.bookstore_coming_soon, - 'comp_copy_available': book.comp_copy_available, - 'salesforce_abbreviation': book.salesforce_abbreviation, - 'salesforce_name': book.salesforce_name, - 'urls': book.book_urls(), - 'updated': book.updated, - 'created': book.created, - 'publish_date': book.publish_date, - 'last_updated_pdf': book.last_updated_pdf - }) - return book_data - + books = Book.objects.order_by('path') + book_data = [] + for book in books: + k12subjects = [] + for subject in book.k12book_subjects.all(): + k12subjects.append(subject.subject_name) + subjects = [] + for subject in book.book_subjects.all(): + subjects.append(subject.subject_name) + if book.k12book_subjects is not None \ + and self.title in k12subjects \ + and book.book_state not in ['retired', 'draft']: + book_data.append({ + 'id': book.id, + 'slug': 'books/{}'.format(book.slug), + 'title': book.title, + 'description': book.description, + 'cover_url': book.cover_url, + 'is_ap': book.is_ap, + 'is_hs': 'High School' in subjects, + 'cover_color': book.cover_color, + 'high_resolution_pdf_url': book.high_resolution_pdf_url, + 'low_resolution_pdf_url': book.low_resolution_pdf_url, + 'ibook_link': book.ibook_link, + 'ibook_link_volume_2': book.ibook_link_volume_2, + 'webview_link': book.webview_link, + 'webview_rex_link': book.webview_rex_link, + 'bookshare_link': book.bookshare_link, + 'kindle_link': book.kindle_link, + 'amazon_coming_soon': book.amazon_coming_soon, + 'amazon_link': book.amazon_link, + 'bookstore_coming_soon': book.bookstore_coming_soon, + 'comp_copy_available': book.comp_copy_available, + 'salesforce_abbreviation': book.salesforce_abbreviation, + 'salesforce_name': book.salesforce_name, + 'urls': book.book_urls(), + 'updated': book.updated, + 'created': book.created, + 'publish_date': book.publish_date, + 'last_updated_pdf': book.last_updated_pdf + }) + return book_data def student_resource_headers(self): - student_resource_data=[] - book_ids={} + student_resource_data = [] + book_ids = {} for book in self.books: book_id = book.get('id') book_title = book.get('title') - book_ids[book_id]=book_title - for resource in BookStudentResources.objects.filter(display_on_k12=True, book_student_resource_id__in = book_ids).all(): - link_document_url= None + book_ids[book_id] = book_title + for resource in BookStudentResources.objects.filter(display_on_k12=True, + book_student_resource_id__in=book_ids).all(): + link_document_url = None if resource.link_document_id is not None: link_document_url = resource.link_document_url student_resource_data.append({ @@ -2994,18 +3007,19 @@ def student_resource_headers(self): 'print_link': resource.print_link, 'display_on_k12': resource.display_on_k12, 'resource_category': resource.resource_category, - }) + }) return student_resource_data def faculty_resource_headers(self): - faculty_resource_data=[] - book_ids={} + faculty_resource_data = [] + book_ids = {} for book in self.books: book_id = book.get('id') book_title = book.get('title') - book_ids[book_id]=book_title - for resource in BookFacultyResources.objects.filter(display_on_k12=True, book_faculty_resource_id__in = book_ids).all(): - link_document_url= None + book_ids[book_id] = book_title + for resource in BookFacultyResources.objects.filter(display_on_k12=True, + book_faculty_resource_id__in=book_ids).all(): + link_document_url = None if resource.link_document_id is not None: link_document_url = resource.link_document_url faculty_resource_data.append({ @@ -3025,7 +3039,7 @@ def faculty_resource_headers(self): 'k12': resource.k12, 'display_on_k12': resource.display_on_k12, 'resource_category': resource.resource_category, - }) + }) return faculty_resource_data def get_sitemap_urls(self, request=None): @@ -3082,7 +3096,7 @@ def get_sitemap_urls(self, request=None): ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -3092,10 +3106,8 @@ def get_sitemap_urls(self, request=None): parent_page_types = ['pages.K12MainPage'] - - class Meta: - verbose_name = "K12 Subject" + verbose_name = "K12 Subject" class AllyLogos(Page): @@ -3121,7 +3133,7 @@ class AllyLogos(Page): ) content_panels = [ - FieldPanel('title'), + TitleFieldPanel('title'), FieldPanel('heading'), FieldPanel('description'), FieldPanel('ally_logos_heading'), @@ -3144,7 +3156,7 @@ class AllyLogos(Page): ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -3173,25 +3185,26 @@ class Assignable(Page): def get_heading_title_image_url(self): return build_document_url(self.heading_title_image.url) + heading_title_image_url = property(get_heading_title_image_url) - subheading = models.CharField(max_length=255,blank=True, null=True) + subheading = models.CharField(max_length=255, blank=True, null=True) heading_description = RichTextField(blank=True, null=True) add_assignable_cta_header = models.CharField(max_length=255, blank=True, null=True) add_assignable_cta_description = models.TextField(blank=True, null=True) add_assignable_cta_link = models.URLField(blank=True, null=True) add_assignable_cta_button_text = models.CharField(max_length=255, blank=True, null=True) - available_courses_header = models.CharField(max_length=255,blank=True, null=True) + available_courses_header = models.CharField(max_length=255, blank=True, null=True) available_books = StreamField([ ('course', AssignableBookBlock()), ], null=True, blank=True, use_json_field=True) - courses_coming_soon_header = models.CharField(max_length=255,blank=True, null=True) + courses_coming_soon_header = models.CharField(max_length=255, blank=True, null=True) coming_soon_books = StreamField([ ('course', AssignableBookBlock()), ], null=True, blank=True, use_json_field=True) - assignable_cta_text = models.CharField(max_length=255,blank=True, null=True) + assignable_cta_text = models.CharField(max_length=255, blank=True, null=True) assignable_cta_link = models.URLField(blank=True, null=True) - assignable_cta_button_text = models.CharField(max_length=255,blank=True, null=True) - section_2_heading = models.CharField(max_length=255,blank=True, null=True) + assignable_cta_button_text = models.CharField(max_length=255, blank=True, null=True) + section_2_heading = models.CharField(max_length=255, blank=True, null=True) section_2_description = models.TextField(blank=True, null=True) image_carousel = StreamField( blocks.StreamBlock([ @@ -3203,7 +3216,7 @@ def get_heading_title_image_url(self): ('faq', FAQBlock()), ], blank=True, null=True, use_json_field=True) quote = models.TextField(blank=True, null=True) - quote_author = models.CharField(max_length=255,blank=True, null=True) + quote_author = models.CharField(max_length=255, blank=True, null=True) quote_title = models.CharField(max_length=255, blank=True, null=True) quote_school = models.CharField(max_length=255, blank=True, null=True) tos_link = models.URLField(blank=True, null=True) @@ -3217,7 +3230,7 @@ def get_heading_title_image_url(self): ) content_panels = [ - FieldPanel('title'), + TitleFieldPanel('title'), FieldPanel('heading_image'), FieldPanel('heading_title_image'), FieldPanel('subheading'), @@ -3278,7 +3291,7 @@ def get_heading_title_image_url(self): ] promote_panels = [ - FieldPanel('slug'), + FieldPanel('slug', widget=SlugInput), FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('promote_image') @@ -3287,6 +3300,3 @@ def get_heading_title_image_url(self): parent_page_type = ['pages.HomePage'] template = 'page.html' max_count = 1 - - - diff --git a/pages/tests.py b/pages/tests.py index f4fdeb15a..7ddb1f753 100644 --- a/pages/tests.py +++ b/pages/tests.py @@ -583,7 +583,6 @@ def setUp(self): self.client = Client() self.sso_cookie = 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..mktqRLloaCze0VeT.0WU9d20PI1Iu6zajuQMiIJ5GQJVy0DQ1H3BINxkYIS56f1X6p_mesuujHphLcukQny6q0_rmvZUkKLEyojpo14p3czXXu0GF2EWtxprPmfOnPdAig0GXCBJxYVbxKIuZdR4FYodIZaDuDUjXC_hJohHuVoiTQW7jJEGIFj8m9dgAqP-3hOnqaO0C0OvKWoXi0sLb3CvTraT2AGRgj69jkg4B-2y1sUZn_yZO6o2HekGlxzhnKT7ILEAl7cd68W6LmmN0vk4V4nNFkcg0XQ3i1MzWLZroGSjD_9HrhALdHcofBC39UClOzxnbpynFlZk0gr7m0_MmCUFqWKSL8G0eRT9sgOIW6THsl0jpT4KyUFREVdGkTWxaS2qYRqZEQBk9wZlAHuHkE8s6UNZNYhvU5RJkGC1m4faHnM_umTEFQq5aBvRe7gq_8OtOGASXtmFggapuUSfxWX8Bxh8IgA4BT_DuxQbC4biO7FauOT0glZ0Dk4ZXIjbhxBnedtjmeH02xa4RpcKOSOv4UMy1ajCc4KLp9K5drQTKs1PeShAUg1aN58ZwgQq1w4MdjXHkMqnJFiFWt9abF3WYG2EywhTogxIcgq6IrrAIzW4NSptnQVAcxTPxp7hb3OT1mY9dcK138h0GtBR6Z562VbDRaSwplXCLvwcqKoGJyhotcl2m78ESmu1kIO_AJVstlaZprEA0Ji0A7uJlL03JAeTvlJptcouFGax8LGlxq9Ekpz_zifUxLR52MJ8otKcapsZvmtOpc8A70p7V-ceinzKdL5Jq_zA0teJ1Qk2gjMJ7IdeyG3VZ0QIFOQ_FffGkbdW1Ow-nHFHLVfHbkyEF65HIGHEN_RqL0OKUa1HcPlmL5c1S4w5zgdA-2ZAda6ASnY1clwGufFek5AwMp5SAUmdNUBYIDdQm6hbzn0Xe2VB5Y-jqKBOq4yk-M4rmwAf-tD5NJiKaYLZwT-Zst2eG8InRUOmQt6lBJtGNIYnk_a__rX8rfJpvKIJ-Ws8b9fHvSjzSQOroqM3KmfLrevucaJA7jM7CydgOOnt3VQGXKnYqgnhD6jii0zjR3sGAJHF24tQIVtoFCQGBJZly_MQL2Y-EeLqz64Z7AZDptM9oXK035cbvMMuzG9b1jH3XE2o3gO5ekixDpMwTKT8uI1wH9gOqbf9ehirExYgxEh-EVXTlT6t-kk4N-tu83zCLi8MZNTAVqlisakFlnkn4o53Bj5nn9bRQT61HJG_cLxs.Sz3rB-YyqatRUpekkxpyfw' - @property def target(self): def test_redirect(path): @@ -593,7 +592,7 @@ def test_redirect(path): mock_user_login() response = self.client.get(path) - self.assertEqual(response.status_code, 302) + self.assertIn(response.status_code, (301, 302)) return response return test_redirect diff --git a/pages/wagtail_hooks.py b/pages/wagtail_hooks.py index fb4778484..6f5803767 100644 --- a/pages/wagtail_hooks.py +++ b/pages/wagtail_hooks.py @@ -4,7 +4,7 @@ from wagtail import hooks -@hooks.register('insert_editor_css') +@hooks.register('insert_global_admin_css') def editor_css(): return format_html( '', @@ -21,4 +21,4 @@ def editor_js(): """, reverse('wagtailadmin_choose_page_external_link') - ) \ No newline at end of file + ) diff --git a/requirements/base.txt b/requirements/base.txt index 089c8e2bd..f6fdc7615 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,10 +1,10 @@ appdirs==1.4.4 chardet==4.0.0 -django==4.1.7 +django==4.2.8 django-admin-rangefilter==0.8.4 django-crontab==0.7.1 django-extensions==3.2.1 -django-filter==22.1 +django-filter==23.5 django-import-export==2.8.0 django-libsass==0.9 django-rest-auth==0.9.5 @@ -31,7 +31,8 @@ ua_parser==0.16.1 unicodecsv==0.14.1 Unidecode==1.3.4 vcrpy==4.1.1 # for recording test interactions with third-party APIs -wagtail==4.1.9 +wagtail==5.2.3 +wagtail-modeladmin==1.0.0 Wand==0.6.7 # for supporting animated gifs whitenoise==6.1.0 xlrd==2.0.1 diff --git a/salesforce/models.py b/salesforce/models.py index 148941cf5..5cb94636f 100644 --- a/salesforce/models.py +++ b/salesforce/models.py @@ -1,5 +1,4 @@ from django.db import models -from django.db.models import Avg from django.core.validators import MaxValueValidator, MinValueValidator from django.core.exceptions import ValidationError @@ -287,7 +286,7 @@ def rating_count(self): @hooks.register('register_admin_menu_item') def register_partner_menu_item(): - return MenuItem('Partners', '/django-admin/salesforce/partner/', classnames='icon icon-group', order=3000) + return MenuItem('Partners', '/django-admin/salesforce/partner/', classname='icon icon-group', order=3000) class PartnerFieldNameMapping(models.Model): diff --git a/salesforce/tests.py b/salesforce/tests.py index 5972346f3..ca1e989dc 100644 --- a/salesforce/tests.py +++ b/salesforce/tests.py @@ -168,7 +168,7 @@ def setUp(self): self.opportunity.save() def test_query_opportunity_by_account_uuid(self): - response = self.client.get('/apps/cms/api/salesforce/renewal?account_uuid=f826f1b1-ead5-4594-82b3-df9a2753cb43') + response = self.client.get('/apps/cms/api/salesforce/renewal/?account_uuid=f826f1b1-ead5-4594-82b3-df9a2753cb43') self.assertIn(b'"students": "123"', response.content) diff --git a/shared/test_utilities.py b/shared/test_utilities.py index 111276c16..0516c11b7 100644 --- a/shared/test_utilities.py +++ b/shared/test_utilities.py @@ -2,10 +2,8 @@ from django.core.handlers.base import BaseHandler from django.test.client import RequestFactory from django.http.response import HttpResponsePermanentRedirect -from django.contrib.auth import login as auth_login from django.contrib.auth.models import User -from oxauth.functions import decrypt_cookie -from oxauth.views import create_sso_profile + def assertPathDoesNotRedirectToTrailingSlash(test, path): try: @@ -15,19 +13,24 @@ def assertPathDoesNotRedirectToTrailingSlash(test, path): response = test.client.get(path) - if (isinstance(response, HttpResponsePermanentRedirect)): - test.assertNotEqual(response.url, path + "/") + if isinstance(response, HttpResponsePermanentRedirect): + test.assertNotEqual(response.url, path + "/") + def mock_user_login(): - cookie = 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..mktqRLloaCze0VeT.0WU9d20PI1Iu6zajuQMiIJ5GQJVy0DQ1H3BINxkYIS56f1X6p_mesuujHphLcukQny6q0_rmvZUkKLEyojpo14p3czXXu0GF2EWtxprPmfOnPdAig0GXCBJxYVbxKIuZdR4FYodIZaDuDUjXC_hJohHuVoiTQW7jJEGIFj8m9dgAqP-3hOnqaO0C0OvKWoXi0sLb3CvTraT2AGRgj69jkg4B-2y1sUZn_yZO6o2HekGlxzhnKT7ILEAl7cd68W6LmmN0vk4V4nNFkcg0XQ3i1MzWLZroGSjD_9HrhALdHcofBC39UClOzxnbpynFlZk0gr7m0_MmCUFqWKSL8G0eRT9sgOIW6THsl0jpT4KyUFREVdGkTWxaS2qYRqZEQBk9wZlAHuHkE8s6UNZNYhvU5RJkGC1m4faHnM_umTEFQq5aBvRe7gq_8OtOGASXtmFggapuUSfxWX8Bxh8IgA4BT_DuxQbC4biO7FauOT0glZ0Dk4ZXIjbhxBnedtjmeH02xa4RpcKOSOv4UMy1ajCc4KLp9K5drQTKs1PeShAUg1aN58ZwgQq1w4MdjXHkMqnJFiFWt9abF3WYG2EywhTogxIcgq6IrrAIzW4NSptnQVAcxTPxp7hb3OT1mY9dcK138h0GtBR6Z562VbDRaSwplXCLvwcqKoGJyhotcl2m78ESmu1kIO_AJVstlaZprEA0Ji0A7uJlL03JAeTvlJptcouFGax8LGlxq9Ekpz_zifUxLR52MJ8otKcapsZvmtOpc8A70p7V-ceinzKdL5Jq_zA0teJ1Qk2gjMJ7IdeyG3VZ0QIFOQ_FffGkbdW1Ow-nHFHLVfHbkyEF65HIGHEN_RqL0OKUa1HcPlmL5c1S4w5zgdA-2ZAda6ASnY1clwGufFek5AwMp5SAUmdNUBYIDdQm6hbzn0Xe2VB5Y-jqKBOq4yk-M4rmwAf-tD5NJiKaYLZwT-Zst2eG8InRUOmQt6lBJtGNIYnk_a__rX8rfJpvKIJ-Ws8b9fHvSjzSQOroqM3KmfLrevucaJA7jM7CydgOOnt3VQGXKnYqgnhD6jii0zjR3sGAJHF24tQIVtoFCQGBJZly_MQL2Y-EeLqz64Z7AZDptM9oXK035cbvMMuzG9b1jH3XE2o3gO5ekixDpMwTKT8uI1wH9gOqbf9ehirExYgxEh-EVXTlT6t-kk4N-tu83zCLi8MZNTAVqlisakFlnkn4o53Bj5nn9bRQT61HJG_cLxs.Sz3rB-YyqatRUpekkxpyfw' - decrypted_cookie = decrypt_cookie(cookie).payload_dict['sub'] + user, _ = User.objects.get_or_create( + username='test', + first_name='Test', + last_name='User', + email='test@openstax.org' + ) - user = create_sso_profile(decrypted_cookie) return user + class RequestMock(RequestFactory): def request(self, **request): - "Construct a generic request object." + """Construct a generic request object.""" request = RequestFactory.request(self, **request) handler = BaseHandler() handler.load_middleware() diff --git a/snippets/models.py b/snippets/models.py index a410e7215..ff6524b0e 100644 --- a/snippets/models.py +++ b/snippets/models.py @@ -1,9 +1,9 @@ from django.db import models from django.core.exceptions import ValidationError from wagtail.search import index -from wagtail.admin.panels import FieldPanel, InlinePanel +from wagtail.admin.panels import FieldPanel from wagtail.fields import RichTextField -from wagtail.models import TranslatableMixin, Orderable +from wagtail.models import TranslatableMixin from wagtail.snippets.models import register_snippet from openstax.functions import build_image_url from books.constants import BOOK_STATES, COVER_COLORS, K12_CATEGORIES @@ -114,15 +114,17 @@ def get_resource_icon(self): FieldPanel('resource_category') ] - search_fields = [ - index.SearchField('heading', partial_match=True), + index.SearchField('heading', boost=10), + index.AutocompleteField('heading'), + index.FilterField('heading'), index.FilterField('locale_id'), ] def __str__(self): return self.heading + register_snippet(FacultyResource) @@ -155,7 +157,10 @@ def get_resource_icon(self): ] search_fields = [ - index.SearchField('heading', partial_match=True), + index.SearchField('heading', boost=10), + index.AutocompleteField('heading'), + index.FilterField('heading'), + index.FilterField('locale_id'), ] def __str__(self): @@ -204,7 +209,9 @@ class SharedContent(TranslatableMixin, index.Indexed, models.Model): ] search_fields = [ - index.SearchField('title', partial_match=True), + index.SearchField('title', boost=10), + index.AutocompleteField('title'), + index.FilterField('title'), ] def __str__(self): @@ -237,7 +244,9 @@ def get_news_logo(self): ] search_fields = [ - index.SearchField('name', partial_match=True), + index.SearchField('name', boost=10), + index.AutocompleteField('name'), + index.FilterField('name'), ] def __str__(self): @@ -475,4 +484,3 @@ def __str__(self): register_snippet(AmazonBookBlurb) - diff --git a/wagtailimportexport/wagtail_hooks.py b/wagtailimportexport/wagtail_hooks.py index a47b59588..7001d7487 100644 --- a/wagtailimportexport/wagtail_hooks.py +++ b/wagtailimportexport/wagtail_hooks.py @@ -33,5 +33,5 @@ def register_import_export_menu_item(): Add the menu item to admin side menu. """ return ImportExportMenuItem( - 'Import / Export', reverse('wagtailimportexport:index'), classnames='icon icon-download', order=800 + 'Import / Export', reverse('wagtailimportexport:index'), classname='icon icon-download', order=800 ) diff --git a/webinars/tests.py b/webinars/tests.py index 72e550ff5..fa18b902e 100644 --- a/webinars/tests.py +++ b/webinars/tests.py @@ -92,6 +92,6 @@ def test_all_webinars(self): self.assertContains(response, 'Economics') def test_webinar_search(self): - response = self.client.get('/apps/cms/api/webinars/search?q=Webinar 2') + response = self.client.get('/apps/cms/api/webinars/search/?q=Webinar 2') self.assertContains(response, 'Economics') self.assertContains(response, 'Research') diff --git a/webinars/wagtail_hooks.py b/webinars/wagtail_hooks.py index 6a6cfb312..4ea023ba1 100644 --- a/webinars/wagtail_hooks.py +++ b/webinars/wagtail_hooks.py @@ -1,4 +1,4 @@ -from wagtail.contrib.modeladmin.options import ModelAdmin, ModelAdminGroup, modeladmin_register +from wagtail_modeladmin.options import ModelAdmin, modeladmin_register from .models import Webinar