class VideoPage(CoderedArticlePage): class Meta: verbose_name = "Video Page" ordering = [ "-first_published_at", ] # Override to have default today date_display = models.DateTimeField(verbose_name=_("Publish date"), default=timezone.now) link_youtube = models.URLField(max_length=511) # Additional attribute hits = models.IntegerField(default=0, editable=False) # Override from streamfield to richtextfield body_text = RichTextField(verbose_name=_("body")) @property def body(self): return self.body_text @body.setter def body(self, body): self.body_text = body # get set def add_hits(self): self.hits += 1 self.save() return "" def get_pub_date(self): """ Gets published date. """ locale.setlocale(locale.LC_ALL, "en_US") if hasattr(self, "date_display") and self.date_display: return self.date_display.strftime("%d %B %Y") return "" template = "video/video_page.html" # Override to become empty layout_panels = [] # Friend panels promote_panels = [ MultiFieldPanel( [ FieldPanel("slug"), FieldPanel("seo_title"), FieldPanel("search_description"), ImageChooserPanel("og_image"), ], _("Page Meta Data"), ), ] # Override to become empty body_content_panels = [] # Override without content walls settings_panels = Page.settings_panels # Override with additional hits attribute content_panels = (Page.content_panels + [FieldPanel("link_youtube")] + [ MultiFieldPanel( [ FieldPanel("author"), FieldPanel("date_display"), ReadOnlyPanel("hits", heading="Hits"), ], _("Publication Info"), ), ] + [ FieldPanel( "body_text", classname="full", ), ]) # Override edit_handler to not contain layout tabs @cached_classmethod def get_edit_handler(cls): # noqa """ Override to "lazy load" the panels overriden by subclasses. """ panels = [ ObjectList( cls.content_panels + cls.body_content_panels + cls.bottom_content_panels, heading=_("Content"), ), ObjectList(cls.classify_panels, heading=_("Classify")), ObjectList(cls.promote_panels, heading=_("SEO"), classname="seo"), ObjectList(cls.settings_panels, heading=_("Settings"), classname="settings"), ] if cls.integration_panels: panels.append( ObjectList( cls.integration_panels, heading="Integrations", classname="integrations", )) return TabbedInterface(panels).bind_to(model=cls) search_fields = CoderedArticlePage.search_fields + [ index.SearchField("body_text", partial_match=True) ] parent_page_types = ["video.VideoIndexPage"]
def test_render_with_panel_overrides(self): """ Check that inline panel renders the panels listed in the InlinePanel definition where one is specified """ speaker_object_list = ObjectList([ InlinePanel('speakers', label="Speakers", panels=[ FieldPanel('first_name', widget=forms.Textarea), ImageChooserPanel('image'), ]), ]).bind_to(model=EventPage, request=self.request) speaker_inline_panel = speaker_object_list.children[0] EventPageForm = speaker_object_list.get_form_class() # speaker_inline_panel should instruct the form class to include a 'speakers' formset self.assertEqual(['speakers'], list(EventPageForm.formsets.keys())) event_page = EventPage.objects.get(slug='christmas') form = EventPageForm(instance=event_page) panel = speaker_inline_panel.bind_to(instance=event_page, form=form) result = panel.render_as_field() # rendered panel should contain first_name rendered as a text area, but no last_name field self.assertIn('<label for="id_speakers-0-first_name">Name:</label>', result) self.assertIn('Father</textarea>', result) self.assertNotIn( '<label for="id_speakers-0-last_name">Surname:</label>', result) # test for #338: surname field should not be rendered as a 'stray' label-less field self.assertTagInHTML('<input id="id_speakers-0-last_name">', result, count=0, allow_extra_attrs=True) self.assertIn('<label for="id_speakers-0-image">Image:</label>', result) self.assertIn('Choose an image', result) # rendered panel must also contain hidden fields for id, DELETE and ORDER self.assertTagInHTML( '<input id="id_speakers-0-id" name="speakers-0-id" type="hidden">', result, allow_extra_attrs=True) self.assertTagInHTML( '<input id="id_speakers-0-DELETE" name="speakers-0-DELETE" type="hidden">', result, allow_extra_attrs=True) self.assertTagInHTML( '<input id="id_speakers-0-ORDER" name="speakers-0-ORDER" type="hidden">', result, allow_extra_attrs=True) # rendered panel must contain maintenance form for the formset self.assertTagInHTML( '<input id="id_speakers-TOTAL_FORMS" name="speakers-TOTAL_FORMS" type="hidden">', result, allow_extra_attrs=True) # render_js_init must provide the JS initializer self.assertIn('var panel = InlinePanel({', panel.render_js_init())
return self.get_ancestors().type(SiteIndexPage).last() SitePage.content_panels = [ FieldPanel('site'), FieldPanel('title', classname="full title"), FieldPanel('date'), FieldPanel('intro', classname="full"), StreamFieldPanel('body'), InlinePanel('carousel_items', label="Carousel items"), InlinePanel('related_links', label="Related links"), GeoPanel('location') ] SitePage.promote_panels = Page.promote_panels + [ ImageChooserPanel('feed_image'), FieldPanel('tags'), ] # Prototype Fossil List Views # class FossilListViewRelatedLink(Orderable, RelatedLink): # page = ParentalKey('FossilListView', related_name='fossil_related_links') # # # class FossilListView(Page): # """ # Type Fossil List View Page # """ # TEMPLATE_CHOICES = [ # ('origins/fossil_list_view.html', 'Default Template'), # ]
class PostPage(Page, HitCountMixin): """Page for posts, the main content of the website.""" # Database fields header_image = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', ) body = StreamField([ ('cut', blocks.CharBlock( classname='full subtitle', help_text= 'After this block the post will be cutted when displayed on the home page. On the post page this field is ignored.' )), ('paragraph', blocks.RichTextBlock()), ('quote', CustomBlockquoteBlock(classname='full')), ('figure', CaptionedImageBlock(label='Figure')), ('embed', EmbedBlock()), ('document', DocumentChooserBlock( help_text= 'All the text in other blocks, which is the same as document title will be replaced with the link to the document.' )), ('markdown', MarkdownxBlock()), ('equation', CaptionedEquationBlock()), ('pages', blocks.PageChooserBlock()), ('columns', TwoColumnBlock()), ('table', CaptionedTableBlock()), ('table_figure', CaptionedImageBlock(label='Table as Figure', icon='table')), ]) pin_on_home = models.BooleanField( default=False, help_text=_('Indicates if the Post is pinned on the Home page.'), verbose_name=_('Pin on Home page')) show_sidebar = models.BooleanField( default=True, help_text= _('Indicates if the sidebar with contents, figures and equations is shown on the page.' ), verbose_name=_('Show sidebar')) show_comments = models.BooleanField( default=True, help_text=_('Indicates if comments are shown on the page.'), verbose_name=_('Show comments')) generate_figure_numbers = models.BooleanField( default=False, help_text= _('Indcates if figure numbers (such as Figure 1) should be generated for Figure block when rendring post.' ), verbose_name=_('Generate figure numbers')) generate_table_numbers = models.BooleanField( default=False, help_text= _('Indicates if table numbers (such as Table 1) should be geberated for Table block when rendering post.' ), verbose_name=_('Generate table numbers')) generate_equation_numbers = models.BooleanField( default=False, help_text= _('Indicates if equation numbers (such as (1)) should be added on the right side of the Equation block when rendering post.' ), verbose_name=_('Generate equation numbers')) categories = ParentalManyToManyField( 'main.BlogCategory', verbose_name=_('Categories'), blank=True, ) tags = ClusterTaggableManager(through='main.PostTag', help_text=None, blank=True) hit_count_generic = GenericRelation( HitCount, object_id_field='pk', related_query_name='hit_count_relation', ) # Search index configuration search_fields = Page.search_fields + [ index.SearchField('body', partial_match=True, boost=4), index.FilterField('pin_on_home'), index.FilterField('categories'), index.FilterField('tags') ] # Editor panels configuration content_panels = Page.content_panels + [StreamFieldPanel('body')] promote_panels = Page.promote_panels + [ ImageChooserPanel('header_image'), FieldPanel('tags'), InlinePanel('blog_categories', label=_('Categories')) ] settings_panels = Page.settings_panels + [ MultiFieldPanel([ FieldPanel('pin_on_home'), FieldPanel('show_sidebar'), FieldPanel('show_comments'), FieldPanel('generate_figure_numbers'), FieldPanel('generate_table_numbers'), FieldPanel('generate_equation_numbers') ], heading=_('Post settings')), MultiFieldPanel([ ReadOnlyPanel('first_published_at', heading='First published at'), ReadOnlyPanel('last_published_at', heading='Last published at'), ReadOnlyPanel('hit_counts', heading='Number of views') ], heading=_('General information')), ] # Parent page / subpage type rules # PostPage can have children PostPages. In this case the parent page is # considered as 'series' type post and the links to the children pages are # generated, when page is accessed. Also the links to parent page and siblings # are rendered on the child page. parent_page_types = ['main.HomePage', 'main.PostPage'] subpage_types = ['main.PostPage'] # Methods def update_body(self): """Updates captions of figures, tables and equations if PostPage settings require so. Collects figures and tables into a separate list to ease the rendering of Graphics sidebar on the PostPage. Collects equations into another separate list to ease the rendering of Equations sidebar on the PostPage.""" fig_idx = 1 tbl_idx = 1 eq_idx = 1 graphics = list() equations = list() for block in self.body: if block.block_type == 'figure': if self.generate_figure_numbers: block.value['caption'] = _('<b>Figure ') + str( fig_idx) + '.</b> ' + block.value['caption'] fig_idx += 1 graphics.append(block) if block.block_type == 'table' or block.block_type == 'table_figure': if self.generate_table_numbers: block.value['caption'] = _('<b>Table ') + str( tbl_idx) + '.</b> ' + block.value['caption'] tbl_idx += 1 graphics.append(block) if block.block_type == 'equation': if self.generate_equation_numbers: block.value['caption'] = _('<b>Equation ') + str( eq_idx) + '.</b> ' + block.value['caption'] eq_idx += 1 equations.append(block) if block.block_type == 'columns': for column in [block.value['left'], block.value['right']]: for col_block in column: if col_block.block_type == 'figure': if self.generate_figure_numbers: col_block.value['caption'] = _( '<b>Figure ') + str( fig_idx ) + '.</b> ' + col_block.value['caption'] fig_idx += 1 graphics.append(col_block) if col_block.block_type == 'table' or col_block.block_type == 'table_figure': if self.generate_table_numbers: col_block.value['caption'] = _( '<b>Table ') + str( tbl_idx ) + '.</b> ' + col_block.value['caption'] tbl_idx += 1 graphics.append(col_block) if col_block.block_type == 'equation': if self.generate_equation_numbers: col_block.value['caption'] = _( '<b>Equation ') + str( eq_idx ) + '.</b> ' + col_block.value['caption'] eq_idx += 1 equations.append(col_block) return graphics, equations def is_series(self): """Verifies that post is series""" # parent_page = self.get_parent().specific if self.get_parent().specific_class == PostPage: # this is the child post of series return True if self.get_descendant_count() > 0: # this is parent post of series return True return False def get_context(self, request, *args, **kwargs): graphics, equations = self.update_body() context = super().get_context(request, *args, **kwargs) context['post'] = self context['graphics'] = graphics context['equations'] = equations context['previous_post'] = PostPage.objects.live().filter( first_published_at__lt=self.first_published_at).order_by( '-first_published_at').first() context['next_post'] = PostPage.objects.live().filter( first_published_at__gt=self.first_published_at).order_by( 'first_published_at').first() context['is_series'] = False # verify post is series parent_page = self.get_parent().specific if parent_page.specific_class == PostPage: # this is a child post from the series context['is_series'] = True context['parent_post'] = parent_page context['child_posts'] = parent_page.get_descendants().live() else: # this is a normal post, check if it is series if self.get_descendant_count() > 0: # this post is the parent post for series context['is_series'] = True context['parent_post'] = self context['child_posts'] = self.get_descendants().live() return context def hit_counts(self): if self.pk is not None: # the page is created and hitcounts make sense return self.hit_count.hits else: return 0 def serve(self, request, *args, **kwargs): hit_count = HitCount.objects.get_for_object(self) ViewHitCountMixin.hit_count(request, hit_count) return super().serve(request, *args, **kwargs)
class PublicationPage(FoundationMetadataPageMixin, Page): """ This is the root page of a publication. From here the user can browse to the various sections (called chapters). It will have information on the publication, its authors, and metadata from it's children TODO: this poem is beautiful, but it may not belong here Publications are collections of Articles Publications can also be broken down into Chapters, which are really just child publication pages Each of those Chapters may have several Articles An Article can only belong to one Chapter/Publication Page """ subpage_types = ['ArticlePage', 'PublicationPage'] hero_image = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='publication_hero_image', verbose_name='Publication Hero Image', ) subtitle = models.CharField( blank=True, max_length=250, ) secondary_subtitle = models.CharField( blank=True, max_length=250, ) publication_date = models.DateField("Publication date", null=True, blank=True) publication_file = models.ForeignKey('wagtaildocs.Document', null=True, blank=True, on_delete=models.SET_NULL, related_name='+') additional_author_copy = models.CharField( help_text="Example: with contributing authors", max_length=100, blank=True, ) notes = RichTextField(blank=True, ) contents_title = models.CharField( blank=True, default="Table of Contents", max_length=250, ) content_panels = Page.content_panels + [ MultiFieldPanel([ FieldPanel('subtitle'), FieldPanel('secondary_subtitle'), FieldPanel('publication_date'), ImageChooserPanel('hero_image'), DocumentChooserPanel('publication_file'), InlinePanel("authors", label="Author"), FieldPanel("additional_author_copy"), ], heading="Hero"), FieldPanel('contents_title'), FieldPanel('notes') ]
class ArticlePage(Page): """ Represents a blog article page. This Page subclass provide a way to define blog article pages through the Wagtail's admin. It defines the basic fields and information that are generally associated with blog pages. """ # Basically a blog article page is characterized by a body field (the actual content of the blog # post), a date and a title (which is provided by the wagtail's Page model). body = RichTextField(verbose_name=_('Introduction')) date = models.DateField(verbose_name=_('Post date'), default=dt.datetime.today) # A blog article page can have an header image that'll be used when rendering the blog post. # It'll also be displayed if the blog post is featured in the home page. header_image = models.ForeignKey( 'wagtailimages.Image', blank=False, null=True, on_delete=models.SET_NULL, related_name='+', verbose_name=_('Header image'), help_text=_('Header image displayed when rendering the page.'), ) ############################## # SEARCH INDEX CONFIGURATION # ############################## search_fields = Page.search_fields + [ index.SearchField('body'), index.FilterField('date'), ] ############################### # EDITOR PANELS CONFIGURATION # ############################### content_panels = Page.content_panels + [ FieldPanel('date'), FieldPanel('body', classname='full'), ImageChooserPanel('header_image'), ] #################################### # PARENT PAGE / SUBPAGE TYPE RULES # #################################### parent_page_types = ['blog.BlogPage'] subpage_types: List[str] = [] class Meta: verbose_name = _('Article') verbose_name_plural = _('Articles') def get_context(self, request, *args, **kwargs): """ Returns a dictionary of variables to bind into the template. """ context = super().get_context(request, *args, **kwargs) # Inserts the top-level blog page into the context. context['blog_page'] = self.get_parent().specific return context
class SimplePage(Page): """ Represents a simple page. """ # A simple page has a single body field, which degines the actual content of the page. body = RichTextField(verbose_name=_('Body')) # A simple page can have an header image that'll be used when rendering the page. header_image = models.ForeignKey( 'wagtailimages.Image', blank=False, null=True, on_delete=models.SET_NULL, related_name='+', verbose_name=_('Header image'), help_text=_('Header image displayed when rendering the page.'), ) # It is possible to block indexation for a specific page by setting the "noindex" option to # true. noindex = models.BooleanField(default=False, verbose_name=_('Disable robots indexation')) ############################### # EDITOR PANELS CONFIGURATION # ############################### content_panels = Page.content_panels + [ FieldPanel('body', classname='full'), ImageChooserPanel('header_image'), ] promote_panels = [ MultiFieldPanel( [ FieldPanel('slug'), FieldPanel('seo_title'), FieldPanel('show_in_menus'), FieldPanel('search_description'), FieldPanel('noindex'), ], _('Common page configuration') ), ] #################################### # PARENT PAGE / SUBPAGE TYPE RULES # #################################### parent_page_types = ['blog.BlogPage'] subpage_types: List[str] = [] class Meta: verbose_name = _('Simple page') verbose_name_plural = _('Simple pages') def get_context(self, request, *args, **kwargs): """ Returns a dictionary of variables to bind into the template. """ context = super().get_context(request, *args, **kwargs) # Inserts the top-level blog page into the context. context['blog_page'] = self.get_parent().specific return context def get_sitemap_urls(self, request=None): """ Returns URLs to include in sitemaps. """ return [] if self.noindex else super().get_sitemap_urls(request=request)
class Team(models.Model): """This class represents a working group within UTN""" class Meta: verbose_name = _('Team') verbose_name_plural = _('Teams') default_permissions = () ordering = ["name_sv"] # ---- General Information ------ name_en = models.CharField( max_length=255, verbose_name=_('English team name'), help_text=_('Enter the name of the team'), blank=False, ) name_sv = models.CharField( max_length=255, verbose_name=_('Swedish team name'), help_text=_('Enter the name of the team'), blank=False, ) name = TranslatedField('name_en', 'name_sv') logo = models.ForeignKey('wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+') description_en = models.TextField( verbose_name=_('English team description'), help_text=_('Enter a description of the team'), blank=True, ) description_sv = models.TextField( verbose_name=_('Swedish team description'), help_text=_('Enter a description of the team'), blank=True, ) description = TranslatedField('description_en', 'description_sv') def __str__(self) -> str: return '{}'.format(self.name) @property def members(self): return get_user_model().objects.filter( application__position__role__teams__pk=self.pk, application__position__term_from__lte=date.today(), application__position__term_to__gte=date.today(), application__status='appointed', ) @property def manual_members(self): members = self.members.values('pk') return get_user_model().objects.filter(groups=self.group).exclude( pk__in=members) # ------ Administrator settings ------ panels = [ MultiFieldPanel([ FieldRowPanel([ FieldPanel('name_en'), FieldPanel('name_sv'), ]), ImageChooserPanel('logo'), FieldPanel('description_en'), FieldPanel('description_sv'), ]) ]
class HeroSection(SectionBase, SectionTitleBlock, ButtonAction, Page): hero_layout = models.CharField( blank=True, max_length=100, verbose_name='Layout', choices=[('simple_centered', 'Simple centered'), ('image_right', 'Image on right')], default='simple_centered', ) hero_first_button_text = models.CharField( blank=True, max_length=100, verbose_name='Hero button text', default='Subscribe', help_text="Leave field empty to hide.", ) hero_second_button_text = models.CharField( blank=True, max_length=100, verbose_name='Hero button text', default='Subscribe', help_text="Leave field empty to hide.", ) hero_image = models.ForeignKey( 'wagtailimages.Image', blank=True, null=True, on_delete=models.SET_NULL, verbose_name='Image', related_name='+', ) hero_image_size = models.CharField( max_length=50, choices=cr_settings['HERO_IMAGE_SIZE_CHOICES'], default=cr_settings['HERO_IMAGE_SIZE_CHOICES_DEFAULT'], verbose_name=('Image size'), ) hero_action_type_1 = models.CharField( max_length=50, choices=cr_settings['HERO_ACTION_TYPE_CHOICES'], default=cr_settings['HERO_ACTION_TYPE_CHOICES_DEFAULT'], verbose_name=('Action type (First)'), ) hero_action_type_2 = models.CharField( max_length=50, choices=cr_settings['HERO_ACTION_TYPE_CHOICES'], default=cr_settings['HERO_ACTION_TYPE_CHOICES_DEFAULT'], verbose_name=('Action type (Second)'), ) hero_buttons = StreamField([('action_button', ActionButton()), ('primary_button', PrimaryButton())], null=True, verbose_name="Buttons", help_text="Please choose Buttons") # basic tab panels basic_panels = Page.content_panels + [ FieldPanel('hero_layout', heading='Layout', classname="title full"), MultiFieldPanel( [ FieldRowPanel([ FieldPanel('hero_layout', classname="col6"), FieldPanel('hero_image_size', classname="col6"), ]), FieldRowPanel([ FieldPanel('section_heading', heading='Heading', classname="col6"), FieldPanel('section_subheading', heading='Subheading', classname="col6"), ]), FieldRowPanel([ FieldPanel('section_description', heading='Description', classname="col6"), ]), FieldPanel('hero_first_button_text'), FieldPanel('hero_second_button_text'), ImageChooserPanel('hero_image'), ], heading='Content', ), SectionBase.section_layout_panels, SectionBase.section_design_panels, ] # advanced tab panels advanced_panels = (SectionTitleBlock.title_basic_panels, ) + ButtonAction.button_action_panels # Register Tabs edit_handler = TabbedInterface([ ObjectList(basic_panels, heading="Basic"), ObjectList(advanced_panels, heading="Plus+"), ]) # Page settings template = 'sections/hero_section_preview.html' parent_page_types = ['home.HomePage'] subpage_types = [] # Overring methods def set_url_path(self, parent): """ Populate the url_path field based on this page's slug and the specified parent page. (We pass a parent in here, rather than retrieving it via get_parent, so that we can give new unsaved pages a meaningful URL when previewing them; at that point the page has not been assigned a position in the tree, as far as treebeard is concerned. """ if parent: self.url_path = '' else: # a page without a parent is the tree root, which always has a url_path of '/' self.url_path = '/' return self.url_path
class Content(CreateMixin, ClusterableModel): author = models.ForeignKey(User, on_delete=models.DO_NOTHING, null=True) image = models.ForeignKey('wagtailimages.Image', on_delete=models.DO_NOTHING, related_name='+', verbose_name="Imagen") title = models.CharField(max_length=250, verbose_name='Título') body = RichTextField(blank=True, verbose_name="Descripción") tags = TaggableManager(through=ContentTags, blank=True) publish_at = models.DateTimeField('Publicar el', default=now) unpublish_at = models.DateTimeField('Despublicar el', null=True, blank=True) pinned = models.BooleanField( 'Destacar', default=False, help_text='Destacar el contenido para que aparezca al comienzo.') search_fields = [ index.SearchField('body'), ] panels = [ FieldPanel('title', classname='title'), FieldPanel('body', classname="full"), ImageChooserPanel('image', heading='heading'), FieldPanel('pinned', classname="full"), ] end_panels = [ ReadOnlyPanel('author', heading="Autor"), FieldPanel('tags'), MultiFieldPanel( [ FieldRowPanel([ FieldPanel( 'publish_at', classname="col6", heading='heading'), FieldPanel('unpublish_at', classname="col6"), ]) ], heading="Publicar automáticamente", help_text= 'Agenda este contenido para ser publicado y/o despublicado automáticamente. Si el campo \'despublicar\' está vacío no se agendará la despublicación.' ), UnnorderedInlinePanel( 'notifications', label="Notificación automática", help_text= 'Selecciona un canal, una fecha y hora para notificar este contenido automáticamente a todos los estudiantes suscritos.' ), UnnorderedInlinePanel( 'sharings', label="Publicación en red social", help_text= 'Selecciona una red social, una fecha y hora para publicar este contenido automáticamente en dicha red social.' ) ] api_fields = [ APIField('title'), APIField('body', serializer=RichTextRendereableField()), APIField('image'), APIField('author', serializer=UserSerializer()), APIField('tags'), APIField('publish_at'), APIField('pinned'), ] @property def image_path(self): return self.image.file.path @property def body_as_html(self): rich_text = RichText(self.body) return rich_text.__html__() def __str__(self): date = format_datetime(self.created_at.astimezone( pytz.timezone("America/Santiago")), 'dd/MMM/YYYY', locale='es') return '%s. %s - %s' % (date, self.title, self.get_published_label()) def after_save(self, request): self.author = request.user return self.save() def is_published(self): dt_now = now() if (self.publish_at is not None and self.publish_at < dt_now) and ( self.unpublish_at is None or self.unpublish_at > dt_now): return True return False def was_unpublished(self): dt_now = now() if self.unpublish_at is not None and self.unpublish_at < dt_now: return True return False def is_pinned(self): return self.pinned def get_published_label(self): if self.was_unpublished(): label = 'Publicación finalizada' elif self.is_published(): label = 'Publicado' else: dt_now = now() days = (self.publish_at - dt_now).days label = 'queda %d día' if days == 1 else 'quedan %d días' label = ('Por publicar (%s)' % label) % days label = '%s%s' % ('📌 ' if self.is_pinned() else '', label) return label + self.get_notification_labels( ) + self.get_sharing_labels() def get_notification_labels(self): label = '' mobile_notifications = self.notifications.filter(channel='MOBILE') email_notifications = self.notifications.filter(channel='EMAIL') if mobile_notifications.count(): count = mobile_notifications.filter(notified=True).count() label = label + ' 🔔%d' % count if email_notifications.count(): count = email_notifications.filter(notified=True).count() label = label + ' @%d' % count return label def get_sharing_labels(self): label = '' twitter_notifications = self.sharings.filter(channel='TWITTER') ig_notifications = self.sharings.filter(channel='INSTAGRAM') if twitter_notifications.count(): count = twitter_notifications.filter(published=True).count() label = label + ' 🕊%d' % count if ig_notifications.count(): count = ig_notifications.filter(published=True).count() label = label + ' ⧇%d' % count return label
class ToolkitPage(Page): title_fi = models.CharField(_('Title (Finnish)'), max_length=200, blank=True, null=True) header = RichTextField(_('Header (English)'), max_length=500) header_fi = RichTextField(_('Header (Finnish)'), max_length=500, blank=True, null=True) excerpt = RichTextField( _('Excerpt'), null=True, help_text='This text will appear on the toolkits index page.') excerpt_fi = RichTextField( _('Excerpt (Finnish)'), null=True, help_text=_('This text will appear on the toolkits index page.'), ) text = RichTextField(_('Text (English)')) text_fi = RichTextField(_('Text (Finnish)'), blank=True, null=True) image = models.ForeignKey( 'wagtailimages.Image', null=True, on_delete=models.SET_NULL, related_name='+', help_text='This image will appear on the toolkits index page.') background_image = models.ForeignKey( 'wagtailimages.Image', null=True, on_delete=models.SET_NULL, related_name='+', help_text="This is the background image of the toolkit's detail page.") tools = StreamField([ ('tool', blocks.StructBlock([ ('title', blocks.CharBlock(label=_('Title (English)'))), ('title_fi', blocks.CharBlock(label=_('Title (Finnish)'), required=False)), ('thumbnail', ImageChooserBlock()), ('description', blocks.RichTextBlock(label=_('Description (English)'), required=False)), ('description_fi', blocks.RichTextBlock(label=_('Description (Finnish)'), required=False)), ('url', blocks.URLBlock(label=_('Link (English)'), required=False)), ('url_fi', blocks.URLBlock(label=_('Link (Finnish)'), required=False)), ])), ('doc', blocks.StructBlock([ ('title', blocks.CharBlock(label=_('Title (English)'))), ('title_fi', blocks.CharBlock(label=_('Title (Finnish)'), required=False)), ('thumbnail', ImageChooserBlock()), ('description', blocks.RichTextBlock(label=_('Description (English)'), required=False)), ('description_fi', blocks.RichTextBlock(label=_('Description (Finnish)'), required=False)), ('file', DocumentChooserBlock(label=_('File (English)'), required=False)), ('file_fi', DocumentChooserBlock(label=_('File (Finnish)'), required=False)), ])) ]) content_panels = Page.content_panels + [ FieldPanel('title_fi'), MultiFieldPanel([ FieldPanel('header'), FieldPanel('header_fi'), FieldPanel('excerpt'), FieldPanel('excerpt_fi'), FieldPanel('text'), FieldPanel('text_fi'), ImageChooserPanel('image'), ImageChooserPanel('background_image'), ], heading="Toolkit information"), StreamFieldPanel('tools'), ] def get_title(self): return self.header def get_thumbnail(self): return self.image def get_category(self): return 'Toolkits'
class Report(RoutablePageMixin, Post): """ Report class that inherits from the abstract Post model and creates pages for Policy Papers. """ parent_page_types = ['ReportsHomepage'] subpage_types = [] sections = StreamField( [('section', ReportSectionBlock(template="components/report_section_body.html", required=False))], null=True, blank=True) abstract = RichTextField(blank=True, null=True) acknowledgements = RichTextField(blank=True, null=True) source_word_doc = models.ForeignKey('wagtaildocs.Document', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Source Word Document') report_pdf = models.ForeignKey('wagtaildocs.Document', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Report PDF') dataviz_src = models.CharField(blank=True, null=True, max_length=300, help_text="") overwrite_sections_on_save = models.BooleanField( default=False, help_text= 'If checked, sections and endnote fields ⚠ will be overwritten ⚠ with Word document source on save. Use with CAUTION!' ) generate_pdf_on_publish = models.BooleanField( 'Generate PDF on save', default=False, help_text= '⚠ Save latest content before checking this ⚠\nIf checked, the "Report PDF" field will be filled with a generated pdf. Otherwise, leave this unchecked and upload a pdf to the "Report PDF" field.' ) revising = False featured_sections = StreamField([ ('featured', FeaturedReportSectionBlock(required=False, null=True)), ], null=True, blank=True) endnotes = StreamField([ ('endnote', EndnoteBlock(required=False, null=True)), ], null=True, blank=True) report_url = StreamField([ ('report_url', URLBlock(required=False, null=True)), ], null=True, blank=True) attachment = StreamField([ ('attachment', DocumentChooserBlock(required=False, null=True)), ], null=True, blank=True) partner_logo = models.ForeignKey('home.CustomImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+') theme_full_bleed = models.BooleanField( default=False, help_text="Display bleed image on landing page") content_panels = [ MultiFieldPanel([ FieldPanel('title'), FieldPanel('subheading'), FieldPanel('date'), ImageChooserPanel('story_image'), ]), InlinePanel('authors', label=("Authors")), InlinePanel('programs', label=("Programs")), InlinePanel('subprograms', label=("Subprograms")), InlinePanel('topics', label=("Topics")), InlinePanel('location', label=("Locations")), MultiFieldPanel([ FieldPanel('abstract'), StreamFieldPanel('featured_sections'), FieldPanel('acknowledgements'), ]) ] sections_panels = [StreamFieldPanel('sections')] endnote_panels = [StreamFieldPanel('endnotes')] settings_panels = Post.settings_panels + [ FieldPanel('theme_full_bleed'), FieldPanel('dataviz_src') ] promote_panels = Page.promote_panels + [ FieldPanel('story_excerpt'), ] pdf_panels = [ MultiFieldPanel([ DocumentChooserPanel('source_word_doc'), FieldPanel('overwrite_sections_on_save'), ], heading='Word Doc Import'), MultiFieldPanel([ FieldPanel('generate_pdf_on_publish'), DocumentChooserPanel('report_pdf'), StreamFieldPanel('attachment') ], heading='PDF Generation'), ImageChooserPanel('partner_logo') ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading="Landing"), ObjectList(sections_panels, heading="Sections"), ObjectList(endnote_panels, heading="Endnotes"), ObjectList(promote_panels, heading="Promote"), ObjectList(settings_panels, heading='Settings', classname="settings"), ObjectList(pdf_panels, heading="PDF Publishing") ]) search_fields = Post.search_fields + [index.SearchField('sections')] def get_context(self, request): context = super().get_context(request) if getattr(request, 'is_preview', False): import newamericadotorg.api.report revision = PageRevision.objects.filter( page=self).last().as_page_object() report_data = newamericadotorg.api.report.serializers.ReportDetailSerializer( revision).data context['initial_state'] = json.dumps(report_data) return context def save(self, *args, **kwargs): super(Report, self).save(*args, **kwargs) if not self.overwrite_sections_on_save and not self.generate_pdf_on_publish: self.revising = False if not self.revising and self.source_word_doc is not None and self.overwrite_sections_on_save: self.revising = True parse_pdf(self) self.overwrite_sections_on_save = False self.save_revision() if not self.revising and self.generate_pdf_on_publish: generate_pdf.apply_async(args=(self.id, )) # Extra views @route(r'pdf/$') def pdf(self, request): if not self.report_pdf: return self.pdf_render(request) url = 'https://s3.amazonaws.com/newamericadotorg/' + self.report_pdf.file.name return redirect(url) @route(r'pdf/render/$') def pdf_render(self, request): response = HttpResponse(content_type='application/pdf;') response[ 'Content-Disposition'] = 'inline; filename=%s.pdf' % self.title response['Content-Transfer-Encoding'] = 'binary' protocol = 'https://' if request.is_secure() else 'http://' base_url = protocol + request.get_host() contents = generate_report_contents(self) authors = get_report_authors(self) html = loader.get_template('report/pdf.html').render({ 'page': self, 'contents': contents, 'authors': authors }) pdf = write_pdf(response, html, base_url) return response @route(r'print/$') def print(self, request): contents = generate_report_contents(self) authors = get_report_authors(self) return render(request, 'report/pdf.html', context={ 'page': self, 'contents': contents, 'authors': authors }) @route(r'[a-zA-Z0-9_\.\-]*/$') def section(self, request): # Serve the whole report, subsection routing is handled by React return self.serve(request) class Meta: verbose_name = 'Report'
class SectionBase(models.Model): # Content section_name = models.CharField( blank=True, null=True, max_length=100, verbose_name='Title', ) section_heading = models.CharField( blank=True, null=True, max_length=100, verbose_name='Hero title', default='We are heroes', ) section_subheading = models.CharField( blank=True, null=True, max_length=100, verbose_name='Hero subtitle', default='What business are you?', help_text=mark_safe( "Leave field empty to hide. <a href='/contract.pdf'>contract</a>.") ) section_description = models.TextField( blank=True, null=True, max_length=400, verbose_name='Hero description', default= ('The thing we do is better than any other similar thing and this hero panel will convince you of that, just by having a glorious background image.' ), help_text="Leave field empty to hide.", ) # Background section_background_type = models.CharField( blank=True, null=True, choices=[ ('transparent', 'Transparent'), ('solid', 'Solid Color'), ('gradient', 'Gradient Color'), ('image', 'Background Image'), ], verbose_name='Type', default='trasparent', max_length=100, help_text= 'Transparent background will use the background-color "page settings".', ) section_background_color = ColorField( blank=True, null=True, help_text="Choose background color", verbose_name=('Color 1'), ) section_background_color_2 = ColorField( blank=True, null=True, help_text="Choose background color", verbose_name=('Color 2'), ) section_background_image = models.ForeignKey( 'wagtailimages.Image', blank=True, null=True, on_delete=models.SET_NULL, verbose_name='Image', related_name='+', ) # Layout section_color_theme = models.CharField( null=True, blank=True, max_length=50, choices=cr_settings['SECTION_COLOR_THEME_CHOICES'], verbose_name='Theme', # help_text='Choose font weight.', ) section_top_bottom_padding = models.CharField( null=True, blank=True, max_length=50, choices=cr_settings['SECTION_TOP_BOTTOM_PADDING_CHOICES'], default=cr_settings['SECTION_TOP_BOTTOM_PADDING_CHOICES_DEFAULT'], verbose_name='Height', # help_text='Choose font weight.', ) section_container_width = models.CharField( null=True, blank=True, max_length=50, choices=cr_settings['SECTION_CONTAINER_WIDTH_CHOICES'], default=cr_settings['SECTION_CONTAINER_WIDTH_CHOICES_DEFAULT'], verbose_name='Width', # help_text='Choose font weight.', ) # Basic Panels section_content_panels = MultiFieldPanel( [ FieldRowPanel([ FieldPanel( 'section_heading', heading='Heading', classname="col6"), FieldPanel('section_subheading', heading='Subheading', classname="col6"), ]), FieldRowPanel([ FieldPanel('section_description', heading='Description', classname="col6"), ]), ], heading='Content', ) section_layout_panels = MultiFieldPanel( [ FieldRowPanel([ FieldPanel('section_top_bottom_padding', heading='Size', classname="col6"), FieldPanel('section_container_width', heading='Width', classname="col6"), ]), FieldRowPanel([ FieldPanel( 'section_color_theme', heading='Theme', classname="col6"), ]), ], heading='Layout', ) section_design_panels = MultiFieldPanel( [ FieldRowPanel([ FieldPanel('section_background_type', heading='Type', classname="col6"), ]), FieldRowPanel([ NativeColorPanel('section_background_color', heading='Color 1', classname="col6"), NativeColorPanel('section_background_color_2', heading='Color 2', classname="col6"), ]), FieldRowPanel([ ImageChooserPanel('section_background_image', heading='Image'), ]), ], heading='Design', ) # Legacy advanced_tab_panels = [ MultiFieldPanel( [ FieldPanel('section_background_type'), ], heading='Background > Type', classname='collapsible', ), MultiFieldPanel([ NativeColorPanel('section_background_color'), NativeColorPanel('section_background_color_2'), ], heading='Background > Color', classname='collapsible', help_text="Please select Background Type first."), MultiFieldPanel( [ ImageChooserPanel('section_background_image'), ], heading='Background > Image', classname='collapsible', ), ] # Define abstract to dont create own database table for this model - fields are created in the child class class Meta: abstract = True def __str__(self): if self.section_name: return self.section_name + " (Hero Section)" else: return super(SectionBase, self).__str__()
class HomePage(Page): image = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', help_text='Homepage image' ) hero_text = models.CharField( max_length=255, help_text='Write an introduction for the bakery' ) hero_cta = models.CharField( verbose_name='Hero CTA', max_length=255, help_text='Text to display on Call to Action' ) hero_cta_link = models.ForeignKey( 'wagtailcore.Page', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Hero CTA link', help_text='Choose a page to link to for the Call to Action' ) # Body section of the HomePage body = StreamField( BaseStreamBlock(), verbose_name="Home content block", blank=True ) # Promo section of the HomePage promo_image = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', help_text='Promo image' ) promo_title = models.CharField( null=True, blank=True, max_length=255, help_text='Title to display above the promo copy' ) promo_text = RichTextField( null=True, blank=True, help_text='Write some promotional copy' ) featured_section_1_title = models.CharField( null=True, blank=True, max_length=255, help_text='Title to display above the promo copy' ) featured_section_1 = models.ForeignKey( 'wagtailcore.Page', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', help_text='First featured section for the homepage. Will display up to ' 'three child items.', verbose_name='Featured section 1' ) featured_section_2_title = models.CharField( null=True, blank=True, max_length=255, help_text='Title to display above the promo copy' ) featured_section_2 = models.ForeignKey( 'wagtailcore.Page', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', help_text='Second featured section for the homepage. Will display up to ' 'three child items.', verbose_name='Featured section 2' ) featured_section_3_title = models.CharField( null=True, blank=True, max_length=255, help_text='Title to display above the promo copy' ) featured_section_3 = models.ForeignKey( 'wagtailcore.Page', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', help_text='Third featured section for the homepage. Will display up to ' 'six child items.', verbose_name='Featured section 3' ) content_panels = Page.content_panels + [ MultiFieldPanel([ ImageChooserPanel('image'), FieldPanel('hero_text', classname="full"), MultiFieldPanel([ FieldPanel('hero_cta'), PageChooserPanel('hero_cta_link'), ]) ], heading="Hero section"), MultiFieldPanel([ ImageChooserPanel('promo_image'), FieldPanel('promo_title'), FieldPanel('promo_text'), ], heading="Promo section"), StreamFieldPanel('body'), MultiFieldPanel([ MultiFieldPanel([ FieldPanel('featured_section_1_title'), PageChooserPanel('featured_section_1'), ]), MultiFieldPanel([ FieldPanel('featured_section_2_title'), PageChooserPanel('featured_section_2'), ]), MultiFieldPanel([ FieldPanel('featured_section_3_title'), PageChooserPanel('featured_section_3'), ]) ], heading="Featured homepage sections", classname="collapsible") ] def __str__(self): return self.title
class OldHomePage(Page): """ This is legacy HP model. It will be deleted once `HomePage` model (above) is complete and operational. """ is_creatable = False header = models.CharField(max_length=255) specializations_headline = models.CharField(max_length=128) r_and_d_center_headline = models.CharField(max_length=128, null=True, blank=True) r_and_d_center_body = models.CharField(max_length=256, null=True, blank=True) join_us_headline = models.CharField(max_length=128) join_us_body = models.TextField() join_us_background = models.ForeignKey('wagtailimages.Image', null=True, on_delete=models.SET_NULL, related_name='+') content_panels = Page.content_panels + [ FieldPanel('header'), FieldPanel('specializations_headline'), MultiFieldPanel([ FieldPanel('r_and_d_center_headline', classname="full"), FieldPanel('r_and_d_center_body'), ], heading="R&D center section"), MultiFieldPanel([ FieldPanel('join_us_headline', classname="full"), FieldPanel('join_us_body'), ImageChooserPanel('join_us_background'), ], heading=_("Join us section")), InlinePanel('cooperators_logos', heading="We work with") ] @property def articles(self): return NewsPage.objects.live().descendant_of(self).order_by( '-marked', '-publication_date') @property def our_initiatives(self): ProjectPage = apps.get_model('projects', 'ProjectPage') return ProjectPage.objects.live().filter(self_initiated=True) @property def info_pages(self): return InfoPage.objects.live().descendant_of(self) @property def topics(self): return TopicPage.objects.live().descendant_of(self).filter(marked=True) @property def random_team_member(self): """ Every hit on home page should present a randomly picked team member. This method of random selection is based on https://books.agiliq.com/projects/django-orm-cookbook/en/latest/random.html Should be more efficient than Model.objects.order_by("?").first() """ TeamMember = apps.get_model('projects', 'TeamMember') team_member_queryset = TeamMember.objects.live().descendant_of(self) max_id = team_member_queryset.aggregate(max_id=Max("id"))['max_id'] if max_id is None: # there are no team members specified return None while True: pk = random.randint(1, max_id) team_member = team_member_queryset.filter(pk=pk).first() if team_member: return team_member.specific @property def rnd_block(self): return custom_blocks.RNDBlock().bind({ 'headline': self.r_and_d_center_headline, 'body': self.r_and_d_center_body, }) @property def specializations_block(self): return custom_blocks.TriptychBlock().bind({ 'headline': self.specializations_headline, 'tiles': [{ 'background_image': specialization.background_image, 'content': specialization.short_description, 'page': specialization, } for specialization in SpecializationPage.objects.live().descendant_of(self)], }) @property def our_stories_block(self): return custom_blocks.HeroCarouselBlock().bind({ 'headline': _('Poznaj nas przez nasze historie'), 'tiles': [{ 'background_image': news.photo, 'headline': news.headline, 'page': news, 'secondary_page': news.specialization, } for news in self.articles[:3]], }) @property def topics_block(self): return custom_blocks.HeroSwitchBlock().bind({ 'headline': _('Działamy w tematach'), 'tiles': [{ 'background_image': topic.background_image, 'title': topic.title, 'page': topic.projects.first(), 'side_image': topic.phone_image, } for topic in TopicPage.objects.live().descendant_of(self).filter( marked=True)], }) @property def animated_process_block(self): return custom_blocks.AnimatedProcessBlock().bind(None) def join_us_block(self): return custom_blocks.HeroJoinUsBlock().bind({ 'background_image': self.join_us_background, 'headline': self.join_us_headline, 'body': self.join_us_body, 'page': JobOfferIndexPage.objects.live().descendant_of(self).first(), }) @property def our_initiatives_block(self): return custom_blocks.TriptychBlock().bind({ 'headline': _('Nasze inicjatywy'), 'tiles': [{ 'background_image': project.background_image, 'content': project.subtitle, 'page': project, 'external_url': project.project_url, } for project in self.our_initiatives], }) @property def cooperation_block(self): return custom_blocks.LogoWallBlock().bind({ 'title': _("We have worked with"), 'logos': [logo.image for logo in self.cooperators_logos.all()], }) @property def member_block(self): member = self.random_team_member if not member: return None return custom_blocks.HeroStaticLeftBlock().bind({ 'background_image': member.photo, 'title': _("Team"), 'headline': member.name, 'body': member.long_description, 'page': TeamIndexPage.objects.live().descendant_of(self).first(), })
class CommissionerPage(Page): first_name = models.CharField(max_length=255, default='', blank=False) middle_initial = models.CharField(max_length=255, blank=True) last_name = models.CharField(max_length=255, default='', blank=False) picture = models.ForeignKey('wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+') sworn_in = models.DateField(null=True, blank=True) term_expiration = models.DateField(null=True, blank=True) reappointed_dates = models.CharField(max_length=255, blank=True) party_affiliation = models.CharField(max_length=2, choices=( ('D', 'Democrat'), ('R', 'Republican'), ('I', 'Independent'), )) commissioner_title = models.CharField(max_length=255, blank=True) commissioner_bio = StreamField([('paragraph', blocks.RichTextBlock())], null=True, blank=True) commissioner_email = models.CharField(max_length=255, blank=True) commissioner_phone = models.CharField(max_length=255, null=True, blank=True) commissioner_twitter = models.CharField(max_length=255, null=True, blank=True) content_panels = Page.content_panels + [ FieldPanel('first_name'), FieldPanel('middle_initial'), FieldPanel('last_name'), ImageChooserPanel('picture'), FieldPanel('sworn_in'), FieldPanel('term_expiration'), FieldPanel('reappointed_dates'), FieldPanel('party_affiliation'), FieldPanel('commissioner_title'), StreamFieldPanel('commissioner_bio'), FieldPanel('commissioner_email'), FieldPanel('commissioner_phone'), FieldPanel('commissioner_twitter'), ] def get_context(self, request): context = super(CommissionerPage, self).get_context(request) # Breadcrumbs for Commissioner pages context['ancestors'] = [{ 'title': 'About the FEC', 'url': '/about/', }, { 'title': 'Leadership and Structure', 'url': '/about/leadership-and-structure', }, { 'title': 'All Commissioners', 'url': '/about/leadership-and-structure/commissioners', }] return context
class SeoMixin(Page): og_title = models.CharField( max_length=40, blank=True, null=True, verbose_name=_("Facebook title"), help_text=_("Fallbacks to seo title if empty"), ) og_description = models.CharField( max_length=300, blank=True, null=True, verbose_name=_("Facebook description"), help_text=_("Fallbacks to seo description if empty"), ) og_image = models.ForeignKey( "customimage.CustomImage", null=True, blank=True, on_delete=models.SET_NULL, help_text=_("If you want to override the image used on Facebook for \ this item, upload an image here. \ The recommended image size for Facebook is 1200 × 630px"), related_name="+", ) twitter_title = models.CharField( max_length=40, blank=True, null=True, verbose_name=_("Twitter title"), help_text=_("Fallbacks to facebook title if empty"), ) twitter_description = models.CharField( max_length=300, blank=True, null=True, verbose_name=_("Twitter description"), help_text=_("Fallbacks to facebook description if empty"), ) twitter_image = models.ForeignKey( "customimage.CustomImage", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", verbose_name=_("Twitter image"), help_text=_("Fallbacks to facebook image if empty"), ) robot_noindex = models.BooleanField( default=False, verbose_name=_("No index"), help_text=_("Check to add noindex to robots"), ) robot_nofollow = models.BooleanField( default=False, verbose_name=_("No follow"), help_text=_("Check to add nofollow to robots"), ) canonical_link = models.URLField(blank=True, null=True, verbose_name=_("Canonical link")) promote_panels = [ FieldPanel("slug"), MultiFieldPanel( [FieldPanel("seo_title"), FieldPanel("search_description")], _("SEO settings"), ), MultiFieldPanel( [ FieldPanel("og_title"), FieldPanel("og_description"), ImageChooserPanel("og_image"), FieldPanel("twitter_title"), FieldPanel("twitter_description"), ImageChooserPanel("twitter_image"), ], _("Social settings"), ), MultiFieldPanel( [ FieldPanel("robot_noindex"), FieldPanel("robot_nofollow"), FieldPanel("canonical_link"), ], _("Robot settings"), ), ] og_image_list = ["og_image"] @cached_property def seo_og_image(self): images = [getattr(self, x) for x in self.og_image_list] images = list(filter(None.__ne__, images)) if not len(images): return None return images[0] @cached_property def seo_html_title(self): return self.seo_title or self.title @cached_property def seo_meta_description(self): return self.search_description @cached_property def seo_og_title(self): return self.og_title or self.title @cached_property def seo_og_description(self): return self.og_description or self.title @cached_property def seo_og_url(self): return self.seo_canonical_link @cached_property def seo_canonical_link(self): return self.canonical_link or self.full_url @cached_property def seo_og_type(self): return None @cached_property def seo_twitter_title(self): return self.twitter_title or self.title @cached_property def seo_twitter_description(self): return self.twitter_description @cached_property def seo_twitter_url(self): return self.seo_canonical_link @cached_property def seo_twitter_image(self): return self.twitter_image or self.seo_og_image @cached_property def seo_meta_robots(self): index = "noindex" if self.robot_noindex else "index" follow = "nofollow" if self.robot_nofollow else "follow" return "{},{}".format(index, follow) class Meta: abstract = True
class LayoutSettings(BaseSetting): """ Branding, navbar, and theme settings. """ class Meta: verbose_name = _('Layout') logo = models.ForeignKey( get_image_model_string(), null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name=_('Logo'), help_text=_('Brand logo used in the navbar and throughout the site')) favicon = models.ForeignKey( get_image_model_string(), null=True, blank=True, on_delete=models.SET_NULL, related_name='favicon', verbose_name=_('Favicon'), ) navbar_color_scheme = models.CharField( blank=True, max_length=50, choices=cr_settings['FRONTEND_NAVBAR_COLOR_SCHEME_CHOICES'], default=cr_settings['FRONTEND_NAVBAR_COLOR_SCHEME_DEFAULT'], verbose_name=_('Navbar color scheme'), help_text= _('Optimizes text and other navbar elements for use with light or dark backgrounds.' ), # noqa ) navbar_class = models.CharField( blank=True, max_length=255, default=cr_settings['FRONTEND_NAVBAR_CLASS_DEFAULT'], verbose_name=_('Navbar CSS class'), help_text= _('Custom classes applied to navbar e.g. "bg-light", "bg-dark", "bg-primary".' ), ) navbar_fixed = models.BooleanField( default=False, verbose_name=_('Fixed navbar'), help_text=_( 'Fixed navbar will remain at the top of the page when scrolling.'), ) navbar_wrapper_fluid = models.BooleanField( default=True, verbose_name=_('Full width navbar'), help_text=_('The navbar will fill edge to edge.'), ) navbar_content_fluid = models.BooleanField( default=False, verbose_name=_('Full width navbar contents'), help_text=_('Content within the navbar will fill edge to edge.'), ) navbar_collapse_mode = models.CharField( blank=True, max_length=50, choices=cr_settings['FRONTEND_NAVBAR_COLLAPSE_MODE_CHOICES'], default=cr_settings['FRONTEND_NAVBAR_COLLAPSE_MODE_DEFAULT'], verbose_name=_('Collapse navbar menu'), help_text= _('Control on what screen sizes to show and collapse the navbar menu links.' ), ) navbar_format = models.CharField( blank=True, max_length=50, choices=cr_settings['FRONTEND_NAVBAR_FORMAT_CHOICES'], default=cr_settings['FRONTEND_NAVBAR_FORMAT_DEFAULT'], verbose_name=_('Navbar format'), ) navbar_search = models.BooleanField( default=True, verbose_name=_('Search box'), help_text=_('Show search box in navbar')) frontend_theme = models.CharField( blank=True, max_length=50, choices=cr_settings['FRONTEND_THEME_CHOICES'], default=cr_settings['FRONTEND_THEME_DEFAULT'], verbose_name=_('Theme variant'), help_text=cr_settings['FRONTEND_THEME_HELP'], ) panels = [ MultiFieldPanel([ ImageChooserPanel('logo'), ImageChooserPanel('favicon'), ], heading=_('Branding')), MultiFieldPanel([ FieldPanel('navbar_color_scheme'), FieldPanel('navbar_class'), FieldPanel('navbar_fixed'), FieldPanel('navbar_wrapper_fluid'), FieldPanel('navbar_content_fluid'), FieldPanel('navbar_collapse_mode'), FieldPanel('navbar_format'), FieldPanel('navbar_search'), ], heading=_('Site Navbar Layout')), MultiFieldPanel([ FieldPanel('frontend_theme'), ], heading=_('Theming')), ]
class RecipePage(Page): """ Represents a blog recipe page. This Page subclass provide a way to define blog recipe pages through the Wagtail's admin. It defines the basic fields and information that are generally associated with recipes showcased in the context of a blog application. """ # Like any blog article, a recipe has a date and a title. But it has no body: instead it only # has an introduction field to contain a small content to be displayed before the recipe # details. introduction = RichTextField(verbose_name=_('Introduction')) date = models.DateField(verbose_name=_('Post date'), default=dt.datetime.today) # A blog recipe page can have an header image that'll be used when rendering the blog post. header_image = models.ForeignKey( 'wagtailimages.Image', blank=False, null=True, on_delete=models.SET_NULL, related_name='+', verbose_name=_('Header image'), help_text=_('Header image displayed when rendering the page.'), ) # The following fields define basic meta-information regarding a recipe (times, yiels, etc). preparation_time = models.DurationField( blank=True, null=True, verbose_name=_('Preparation time'), ) cook_time = models.DurationField( blank=True, null=True, verbose_name=_('Cook time'), ) fridge_time = models.DurationField( blank=True, null=True, verbose_name=_('Fridge time'), ) rest_time = models.DurationField( blank=True, null=True, verbose_name=_('Rest time'), ) recipe_yield = models.CharField( max_length=127, blank=True, verbose_name=_('Yield'), help_text=_('Enter a yield indication such as "4 persons", "3 servings", etc.'), ) # A recipe is defined by at least one dish type. DISH_TYPE_APPETIZERS = 'appetizers' DISH_TYPE_BEVERAGES = 'beverages' DISH_TYPE_BREAKFAST = 'breakfast' DISH_TYPE_DESSERTS = 'desserts' DISH_TYPE_MAIN_COURSE = 'main-course' DISH_TYPE_SAUCES_SALAD_DRESSINGS = 'sauces+salad-dressings' DISH_TYPE_SOUPS = 'soups' DISH_TYPE_VEGETABLES_SALADS = 'vegetables+salads' DISH_TYPE_CHOICES = ( (DISH_TYPE_APPETIZERS, _('Appetizers')), (DISH_TYPE_BEVERAGES, _('Beverages')), (DISH_TYPE_BREAKFAST, _('Breakfast')), (DISH_TYPE_DESSERTS, _('Desserts')), (DISH_TYPE_MAIN_COURSE, _('Main course')), (DISH_TYPE_SAUCES_SALAD_DRESSINGS, _('Sauces and salad dressings')), (DISH_TYPE_SOUPS, _('Soups')), (DISH_TYPE_VEGETABLES_SALADS, _('Vegetables and salads')), ) dish_types = ChoiceArrayField( models.CharField(max_length=64, choices=DISH_TYPE_CHOICES), size=3, default=list, verbose_name=_('Dish types'), ) ############################## # SEARCH INDEX CONFIGURATION # ############################## search_fields = Page.search_fields + [ index.SearchField('introduction'), index.FilterField('date'), ] ############################### # EDITOR PANELS CONFIGURATION # ############################### content_panels = Page.content_panels + [ FieldPanel('introduction', classname='full'), FieldPanel('dish_types', widget=CheckboxSelectMultiple), MultiFieldPanel( [ FieldPanel('preparation_time', widget=ShortDurationSelectWidget), FieldPanel('cook_time', widget=ShortDurationSelectWidget), FieldPanel('fridge_time', widget=ShortDurationSelectWidget), FieldPanel('rest_time', widget=ShortDurationSelectWidget), FieldPanel('recipe_yield'), ], heading=_('Recipe information'), ), InlinePanel('ingredients_sections', label=_('Recipe ingredients sections')), InlinePanel('instructions_sections', label=_('Recipe instructions sections')), ImageChooserPanel('header_image'), FieldPanel('date'), ] #################################### # PARENT PAGE / SUBPAGE TYPE RULES # #################################### parent_page_types = ['blog.BlogPage'] subpage_types: List[str] = [] class Meta: verbose_name = _('Recipe') verbose_name_plural = _('Recipes') @property def verbose_dish_types(self): """ Returns verbose names of dish types. """ dish_types_choices = dict(self.DISH_TYPE_CHOICES) return [dish_types_choices[d] for d in self.dish_types] def get_context(self, request, *args, **kwargs): """ Returns a dictionary of variables to bind into the template. """ context = super().get_context(request, *args, **kwargs) # Inserts the top-level blog page into the context. context['blog_page'] = self.get_parent().specific return context
class LocationPage(Page): """ Detail for a specific bakery location. """ introduction = models.TextField(help_text='Text to describe the page', blank=True) image = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', help_text= 'Landscape mode only; horizontal width between 1000px and 3000px.') body = StreamField(BaseStreamBlock(), verbose_name="Page body", blank=True) address = models.TextField() lat_long = models.CharField( max_length=36, help_text="Comma separated lat/long. (Ex. 64.144367, -21.939182) \ Right click Google Maps and select 'What\'s Here'", validators=[ RegexValidator( regex=r'^(\-?\d+(\.\d+)?),\s*(\-?\d+(\.\d+)?)$', message= 'Lat Long must be a comma-separated numeric lat and long', code='invalid_lat_long'), ]) # Search index configuration search_fields = Page.search_fields + [ index.SearchField('address'), index.SearchField('body'), ] # Fields to show to the editor in the admin view content_panels = [ FieldPanel('title', classname="full"), FieldPanel('introduction', classname="full"), ImageChooserPanel('image'), StreamFieldPanel('body'), FieldPanel('address', classname="full"), FieldPanel('lat_long'), InlinePanel('hours_of_operation', label="Hours of Operation"), ] def __str__(self): return self.title @property def operating_hours(self): hours = self.hours_of_operation.all() return hours # Determines if the location is currently open. It is timezone naive def is_open(self): now = datetime.now() current_time = now.time() current_day = now.strftime('%a').upper() try: self.operating_hours.get(day=current_day, opening_time__lte=current_time, closing_time__gte=current_time) return True except LocationOperatingHours.DoesNotExist: return False # Makes additional context available to the template so that we can access # the latitude, longitude and map API key to render the map def get_context(self, request): context = super(LocationPage, self).get_context(request) context['lat'] = self.lat_long.split(",")[0] context['long'] = self.lat_long.split(",")[1] context['google_map_api_key'] = settings.GOOGLE_MAP_API_KEY return context # Can only be placed under a LocationsIndexPage object parent_page_types = ['LocationsIndexPage']
class MozfestPrimaryPage(FoundationMetadataPageMixin, FoundationBannerInheritanceMixin, Page): header = models.CharField(max_length=250, blank=True) banner = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='mozfest_primary_banner', verbose_name='Hero Image', help_text= 'Choose an image that\'s bigger than 4032px x 1152px with aspect ratio 3.5:1', ) intro = RichTextField(help_text='Page intro content', blank=True) signup = models.ForeignKey( Signup, related_name='mozfestpage', blank=True, null=True, on_delete=models.SET_NULL, help_text='Choose an existing, or create a new, sign-up form') body = StreamField(base_fields) content_panels = Page.content_panels + [ FieldPanel('header'), ImageChooserPanel('banner'), FieldPanel('intro'), SnippetChooserPanel('signup'), StreamFieldPanel('body'), ] subpage_types = [ 'MozfestPrimaryPage', ] show_in_menus_default = True use_wide_template = models.BooleanField( default=False, help_text= "Make the body content wide, useful for components like directories") settings_panels = Page.settings_panels + [FieldPanel('use_wide_template')] def get_template(self, request): if self.use_wide_template: return 'mozfest/mozfest_primary_page_wide.html' return 'mozfest/mozfest_primary_page.html' def get_context(self, request, bypass_menu_buildstep=False): context = super().get_context(request) context = set_main_site_nav_information(self, context, 'MozfestHomepage') context = get_page_tree_information(self, context) # primary nav information context['menu_root'] = self context['menu_items'] = self.get_children().live().in_menu() # Also make sure that these pages always tap into the mozfest newsletter for the footer! mozfest_footer = Signup.objects.filter(name__iexact='mozfest').first() context['mozfest_footer'] = mozfest_footer if not bypass_menu_buildstep: context = set_main_site_nav_information(self, context, 'MozfestHomepage') return context
class AllTagsPage(Page): parent_page_types = ['home.HomePage'] subpage_types = [] max_count = 1 banner = models.ForeignKey('wagtailimages.Image', on_delete=models.SET_NULL, related_name='+', null=True) @property def breadcrumbs(self): breadcrumbs = [] for page in self.get_ancestors()[2:]: breadcrumbs.append({'title': page.title, 'url': page.url}) breadcrumbs.append({'title': self.title, 'url': self.url}) return breadcrumbs @property def all_tags(self): tags = TaggitTag.objects.all() tags_list = [] for tag in tags: tags_list.append({ 'id': tag.id, 'name': tag.name, 'slug': tag.slug }) tags_sorted = sorted(tags_list, key=lambda i: str(i['name']).lower()) letter_groups = set() for tag in tags_sorted: letter_groups.add(tag['name'][0].lower()) letter_groups = sorted(list(letter_groups)) result = [] for letter in letter_groups: letter_dict = {'group': letter.upper(), 'tags': []} for tag in tags_sorted: if tag['name'][0].lower() == letter: letter_dict['tags'].append(tag) result.append(letter_dict) return result content_panels = Page.content_panels + [ ImageChooserPanel('banner', heading='Иллюстрация') ] api_fields = [ APIField('breadcrumbs'), APIField('all_tags'), APIField('banner', serializer=ImageRenditionField(BANNER_SIZE)) ] def get_sitemap_urls(self, request): return [{ 'location': self.full_url[:-1], 'lastmod': self.last_published_at, }] class Meta: verbose_name = 'Страница тегов'
class ProjectsIndexPage(Page): """ Index page for projects. This is more complex than other index pages on the bakery demo site as we've included pagination. We've separated the different aspects of the index page to be discrete functions to make it easier to follow """ introduction = models.TextField(help_text='Text to describe the page', blank=True) image = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', help_text='Landscape mode only; horizontal width between 1000px and ' '3000px.') content_panels = Page.content_panels + [ FieldPanel('introduction', classname="full"), ImageChooserPanel('image'), ] # Can only have BreadPage children subpage_types = ['ProjectPage'] # Returns a queryset of BreadPage objects that are live, that are direct # descendants of this index page with most recent first def get_projects(self): return ProjectPage.objects.live().descendant_of(self).order_by( '-first_published_at') # Allows child objects (e.g. BreadPage objects) to be accessible via the # template. We use this on the HomePage to display child items of featured # content def children(self): return self.get_children().specific().live() # Pagination for the index page. We use the `django.core.paginator` as any # standard Django app would, but the difference here being we have it as a # method on the model rather than within a view function def paginate(self, request, *args): page = request.GET.get('page') paginator = Paginator(self.get_projects(), 12) try: pages = paginator.page(page) except PageNotAnInteger: pages = paginator.page(1) except EmptyPage: pages = paginator.page(paginator.num_pages) return pages # Returns the above to the get_context method that is used to populate the # template def get_context(self, request): context = super(ProjectsIndexPage, self).get_context(request) # BreadPage objects (get_breads) are passed through pagination projects = self.paginate(request, self.get_projects()) context['projects'] = projects return context
class Author(index.Indexed, models.Model): """Авторы для различного контента""" surname = models.CharField(max_length=50, verbose_name='Фамилия', null=True, blank=True) name = models.CharField(max_length=50, verbose_name='Имя', null=True, blank=True) middle_name = models.CharField(max_length=50, verbose_name='Отчество', null=True, blank=True) alias = models.CharField(max_length=50, verbose_name='Псевдоним', null=True, blank=True) profession = models.CharField(max_length=200, verbose_name='Профессия/Должность/...', null=True, blank=True) # TODO добавить выбор страницы сайта handhelp_url = models.ForeignKey('wagtailcore.Page', null=True, blank=True, related_name='+', on_delete=models.CASCADE, verbose_name='Страница на hand-help') outer_url = models.URLField(blank=True, null=True, default='hand-help.ru', verbose_name='Страница на стороннем сайте') image = models.ForeignKey('wagtailimages.Image', on_delete=models.SET_NULL, related_name='+', null=True, verbose_name='Изображение') @property def full_name(self): if self.alias: return self.alias else: return f'{self.name} {self.surname}' @property def short_name(self): ## Фамилия И.О. try: return f'{self.surname} {self.name[0]}.{self.middle_name[0]}.' except TypeError: try: return f'{self.surname} {self.name[0]}.' except TypeError: return f'{self.name}' @property def inner_link(self): if self.handhelp_url: return self.handhelp_url.url @property def outer_link(self): return self.outer_url # @property # def link(self): # if self.handhelp_url: # return self.handhelp_url.url # elif self.outer_url: # return self.outer_url # return '#' @property def ava(self): # маленькая аватарка return self.image.get_rendition('fill-24x24') panels = [ MultiFieldPanel([ FieldPanel('surname'), FieldPanel('name'), FieldPanel('middle_name') ], heading='ФИО'), FieldPanel('alias'), FieldPanel('profession'), MultiFieldPanel( [PageChooserPanel('handhelp_url'), FieldPanel('outer_url')], heading='Страница'), ImageChooserPanel('image'), ] api_fields = [ APIField('alias'), APIField('profession'), APIField('image', serializer=ImageRenditionField( 'fill-100x100')), # TODO размеры по макету APIField('image_small', serializer=ImageRenditionField('fill-20x20', source='image')) ] search_fields = [index.SearchField('full_name', partial_match=True)] def __str__(self): return self.full_name # TODO исправить с учетом разных контекстов, изменить порядок на ФИ class Meta: verbose_name = "Автор" verbose_name_plural = "Авторы" ordering = [ 'surname', ]
class Person(Page): resource_type = 'person' parent_page_types = ['People'] subpage_types = [] template = 'person.html' # Content fields job_title = CharField(max_length=250) role = CharField(max_length=250, choices=ROLE_CHOICES, default='staff') description = RichTextField(default='', blank=True) image = ForeignKey( 'mozimages.MozImage', null=True, blank=True, on_delete=SET_NULL, related_name='+' ) # Card fields card_title = CharField('Title', max_length=140, blank=True, default='') card_description = TextField('Description', max_length=140, blank=True, default='') card_image = ForeignKey( 'mozimages.MozImage', null=True, blank=True, on_delete=SET_NULL, related_name='+', verbose_name='Image', ) # Meta twitter = CharField(max_length=250, blank=True, default='') facebook = CharField(max_length=250, blank=True, default='') linkedin = CharField(max_length=250, blank=True, default='') github = CharField(max_length=250, blank=True, default='') email = CharField(max_length=250, blank=True, default='') websites = StreamField( StreamBlock([ ('website', PersonalWebsiteBlock()) ], min_num=0, max_num=3, required=False), null=True, blank=True, ) keywords = ClusterTaggableManager(through=PersonTag, blank=True) # Content panels content_panels = [ MultiFieldPanel([ CustomLabelFieldPanel('title', label='Full name'), FieldPanel('job_title'), FieldPanel('role'), ], heading='About'), FieldPanel('description'), ImageChooserPanel('image'), ] # Card panels card_panels = [ FieldPanel('card_title'), FieldPanel('card_description'), ImageChooserPanel('card_image'), ] # Meta panels meta_panels = [ MultiFieldPanel([ InlinePanel('topics'), ], heading='Topics interested in'), MultiFieldPanel([ FieldPanel('twitter'), FieldPanel('facebook'), FieldPanel('linkedin'), FieldPanel('github'), FieldPanel('email'), ], heading='Profiles'), StreamFieldPanel('websites'), MultiFieldPanel([ FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('keywords'), ], heading='SEO'), ] # Settings panels settings_panels = [ FieldPanel('slug'), ] # Tabs edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(card_panels, heading='Card'), ObjectList(meta_panels, heading='Meta'), ObjectList(settings_panels, heading='Settings', classname='settings'), ]) @property def events(self): ''' Return upcoming events where this person is a speaker, ordered by start date ''' from ..events.models import Event upcoming_events = (Event .objects .filter(start_date__gte=datetime.datetime.now()) .live() .public() ) speaker_events = Event.objects.none() for event in upcoming_events.all(): # add the event to the list if the current person is a speaker if event.has_speaker(self): speaker_events = speaker_events | Event.objects.page(event) return speaker_events.order_by('start_date') @property def articles(self): ''' Return articles and external articles where this person is (one of) the authors, ordered by article date, most recent first ''' from ..articles.models import Article from ..externalcontent.models import ExternalArticle articles = Article.objects.none() external_articles = ExternalArticle.objects.none() all_articles = Article.objects.live().public().all() all_external_articles = ExternalArticle.objects.live().public().all() for article in all_articles: if article.has_author(self): articles = articles | Article.objects.page(article) for external_article in all_external_articles: if external_article.has_author(self): external_articles = external_articles | ExternalArticle.objects.page(external_article) return sorted(chain(articles, external_articles), key=attrgetter('date'), reverse=True) @property def videos(self): ''' Return the most recent videos and external videos where this person is (one of) the speakers. ''' from ..videos.models import Video from ..externalcontent.models import ExternalVideo videos = Video.objects.none() external_videos = ExternalVideo.objects.none() all_videos = Video.objects.live().public().all() all_external_videos = ExternalVideo.objects.live().public().all() for video in all_videos: if video.has_speaker(self): videos = videos | Video.objects.page(video) for external_video in all_external_videos: if external_video.has_speaker(self): external_videos = external_videos | ExternalVideo.objects.page(external_video) return sorted(chain(videos, external_videos), key=attrgetter('date'), reverse=True) @property def role_group(self): return { 'slug': self.role, 'title': dict(ROLE_CHOICES).get(self.role, ''), }
class PublicationPage( BasicPageAbstract, ContentPage, FeatureablePageAbstract, FromTheArchivesPageAbstract, ShareablePageAbstract, ): """View publication page""" class BookFormats(models.TextChoices): HARDCOVER = ('HC', 'Hardcover') PAPERBACK = ('PB', 'Paperback') TRADE_PB = ('TP', 'Trade PB') class PublicationTypes(models.TextChoices): BOOKS = ('books', 'Books') CIGI_COMMENTARIES = ('cigi_commentaries', 'CIGI Commentaries') CIGI_PAPERS = ('cigi_papers', 'CIGI Papers') COLLECTED_SERIES = ('collected_series', 'Collected Series') CONFERENCE_REPORTS = ('conference_reports', 'Conference Reports') ESSAY_SERIES = ('essay_series', 'Essay Series') POLICY_BRIEFS = ('policy_briefs', 'Policy Briefs') POLICY_MEMOS = ('policy_memos', 'Policy Memos') SPECIAL_REPORTS = ('special_reports', 'Special Reports') SPEECHES = ('speeches', 'Speeches') STUDENT_ESSAY = ('student_essay', 'Student Essay') book_excerpt = RichTextField( blank=True, features=['bold', 'italic', 'link'], verbose_name='Excerpt', ) book_excerpt_download = models.ForeignKey( 'wagtaildocs.Document', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Excerpt Download', ) book_format = models.CharField( blank=True, max_length=2, choices=BookFormats.choices, verbose_name='Format', help_text='Select the formation of this book/publication.', ) book_pages = models.IntegerField( blank=True, null=True, verbose_name='Pages', help_text='Enter the number of pages in the book.', ) book_publisher = models.CharField(blank=True, max_length=255) book_publisher_url = models.URLField(blank=True) book_purchase_links = StreamField( [ ('purchase_link', BookPurchaseLinkBlock()) ], blank=True, ) ctas = StreamField( [ ('ctas', CTABlock()) ], blank=True, verbose_name='Call to Action Buttons', ) editorial_reviews = StreamField( [ ('editorial_review', RichTextBlock( features=['bold', 'italic', 'link'], )), ], blank=True, ) embed_issuu = models.URLField( blank=True, verbose_name='Issuu Embed', help_text='Enter the Issuu URL (https://issuu.com/cigi/docs/modern_conflict_and_ai_web) to add an embedded Issuu document.', ) embed_youtube = models.URLField( blank=True, verbose_name='YouTube Embed', help_text='Enter the YouTube URL (https://www.youtube.com/watch?v=4-Xkn1U1DkA) or short URL (https://youtu.be/o5acQ2GxKbQ) to add an embedded video.', ) image_cover = models.ForeignKey( 'images.CigionlineImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Cover image', help_text='An image of the cover of the publication.', ) image_poster = models.ForeignKey( 'images.CigionlineImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Poster image', help_text='A poster image which will be used in the highlights section of the homepage.', ) isbn = models.CharField( blank=True, max_length=32, verbose_name='ISBN', help_text='Enter the print ISBN for this book.', ) isbn_ebook = models.CharField( blank=True, max_length=32, verbose_name='eBook ISBN', help_text='Enter the ISBN for the eBook version of this publication.', ) isbn_hardcover = models.CharField( blank=True, max_length=32, verbose_name='Hardcover ISBN', help_text='Enter the ISBN for the hardcover version of this publication.', ) pdf_downloads = StreamField( [ ('pdf_download', PDFDownloadBlock()) ], blank=True, verbose_name='PDF Downloads', ) publication_series = models.ForeignKey( 'publications.PublicationSeriesPage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', ) publication_type = models.ForeignKey( 'publications.PublicationTypePage', null=True, blank=False, on_delete=models.SET_NULL, related_name='publications', ) short_description = RichTextField( blank=True, null=False, features=['bold', 'italic', 'link'], ) # Reference field for the Drupal-Wagtail migrator. Can be removed after. drupal_node_id = models.IntegerField(blank=True, null=True) def featured_person_list(self): """ For featured publications, only display the first 3 authors/editors. """ # @todo test person_list = list(self.authors.all()) + list(self.editors.all()) del person_list[3:] result = [] for person in person_list: if person.author: result.append(person.author) elif person.editor: result.append(person.editor) return result def featured_person_list_has_more(self): """ If there are more than 3 authors/editors for featured publications, display "and more". """ # @todo test return (self.author_count + self.editor_count) > 3 def has_book_metadata(self): return ( self.publication_type and self.publication_type.title == 'Books' and (self.book_format or self.book_pages or self.book_publisher or self.isbn or self.isbn_ebook or self.isbn_hardcover) ) content_panels = [ BasicPageAbstract.title_panel, MultiFieldPanel( [ FieldPanel('short_description'), StreamFieldPanel('body'), ], heading='Body', classname='collapsible collapsed', ), MultiFieldPanel( [ PageChooserPanel( 'publication_type', ['publications.PublicationTypePage'], ), FieldPanel('publishing_date'), ], heading='General Information', classname='collapsible collapsed', ), ContentPage.authors_panel, ContentPage.editors_panel, MultiFieldPanel( [ FieldPanel('book_publisher'), FieldPanel('book_publisher_url'), FieldPanel('book_format'), FieldPanel('isbn'), FieldPanel('isbn_hardcover'), FieldPanel('isbn_ebook'), FieldPanel('book_pages'), StreamFieldPanel('book_purchase_links'), DocumentChooserPanel('book_excerpt_download'), FieldPanel('book_excerpt'), ], heading='Book Info', classname='collapsible collapsed', ), MultiFieldPanel( [ StreamFieldPanel('editorial_reviews'), ], heading='Editorial Reviews', classname='collapsible collapsed', ), MultiFieldPanel( [ ImageChooserPanel('image_cover'), ImageChooserPanel('image_poster'), ], heading='Images', classname='collapsible collapsed', ), MultiFieldPanel( [ FieldPanel('embed_issuu'), StreamFieldPanel('pdf_downloads'), StreamFieldPanel('ctas'), FieldPanel('embed_youtube'), ], heading='Media', classname='collapsible collapsed', ), ContentPage.recommended_panel, MultiFieldPanel( [ FieldPanel('topics'), PageChooserPanel( 'publication_series', ['publications.PublicationSeriesPage'], ), FieldPanel('projects'), ], heading='Related', classname='collapsible collapsed', ), FromTheArchivesPageAbstract.from_the_archives_panel, ] promote_panels = Page.promote_panels + [ FeatureablePageAbstract.feature_panel, ShareablePageAbstract.social_panel, SearchablePageAbstract.search_panel, ] search_fields = BasicPageAbstract.search_fields \ + ContentPage.search_fields \ + [ index.FilterField('publication_series'), index.FilterField('publication_type'), index.FilterField('publishing_date'), ] parent_page_types = ['publications.PublicationListPage'] subpage_types = [] templates = 'publications/publication_page.html' class Meta: verbose_name = 'Publication' verbose_name_plural = 'Publications'
class PublicationPage(FoundationMetadataPageMixin, Page): """ This is the root page of a publication. From here the user can browse to the various sections (called chapters). It will have information on the publication, its authors, and metadata from it's children Publications are collections of Articles Publications can also be broken down into Chapters, which are really just child publication pages Each of those Chapters may have several Articles An Article can only belong to one Chapter/Publication Page """ subpage_types = ['ArticlePage', 'PublicationPage'] hero_image = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='publication_hero_image', verbose_name='Publication Hero Image', ) subtitle = models.CharField( blank=True, max_length=250, ) secondary_subtitle = models.CharField( blank=True, max_length=250, ) publication_date = models.DateField("Publication date", null=True, blank=True) publication_file = models.ForeignKey( 'wagtaildocs.Document', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', ) additional_author_copy = models.CharField( help_text="Example: with contributing authors", max_length=100, blank=True, ) notes = RichTextField(blank=True, features=['link', 'bold', 'italic']) contents_title = models.CharField( blank=True, default="Table of Contents", max_length=250, ) content_panels = Page.content_panels + [ MultiFieldPanel( [ FieldPanel('subtitle'), FieldPanel('secondary_subtitle'), FieldPanel('publication_date'), ImageChooserPanel('hero_image'), DocumentChooserPanel('publication_file'), InlinePanel('authors', label='Author'), FieldPanel('additional_author_copy'), ], heading='Hero', ), FieldPanel('contents_title'), FieldPanel('notes'), ] @property def is_chapter_page(self): """ A PublicationPage nested under a PublicationPage is considered to be a "ChapterPage". The templates used very similar logic and structure, and all the fields are the same. """ parent = self.get_parent().specific return parent.__class__ is PublicationPage @property def next_page(self): """ Only applies to Chapter Publication (sub-Publication Pages). Returns a Page object or None. """ if self.is_chapter_page: next_page = self.get_siblings().filter(path__gt=self.path, live=True).first() if not next_page: # If there is no more chapters. Return the parent page. next_page = self.get_parent() return next_page @property def prev_page(self): """ Only applies to Chapter Publication (sub-Publication Pages). Returns a Page object or None. """ if self.is_chapter_page: prev_page = self.get_siblings().filter( path__lt=self.path, live=True).reverse().first() if not prev_page: # If there is no more chapters. Return the parent page. prev_page = self.get_parent() return prev_page @property def zen_nav(self): return True def breadcrumb_list(self): """ Get all the parent PublicationPages and return a QuerySet """ return Page.objects.ancestor_of(self).type(PublicationPage).live() def get_context(self, request, *args, **kwargs): context = super().get_context(request, *args, **kwargs) pages = [] for page in self.get_children(): if request.user.is_authenticated: # User is logged in, and can preview a page. Get all pages, even drafts. pages.append({ 'child': page, 'grandchildren': page.get_children() }) elif page.live: # User is not logged in AND this page is live. Only fetch live grandchild pages. pages.append({ 'child': page, 'grandchildren': page.get_children().live() }) context['child_pages'] = pages return set_main_site_nav_information(self, context, 'Homepage')
class BlogPage(HeadlessPreviewMixin, Page): date = models.DateField("Post date") advert = models.ForeignKey( "home.Advert", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) cover = models.ForeignKey( "wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) book_file = models.ForeignKey( "wagtaildocs.Document", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) featured_media = models.ForeignKey( "wagtailmedia.Media", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) author = models.ForeignKey(AuthorPage, null=True, blank=True, on_delete=models.SET_NULL, related_name="+") body = StreamField(StreamFieldBlock()) content_panels = Page.content_panels + [ FieldPanel("date"), ImageChooserPanel("cover"), StreamFieldPanel("body"), InlinePanel("related_links", label="Related links"), InlinePanel("authors", label="Authors"), FieldPanel("author"), SnippetChooserPanel("advert"), DocumentChooserPanel("book_file"), MediaChooserPanel("featured_media"), ] @property def copy(self): return self graphql_fields = [ GraphQLString("heading"), GraphQLString("date", required=True), GraphQLStreamfield("body"), GraphQLCollection( GraphQLForeignKey, "related_links", "home.blogpagerelatedlink", required=True, item_required=True, ), GraphQLCollection(GraphQLString, "related_urls", source="related_links.url"), GraphQLCollection(GraphQLString, "authors", source="authors.person.name"), GraphQLSnippet("advert", "home.Advert"), GraphQLImage("cover"), GraphQLDocument("book_file"), GraphQLMedia("featured_media"), GraphQLForeignKey("copy", "home.BlogPage"), GraphQLPage("author"), ]
class BookPage(Page): """A page for an individual book.""" book_title = models.CharField( max_length=200, help_text="""The exact title of the book. It will be saved in this format in the database to display on related pages.""") author = models.ForeignKey( 'wagtailcore.Page', on_delete=models.SET_NULL, blank=True, null=True, related_name='+', help_text= """Choose one of your Author Pages to assign a pen name to this book. If you don't see the pen name you want, you might need to set up a new AuthorPage for that pen name!""") series = models.ForeignKey( 'wagtailcore.Page', on_delete=models.SET_NULL, blank=True, null=True, related_name='+', help_text= """Choose a series page to assign this book to a Series. If you don't see the series you want, you might need to set up a new Series Page.""") release_date = models.DateField(blank=True, null=True, help_text="""The book release date. This field is optional.""") genre = models.ManyToManyField('books.Genre', blank=True, help_text="""Optional: Choose one or more genres for this book. If you don't see the genre you need, click "Add Genre to List" from the menu on the left and add in your genre.""") teaser = models.TextField(blank=True, null=True, help_text="""A brief teaser description of your book. This is the paragraph that will show up on the "all books" page.""") description = RichTextField(blank=True, null=True, help_text="""A description of your book.""") content_warnings = models.ManyToManyField('books.ContentWarning', blank=True, help_text="""(Optional): Choose one or more content warnings for this book. If you don't see the warning you want listed here, click "Add Content Warnings on the menu on the left and add in your warnings.""" ) cover_image = models.ForeignKey('wagtailimages.Image', on_delete=models.SET_NULL, related_name='+', null=True, blank=True) other_text = RichTextField(blank=True, null=True, help_text="""This is an optional field. Text added and formatted here will appear close to the bottom of the book page, right above the section for content warnings.""") sort_order = models.IntegerField( null=True, blank=True, help_text="""This field is optional, but if filled in, it lets you pick where this book shows up in the "all books" section of your website. The higher the number, the closer to the top of the page the book will be.""") # Add content panels content_panels = Page.content_panels + [ FieldPanel('book_title', classname="full"), PageChooserPanel('author', 'books.AuthorPage'), FieldPanel('release_date'), FieldPanel('genre'), FieldPanel('description', classname="full"), ImageChooserPanel('cover_image'), FieldPanel('sort_order'), FieldPanel('content_warnings'), InlinePanel('buy_links', label="Buy Links"), InlinePanel('book_reviews', label="Book Reviews"), ] # Parent page / subpage type rules parent_page_types = ['books.BooksIndexPage']
class VideoIndexPage(CoderedArticleIndexPage): class Meta: verbose_name = "Video Landing Page" hits = models.IntegerField(default=0, editable=False) body = None def add_hits(self): self.hits += 1 self._meta.get_field( "index_order_by").choices = self.index_order_by_choices self.save() return "" search_fields = [] # Panel # Friend panels promote_panels = [ MultiFieldPanel( [ FieldPanel("slug"), FieldPanel("seo_title"), FieldPanel("search_description"), ImageChooserPanel("og_image"), ], _("Page Meta Data"), ), ] # Override to not contain template form layout_panels = [] # Override to become empty body_content_panels = [] # Override without content walls settings_panels = Page.settings_panels # Override with additional hits attribute content_panels = Page.content_panels + [ MultiFieldPanel( [ ReadOnlyPanel("hits", heading="Hits"), ], _("Publication Info"), ), ] # Override to specify custom index ordering choice/default. index_query_pagemodel = "video.VideoPage" template = "video/video_index_page.html" @cached_classmethod def get_edit_handler(cls): # noqa """ Override to "lazy load" the panels overriden by subclasses. """ panels = [ ObjectList( cls.content_panels + cls.body_content_panels + cls.bottom_content_panels, heading=_("Content"), ), ObjectList(cls.classify_panels, heading=_("Classify")), ObjectList(cls.promote_panels, heading=_("SEO"), classname="seo"), ObjectList(cls.settings_panels, heading=_("Settings"), classname="settings"), ] if cls.integration_panels: panels.append( ObjectList( cls.integration_panels, heading="Integrations", classname="integrations", )) return TabbedInterface(panels).bind_to(model=cls) # Only allow VideoPages beneath this page. parent_page_types = ["home.HomePage"]