class LearnPage(AbstractFilterPage): content = StreamField([ ('full_width_text', organisms.FullWidthText()), ('info_unit_group', organisms.InfoUnitGroup()), ('expandable_group', organisms.ExpandableGroup()), ('contact_expandable_group', organisms.ContactExpandableGroup()), ('expandable', organisms.Expandable()), ('well', organisms.Well()), ('call_to_action', molecules.CallToAction()), ('email_signup', organisms.EmailSignUp()), ('video_player', organisms.VideoPlayer()), ('audio_player', organisms.AudioPlayer()), ('table_block', organisms.AtomicTableBlock( table_options={'renderer': 'html'} )), ('feedback', v1_blocks.Feedback()), ], blank=True) edit_handler = AbstractFilterPage.generate_edit_handler( content_panel=StreamFieldPanel('content') ) template = 'learn-page/index.html' objects = PageManager() search_fields = AbstractFilterPage.search_fields + [ index.SearchField('content') ]
class FullWidthText(blocks.StreamBlock): content = blocks.RichTextBlock(icon='edit') content_with_anchor = molecules.ContentWithAnchor() heading = v1_blocks.HeadingBlock(required=False) image = molecules.ContentImage() table_block = AtomicTableBlock(table_options={'renderer': 'html'}) quote = molecules.Quote() cta = molecules.CallToAction() related_links = molecules.RelatedLinks() reusable_text = v1_blocks.ReusableTextChooserBlock('v1.ReusableText') email_signup = EmailSignUp() class Meta: icon = 'edit' template = '_includes/organisms/full-width-text.html'
class FullWidthText(blocks.StreamBlock): content_with_anchor = molecules.ContentWithAnchor() content = blocks.RichTextBlock(icon='edit') media = images_blocks.ImageChooserBlock(icon='image') quote = molecules.Quote() cta = molecules.CallToAction() related_links = molecules.RelatedLinks() table = Table(editable=False) table_block = AtomicTableBlock(table_options={'renderer': 'html'}) image_inset = molecules.ImageInset() reusable_text = v1_blocks.ReusableTextChooserBlock('v1.ReusableText') class Meta: icon = 'edit' template = '_includes/organisms/full-width-text.html'
class CFGOVPage(Page): authors = ClusterTaggableManager(through=CFGOVAuthoredPages, blank=True, verbose_name='Authors', help_text='A comma separated list of ' + 'authors.', related_name='authored_pages') tags = ClusterTaggableManager(through=CFGOVTaggedPages, blank=True, related_name='tagged_pages') shared = models.BooleanField(default=False) has_unshared_changes = models.BooleanField(default=False) language = models.CharField(choices=ref.supported_languagues, default='en', max_length=2) # This is used solely for subclassing pages we want to make at the CFPB. is_creatable = False objects = CFGOVPageManager() # These fields show up in either the sidebar or the footer of the page # depending on the page type. sidefoot = StreamField([ ('call_to_action', molecules.CallToAction()), ('related_links', molecules.RelatedLinks()), ('related_posts', organisms.RelatedPosts()), ('related_metadata', molecules.RelatedMetadata()), ('email_signup', organisms.EmailSignUp()), ('sidebar_contact', organisms.SidebarContactInfo()), ('rss_feed', molecules.RSSFeed()), ('social_media', molecules.SocialMedia()), ('reusable_text', ReusableTextChooserBlock(ReusableText)), ], blank=True) # Panels sidefoot_panels = [ StreamFieldPanel('sidefoot'), ] settings_panels = [ MultiFieldPanel(Page.promote_panels, 'Settings'), InlinePanel('categories', label="Categories", max_num=2), FieldPanel('tags', 'Tags'), FieldPanel('authors', 'Authors'), MultiFieldPanel(Page.settings_panels, 'Scheduled Publishing'), FieldPanel('language', 'language'), ] # Tab handler interface guide because it must be repeated for each subclass edit_handler = TabbedInterface([ ObjectList(Page.content_panels, heading='General Content'), ObjectList(sidefoot_panels, heading='Sidebar/Footer'), ObjectList(settings_panels, heading='Configuration'), ]) def get_authors(self): """ Returns a sorted list of authors. Default is alphabetical """ return self.alphabetize_authors() def alphabetize_authors(self): """ Alphabetize authors of this page by last name, then first name if needed """ # First sort by first name author_names = self.authors.order_by('name') # Then sort by last name return sorted(author_names, key=lambda x: x.name.split()[-1]) def generate_view_more_url(self, request): activity_log = CFGOVPage.objects.get(slug='activity-log').specific tags = [] index = activity_log.form_id() tags = urlencode([('filter%s_topics' % index, tag) for tag in self.tags.slugs()]) return (get_protected_url({'request': request}, activity_log) + '?' + tags) def related_posts(self, block, hostname): from v1.models.learn_page import AbstractFilterPage related = {} query = models.Q(('tags__name__in', self.tags.names())) search_types = [ ('blog', 'posts', 'Blog', query), ('newsroom', 'newsroom', 'Newsroom', query), ('events', 'events', 'Events', query), ] def fetch_children_by_specific_category(block, parent_slug): """ This used to be a Page.objects.get, which would throw an exception if the requested parent wasn't found. As of Django 1.6, you can now do Page.objects.filter().first(); the advantage here is that you can check for None right away and not have to rely on catching exceptions, which in any case didn't do anything useful other than to print an error message. Instead, we just return an empty query which has no effect on the final result. """ parent = Page.objects.filter(slug=parent_slug).first() if parent: child_query = Page.objects.child_of_q(parent) if 'specific_categories' in block.value: child_query &= specific_categories_query( block, parent_slug) else: child_query = Q() return child_query def specific_categories_query(block, parent_slug): specific_categories = ref.related_posts_category_lookup( block.value['specific_categories']) choices = [c[0] for c in ref.choices_for_page_type(parent_slug)] categories = [c for c in specific_categories if c in choices] if categories: return Q(('categories__name__in', categories)) else: return Q() for parent_slug, search_type, search_type_name, search_query in \ search_types: search_query &= fetch_children_by_specific_category( block, parent_slug) if parent_slug == 'events': search_query |= fetch_children_by_specific_category( block, 'archive-past-events') & query relate = block.value.get('relate_{}'.format(search_type), None) if relate: related[search_type_name] = (AbstractFilterPage.objects.live( ).filter(search_query).distinct().exclude(id=self.id).order_by( '-date_published')[:block.value['limit']]) # Return a dictionary of lists of each type when there's at least one # hit for that type. return { search_type: queryset for search_type, queryset in related.items() if queryset } def get_breadcrumbs(self, request): ancestors = self.get_ancestors() home_page_children = request.site.root_page.get_children() for i, ancestor in enumerate(ancestors): if ancestor in home_page_children: return [ancestor for ancestor in ancestors[i + 1:]] return [] def get_appropriate_descendants(self, hostname, inclusive=True): return CFGOVPage.objects.live().descendant_of(self, inclusive) def get_appropriate_siblings(self, hostname, inclusive=True): return CFGOVPage.objects.live().sibling_of(self, inclusive) def get_next_appropriate_siblings(self, hostname, inclusive=False): return self.get_appropriate_siblings( hostname=hostname, inclusive=inclusive).filter(path__gte=self.path).order_by('path') def get_prev_appropriate_siblings(self, hostname, inclusive=False): return self.get_appropriate_siblings( hostname=hostname, inclusive=inclusive).filter(path__lte=self.path).order_by('-path') def get_context(self, request, *args, **kwargs): context = super(CFGOVPage, self).get_context(request, *args, **kwargs) for hook in hooks.get_hooks('cfgovpage_context_handlers'): hook(self, request, context, *args, **kwargs) return context def serve(self, request, *args, **kwargs): """ If request is ajax, then return the ajax request handler response, else return the super. """ if request.method == 'POST': return self.serve_post(request, *args, **kwargs) return super(CFGOVPage, self).serve(request, *args, **kwargs) def serve_post(self, request, *args, **kwargs): """ Attempts to retreive form_id from the POST request and returns a JSON response. If form_id is found, it returns the response from the block method retrieval. If form_id is not found, it returns an error response. """ form_id = request.POST.get('form_id', None) if not form_id: if request.is_ajax(): return JsonResponse({'result': 'error'}, status=400) return HttpResponseBadRequest(self.url) sfname, index = form_id.split('-')[1:] streamfield = getattr(self, sfname) module = streamfield[int(index)] result = module.block.get_result(self, request, module.value, True) if isinstance(result, HttpResponse): return result else: context = self.get_context(request, *args, **kwargs) context['form_modules'][sfname].update({int(index): result}) return TemplateResponse( request, self.get_template(request, *args, **kwargs), context) class Meta: app_label = 'v1' def parent(self): parent = self.get_ancestors(inclusive=False).reverse()[0].specific return parent # To be overriden if page type requires JS files every time # 'template' is used as the key for front-end consistency def add_page_js(self, js): js['template'] = [] # Retrieves the stream values on a page from it's Streamfield def _get_streamfield_blocks(self): lst = [ value for key, value in vars(self).iteritems() if type(value) is StreamValue ] return list(chain(*lst)) # Gets the JS from the Streamfield data def _add_streamfield_js(self, js): # Create a dict with keys ordered organisms, molecules, then atoms for child in self._get_streamfield_blocks(): self._add_block_js(child.block, js) # Recursively search the blocks and classes for declared Media.js def _add_block_js(self, block, js): self._assign_js(block, js) if issubclass(type(block), blocks.StructBlock): for child in block.child_blocks.values(): self._add_block_js(child, js) elif issubclass(type(block), blocks.ListBlock): self._add_block_js(block.child_block, js) # Assign the Media js to the dictionary appropriately def _assign_js(self, obj, js): try: if hasattr(obj.Media, 'js'): for key in js.keys(): if obj.__module__.endswith(key): js[key] += obj.Media.js if not [ key for key in js.keys() if obj.__module__.endswith(key) ]: js.update({'other': obj.Media.js}) except: pass # Returns all the JS files specific to this page and it's current # Streamfield's blocks @property def media(self): js = OrderedDict() for key in ['template', 'organisms', 'molecules', 'atoms']: js.update({key: []}) self.add_page_js(js) self._add_streamfield_js(js) for key, js_files in js.iteritems(): js[key] = OrderedDict.fromkeys(js_files).keys() return js
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 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 CFGOVPage(Page): authors = ClusterTaggableManager(through=CFGOVAuthoredPages, blank=True, verbose_name='Authors', help_text='A comma separated list of ' + 'authors.', related_name='authored_pages') tags = ClusterTaggableManager(through=CFGOVTaggedPages, blank=True, related_name='tagged_pages') language = models.CharField(choices=ref.supported_languagues, default='en', max_length=2) social_sharing_image = models.ForeignKey( 'v1.CFGOVImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', help_text=( 'Optionally select a custom image to appear when users share this ' 'page on social media websites. Recommended size: 1200w x 630h. ' 'Maximum size: 4096w x 4096h.')) schema_json = JSONField( null=True, blank=True, verbose_name='Schema JSON', help_text=mark_safe( 'Enter structured data for this page in JSON-LD format, ' 'for use by search engines in providing rich search results. ' '<a href="https://developers.google.com/search/docs/guides/' 'intro-structured-data">Learn more.</a> ' 'JSON entered here will be output in the ' '<code><head></code> of the page between ' '<code><script type="application/ld+json"></code> and ' '<code></script></code> tags.'), ) force_breadcrumbs = models.BooleanField( "Force breadcrumbs on child pages", default=False, blank=True, help_text=( "Normally breadcrumbs don't appear on pages one or two levels " "below the homepage. Check this option to force breadcrumbs to " "appear on all children of this page no matter how many levels " "below the homepage they are (for example, if you want " "breadcrumbs to appear on all children of a top-level campaign " "page)."), ) # This is used solely for subclassing pages we want to make at the CFPB. is_creatable = False objects = CFGOVPageManager() search_fields = Page.search_fields + [ index.SearchField('sidefoot'), ] # These fields show up in either the sidebar or the footer of the page # depending on the page type. sidefoot = StreamField([ ('call_to_action', molecules.CallToAction()), ('related_links', molecules.RelatedLinks()), ('related_posts', organisms.RelatedPosts()), ('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) # Panels promote_panels = Page.promote_panels + [ ImageChooserPanel('social_sharing_image'), FieldPanel('force_breadcrumbs', 'Breadcrumbs'), ] sidefoot_panels = [ StreamFieldPanel('sidefoot'), ] settings_panels = [ MultiFieldPanel(promote_panels, 'Settings'), InlinePanel('categories', label="Categories", max_num=2), FieldPanel('tags', 'Tags'), FieldPanel('authors', 'Authors'), FieldPanel('schema_json', 'Structured Data'), MultiFieldPanel(Page.settings_panels, 'Scheduled Publishing'), FieldPanel('language', 'language'), ] # Tab handler interface guide because it must be repeated for each subclass edit_handler = TabbedInterface([ ObjectList(Page.content_panels, heading='General Content'), ObjectList(sidefoot_panels, heading='Sidebar/Footer'), ObjectList(settings_panels, heading='Configuration'), ]) def clean(self): super(CFGOVPage, self).clean() validate_social_sharing_image(self.social_sharing_image) def get_authors(self): """ Returns a sorted list of authors. Default is alphabetical """ return self.alphabetize_authors() def alphabetize_authors(self): """ Alphabetize authors of this page by last name, then first name if needed """ # First sort by first name author_names = self.authors.order_by('name') # Then sort by last name return sorted(author_names, key=lambda x: x.name.split()[-1]) def related_metadata_tags(self): # Set the tags to correct data format tags = {'links': []} filter_page = self.get_filter_data() for tag in self.specific.tags.all(): tag_link = {'text': tag.name, 'url': ''} if filter_page: relative_url = filter_page.relative_url(filter_page.get_site()) param = '?topics=' + tag.slug tag_link['url'] = relative_url + param tags['links'].append(tag_link) return tags def get_filter_data(self): for ancestor in self.get_ancestors().reverse().specific(): if ancestor.specific_class.__name__ in [ 'BrowseFilterablePage', 'SublandingFilterablePage', 'EventArchivePage', 'NewsroomLandingPage' ]: return ancestor return None def get_breadcrumbs(self, request): ancestors = self.get_ancestors().specific() site = Site.find_for_request(request) for i, ancestor in enumerate(ancestors): if ancestor.is_child_of(site.root_page): if ancestor.specific.force_breadcrumbs: return ancestors[i:] return ancestors[i + 1:] return [] def get_appropriate_descendants(self, inclusive=True): return CFGOVPage.objects.live().descendant_of(self, inclusive) def get_appropriate_siblings(self, inclusive=True): return CFGOVPage.objects.live().sibling_of(self, inclusive) def get_context(self, request, *args, **kwargs): context = super(CFGOVPage, self).get_context(request, *args, **kwargs) for hook in hooks.get_hooks('cfgovpage_context_handlers'): hook(self, request, context, *args, **kwargs) # Add any banners that are enabled and match the current request path # to a context variable. context['banners'] = Banner.objects \ .filter(enabled=True) \ .annotate( # This annotation creates a path field in the QuerySet # that we can use in the filter below to compare with # the url_pattern defined on each enabled banner. path=Value(request.path, output_field=models.CharField())) \ .filter(path__regex=F('url_pattern')) if self.schema_json: context['schema_json'] = self.schema_json return context def serve(self, request, *args, **kwargs): """ If request is ajax, then return the ajax request handler response, else return the super. """ if request.method == 'POST': return self.serve_post(request, *args, **kwargs) # Force the page's language on the request translation.activate(self.language) request.LANGUAGE_CODE = translation.get_language() return super(CFGOVPage, self).serve(request, *args, **kwargs) def _return_bad_post_response(self, request): if request.is_ajax(): return JsonResponse({'result': 'error'}, status=400) return HttpResponseBadRequest(self.url) def serve_post(self, request, *args, **kwargs): """Handle a POST to a specific form on the page. Attempts to retrieve form_id from the POST request, which must be formatted like "form-name-index" where the "name" part is the name of a StreamField on the page and the "index" part refers to the index of the form element in the StreamField. If form_id is found, it returns the response from the block method retrieval. If form_id is not found, or if form_id is not a block that implements get_result() to process the POST, it returns an error response. """ form_module = None form_id = request.POST.get('form_id', None) if form_id: form_id_parts = form_id.split('-') if len(form_id_parts) == 3: streamfield_name = form_id_parts[1] streamfield = getattr(self, streamfield_name, None) if streamfield is not None: try: streamfield_index = int(form_id_parts[2]) except ValueError: streamfield_index = None if streamfield_index is not None: try: form_module = streamfield[streamfield_index] except IndexError: form_module = None try: result = form_module.block.get_result(self, request, form_module.value, True) except AttributeError: return self._return_bad_post_response(request) if isinstance(result, HttpResponse): return result context = self.get_context(request, *args, **kwargs) context['form_modules'][streamfield_name].update( {streamfield_index: result}) return TemplateResponse(request, self.get_template(request, *args, **kwargs), context) class Meta: app_label = 'v1' def parent(self): parent = self.get_ancestors(inclusive=False).reverse()[0].specific return parent # To be overriden if page type requires JS files every time @property def page_js(self): return [] @property def streamfield_js(self): js = [] block_cls_names = get_page_blocks(self) for block_cls_name in block_cls_names: block_cls = import_string(block_cls_name) if hasattr(block_cls, 'Media') and hasattr(block_cls.Media, 'js'): js.extend(block_cls.Media.js) return js # Returns the JS files required by this page and its StreamField blocks. @property def media(self): return sorted(set(self.page_js + self.streamfield_js)) # Returns an image for the page's meta Open Graph tag @property def meta_image(self): return self.social_sharing_image @property def post_preview_cache_key(self): return 'post_preview_{}'.format(self.id)
class CFGOVPage(Page): authors = ClusterTaggableManager(through=CFGOVAuthoredPages, blank=True, verbose_name='Authors', help_text='A comma separated list of ' + 'authors.', related_name='authored_pages') tags = ClusterTaggableManager(through=CFGOVTaggedPages, blank=True, related_name='tagged_pages') language = models.CharField(choices=ref.supported_languagues, default='en', max_length=2) social_sharing_image = models.ForeignKey( 'v1.CFGOVImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', help_text=( 'Optionally select a custom image to appear when users share this ' 'page on social media websites. Recommended size: 1200w x 630h. ' 'Maximum size: 4096w x 4096h.')) # This is used solely for subclassing pages we want to make at the CFPB. is_creatable = False objects = CFGOVPageManager() search_fields = Page.search_fields + [ index.SearchField('sidefoot'), ] # These fields show up in either the sidebar or the footer of the page # depending on the page type. sidefoot = StreamField([ ('call_to_action', molecules.CallToAction()), ('related_links', molecules.RelatedLinks()), ('related_posts', organisms.RelatedPosts()), ('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) # Panels promote_panels = Page.promote_panels + [ ImageChooserPanel('social_sharing_image'), ] sidefoot_panels = [ StreamFieldPanel('sidefoot'), ] settings_panels = [ MultiFieldPanel(promote_panels, 'Settings'), InlinePanel('categories', label="Categories", max_num=2), FieldPanel('tags', 'Tags'), FieldPanel('authors', 'Authors'), MultiFieldPanel(Page.settings_panels, 'Scheduled Publishing'), FieldPanel('language', 'language'), ] # Tab handler interface guide because it must be repeated for each subclass edit_handler = TabbedInterface([ ObjectList(Page.content_panels, heading='General Content'), ObjectList(sidefoot_panels, heading='Sidebar/Footer'), ObjectList(settings_panels, heading='Configuration'), ]) def clean(self): super(CFGOVPage, self).clean() validate_social_sharing_image(self.social_sharing_image) def get_authors(self): """ Returns a sorted list of authors. Default is alphabetical """ return self.alphabetize_authors() def alphabetize_authors(self): """ Alphabetize authors of this page by last name, then first name if needed """ # First sort by first name author_names = self.authors.order_by('name') # Then sort by last name return sorted(author_names, key=lambda x: x.name.split()[-1]) def related_metadata_tags(self): # Set the tags to correct data format tags = {'links': []} filter_page = self.get_filter_data() for tag in self.specific.tags.all(): tag_link = {'text': tag.name, 'url': ''} if filter_page: relative_url = filter_page.relative_url(filter_page.get_site()) param = '?topics=' + tag.slug tag_link['url'] = relative_url + param tags['links'].append(tag_link) return tags def get_filter_data(self): for ancestor in self.get_ancestors().reverse().specific(): if ancestor.specific_class.__name__ in [ 'BrowseFilterablePage', 'SublandingFilterablePage', 'EventArchivePage', 'NewsroomLandingPage' ]: return ancestor return None def get_breadcrumbs(self, request): ancestors = self.get_ancestors() home_page_children = request.site.root_page.get_children() for i, ancestor in enumerate(ancestors): if ancestor in home_page_children: # Add top level parent page and `/process/` url segments # where necessary to BAH page breadcrumbs. # TODO: Remove this when BAH moves under /consumer-tools # and redirects are added after 2018 homebuying campaign. if ancestor.slug == 'owning-a-home': breadcrumbs = [] for ancestor in ancestors[i:]: ancestor_url = ancestor.relative_url(request.site) if ancestor_url.startswith( ('/owning-a-home/prepare', '/owning-a-home/explore', '/owning-a-home/compare', '/owning-a-home/close', '/owning-a-home/sources')): ancestor_url = ancestor_url.replace( 'owning-a-home', 'owning-a-home/process') breadcrumbs.append({ 'title': ancestor.title, 'href': ancestor_url, }) return breadcrumbs # END TODO return [ancestor for ancestor in ancestors[i + 1:]] return [] def get_appropriate_descendants(self, inclusive=True): return CFGOVPage.objects.live().descendant_of(self, inclusive) def get_appropriate_siblings(self, inclusive=True): return CFGOVPage.objects.live().sibling_of(self, inclusive) def get_context(self, request, *args, **kwargs): context = super(CFGOVPage, self).get_context(request, *args, **kwargs) for hook in hooks.get_hooks('cfgovpage_context_handlers'): hook(self, request, context, *args, **kwargs) return context def serve(self, request, *args, **kwargs): """ If request is ajax, then return the ajax request handler response, else return the super. """ if request.method == 'POST': return self.serve_post(request, *args, **kwargs) # Force the page's language on the request translation.activate(self.language) request.LANGUAGE_CODE = translation.get_language() return super(CFGOVPage, self).serve(request, *args, **kwargs) def _return_bad_post_response(self, request): if request.is_ajax(): return JsonResponse({'result': 'error'}, status=400) return HttpResponseBadRequest(self.url) def serve_post(self, request, *args, **kwargs): """Handle a POST to a specific form on the page. Attempts to retrieve form_id from the POST request, which must be formatted like "form-name-index" where the "name" part is the name of a StreamField on the page and the "index" part refers to the index of the form element in the StreamField. If form_id is found, it returns the response from the block method retrieval. If form_id is not found, it returns an error response. """ form_module = None form_id = request.POST.get('form_id', None) if form_id: form_id_parts = form_id.split('-') if len(form_id_parts) == 3: streamfield_name = form_id_parts[1] streamfield = getattr(self, streamfield_name, None) if streamfield is not None: try: streamfield_index = int(form_id_parts[2]) except ValueError: streamfield_index = None try: form_module = streamfield[streamfield_index] except IndexError: form_module = None if form_module is None: return self._return_bad_post_response(request) result = form_module.block.get_result(self, request, form_module.value, True) if isinstance(result, HttpResponse): return result context = self.get_context(request, *args, **kwargs) context['form_modules'][streamfield_name].update( {streamfield_index: result}) return TemplateResponse(request, self.get_template(request, *args, **kwargs), context) class Meta: app_label = 'v1' def parent(self): parent = self.get_ancestors(inclusive=False).reverse()[0].specific return parent # To be overriden if page type requires JS files every time @property def page_js(self): return [] @property def streamfield_js(self): js = [] block_cls_names = get_page_blocks(self) for block_cls_name in block_cls_names: block_cls = import_string(block_cls_name) if hasattr(block_cls, 'Media') and hasattr(block_cls.Media, 'js'): js.extend(block_cls.Media.js) return js # Returns the JS files required by this page and its StreamField blocks. @property def media(self): return sorted(set(self.page_js + self.streamfield_js)) # Returns an image for the page's meta Open Graph tag @property def meta_image(self): return self.social_sharing_image @property def post_preview_cache_key(self): return 'post_preview_{}'.format(self.id)
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 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 CFGOVPage(Page): authors = ClusterTaggableManager(through=CFGOVAuthoredPages, blank=True, verbose_name='Authors', help_text='A comma separated list of ' + 'authors.', related_name='authored_pages') tags = ClusterTaggableManager(through=CFGOVTaggedPages, blank=True, related_name='tagged_pages') shared = models.BooleanField(default=False) has_unshared_changes = models.BooleanField(default=False) language = models.CharField(choices=ref.supported_languagues, default='en', max_length=2) social_sharing_image = models.ForeignKey( 'v1.CFGOVImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', help_text=( 'Optionally select a custom image to appear when users share this ' 'page on social media websites. Minimum size: 1200w x 630h.')) # This is used solely for subclassing pages we want to make at the CFPB. is_creatable = False objects = CFGOVPageManager() # These fields show up in either the sidebar or the footer of the page # depending on the page type. sidefoot = StreamField([ ('call_to_action', molecules.CallToAction()), ('related_links', molecules.RelatedLinks()), ('related_posts', organisms.RelatedPosts()), ('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) # Panels promote_panels = Page.promote_panels + [ ImageChooserPanel('social_sharing_image'), ] sidefoot_panels = [ StreamFieldPanel('sidefoot'), ] settings_panels = [ MultiFieldPanel(promote_panels, 'Settings'), InlinePanel('categories', label="Categories", max_num=2), FieldPanel('tags', 'Tags'), FieldPanel('authors', 'Authors'), MultiFieldPanel(Page.settings_panels, 'Scheduled Publishing'), FieldPanel('language', 'language'), ] # Tab handler interface guide because it must be repeated for each subclass edit_handler = TabbedInterface([ ObjectList(Page.content_panels, heading='General Content'), ObjectList(sidefoot_panels, heading='Sidebar/Footer'), ObjectList(settings_panels, heading='Configuration'), ]) def get_authors(self): """ Returns a sorted list of authors. Default is alphabetical """ return self.alphabetize_authors() def alphabetize_authors(self): """ Alphabetize authors of this page by last name, then first name if needed """ # First sort by first name author_names = self.authors.order_by('name') # Then sort by last name return sorted(author_names, key=lambda x: x.name.split()[-1]) def generate_view_more_url(self, request): activity_log = CFGOVPage.objects.get(slug='activity-log').specific tags = [] tags = urlencode([('topics', tag) for tag in self.tags.slugs()]) return (get_protected_url({'request': request}, activity_log) + '?' + tags) def related_posts(self, block): from v1.models.learn_page import AbstractFilterPage def tag_set(related_page): return set([tag.pk for tag in related_page.tags.all()]) def match_all_topic_tags(queryset, page_tags): """Return pages that share every one of the current page's tags.""" current_tag_set = set([tag.pk for tag in page_tags]) return [ page for page in queryset if current_tag_set.issubset(tag_set(page)) ] related_types = [] related_items = {} if block.value.get('relate_posts'): related_types.append('blog') if block.value.get('relate_newsroom'): related_types.append('newsroom') if block.value.get('relate_events'): related_types.append('events') if not related_types: return related_items tags = self.tags.all() and_filtering = block.value['and_filtering'] specific_categories = block.value['specific_categories'] limit = int(block.value['limit']) queryset = AbstractFilterPage.objects.live().exclude( id=self.id).order_by('-date_published').distinct() for parent in related_types: # blog, newsroom or events # Include children of this slug that match at least 1 tag children = Page.objects.child_of_q(Page.objects.get(slug=parent)) filters = children & Q(('tags__in', tags)) if parent == 'events': # Include archived events matches archive = Page.objects.get(slug='archive-past-events') children = Page.objects.child_of_q(archive) filters |= children & Q(('tags__in', tags)) if specific_categories: # Filter by any additional categories specified categories = ref.get_appropriate_categories( specific_categories=specific_categories, page_type=parent) if categories: filters &= Q(('categories__name__in', categories)) related_queryset = queryset.filter(filters) if and_filtering: # By default, we need to match at least one tag # If specified in the admin, change this to match ALL tags related_queryset = match_all_topic_tags(related_queryset, tags) related_items[parent.title()] = related_queryset[:limit] # Return items in the dictionary that have non-empty querysets return {key: value for key, value in related_items.items() if value} def related_metadata_tags(self): # Set the tags to correct data format tags = {'links': []} filter_page = self.get_filter_data() for tag in self.specific.tags.all(): tag_link = {'text': tag.name, 'url': ''} if filter_page: relative_url = filter_page.relative_url(filter_page.get_site()) param = '?topics=' + tag.slug tag_link['url'] = relative_url + param tags['links'].append(tag_link) return tags def get_filter_data(self): for ancestor in self.get_ancestors().reverse().specific(): if ancestor.specific_class.__name__ in [ 'BrowseFilterablePage', 'SublandingFilterablePage', 'EventArchivePage', 'NewsroomLandingPage' ]: return ancestor return None def get_breadcrumbs(self, request): ancestors = self.get_ancestors() home_page_children = request.site.root_page.get_children() for i, ancestor in enumerate(ancestors): if ancestor in home_page_children: return [ancestor for ancestor in ancestors[i + 1:]] return [] def get_appropriate_descendants(self, inclusive=True): return CFGOVPage.objects.live().descendant_of(self, inclusive) def get_appropriate_siblings(self, inclusive=True): return CFGOVPage.objects.live().sibling_of(self, inclusive) def get_context(self, request, *args, **kwargs): context = super(CFGOVPage, self).get_context(request, *args, **kwargs) for hook in hooks.get_hooks('cfgovpage_context_handlers'): hook(self, request, context, *args, **kwargs) return context def serve(self, request, *args, **kwargs): """ If request is ajax, then return the ajax request handler response, else return the super. """ if request.method == 'POST': return self.serve_post(request, *args, **kwargs) # Force the page's language on the request translation.activate(self.language) request.LANGUAGE_CODE = translation.get_language() return super(CFGOVPage, self).serve(request, *args, **kwargs) def _return_bad_post_response(self, request): if request.is_ajax(): return JsonResponse({'result': 'error'}, status=400) return HttpResponseBadRequest(self.url) def serve_post(self, request, *args, **kwargs): """Handle a POST to a specific form on the page. Attempts to retrieve form_id from the POST request, which must be formatted like "form-name-index" where the "name" part is the name of a StreamField on the page and the "index" part refers to the index of the form element in the StreamField. If form_id is found, it returns the response from the block method retrieval. If form_id is not found, it returns an error response. """ form_module = None form_id = request.POST.get('form_id', None) if form_id: form_id_parts = form_id.split('-') if len(form_id_parts) == 3: streamfield_name = form_id_parts[1] streamfield = getattr(self, streamfield_name, None) if streamfield is not None: try: streamfield_index = int(form_id_parts[2]) except ValueError: streamfield_index = None try: form_module = streamfield[streamfield_index] except IndexError: form_module = None if form_module is None: return self._return_bad_post_response(request) result = form_module.block.get_result(self, request, form_module.value, True) if isinstance(result, HttpResponse): return result context = self.get_context(request, *args, **kwargs) context['form_modules'][streamfield_name].update( {streamfield_index: result}) return TemplateResponse(request, self.get_template(request, *args, **kwargs), context) class Meta: app_label = 'v1' def parent(self): parent = self.get_ancestors(inclusive=False).reverse()[0].specific return parent # To be overriden if page type requires JS files every time @property def page_js(self): return [] @property def streamfield_js(self): js = [] block_cls_names = get_page_blocks(self) for block_cls_name in block_cls_names: block_cls = import_string(block_cls_name) if hasattr(block_cls, 'Media') and hasattr(block_cls.Media, 'js'): js.extend(block_cls.Media.js) return js # Returns the JS files required by this page and its StreamField blocks. @property def media(self): return sorted(set(self.page_js + self.streamfield_js)) # Returns an image for the page's meta Open Graph tag @property def meta_image(self): return self.social_sharing_image @property def post_preview_cache_key(self): return 'post_preview_{}'.format(self.id)
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).")) 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