From dde2bf008cdf1177e7d701cc422541d8f1199a97 Mon Sep 17 00:00:00 2001 From: Michael Volo Date: Thu, 8 May 2025 00:33:33 -0500 Subject: [PATCH 01/14] add book list block to flex pages --- books/models.py | 80 ++-- pages/custom_blocks.py | 13 + pages/migrations/0156_alter_rootpage_body.py | 412 +++++++++++++++++++ pages/models.py | 6 +- 4 files changed, 473 insertions(+), 38 deletions(-) create mode 100644 pages/migrations/0156_alter_rootpage_body.py diff --git a/books/models.py b/books/models.py index 68a1d4c57..dae32e3fc 100644 --- a/books/models.py +++ b/books/models.py @@ -32,6 +32,47 @@ def cleanhtml(raw_html): return cleantext +def get_book_data(book): + """Return the book data dict for a single Book instance, matching BookIndex.books property.""" + has_faculty_resources = BookFacultyResources.objects.filter(book_faculty_resource=book).exists() + has_student_resources = BookStudentResources.objects.filter(book_student_resource=book).exists() + try: + return { + 'id': book.id, + 'cnx_id': book.cnx_id, + 'slug': f'books/{book.slug}', + 'book_state': book.book_state, + 'title': book.title, + 'subjects': book.subjects(), + 'is_ap': book.is_ap, + 'cover_url': book.cover_url, + '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(), + 'last_updated_pdf': book.last_updated_pdf, + 'has_faculty_resources': has_faculty_resources, + 'has_student_resources': has_student_resources, + 'assignable_book': book.assignable_book, + 'promote_tags': [snippet.value.name for snippet in book.promote_snippet], + } + except Exception as e: + capture_exception(e) + return None + + class VideoFacultyResource(models.Model): resource_heading = models.CharField(max_length=255) resource_description = RichTextField(blank=True, null=True) @@ -1101,42 +1142,9 @@ def books(self): 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() - has_student_resources = BookStudentResources.objects.filter(book_student_resource=book).exists() - try: - book_data.append({ - 'id': book.id, - 'cnx_id': book.cnx_id, - 'slug': 'books/{}'.format(book.slug), - 'book_state': book.book_state, - 'title': book.title, - 'subjects': book.subjects(), - 'is_ap': book.is_ap, - 'cover_url': book.cover_url, - '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(), - 'last_updated_pdf': book.last_updated_pdf, - 'has_faculty_resources': has_faculty_resources, - 'has_student_resources': has_student_resources, - 'assignable_book': book.assignable_book, - 'promote_tags': [snippet.value.name for snippet in book.promote_snippet], - }) - except Exception as e: - capture_exception(e) + data = get_book_data(book) + if data: + book_data.append(data) return book_data content_panels = Page.content_panels + [ diff --git a/pages/custom_blocks.py b/pages/custom_blocks.py index 798e2ad28..4d3c208a6 100644 --- a/pages/custom_blocks.py +++ b/pages/custom_blocks.py @@ -8,6 +8,7 @@ from api.serializers import ImageSerializer from openstax.functions import build_image_url, build_document_url from wagtail.rich_text import expand_db_html +from books.models import get_book_data class APIRichTextBlock(blocks.RichTextBlock): @@ -144,6 +145,7 @@ class QuoteBlock(StructBlock): name = blocks.CharBlock(help_text="The name of the person or entity to attribute the quote to.") title = blocks.CharBlock(requred=False, help_text="Additional title or label about the quotee.") + class DividerBlock(StructBlock): image = APIImageChooserBlock() config = blocks.StreamBlock([ @@ -323,3 +325,14 @@ def get_api_representation(self, value, context=None): 'cover': build_document_url(value['cover'].url), 'title': value['title'], } + +class BookListBlock(blocks.StreamBlock): + books = blocks.PageChooserBlock(page_type=['books.Book'], required=False) + + class Meta: + icon = 'placeholder' + label = "Book List" + + def get_api_representation(self, value, context=None): + # value is a StreamValue of blocks, each with .value as a Book page + return [get_book_data(book.value) for book in value if get_book_data(book.value)] diff --git a/pages/migrations/0156_alter_rootpage_body.py b/pages/migrations/0156_alter_rootpage_body.py new file mode 100644 index 000000000..fb9a14c20 --- /dev/null +++ b/pages/migrations/0156_alter_rootpage_body.py @@ -0,0 +1,412 @@ +# Generated by Django 5.0.14 on 2025-05-08 05:14 + +import wagtail.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("pages", "0155_alter_rootpage_layout"), + ] + + operations = [ + migrations.AlterField( + model_name="rootpage", + name="body", + field=wagtail.fields.StreamField( + [("hero", 53), ("section", 55), ("divider", 62), ("html", 18)], + block_lookup={ + 0: ("pages.custom_blocks.APIRichTextBlock", (), {}), + 1: ( + "wagtail.blocks.CharBlock", + (), + {"help_text": "Visible text of the link or button.", "required": True}, + ), + 2: ( + "wagtail.blocks.CharBlock", + (), + { + "help_text": "Accessible label for the link or button. if provided, must begin with the visible text.", + "required": False, + }, + ), + 3: ( + "wagtail.blocks.URLBlock", + (), + {"help_text": "External links are full urls that can go anywhere", "required": False}, + ), + 4: ("wagtail.blocks.PageChooserBlock", (), {"required": False}), + 5: ("wagtail.documents.blocks.DocumentChooserBlock", (), {"required": False}), + 6: ( + "wagtail.blocks.CharBlock", + (), + { + "help_text": "Anchor links reference the ID of an element on the page, and scroll the page there.", + "required": False, + }, + ), + 7: ( + "wagtail.blocks.StreamBlock", + [[("external", 3), ("internal", 4), ("document", 5), ("anchor", 6)]], + {"required": True}, + ), + 8: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": [ + ("orange", "Orange"), + ("white", "White"), + ("blue_outline", "Blue Outline"), + ("deep_green_outline", "Deep Green Outline"), + ], + "help_text": "Specifies the button style. Default unspecified, meaning the first button in the block is orange and the second is white.", + }, + ), + 9: ( + "wagtail.blocks.StreamBlock", + [[("style", 8)]], + {"block_counts": {"style": {"max_num": 1}}, "required": False}, + ), + 10: ( + "wagtail.blocks.StructBlock", + [[("text", 1), ("aria_label", 2), ("target", 7), ("config", 9)]], + {"label": "Link", "required": False}, + ), + 11: ("wagtail.blocks.ListBlock", (10,), {"default": [], "label": "Call To Action", "max_num": 1}), + 12: ("wagtail.blocks.StructBlock", [[("text", 0), ("cta_block", 11)]], {}), + 13: ("wagtail.blocks.ListBlock", (12,), {}), + 14: ( + "wagtail.blocks.IntegerBlock", + (), + {"help_text": "Sets the width of the individual cards. default 27.", "min_value": 0}, + ), + 15: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": [("rounded", "Rounded"), ("square", "Square")], + "help_text": "The border style of the cards. default borderless.", + }, + ), + 16: ( + "wagtail.blocks.StreamBlock", + [[("card_size", 14), ("card_style", 15)]], + { + "block_counts": {"card_size": {"max_num": 1}, "card_style": {"max_num": 1}}, + "required": False, + }, + ), + 17: ("wagtail.blocks.StructBlock", [[("cards", 13), ("config", 16)]], {"label": "Cards Block"}), + 18: ("wagtail.blocks.RawHTMLBlock", (), {}), + 19: ( + "wagtail.blocks.StructBlock", + [[("text", 1), ("aria_label", 2), ("target", 7), ("config", 9)]], + {"label": "Button", "required": False}, + ), + 20: ("wagtail.blocks.ListBlock", (19,), {"default": [], "label": "Actions", "max_num": 2}), + 21: ( + "wagtail.blocks.CharBlock", + (), + {"help_text": 'Sets the "analytics nav" field for links within this group.', "required": False}, + ), + 22: ( + "wagtail.blocks.StreamBlock", + [[("analytics_label", 21)]], + {"block_counts": {"analytics_label": {"max_num": 1}}, "required": False}, + ), + 23: ("wagtail.blocks.StructBlock", [[("actions", 20), ("config", 22)]], {}), + 24: ( + "wagtail.blocks.StructBlock", + [[("text", 1), ("aria_label", 2), ("target", 7)]], + {"label": "Link", "required": False}, + ), + 25: ("wagtail.blocks.ListBlock", (24,), {"default": [], "label": "Links"}), + 26: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": [("white", "White"), ("blue", "Blue"), ("deep-green", "Deep Green")], + "help_text": "The color of the link buttons. Default white.", + }, + ), + 27: ( + "wagtail.blocks.StreamBlock", + [[("color", 26), ("analytics_label", 21)]], + { + "block_counts": {"analytics_label": {"max_num": 1}, "color": {"max_num": 1}}, + "required": False, + }, + ), + 28: ("wagtail.blocks.StructBlock", [[("links", 25), ("config", 27)]], {}), + 29: ("pages.custom_blocks.APIImageChooserBlock", (), {}), + 30: ("wagtail.blocks.RichTextBlock", (), {"help_text": "The quote content."}), + 31: ( + "wagtail.blocks.CharBlock", + (), + {"help_text": "The name of the person or entity to attribute the quote to."}, + ), + 32: ( + "wagtail.blocks.CharBlock", + (), + {"help_text": "Additional title or label about the quotee.", "requred": False}, + ), + 33: ( + "wagtail.blocks.StructBlock", + [[("image", 29), ("content", 30), ("name", 31), ("title", 32)]], + {}, + ), + 34: ( + "wagtail.blocks.RichTextBlock", + (), + {"help_text": "The visible text of the question (does not collapse).", "required": True}, + ), + 35: ( + "wagtail.blocks.CharBlock", + (), + {"help_text": "Not visible to user, must be unique in this FAQ.", "required": True}, + ), + 36: ( + "wagtail.blocks.RichTextBlock", + (), + { + "help_text": "The answer to the question, is hidden until the question is expanded.", + "required": True, + }, + ), + 37: ( + "wagtail.documents.blocks.DocumentChooserBlock", + (), + {"help_text": "Not sure this does anything.", "required": False}, + ), + 38: ( + "wagtail.blocks.StructBlock", + [[("question", 34), ("slug", 35), ("answer", 36), ("document", 37)]], + {}, + ), + 39: ("wagtail.blocks.StreamBlock", [[("faq", 38)]], {}), + 40: ("wagtail.blocks.StreamBlock", [[("books", 4)]], {}), + 41: ( + "wagtail.blocks.StreamBlock", + [ + [ + ("cards_block", 17), + ("text", 0), + ("html", 18), + ("cta_block", 23), + ("links_group", 28), + ("quote", 33), + ("faq", 39), + ("book_list", 40), + ] + ], + {}, + ), + 42: ("pages.custom_blocks.APIImageChooserBlock", (), {"required": False}), + 43: ("wagtail.blocks.CharBlock", (), {"required": False}), + 44: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": [ + ("left", "Left"), + ("right", "Right"), + ("top_left", "Top Left"), + ("top_right", "Top Right"), + ("bottom_left", "Bottom Left"), + ("bottom_right", "Bottom Right"), + ], + "help_text": "Controls if the image is on the left or right side of the content, and if it prefers to be at the top, center, or bottom of the available space.", + }, + ), + 45: ( + "wagtail.blocks.RegexBlock", + (), + { + "error_mssages": {"invalid": "not a valid id."}, + "help_text": "HTML id of this element. not visible to users, but is visible in urls and is used to link to a certain part of the page with an anchor link. eg: cool_section", + "regex": "[a-zA-Z0-9\\-_]", + }, + ), + 46: ( + "wagtail.blocks.RegexBlock", + (), + { + "error_mssages": {"invalid": "not a valid hex color."}, + "help_text": "Sets the background color of the section. value must be hex eg: #ff0000. Default grey.", + "regex": "#[a-zA-Z0-9]{6}", + }, + ), + 47: ( + "wagtail.blocks.IntegerBlock", + (), + {"help_text": "Creates space above and below this section. default 0.", "min_value": 0}, + ), + 48: ( + "wagtail.blocks.IntegerBlock", + (), + {"help_text": "Creates space above this section. default 0.", "min_value": 0}, + ), + 49: ( + "wagtail.blocks.IntegerBlock", + (), + {"help_text": "Creates space below this section. default 0.", "min_value": 0}, + ), + 50: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": [("center", "Center"), ("left", "Left"), ("right", "Right")], + "help_text": "Configures text alignment within the container. Default Left.", + }, + ), + 51: ( + "wagtail.blocks.CharBlock", + (), + { + "help_text": 'Sets the "analytics nav" field for links within this section.', + "required": False, + }, + ), + 52: ( + "wagtail.blocks.StreamBlock", + [ + [ + ("image_alignment", 44), + ("id", 45), + ("background_color", 46), + ("padding", 47), + ("padding_top", 48), + ("padding_bottom", 49), + ("text_alignment", 50), + ("analytics_label", 51), + ] + ], + { + "block_counts": { + "analytics_label": {"max_num": 1}, + "background_color": {"max_num": 1}, + "id": {"max_num": 1}, + "image_alignment": {"max_num": 1}, + "padding": {"max_num": 1}, + "padding_bottom": {"max_num": 1}, + "padding_top": {"max_num": 1}, + "text_alignment": {"max_num": 1}, + }, + "required": False, + }, + ), + 53: ( + "wagtail.blocks.StructBlock", + [[("content", 41), ("image", 42), ("image_alt", 43), ("config", 52)]], + {}, + ), + 54: ( + "wagtail.blocks.StreamBlock", + [ + [ + ("id", 45), + ("background_color", 46), + ("padding", 47), + ("padding_top", 48), + ("padding_bottom", 49), + ("text_alignment", 50), + ("analytics_label", 51), + ] + ], + { + "block_counts": { + "analytics_label": {"max_num": 1}, + "background_color": {"max_num": 1}, + "id": {"max_num": 1}, + "padding": {"max_num": 1}, + "padding_bottom": {"max_num": 1}, + "padding_top": {"max_num": 1}, + "text_alignment": {"max_num": 1}, + }, + "required": False, + }, + ), + 55: ("wagtail.blocks.StructBlock", [[("content", 41), ("config", 54)]], {}), + 56: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": [ + ("center", "Center"), + ("content_left", "Left side of content."), + ("content_right", "Right side of content."), + ("body_left", "Left side of window."), + ("body_right", "Right side of window."), + ], + "help_text": 'Sets the horizontal alignment of the image. can be further customized with the "Offset..." configurations. Default is Left side of window.', + }, + ), + 57: ( + "wagtail.blocks.RegexBlock", + (), + { + "error_mssages": {"invalid": "not a valid size."}, + "help_text": "Specifies the width of the image. Percentages are relative to the container (body or content, depending on alignment option). Must be valid css measurement. eg: 30px, 50%, 10rem. Default is the size of the image.", + "regex": "^[0-9]+(px|%|rem)$", + "required": False, + }, + ), + 58: ( + "wagtail.blocks.RegexBlock", + (), + { + "error_mssages": {"invalid": "not a valid size."}, + "help_text": "Specifies the height of the image. Percentages are relative to the container (body or content, depending on alignment option). Must be valid css measurement. eg: 30px, 50%, 10rem. Default is the size of the image.", + "regex": "^[0-9]+(px|%|rem)$", + "required": False, + }, + ), + 59: ( + "wagtail.blocks.RegexBlock", + (), + { + "error_mssages": {"invalid": "not a valid size."}, + "help_text": "Moves the image up or down. Percentages are relative to the image size. Must be valid css measurement. eg: 30px, 50%, 10rem. Default is -50%, which moves the image up by half its width (centering it vertically on the divider).", + "regex": "^\\-?[0-9]+(px|%|rem)$", + "required": False, + }, + ), + 60: ( + "wagtail.blocks.RegexBlock", + (), + { + "error_mssages": {"invalid": "not a valid size."}, + "help_text": "Moves the image left or right. Percentages are relative to the image size. Must be valid css measurement. eg: 30px, 50%, 10rem. Default is no offset, which means the image's outer edge will align with the container's edge for left and right alignment. or it'll be perfectly centered for centered alignment.", + "regex": "^\\-?[0-9]+(px|%|rem)$", + "required": False, + }, + ), + 61: ( + "wagtail.blocks.StreamBlock", + [ + [ + ("alignment", 56), + ("width", 57), + ("height", 58), + ("offset_vertical", 59), + ("offset_horizontal", 60), + ] + ], + { + "block_counts": { + "alignment": {"max_num": 1}, + "height": {"max_num": 1}, + "offset_horizontal": {"max_num": 1}, + "offset_vertical": {"max_num": 1}, + "width": {"max_num": 1}, + }, + "required": False, + }, + ), + 62: ("wagtail.blocks.StructBlock", [[("image", 29), ("config", 61)]], {}), + }, + ), + ), + ] diff --git a/pages/models.py b/pages/models.py index 41d4b51da..46895130d 100644 --- a/pages/models.py +++ b/pages/models.py @@ -38,7 +38,8 @@ LinksGroupBlock, \ QuoteBlock, \ LinkInfoBlock, \ - CTALinkBlock + CTALinkBlock, \ + BookListBlock from .custom_fields import Group import snippets.models as snippets @@ -84,7 +85,8 @@ ('quote', QuoteBlock()), ('faq', blocks.StreamBlock([ ('faq', FAQBlock()), - ])) + ])), + ('book_list', BookListBlock()), ] # we have one RootPage, which is the parent of all other pages From 2c4c621f152de1158c2f9f690c710fcd456a0910 Mon Sep 17 00:00:00 2001 From: Michael Volo Date: Thu, 8 May 2025 00:33:58 -0500 Subject: [PATCH 02/14] don't codecov the unnecessary --- codecov.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/codecov.yml b/codecov.yml index ff790fc3f..3c506ccb7 100644 --- a/codecov.yml +++ b/codecov.yml @@ -27,3 +27,7 @@ ignore: - '*/settings/*' - '*settings.py' - '*wsgi.py' + - '*/__init__.py' + - '*/urls.py' + - 'versions/*' # this is internal only, and not mission critical + - 'wagtailimportexport/*' # this doesn't work, canidate for removal / offloading / rewriting \ No newline at end of file From 77831faa33f12b7eff1ecc334c08e1b1b9200190 Mon Sep 17 00:00:00 2001 From: Michael Volo Date: Wed, 7 May 2025 23:35:37 -0600 Subject: [PATCH 03/14] copilot performance Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pages/custom_blocks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/custom_blocks.py b/pages/custom_blocks.py index 4d3c208a6..d00d81a59 100644 --- a/pages/custom_blocks.py +++ b/pages/custom_blocks.py @@ -335,4 +335,4 @@ class Meta: def get_api_representation(self, value, context=None): # value is a StreamValue of blocks, each with .value as a Book page - return [get_book_data(book.value) for book in value if get_book_data(book.value)] + return [book_data for book in value if (book_data := get_book_data(book.value))] From 2518d2613f812a09a534883ed3d9b7612562f820 Mon Sep 17 00:00:00 2001 From: Michael Volo Date: Wed, 7 May 2025 23:40:11 -0600 Subject: [PATCH 04/14] spelling Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- codecov.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codecov.yml b/codecov.yml index 3c506ccb7..e356f9a6e 100644 --- a/codecov.yml +++ b/codecov.yml @@ -30,4 +30,4 @@ ignore: - '*/__init__.py' - '*/urls.py' - 'versions/*' # this is internal only, and not mission critical - - 'wagtailimportexport/*' # this doesn't work, canidate for removal / offloading / rewriting \ No newline at end of file + - 'wagtailimportexport/*' # this doesn't work, candidate for removal / offloading / rewriting \ No newline at end of file From 1f046d747b03782fa52f295280be2ac4444d008b Mon Sep 17 00:00:00 2001 From: Michael Volo Date: Wed, 7 May 2025 23:40:36 -0600 Subject: [PATCH 05/14] prefetching related models Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- books/models.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/books/models.py b/books/models.py index dae32e3fc..30814fe97 100644 --- a/books/models.py +++ b/books/models.py @@ -32,10 +32,17 @@ def cleanhtml(raw_html): return cleantext +def prefetch_book_resources(queryset): + """Prefetch related faculty and student resources for a queryset of books.""" + return queryset.prefetch_related( + models.Prefetch('bookfacultyresources_set', queryset=BookFacultyResources.objects.all(), to_attr='prefetched_faculty_resources'), + models.Prefetch('bookstudentresources_set', queryset=BookStudentResources.objects.all(), to_attr='prefetched_student_resources') + ) + def get_book_data(book): """Return the book data dict for a single Book instance, matching BookIndex.books property.""" - has_faculty_resources = BookFacultyResources.objects.filter(book_faculty_resource=book).exists() - has_student_resources = BookStudentResources.objects.filter(book_student_resource=book).exists() + has_faculty_resources = hasattr(book, 'prefetched_faculty_resources') and bool(book.prefetched_faculty_resources) + has_student_resources = hasattr(book, 'prefetched_student_resources') and bool(book.prefetched_student_resources) try: return { 'id': book.id, From 25668596244db6a08afd555d3284c9333a6f6e89 Mon Sep 17 00:00:00 2001 From: Michael Volo Date: Thu, 8 May 2025 10:39:55 -0500 Subject: [PATCH 06/14] add test database connection --- openstax/settings/test.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openstax/settings/test.py b/openstax/settings/test.py index 5b196c203..78fde067c 100644 --- a/openstax/settings/test.py +++ b/openstax/settings/test.py @@ -13,6 +13,17 @@ 'host': 'test', } +DATABASES = { + 'default': { + 'ENGINE': "django.db.backends.postgresql", + 'NAME': os.getenv('DATABASE_NAME', 'oscms_test'), + 'USER': os.getenv('DATABASE_USER', 'postgres'), + 'PASSWORD': os.getenv('DATABASE_PASSWORD', 'postgres'), + 'HOST': os.getenv('DATABASE_HOST', 'localhost'), + 'PORT': os.getenv('DATABASE_PORT', '5432'), + } +} + # silence whitenoise warnings for CI import warnings warnings.filterwarnings("ignore", message="No directory at", module="whitenoise.base") From d281b1a2e8a913b29bc321d436433facb6eef1ad Mon Sep 17 00:00:00 2001 From: Michael Volo Date: Thu, 8 May 2025 10:45:17 -0500 Subject: [PATCH 07/14] upgrade postgres, add env vars --- .github/workflows/tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a6b2387f5..898161755 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,12 +15,14 @@ jobs: services: postgres: - image: postgres:13 + image: postgres:16 ports: - 5432:5432 options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 env: + POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres + POSTGRES_DB: oscms_test steps: - uses: actions/checkout@v4 From af0390736b6dbd17620da4e21c896942c84461eb Mon Sep 17 00:00:00 2001 From: Michael Volo Date: Thu, 8 May 2025 11:11:52 -0500 Subject: [PATCH 08/14] is it just migration check? --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 898161755..9abf21aaf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,8 +37,8 @@ jobs: cache-dependency-path: requirements/test.txt - run: pip install -r requirements/test.txt - - name: Check migrations - run: python manage.py makemigrations --check + #- name: Check migrations + # run: python manage.py makemigrations --check - name: Run tests and generate coverage reports run: coverage run --source '.' manage.py test --settings=openstax.settings.test From 2662d8385d3dddffe478934e59a27ea061bbc996 Mon Sep 17 00:00:00 2001 From: Michael Volo Date: Thu, 8 May 2025 11:12:17 -0500 Subject: [PATCH 09/14] use get_book_data for new subjects page --- books/models.py | 3 ++- pages/models.py | 38 ++++++-------------------------------- 2 files changed, 8 insertions(+), 33 deletions(-) diff --git a/books/models.py b/books/models.py index 30814fe97..50dc93b90 100644 --- a/books/models.py +++ b/books/models.py @@ -46,11 +46,12 @@ def get_book_data(book): try: return { 'id': book.id, - 'cnx_id': book.cnx_id, 'slug': f'books/{book.slug}', 'book_state': book.book_state, 'title': book.title, 'subjects': book.subjects(), + 'subject_categories': book.subject_categories, + 'k12subject': book.k12subjects(), 'is_ap': book.is_ap, 'cover_url': book.cover_url, 'cover_color': book.cover_color, diff --git a/pages/models.py b/pages/models.py index 46895130d..285494b73 100644 --- a/pages/models.py +++ b/pages/models.py @@ -14,6 +14,7 @@ from api.models import FeatureFlag from openstax.functions import build_image_url, build_document_url from books.models import Book, SubjectBooks, BookFacultyResources, BookStudentResources +from books.models import get_book_data from webinars.models import Webinar from news.models import BlogStreamBlock # for use on the ImpactStories @@ -2953,44 +2954,17 @@ def subjects(self): subject_categories['icon'] = subject.subject_icon all_books = [book for book in Book.objects.all().order_by('title') if subject.name in book.subjects()] for category in snippets.SubjectCategory.objects.filter(subject_id=subject.id).order_by('subject_category'): - books = {} book_list = {} + book_data = [] for book in all_books: if book.subject_categories is not None \ and category.subject_category in book.subject_categories \ and book.book_state not in ['retired', 'unlisted']: - book_data = [] - book_data.append({ - 'id': book.id, - 'slug': 'books/{}'.format(book.slug), - 'book_state': book.book_state, - 'title': book.title, - 'subjects': book.subjects(), - 'subject_categories': book.subject_categories, - 'k12subject': book.k12subjects(), - 'is_ap': book.is_ap, - 'cover_url': book.cover_url, - '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(), - 'last_updated_pdf': book.last_updated_pdf, - }) - books[book.title] = book_data + data = get_book_data(book) + if data: + book_data.append(data) book_list['category_description'] = category.description - book_list['books'] = books + book_list['books'] = book_data categories[category.subject_category] = book_list subject_categories['categories'] = categories subject_list[subject.name] = subject_categories From 422b5d6becf7b855d4239499aa00e9f3a7da0a4c Mon Sep 17 00:00:00 2001 From: Michael Volo Date: Thu, 8 May 2025 11:35:54 -0500 Subject: [PATCH 10/14] add is_hs --- books/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/books/models.py b/books/models.py index 50dc93b90..bbed970a4 100644 --- a/books/models.py +++ b/books/models.py @@ -40,7 +40,6 @@ def prefetch_book_resources(queryset): ) def get_book_data(book): - """Return the book data dict for a single Book instance, matching BookIndex.books property.""" has_faculty_resources = hasattr(book, 'prefetched_faculty_resources') and bool(book.prefetched_faculty_resources) has_student_resources = hasattr(book, 'prefetched_student_resources') and bool(book.prefetched_student_resources) try: From a8878119579fd61e098c4231028591ba1f549aa9 Mon Sep 17 00:00:00 2001 From: Michael Volo Date: Thu, 8 May 2025 11:36:05 -0500 Subject: [PATCH 11/14] add is_hs (part 2) --- books/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/books/models.py b/books/models.py index bbed970a4..c5c4f3751 100644 --- a/books/models.py +++ b/books/models.py @@ -52,6 +52,7 @@ def get_book_data(book): 'subject_categories': book.subject_categories, 'k12subject': book.k12subjects(), 'is_ap': book.is_ap, + 'is_hs': 'High School' in book.subjects(), 'cover_url': book.cover_url, 'cover_color': book.cover_color, 'high_resolution_pdf_url': book.high_resolution_pdf_url, From 8392ea6e15c68cc929787ce759b3ec8e4f7f2e98 Mon Sep 17 00:00:00 2001 From: Michael Volo Date: Thu, 8 May 2025 12:20:39 -0500 Subject: [PATCH 12/14] revert subjects --- pages/models.py | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/pages/models.py b/pages/models.py index 285494b73..46895130d 100644 --- a/pages/models.py +++ b/pages/models.py @@ -14,7 +14,6 @@ from api.models import FeatureFlag from openstax.functions import build_image_url, build_document_url from books.models import Book, SubjectBooks, BookFacultyResources, BookStudentResources -from books.models import get_book_data from webinars.models import Webinar from news.models import BlogStreamBlock # for use on the ImpactStories @@ -2954,17 +2953,44 @@ def subjects(self): subject_categories['icon'] = subject.subject_icon all_books = [book for book in Book.objects.all().order_by('title') if subject.name in book.subjects()] for category in snippets.SubjectCategory.objects.filter(subject_id=subject.id).order_by('subject_category'): + books = {} book_list = {} - book_data = [] for book in all_books: if book.subject_categories is not None \ and category.subject_category in book.subject_categories \ and book.book_state not in ['retired', 'unlisted']: - data = get_book_data(book) - if data: - book_data.append(data) + book_data = [] + book_data.append({ + 'id': book.id, + 'slug': 'books/{}'.format(book.slug), + 'book_state': book.book_state, + 'title': book.title, + 'subjects': book.subjects(), + 'subject_categories': book.subject_categories, + 'k12subject': book.k12subjects(), + 'is_ap': book.is_ap, + 'cover_url': book.cover_url, + '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(), + 'last_updated_pdf': book.last_updated_pdf, + }) + books[book.title] = book_data book_list['category_description'] = category.description - book_list['books'] = book_data + book_list['books'] = books categories[category.subject_category] = book_list subject_categories['categories'] = categories subject_list[subject.name] = subject_categories From 1a176713137cfff948a113dd5a89421adf05a758 Mon Sep 17 00:00:00 2001 From: Michael Volo Date: Tue, 13 May 2025 11:22:30 -0500 Subject: [PATCH 13/14] centralize book data method --- books/models.py | 3 +++ pages/models.py | 61 +++---------------------------------------------- 2 files changed, 6 insertions(+), 58 deletions(-) diff --git a/books/models.py b/books/models.py index c5c4f3751..2f8bd8eb8 100644 --- a/books/models.py +++ b/books/models.py @@ -71,6 +71,9 @@ def get_book_data(book): 'salesforce_name': book.salesforce_name, 'urls': book.book_urls(), 'last_updated_pdf': book.last_updated_pdf, + 'created': book.created, + 'updated': book.updated, + 'publish_date': book.publish_date, 'has_faculty_resources': has_faculty_resources, 'has_student_resources': has_student_resources, 'assignable_book': book.assignable_book, diff --git a/pages/models.py b/pages/models.py index 46895130d..403097050 100644 --- a/pages/models.py +++ b/pages/models.py @@ -13,7 +13,7 @@ from api.models import FeatureFlag from openstax.functions import build_image_url, build_document_url -from books.models import Book, SubjectBooks, BookFacultyResources, BookStudentResources +from books.models import Book, SubjectBooks, BookFacultyResources, BookStudentResources, get_book_data from webinars.models import Webinar from news.models import BlogStreamBlock # for use on the ImpactStories @@ -2960,34 +2960,7 @@ def subjects(self): and category.subject_category in book.subject_categories \ and book.book_state not in ['retired', 'unlisted']: book_data = [] - book_data.append({ - 'id': book.id, - 'slug': 'books/{}'.format(book.slug), - 'book_state': book.book_state, - 'title': book.title, - 'subjects': book.subjects(), - 'subject_categories': book.subject_categories, - 'k12subject': book.k12subjects(), - 'is_ap': book.is_ap, - 'cover_url': book.cover_url, - '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(), - 'last_updated_pdf': book.last_updated_pdf, - }) + book_data.append(get_book_data(book)) books[book.title] = book_data book_list['category_description'] = category.description book_list['books'] = books @@ -3156,35 +3129,7 @@ def books(self): 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 - }) + book_data.append(get_book_data(book)) return book_data def student_resource_headers(self): From 0c74be73681842a82103e7df1aa7dc985161b90d Mon Sep 17 00:00:00 2001 From: Michael Volo Date: Wed, 14 May 2025 10:41:11 -0500 Subject: [PATCH 14/14] Revert "centralize book data method" This reverts commit 1a176713137cfff948a113dd5a89421adf05a758. --- books/models.py | 3 --- pages/models.py | 61 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/books/models.py b/books/models.py index 2f8bd8eb8..c5c4f3751 100644 --- a/books/models.py +++ b/books/models.py @@ -71,9 +71,6 @@ def get_book_data(book): 'salesforce_name': book.salesforce_name, 'urls': book.book_urls(), 'last_updated_pdf': book.last_updated_pdf, - 'created': book.created, - 'updated': book.updated, - 'publish_date': book.publish_date, 'has_faculty_resources': has_faculty_resources, 'has_student_resources': has_student_resources, 'assignable_book': book.assignable_book, diff --git a/pages/models.py b/pages/models.py index 403097050..46895130d 100644 --- a/pages/models.py +++ b/pages/models.py @@ -13,7 +13,7 @@ from api.models import FeatureFlag from openstax.functions import build_image_url, build_document_url -from books.models import Book, SubjectBooks, BookFacultyResources, BookStudentResources, get_book_data +from books.models import Book, SubjectBooks, BookFacultyResources, BookStudentResources from webinars.models import Webinar from news.models import BlogStreamBlock # for use on the ImpactStories @@ -2960,7 +2960,34 @@ def subjects(self): and category.subject_category in book.subject_categories \ and book.book_state not in ['retired', 'unlisted']: book_data = [] - book_data.append(get_book_data(book)) + book_data.append({ + 'id': book.id, + 'slug': 'books/{}'.format(book.slug), + 'book_state': book.book_state, + 'title': book.title, + 'subjects': book.subjects(), + 'subject_categories': book.subject_categories, + 'k12subject': book.k12subjects(), + 'is_ap': book.is_ap, + 'cover_url': book.cover_url, + '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(), + 'last_updated_pdf': book.last_updated_pdf, + }) books[book.title] = book_data book_list['category_description'] = category.description book_list['books'] = books @@ -3129,7 +3156,35 @@ def books(self): if book.k12book_subjects is not None \ and self.title in k12subjects \ and book.book_state not in ['retired', 'draft']: - book_data.append(get_book_data(book)) + 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):