class ArticlePage(Page): image = models.ForeignKey('wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+') body = StreamField( [('paragraph', blocks.RichTextBlock(label='Editor de texto')), ('html', blocks.RawHTMLBlock(label='HTML')), ('md', MarkdownBlock(label='Markdown')), ('notebook', NotebookBlock())], null=True, blank=True) content_panels = Page.content_panels + [ ImageChooserPanel('image'), StreamFieldPanel('body'), ] parent_page_types = ['article.IndexPage'] subpage_types = [] class Meta: verbose_name = 'Articulo' verbose_name_plural = 'Articulos' def get_context(self, request): context = super().get_context(request) site = Site.find_for_request(request) context['related_articles'] = ArticlePage.objects.in_site( site).live().exact_type(ArticlePage).order_by('?')[:3] return context
class StandardPage(Page): TEMPLATE_CHOICES = [ ('pages/standard_page.html', 'Default Template'), ('pages/standard_page_full.html', 'Standard Page Full'), ] subtitle = models.CharField(max_length=255, blank=True) intro = RichTextField(blank=True) body = StreamField([ ('paragraph', blocks.RichTextBlock()), ('image', ImageChooserBlock()), ('markdown', MarkdownBlock(icon="code")), ('html', blocks.RawHTMLBlock()), ]) template_string = models.CharField(max_length=255, choices=TEMPLATE_CHOICES, default='pages/standard_page.html') feed_image = models.ForeignKey(Image, null=True, blank=True, on_delete=models.SET_NULL, related_name='+') search_fields = Page.search_fields + [ index.SearchField('intro'), index.SearchField('body'), ] @property def template(self): return self.template_string
class PulloutBlock(StructBlock): text = MarkdownBlock() stat_header = CharBlock() stat_text = CharBlock() class Meta: template = "industry/blocks/pullout.html"
class WorkExperienceBlock(blocks.StructBlock): class Meta: template = "wagtail_resume/blocks/work_experience_block.html" icon = "doc-full-inverse" heading = blocks.CharBlock(default="Work experience") fa_icon = blocks.CharBlock(default="fas fa-tools") experiences = blocks.ListBlock( blocks.StructBlock( [ ("role", blocks.CharBlock()), ("company", blocks.CharBlock()), ("url", blocks.URLBlock()), ("from_date", blocks.DateBlock()), ("to_date", blocks.DateBlock(required=False)), ( "currently_working_here", blocks.BooleanBlock( help_text= "Check this box if you are currently working here and it will indicate so on the resume.", required=False, ), ), ("text", MarkdownBlock()), ], icon="folder-open-inverse", ))
class MarkdownAccordionItemBlock(StructBlock): class Meta: template = 'blocks/accordion_item_markdown.html' # accordion section title = CharBlock(max_length=255) content = MarkdownBlock()
class LocationAccordionItemBlock(StructBlock): class Meta: template = 'blocks/accordion_item_location.html' # accordion section title = CharBlock(max_length=255) info = MarkdownBlock() map = ImageChooserBlock()
class BlogPage(RoutablePageMixin, Page): intro = RichTextField() body = StreamField([ ('paragraph', blocks.RichTextBlock()), ('markdown', MarkdownBlock(icon="code")), ('image', ImageChooserBlock()), ('html', blocks.RawHTMLBlock()), ]) tags = ClusterTaggableManager(through=BlogPageTag, blank=True) date = models.DateField("Post date") feed_image = models.ForeignKey('wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+') search_fields = Page.search_fields + [ index.SearchField('body'), ] parent_page_types = ['blog.BlogIndexPage'] @property def blog_index(self): # Find closest ancestor which is a blog index return self.get_ancestors().type(BlogIndexPage).last() @route(r'^$') def normal_page(self, request): return Page.serve(self, request) @route(r'^amp/$') def amp(self, request): context = self.get_context(request) body_html = self.body.__html__() soup = BeautifulSoup(body_html, 'html.parser') # Remove style attribute to remove large bottom padding for div in soup.find_all("div", {'class': 'responsive-object'}): del div['style'] # Change img tags to amp-img for img_tag in soup.findAll('img'): img_tag.name = 'amp-img' img_tag.append(BeautifulSoup('</amp-img>', 'html.parser')) img_tag['layout'] = 'responsive' # Change iframe tags to amp-iframe for iframe in soup.findAll('iframe'): iframe.name = 'amp-iframe' iframe['sandbox'] = 'allow-scripts allow-same-origin' iframe['layout'] = 'responsive' context['body_html'] = mark_safe(soup.prettify(formatter="html")) context['is_amp'] = True context['base_template'] = 'amp_base.html' response = TemplateResponse(request, 'blog/blog_page_amp.html', context) return response
class BodyBlock(StreamBlock): h1 = CharBlock() h2 = CharBlock() paragraph = RichTextBlock() markdown = MarkdownBlock(icon="code") image_text = ImageText() image_carousel = ListBlock(ImageChooserBlock()) thumbnail_gallery = ListBlock(ImageChooserBlock())
class BlogBodyBlock(StructBlock): """ Custom `StructBlock` to organize blog articles' bodies into sections, each with a title, used to create a TOC """ title = CharBlock(required=False) content = MarkdownBlock(required=False) class Meta: icon = 'fa-paragraph' templates = 'blocks/blog_body_block.html'
def test_that_article_intro_should_strip_markdown_syntax(self): paragraph_string = "###Lorem ipsum `dolor` sit amet, **consectetur** adipiscing [elit](http://www.google.com). \r\n* Nullam *laoreet* venenatis enim, non luctus nisi finibus ut.\r\n> Mauris porta eleifend massa, nec maximus lacus luctus a. Aenean libero felis, placerat non malesuada a, maximus id erat. Nulla ut" expected_string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam laoreet venenatis enim, non luctus nisi finibus ut. Mauris porta eleifend massa, nec maximus lacus luctus a. Aenean libero felis, placerat non malesuada a, maximus id erat. Nulla ut" body_block = StreamBlock([(ArticleBodyBlockNames.MARKDOWN.value, MarkdownBlock())]) body = StreamValue( body_block, [(ArticleBodyBlockNames.MARKDOWN.value, paragraph_string)]) blog_article = self._create_blog_article_page(body=body) self.assertEqual(blog_article.intro, expected_string + INTRO_ELLIPSIS)
def test_that_article_intro_should_support_markdown_blocks(self): paragraph_string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam laoreet venenatis enim, non luctus nisi finibus ut. Mauris porta eleifend massa, nec maximus lacus luctus a. Aenean libero felis, placerat non malesuada a, maximus id erat. Nulla ut purus elementum, auctor orci eget, facilisis est." expected_string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam laoreet venenatis enim, non luctus nisi finibus ut. Mauris porta eleifend massa, nec maximus lacus luctus a. Aenean libero felis, placerat non malesuada a, maximus id erat. Nulla ut" body_block = StreamBlock([(ArticleBodyBlockNames.MARKDOWN.value, MarkdownBlock())]) body = StreamValue( body_block, [(ArticleBodyBlockNames.MARKDOWN.value, paragraph_string)]) blog_article = self._create_blog_article_page(body=body) self.assertEqual(blog_article.intro, expected_string + INTRO_ELLIPSIS)
class StoryBlock(StreamBlock): h2 = CharBlock( form_classname="title", icon="title", template="patterns/molecules/streamfield/blocks/heading2_block.html", ) h3 = CharBlock( form_classname="title", icon="title", template="patterns/molecules/streamfield/blocks/heading3_block.html", ) h4 = CharBlock( form_classname="title", icon="title", template="patterns/molecules/streamfield/blocks/heading4_block.html", ) intro = RichTextBlock( icon="pilcrow", template="patterns/molecules/streamfield/blocks/intro_block.html", ) paragraph = RichTextBlock( icon="pilcrow", template="patterns/molecules/streamfield/blocks/paragraph_block.html", ) aligned_image = ImageBlock( label="Aligned image", template= "patterns/molecules/streamfield/blocks/aligned_image_block.html", ) wide_image = WideImage( label="Wide image", template="patterns/molecules/streamfield/blocks/wide_image_block.html", ) bustout = BustoutBlock( template="patterns/molecules/streamfield/blocks/bustout_block.html") pullquote = PullQuoteBlock( template="patterns/molecules/streamfield/blocks/pullquote_block.html") raw_html = RawHTMLBlock( label="Raw HTML", icon="code", template="patterns/molecules/streamfield/blocks/raw_html_block.html", ) embed = EmbedBlock( icon="code", template="patterns/molecules/streamfield/blocks/embed_block.html", ) markdown = MarkdownBlock( icon="code", template="patterns/molecules/streamfield/blocks/markdown_block.html", ) class Meta: template = "patterns/molecules/streamfield/stream_block.html"
class StoryBlock(StreamBlock): h2 = CharBlock(icon="title", classname="title") h3 = CharBlock(icon="title", classname="title") h4 = CharBlock(icon="title", classname="title") intro = RichTextBlock(icon="pilcrow") paragraph = RichTextBlock(icon="pilcrow") aligned_image = ImageBlock(label="Aligned image") wide_image = WideImage(label="Wide image") bustout = BustoutBlock() pullquote = PullQuoteBlock() raw_html = RawHTMLBlock(label='Raw HTML', icon="code") embed = EmbedBlock(icon="code") markdown = MarkdownBlock(icon="code")
class PostPage(Page): excerpt = MarkdownField() featured = models.BooleanField(default=False) body = StreamField([ ('image', ImageChooserBlock()), ('html', blocks.RawHTMLBlock()), ('embed', EmbedBlock()), ('paragraph', MarkdownBlock(icon="code")), ]) date = models.DateTimeField(verbose_name="Post date", default=datetime.datetime.today) header_image = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', ) header_image_link = models.CharField(max_length=255, blank=True) categories = ParentalManyToManyField('blog.BlogCategory', blank=True) tags = ClusterTaggableManager(through='blog.BlogPageTag', blank=True) content_panels = Page.content_panels + [ StreamFieldPanel("body"), ImageChooserPanel('header_image'), FieldPanel('header_image_link'), FieldPanel('excerpt'), FieldPanel('categories', widget=forms.CheckboxSelectMultiple), #FieldPanel('tags'), FieldPanel('featured'), ] settings_panels = Page.settings_panels + [ FieldPanel('date'), ] @property def blog_page(self): return self.get_parent().specific def get_context(self, request, *args, **kwargs): context = super(PostPage, self).get_context(request, *args, **kwargs) context['blog_page'] = self.blog_page context['post'] = self return context def get_absolute_url(self): return "/news/%s/" % self.slug
class SectorPage(Page): # Related sector are implemented as subpages subpage_types = ['sector.sectorPage'] featured = models.BooleanField(default=False) description = models.TextField() # appears in card on external pages # page fields heading = models.CharField(max_length=255) hero_image = models.ForeignKey('wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+') pullout = StreamField([('content', StructBlock([('text', MarkdownBlock()), ('stat', CharBlock()), ('stat_text', CharBlock())], max_num=1, min_num=0))], blank=True) # accordion subsections = StreamField([ ('markdown', MarkdownAccordionItemBlock()), ('location', LocationAccordionItemBlock()), ]) content_panels = Page.content_panels + [ FieldPanel('description'), FieldPanel('featured'), ImageChooserPanel('hero_image'), FieldPanel('heading'), StreamFieldPanel('pullout'), StreamFieldPanel('subsections') ] def get_context(self, request): context = super().get_context(request) context['sector_cards'] = self.get_children().type(SectorPage) \ .live() \ .order_by('sectorpage__heading') # pages will return as Page type, use .specific to get sectorPage return context
class BaseStreamBlock(StreamBlock): """ Define the custom blocks that `StreamField` will utilize """ markdown_block = MarkdownBlock(icon="code") heading_block = HeadingBlock() paragraph_block = RichTextBlock( icon="fa-paragraph", template="blocks/paragraph_block.html", ) image_block = ImageBlock() block_quote = BlockQuote() embed_block = EmbedBlock( help_text= 'Insert an embed URL e.g https://www.youtube.com/embed/SGJFWirQ3ks', icon="fa-s15", template="blocks/embed_block.html")
class StandardPage(Page): TEMPLATE_CHOICES = [ ('pages/standard_page_full.html', 'Optional custom sidebar'), ('pages/standard_page.html', 'Newsfeed sidebar'), ] subtitle = models.CharField( max_length=255, blank=True, help_text="This will override the title of the page.") intro = RichTextField(blank=True) midpage_subtitle = models.CharField( max_length=255, blank=True, help_text="This will override the title of the page.") body = StreamField([ ('paragraph', blocks.RichTextBlock()), ('image', ImageChooserBlock()), ('markdown', MarkdownBlock(icon="code")), ('html', blocks.RawHTMLBlock()), ]) template_string = models.CharField(max_length=255, choices=TEMPLATE_CHOICES, default=TEMPLATE_CHOICES[0][0], verbose_name='Page Layout') feed_image = models.ForeignKey(Image, null=True, blank=True, on_delete=models.SET_NULL, related_name='+') sidebar_text = RichTextField( blank=True, help_text= "only include text/images in here if you want the side bar, otherwise it will render full page" ) search_fields = Page.search_fields + [ index.SearchField('intro'), index.SearchField('body'), ] @property def template(self): return self.template_string
class IndustryPage(Page): """ header: - lockup text - hero image intro - pullout text - pullout star content made up of sections next steps this is basically a snippet but has formatting """ # header # heading lockup # hero image body = StreamField([ ('heading', HeaderBlock()), ('pullout', PulloutBlock()), ('content', MarkdownBlock()), ('common_content', IndustrySnippetBlock(target_model=StaticContent)), ]) report_problem = models.ForeignKey(StaticContent, on_delete=CASCADE, null=True, related_name='report_problem') sharing_text = models.ForeignKey(StaticContent, on_delete=CASCADE, null=True, related_name='sharing_text') content_panels = Page.content_panels + [ StreamFieldPanel('body'), SnippetChooserPanel('report_problem'), SnippetChooserPanel('sharing_text'), ]
def _create_blog_article_page( self, blog_index_page=None, title="Simple Article Title", page_title="Simple Article Title", date=datetime.now(), body=None, author=None, read_time=7, table_of_contents=False, recommended_articles=None, views=0, cover_photo=None, article_photo=None, is_main_article=False, ): if body is None: block = StreamBlock([(ArticleBodyBlockNames.MARKDOWN.value, MarkdownBlock())]) body = StreamValue( block, [(ArticleBodyBlockNames.MARKDOWN.value, "Hello, World")]) if author is None: author = BossFactory() blog_article_page = BlogArticlePage( title=title, page_title=page_title, date=date, body=body, author=author, read_time=read_time, table_of_contents=table_of_contents, recommended_articles=recommended_articles, views=views, cover_photo=cover_photo, article_photo=article_photo, is_main_article=is_main_article, ) if blog_index_page is None: blog_index_page = self._get_newest_blog_index_page() blog_index_page.add_child(instance=blog_article_page) blog_article_page.save() return blog_article_page
class WorkExperienceBlock(blocks.StructBlock): class Meta: template = "wagtail_resume/blocks/work_experience_block.html" icon = "doc-full-inverse" heading = blocks.CharBlock(default="Work experience") fa_icon = blocks.CharBlock(default="fas fa-tools") experiences = blocks.ListBlock( blocks.StructBlock( [ ("role", blocks.CharBlock()), ("company", blocks.CharBlock()), ("url", blocks.URLBlock()), ("from_date", blocks.DateBlock()), ("to_date", blocks.DateBlock()), ("text", MarkdownBlock()), ], icon="folder-open-inverse", ) )
class BlogPage(Page): """ This is the core of the Blog app. BlogPage are individual articles """ subtitle = models.CharField(blank=True, max_length=255) body = MarkdownField(blank=True) extended_body = StreamField([ ('content', MarkdownBlock(template='blog/markdown_block.html')), ('newsletter', NewsletterSubscribe()), ('book', BookInline()), ], null=True) image = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', help_text='Header Image, used also for social sharing') image_data = RichTextField( blank=True, null=True, help_text= 'Information about the header image, to appear after the article') tags = ClusterTaggableManager(through=BlogPageTag, blank=True) date_published = models.DateField("Date article published", blank=True, null=True) allow_comments = models.BooleanField(default=True) content_panels = Page.content_panels + [ FieldPanel('subtitle'), StreamFieldPanel('extended_body'), MarkdownPanel('body'), ImageChooserPanel('image'), FieldPanel('image_data'), FieldPanel('date_published'), InlinePanel('blog_person_relationship', label="Author(s)", panels=None, min_num=1), FieldPanel('tags'), ] promote_panels = Page.promote_panels + [ FieldPanel('allow_comments'), ] search_fields = Page.search_fields + [ index.SearchField('body'), ] def authors(self): """ Returns the BlogPage's related People. Again note that we are using the ParentalKey's related_name from the BlogPeopleRelationship model to access these objects. This allows us to access the People objects with a loop on the template. If we tried to access the blog_person_ relationship directly we'd print `blog.BlogPeopleRelationship.None` """ authors = [n.people for n in self.blog_person_relationship.all()] return authors @property def first_author(self): return self.authors()[-1] @property def get_tags(self): """ Similar to the authors function above we're returning all the tags that are related to the blog post into a list we can access on the template. We're additionally adding a URL to access BlogPage objects with that tag """ tags = self.tags.all() for tag in tags: tag.url = '/' + '/'.join( s.strip('/') for s in [self.get_parent().url, 'tags', tag.slug]) return tags # Specifies parent to BlogPage as being BlogIndexPages parent_page_types = ['BlogIndexPage'] # Specifies what content types can exist as children of BlogPage. # Empty list means that no child content types are allowed. subpage_types = [] def get_absolute_url(self): return self.specific.url def get_context(self, request): context = super(BlogPage, self).get_context(request) context['latest_articles'] = BlogPage.objects.live().order_by( '-date_published')[:5] context['intro_class'] = 'blog article' return context @property def introduction(self): html = render_markdown(self.body) soup = BeautifulSoup(html, "html.parser") try: introduction = soup.find('p').text return introduction except AttributeError: return None class Meta: ordering = ['-date_published']
class BlogDetailPage(Page): template = "blog/blog_detail_page.html" custom_title = models.CharField('Title', max_length=80, help_text='文章标题') author = models.CharField("Author", max_length=255, default="Wang Zhenxuan") create_date = models.DateField("Create date", auto_now_add=True) update_date = models.DateField("Update date", auto_now=True) intro = RichTextField(max_length=250, help_text='文章简介') tags = ClusterTaggableManager(through=BlogPageTag, blank=True) #categories = ParentalManyToManyField('blog.BlogCategory', blank=True) content = StreamField( [ ('heading', blocks.CharBlock(classname="full title")), ('paragraph', blocks.RichTextBlock()), ('image', ImageChooserBlock()), ('blockquote', blocks.BlockQuoteBlock(label='Block Quote')), ('documentchooser', DocumentChooserBlock(label='Document Chooser')), ('url', blocks.URLBlock(label='URL')), ('embed', EmbedBlock(label='Embed')), #('snippetchooser', SnippetChooserBlock(label='Snippet Chooser')), ('rawhtml', blocks.RawHTMLBlock(label='Raw HTML')), ('table', TableBlock(label='Table')), ('markdown', MarkdownBlock(label='Markdown')), ('code', CodeBlock(label='Code')), ('imagedeck', CardBlock(label='Imagedeck')), ], null=True, blank=True, ) def main_image(self): gallery_item = self.gallery_images.first() if gallery_item: return gallery_item.image else: return None def prev(self): try: previous = self.get_next_sibling() return (previous) except self.DoesNotExist: return (None) def next(self): try: return (self.get_prev_sibling()) except self.DoesNotExist: return (None) search_fields = Page.search_fields + [ index.SearchField('custom_title'), index.SearchField('intro'), index.SearchField('content'), index.SearchField('create_date'), ] content_panels = Page.content_panels + [ MultiFieldPanel( [ FieldPanel('custom_title'), FieldPanel('intro'), FieldPanel('author'), #FieldPanel('create_date'), #FieldPanel('update_date'), FieldPanel('tags'), #FieldPanel('categories', widget=forms.CheckboxSelectMultiple), ], heading="Blog information"), InlinePanel('gallery_images', label="Gallery images"), # FieldPanel('body'), StreamFieldPanel('content'), ] class Meta: ordering = ['-create_date']
def test_markdown_block(self): block = MarkdownBlock() self.assertEqual(block.render_basic("# hello"), "<h1>hello</h1>")
class BlogDetailPage(Page): template = "blog/blog_detail_page.html" def get_context(self, request): authorname = self.author.get_fullname_or_username() data = count_visits(request, self) context = super().get_context(request) context['client_ip'] = data['client_ip'] context['location'] = data['location'] context['total_hits'] = data['total_hits'] context['total_visitors'] = data['total_vistors'] context['cookie'] = data['cookie'] context['author'] = authorname return context def serve(self, request): context = self.get_context(request) template = self.get_template(request) response = render(request, template, context) response.set_cookie(context['cookie'], 'true', max_age=300) return response custom_title = models.CharField('Title', max_length=60, help_text='文章标题') author = models.ForeignKey(User, on_delete=models.PROTECT) create_date = models.DateField("Create date", auto_now_add=True) update_date = models.DateField("Update date", auto_now=True) intro = models.CharField('Introduction', max_length=500, help_text='文章简介') tags = ClusterTaggableManager(through=BlogPageTag, blank=True) categories = ParentalManyToManyField('blog.BlogCategory', blank=True) content = CustomStreamField( [ ('heading', blocks.CharBlock(form_classname="full title")), ('paragraph', blocks.RichTextBlock()), ('image', ImageChooserBlock()), ('blockquote', blocks.BlockQuoteBlock(label='Block Quote')), ('documentchooser', DocumentChooserBlock(label='Document Chooser')), ('url', blocks.URLBlock(label='URL')), ('embed', EmbedBlock(label='Embed', width=800)), #('snippetchooser', SnippetChooserBlock(label='Snippet Chooser')), ('rawhtml', blocks.RawHTMLBlock(label='Raw HTML')), ('table', TableBlock(label='Table')), ('markdown', MarkdownBlock(label='Markdown')), ('equation', MathBlock(label='Equation')), ('code', CodeBlock(label='Code')), ('imagedeck', CardBlock(label='Imagedeck')), ], null=True, blank=True, ) def main_image(self): gallery_item = self.gallery_images.first() if gallery_item: return gallery_item.image else: return None def prev(self): try: previous = self.get_next_sibling() return (previous) except self.DoesNotExist: return (None) def next(self): try: return (self.get_prev_sibling()) except self.DoesNotExist: return (None) def get_url(self): return self.url def get_user(self): return self.author def get_email(self): return self.author.email search_fields = Page.search_fields + [ index.SearchField('custom_title'), index.SearchField('intro'), index.SearchField('content'), index.SearchField('create_date'), index.SearchField('tags'), ] content_panels = Page.content_panels + [ InlinePanel('gallery_images', label="Gallery images"), MultiFieldPanel( [ FieldPanel('custom_title'), FieldPanel('intro'), FieldPanel('author'), #FieldPanel('create_date'), #FieldPanel('update_date'), FieldPanel('tags'), FieldPanel('categories', widget=forms.CheckboxSelectMultiple), ], heading="Blog information"), # FieldPanel('body'), StreamFieldPanel('content'), ] class Meta: ordering = ['-create_date']
class MyStreamBlock(StreamBlock): markdown = MarkdownBlock(icon="code")
class StoryPage(Page): MAX_RELATED_STORIES = 10 # This is a leaf page subpage_types = [] parent_page_types = [ 'news.StoryIndexPage' ] author = models.ForeignKey( 'common.User', null=True, blank=True, on_delete=models.SET_NULL, related_name="+" ) date = models.DateField("post date") lede = models.CharField( max_length=1024, help_text="A short intro that appears in the story index page" ) tags = ClusterTaggableManager(through=StoryTag, blank=True) categories = ParentalManyToManyField( 'news.StoryCategory', blank=True, help_text="The set of categories this page will be served" ) # Add allowed block types to StreamPanel content = StreamField( [ ('paragraph', RichTextBlock(features=[ 'h2', 'h3', 'bold', 'italic', 'link', 'ol', 'ul' ])), ('markdown', MarkdownBlock(icon='code')), ('image', StructBlock([ ('image', ImageChooserBlock()), ('caption', CharBlock(required=False)) ])), ('carousel', ImageCarouselBlock()), ('quote', BlockQuoteBlock()), ('embedded_content', EmbedContentBlock()), ] ) def get_context(self, request): context = super().get_context(request) tags_list = list(self.tags.all()) # Collects set of stories that has the same tags as this story. related = StoryPage.objects.live().order_by('-first_published_at') related = related.filter(tags__in=tags_list) related = related.exclude(id=self.id) if related.count() > 0: related = related.distinct()[0:self.MAX_RELATED_STORIES] context["related"] = related try: context['prevstory'] = self.get_previous_by_date(tags__in=tags_list) except (StoryPage.DoesNotExist, ValueError): pass try: context['nextstory'] = self.get_next_by_date(tags__in=tags_list) except (StoryPage.DoesNotExist, ValueError): pass return context def hero_image(self): gallery_item = self.gallery_images.first() if gallery_item: return gallery_item.image else: return None def media_thumbnail_url(self): for child in self.content: if child.block.name == 'embedded_content': return child.value.thumbnail_url return None def hero_image_url(self): hero_image = self.hero_image() if hero_image: return hero_image.url return self.media_thumbnail_url() def author_name(self): if not self.author: return ANONYMOUS_AUTHOR_NAME else: return self.owner.username search_fields = Page.search_fields = [ index.SearchField('lede'), index.SearchField('body'), ] content_panels = Page.content_panels + [ MultiFieldPanel([ StoryAuthorFieldPanel('author', widget=forms.Select), FieldPanel('date'), FieldPanel('tags'), FieldPanel('categories', widget=forms.CheckboxSelectMultiple), ]), FieldPanel('lede'), InlinePanel('gallery_images', label="Gallery Images"), StreamFieldPanel('content') ]
class BlogArticlePage(MixinSeoFields, Page, MixinPageMethods, GoogleAdsMixin): template = "blog_post.haml" date = models.DateField("Post date") page_title = models.CharField( max_length=MAX_BLOG_ARTICLE_TITLE_LENGTH, help_text=ArticleBlogPageHelpTexts.ARTICLE_PAGE_TITLE.value) body = StreamField([ (ArticleBodyBlockNames.MARKDOWN.value, MarkdownBlock(icon="code")), (ArticleBodyBlockNames.HEADER.value, CharBlock()), (ArticleBodyBlockNames.PARAGRAPH.value, RichTextBlock(features=RICH_TEXT_BLOCK_FEATURES)), (ArticleBodyBlockNames.TABLE.value, TableBlock()), (ArticleBodyBlockNames.IMAGE.value, CaptionedImageBlock()), ], ) search_fields = Page.search_fields + [ index.SearchField("intro"), index.SearchField("body") ] author = models.ForeignKey(Employees, on_delete=models.DO_NOTHING) read_time = models.PositiveIntegerField() table_of_contents = models.BooleanField(default=False) recommended_articles = StreamField( [("page", PageChooserBlock(can_choose_root=False, page_type="blog.BlogArticlePage"))], null=True, blank=True, ) views = models.PositiveIntegerField(default=0) cover_photo = models.ForeignKey("wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+") cover_photo_alt_description = models.CharField(max_length=125, blank=True, default="Open the article") article_photo = models.ForeignKey("wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+") article_photo_alt_description = models.CharField(max_length=125, blank=True, default="") is_main_article = models.BooleanField(default=False) Page._meta.get_field( "title").help_text = ArticleBlogPageHelpTexts.PAGE_TITLE.value content_panels = Page.content_panels + [ FieldPanel("page_title", classname="title full"), FieldPanel("date"), FieldPanel("author"), FieldPanel("read_time"), StreamFieldPanel("recommended_articles"), FieldPanel("views"), FieldPanel("is_main_article"), ImageChooserPanel("cover_photo"), FieldPanel("cover_photo_alt_description"), ImageChooserPanel("article_photo"), FieldPanel("article_photo_alt_description"), FieldPanel("table_of_contents"), StreamFieldPanel("body"), ] @cached_property def headers_list(self) -> List[str]: list_of_headers = [] for stream_child in self.body: # pylint: disable=not-an-iterable if stream_child.block.name == ArticleBodyBlockNames.HEADER.value: list_of_headers.append(stream_child.value) return list_of_headers def get_header_id(self, title: str) -> int: return self.headers_list.index(title) @cached_property def intro(self) -> str: paragraph_text = self._get_text_for_intro( MAX_BLOG_ARTICLE_INTRO_LENGTH) if len(paragraph_text) == 0: return "Article intro not available." words_cycle = cycle(paragraph_text.split()) intro_text = self._concatenate_intro_text_from_paragraphs_text( words_cycle, MAX_BLOG_ARTICLE_INTRO_LENGTH) end_ellipsis = INTRO_ELLIPSIS return intro_text + end_ellipsis def _get_text_for_intro(self, character_limit: int) -> str: text_blocks: list = self._get_list_of_text_blocks() paragraphs_text = "" if len(text_blocks) == 0: return paragraphs_text blocks_cycle = cycle(text_blocks) while len(paragraphs_text) < character_limit: paragraphs_text = self._extract_paragraph_text_from_block( blocks_cycle, paragraphs_text) return paragraphs_text def _get_list_of_text_blocks(self) -> list: whitelisted_block_names = [ ArticleBodyBlockNames.PARAGRAPH.value, ArticleBodyBlockNames.MARKDOWN.value ] return list( filter( lambda body_element: body_element.block.name in whitelisted_block_names, self.body)) @staticmethod def _extract_paragraph_text_from_block(blocks: cycle, text: str) -> str: space_between_texts = " " next_block = next(blocks) if next_block.block.name == ArticleBodyBlockNames.MARKDOWN.value: source_text = "".join( BeautifulSoup(markdown(next_block.value), "html.parser").findAll(text=True)) else: source_text = next_block.value.source next_text = strip_tags(source_text) if len(text) == 0: text = next_text else: text += f"{space_between_texts}{next_text}" return text def _concatenate_intro_text_from_paragraphs_text( self, words_cycle: cycle, character_limit: int) -> str: intro_text = "" new_text = next(words_cycle) while len(new_text) < character_limit: intro_text = new_text new_text = self._concatenate_strings(intro_text, words_cycle) return intro_text @staticmethod def _concatenate_strings(text: str, words: cycle) -> str: space_between_texts = " " text += f"{space_between_texts}{next(words)}" return text def get_proper_url(self) -> str: return self.slug def get_absolute_url(self) -> str: return self.url_path def get_context(self, request: WSGIRequest, *args: Any, **kwargs: Any) -> dict: context = super().get_context(request, *args, **kwargs) self._increase_view_counter() context["URL_PREFIX"] = settings.URL_PREFIX context["article_body_block_names"] = ArticleBodyBlockNames context["GOOGLE_ADS_CONVERSION_ID"] = settings.GOOGLE_ADS_CONVERSION_ID context["GOOGLE_TAG_MANAGER_ID"] = settings.GOOGLE_TAG_MANAGER_ID return context def _increase_view_counter(self) -> None: # increase page view counter self.views += 1 self.full_clean() self.save() def save(self, *args: Any, **kwargs: Any) -> None: # pylint: disable=signature-differs if not BlogArticlePage.objects.filter( is_main_article=True) and not self.is_main_article: self.is_main_article = True if self.is_main_article: try: article = BlogArticlePage.objects.get( is_main_article=self.is_main_article) article.is_main_article = False article.save() except BlogArticlePage.DoesNotExist: pass if self.table_of_contents and len(self.headers_list) == 0: self.table_of_contents = False self._validate_parent_page() super().save(*args, **kwargs) def clean(self) -> None: super().clean() self._clean_recommended_articles() self._validate_recommended_articles_uniqueness() def _clean_recommended_articles(self) -> None: self.recommended_articles = StreamValue( stream_block=StreamBlock([("page", PageChooserBlock())]), stream_data=[("page", stream_child.value) for stream_child in self.recommended_articles if stream_child.value is not None], ) def _validate_parent_page(self) -> None: if not isinstance(self.get_parent().specific, BlogIndexPage): raise ValidationError( message=f"{self.title} must be child of BlogIndexPage") def _validate_recommended_articles_uniqueness(self) -> None: article_pages_set = set() for stream_child in self.recommended_articles: # pylint: disable=not-an-iterable if stream_child.value in article_pages_set: raise ValidationError( message=f"'{stream_child.value}' is listed more than once!" ) article_pages_set.add(stream_child.value)