class AnswerLandingPage(LandingPage): """ Page type for Ask CFPB's landing page. """ content_panels = [ StreamFieldPanel('header') ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(LandingPage.settings_panels, heading='Configuration'), ]) objects = CFGOVPageManager() def get_context(self, request, *args, **kwargs): from ask_cfpb.models import Category, Audience context = super(AnswerLandingPage, self).get_context(request) context['categories'] = Category.objects.all() if self.language == 'en': context['about_us'] = get_reusable_text_snippet( ABOUT_US_SNIPPET_TITLE) context['disclaimer'] = get_reusable_text_snippet( ENGLISH_DISCLAIMER_SNIPPET_TITLE) context['audiences'] = [ {'text': audience.name, 'url': '/ask-cfpb/audience-{}'.format( slugify(audience.name))} for audience in Audience.objects.all().order_by('name')] return context def get_template(self, request): if self.language == 'es': return 'ask-cfpb/landing-page-spanish.html' return 'ask-cfpb/landing-page.html'
class TagResultsPage(RoutablePageMixin, AnswerResultsPage): """A routable page for serving Answers by tag""" objects = CFGOVPageManager() def get_template(self, request): if self.language == 'es': return 'ask-cfpb/answer-tag-spanish-results.html' else: return 'ask-cfpb/answer-search-results.html' @route(r'^$') def tag_base(self, request): raise Http404 @route(r'^(?P<tag>[^/]+)/$') def tag_search(self, request, **kwargs): tag = kwargs.get('tag').replace('_', ' ') self.answers = AnswerPage.objects.filter(language=self.language, search_tags__contains=tag, redirect_to_page=None, live=True) paginator = Paginator(self.answers, 20) page_number = validate_page_number(request, paginator) page = paginator.page(page_number) context = self.get_context(request) context['current_page'] = page_number context['results'] = page context['results_count'] = len(self.answers) context['tag'] = tag context['paginator'] = paginator return TemplateResponse(request, self.get_template(request), context)
class AnswerResultsPage(CFGOVPage): objects = CFGOVPageManager() answers = [] edit_handler = TabbedInterface([ ObjectList(CFGOVPage.content_panels, heading='Content'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) template = 'ask-cfpb/answer-search-results.html' def get_context(self, request, **kwargs): context = super(AnswerResultsPage, self).get_context(request, **kwargs) context.update(**kwargs) paginator = Paginator(self.answers, 20) page_number = validate_page_number(request, paginator) results = paginator.page(page_number) context['current_page'] = page_number context['paginator'] = paginator context['results'] = results context['results_count'] = len(self.answers) context['breadcrumb_items'] = get_ask_breadcrumbs( language=self.language) context['about_us'] = get_standard_text(self.language, 'about_us') context['disclaimer'] = get_standard_text(self.language, 'disclaimer') return context
class AnswerLandingPage(LandingPage): """ Page type for Ask CFPB's landing page. """ content_panels = [ StreamFieldPanel('header') ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(LandingPage.settings_panels, heading='Configuration'), ]) template = 'ask-cfpb/landing-page.html' objects = CFGOVPageManager() def get_portal_cards(self): """Return an array of dictionaries used to populate portal cards.""" portal_cards = [] portal_pages = SublandingPage.objects.filter( portal_topic_id__isnull=False, language=self.language, ).order_by('portal_topic__heading') for portal_page in portal_pages: topic = portal_page.portal_topic # Only include a portal if it has featured answers featured_answers = topic.featured_answers(self.language) if not featured_answers: continue # If the portal page is live, link to it if portal_page.live: url = portal_page.url # Otherwise, link to the topic "see all" page if there is one else: topic_page = topic.portal_search_pages.filter( language=self.language, live=True).first() if topic_page: url = topic_page.url else: continue # pragma: no cover portal_cards.append({ 'topic': topic, 'title': topic.title(self.language), 'url': url, 'featured_answers': featured_answers, }) return portal_cards def get_context(self, request, *args, **kwargs): context = super(AnswerLandingPage, self).get_context(request) context['portal_cards'] = self.get_portal_cards() context['about_us'] = get_standard_text(self.language, 'about_us') context['disclaimer'] = get_standard_text(self.language, 'disclaimer') return context
class RegulationLandingPage(RoutablePageMixin, CFGOVPage): """Landing page for eregs.""" header = StreamField([ ('hero', molecules.Hero()), ], blank=True) content = StreamField([ ('notification', molecules.Notification()), ('full_width_text', RegulationsListingFullWidthText()), ], blank=True) # General content tab content_panels = CFGOVPage.content_panels + [ StreamFieldPanel('header'), StreamFieldPanel('content'), ] # Tab handler interface edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), ObjectList(CFGOVPage.sidefoot_panels, heading='Sidebar'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) objects = CFGOVPageManager() subpage_types = ['regulations3k.RegulationPage', 'RegulationsSearchPage'] template = 'regulations3k/landing-page.html' def get_context(self, request, *args, **kwargs): context = super(CFGOVPage, self).get_context(request, *args, **kwargs) context.update({ 'get_secondary_nav_items': get_secondary_nav_items, }) return context @route(r'^recent-notices-json/$', name='recent_notices') def recent_notices(self, request): fr_api_url = 'https://www.federalregister.gov/api/v1/' fr_documents_url = fr_api_url + 'documents.json' params = { 'fields_list': ['html_url', 'title'], 'per_page': '3', 'order': 'newest', 'conditions[agencies][]': 'consumer-financial-protection-bureau', 'conditions[type][]': 'RULE', 'conditions[cfr][title]': '12', } response = requests.get(fr_documents_url, params=params) if response.status_code != 200: return HttpResponse(status=response.status_code) return JsonResponse(response.json())
class TagResultsPage(RoutablePageMixin, AnswerResultsPage): """A routable page for serving Answers by tag""" objects = CFGOVPageManager() def get_template(self, request): if self.language == 'es': return 'ask-cfpb/answer-tag-spanish-results.html' else: return 'ask-cfpb/answer-search-results.html' @route(r'^$') def tag_base(self, request): raise Http404 @route(r'^(?P<tag>[^/]+)/$') def tag_search(self, request, **kwargs): from ask_cfpb.models import Answer tag_dict = Answer.valid_tags(language=self.language) tag = kwargs.get('tag').replace('_', ' ') if not tag or tag not in tag_dict['valid_tags']: raise Http404 if self.language == 'es': self.answers = [ (SPANISH_ANSWER_SLUG_BASE.format(a.id), a.question_es, Truncator(a.answer_es).words(40, truncate=' ...')) for a in tag_dict['tag_map'][tag] if a.answer_pages.filter(language='es', live=True) ] else: self.answers = [ (ENGLISH_ANSWER_SLUG_BASE.format(a.id), a.question, Truncator(a.answer).words(40, truncate=' ...')) for a in tag_dict['tag_map'][tag] if a.answer_pages.filter(language='en', live=True) ] paginator = Paginator(self.answers, 20) page_number = validate_page_number(request, paginator) page = paginator.page(page_number) context = self.get_context(request) context['current_page'] = page_number context['results'] = page context['results_count'] = len(self.answers) context['tag'] = tag context['paginator'] = paginator return TemplateResponse( request, self.get_template(request), context)
class TagResultsPage(RoutablePageMixin, AnswerResultsPage): """A routable page for serving Answers by tag""" template = 'ask-cfpb/answer-search-results.html' objects = CFGOVPageManager() def get_context(self, request, *args, **kwargs): if self.language != 'en': activate(self.language) else: deactivate_all() context = super( TagResultsPage, self).get_context(request, *args, **kwargs) return context @route(r'^$') def tag_base(self, request): raise Http404 @route(r'^(?P<tag>[^/]+)/$') def tag_search(self, request, **kwargs): """ Return results as a ist of 3-tuples: (url, question, answer-preview). This matches the result form used for /ask-cfpb/search/ queries, which use the same template but deliver results from Elasticsearch. """ tag = kwargs.get('tag').replace('_', ' ') base_query = AnswerPage.objects.filter( language=self.language, redirect_to_page=None, live=True) answer_tuples = [ (page.url, page.question, get_answer_preview(page)) for page in base_query if tag in page.clean_search_tags ] paginator = Paginator(answer_tuples, 20) page_number = validate_page_number(request, paginator) page = paginator.page(page_number) context = self.get_context(request) context['current_page'] = page_number context['results'] = page context['results_count'] = len(answer_tuples) context['tag'] = tag context['paginator'] = paginator return TemplateResponse( request, self.template, context)
class AnswerAudiencePage(SecondaryNavigationJSMixin, CFGOVPage): from ask_cfpb.models import Audience objects = CFGOVPageManager() content = StreamField([ ], null=True) ask_audience = models.ForeignKey( Audience, blank=True, null=True, on_delete=models.PROTECT, related_name='audience_page') content_panels = CFGOVPage.content_panels + [ FieldPanel('ask_audience', Audience), StreamFieldPanel('content'), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) def get_context(self, request, *args, **kwargs): from ask_cfpb.models import Answer context = super(AnswerAudiencePage, self).get_context(request) answers = Answer.objects.filter(audiences__id=self.ask_audience.id) paginator = Paginator(answers, 20) page_number = validate_page_number(request, paginator) page = paginator.page(page_number) context.update({ 'answers': page, 'current_page': page_number, 'paginator': paginator, 'results_count': len(answers), 'get_secondary_nav_items': get_ask_nav_items }) if self.language == 'en': context['about_us'] = get_reusable_text_snippet( ABOUT_US_SNIPPET_TITLE) context['disclaimer'] = get_reusable_text_snippet( ENGLISH_DISCLAIMER_SNIPPET_TITLE) context['breadcrumb_items'] = get_ask_breadcrumbs() return context template = 'ask-cfpb/audience-page.html'
class RegulationLandingPage(CFGOVPage): """Landing page for eregs.""" objects = CFGOVPageManager() subpage_types = ['regulations3k.RegulationPage', 'RegulationsSearchPage'] regs = Part.objects.order_by('part_number') def get_context(self, request, *args, **kwargs): context = super(CFGOVPage, self).get_context(request, *args, **kwargs) context.update({ 'get_secondary_nav_items': get_reg_nav_items, 'regs': self.regs, }) return context def get_template(self, request): return 'regulations3k/base.html'
class AnswerResultsPage(SecondaryNavigationJSMixin, CFGOVPage): objects = CFGOVPageManager() answers = [] content = StreamField([ ], null=True) content_panels = CFGOVPage.content_panels + [ StreamFieldPanel('content'), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) def get_context(self, request, **kwargs): context = super( AnswerResultsPage, self).get_context(request, **kwargs) context.update(**kwargs) paginator = Paginator(self.answers, 20) page_number = validate_page_number(request, paginator) page = paginator.page(page_number) context['current_page'] = page_number context['paginator'] = paginator context['results'] = page context['results_count'] = len(self.answers) context['get_secondary_nav_items'] = get_ask_nav_items if self.language == 'en': context['about_us'] = get_reusable_text_snippet( ABOUT_US_SNIPPET_TITLE) context['disclaimer'] = get_reusable_text_snippet( ENGLISH_DISCLAIMER_SNIPPET_TITLE) context['breadcrumb_items'] = get_ask_breadcrumbs() return context def get_template(self, request): if self.language == 'en': return 'ask-cfpb/answer-search-results.html' elif self.language == 'es': return 'ask-cfpb/answer-search-spanish-results.html'
class PayingForCollegePage(CFGOVPage): """A base class for our suite of PFC pages.""" header = StreamField([ ('text_introduction', molecules.TextIntroduction()), ('featured_content', organisms.FeaturedContent()), ], blank=True) content_panels = CFGOVPage.content_panels + [ StreamFieldPanel('header'), StreamFieldPanel('content'), ] # Tab handler interface edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), ObjectList(CFGOVPage.sidefoot_panels, heading='Sidebar'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) objects = CFGOVPageManager() class Meta: abstract = True
class CollegeCostsPage(PayingForCollegePage): """Breaking down financial aid and loans for prospective students.""" header = StreamField([ ('hero', molecules.Hero()), ('text_introduction', molecules.TextIntroduction()), ('featured_content', organisms.FeaturedContent()), ], blank=True) content_panels = CFGOVPage.content_panels + [ StreamFieldPanel('header'), StreamFieldPanel('content'), ] # Tab handler interface edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), # ObjectList(, heading='School and living situation'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) objects = CFGOVPageManager() content = StreamField(PayingForCollegeContent, blank=True) template = 'paying-for-college/college-costs.html'
class AnswerPage(CFGOVPage): """Page type for Ask CFPB answers.""" from ask_cfpb.models import Answer last_edited = models.DateField( blank=True, null=True, help_text="Change the date to today if you make a significant change.") question = models.TextField(blank=True) statement = models.TextField( blank=True, help_text=( "(Optional) Use this field to rephrase the question title as " "a statement. Use only if this answer has been chosen to appear " "on a money topic portal (e.g. /consumer-tools/debt-collection).")) short_answer = RichTextField(blank=True, features=['link', 'document-link'], help_text='Optional answer intro') answer_content = StreamField(ask_blocks.AskAnswerContent(), blank=True, verbose_name='Answer') answer_base = models.ForeignKey(Answer, blank=True, null=True, related_name='answer_pages', on_delete=models.SET_NULL) redirect_to_page = models.ForeignKey( 'self', blank=True, null=True, on_delete=models.SET_NULL, related_name='redirect_to_pages', help_text="Choose another AnswerPage to redirect this page to") featured = models.BooleanField( default=False, help_text=("Check to make this one of two featured answers " "on the landing page.")) featured_rank = models.IntegerField(blank=True, null=True) category = models.ManyToManyField( 'Category', blank=True, help_text=("Categorize this answer. " "Avoid putting into more than one category.")) search_tags = models.CharField( max_length=1000, blank=True, help_text="Search words or phrases, separated by commas") related_resource = models.ForeignKey(RelatedResource, blank=True, null=True, on_delete=models.SET_NULL) related_questions = ParentalManyToManyField( 'self', symmetrical=False, blank=True, related_name='related_question', help_text='Maximum of 3 related questions') portal_topic = ParentalManyToManyField( PortalTopic, blank=True, help_text='Limit to 1 portal topic if possible') primary_portal_topic = ParentalKey( PortalTopic, blank=True, null=True, on_delete=models.SET_NULL, related_name='primary_portal_topic', help_text=("Use only if assigning more than one portal topic, " "to control which topic is used as a breadcrumb.")) portal_category = ParentalManyToManyField(PortalCategory, blank=True) user_feedback = StreamField([ ('feedback', v1_blocks.Feedback()), ], blank=True) content_panels = CFGOVPage.content_panels + [ MultiFieldPanel([ FieldPanel('last_edited'), FieldPanel('question'), FieldPanel('statement'), FieldPanel('short_answer') ], heading="Page content", classname="collapsible"), StreamFieldPanel('answer_content'), MultiFieldPanel([ SnippetChooserPanel('related_resource'), AutocompletePanel('related_questions', page_type='ask_cfpb.AnswerPage', is_single=False) ], heading="Related resources", classname="collapsible"), MultiFieldPanel([ FieldPanel('portal_topic', widget=forms.CheckboxSelectMultiple), FieldPanel('primary_portal_topic'), FieldPanel('portal_category', widget=forms.CheckboxSelectMultiple) ], heading="Portal tags", classname="collapsible"), MultiFieldPanel([FieldPanel('featured')], heading="Featured answer on Ask landing page", classname="collapsible"), MultiFieldPanel([ AutocompletePanel('redirect_to_page', page_type='ask_cfpb.AnswerPage') ], heading="Redirect to another answer", classname="collapsible"), MultiFieldPanel([StreamFieldPanel('user_feedback')], heading="User feedback", classname="collapsible collapsed"), ] sidebar = StreamField([ ('call_to_action', molecules.CallToAction()), ('related_links', molecules.RelatedLinks()), ('related_metadata', molecules.RelatedMetadata()), ('email_signup', organisms.EmailSignUp()), ('sidebar_contact', organisms.SidebarContactInfo()), ('rss_feed', molecules.RSSFeed()), ('social_media', molecules.SocialMedia()), ('reusable_text', v1_blocks.ReusableTextChooserBlock(ReusableText)), ], blank=True) sidebar_panels = [ StreamFieldPanel('sidebar'), ] search_fields = Page.search_fields + [ index.SearchField('answer_content'), index.SearchField('short_answer') ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(sidebar_panels, heading='Sidebar'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) template = 'ask-cfpb/answer-page.html' objects = CFGOVPageManager() def get_sibling_url(self): if self.answer_base: if self.language == 'es': sibling = self.answer_base.english_page else: sibling = self.answer_base.spanish_page if sibling and sibling.live and not sibling.redirect_to_page: return sibling.url def get_context(self, request, *args, **kwargs): portal_topic = self.primary_portal_topic or self.portal_topic.first() context = super(AnswerPage, self).get_context(request) context['related_questions'] = self.related_questions.all() context['description'] = (self.short_answer if self.short_answer else Truncator(self.answer_content).words( 40, truncate=' ...')) context['last_edited'] = self.last_edited context['portal_page'] = get_portal_or_portal_search_page( portal_topic, language=self.language) context['breadcrumb_items'] = get_ask_breadcrumbs( language=self.language, portal_topic=portal_topic, ) context['about_us'] = get_standard_text(self.language, 'about_us') context['disclaimer'] = get_standard_text(self.language, 'disclaimer') context['sibling_url'] = self.get_sibling_url() return context def __str__(self): if self.answer_base: return '{}: {}'.format(self.answer_base.id, self.title) else: return self.title @property def clean_search_tags(self): return [tag.strip() for tag in self.search_tags.split(',')] @property def status_string(self): if self.redirect_to_page: if not self.live: return ("redirected but not live") else: return ("redirected") else: return super(AnswerPage, self).status_string # Returns an image for the page's meta Open Graph tag @property def meta_image(self): if self.social_sharing_image: return self.social_sharing_image if not self.category.exists(): return None return self.category.first().category_image # Overrides the default of page.id for comparing against split testing # clusters. See: core.feature_flags.in_split_testing_cluster @property def split_test_id(self): return self.answer_base.id
class ArticlePage(CFGOVPage): """ General article page type. """ category = models.CharField( choices=[ ('basics', 'Basics'), ('common_issues', 'Common issues'), ('howto', 'How to'), ('know_your_rights', 'Know your rights'), ], max_length=255, ) heading = models.CharField( max_length=255, blank=False, ) intro = models.TextField(blank=False) inset_heading = models.CharField(max_length=255, blank=True, verbose_name="Heading") sections = StreamField([ ('section', blocks.StructBlock([ ('heading', blocks.CharBlock(max_length=255, required=True, label='Section heading')), ('summary', blocks.TextBlock(required=False, blank=True, label='Section summary')), ('link_text', blocks.CharBlock(required=False, blank=True, label="Section link text")), ('url', blocks.CharBlock( required=False, blank=True, label='Section link URL', max_length=255, )), ('subsections', blocks.ListBlock( blocks.StructBlock([ ('heading', blocks.CharBlock(max_length=255, required=False, blank=True, label='Subsection heading')), ('summary', blocks.TextBlock(required=False, blank=True, label='Subsection summary')), ('link_text', blocks.CharBlock(required=True, label='Subsection link text')), ('url', blocks.CharBlock(required=True, label='Subsection link URL')) ]))) ])) ]) content_panels = CFGOVPage.content_panels + [ MultiFieldPanel([ FieldPanel('category'), FieldPanel('heading'), FieldPanel('intro') ], heading="Heading", classname="collapsible"), MultiFieldPanel([ FieldPanel('inset_heading'), InlinePanel('article_links', label='Inset link', max_num=2), ], heading="Inset links", classname="collapsible"), StreamFieldPanel('sections'), ] sidebar = StreamField([ ('call_to_action', molecules.CallToAction()), ('related_links', molecules.RelatedLinks()), ('related_metadata', molecules.RelatedMetadata()), ('email_signup', organisms.EmailSignUp()), ('reusable_text', v1_blocks.ReusableTextChooserBlock(ReusableText)), ], blank=True) sidebar_panels = [ StreamFieldPanel('sidebar'), ] search_fields = Page.search_fields + [ index.SearchField('title'), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(sidebar_panels, heading='Sidebar'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) template = 'ask-cfpb/article-page.html' objects = CFGOVPageManager() def get_context(self, request, *args, **kwargs): context = super(ArticlePage, self).get_context(request) context['about_us'] = get_standard_text(self.language, 'about_us') return context def __str__(self): return self.title
class PortalSearchPage(RoutablePageMixin, SecondaryNavigationJSMixin, CFGOVPage): """ A routable page type for Ask CFPB portal search ("see-all") pages. """ objects = CFGOVPageManager() portal_topic = models.ForeignKey(PortalTopic, blank=True, null=True, related_name='portal_search_pages', on_delete=models.SET_NULL) portal_category = None query_base = None glossary_terms = None overview = models.TextField(blank=True) content_panels = CFGOVPage.content_panels + [ FieldPanel('portal_topic'), FieldPanel('overview'), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) @property def category_map(self): """ Return an ordered dictionary of translated-slug:object pairs. We use this custom sequence for categories in the navigation sidebar, controlled by the 'display_order' field of portal categories: - Basics - Key terms - Common issues - Know your rights - How-to guides """ categories = PortalCategory.objects.all() sorted_mapping = OrderedDict() for category in categories: sorted_mapping.update( {slugify(category.title(self.language)): category}) return sorted_mapping def results_message(self, count, heading, search_term): if search_term: _for_term = '{} "{}"'.format(_('for'), search_term) else: _for_term = '' if count == 1: _showing = _('Showing ') # trailing space triggers singular es _results = _('result') else: _showing = _('Showing') _results = _('results') if self.portal_category and search_term: return format_html( '{} {} {} {} {} {}' '<span class="results-link"><a href="../?search_term={}">' '{} {}</a></span>', _showing, count, _results, _for_term, _('within'), heading.lower(), search_term, _('See all results within'), self.portal_topic.title(self.language).lower()) elif self.portal_category: return '{} {} {} {} {}'.format(_showing, count, _results, _('within'), heading.lower()) return '{} {} {} {} {} {}'.format(_showing, count, _results, _for_term, _('within'), heading.lower()) def get_heading(self): if self.portal_category: return self.portal_category.title(self.language) else: return self.portal_topic.title(self.language) def get_context(self, request, *args, **kwargs): if self.language != 'en': activate(self.language) else: deactivate_all() return super(PortalSearchPage, self).get_context(request, *args, **kwargs) def get_nav_items(self, request, page): """Return sorted nav items for sidebar.""" sorted_categories = [{ 'title': category.title(self.language), 'url': "{}{}/".format(page.url, slug), 'active': (False if not page.portal_category else category.title( self.language) == page.portal_category.title(self.language)) } for slug, category in self.category_map.items()] return [{ 'title': page.portal_topic.title(self.language), 'url': page.url, 'active': False if page.portal_category else True, 'expanded': True, 'children': sorted_categories }], True def get_results(self, request): context = self.get_context(request) search_term = request.GET.get('search_term', '').strip() if not search_term or len(unquote(search_term)) == 1: results = self.query_base else: search = AskSearch(search_term=search_term, query_base=self.query_base) results = search.queryset if results.count() == 0: # No results, so let's try to suggest a better query search.suggest(request=request) results = search.queryset search_term = search.search_term search_message = self.results_message(results.count(), self.get_heading(), search_term) paginator = Paginator(results, 10) page_number = validate_page_number(request, paginator) context.update({ 'search_term': search_term, 'results_message': search_message, 'pages': paginator.page(page_number), 'paginator': paginator, 'current_page': page_number, 'get_secondary_nav_items': self.get_nav_items, }) return TemplateResponse(request, 'ask-cfpb/see-all.html', context) def get_glossary_terms(self): if self.language == 'es': terms = self.portal_topic.glossary_terms.order_by('name_es') else: terms = self.portal_topic.glossary_terms.order_by('name_en') for term in terms: if term.name(self.language) and term.definition(self.language): yield term @route(r'^$') def portal_topic_page(self, request): self.query_base = SearchQuerySet().filter( portal_topics=self.portal_topic.heading, language=self.language) self.portal_category = None return self.get_results(request) @route(r'^(?P<category>[^/]+)/$') def portal_category_page(self, request, **kwargs): category_slug = kwargs.get('category') if category_slug not in self.category_map: raise Http404 self.portal_category = self.category_map.get(category_slug) self.title = "{} {}".format( self.portal_topic.title(self.language), self.portal_category.title(self.language).lower()) if self.portal_category.heading == 'Key terms': self.glossary_terms = self.get_glossary_terms() context = self.get_context(request) context.update({'get_secondary_nav_items': self.get_nav_items}) return TemplateResponse(request, 'ask-cfpb/see-all.html', context) self.query_base = SearchQuerySet().filter( portal_topics=self.portal_topic.heading, language=self.language, portal_categories=self.portal_category.heading) return self.get_results(request)
class AnswerPage(CFGOVPage): """ Page type for Ask CFPB answers. """ from ask_cfpb.models import Answer last_edited = models.DateField( blank=True, null=True, help_text="Change the date to today if you make a significant change.") question = models.TextField(blank=True) statement = models.TextField( blank=True, help_text=( "(Optional) Use this field to rephrase the question title as " "a statement. Use only if this answer has been chosen to appear " "on a money topic portal (e.g. /consumer-tools/debt-collection).")) short_answer = RichTextField(blank=True, help_text='Optional answer intro') answer = RichTextField( blank=True, features=[ 'bold', 'italic', 'h2', 'h3', 'h4', 'link', 'ol', 'ul', 'document-link', 'image', 'embed', 'ask-tips', 'edit-html' ], help_text=( "Do not use H2 or H3 to style text. Only use the HTML Editor " "for troubleshooting. To style tips, warnings and notes, " "select the content that will go inside the rule lines " "(so, title + paragraph) and click the Pencil button " "to style it. Re-select the content and click the button " "again to unstyle the tip.")) answer_base = models.ForeignKey(Answer, blank=True, null=True, related_name='answer_pages', on_delete=models.SET_NULL) redirect_to = models.ForeignKey( Answer, blank=True, null=True, on_delete=models.SET_NULL, related_name='redirected_pages', help_text="Choose another Answer to redirect this page to") redirect_to_page = models.ForeignKey( 'self', blank=True, null=True, on_delete=models.SET_NULL, related_name='redirect_to_pages', help_text="Choose another AnswerPage to redirect this page to") featured = models.BooleanField( default=False, help_text=("Check to make this one of two featured answers " "on the landing page.")) featured_rank = models.IntegerField(blank=True, null=True) category = models.ManyToManyField( 'Category', blank=True, help_text=("Categorize this answer. " "Avoid putting into more than one category.")) subcategory = models.ManyToManyField( 'SubCategory', blank=True, help_text=("Choose only subcategories that belong " "to one of the categories checked above.")) search_tags = models.CharField( max_length=1000, blank=True, help_text="Search words or phrases, separated by commas") related_resource = models.ForeignKey(RelatedResource, blank=True, null=True, on_delete=models.SET_NULL) related_questions = ParentalManyToManyField( 'self', symmetrical=False, blank=True, related_name='related_question', help_text='Maximum of 3 related questions') answer_id = models.IntegerField(default=0) portal_topic = ParentalManyToManyField( PortalTopic, blank=True, help_text='Limit to 1 portal topic if possible') primary_portal_topic = ParentalKey( PortalTopic, blank=True, null=True, on_delete=models.SET_NULL, related_name='primary_portal_topic', help_text=("Use only if assigning more than one portal topic, " "to control which topic is used as a breadcrumb.")) portal_category = ParentalManyToManyField(PortalCategory, blank=True) user_feedback = StreamField([ ('feedback', v1_blocks.Feedback()), ], blank=True) content_panels = CFGOVPage.content_panels + [ MultiFieldPanel([ FieldPanel('last_edited'), FieldPanel('question'), FieldPanel('statement'), FieldPanel('short_answer'), FieldPanel('answer') ], heading="Page content", classname="collapsible"), MultiFieldPanel([ SnippetChooserPanel('related_resource'), AutocompletePanel('related_questions', page_type='ask_cfpb.AnswerPage', is_single=False) ], heading="Related resources", classname="collapsible"), MultiFieldPanel([ FieldPanel('portal_topic', widget=forms.CheckboxSelectMultiple), FieldPanel('primary_portal_topic'), FieldPanel('portal_category', widget=forms.CheckboxSelectMultiple) ], heading="Portal tags", classname="collapsible"), MultiFieldPanel([ AutocompletePanel('redirect_to_page', page_type='ask_cfpb.AnswerPage') ], heading="Redirect to another answer", classname="collapsible"), MultiFieldPanel([StreamFieldPanel('user_feedback')], heading="User feedback", classname="collapsible collapsed"), ] sidebar = StreamField([ ('call_to_action', molecules.CallToAction()), ('related_links', molecules.RelatedLinks()), ('related_metadata', molecules.RelatedMetadata()), ('email_signup', organisms.EmailSignUp()), ('sidebar_contact', organisms.SidebarContactInfo()), ('rss_feed', molecules.RSSFeed()), ('social_media', molecules.SocialMedia()), ('reusable_text', v1_blocks.ReusableTextChooserBlock(ReusableText)), ], blank=True) sidebar_panels = [ StreamFieldPanel('sidebar'), ] search_fields = Page.search_fields + [ index.SearchField('answer'), index.SearchField('short_answer') ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(sidebar_panels, heading='Sidebar (English only)'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) objects = CFGOVPageManager() def get_context(self, request, *args, **kwargs): context = super(AnswerPage, self).get_context(request) context['related_questions'] = self.related_questions.all() context['description'] = self.short_answer if self.short_answer \ else Truncator(self.answer).words(40, truncate=' ...') context['answer_id'] = self.answer_base.id if self.language == 'es': context['search_tags'] = self.clean_search_tags context['tweet_text'] = Truncator(self.question).chars( 100, truncate=' ...') context['disclaimer'] = get_reusable_text_snippet( SPANISH_DISCLAIMER_SNIPPET_TITLE) context['category'] = self.category.first() elif self.language == 'en': context['about_us'] = get_reusable_text_snippet( ABOUT_US_SNIPPET_TITLE) context['disclaimer'] = get_reusable_text_snippet( ENGLISH_DISCLAIMER_SNIPPET_TITLE) context['last_edited'] = self.last_edited # breadcrumbs and/or category should reflect # the referrer if it is a consumer tools portal or # ask category page context['category'], context['breadcrumb_items'] = \ get_question_referrer_data( request, self.category.all()) subcategories = [] for subcat in self.subcategory.all(): if subcat.parent == context['category']: subcategories.append(subcat) for related in subcat.related_subcategories.all(): if related.parent == context['category']: subcategories.append(related) context['subcategories'] = set(subcategories) return context def get_template(self, request): printable = request.GET.get('print', False) if self.language == 'es': if printable == 'true': return 'ask-cfpb/answer-page-spanish-printable.html' return 'ask-cfpb/answer-page-spanish.html' return 'ask-cfpb/answer-page.html' def __str__(self): if self.answer_base: return '{}: {}'.format(self.answer_base.id, self.title) else: return self.title @property def clean_search_tags(self): return [tag.strip() for tag in self.search_tags.split(',')] @property def status_string(self): if self.redirect_to_page: if not self.live: return ("redirected but not live") else: return ("redirected") else: return super(AnswerPage, self).status_string # Returns an image for the page's meta Open Graph tag @property def meta_image(self): if self.social_sharing_image: return self.social_sharing_image if not self.category.exists(): return None return self.category.first().category_image # Overrides the default of page.id for comparing against split testing # clusters. See: core.feature_flags.in_split_testing_cluster @property def split_test_id(self): return self.answer_base.id
class ActivityPage(CFGOVPage): """ A model for the Activity Detail page. """ # Allow Activity pages to exist under the ActivityIndexPage or the Trash parent_page_types = [ActivityIndexPage, HomePage] subpage_types = [] objects = CFGOVPageManager() date = models.DateField('Updated', default=timezone.now) summary = models.TextField('Summary', blank=False) big_idea = RichTextField('Big idea', blank=False) essential_questions = RichTextField('Essential questions', blank=False) objectives = RichTextField('Objectives', blank=False) what_students_will_do = RichTextField('What students will do', blank=False) # noqa: E501 activity_file = models.ForeignKey('wagtaildocs.Document', null=True, blank=False, on_delete=models.SET_NULL, related_name='+', verbose_name='Teacher guide') # TODO: to figure out how to use Document choosers on ManyToMany fields handout_file = models.ForeignKey('wagtaildocs.Document', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Student file 1') handout_file_2 = models.ForeignKey('wagtaildocs.Document', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Student file 2') handout_file_3 = models.ForeignKey('wagtaildocs.Document', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Student file 3') building_block = ParentalManyToManyField( 'teachers_digital_platform.ActivityBuildingBlock', blank=False) # noqa: E501 school_subject = ParentalManyToManyField( 'teachers_digital_platform.ActivitySchoolSubject', blank=False) # noqa: E501 topic = ParentalTreeManyToManyField( 'teachers_digital_platform.ActivityTopic', blank=False) # noqa: E501 # Audience grade_level = ParentalManyToManyField( 'teachers_digital_platform.ActivityGradeLevel', blank=False) # noqa: E501 age_range = ParentalManyToManyField( 'teachers_digital_platform.ActivityAgeRange', blank=False) # noqa: E501 student_characteristics = ParentalManyToManyField( 'teachers_digital_platform.ActivityStudentCharacteristics', blank=True) # noqa: E501 # Activity Characteristics activity_type = ParentalManyToManyField( 'teachers_digital_platform.ActivityType', blank=False) # noqa: E501 teaching_strategy = ParentalManyToManyField( 'teachers_digital_platform.ActivityTeachingStrategy', blank=False) # noqa: E501 blooms_taxonomy_level = ParentalManyToManyField( 'teachers_digital_platform.ActivityBloomsTaxonomyLevel', blank=False) # noqa: E501 activity_duration = models.ForeignKey( ActivityDuration, blank=False, on_delete=models.PROTECT) # noqa: E501 # Standards taught jump_start_coalition = ParentalManyToManyField( 'teachers_digital_platform.ActivityJumpStartCoalition', blank=True, verbose_name='Jump$tart Coalition', ) council_for_economic_education = ParentalManyToManyField( 'teachers_digital_platform.ActivityCouncilForEconEd', blank=True, verbose_name='Council for Economic Education', ) content_panels = CFGOVPage.content_panels + [ FieldPanel('date'), FieldPanel('summary'), FieldPanel('big_idea'), FieldPanel('essential_questions'), FieldPanel('objectives'), FieldPanel('what_students_will_do'), MultiFieldPanel( [ DocumentChooserPanel('activity_file'), DocumentChooserPanel('handout_file'), DocumentChooserPanel('handout_file_2'), DocumentChooserPanel('handout_file_3'), ], heading="Download activity", ), FieldPanel('building_block', widget=forms.CheckboxSelectMultiple), FieldPanel('school_subject', widget=forms.CheckboxSelectMultiple), FieldPanel('topic', widget=forms.CheckboxSelectMultiple), MultiFieldPanel( [ FieldPanel('grade_level', widget=forms.CheckboxSelectMultiple), # noqa: E501 FieldPanel('age_range', widget=forms.CheckboxSelectMultiple), FieldPanel('student_characteristics', widget=forms.CheckboxSelectMultiple), # noqa: E501 ], heading="Audience", ), MultiFieldPanel( [ FieldPanel('activity_type', widget=forms.CheckboxSelectMultiple), # noqa: E501 FieldPanel('teaching_strategy', widget=forms.CheckboxSelectMultiple), # noqa: E501 FieldPanel('blooms_taxonomy_level', widget=forms.CheckboxSelectMultiple), # noqa: E501 FieldPanel('activity_duration'), ], heading="Activity characteristics", ), MultiFieldPanel( [ FieldPanel('council_for_economic_education', widget=forms.CheckboxSelectMultiple), # noqa: E501 FieldPanel('jump_start_coalition', widget=forms.CheckboxSelectMultiple), # noqa: E501 ], heading="National standards", ), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), ObjectList(CFGOVPage.sidefoot_panels, heading='Sidebar/Footer'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) # admin use only search_fields = Page.search_fields + [ index.SearchField('summary'), index.SearchField('big_idea'), index.SearchField('essential_questions'), index.SearchField('objectives'), index.SearchField('what_students_will_do'), index.FilterField('date'), index.FilterField('building_block'), index.FilterField('school_subject'), index.FilterField('topic'), index.FilterField('grade_level'), index.FilterField('age_range'), index.FilterField('student_characteristics'), index.FilterField('activity_type'), index.FilterField('teaching_strategy'), index.FilterField('blooms_taxonomy_level'), index.FilterField('activity_duration'), index.FilterField('jump_start_coalition'), index.FilterField('council_for_economic_education'), ] def get_topics_list(self, parent=None): """ Get a hierarchical list of this activity's topics. parent: ActivityTopic """ if parent: descendants = set(parent.get_descendants()) & set( self.topic.all()) # noqa: E501 children = parent.get_children() children_list = [] # If this parent has descendants in self.topic, add its children. if descendants: for child in children: if set(child.get_descendants()) & set(self.topic.all()): children_list.append(self.get_topics_list(child)) elif child in self.topic.all(): children_list.append(child.title) if children_list: return parent.title + " (" + ', '.join( children_list) + ")" # noqa: E501 # Otherwise, just add the parent. else: return parent.title else: # Build root list of topics and recurse their children. topic_list = [] topic_ids = [topic.id for topic in self.topic.all()] ancestors = ActivityTopic.objects.filter( id__in=topic_ids).get_ancestors(True) # noqa: E501 roots = ActivityTopic.objects.filter(parent=None) & ancestors for root_topic in roots: topic_list.append(self.get_topics_list(root_topic)) if topic_list: return ', '.join(topic_list) else: return '' class Meta: verbose_name = "TDP Activity page"
class ActivityIndexPage(CFGOVPage): """ A model for the Activity Search page. """ subpage_types = ['teachers_digital_platform.ActivityPage'] objects = CFGOVPageManager() header = StreamField([ ('text_introduction', molecules.TextIntroduction()), ], blank=True) results = {} content_panels = CFGOVPage.content_panels + [ StreamFieldPanel('header'), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), ObjectList(CFGOVPage.sidefoot_panels, heading='Sidebar/Footer'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) @classmethod def can_create_at(cls, parent): # You can only create one of these! return super(ActivityIndexPage, cls).can_create_at(parent) \ and not cls.objects.exists() def get_template(self, request): template = 'teachers_digital_platform/activity_index_page.html' if 'partial' in request.GET: template = 'teachers_digital_platform/activity_search_facets_and_results.html' # noqa: E501 return template def get_context(self, request, *args, **kwargs): facet_map = ( ('building_block', (ActivityBuildingBlock, False, 10)), ('school_subject', (ActivitySchoolSubject, False, 25)), ('topic', (ActivityTopic, True, 25)), ('grade_level', (ActivityGradeLevel, False, 10)), ('age_range', (ActivityAgeRange, False, 10)), ('student_characteristics', (ActivityStudentCharacteristics, False, 10)), # noqa: E501 ('activity_type', (ActivityType, False, 10)), ('teaching_strategy', (ActivityTeachingStrategy, False, 25)), ('blooms_taxonomy_level', (ActivityBloomsTaxonomyLevel, False, 25)), # noqa: E501 ('activity_duration', (ActivityDuration, False, 10)), ('jump_start_coalition', (ActivityJumpStartCoalition, False, 25)), ('council_for_economic_education', (ActivityCouncilForEconEd, False, 25)), # noqa: E501 ) search_query = request.GET.get('q', '') # haystack cleans this string sqs = SearchQuerySet().models(ActivityPage).filter(live=True) total_activities = sqs.count() # Load selected facets selected_facets = {} facet_queries = {} for facet, facet_config in facet_map: sqs = sqs.facet(str(facet), size=facet_config[2]) if facet in request.GET and request.GET.get(facet): selected_facets[facet] = [ int(value) for value in request.GET.getlist(facet) if value.isdigit() ] facet_queries[facet] = facet + '_exact:' + ( " OR " + facet + "_exact:").join( [str(value) for value in selected_facets[facet]]) payload = { 'search_query': search_query, 'results': [], 'total_results': 0, 'total_activities': total_activities, 'selected_facets': selected_facets, 'facet_queries': facet_queries, 'all_facets': {}, } # Apply search query if it exists, but don't apply facets if search_query: sqs = sqs.filter(content=search_query).order_by( '-_score', '-date') # noqa: E501 else: sqs = sqs.order_by('-date') # Get all facets and their counts facet_counts = sqs.facet_counts() all_facets = self.get_all_facets(facet_map, sqs, facet_counts, facet_queries, selected_facets) # noqa: E501 # List all facet blocks that need to be expanded always_expanded = {'building_block', 'topic', 'school_subject'} conditionally_expanded = { facet_name for facet_name, facet_items in all_facets.items() if any(facet['selected'] is True for facet in facet_items) } expanded_facets = always_expanded.union(set(conditionally_expanded)) payload.update({ 'facet_counts': facet_counts, 'all_facets': all_facets, 'expanded_facets': expanded_facets, }) # Apply all the active facet values to our search results for facet_narrow_query in facet_queries.values(): sqs = sqs.narrow(facet_narrow_query) results = [activity.object for activity in sqs] total_results = sqs.count() payload.update({ 'results': results, 'total_results': total_results, }) self.results = payload results_per_page = validate_results_per_page(request) paginator = Paginator(payload['results'], results_per_page) current_page = validate_page_number(request, paginator) paginated_page = paginator.page(current_page) context = super(ActivityIndexPage, self).get_context(request) context.update({ 'facet_counts': facet_counts, 'facets': all_facets, 'activities': paginated_page, 'total_results': total_results, 'results_per_page': results_per_page, 'current_page': current_page, 'paginator': paginator, 'show_filters': bool(facet_queries), }) return context def get_all_facets(self, facet_map, sqs, facet_counts, facet_queries, selected_facets): # noqa: E501 all_facets = {} if 'fields' in facet_counts: for facet, facet_config in facet_map: class_object, is_nested, max_facet_count = facet_config all_facets_sqs = sqs other_facet_queries = [ facet_query for facet_query_name, facet_query in facet_queries.items() # noqa: E501 if facet != facet_query_name ] for other_facet_query in other_facet_queries: all_facets_sqs = all_facets_sqs.narrow( str(other_facet_query)) # noqa: E501 narrowed_facet_counts = all_facets_sqs.facet_counts() if 'fields' in narrowed_facet_counts and facet in narrowed_facet_counts[ 'fields']: # noqa: E501 narrowed_facets = [ value[0] for value in narrowed_facet_counts['fields'][facet] ] # noqa: E501 narrowed_selected_facets = selected_facets[ facet] if facet in selected_facets else [ ] # noqa: E501 if is_nested: all_facets[facet] = self.get_nested_facets( class_object, narrowed_facets, narrowed_selected_facets) else: all_facets[facet] = self.get_flat_facets( class_object, narrowed_facets, narrowed_selected_facets) return all_facets def get_flat_facets(self, class_object, narrowed_facets, selected_facets): final_facets = [{ 'selected': result['id'] in selected_facets, 'id': result['id'], 'title': result['title'], } for result in class_object.objects.filter( pk__in=narrowed_facets).values('id', 'title')] # noqa: E501 return final_facets def get_nested_facets(self, class_object, narrowed_facets, selected_facets, parent=None): # noqa: E501 if not parent: flat_final_facets = [{ 'selected': result['id'] in selected_facets, 'id': result['id'], 'title': result['title'], 'parent': result['parent'], } for result in class_object.objects.filter( pk__in=narrowed_facets).get_ancestors(True).values( 'id', 'title', 'parent')] # noqa: E501 final_facets = [] root_facets = [ root_facet for root_facet in flat_final_facets if root_facet['parent'] == None ] # noqa: E501 for root_facet in root_facets: children_list = self.get_nested_facets( class_object, narrowed_facets, selected_facets, root_facet['id']) # noqa: E501 child_selected = any(child['selected'] is True or child['child_selected'] is True for child in children_list # noqa: E501 ) final_facets.append({ 'selected': root_facet['selected'], 'child_selected': child_selected, 'id': root_facet['id'], 'title': root_facet['title'], 'parent': root_facet['parent'], 'children': children_list }) return final_facets else: children = [ { 'selected': result['id'] in selected_facets or result['parent'] in selected_facets, # noqa: E501 'id': result['id'], 'title': result['title'], 'parent': result['parent'], 'children': self.get_nested_facets(class_object, narrowed_facets, selected_facets, result['id']), # noqa: E501 'child_selected': any(child['selected'] is True or child['child_selected'] is True for child in # noqa: E501 self.get_nested_facets(class_object, narrowed_facets, selected_facets, result['id']) # noqa: E501 ) } for result in class_object.objects.filter( pk__in=narrowed_facets).filter( parent_id=parent).values('id', 'title', 'parent') ] # noqa: E501 return children class Meta: verbose_name = "TDP Activity search page"
class AnswerPage(CFGOVPage): """Page type for Ask CFPB answers.""" from ask_cfpb.models.django import Answer last_edited = models.DateField( blank=True, null=True, help_text="Change the date to today if you make a significant change.") question = models.TextField(blank=True) statement = models.TextField( blank=True, help_text=( "(Optional) Use this field to rephrase the question title as " "a statement. Use only if this answer has been chosen to appear " "on a money topic portal (e.g. /consumer-tools/debt-collection).")) short_answer = RichTextField(blank=True, features=['link', 'document-link'], help_text='Optional answer intro') answer_content = StreamField(ask_blocks.AskAnswerContent(), blank=True, verbose_name='Answer') answer_base = models.ForeignKey(Answer, blank=True, null=True, related_name='answer_pages', on_delete=models.SET_NULL) redirect_to_page = models.ForeignKey( 'self', blank=True, null=True, on_delete=models.SET_NULL, related_name='redirect_to_pages', help_text="Choose another AnswerPage to redirect this page to") featured = models.BooleanField( default=False, help_text=("Check to make this one of two featured answers " "on the landing page.")) featured_rank = models.IntegerField(blank=True, null=True) category = models.ManyToManyField( 'Category', blank=True, help_text=("Categorize this answer. " "Avoid putting into more than one category.")) search_tags = models.CharField( max_length=1000, blank=True, help_text="Search words or phrases, separated by commas") related_resource = models.ForeignKey(RelatedResource, blank=True, null=True, on_delete=models.SET_NULL) related_questions = ParentalManyToManyField( 'self', symmetrical=False, blank=True, related_name='related_question', help_text='Maximum of 3 related questions') portal_topic = ParentalManyToManyField( PortalTopic, blank=True, help_text='Limit to 1 portal topic if possible') primary_portal_topic = ParentalKey( PortalTopic, blank=True, null=True, on_delete=models.SET_NULL, related_name='primary_portal_topic', help_text=("Use only if assigning more than one portal topic, " "to control which topic is used as a breadcrumb.")) portal_category = ParentalManyToManyField(PortalCategory, blank=True) user_feedback = StreamField([ ('feedback', v1_blocks.Feedback()), ], blank=True) share_and_print = models.BooleanField( default=False, help_text="Include share and print buttons above answer.") content_panels = CFGOVPage.content_panels + [ MultiFieldPanel([ FieldPanel('last_edited'), FieldPanel('question'), FieldPanel('statement'), FieldPanel('short_answer') ], heading="Page content", classname="collapsible"), FieldPanel('share_and_print'), StreamFieldPanel('answer_content'), MultiFieldPanel([ SnippetChooserPanel('related_resource'), AutocompletePanel('related_questions', target_model='ask_cfpb.AnswerPage') ], heading="Related resources", classname="collapsible"), MultiFieldPanel([ FieldPanel('portal_topic', widget=forms.CheckboxSelectMultiple), FieldPanel('primary_portal_topic'), FieldPanel('portal_category', widget=forms.CheckboxSelectMultiple) ], heading="Portal tags", classname="collapsible"), MultiFieldPanel([FieldPanel('featured')], heading="Featured answer on Ask landing page", classname="collapsible"), MultiFieldPanel([ AutocompletePanel('redirect_to_page', target_model='ask_cfpb.AnswerPage') ], heading="Redirect to another answer", classname="collapsible"), MultiFieldPanel([StreamFieldPanel('user_feedback')], heading="User feedback", classname="collapsible collapsed"), ] sidebar = StreamField([ ('call_to_action', molecules.CallToAction()), ('related_links', molecules.RelatedLinks()), ('related_metadata', molecules.RelatedMetadata()), ('email_signup', organisms.EmailSignUp()), ('sidebar_contact', organisms.SidebarContactInfo()), ('rss_feed', molecules.RSSFeed()), ('social_media', molecules.SocialMedia()), ('reusable_text', v1_blocks.ReusableTextChooserBlock(ReusableText)), ], blank=True) sidebar_panels = [ StreamFieldPanel('sidebar'), ] search_fields = Page.search_fields + [ index.SearchField('answer_content'), index.SearchField('short_answer') ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(sidebar_panels, heading='Sidebar'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) template = 'ask-cfpb/answer-page.html' objects = CFGOVPageManager() def get_sibling_url(self): if self.answer_base: if self.language == 'es': sibling = self.answer_base.english_page else: sibling = self.answer_base.spanish_page if sibling and sibling.live and not sibling.redirect_to_page: return sibling.url def get_meta_description(self): """Determine what the page's meta and OpenGraph description should be Checks several different possible fields in order of preference. If none are found, returns an empty string, which is preferable to a generic description repeated on many pages. This method is overriding the standard one on CFGOVPage to factor in Ask CFPB AnswerPage-specific fields. """ preference_order = [ 'search_description', 'short_answer', 'first_text', ] candidates = {} if self.search_description: candidates['search_description'] = self.search_description if self.short_answer: candidates['short_answer'] = strip_tags(self.short_answer) if hasattr(self, 'answer_content'): for block in self.answer_content: if block.block_type == 'text': candidates['first_text'] = truncate_by_words_and_chars( strip_tags(block.value['content'].source), word_limit=35, char_limit=160) break for entry in preference_order: if candidates.get(entry): return candidates[entry] return '' def get_context(self, request, *args, **kwargs): # self.get_meta_description() is not called here because it is called # and added to the context by CFGOVPage's get_context() method. portal_topic = self.primary_portal_topic or self.portal_topic.first() context = super(AnswerPage, self).get_context(request) context['related_questions'] = self.related_questions.all() context['last_edited'] = self.last_edited context['portal_page'] = get_portal_or_portal_search_page( portal_topic, language=self.language) context['breadcrumb_items'] = get_ask_breadcrumbs( language=self.language, portal_topic=portal_topic, ) context['about_us'] = get_standard_text(self.language, 'about_us') context['disclaimer'] = get_standard_text(self.language, 'disclaimer') context['sibling_url'] = self.get_sibling_url() return context def answer_content_text(self): raw_text = extract_raw_text(self.answer_content.stream_data) return strip_tags(" ".join([self.short_answer, raw_text])) def answer_content_data(self): return truncate_by_words_and_chars(self.answer_content_text()) def short_answer_data(self): return ' '.join( RichTextField.get_searchable_content(self, self.short_answer)) def text(self): short_answer = self.short_answer_data() answer_text = self.answer_content_text() full_text = f"{short_answer}\n\n{answer_text}\n\n{self.question}" return full_text def __str__(self): if self.answer_base: return f"{self.answer_base.id}: {self.title}" else: return self.title @property def clean_search_tags(self): return [tag.strip() for tag in self.search_tags.split(",")] @property def status_string(self): if self.redirect_to_page: if not self.live: return ("redirected but not live") else: return ("redirected") else: return super(AnswerPage, self).status_string # Returns an image for the page's meta Open Graph tag @property def meta_image(self): if self.social_sharing_image: return self.social_sharing_image if not self.category.exists(): return None return self.category.first().category_image # Overrides the default of page.id for comparing against split testing # clusters. See: core.feature_flags.in_split_testing_cluster @property def split_test_id(self): return self.answer_base.id
class AnswerPage(CFGOVPage): """ Page type for Ask CFPB answers. """ from ask_cfpb.models import Answer last_edited = models.DateField( blank=True, null=True, help_text="Change the date to today if you make a significant change.") question = models.TextField(blank=True) statement = models.TextField( blank=True, help_text=( "(Optional) Use this field to rephrase the question title as " "a statement. Use only if this answer has been chosen to appear " "on a money topic portal (e.g. /consumer-tools/debt-collection).")) answer = RichTextField( blank=True, features=[ 'bold', 'italic', 'h2', 'h3', 'h4', 'link', 'ol', 'ul', 'document-link', 'image', 'embed', 'ask-tips', 'edit-html' ], help_text=( "Do not use H2 or H3 to style text. Only use the HTML Editor " "for troubleshooting. To style tips, warnings and notes, " "select the content that will go inside the rule lines " "(so, title + paragraph) and click the Pencil button " "to style it. Re-select the content and click the button " "again to unstyle the tip.")) snippet = RichTextField(blank=True, help_text='Optional answer intro') search_tags = models.CharField( max_length=1000, blank=True, help_text="PLEASE DON'T USE. THIS FIELD IS NOT YET ACTIVATED.") answer_base = models.ForeignKey(Answer, blank=True, null=True, related_name='answer_pages', on_delete=models.SET_NULL) redirect_to = models.ForeignKey( Answer, blank=True, null=True, on_delete=models.SET_NULL, related_name='redirected_pages', help_text="Choose another Answer to redirect this page to") content = StreamField([ ('feedback', v1_blocks.Feedback()), ], blank=True) content_panels = CFGOVPage.content_panels + [ MultiFieldPanel([FieldRowPanel([FieldPanel('last_edited')])], heading="Visible time stamp"), FieldPanel('question'), FieldPanel('statement'), FieldPanel('snippet'), FieldPanel('answer'), FieldPanel('search_tags'), FieldPanel('redirect_to'), ] sidebar = StreamField([ ('call_to_action', molecules.CallToAction()), ('related_links', molecules.RelatedLinks()), ('related_metadata', molecules.RelatedMetadata()), ('email_signup', organisms.EmailSignUp()), ('sidebar_contact', organisms.SidebarContactInfo()), ('rss_feed', molecules.RSSFeed()), ('social_media', molecules.SocialMedia()), ('reusable_text', v1_blocks.ReusableTextChooserBlock(ReusableText)), ], blank=True) sidebar_panels = [ StreamFieldPanel('sidebar'), ] search_fields = Page.search_fields + [ index.SearchField('answer'), index.SearchField('snippet') ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(sidebar_panels, heading='Sidebar (English only)'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) objects = CFGOVPageManager() def get_context(self, request, *args, **kwargs): context = super(AnswerPage, self).get_context(request) context['answer_id'] = self.answer_base.id context['related_questions'] = self.answer_base.related_questions.all() context['description'] = self.snippet if self.snippet \ else Truncator(self.answer).words(40, truncate=' ...') context['audiences'] = [{ 'text': audience.name, 'url': '/ask-cfpb/audience-{}'.format(slugify(audience.name)) } for audience in self.answer_base.audiences.all()] if self.language == 'es': tag_dict = self.Answer.valid_tags(language='es') context['tags_es'] = [ tag for tag in self.answer_base.clean_tags_es if tag in tag_dict['valid_tags'] ] context['tweet_text'] = Truncator(self.question).chars( 100, truncate=' ...') context['disclaimer'] = get_reusable_text_snippet( SPANISH_DISCLAIMER_SNIPPET_TITLE) context['category'] = self.answer_base.category.first() elif self.language == 'en': # we're not using tags on English pages yet, so cut the overhead # tag_dict = self.Answer.valid_tags() # context['tags'] = [tag for tag in self.answer_base.clean_tags # if tag in tag_dict['valid_tags']] context['about_us'] = get_reusable_text_snippet( ABOUT_US_SNIPPET_TITLE) context['disclaimer'] = get_reusable_text_snippet( ENGLISH_DISCLAIMER_SNIPPET_TITLE) context['last_edited'] = (self.last_edited or self.answer_base.last_edited) # breadcrumbs and/or category should reflect # the referrer if it is a consumer tools portal or # ask category page context['category'], context['breadcrumb_items'] = \ get_question_referrer_data( request, self.answer_base.category.all()) subcategories = [] for subcat in self.answer_base.subcategory.all(): if subcat.parent == context['category']: subcategories.append(subcat) for related in subcat.related_subcategories.all(): if related.parent == context['category']: subcategories.append(related) context['subcategories'] = set(subcategories) return context def get_template(self, request): printable = request.GET.get('print', False) if self.language == 'es': if printable == 'true': return 'ask-cfpb/answer-page-spanish-printable.html' return 'ask-cfpb/answer-page-spanish.html' return 'ask-cfpb/answer-page.html' def __str__(self): if self.answer_base: return '{}: {}'.format(self.answer_base.id, self.title) else: return self.title @property def status_string(self): if self.redirect_to: if not self.live: return _("redirected but not live") else: return _("redirected") else: return super(AnswerPage, self).status_string # Returns an image for the page's meta Open Graph tag @property def meta_image(self): if self.answer_base.social_sharing_image: return self.answer_base.social_sharing_image if not self.answer_base.category.exists(): return None return self.answer_base.category.first().category_image # Overrides the default of page.id for comparing against split testing # clusters. See: core.feature_flags.in_split_testing_cluster @property def split_test_id(self): return self.answer_base.id
class ActivityIndexPage(CFGOVPage): """A model for the Activity Search page.""" subpage_types = ['teachers_digital_platform.ActivityPage'] objects = CFGOVPageManager() header = StreamField([ ('text_introduction', molecules.TextIntroduction()), ('notification', molecules.Notification()), ], blank=True) header_sidebar = StreamField([ ('image', TdpSearchHeroImage()), ], blank=True) results = {} activity_setups = None content_panels = CFGOVPage.content_panels + [ StreamFieldPanel('header'), StreamFieldPanel('header_sidebar'), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), ObjectList(CFGOVPage.sidefoot_panels, heading='Sidebar/Footer'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) @classmethod def can_create_at(cls, parent): # You can only create one of these! return super(ActivityIndexPage, cls).can_create_at(parent) \ and not cls.objects.exists() def get_template(self, request): template = 'teachers_digital_platform/activity_index_page.html' if 'partial' in request.GET: template = 'teachers_digital_platform/activity_search_facets_and_results.html' # noqa: E501 return template def dsl_search(self, request, *args, **kwargs): """Search using Elasticsearch 7 and django-elasticsearch-dsl.""" all_facets = copy.copy(self.activity_setups.facet_setup) selected_facets = {} card_setup = self.activity_setups.ordered_cards total_activities = len(card_setup) search_query = request.GET.get('q', '') facet_called = any( [request.GET.get(facet, '') for facet in FACET_LIST] ) # If there's no query or facet request, we can return cached setups: if not search_query and not facet_called: payload = { 'search_query': search_query, 'results': list(card_setup.values()), 'total_results': total_activities, 'total_activities': total_activities, 'selected_facets': selected_facets, 'all_facets': all_facets, 'expanded_facets': ALWAYS_EXPANDED, } self.results = payload results_per_page = validate_results_per_page(request) paginator = Paginator(payload['results'], results_per_page) current_page = validate_page_number(request, paginator) paginated_page = paginator.page(current_page) context_update = { 'facets': all_facets, 'activities': paginated_page, 'total_results': total_activities, 'results_per_page': results_per_page, 'current_page': current_page, 'paginator': paginator, 'show_filters': bool(selected_facets), } return context_update dsl_search = ActivityPageDocument().search() if search_query: terms = search_query.split() for term in terms: dsl_search = dsl_search.query( "bool", must=Q("multi_match", query=term, fields=SEARCH_FIELDS) ) else: dsl_search = dsl_search.sort('-date') for facet, facet_config in FACET_MAP: if facet in request.GET and request.GET.get(facet): facet_ids = [ value for value in request.GET.getlist(facet) if value.isdigit() ] selected_facets[facet] = facet_ids for facet, pks in selected_facets.items(): dsl_search = dsl_search.query( "bool", should=[Q("match", **{facet: pk}) for pk in pks] ) facet_search = dsl_search.update_from_dict(FACET_DICT) total_results = dsl_search.count() dsl_search = dsl_search[:total_results] response = dsl_search.execute() results = [ card_setup[str(hit.id)] for hit in response[:total_results] ] facet_response = facet_search.execute() facet_counts = {facet: getattr( facet_response.aggregations, f"{facet}_terms").buckets for facet in FACET_LIST} all_facets = parse_dsl_facets( all_facets, facet_counts, selected_facets ) payload = { 'search_query': search_query, 'results': results, 'total_results': total_results, 'total_activities': total_activities, 'selected_facets': selected_facets, 'all_facets': all_facets, } # List all facet blocks that need to be expanded conditionally_expanded = { facet_name for facet_name, facet_items in all_facets.items() if any( facet['selected'] is True for facet in facet_items ) } expanded_facets = ALWAYS_EXPANDED.union(set(conditionally_expanded)) payload.update({ 'expanded_facets': expanded_facets, }) self.results = payload results_per_page = validate_results_per_page(request) paginator = Paginator(payload['results'], results_per_page) current_page = validate_page_number(request, paginator) paginated_page = paginator.page(current_page) context_update = { 'facets': all_facets, 'activities': paginated_page, 'total_results': total_results, 'results_per_page': results_per_page, 'current_page': current_page, 'paginator': paginator, 'show_filters': bool(selected_facets), } return context_update def get_context(self, request, *args, **kwargs): if not self.activity_setups: self.activity_setups = get_activity_setup() context_update = self.dsl_search(request, *args, **kwargs) context = super(ActivityIndexPage, self).get_context(request) context.update(context_update) return context class Meta: verbose_name = "TDP Activity search page"
class AnswerPage(CFGOVPage): """ Page type for Ask CFPB answers. """ from ask_cfpb.models import Answer question = RichTextField(blank=True, editable=False) answer = RichTextField(blank=True, editable=False) snippet = RichTextField( blank=True, help_text='Optional answer intro', editable=False) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) publish_date = models.DateTimeField(default=timezone.now) answer_base = models.ForeignKey( Answer, blank=True, null=True, related_name='answer_pages', on_delete=models.SET_NULL) redirect_to = models.ForeignKey( Answer, blank=True, null=True, on_delete=models.SET_NULL, related_name='redirected_pages', help_text="Choose another Answer to redirect this page to") content = StreamField([ ('feedback', v1_blocks.Feedback()), ], blank=True) content_panels = CFGOVPage.content_panels + [ FieldPanel('redirect_to'), ] sidebar = StreamField([ ('call_to_action', molecules.CallToAction()), ('related_links', molecules.RelatedLinks()), ('related_metadata', molecules.RelatedMetadata()), ('email_signup', organisms.EmailSignUp()), ('sidebar_contact', organisms.SidebarContactInfo()), ('rss_feed', molecules.RSSFeed()), ('social_media', molecules.SocialMedia()), ('reusable_text', v1_blocks.ReusableTextChooserBlock(ReusableText)), ], blank=True) sidebar_panels = [StreamFieldPanel('sidebar'), ] search_fields = Page.search_fields + [ index.SearchField('question'), index.SearchField('answer'), index.SearchField('answer_base'), index.FilterField('language') ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(sidebar_panels, heading='Sidebar (English only)'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) objects = CFGOVPageManager() def get_context(self, request, *args, **kwargs): context = super(AnswerPage, self).get_context(request) context['answer_id'] = self.answer_base.id context['related_questions'] = self.answer_base.related_questions.all() context['description'] = self.snippet if self.snippet \ else Truncator(self.answer).words(40, truncate=' ...') context['audiences'] = [ {'text': audience.name, 'url': '/ask-cfpb/audience-{}'.format( slugify(audience.name))} for audience in self.answer_base.audiences.all()] if self.language == 'es': tag_dict = self.Answer.valid_tags(language='es') context['tags_es'] = [tag for tag in self.answer_base.tags_es if tag in tag_dict['valid_tags']] context['tweet_text'] = Truncator(self.question).chars( 100, truncate=' ...') context['disclaimer'] = get_reusable_text_snippet( SPANISH_DISCLAIMER_SNIPPET_TITLE) context['category'] = self.answer_base.category.first() elif self.language == 'en': # we're not using tags on English pages yet, so cut the overhead # tag_dict = self.Answer.valid_tags() # context['tags'] = [tag for tag in self.answer_base.tags # if tag in tag_dict['valid_tags']] context['about_us'] = get_reusable_text_snippet( ABOUT_US_SNIPPET_TITLE) context['disclaimer'] = get_reusable_text_snippet( ENGLISH_DISCLAIMER_SNIPPET_TITLE) context['last_edited'] = self.answer_base.last_edited # breadcrumbs and/or category should reflect # the referrer if it is a consumer tools portal or # ask category page context['category'], context['breadcrumb_items'] = \ get_question_referrer_data( request, self.answer_base.category.all()) subcategories = [] for subcat in self.answer_base.subcategory.all(): if subcat.parent == context['category']: subcategories.append(subcat) for related in subcat.related_subcategories.all(): if related.parent == context['category']: subcategories.append(related) context['subcategories'] = set(subcategories) return context def get_template(self, request): printable = request.GET.get('print', False) if self.language == 'es': if printable == 'true': return 'ask-cfpb/answer-page-spanish-printable.html' return 'ask-cfpb/answer-page-spanish.html' return 'ask-cfpb/answer-page.html' def __str__(self): if self.answer_base: return '{}: {}'.format(self.answer_base.id, self.title) else: return self.title @property def status_string(self): if self.redirect_to: if not self.live: return _("redirected but not live") else: return _("redirected") else: return super(AnswerPage, self).status_string # Returns an image for the page's meta Open Graph tag @property def meta_image(self): if self.answer_base.social_sharing_image: return self.answer_base.social_sharing_image if not self.answer_base.category.exists(): return None return self.answer_base.category.first().category_image
class AnswerCategoryPage(RoutablePageMixin, SecondaryNavigationJSMixin, CFGOVPage): """ A routable page type for Ask CFPB category pages and their subcategories. """ from ask_cfpb.models import Answer, Audience, Category, SubCategory objects = CFGOVPageManager() content = StreamField([], null=True) ask_category = models.ForeignKey( Category, blank=True, null=True, on_delete=models.PROTECT, related_name='category_page') ask_subcategory = models.ForeignKey( SubCategory, blank=True, null=True, on_delete=models.PROTECT, related_name='subcategory_page') content_panels = CFGOVPage.content_panels + [ FieldPanel('ask_category', Category), StreamFieldPanel('content'), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) def get_template(self, request): if self.language == 'es': return 'ask-cfpb/category-page-spanish.html' return 'ask-cfpb/category-page.html' def get_context(self, request, *args, **kwargs): context = super( AnswerCategoryPage, self).get_context(request, *args, **kwargs) sqs = SearchQuerySet().models(self.Category) if self.language == 'es': sqs = sqs.filter(content=self.ask_category.name_es) else: sqs = sqs.filter(content=self.ask_category.name) if sqs: facet_map = sqs[0].facet_map else: facet_map = self.ask_category.facet_map facet_dict = json.loads(facet_map) subcat_ids = facet_dict['subcategories'].keys() answer_ids = facet_dict['answers'].keys() audience_ids = facet_dict['audiences'].keys() subcats = self.SubCategory.objects.filter( pk__in=subcat_ids).values( 'id', 'slug', 'slug_es', 'name', 'name_es') answers = self.Answer.objects.filter( pk__in=answer_ids).order_by('-pk').values( 'id', 'question', 'question_es', 'slug', 'slug_es', 'answer_es') for a in answers: a['answer_es'] = Truncator(a['answer_es']).words( 40, truncate=' ...') audiences = self.Audience.objects.filter( pk__in=audience_ids).values('id', 'name') context.update({ 'answers': answers, 'audiences': audiences, 'facets': facet_dict, 'choices': subcats, 'results_count': answers.count(), 'get_secondary_nav_items': get_ask_nav_items }) if self.language == 'en': context['about_us'] = get_reusable_text_snippet( ABOUT_US_SNIPPET_TITLE) context['disclaimer'] = get_reusable_text_snippet( ENGLISH_DISCLAIMER_SNIPPET_TITLE) context['breadcrumb_items'] = get_ask_breadcrumbs() elif self.language == 'es': context['tags'] = self.ask_category.top_tags_es return context # Returns an image for the page's meta Open Graph tag @property def meta_image(self): return self.ask_category.category_image @route(r'^$') def category_page(self, request): context = self.get_context(request) paginator = Paginator(context.get('answers'), 20) page_number = validate_page_number(request, paginator) page = paginator.page(page_number) context.update({ 'paginator': paginator, 'current_page': page_number, 'questions': page, }) return TemplateResponse( request, self.get_template(request), context) @route(r'^(?P<subcat>[^/]+)/$') def subcategory_page(self, request, **kwargs): subcat = self.SubCategory.objects.filter( slug=kwargs.get('subcat')).first() if subcat: self.ask_subcategory = subcat else: raise Http404 context = self.get_context(request) id_key = str(subcat.pk) answers = context['answers'].filter( pk__in=context['facets']['subcategories'][id_key]) paginator = Paginator(answers, 20) page_number = validate_page_number(request, paginator) page = paginator.page(page_number) context.update({ 'paginator': paginator, 'current_page': page_number, 'results_count': answers.count(), 'questions': page, 'breadcrumb_items': get_ask_breadcrumbs( self.ask_category) }) return TemplateResponse( request, self.get_template(request), context)