diff --git a/news/search.py b/news/search.py index ee524cf23..fe3680ee8 100644 --- a/news/search.py +++ b/news/search.py @@ -65,7 +65,6 @@ def search(request): search=vector, ).filter(rank__gte=0.3,live=True).order_by('-date', 'rank') - if ('collection' in request.GET) and request.GET['collection'].strip(): collection_name = request.GET['collection'] types = [] diff --git a/pages/tests.py b/pages/tests.py index 826ce0637..deb6d782e 100644 --- a/pages/tests.py +++ b/pages/tests.py @@ -1,3 +1,4 @@ +import datetime import json from django.test import TestCase, Client @@ -37,7 +38,8 @@ FormHeadings, AllyLogos, K12MainPage, - Assignable) + Assignable, ImpactStory) + from news.models import NewsIndex, PressIndex from books.models import BookIndex from shared.test_utilities import assertPathDoesNotRedirectToTrailingSlash, mock_user_login @@ -207,6 +209,293 @@ def test_can_create_general_page(self): retrieved_page = Page.objects.get(id=general_page.id) self.assertEqual(retrieved_page.title, "General Page") + def test_can_create_supporters_page(self): + supporters_page = Supporters(title='Supporters Page', + banner_heading='Banner heading', + banner_description='Banner description', + funder_groups=json.dumps( + [{"id": "5cf47334-37f6-433c-b695-80936bc7d236", "type": "content", "value": + {"image": None, "funders": [{"id": "647ce8be-eabb-40a2-abb9-136c2bb00e53", "type": "item", "value": {"url": "https://openstax.org", "funder_name": "Musser Foundation"}}, + {"id": "c31a9a7a-f6e2-4b74-929b-5bc39f96568d", "type": "item", "value": {"url": "https://openstax.org", "funder_name": "Mike and Patricia Foundation"}} + ]}}] + ), + disclaimer='This field cannot be left blank', + ) + self.homepage.add_child(instance=supporters_page) + self.assertCanCreateAt(HomePage, Supporters) + + retrieved_page = Page.objects.get(id=supporters_page.id) + self.assertEqual(retrieved_page.title, "Supporters Page") + + def test_can_create_tos_page(self): + tos_page = TermsOfService(title='Terms of Service Page', + intro_heading='Intro heading', + terms_of_service_content='This is the terms of service', + ) + self.homepage.add_child(instance=tos_page) + self.assertCanCreateAt(HomePage, TermsOfService) + + retrieved_page = Page.objects.get(id=tos_page.id) + self.assertEqual(retrieved_page.title, "Terms of Service Page") + + def test_can_create_faq_page(self): + faq_page = FAQ(title='FAQ Page', + intro_heading='Intro heading', + intro_description='This is the FAQ page', + questions=json.dumps( + [{"id": "bc328439-9ad5-4fe7-9adc-1dba59389330", "type": "question", + "value": {"slug": "how-does-openstax-work", + "answer": "
Using OpenStax is simple! Review the textbook online, and if you decide to use it in your class, let us know. To access faculty-only materials, you can create an OpenStax account and request faculty access. Once we manually verify that you’re an instructor, you will have access to all faculty content. Include the textbook URL in your course materials, and from there, students can choose how they want to view the book.
If you’re a student, simply access the web view, download a PDF, or purchase a hard copy via Amazon or your campus. Even students whose professors have not adopted OpenStax are welcome to use OpenStax textbooks.
How does OpenStax work?
"}}, + {"id": "c985605f-cc41-4758-84dc-a5e42098814c", "type": "question", + "value": {"slug": "why-use-openstax-textbooks", + "answer": "The costs of textbooks are rising, and students have difficulty keeping up with the high price of required materials. A large percentage of students show up for the first day of class without the course textbook. Imagine if every student had immediate, unlimited access to the text. How would that help you meet your goals?
Open resources also allow you to use the text in a way that’s best for you and your students. You aren’t bound by copyright or digital rights restrictions, and you can adapt the book as you see fit.
", + "document": None, + "question": "Why should instructors use OpenStax textbooks?
OpenStax believes that learning is a public good and that every learner has the fundamental right to pursue their education in areas that inspire them most. Our goal is to remain 100% free and easily accessible, while improving learning proficiency for every learner. Open education can open doorways to new careers, intellectual pursuits, and the betterment of society.
What might you achieve if you had the right resources at your fingertips? Join us today and help millions of learners experience an affordable and engaging education.
"}}] + ), + reach=json.dumps( + [{"id": "0b1d5fe8-f77a-4eb6-ae6a-1dc58cb08c10", "type": "content", "value": {"cards": [ + {"id": "ce31a4fb-b929-493e-96ce-5ea945c82ddd", "type": "item", + "value": {"icon": None, + "link_href": "https://openstax.org/press/openstax-surpasses-1-billion-textbook-savings-wide-ranging-impact-teaching-learning-and-student-success", + "link_text": "Read more", + "description": "$1.8 billion saved in education costs since 2012"}}]}}] + ), + quote=json.dumps( + [{"id": "49d89977-5433-4b1f-9e0f-ed4982ac4201", "type": "content", + "value": {"image": {"link": "", "image": None}, + "quote": "I believe that knowledge should be free. And that means we shouldn’t be tying up ideas and knowledge in proprietary systems. We should be finding ways to share ideas and knowledge to make the world a better place. – Prof. Richard Baraniuk,
Founder of OpenStax, the C. Sidney Burrus Professor of Electrical and Computer Engineering at Rice University, and Fellow of the American Academy of Arts and Sciences
"}}] + ), + making_a_difference=json.dumps( + [{"id": "c1dc4294-2b44-4320-9d72-2286af863302", "type": "content", + "value": {"heading": "Community Stories", "stories": [ + {"id": "8593338c-752a-491c-8b07-33ef8d42bc10", "type": "item", + "value": {"image": None, + "story_text": "Rahul Kane grew up in India in a family of educators, and upon completing his undergraduate studies, moved to the United States in 2005 to pursue graduate school. Now, he teaches biology at Century College and the University of St. Thomas. Read more about why he prefers open educational resources!", + "linked_story": None, "embedded_video": ""}}, + {"id": "29293e1b-bd9c-4f66-8e7d-d35e279aaee9", "type": "item", + "value": {"image": None, + "story_text": "Megan expects to get her nursing diploma in five years and sees herself as a successful nurse working in a hospital.", + "linked_story": None, "embedded_video": ""}}]}}] + ), + disruption=json.dumps( + [{"id": "81f78c27-d65b-449a-b930-21a3449c11f1", "type": "content", + "value": {"graph": {"image": {"link": "", "image": None}, "image_alt_text": ""}, + "heading": "Positive Disruption", + "description": "The price of textbooks is declining due to open education’s disruption of the college textbook market, removing financial barriers to advanced education, and reducing student debt. According to an economist, “The ‘textbook bubble’ is finally starting to deflate, due to the creative destruction and competition from free/low-cost textbooks from groups like OpenStax” (Mark Perry, AEI, 2019)."}}] + ), + supporter_community=json.dumps( + [{"id": "0d555d88-1c47-483f-bdd7-bd64aa4fae39", "type": "content", + "value": {"image": {"link": "", "image": None}, + "quote": "OpenStax continues to expand to new subject areas, grade levels, and languages to reach more students. Yet, OpenStax is more than free textbooks. With a team of researchers, educators, and learning engineers at Rice University, OpenStax is creating research-based learning tools to help teachers and learners better personalize the education experience. OpenStax needs your partnership to continue its impact.
– Ann Doerr, OpenStax Advisor
", + "heading": "Our Supporter Community", + "link_href": "https://openstax.org/foundation", + "link_text": "View our supporters"}}] + ), + giving=json.dumps( + [{"id": "e5a9a3ad-f8c6-409f-a774-f6c99daf4990", "type": "content", + "value": {"heading": "Students need your help today.", + "link_href": "https://example.com/donate", "link_text": "Give today", + "description": "Together, we can increase educational equity and quality for millions of students worldwide.", + "nonprofit_statement": "As a part of Rice University, a 501(c)(3) nonprofit, gifts to OpenStax are tax deductible to the fullest extent allowed by law. Our tax ID number is 00-111111. Read our latest Annual Report", + "annual_report_link_href": "https://example.com/annual-report", + "annual_report_link_text": "Read our latest OpenStax Brochure"}}] + ) + + ) + self.homepage.add_child(instance=impact_page) + self.assertCanCreateAt(HomePage, Impact) + + retrieved_page = Page.objects.get(id=impact_page.id) + self.assertEqual(retrieved_page.title, "Impact Page") + + story_page = ImpactStory(title='Impact Story Page', + date=datetime.datetime.now(), + heading='Impact Story', + author='Jane Doe', + body=json.dumps([{"id": "ae6f048b-6eb5-42e7-844f-cfcd459f81b5", "type": "heading", + "value": "Impact Story"}]) + ) + impact_page.add_child(instance=story_page) + self.assertCanCreateAt(Impact, ImpactStory) + + retrieved_page = Page.objects.get(id=story_page.id) + self.assertEqual(retrieved_page.title, "Impact Story Page") + + def test_can_create_learning_research_page(self): + research_page = LearningResearchPage(title='Learning Research Page', + mission_body='This is our mission', + research_area_header='Research area header', + research_area_description_mobile='Research area mobile header', + people_header='People header', + publication_header='Publication header' + ) + self.homepage.add_child(instance=research_page) + self.assertCanCreateAt(HomePage, LearningResearchPage) + + retrieved_page = Page.objects.get(id=research_page.id) + self.assertEqual(retrieved_page.title, "Learning Research Page") + + def test_can_create_webinar_page(self): + webinar_page = WebinarPage(title='Webinar Page', + heading='Heading', + ) + self.homepage.add_child(instance=webinar_page) + self.assertCanCreateAt(HomePage, WebinarPage) + + retrieved_page = Page.objects.get(id=webinar_page.id) + self.assertEqual(retrieved_page.title, "Webinar Page") + + def test_can_create_form_headings_page(self): + form_page = FormHeadings(title='Form Headings Page', + adoption_intro_heading='Adoption intro heading', + adoption_intro_description='Adoption intro description', + interest_intro_heading='Interest intro heading', + interest_intro_description='Interest intro description' + ) + self.homepage.add_child(instance=form_page) + self.assertCanCreateAt(HomePage, FormHeadings) + + retrieved_page = Page.objects.get(id=form_page.id) + self.assertEqual(retrieved_page.title, "Form Headings Page") + + def test_can_create_ally_logos_page(self): + ally_page = AllyLogos(title='Ally Logos Page', + heading='Ally logos heading', + description='Alloy logos description', + ally_logos_heading='Ally logos heading', + ally_logos_description='Ally logos description', + ally_logos='', + book_ally_logos_heading='Book ally logo heading', + book_ally_logos_description='Book ally logo description', + book_ally_logos = '', + ) + self.homepage.add_child(instance=ally_page) + self.assertCanCreateAt(HomePage, AllyLogos) + + retrieved_page = Page.objects.get(id=ally_page.id) + self.assertEqual(retrieved_page.title, "Ally Logos Page") + + def test_can_create_assignable_page(self): + assignable_page = Assignable(title='Assignable Page', + subheading='Assignable subheading', + heading_description='Assignable heading description', + add_assignable_cta_header='Add assignable heading', + add_assignable_cta_description='Add assignable description', + add_assignable_cta_button_text='Add assignable', + available_courses_header='Available courses header', + courses_coming_soon_header = 'Courses coming soon header', + ) + self.homepage.add_child(instance=assignable_page) + self.assertCanCreateAt(HomePage, Assignable) + + retrieved_page = Page.objects.get(id=assignable_page.id) + self.assertEqual(retrieved_page.title, "Assignable Page") + class ErrataListTest(WagtailPageTests): diff --git a/snippets/migrations/0029_assignableavailable.py b/snippets/migrations/0029_assignableavailable.py new file mode 100644 index 000000000..e0748d063 --- /dev/null +++ b/snippets/migrations/0029_assignableavailable.py @@ -0,0 +1,32 @@ +# Generated by Django 4.0.8 on 2023-08-22 20:00 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('wagtailimages', '0024_index_image_file_hash'), + ('wagtailcore', '0069_log_entry_jsonfield'), + ('snippets', '0028_webinarcollection'), + ] + + operations = [ + migrations.CreateModel( + name='AssignableAvailable', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('translation_key', models.UUIDField(default=uuid.uuid4, editable=False)), + ('name', models.CharField(blank=True, max_length=255, null=True)), + ('assignable_description', models.TextField(default='')), + ('image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image')), + ('locale', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='wagtailcore.locale')), + ], + options={ + 'abstract': False, + 'unique_together': {('translation_key', 'locale')}, + }, + ), + ] diff --git a/snippets/models.py b/snippets/models.py index d54f004a1..65be50395 100644 --- a/snippets/models.py +++ b/snippets/models.py @@ -431,3 +431,35 @@ def __str__(self): register_snippet(WebinarCollection) + +class AssignableAvailable(TranslatableMixin, models.Model): + name = models.CharField(max_length=255, null=True, blank=True) + assignable_description = models.TextField(default='') + image = models.ForeignKey( + 'wagtailimages.Image', + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name='+' + ) + + def get_assignable_available_image(self): + return build_image_url(self.image) + + assignable_available_image = property(get_assignable_available_image) + + api_fields = ('assignable_description', + 'assignable_available_image') + + panels = [ + FieldPanel('name'), + FieldPanel('assignable_description'), + FieldPanel('image'), + ] + + def __str__(self): + return self.name + + +register_snippet(AssignableAvailable) + diff --git a/snippets/serializers.py b/snippets/serializers.py index cc41c4b60..b02aea7d7 100644 --- a/snippets/serializers.py +++ b/snippets/serializers.py @@ -1,5 +1,5 @@ from .models import Role, Subject, K12Subject, ErrataContent, SubjectCategory, GiveBanner, BlogContentType, \ - BlogCollection, NoWebinarMessage, WebinarCollection + BlogCollection, NoWebinarMessage, WebinarCollection, AssignableAvailable from rest_framework import serializers, generics @@ -92,3 +92,10 @@ class Meta: 'description', 'collection_image') + +class AssignableAvailableSerializer(serializers.ModelSerializer): + class Meta: + model = AssignableAvailable + fields = ('assignable_description', + 'assignable_available_image') + diff --git a/snippets/signals.py b/snippets/signals.py index fa0397d6c..8cee21c63 100644 --- a/snippets/signals.py +++ b/snippets/signals.py @@ -3,7 +3,7 @@ from global_settings.functions import invalidate_cloudfront_caches from snippets.models import Subject, Role, ErrataContent, SubjectCategory, GiveBanner, BlogContentType, BlogCollection, \ - WebinarCollection + WebinarCollection, AssignableAvailable @receiver(post_save, sender=Subject) @@ -44,3 +44,9 @@ def clear_cloudfront_on_blog_collection_save(sender, **kwargs): @receiver(post_save, sender=WebinarCollection) def clear_cloudfront_on_webinar_collection_save(sender, **kwargs): invalidate_cloudfront_caches('snippets/webinarcollection') + + +@receiver(post_save, sender=AssignableAvailable) +def clear_cloudfront_on_assignable_available_save(sender, **kwargs): + invalidate_cloudfront_caches('snippets/assignableavailable') + diff --git a/snippets/tests.py b/snippets/tests.py index f52e12896..4dded5484 100644 --- a/snippets/tests.py +++ b/snippets/tests.py @@ -6,7 +6,9 @@ from django.conf import settings from django.urls import reverse -from snippets.models import Subject, ErrataContent, GiveBanner, BlogContentType, NoWebinarMessage +from snippets.models import Subject, ErrataContent, GiveBanner, BlogContentType, NoWebinarMessage, K12Subject, \ + FacultyResource, StudentResource, Role, SharedContent, NewsSource, SubjectCategory, BlogCollection, AssignableAvailable +import snippets class SnippetsTestCase(TestCase): @@ -39,6 +41,35 @@ def setUp(self): self.no_webinar_message = NoWebinarMessage(no_webinar_message="No webinars currently scheduled. In the meantime, please watch any of our past webinars.") self.no_webinar_message.save() + self.k12subject = K12Subject(name="Test Subject", intro_text='Intro text',subject_link="https://example.com/openstaxk12") + self.k12subject.save() + + self.faculty_resource = FacultyResource(heading="Faculty Resource", description='resource description',unlocked_resource=True) + self.faculty_resource.save() + + self.student_resource = StudentResource(heading="Student Resource", description='resource description', + unlocked_resource=True) + self.student_resource.save() + + self.role = Role(display_name="role display name", salesforce_name='role salesforce name') + self.role.save() + + self.shared_content = SharedContent(title="shared content", heading='shared content heading', content='shared content') + self.shared_content.save() + + self.news_source = NewsSource(name="news source") + self.news_source.save() + + self.subject_category = SubjectCategory(subject_category="subject category", description='subject category description') + self.subject_category.save() + + self.blog_collection = BlogCollection(name="blog collection", description='blog collection description') + self.blog_collection.save() + + self.assignable_available = AssignableAvailable( + assignable_description="Assignable is ...") + self.assignable_available.save() + def test_can_create_subject(self): subject = Subject(name="Science", page_content="Science page content.", seo_title="Science SEO Title", search_description="Science page description.") @@ -75,3 +106,40 @@ def test_can_fetch_all_blog_content_types(self): def test_can_fetch_no_webinar_message(self): response = self.client.get('/apps/cms/api/snippets/nowebinarmessage/?format=json') self.assertIn(b"No webinars currently scheduled", response.content) + + def test_can_fetch_k12_subject(self): + response = self.client.get('/apps/cms/api/snippets/k12subjects/?format=json') + self.assertIn(b"https://example.com/openstaxk12", response.content) + + def test_can_fetch_faculty_resource(self): + faculty_resource = FacultyResource.objects.all()[0] + self.assertEquals(True, faculty_resource.unlocked_resource) + + def test_can_fetch_student_resource(self): + student_resource = StudentResource.objects.all()[0] + self.assertEquals(True, student_resource.unlocked_resource) + + def test_can_fetch_role(self): + response = self.client.get('/apps/cms/api/snippets/roles/?format=json') + self.assertIn(b"role display name", response.content) + + def test_can_fetch_shared_content(self): + shared_content = SharedContent.objects.all()[0] + self.assertEquals('shared content', shared_content.title) + + def test_can_fetch_news_source(self): + news_source = NewsSource.objects.all()[0] + self.assertEquals('news source', news_source.name) + + def test_can_fetch_subject_category(self): + response = self.client.get('/apps/cms/api/snippets/subjectcategory/?format=json') + self.assertIn(b"subject category description", response.content) + + def test_can_fetch_blog_collection(self): + response = self.client.get('/apps/cms/api/snippets/blogcollection/?format=json') + self.assertIn(b"blog collection description", response.content) + + def test_can_fetch_assignable_available(self): + response = self.client.get('/apps/cms/api/snippets/assignableavailable/?format=json') + self.assertIn(b"Assignable is ...", response.content) + diff --git a/snippets/urls.py b/snippets/urls.py index ccece7a52..83b1e3c42 100644 --- a/snippets/urls.py +++ b/snippets/urls.py @@ -12,4 +12,5 @@ router.register('blogcollection', views.BlogCollectionViewSet, basename="BlogCollection") router.register('nowebinarmessage', views.NoWebinarMessageViewSet, basename="NoWebinarMessage") router.register('webinarcollection', views.WebinarCollectionViewSet, basename="WebinarCollection") +router.register('assignableavailable', views.AssignableAvailableViewSet, basename="AssignableAvailable") urlpatterns = router.urls diff --git a/snippets/views.py b/snippets/views.py index bd1d7c018..7f46a0303 100644 --- a/snippets/views.py +++ b/snippets/views.py @@ -1,11 +1,11 @@ from rest_framework import viewsets from .models import Role, Subject, K12Subject, ErrataContent, SubjectCategory, GiveBanner, BlogContentType, \ - BlogCollection, NoWebinarMessage, WebinarCollection + BlogCollection, NoWebinarMessage, WebinarCollection, AssignableAvailable from .serializers import RoleSerializer, SubjectSerializer, K12SubjectSerializer, ErrataContentSerializer, \ SubjectCategorySerializer, \ GiveBannerSerializer, BlogContentTypeSerializer, BlogCollectionSerializer, NoWebinarMessageSerializer, \ - WebinarCollectionSerializer + WebinarCollectionSerializer, AssignableAvailableSerializer from rest_framework import generics, viewsets from django_filters.rest_framework import DjangoFilterBackend @@ -104,6 +104,11 @@ class WebinarCollectionViewSet(viewsets.ModelViewSet): serializer_class = WebinarCollectionSerializer +class AssignableAvailableViewSet(viewsets.ModelViewSet): + queryset = AssignableAvailable.objects.all() + serializer_class = AssignableAvailableSerializer + + def convert_locale(locale): if locale == 'es': return SPANISH_LOCALE_ID diff --git a/wagtailimportexport/functions.py b/wagtailimportexport/functions.py index 0b19bd0ed..2b7871968 100644 --- a/wagtailimportexport/functions.py +++ b/wagtailimportexport/functions.py @@ -243,4 +243,3 @@ def content_type_by_model(model): return None else: return str(content_type[0].pk) -