def setUp(self): # a custom tabbed interface for EventPage self.EventPageTabbedInterface = TabbedInterface([ ObjectList([ FieldPanel('title', widget=forms.Textarea), FieldPanel('date_from'), FieldPanel('date_to'), ], heading='Event details', classname="shiny"), ObjectList([ InlinePanel('speakers', label="Speakers"), ], heading='Speakers'), ]).bind_to_model(EventPage)
def get_page_edit_handler(page_class): if page_class not in PAGE_EDIT_HANDLERS: if hasattr(page_class, 'edit_handler'): # use the edit handler specified on the page class edit_handler = page_class.edit_handler else: # construct a TabbedInterface made up of content_panels, promote_panels # and settings_panels, skipping any which are empty tabs = [] if page_class.content_panels: tabs.append(ObjectList(page_class.content_panels, heading=_('Content'))) if page_class.promote_panels: tabs.append(ObjectList(page_class.promote_panels, heading=_('Promote'))) if page_class.settings_panels: tabs.append(ObjectList(page_class.settings_panels, heading=_('Settings'), classname="settings")) edit_handler = TabbedInterface(tabs) PAGE_EDIT_HANDLERS[page_class] = edit_handler.bind_to_model(page_class) return PAGE_EDIT_HANDLERS[page_class]
class HomePage(CFGOVPage): header = StreamField([ ('info_unit', molecules.InfoUnit()), ('half_width_link_blob', molecules.HalfWidthLinkBlob()), ], blank=True) latest_updates = StreamField([ ('posts', blocks.ListBlock(blocks.StructBlock([ ('categories', blocks.ChoiceBlock(choices=ref.limited_categories, required=False)), ('link', atoms.Hyperlink()), ('date', blocks.DateTimeBlock(required=False)), ]))), ], blank=True) # General content tab content_panels = CFGOVPage.content_panels + [ StreamFieldPanel('header'), StreamFieldPanel('latest_updates'), ] # Tab handler interface edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), ObjectList(CFGOVPage.sidefoot_panels, heading='Sidebar'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) # Sets page to only be createable at the root parent_page_types = ['wagtailcore.Page'] template = 'index.html' objects = PageManager() def get_category_name(self, category_icon_name): cats = dict(ref.limited_categories) return cats[str(category_icon_name)] def get_context(self, request): context = super(HomePage, self).get_context(request) return context
class ProjectPage(ThemeablePage): description = RichTextField(blank=True, default="") search_fields = Page.search_fields + [ index.SearchField('description', partial_match=True), ] def search_result_text(self): if self.description: self.search_result_text = self.description[0:240] return self.search_result_text def project_articles(self): return self.articlepage_set.filter( live=True).order_by("-first_published_at") def project_series(self): return self.seriespage_set.filter( live=True).order_by("-first_published_at") def get_related_series(self, series_page): return self.seriespage_set.filter(live=True).exclude( pk=series_page.pk).order_by("-first_published_at") def __str__(self): return "{}".format(self.title) content_panels = Page.content_panels + [ RichTextFieldPanel('description'), ] style_panels = ThemeablePage.style_panels edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(style_panels, heading='Page Style Options'), ObjectList(Page.promote_panels, heading='Promote'), ObjectList(Page.settings_panels, heading='Settings', classname="settings"), ])
class CampsitePage(Page, ApiCampsiteMixin, MetaMixin): subpage_types = [] parent_page_types = ['CampsiteIndexPage'] alerts = GenericRelation(Alert) facilities = ClusterTaggableManager(through=CampsitePageFacility, blank=True, related_name='facility_campsites') landscapes = ClusterTaggableManager(through=CampsitePageLandscape, blank=True, related_name='landscape_campsites') activities = ClusterTaggableManager(through=CampsitePageActivity, blank=True, related_name='activity_campsites') content_panels = Page.content_panels + [ ImageChooserPanel('meta_image'), FieldPanel('facilities'), FieldPanel('landscapes'), FieldPanel('activities'), ] api_panels = COMMON_PANELS + [ # campsite specific fields FieldPanel('dogs_allowed'), FieldPanel('is_free'), FieldPanel('powered_sites'), FieldPanel('unpowered_sites'), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(api_panels, heading='API fields'), ObjectList(MetaMixin.meta_panels, heading='Meta'), ObjectList(Page.promote_panels, heading='Promote'), ObjectList(Page.settings_panels, heading='Settings', classname="settings"), ]) class Meta: verbose_name = "Campsite Page"
class AdvertWithTabbedInterface(models.Model): url = models.URLField(null=True, blank=True) text = models.CharField(max_length=191) something_else = models.CharField(max_length=191) advert_panels = [ FieldPanel('url'), FieldPanel('text'), ] other_panels = [ FieldPanel('something_else'), ] edit_handler = TabbedInterface([ ObjectList(advert_panels, heading='Advert'), ObjectList(other_panels, heading='Other'), ]) def __str__(self): return self.text
class BrowseFilterablePage(FilterableFeedPageMixin, FilterableListMixin, CFGOVPage): header = StreamField([ ('text_introduction', molecules.TextIntroduction()), ('featured_content', organisms.FeaturedContent()), ]) content = StreamField(BrowseFilterableContent) secondary_nav_exclude_sibling_pages = models.BooleanField(default=False) # General content tab content_panels = CFGOVPage.content_panels + [ StreamFieldPanel('header'), StreamFieldPanel('content'), ] sidefoot_panels = CFGOVPage.sidefoot_panels + [ FieldPanel('secondary_nav_exclude_sibling_pages'), ] # Tab handler interface edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), ObjectList(sidefoot_panels, heading='SideFoot'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) template = 'browse-filterable/index.html' objects = PageManager() search_fields = CFGOVPage.search_fields + [ index.SearchField('content'), index.SearchField('header') ] @property def page_js(self): return (super(BrowseFilterablePage, self).page_js + ['secondary-navigation.js'])
class PayingForCollegePage(CFGOVPage): """A base class for our suite of PFC pages.""" header = StreamField([ ('text_introduction', molecules.TextIntroduction()), ('featured_content', organisms.FeaturedContent()), ], blank=True) content_panels = CFGOVPage.content_panels + [ StreamFieldPanel('header'), StreamFieldPanel('content'), ] # Tab handler interface edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), ObjectList(CFGOVPage.sidefoot_panels, heading='Sidebar'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) objects = CFGOVPageManager() class Meta: abstract = True
class AnswerCategoryPage(FilterableFeedPageMixin, FilterableListMixin, CFGOVPage): """ Page type for Ask CFPB parent-category pages. """ from .django import Category objects = PageManager() content = StreamField([ ('filter_controls', FilterControls()), ], null=True) ask_category = models.ForeignKey(Category, blank=True, null=True, on_delete=models.PROTECT, related_name='category_page') content_panels = CFGOVPage.content_panels + [ FieldPanel('ask_category', Category), StreamFieldPanel('content'), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) template = 'ask-cfpb/category-page.html' def add_page_js(self, js): super(AnswerCategoryPage, self).add_page_js(js) js['template'] += ['secondary-navigation.js'] def get_context(self, request, *args, **kwargs): context = super(AnswerCategoryPage, self).get_context(request, *args, **kwargs) context.update({ 'choices': self.ask_category.subcategories.all().values_list('slug', 'name') }) return context
class BrowseFilterablePage(FilterableFeedPageMixin, FilterableListMixin, CFGOVPage): header = StreamField([ ('text_introduction', molecules.TextIntroduction()), ('featured_content', molecules.FeaturedContent()), ]) content = StreamField([ ('full_width_text', organisms.FullWidthText()), ('filter_controls', organisms.FilterControls()), ('feedback', v1_blocks.Feedback()), ]) secondary_nav_exclude_sibling_pages = models.BooleanField(default=False) # General content tab content_panels = CFGOVPage.content_panels + [ StreamFieldPanel('header'), StreamFieldPanel('content'), ] sidefoot_panels = CFGOVPage.sidefoot_panels + [ FieldPanel('secondary_nav_exclude_sibling_pages'), ] # Tab handler interface edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), ObjectList(sidefoot_panels, heading='SideFoot'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) template = 'browse-filterable/index.html' objects = PageManager() def add_page_js(self, js): super(BrowseFilterablePage, self).add_page_js(js) js['template'] += ['secondary-navigation.js']
class MerchantPage(Page): logo = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) def get_context(self, request): context = super(MerchantPage, self).get_context(request) context['product_list'] = Product.objects.order_by('?')[:8] context['random_categories'] = Category.objects.order_by('?')[:2] return context subpage_types = ['products.Category'] content_panels = [ FieldPanel('title'), ImageChooserPanel('logo') ] main_slide_panels = [ InlinePanel('related_slide_pictures', label='Slide principal') ] social_panels = [ InlinePanel('related_social_media_links', label='Redes sociales') ] contact_panels = [ InlinePanel('related_contact_config', label='Contacto') ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Configuración general'), ObjectList(main_slide_panels, heading='Slide principal'), ObjectList(social_panels, heading='Redes sociales'), ObjectList(contact_panels, heading='Contacto'), ObjectList(Page.promote_panels, heading='SEO') ])
class GeoPage(Page): address = models.CharField(max_length=250, blank=True, null=True) location = models.PointField(srid=4326, null=True, blank=True) content_panels = Page.content_panels + [ InlinePanel('related_locations', label="Related locations"), ] location_panels = [ MultiFieldPanel([ FieldPanel('address'), GeoPanel('location', address_field='address'), ], heading='Location') ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(location_panels, heading='Location'), ObjectList(Page.settings_panels, heading='Settings', classname="settings"), ])
class ExternalArticleListPage(PaginatedListPageMixin, ThemeablePage): subpage_types = ['ExternalArticlePage'] articles_per_page = models.IntegerField(default=20) counter_field_name = 'articles_per_page' counter_context_name = 'articles' @property def subpages(self): subpages = ExternalArticlePage.objects.live().descendant_of(self).order_by('-first_published_at') return subpages content_panels = Page.content_panels + [ FieldPanel('articles_per_page'), ] style_panels = ThemeablePage.style_panels edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(style_panels, heading='Page Style Options'), ObjectList(Page.promote_panels, heading='Promote'), ObjectList(Page.settings_panels, heading='Settings', classname="settings"), ])
class EventListPage(PaginatedListPageMixin, Page): subpage_types = ['EventPage'] events_per_page = models.IntegerField(default=20) counter_field_name = 'events_per_page' counter_context_name = 'events' @property def subpages(self): # Get list of live event pages that are descendants of this page subpages = EventPage.objects.live().descendant_of(self).order_by( '-date') return subpages content_panels = Page.content_panels + [FieldPanel('events_per_page')] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(Page.promote_panels, heading='Promote'), ObjectList(Page.settings_panels, heading='Settings', classname="settings"), ])
class LandingPage(CFGOVPage): header = StreamField([ ('hero', molecules.Hero()), ('text_introduction', molecules.TextIntroduction()), ], blank=True) content = StreamField([ ('image_text_25_75_group', organisms.ImageText2575Group()), ('image_text_50_50_group', organisms.ImageText5050Group()), ('half_width_link_blob_group', organisms.HalfWidthLinkBlobGroup()), ('well', organisms.Well()), ], blank=True) # General content tab content_panels = CFGOVPage.content_panels + [ StreamFieldPanel('header'), StreamFieldPanel('content'), ] # Tab handler interface edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), ObjectList(CFGOVPage.sidefoot_panels, heading='Sidebar'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) def get_context(self, request, *args, **kwargs): return { PAGE_TEMPLATE_VAR: self, 'self': self, 'request': request, } template = 'landing-page/index.html'
class SublandingFilterablePage(FilterableFeedPageMixin, base.CFGOVPage): header = StreamField([ ('hero', molecules.Hero()), ], blank=True) content = StreamField([ ('text_introduction', molecules.TextIntroduction()), ('full_width_text', organisms.FullWidthText()), ('filter_controls', organisms.FilterControls()), ('featured_content', molecules.FeaturedContent()), ]) # General content tab content_panels = CFGOVPage.content_panels + [ StreamFieldPanel('header'), StreamFieldPanel('content'), ] # Tab handler interface edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), ObjectList(CFGOVPage.sidefoot_panels, heading='Sidebar'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) template = 'sublanding-page/index.html' def get_context(self, request, *args, **kwargs): context = super(SublandingFilterablePage, self).get_context(request, *args, **kwargs) return filterable_context.get_context(self, request, context) def get_form_class(self): return forms.FilterableListForm def get_page_set(self, form, hostname): return filterable_context.get_page_set(self, form, hostname)
class HomePage(CFGOVPage): header = StreamField([ ('info_unit', molecules.InfoUnit()), ('half_width_link_blob', molecules.HalfWidthLinkBlob()), ], blank=True) # General content tab content_panels = CFGOVPage.content_panels + [ StreamFieldPanel('header'), InlinePanel( 'excluded_updates', label='Pages excluded from Latest Updates', help_text=('This block automatically displays the six most ' 'recently published blog posts, press releases, ' 'speeches, testimonies, events, or op-eds. If you want ' 'to exclude a page from appearing as a recent update ' 'in this block, add it as an excluded page.')), ] # Tab handler interface edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), ObjectList(CFGOVPage.sidefoot_panels, heading='Sidebar'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) # Sets page to only be createable at the root parent_page_types = ['wagtailcore.Page'] objects = PageManager() search_fields = CFGOVPage.search_fields + [index.SearchField('header')] @property def page_js(self): return super(HomePage, self).page_js + ['home-page.js'] def get_category_name(self, category_icon_name): cats = dict(ref.limited_categories) return cats[str(category_icon_name)] def get_context(self, request): context = super(HomePage, self).get_context(request) context.update({ 'carousel_items': self.get_carousel_items(), 'info_units': self.get_info_units(), 'latest_updates': self.get_latest_updates(request), }) return context def get_carousel_items(self): return _carousel_items_by_language[self.language] def get_info_units(self): return [ molecules.InfoUnit().to_python(info_unit) for info_unit in _info_units_by_language[self.language] ] def get_latest_updates(self, request): # TODO: There should be a way to express this as part of the query # rather than evaluating it in Python. excluded_updates_pks = [ e.excluded_page.pk for e in self.excluded_updates.all() ] latest_pages = CFGOVPage.objects.in_site( request.site).exclude(pk__in=excluded_updates_pks).filter( Q(content_type__app_label='v1') & (Q(content_type__model='blogpage') | Q(content_type__model='eventpage') | Q(content_type__model='newsroompage')), live=True, ).distinct().order_by('-first_published_at')[:6] return latest_pages def get_template(self, request): if flag_enabled('NEW_HOME_PAGE', request=request): return 'home-page/index_new.html' else: return 'home-page/index_%s.html' % self.language
class HomePage(ThemeablePage): subpage_types = [ article_models.ArticleListPage, article_models.SeriesListPage, article_models.TopicListPage, people_models.ContributorListPage, project_models.ProjectListPage, newsletter_models.NewsletterListPage, event_models.EventListPage, article_models.ExternalArticleListPage, jobs_models.JobPostingListPage, StreamPage, ] featured_item = models.ForeignKey('wagtailcore.Page', null=True, blank=True, on_delete=models.SET_NULL, related_name='+') number_of_rows_of_articles = models.IntegerField(default=12, verbose_name="Rows") number_of_columns_of_articles = models.IntegerField(default=3, verbose_name="Columns") number_of_rows_of_series = models.IntegerField(default=1, verbose_name="Rows") number_of_rows_of_external_articles = models.IntegerField( default=2, verbose_name="Rows") number_of_columns_of_external_articles = models.IntegerField( default=2, verbose_name="Columns") number_of_rows_of_visualizations = models.IntegerField(default=2, verbose_name="Rows") number_of_columns_of_visualizations = models.IntegerField( default=2, verbose_name="Columns") full_bleed_image_size = models.PositiveSmallIntegerField( default=90, help_text= "Enter a value from 0 - 100, indicating the percentage of the screen to use for the featured image layout." ) _articles = None _most_popular_article = None def __str__(self): return self.title def get_article_set(self, columns, rows, article_list, used): if columns == 0 and rows == 0 or not article_list: return [] current_set = [] while rows > 0: row, height = self._fill_row(columns, article_list, used, rows) if height == 0: break current_set.append(row) rows = rows - height return current_set def _fill_row(self, columns, article_list, used, max_height): if columns == 0 or not article_list: return [], 0 for article in article_list: typed_article = article.content_type.get_object_for_this_type( id=article.id) if typed_article.feature_style.number_of_columns <= columns \ and typed_article.id not in used\ and typed_article.feature_style.number_of_rows <= max_height: columns = columns - typed_article.feature_style.number_of_columns used.append(typed_article.id) row = [typed_article] max_height = min(max_height, typed_article.feature_style.number_of_rows) if max_height > 1 and columns > 0: subset = self.get_article_set(columns, max_height, article_list, used) row.append(subset) else: recursive_row, height = self._fill_row( columns, article_list, used, max_height) row.extend(recursive_row) return row, max_height return [], 0 @property def articles(self): if self._articles is not None: return self._articles article_content_type = ContentType.objects.get_for_model( article_models.ArticlePage) series_content_type = ContentType.objects.get_for_model( article_models.SeriesPage) articles = Page.objects.live().filter( models.Q(content_type=article_content_type) | models.Q(content_type=series_content_type)).annotate( sticky=models.Case( models.When(models.Q(seriespage__sticky=True) | (models.Q(articlepage__sticky=True)), then=models.Value(1)), default=models.Value(0), output_field=models.IntegerField())).exclude( models.Q(seriespage__slippery=True) | models.Q(articlepage__slippery=True)).order_by( "-sticky", "-first_published_at") articles = list(articles[:42]) used = [] if self.featured_item: used.append(self.featured_item.id) self._articles = self.get_article_set( self.number_of_columns_of_articles, self.number_of_rows_of_articles, articles, used) return self._articles @property def most_popular_article(self): if self._most_popular_article is not None: return self._most_popular_article def get_views(article): try: if article.analytics: return article.analytics.last_period_views except: return 0 # flatten the list of lists with generator articles = itertools.chain.from_iterable(self.articles) articles_by_popularity = sorted(articles, key=get_views) self._most_popular_article = articles_by_popularity[-1] return self._most_popular_article @property def typed_featured_item(self): if self.featured_item: featured_item = self.featured_item.content_type.get_object_for_this_type( id=self.featured_item.id) return featured_item def get_rows(self, number_of_rows, number_of_columns, items): rows = [] for row_index in range(0, number_of_rows): row = items[(row_index * number_of_columns):(row_index * number_of_columns) + number_of_columns] rows.append(row) return rows @property def external_articles(self): number_of_external_articles = self.number_of_rows_of_external_articles * self.number_of_columns_of_external_articles external_article_list = article_models.ExternalArticlePage.objects.live( ).order_by("-first_published_at")[:number_of_external_articles] return self.get_rows(self.number_of_rows_of_external_articles, self.number_of_columns_of_external_articles, external_article_list) @property def graphics(self): number_of_graphics = self.number_of_rows_of_visualizations * self.number_of_columns_of_visualizations graphics_list = article_models.ArticlePage.objects.live().filter( visualization=True).annotate(sticky_value=models.Case( models.When(models.Q(sticky_for_type_section=True), then=models.Value(1)), default=models.Value(0), output_field=models.IntegerField())).exclude( slippery_for_type_section=True).order_by( "-sticky_value", "-first_published_at")[:number_of_graphics] return self.get_rows(self.number_of_rows_of_visualizations, self.number_of_columns_of_visualizations, graphics_list) @property def series(self): number_of_series = self.number_of_rows_of_series series_list = article_models.SeriesPage.objects.live().annotate( sticky_value=models.Case(models.When( models.Q(sticky_for_type_section=True), then=models.Value(1)), default=models.Value(0), output_field=models.IntegerField()) ).exclude(slippery_for_type_section=True).order_by( "-sticky_value", "-first_published_at")[:number_of_series] return self.get_rows(self.number_of_rows_of_series, 1, series_list) content_panels = Page.content_panels + [ MultiFieldPanel([ PageChooserPanel("featured_item", "wagtailcore.Page"), FieldPanel("full_bleed_image_size"), ], heading="Main Feature"), MultiFieldPanel([ FieldPanel("number_of_rows_of_articles"), FieldPanel("number_of_columns_of_articles"), ], heading="Main Feed Settings"), MultiFieldPanel([ FieldPanel("number_of_rows_of_series"), ], heading="Series Section Settings"), MultiFieldPanel([ FieldPanel("number_of_rows_of_external_articles"), FieldPanel("number_of_columns_of_external_articles"), ], heading="External Articles Section Settings"), MultiFieldPanel([ FieldPanel("number_of_rows_of_visualizations"), FieldPanel("number_of_columns_of_visualizations"), ], heading="Visualization Section Settings"), ] style_panels = ThemeablePage.style_panels edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(style_panels, heading='Page Style Options'), ObjectList(Page.promote_panels, heading='Promote'), ObjectList(Page.settings_panels, heading='Settings', classname="settings"), ])
class CFGOVPage(Page): authors = ClusterTaggableManager(through=CFGOVAuthoredPages, blank=True, verbose_name='Authors', help_text='A comma separated list of ' + 'authors.', related_name='authored_pages') tags = ClusterTaggableManager(through=CFGOVTaggedPages, blank=True, related_name='tagged_pages') shared = models.BooleanField(default=False) has_unshared_changes = models.BooleanField(default=False) language = models.CharField(choices=ref.supported_languagues, default='en', max_length=2) # This is used solely for subclassing pages we want to make at the CFPB. is_creatable = False objects = CFGOVPageManager() # These fields show up in either the sidebar or the footer of the page # depending on the page type. sidefoot = StreamField([ ('call_to_action', molecules.CallToAction()), ('related_links', molecules.RelatedLinks()), ('related_posts', organisms.RelatedPosts()), ('related_metadata', molecules.RelatedMetadata()), ('email_signup', organisms.EmailSignUp()), ('sidebar_contact', organisms.SidebarContactInfo()), ('rss_feed', molecules.RSSFeed()), ('social_media', molecules.SocialMedia()), ('reusable_text', ReusableTextChooserBlock(ReusableText)), ], blank=True) # Panels sidefoot_panels = [ StreamFieldPanel('sidefoot'), ] settings_panels = [ MultiFieldPanel(Page.promote_panels, 'Settings'), InlinePanel('categories', label="Categories", max_num=2), FieldPanel('tags', 'Tags'), FieldPanel('authors', 'Authors'), MultiFieldPanel(Page.settings_panels, 'Scheduled Publishing'), FieldPanel('language', 'language'), ] # Tab handler interface guide because it must be repeated for each subclass edit_handler = TabbedInterface([ ObjectList(Page.content_panels, heading='General Content'), ObjectList(sidefoot_panels, heading='Sidebar/Footer'), ObjectList(settings_panels, heading='Configuration'), ]) def get_authors(self): """ Returns a sorted list of authors. Default is alphabetical """ return self.alphabetize_authors() def alphabetize_authors(self): """ Alphabetize authors of this page by last name, then first name if needed """ # First sort by first name author_names = self.authors.order_by('name') # Then sort by last name return sorted(author_names, key=lambda x: x.name.split()[-1]) def generate_view_more_url(self, request): activity_log = CFGOVPage.objects.get(slug='activity-log').specific tags = [] index = activity_log.form_id() tags = urlencode([('filter%s_topics' % index, tag) for tag in self.tags.slugs()]) return (get_protected_url({'request': request}, activity_log) + '?' + tags) def related_posts(self, block, hostname): from v1.models.learn_page import AbstractFilterPage related = {} query = models.Q(('tags__name__in', self.tags.names())) search_types = [ ('blog', 'posts', 'Blog', query), ('newsroom', 'newsroom', 'Newsroom', query), ('events', 'events', 'Events', query), ] def fetch_children_by_specific_category(block, parent_slug): """ This used to be a Page.objects.get, which would throw an exception if the requested parent wasn't found. As of Django 1.6, you can now do Page.objects.filter().first(); the advantage here is that you can check for None right away and not have to rely on catching exceptions, which in any case didn't do anything useful other than to print an error message. Instead, we just return an empty query which has no effect on the final result. """ parent = Page.objects.filter(slug=parent_slug).first() if parent: child_query = Page.objects.child_of_q(parent) if 'specific_categories' in block.value: child_query &= specific_categories_query( block, parent_slug) else: child_query = Q() return child_query def specific_categories_query(block, parent_slug): specific_categories = ref.related_posts_category_lookup( block.value['specific_categories']) choices = [c[0] for c in ref.choices_for_page_type(parent_slug)] categories = [c for c in specific_categories if c in choices] if categories: return Q(('categories__name__in', categories)) else: return Q() for parent_slug, search_type, search_type_name, search_query in \ search_types: search_query &= fetch_children_by_specific_category( block, parent_slug) if parent_slug == 'events': search_query |= fetch_children_by_specific_category( block, 'archive-past-events') & query relate = block.value.get('relate_{}'.format(search_type), None) if relate: related[search_type_name] = (AbstractFilterPage.objects.live( ).filter(search_query).distinct().exclude(id=self.id).order_by( '-date_published')[:block.value['limit']]) # Return a dictionary of lists of each type when there's at least one # hit for that type. return { search_type: queryset for search_type, queryset in related.items() if queryset } def get_breadcrumbs(self, request): ancestors = self.get_ancestors() home_page_children = request.site.root_page.get_children() for i, ancestor in enumerate(ancestors): if ancestor in home_page_children: return [ancestor for ancestor in ancestors[i + 1:]] return [] def get_appropriate_descendants(self, hostname, inclusive=True): return CFGOVPage.objects.live().descendant_of(self, inclusive) def get_appropriate_siblings(self, hostname, inclusive=True): return CFGOVPage.objects.live().sibling_of(self, inclusive) def get_next_appropriate_siblings(self, hostname, inclusive=False): return self.get_appropriate_siblings( hostname=hostname, inclusive=inclusive).filter(path__gte=self.path).order_by('path') def get_prev_appropriate_siblings(self, hostname, inclusive=False): return self.get_appropriate_siblings( hostname=hostname, inclusive=inclusive).filter(path__lte=self.path).order_by('-path') def get_context(self, request, *args, **kwargs): context = super(CFGOVPage, self).get_context(request, *args, **kwargs) for hook in hooks.get_hooks('cfgovpage_context_handlers'): hook(self, request, context, *args, **kwargs) return context def serve(self, request, *args, **kwargs): """ If request is ajax, then return the ajax request handler response, else return the super. """ if request.method == 'POST': return self.serve_post(request, *args, **kwargs) return super(CFGOVPage, self).serve(request, *args, **kwargs) def serve_post(self, request, *args, **kwargs): """ Attempts to retreive form_id from the POST request and returns a JSON response. If form_id is found, it returns the response from the block method retrieval. If form_id is not found, it returns an error response. """ form_id = request.POST.get('form_id', None) if not form_id: if request.is_ajax(): return JsonResponse({'result': 'error'}, status=400) return HttpResponseBadRequest(self.url) sfname, index = form_id.split('-')[1:] streamfield = getattr(self, sfname) module = streamfield[int(index)] result = module.block.get_result(self, request, module.value, True) if isinstance(result, HttpResponse): return result else: context = self.get_context(request, *args, **kwargs) context['form_modules'][sfname].update({int(index): result}) return TemplateResponse( request, self.get_template(request, *args, **kwargs), context) class Meta: app_label = 'v1' def parent(self): parent = self.get_ancestors(inclusive=False).reverse()[0].specific return parent # To be overriden if page type requires JS files every time # 'template' is used as the key for front-end consistency def add_page_js(self, js): js['template'] = [] # Retrieves the stream values on a page from it's Streamfield def _get_streamfield_blocks(self): lst = [ value for key, value in vars(self).iteritems() if type(value) is StreamValue ] return list(chain(*lst)) # Gets the JS from the Streamfield data def _add_streamfield_js(self, js): # Create a dict with keys ordered organisms, molecules, then atoms for child in self._get_streamfield_blocks(): self._add_block_js(child.block, js) # Recursively search the blocks and classes for declared Media.js def _add_block_js(self, block, js): self._assign_js(block, js) if issubclass(type(block), blocks.StructBlock): for child in block.child_blocks.values(): self._add_block_js(child, js) elif issubclass(type(block), blocks.ListBlock): self._add_block_js(block.child_block, js) # Assign the Media js to the dictionary appropriately def _assign_js(self, obj, js): try: if hasattr(obj.Media, 'js'): for key in js.keys(): if obj.__module__.endswith(key): js[key] += obj.Media.js if not [ key for key in js.keys() if obj.__module__.endswith(key) ]: js.update({'other': obj.Media.js}) except: pass # Returns all the JS files specific to this page and it's current # Streamfield's blocks @property def media(self): js = OrderedDict() for key in ['template', 'organisms', 'molecules', 'atoms']: js.update({key: []}) self.add_page_js(js) self._add_streamfield_js(js) for key, js_files in js.iteritems(): js[key] = OrderedDict.fromkeys(js_files).keys() return js
FieldPanel('slug'), InlinePanel('advert_placements', label="Adverts"), ] StandardIndex.promote_panels = [] class StandardChild(Page): pass # Test overriding edit_handler with a custom one StandardChild.edit_handler = TabbedInterface( [ ObjectList(StandardChild.content_panels, heading='Content'), ObjectList(StandardChild.promote_panels, heading='Promote'), ObjectList(StandardChild.settings_panels, heading='Settings', classname='settings'), ObjectList([], heading='Dinosaurs'), ], base_form_class=WagtailAdminPageForm) class BusinessIndex(Page): """ Can be placed anywhere, can only have Business children """ subpage_types = ['tests.BusinessChild', 'tests.BusinessSubIndex'] class BusinessSubIndex(Page): """ Can be placed under BusinessIndex, and have BusinessChild children """ # BusinessNowherePage is 'incorrectly' added here as a possible child.
class TabbedSettings(TestSetting): edit_handler = TabbedInterface([ ObjectList([FieldPanel('title')], heading='First tab'), ObjectList([FieldPanel('email')], heading='Second tab'), ])
class ActivityIndexPage(CFGOVPage): """ A model for the Activity Search page. """ subpage_types = ['teachers_digital_platform.ActivityPage'] objects = CFGOVPageManager() header = StreamField([ ('text_introduction', molecules.TextIntroduction()), ], blank=True) results = {} content_panels = CFGOVPage.content_panels + [ StreamFieldPanel('header'), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), ObjectList(CFGOVPage.sidefoot_panels, heading='Sidebar/Footer'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) @classmethod def can_create_at(cls, parent): # You can only create one of these! return super(ActivityIndexPage, cls).can_create_at(parent) \ and not cls.objects.exists() def get_template(self, request): template = 'teachers_digital_platform/activity_index_page.html' if 'partial' in request.GET: template = 'teachers_digital_platform/activity_search_facets_and_results.html' # noqa: E501 return template def get_context(self, request, *args, **kwargs): facet_map = ( ('building_block', (ActivityBuildingBlock, False, 10)), ('school_subject', (ActivitySchoolSubject, False, 25)), ('topic', (ActivityTopic, True, 25)), ('grade_level', (ActivityGradeLevel, False, 10)), ('age_range', (ActivityAgeRange, False, 10)), ('student_characteristics', (ActivityStudentCharacteristics, False, 10)), # noqa: E501 ('activity_type', (ActivityType, False, 10)), ('teaching_strategy', (ActivityTeachingStrategy, False, 25)), ('blooms_taxonomy_level', (ActivityBloomsTaxonomyLevel, False, 25)), # noqa: E501 ('activity_duration', (ActivityDuration, False, 10)), ('jump_start_coalition', (ActivityJumpStartCoalition, False, 25)), ('council_for_economic_education', (ActivityCouncilForEconEd, False, 25)), # noqa: E501 ) search_query = request.GET.get('q', '') # haystack cleans this string sqs = SearchQuerySet().models(ActivityPage).filter(live=True) total_activities = sqs.count() # Load selected facets selected_facets = {} facet_queries = {} for facet, facet_config in facet_map: sqs = sqs.facet(str(facet), size=facet_config[2]) if facet in request.GET and request.GET.get(facet): selected_facets[facet] = [ int(value) for value in request.GET.getlist(facet) if value.isdigit() ] facet_queries[facet] = facet + '_exact:' + ( " OR " + facet + "_exact:").join( [str(value) for value in selected_facets[facet]]) payload = { 'search_query': search_query, 'results': [], 'total_results': 0, 'total_activities': total_activities, 'selected_facets': selected_facets, 'facet_queries': facet_queries, 'all_facets': {}, } # Apply search query if it exists, but don't apply facets if search_query: sqs = sqs.filter(content=search_query).order_by( '-_score', '-date') # noqa: E501 else: sqs = sqs.order_by('-date') # Get all facets and their counts facet_counts = sqs.facet_counts() all_facets = self.get_all_facets(facet_map, sqs, facet_counts, facet_queries, selected_facets) # noqa: E501 # List all facet blocks that need to be expanded always_expanded = {'building_block', 'topic', 'school_subject'} conditionally_expanded = { facet_name for facet_name, facet_items in all_facets.items() if any(facet['selected'] is True for facet in facet_items) } expanded_facets = always_expanded.union(set(conditionally_expanded)) payload.update({ 'facet_counts': facet_counts, 'all_facets': all_facets, 'expanded_facets': expanded_facets, }) # Apply all the active facet values to our search results for facet_narrow_query in facet_queries.values(): sqs = sqs.narrow(facet_narrow_query) results = [activity.object for activity in sqs] total_results = sqs.count() payload.update({ 'results': results, 'total_results': total_results, }) self.results = payload results_per_page = validate_results_per_page(request) paginator = Paginator(payload['results'], results_per_page) current_page = validate_page_number(request, paginator) paginated_page = paginator.page(current_page) context = super(ActivityIndexPage, self).get_context(request) context.update({ 'facet_counts': facet_counts, 'facets': all_facets, 'activities': paginated_page, 'total_results': total_results, 'results_per_page': results_per_page, 'current_page': current_page, 'paginator': paginator, 'show_filters': bool(facet_queries), }) return context def get_all_facets(self, facet_map, sqs, facet_counts, facet_queries, selected_facets): # noqa: E501 all_facets = {} if 'fields' in facet_counts: for facet, facet_config in facet_map: class_object, is_nested, max_facet_count = facet_config all_facets_sqs = sqs other_facet_queries = [ facet_query for facet_query_name, facet_query in facet_queries.items() # noqa: E501 if facet != facet_query_name ] for other_facet_query in other_facet_queries: all_facets_sqs = all_facets_sqs.narrow( str(other_facet_query)) # noqa: E501 narrowed_facet_counts = all_facets_sqs.facet_counts() if 'fields' in narrowed_facet_counts and facet in narrowed_facet_counts[ 'fields']: # noqa: E501 narrowed_facets = [ value[0] for value in narrowed_facet_counts['fields'][facet] ] # noqa: E501 narrowed_selected_facets = selected_facets[ facet] if facet in selected_facets else [ ] # noqa: E501 if is_nested: all_facets[facet] = self.get_nested_facets( class_object, narrowed_facets, narrowed_selected_facets) else: all_facets[facet] = self.get_flat_facets( class_object, narrowed_facets, narrowed_selected_facets) return all_facets def get_flat_facets(self, class_object, narrowed_facets, selected_facets): final_facets = [{ 'selected': result['id'] in selected_facets, 'id': result['id'], 'title': result['title'], } for result in class_object.objects.filter( pk__in=narrowed_facets).values('id', 'title')] # noqa: E501 return final_facets def get_nested_facets(self, class_object, narrowed_facets, selected_facets, parent=None): # noqa: E501 if not parent: flat_final_facets = [{ 'selected': result['id'] in selected_facets, 'id': result['id'], 'title': result['title'], 'parent': result['parent'], } for result in class_object.objects.filter( pk__in=narrowed_facets).get_ancestors(True).values( 'id', 'title', 'parent')] # noqa: E501 final_facets = [] root_facets = [ root_facet for root_facet in flat_final_facets if root_facet['parent'] == None ] # noqa: E501 for root_facet in root_facets: children_list = self.get_nested_facets( class_object, narrowed_facets, selected_facets, root_facet['id']) # noqa: E501 child_selected = any(child['selected'] is True or child['child_selected'] is True for child in children_list # noqa: E501 ) final_facets.append({ 'selected': root_facet['selected'], 'child_selected': child_selected, 'id': root_facet['id'], 'title': root_facet['title'], 'parent': root_facet['parent'], 'children': children_list }) return final_facets else: children = [ { 'selected': result['id'] in selected_facets or result['parent'] in selected_facets, # noqa: E501 'id': result['id'], 'title': result['title'], 'parent': result['parent'], 'children': self.get_nested_facets(class_object, narrowed_facets, selected_facets, result['id']), # noqa: E501 'child_selected': any(child['selected'] is True or child['child_selected'] is True for child in # noqa: E501 self.get_nested_facets(class_object, narrowed_facets, selected_facets, result['id']) # noqa: E501 ) } for result in class_object.objects.filter( pk__in=narrowed_facets).filter( parent_id=parent).values('id', 'title', 'parent') ] # noqa: E501 return children class Meta: verbose_name = "TDP Activity search page"
class SublandingPage(CFGOVPage): header = StreamField([ ('hero', molecules.Hero()), ], blank=True) content = StreamField([ ('text_introduction', molecules.TextIntroduction()), ('featured_content', molecules.FeaturedContent()), ('image_text_25_75_group', organisms.ImageText2575Group()), ('image_text_50_50_group', organisms.ImageText5050Group()), ('full_width_text', organisms.FullWidthText()), ('half_width_link_blob_group', organisms.HalfWidthLinkBlobGroup()), ('post_preview_snapshot', organisms.PostPreviewSnapshot()), ('well', organisms.Well()), ('table', organisms.Table()), ('contact', organisms.MainContactInfo()), ('formfield_with_button', molecules.FormFieldWithButton()), ('reg_comment', organisms.RegComment()), ], blank=True) sidebar_breakout = StreamField([ ('slug', blocks.CharBlock(icon='title')), ('heading', blocks.CharBlock(icon='title')), ('paragraph', blocks.RichTextBlock(icon='edit')), ('breakout_image', blocks.StructBlock([ ('image', ImageChooserBlock()), ('is_round', blocks.BooleanBlock(required=False, default=True, label='Round?')), ('icon', blocks.CharBlock(help_text='Enter icon class name.')), ('heading', blocks.CharBlock(required=False, label='Introduction Heading')), ('body', blocks.TextBlock(required=False, label='Introduction Body')), ], heading='Breakout Image', icon='image')), ('related_posts', organisms.RelatedPosts()), ], blank=True) # General content tab content_panels = CFGOVPage.content_panels + [ StreamFieldPanel('header'), StreamFieldPanel('content'), ] sidebar_panels = [ StreamFieldPanel('sidebar_breakout'), ] + CFGOVPage.sidefoot_panels # Tab handler interface edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), ObjectList(sidebar_panels, heading='Sidebar'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) template = 'sublanding-page/index.html' def get_browsefilterable_posts(self, request, limit): filter_pages = [ p.specific for p in self.get_appropriate_descendants(request.site.hostname) if 'FilterablePage' in p.specific_class.__name__ and 'archive' not in p.title.lower() ] filtered_controls = {} for page in filter_pages: id = str(util.get_form_id(page, request.GET)) if id not in filtered_controls.keys(): filtered_controls.update({id: []}) form_class = page.get_form_class() posts = filterable_context.get_page_set( page, form_class(parent=page, hostname=request.site.hostname), request.site.hostname) if filtered_controls[id]: filtered_controls[id] += posts else: filtered_controls[id] = posts posts_tuple_list = [(id, post) for id, posts in filtered_controls.iteritems() for post in posts] posts = sorted(posts_tuple_list, key=lambda p: p[1].date_published, reverse=True)[:limit] return posts
class TestTabbedInterface(TestCase): def setUp(self): # a custom tabbed interface for EventPage self.EventPageTabbedInterface = TabbedInterface([ ObjectList([ FieldPanel('title', widget=forms.Textarea), FieldPanel('date_from'), FieldPanel('date_to'), ], heading='Event details', classname="shiny"), ObjectList([ InlinePanel('speakers', label="Speakers"), ], heading='Speakers'), ]).bind_to_model(EventPage) def test_get_form_class(self): EventPageForm = self.EventPageTabbedInterface.get_form_class(EventPage) form = EventPageForm() # form must include the 'speakers' formset required by the speakers InlinePanel self.assertIn('speakers', form.formsets) # form must respect any overridden widgets self.assertEqual(type(form.fields['title'].widget), forms.Textarea) def test_render(self): EventPageForm = self.EventPageTabbedInterface.get_form_class(EventPage) event = EventPage(title='Abergavenny sheepdog trials') form = EventPageForm(instance=event) tabbed_interface = self.EventPageTabbedInterface( instance=event, form=form ) result = tabbed_interface.render() # result should contain tab buttons self.assertIn('<a href="#event-details" class="active">Event details</a>', result) self.assertIn('<a href="#speakers" class="">Speakers</a>', result) # result should contain tab panels self.assertIn('<div class="tab-content">', result) self.assertIn('<section id="event-details" class="shiny active">', result) self.assertIn('<section id="speakers" class=" ">', result) # result should contain rendered content from descendants self.assertIn('Abergavenny sheepdog trials</textarea>', result) # this result should not include fields that are not covered by the panel definition self.assertNotIn('signup_link', result) def test_required_fields(self): # required_fields should report the set of form fields to be rendered recursively by children of TabbedInterface result = set(self.EventPageTabbedInterface.required_fields()) self.assertEqual(result, set(['title', 'date_from', 'date_to'])) def test_render_form_content(self): EventPageForm = self.EventPageTabbedInterface.get_form_class(EventPage) event = EventPage(title='Abergavenny sheepdog trials') form = EventPageForm(instance=event) tabbed_interface = self.EventPageTabbedInterface( instance=event, form=form ) result = tabbed_interface.render_form_content() # rendered output should contain field content as above self.assertIn('Abergavenny sheepdog trials</textarea>', result) # rendered output should NOT include fields that are in the model but not represented # in the panel definition self.assertNotIn('signup_link', result)
class SeriesPage(ThemeablePage, FeatureStyleFields, Promotable, ShareLinksMixin, PageLayoutOptions, VideoDocumentMixin): subtitle = RichTextField(blank=True, default="") short_description = RichTextField(blank=True, default="") body = article_fields.BodyField(blank=True, default="") main_image = models.ForeignKey( 'images.AttributedImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) feature_image = models.ForeignKey( 'images.AttributedImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) primary_topic = models.ForeignKey( 'articles.Topic', null=True, blank=True, on_delete=models.SET_NULL, related_name='series' ) project = models.ForeignKey( "projects.ProjectPage", null=True, blank=True, on_delete=models.SET_NULL, ) search_fields = Page.search_fields + [ index.SearchField('subtitle', partial_match=True), index.SearchField('body', partial_match=True), index.SearchField('get_primary_topic_name', partial_match=True), index.SearchField('get_topic_names', partial_match=True), ] number_of_related_articles = models.PositiveSmallIntegerField(default=6, verbose_name="Number of Related Articles to Show") def get_primary_topic_name(self): if self.primary_topic: return self.primary_topic.name else: "" def get_topic_names(self): return '\n'.join([topic.name if topic else "" for topic in self.topics]) def get_author_names(self): return '\n'.join([author.full_name if author else "" for author in self.authors]) @property def articles(self): article_list = [] for article_link in self.related_article_links.all(): if article_link.article: article_link.article.override_text = article_link.override_text article_link.article.override_image = article_link.override_image article_list.append(article_link.article) return article_list @property def authors(self): author_list = [] for article_link in self.related_article_links.all(): if article_link.article: if article_link.article: for author_link in article_link.article.author_links.all(): if author_link.author: if author_link.author not in author_list: author_list.append(author_link.author) author_list.sort(key=attrgetter('last_name')) return author_list @property def topics(self): all_topics = [] if self.primary_topic: all_topics.append(self.primary_topic) for article_link in self.related_article_links.all(): if article_link.article: all_topics.extend(article_link.article.topics) all_topics = list(set(all_topics)) if all_topics: all_topics.sort(key=attrgetter('name')) return all_topics @property def related_series(self): related_series_list = [] if self.project: related_series_list = self.project.get_related_series(self) return related_series_list def related_articles(self, number): articles = [] if self.primary_topic: articles = list(ArticlePage.objects.live().filter(primary_topic=self.primary_topic).distinct().order_by( '-first_published_at')[:number]) current_total = len(articles) if current_total < number: for article in self.articles: articles.extend(list(article.related_articles(number))) articles = list(set(articles))[:number] current_total = len(articles) if current_total >= number: return articles return articles content_panels = Page.content_panels + [ FieldPanel('subtitle'), FieldPanel('short_description'), PageChooserPanel('project'), ImageChooserPanel('main_image'), ImageChooserPanel('feature_image'), DocumentChooserPanel('video_document'), StreamFieldPanel('body'), InlinePanel('related_article_links', label="Articles"), SnippetChooserPanel('primary_topic'), ] promote_panels = Page.promote_panels + [ MultiFieldPanel( [ FieldPanel('sticky'), FieldPanel('sticky_for_type_section'), FieldPanel('slippery'), FieldPanel('slippery_for_type_section'), FieldPanel('editors_pick'), FieldPanel('feature_style'), FieldPanel('title_size'), FieldPanel('fullbleed_feature'), FieldPanel('image_overlay_opacity'), ], heading="Featuring Settings" ) ] style_panels = ThemeablePage.style_panels + [ MultiFieldPanel( [ FieldPanel('include_main_image'), FieldPanel('include_main_image_overlay'), FieldPanel('full_bleed_image_size'), FieldPanel('include_caption_in_footer'), ], heading="Main Image" ), MultiFieldPanel( [ FieldPanel('number_of_related_articles'), ], heading="Sections" ) ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(style_panels, heading='Page Style Options'), ObjectList(promote_panels, heading='Promote'), ObjectList(Page.settings_panels, heading='Settings', classname="settings"), ])
class TopicListPage(RoutablePageMixin, ThemeablePage): articles_per_page = models.IntegerField(default=20) @property def topics(self): popular_topics = Topic.objects.annotate( num_articles=Count('article_links') + Count('articles') + Count('series')).order_by("-num_articles")[:25] return sorted(popular_topics, key=lambda x: x.name) @route(r'^$', name="topic_list") def topics_list(self, request): context = { "self": self, } return render(request, "articles/topic_list_page.html", context) @route(r'^([\w-]+)/$', name="topic") def topic_view(self, request, topic_slug): topic = get_object_or_404(Topic, slug=topic_slug) articles = topic.item_list paginator = Paginator(articles, self.articles_per_page) page = request.GET.get('page') try: articles = paginator.page(page) except PageNotAnInteger: articles = paginator.page(1) except EmptyPage: articles = paginator.page(paginator.num_pages) context = { "self": self, "topic": topic, "articles": articles, } return render(request, "articles/topic_page.html", context) def get_cached_paths(self): yield '/' for topic in Topic.objects.all(): articles = ArticlePage.objects.live().filter( models.Q(primary_topic=topic) | models.Q(topic_links__topic=topic) ).order_by('-first_published_at').distinct() paginator = Paginator(articles, self.articles_per_page) topic_url = '/{}/'.format(topic.slug) yield topic_url for page_number in range(2, paginator.num_pages + 1): yield topic_url + '?page=' + str(page_number) content_panels = Page.content_panels + [ FieldPanel('articles_per_page'), ] style_panels = ThemeablePage.style_panels edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(style_panels, heading='Page Style Options'), ObjectList(Page.promote_panels, heading='Promote'), ObjectList(Page.settings_panels, heading='Settings', classname="settings"), ])
class TourPage(Page): search_fields = Page.search_fields + [ index.SearchField('title'), index.SearchField('tour_description'), ] tour_image = models.ForeignKey('wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', help_text='Album cover image') tour_listing_introduction = models.TextField( "A listing introduction for the tour", blank=True, max_length=254) tour_description = RichTextField("A description for the tour") content_panels = Page.content_panels + [ InlinePanel('tour_artist_relationship', label="Arist(s)", panels=None, min_num=1), InlinePanel('tour_album_relationship', label="Album(s)", panels=None, min_num=0), ImageChooserPanel('tour_image'), FieldPanel('tour_listing_introduction'), FieldPanel('tour_description'), ] tour_panels = [ InlinePanel('tourdates', label="Tour dates", help_text="Enter your tour dates", min_num=1), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading="Tour details", classname="content"), ObjectList(tour_panels, heading="Tour dates"), ObjectList(Page.promote_panels, heading="Promote"), ObjectList(Page.settings_panels, heading="Settings", classname="settings"), ]) # We iterate within the model over the artists, genres and subgenres # so they can be accessible to the template via a for loop def artists(self): artists = [n.artists for n in self.tour_artist_relationship.all()] return artists def albums(self): albums = [n.albums for n in self.tour_album_relationship.all()] return albums @property def album_image(self): # fail silently if there is no profile pic or the rendition file can't # be found. Note @richbrennan worked out how to do this... try: return self.image.get_rendition('fill-400x400').img_tag() except: return '' parent_page_types = [ 'tours.TourIndexPage' # app.model ] subpage_types = []
class RegulationPage(RoutablePageMixin, SecondaryNavigationJSMixin, CFGOVPage): """A routable page for serving an eregulations page by Section ID.""" objects = PageManager() parent_page_types = ['regulations3k.RegulationLandingPage'] subpage_types = [] template = 'regulations3k/browse-regulation.html' header = StreamField([ ('text_introduction', molecules.TextIntroduction()), ], blank=True) content = StreamField([], null=True) regulation = models.ForeignKey(Part, blank=True, null=True, on_delete=models.PROTECT, related_name='eregs3k_page') content_panels = CFGOVPage.content_panels + [ StreamFieldPanel('header'), FieldPanel('regulation', Part), ] secondary_nav_exclude_sibling_pages = models.BooleanField(default=False) sidefoot_panels = CFGOVPage.sidefoot_panels + [ FieldPanel('secondary_nav_exclude_sibling_pages'), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), ObjectList(sidefoot_panels, heading='Sidebar'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) @cached_property def section_query(self): """Query set for Sections in this regulation's effective version.""" return Section.objects.filter( subpart__version=self.regulation.effective_version, ) @cached_property def sections(self): return list(self.section_query.all()) def get_context(self, request, *args, **kwargs): context = super(CFGOVPage, self).get_context(request, *args, **kwargs) context.update({ 'get_secondary_nav_items': get_reg_nav_items, 'regulation': self.regulation, 'section': None, 'breadcrumb_items': self.get_breadcrumbs(request) }) return context def get_breadcrumbs(self, request, section=None): landing_page = self.get_parent() crumbs = [{ 'href': landing_page.url, 'title': landing_page.title, }] if section is not None: crumbs = crumbs + [ { 'href': self.url, 'title': str(section.subpart.version.part), }, { 'title': section.subpart.title, }, ] return crumbs @route(r'^(?P<section_label>[0-9A-Za-z-]+)/$', name="section") def section_page(self, request, section_label): section = self.section_query.get(label=section_label) current_index = self.sections.index(section) context = self.get_context(request) content = regdown(section.contents, url_resolver=get_url_resolver(self), contents_resolver=get_contents_resolver(self), render_block_reference=partial( self.render_interp, context)) context.update({ 'version': self.regulation.effective_version, 'content': content, 'get_secondary_nav_items': get_reg_nav_items, 'next_section': get_next_section(self.sections, current_index), 'previous_section': get_previous_section(self.sections, current_index), 'section': section, 'breadcrumb_items': self.get_breadcrumbs(request, section), }) return TemplateResponse(request, self.template, context) def render_interp(self, context, raw_contents, **kwargs): template = get_template('regulations3k/inline_interps.html') # Extract the title from the raw regdown section_title_match = re.search(r'#+\s?(?P<section_title>.*)\s', raw_contents) if section_title_match is not None: context.update({'section_title': section_title_match.group(1)}) span = section_title_match.span() raw_contents = raw_contents[:span[0]] + raw_contents[span[1]:] context.update({'contents': regdown(raw_contents)}) context.update(kwargs) return template.render(context)
class RegulationsSearchPage(RoutablePageMixin, CFGOVPage): """A page for the custom search interface for regulations.""" objects = PageManager() parent_page_types = ['regulations3k.RegulationLandingPage'] subpage_types = [] results = {} content_panels = CFGOVPage.content_panels edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) def get_template(self, request): return 'regulations3k/search-regulations.html' @route(r'^results/') def regulation_results_page(self, request): all_regs = Part.objects.order_by('part_number') regs = [] sqs = SearchQuerySet() if 'regs' in request.GET: regs = request.GET.getlist('regs') if len(regs) == 1: sqs = sqs.filter(part=regs[0]) elif regs: sqs = sqs.filter(part__in=regs) search_query = request.GET.get('q', '') # haystack cleans this string if search_query: query_sqs = sqs.filter(content=search_query).models(Section) else: query_sqs = sqs.models(Section) payload = { 'search_query': search_query, 'results': [], 'total_results': query_sqs.count(), 'regs': regs, 'all_regs': [{ 'name': "Regulation {}".format(reg.letter_code), 'id': reg.part_number, 'num_results': query_sqs.filter(part=reg.part_number).count(), 'selected': reg.part_number in regs } for reg in all_regs] } for hit in query_sqs: label_bits = hit.object.label.partition('-') _part, _section = label_bits[0], label_bits[2] letter_code = LETTER_CODES.get(_part) snippet = Truncator(hit.text).words(40, truncate=' ...') snippet = snippet.replace('*', '').replace('#', '') hit_payload = { 'id': hit.object.pk, 'reg': 'Regulation {}'.format(letter_code), 'label': hit.title, 'snippet': snippet, 'url': "/regulations/{}/{}/".format(_part, _section), } payload['results'].append(hit_payload) self.results = payload # if we want to paginate results: # def get_context(self, request, **kwargs): # context = super(RegulationsSearchPage, self).get_context( # request, **kwargs) # context.update(**kwargs) # paginator = Paginator(payload['results'], 25) # page_number = validate_page_number(request, paginator) # paginated_page = paginator.page(page_number) # context['current_page'] = page_number # context['paginator'] = paginator # context['results'] = paginated_page # return context context = self.get_context(request) return TemplateResponse(request, self.get_template(request), context)
class ActivityPage(CFGOVPage): """ A model for the Activity Detail page. """ # Allow Activity pages to exist under the ActivityIndexPage or the Trash parent_page_types = [ActivityIndexPage, HomePage] subpage_types = [] objects = CFGOVPageManager() date = models.DateField('Updated', default=timezone.now) summary = models.TextField('Summary', blank=False) big_idea = RichTextField('Big idea', blank=False) essential_questions = RichTextField('Essential questions', blank=False) objectives = RichTextField('Objectives', blank=False) what_students_will_do = RichTextField('What students will do', blank=False) # noqa: E501 activity_file = models.ForeignKey('wagtaildocs.Document', null=True, blank=False, on_delete=models.SET_NULL, related_name='+', verbose_name='Teacher guide') # TODO: to figure out how to use Document choosers on ManyToMany fields handout_file = models.ForeignKey('wagtaildocs.Document', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Student file 1') handout_file_2 = models.ForeignKey('wagtaildocs.Document', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Student file 2') handout_file_3 = models.ForeignKey('wagtaildocs.Document', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Student file 3') building_block = ParentalManyToManyField( 'teachers_digital_platform.ActivityBuildingBlock', blank=False) # noqa: E501 school_subject = ParentalManyToManyField( 'teachers_digital_platform.ActivitySchoolSubject', blank=False) # noqa: E501 topic = ParentalTreeManyToManyField( 'teachers_digital_platform.ActivityTopic', blank=False) # noqa: E501 # Audience grade_level = ParentalManyToManyField( 'teachers_digital_platform.ActivityGradeLevel', blank=False) # noqa: E501 age_range = ParentalManyToManyField( 'teachers_digital_platform.ActivityAgeRange', blank=False) # noqa: E501 student_characteristics = ParentalManyToManyField( 'teachers_digital_platform.ActivityStudentCharacteristics', blank=True) # noqa: E501 # Activity Characteristics activity_type = ParentalManyToManyField( 'teachers_digital_platform.ActivityType', blank=False) # noqa: E501 teaching_strategy = ParentalManyToManyField( 'teachers_digital_platform.ActivityTeachingStrategy', blank=False) # noqa: E501 blooms_taxonomy_level = ParentalManyToManyField( 'teachers_digital_platform.ActivityBloomsTaxonomyLevel', blank=False) # noqa: E501 activity_duration = models.ForeignKey( ActivityDuration, blank=False, on_delete=models.PROTECT) # noqa: E501 # Standards taught jump_start_coalition = ParentalManyToManyField( 'teachers_digital_platform.ActivityJumpStartCoalition', blank=True, verbose_name='Jump$tart Coalition', ) council_for_economic_education = ParentalManyToManyField( 'teachers_digital_platform.ActivityCouncilForEconEd', blank=True, verbose_name='Council for Economic Education', ) content_panels = CFGOVPage.content_panels + [ FieldPanel('date'), FieldPanel('summary'), FieldPanel('big_idea'), FieldPanel('essential_questions'), FieldPanel('objectives'), FieldPanel('what_students_will_do'), MultiFieldPanel( [ DocumentChooserPanel('activity_file'), DocumentChooserPanel('handout_file'), DocumentChooserPanel('handout_file_2'), DocumentChooserPanel('handout_file_3'), ], heading="Download activity", ), FieldPanel('building_block', widget=forms.CheckboxSelectMultiple), FieldPanel('school_subject', widget=forms.CheckboxSelectMultiple), FieldPanel('topic', widget=forms.CheckboxSelectMultiple), MultiFieldPanel( [ FieldPanel('grade_level', widget=forms.CheckboxSelectMultiple), # noqa: E501 FieldPanel('age_range', widget=forms.CheckboxSelectMultiple), FieldPanel('student_characteristics', widget=forms.CheckboxSelectMultiple), # noqa: E501 ], heading="Audience", ), MultiFieldPanel( [ FieldPanel('activity_type', widget=forms.CheckboxSelectMultiple), # noqa: E501 FieldPanel('teaching_strategy', widget=forms.CheckboxSelectMultiple), # noqa: E501 FieldPanel('blooms_taxonomy_level', widget=forms.CheckboxSelectMultiple), # noqa: E501 FieldPanel('activity_duration'), ], heading="Activity characteristics", ), MultiFieldPanel( [ FieldPanel('council_for_economic_education', widget=forms.CheckboxSelectMultiple), # noqa: E501 FieldPanel('jump_start_coalition', widget=forms.CheckboxSelectMultiple), # noqa: E501 ], heading="National standards", ), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), ObjectList(CFGOVPage.sidefoot_panels, heading='Sidebar/Footer'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) # admin use only search_fields = Page.search_fields + [ index.SearchField('summary'), index.SearchField('big_idea'), index.SearchField('essential_questions'), index.SearchField('objectives'), index.SearchField('what_students_will_do'), index.FilterField('date'), index.FilterField('building_block'), index.FilterField('school_subject'), index.FilterField('topic'), index.FilterField('grade_level'), index.FilterField('age_range'), index.FilterField('student_characteristics'), index.FilterField('activity_type'), index.FilterField('teaching_strategy'), index.FilterField('blooms_taxonomy_level'), index.FilterField('activity_duration'), index.FilterField('jump_start_coalition'), index.FilterField('council_for_economic_education'), ] def get_topics_list(self, parent=None): """ Get a hierarchical list of this activity's topics. parent: ActivityTopic """ if parent: descendants = set(parent.get_descendants()) & set( self.topic.all()) # noqa: E501 children = parent.get_children() children_list = [] # If this parent has descendants in self.topic, add its children. if descendants: for child in children: if set(child.get_descendants()) & set(self.topic.all()): children_list.append(self.get_topics_list(child)) elif child in self.topic.all(): children_list.append(child.title) if children_list: return parent.title + " (" + ', '.join( children_list) + ")" # noqa: E501 # Otherwise, just add the parent. else: return parent.title else: # Build root list of topics and recurse their children. topic_list = [] topic_ids = [topic.id for topic in self.topic.all()] ancestors = ActivityTopic.objects.filter( id__in=topic_ids).get_ancestors(True) # noqa: E501 roots = ActivityTopic.objects.filter(parent=None) & ancestors for root_topic in roots: topic_list.append(self.get_topics_list(root_topic)) if topic_list: return ', '.join(topic_list) else: return '' class Meta: verbose_name = "TDP Activity page"
class JobListingPage(CFGOVPage): description = RichTextField('Summary') open_date = models.DateField('Open date') close_date = models.DateField('Close date') salary_min = models.DecimalField('Minimum salary', max_digits=11, decimal_places=2) salary_max = models.DecimalField('Maximum salary', max_digits=11, decimal_places=2) division = models.ForeignKey(JobCategory, on_delete=models.PROTECT, null=True) job_length = models.ForeignKey(JobLength, on_delete=models.PROTECT, null=True, verbose_name="Position length", blank=True) service_type = models.ForeignKey(ServiceType, on_delete=models.PROTECT, null=True, blank=True) location = models.ForeignKey(JobLocation, related_name='job_listings', on_delete=models.PROTECT) allow_remote = models.BooleanField( default=False, help_text='Adds remote option to jobs with office locations.', verbose_name="Location can also be remote") responsibilities = RichTextField('Responsibilities', null=True, blank=True) travel_required = models.BooleanField( blank=False, default=False, help_text=('Optional: Check to add a "Travel required" section to the ' 'job description. Section content defaults to "Yes".')) travel_details = RichTextField( null=True, blank=True, help_text='Optional: Add content for "Travel required" section.') additional_section_title = models.CharField( max_length=255, null=True, blank=True, help_text='Optional: Add title for an additional section ' 'that will display at end of job description.') additional_section_content = RichTextField( null=True, blank=True, help_text='Optional: Add content for an additional section ' 'that will display at end of job description.') content_panels = CFGOVPage.content_panels + [ MultiFieldPanel([ FieldPanel('division', classname='full'), InlinePanel('grades', label='Grades'), FieldRowPanel([ FieldPanel('open_date', classname='col6'), FieldPanel('close_date', classname='col6'), ]), FieldRowPanel([ FieldPanel('salary_min', classname='col6'), FieldPanel('salary_max', classname='col6'), ]), FieldRowPanel([ FieldPanel('service_type', classname='col6'), FieldPanel('job_length', classname='col6'), ]), ], heading='Details'), MultiFieldPanel([ FieldPanel('location', classname='full'), FieldPanel('allow_remote', classname='full'), ], heading='Location'), MultiFieldPanel([ FieldPanel('description', classname='full'), FieldPanel('responsibilities', classname='full'), FieldPanel('travel_required', classname='full'), FieldPanel('travel_details', classname='full'), FieldPanel('additional_section_title', classname='full'), FieldPanel('additional_section_content', classname='full'), ], heading='Description'), InlinePanel('usajobs_application_links', label='USAJobs application links'), InlinePanel('email_application_links', label='Email application links'), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) template = 'job-description-page/index.html' objects = PageManager() def get_context(self, request, *args, **kwargs): context = super(JobListingPage, self).get_context(request) try: context['about_us'] = ReusableText.objects.get( title='About us (For consumers)') except Exception: pass if hasattr(self.location, 'region'): context['states'] = [ state.abbreviation for state in self.location.region.states.all() ] context['location_type'] = 'region' else: context['states'] = [] context['location_type'] = 'office' context['cities'] = self.location.cities.all() return context @property def page_js(self): return super(JobListingPage, self).page_js + ['read-more.js'] @property def ordered_grades(self): """Return a list of job grades in numerical order. Non-numeric grades are sorted alphabetically after numeric grades. """ grades = set(g.grade.grade for g in self.grades.all()) return sorted(grades, key=lambda g: '{0:0>8}'.format(g))
class TestTabbedInterface(TestCase): def setUp(self): # a custom tabbed interface for EventPage self.EventPageTabbedInterface = TabbedInterface([ ObjectList([ FieldPanel('title', widget=forms.Textarea), FieldPanel('date_from'), FieldPanel('date_to'), ], heading='Event details', classname="shiny"), ObjectList([ InlinePanel('speakers', label="Speakers"), ], heading='Speakers'), ]).bind_to_model(EventPage) def test_get_form_class(self): EventPageForm = self.EventPageTabbedInterface.get_form_class(EventPage) form = EventPageForm() # form must include the 'speakers' formset required by the speakers InlinePanel self.assertIn('speakers', form.formsets) # form must respect any overridden widgets self.assertEqual(type(form.fields['title'].widget), forms.Textarea) def test_render(self): EventPageForm = self.EventPageTabbedInterface.get_form_class(EventPage) event = EventPage(title='Abergavenny sheepdog trials') form = EventPageForm(instance=event) tabbed_interface = self.EventPageTabbedInterface(instance=event, form=form) result = tabbed_interface.render() # result should contain tab buttons self.assertIn( '<a href="#tab-event-details" class="active">Event details</a>', result) self.assertIn('<a href="#tab-speakers" class="">Speakers</a>', result) # result should contain tab panels self.assertIn('<div class="tab-content">', result) self.assertIn('<section id="tab-event-details" class="shiny active">', result) self.assertIn('<section id="tab-speakers" class=" ">', result) # result should contain rendered content from descendants self.assertIn('Abergavenny sheepdog trials</textarea>', result) # this result should not include fields that are not covered by the panel definition self.assertNotIn('signup_link', result) def test_required_fields(self): # required_fields should report the set of form fields to be rendered recursively by children of TabbedInterface result = set(self.EventPageTabbedInterface.required_fields()) self.assertEqual(result, set(['title', 'date_from', 'date_to'])) def test_render_form_content(self): EventPageForm = self.EventPageTabbedInterface.get_form_class(EventPage) event = EventPage(title='Abergavenny sheepdog trials') form = EventPageForm(instance=event) tabbed_interface = self.EventPageTabbedInterface(instance=event, form=form) result = tabbed_interface.render_form_content() # rendered output should contain field content as above self.assertIn('Abergavenny sheepdog trials</textarea>', result) # rendered output should NOT include fields that are in the model but not represented # in the panel definition self.assertNotIn('signup_link', result)
class ArticlePage(ThemeablePage, FeatureStyleFields, Promotable, ShareLinksMixin, PageLayoutOptions, VideoDocumentMixin): excerpt = RichTextField(blank=True, default="") body = article_fields.BodyField() chapters = article_fields.ChapterField(blank=True, null=True) table_of_contents_heading = models.TextField(blank=True, default="Table of Contents") citations_heading = models.TextField(blank=True, default="Works Cited") endnotes_heading = models.TextField(blank=True, default="End Notes") endnote_identifier_style = models.CharField( max_length=20, default="roman-lower", choices=( ('roman-lower', 'Roman Numerals - Lowercase'), ('roman-upper', 'Roman Numerals - Uppercase'), ('numbers', 'Numbers') ) ) main_image = models.ForeignKey( 'images.AttributedImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) feature_image = models.ForeignKey( 'images.AttributedImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) primary_topic = models.ForeignKey( 'articles.Topic', null=True, blank=True, on_delete=models.SET_NULL, related_name='articles' ) category = models.ForeignKey( 'articles.ArticleCategory', related_name='%(class)s', on_delete=models.SET_NULL, null=True, default=1 ) include_author_block = models.BooleanField(default=True) visualization = models.BooleanField(default=False) interview = models.BooleanField(default=False) video = models.BooleanField(default=False) number_of_related_articles = models.PositiveSmallIntegerField(default=6, verbose_name="Number of Related Articles to Show") json_file = article_fields.WagtailFileField(max_length=255, blank=True, null=True, verbose_name='JSON file', help_text="Only provide if you know your template will be filled with the contents of a JSON data file.") project = models.ForeignKey( "projects.ProjectPage", null=True, blank=True, on_delete=models.SET_NULL, ) _response_to = False search_fields = Page.search_fields + [ index.SearchField('excerpt', partial_match=True), index.SearchField('body', partial_match=True), index.SearchField('chapters', partial_match=True), index.SearchField('get_primary_topic_name', partial_match=True), index.SearchField('get_category_name', partial_match=True), index.SearchField('get_topic_names', partial_match=True), index.SearchField('get_author_names', partial_match=True), ] def get_primary_topic_name(self): if self.primary_topic: return self.primary_topic.name return "" def get_category_name(self): if self.category: return self.category.name return "" def get_topic_names(self): return '\n'.join([link.topic.name if link.topic else "" for link in self.topic_links.all()]) def get_author_names(self): return '\n'.join( [author_link.author.full_name if author_link.author else "" for author_link in self.author_links.all()]) @property def authors(self): author_list = [] for link in self.author_links.all(): if link.author: author_list.append((link.author)) return author_list @property def series_articles(self): related_series_data = [] for link in self.series_links.all(): series_page = link.series series_articles = series_page.articles series_articles.remove(self) related_series_data.append((series_page, series_articles)) return related_series_data @property def topics(self): primary_topic = self.primary_topic all_topics = [link.topic for link in self.topic_links.all()] if primary_topic: all_topics.append(primary_topic) all_topics = list(set(all_topics)) if len(all_topics) > 0: all_topics.sort(key=attrgetter('name')) return all_topics @property def response_to(self): if self._response_to is False: response_to_count = self.response_to_links.count() if response_to_count > 1: logger.warning( 'ArticlePage(pk={0}) appears to be a response to multiple articles. Only the first one is being returned.'.format( self.pk ) ) if response_to_count != 0: self._response_to = self.response_to_links.first().response_to else: self._response_to = None return self._response_to @property def is_response(self): return self.response_to is not None def responses(self): return [link.response for link in self.response_links.all()] def related_articles(self, number): included = [self.id] article_list = [] if self.primary_topic: articles = ArticlePage.objects.live().filter(primary_topic=self.primary_topic).exclude( id=self.id).distinct().order_by('-first_published_at')[:number] article_list.extend(articles.all()) included.extend([article.id for article in articles.all()]) current_total = len(article_list) if current_total < number: # still don't have enough, so pick using secondary topics topics = Topic.objects.filter(article_links__article=self) if topics: additional_articles = ArticlePage.objects.live().filter(primary_topic__in=topics).exclude( id__in=included).distinct().order_by('-first_published_at')[:number - current_total] article_list.extend(additional_articles.all()) current_total = len(article_list) included.extend([article.id for article in additional_articles.all()]) if current_total < number: authors = ContributorPage.objects.live().filter(article_links__article=self) if authors: additional_articles = ArticlePage.objects.live().filter(author_links__author__in=authors).exclude( id__in=included).distinct().order_by('-first_published_at')[:number - current_total] article_list.extend(additional_articles.all()) current_total = len(article_list) included.extend([article.id for article in additional_articles.all()]) if current_total < number: # still don't have enough, so just pick the most recent additional_articles = ArticlePage.objects.live().exclude(id__in=included).order_by('-first_published_at')[:number - current_total] article_list.extend(additional_articles.all()) return article_list content_panels = Page.content_panels + [ FieldPanel('excerpt'), InlinePanel('author_links', label="Authors"), PageChooserPanel('project'), ImageChooserPanel('main_image'), ImageChooserPanel('feature_image'), DocumentChooserPanel('video_document'), StreamFieldPanel('body'), SnippetChooserPanel('primary_topic'), InlinePanel('topic_links', label="Secondary Topics"), InlinePanel('response_links', label="Responses"), ] advanced_content_panels = [ FieldPanel('json_file'), MultiFieldPanel( [ FieldPanel('table_of_contents_heading'), StreamFieldPanel('chapters'), ], heading="Chapters Section" ), MultiFieldPanel( [ FieldPanel('endnotes_heading'), FieldPanel('endnote_identifier_style'), InlinePanel('endnote_links', label="End Notes"), ], heading="End Notes Section" ), MultiFieldPanel( [ FieldPanel('citations_heading'), InlinePanel('citation_links', label="Citations"), ], heading="Citations Section" ), ] promote_panels = Page.promote_panels + [ MultiFieldPanel( [ FieldPanel('sticky'), FieldPanel('sticky_for_type_section'), FieldPanel('slippery'), FieldPanel('slippery_for_type_section'), FieldPanel('editors_pick'), FieldPanel('feature_style'), FieldPanel('title_size'), FieldPanel('fullbleed_feature'), FieldPanel('image_overlay_opacity'), ], heading="Featuring Settings" ), ] style_panels = ThemeablePage.style_panels + [ MultiFieldPanel( [ FieldPanel('include_main_image'), FieldPanel('include_main_image_overlay'), FieldPanel('full_bleed_image_size'), FieldPanel('include_caption_in_footer'), ], heading="Main Image" ), MultiFieldPanel( [ InlinePanel('background_image_links', label="Background Images"), ], heading="Background Images" ), MultiFieldPanel( [ FieldPanel('include_author_block'), FieldPanel('number_of_related_articles') ], heading="Sections" ), MultiFieldPanel( [ FieldPanel('interview'), FieldPanel('video'), FieldPanel('visualization'), ], heading="Categorization" ) ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(advanced_content_panels, heading='Advanced Content'), ObjectList(style_panels, heading='Page Style Options'), ObjectList(promote_panels, heading='Promote'), ObjectList(Page.settings_panels, heading='Settings', classname="settings"), ])
def get_edit_handler(self): """ Get the EditHandler to use in the Wagtail admin when editing this page type. """ if hasattr(self, 'edit_handler'): return self.edit_handler.bind_to_model(self) # construct a TabbedInterface made up of content_panels, promote_panels # and settings_panels, skipping any which are empty tabs = [] for tab in self.tabs: title, content, *args = tab kwargs = args[0] if args else {} tabs.append(ObjectList(content, heading=title, **kwargs)) model = self._meta.model EditHandler = TabbedInterface(tabs, base_form_class=model.base_form_class) return EditHandler.bind_to_model(model)