class HomePage(RoutablePageMixin, Page): templates = 'home/home_page.html' subpage_types = [ 'blog.BlogListingPage', 'contact.ContactPage', 'flex.FlexPage', ] parent_page_type = [ 'wagtailcore.Page' ] #max_count = 1 banner_title = models.CharField(max_length=100, blank=False, null=True) banner_subtitle = RichTextField(features=['bold', 'italic']) banner_image = models.ForeignKey( "wagtailimages.Image", null=True, blank=False, on_delete=models.SET_NULL, related_name='+' ) banner_cta = models.ForeignKey( 'wagtailcore.Page', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) content = StreamField( [ ('cta', blocks.CTABlock()), ], null=True, blank=True ) api_fields = [ APIField('banner_title'), APIField('banner_subtitle'), APIField('banner_image'), APIField('banner_cta'), APIField('carousel_images'), APIField('content'), ] max_count = 1 content_panels = Page.content_panels + [ MultiFieldPanel( [InlinePanel("carousel_images", max_num=5, min_num=1, label="Image")], heading="Carousel Images", ), StreamFieldPanel("content"), ] banner_panels = [ MultiFieldPanel( [ FieldPanel("banner_title"), FieldPanel("banner_subtitle"), ImageChooserPanel("banner_image"), PageChooserPanel("banner_cta"), ], heading="Banner Options", ), ] edit_handler = TabbedInterface( [ ObjectList(content_panels, heading='Content'), ObjectList(banner_panels, heading="Banner Settings"), ObjectList(Page.promote_panels, heading='Promotional Stuff'), ObjectList(Page.settings_panels, heading='Settings Stuff'), ] ) @route(r'^subscribe/$') def the_subscribe_page(self, request, *args, **kwargs): context = self.get_context(request, *args, **kwargs) return render(request, 'home/subscribe.html', context)
class ContactPage(Page, MenuPageMixin): """A custom contact page with contact form.""" body = RichTextField(blank=True) TEMPLATE_CHOICES = [ ('base_dark.html', 'Dark'), ('base_light.html', 'Light') ] template_theme = models.CharField( max_length = 250, choices = TEMPLATE_CHOICES, default = 'Dark', help_text = """ Choose dark theme to match main site and light theme to match light site.""" ) content_panels = Page.content_panels + [ FieldPanel('body', classname="full"), FieldPanel('template_theme'), menupage_panel, ] subpage_types = ['contact.ContactSuccessPage', 'contact.MailChimpPage'] parent_page_types = ['home.HomePage', 'home.FanSiteHomePage'] def serve(self, request): if request.method == 'GET': form = ContactForm() else: form = ContactForm(request.POST) if form.is_valid(): # Clean the data senders_name = form.cleaned_data['your_name'] senders_email = form.cleaned_data['your_email'] message_subject = form.cleaned_data['subject'] message = form.cleaned_data['your_message'] site_name = settings.WAGTAIL_SITE_NAME to_email = settings.CONTACT_EMAIL #Process the form info to get it ready for send_mail subject_line = f"""New message from {site_name} contact form: {message_subject}""" message_body = f"""You have received the following message from your website, {site_name}: \n\n Sender's Name: {senders_name} \n\n Sender's Email: {senders_email} \n\n Subject: {message_subject} \n\n Message Body: {message}""" # And send try: send_mail(subject_line, message_body, to_email, [to_email], fail_silently=False) except BadHeaderError: return HttpResponse('Invalid header found.') success_pages = self.get_specific().get_children() for success in success_pages: return redirect(success.specific.url) return render(request, 'contact/contact_page.html', { 'page':self, 'form':form, 'template_theme':self.template_theme, })
class HomePage(Page): body = RichTextField(blank=True) content_panels = Page.content_panels + [ FieldPanel('body', classname="full"), ]
class StevePage(Page): city = models.CharField(null=True, blank=False, max_length=255) zip_code = models.CharField(null=True, blank=False, max_length=255) address = models.CharField(null=True, blank=False, max_length=255) telephone = models.CharField(null=True, blank=False, max_length=255) telefax = models.CharField(null=True, blank=False, max_length=255) vat_number = models.CharField(null=True, blank=False, max_length=255) whatsapp_telephone = models.CharField(null=True, blank=True, max_length=255) whatsapp_contactline = models.CharField(null=True, blank=True, max_length=255) tax_id = models.CharField(null=True, blank=False, max_length=255) trade_register_number = models.CharField(null=True, blank=False, max_length=255) court_of_registry = models.CharField(null=True, blank=False, max_length=255) place_of_registry = models.CharField(null=True, blank=False, max_length=255) trade_register_number = models.CharField(null=True, blank=False, max_length=255) ownership = models.CharField(null=True, blank=False, max_length=255) email = models.CharField(null=True, blank=False, max_length=255) copyrightholder = models.CharField(null=True, blank=False, max_length=255) about = RichTextField(null=True, blank=False) privacy = RichTextField(null=True, blank=False) sociallinks = StreamField([ ('link', blocks.URLBlock(help_text="Important! Format https://www.domain.tld/xyz")) ]) array = [] def sociallink_company(self): for link in self.sociallinks: self.array.append(str(link).split(".")[1]) return self.array sections = StreamField([ ('s_news', _S_NewsBlock(null=True, blank=False, icon='group')), ('code', blocks.RawHTMLBlock(null=True, blank=True, classname="full", icon='code')) ], null=True, blank=False) token = models.CharField(null=True, blank=True, max_length=255) #graphql_fields = [ # GraphQLStreamfield("headers"), # GraphQLStreamfield("sections"), #] main_content_panels = [ StreamFieldPanel('sections') ] imprint_panels = [ MultiFieldPanel( [ FieldPanel('city'), FieldPanel('zip_code'), FieldPanel('address'), FieldPanel('telephone'), FieldPanel('telefax'), FieldPanel('whatsapp_telephone'), FieldPanel('whatsapp_contactline'), FieldPanel('email'), FieldPanel('copyrightholder') ], heading="contact", ), MultiFieldPanel( [ FieldPanel('vat_number'), FieldPanel('tax_id'), FieldPanel('trade_register_number'), FieldPanel('court_of_registry'), FieldPanel('place_of_registry'), FieldPanel('trade_register_number'), FieldPanel('ownership') ], heading="legal", ), StreamFieldPanel('sociallinks'), MultiFieldPanel( [ FieldPanel('about'), FieldPanel('privacy') ], heading="privacy", ) ] token_panel = [ FieldPanel('token') ] edit_handler = TabbedInterface([ ObjectList(Page.content_panels + main_content_panels, heading='Main'), ObjectList(imprint_panels, heading='Imprint'), ObjectList(Page.promote_panels + token_panel + Page.settings_panels, heading='Settings', classname="settings") ])
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 get_content(self): ''' A generic and generative interface for getting all the content block for an article, including advanced content such as chapters. ''' for block in self.body: yield block 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 AbstractFilterPage(CFGOVPage): header = StreamField([ ('article_subheader', blocks.RichTextBlock(icon='form')), ('text_introduction', molecules.TextIntroduction()), ('item_introduction', organisms.ItemIntroduction()), ], blank=True) preview_title = models.CharField(max_length=255, null=True, blank=True) preview_subheading = models.CharField(max_length=255, null=True, blank=True) preview_description = RichTextField(null=True, blank=True) secondary_link_url = models.CharField(max_length=500, null=True, blank=True) secondary_link_text = models.CharField(max_length=255, null=True, blank=True) preview_image = models.ForeignKey('v1.CFGOVImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+') date_published = models.DateField(default=date.today) date_filed = models.DateField(null=True, blank=True) comments_close_by = models.DateField(null=True, blank=True) # Configuration tab panels settings_panels = [ MultiFieldPanel(CFGOVPage.promote_panels, 'Settings'), InlinePanel('categories', label="Categories", max_num=2), FieldPanel('tags', 'Tags'), MultiFieldPanel([ FieldPanel('preview_title'), FieldPanel('preview_subheading'), FieldPanel('preview_description'), FieldPanel('secondary_link_url'), FieldPanel('secondary_link_text'), ImageChooserPanel('preview_image'), ], heading='Page Preview Fields', classname='collapsible'), FieldPanel('schema_json', 'Structured Data'), FieldPanel('authors', 'Authors'), MultiFieldPanel([ FieldPanel('date_published'), FieldPanel('date_filed'), FieldPanel('comments_close_by'), ], 'Relevant Dates', classname='collapsible'), MultiFieldPanel(Page.settings_panels, 'Scheduled Publishing'), FieldPanel('language', 'Language'), ] # This page class cannot be created. is_creatable = False objects = CFGOVPageManager() search_fields = CFGOVPage.search_fields + [index.SearchField('header')] @classmethod def generate_edit_handler(self, content_panel): content_panels = [ StreamFieldPanel('header'), content_panel, ] return TabbedInterface([ ObjectList(self.content_panels + content_panels, heading='General Content'), ObjectList(CFGOVPage.sidefoot_panels, heading='Sidebar'), ObjectList(self.settings_panels, heading='Configuration'), ]) # Returns an image for the page's meta Open Graph tag @property def meta_image(self): parent_meta = super(AbstractFilterPage, self).meta_image return parent_meta or self.preview_image
class MetaTerm(index.Indexed, MP_Node): """ Hierarchal "Meta" terms """ name = models.CharField( max_length=50, unique=True, help_text='Keep the name short, ideally one word.' ) is_archived = models.BooleanField( default=False, verbose_name=_("Archived"), help_text='Archived terms can be viewed but not set on content.' ) filter_on_dashboard = models.BooleanField( default=True, help_text='Make available to filter on dashboard' ) available_to_applicants = models.BooleanField( default=False, help_text='Make available to applicants' ) help_text = RichTextField(features=[ 'h2', 'h3', 'bold', 'italic', 'link', 'hr', 'ol', 'ul'], blank=True) # node tree specific fields and attributes node_order_index = models.IntegerField(blank=True, default=0, editable=False) node_child_verbose_name = 'child' # important: node_order_by should NOT be changed after first Node created node_order_by = ['node_order_index', 'name'] panels = [ FieldPanel('name'), FieldPanel('parent'), MultiFieldPanel( [ FieldPanel('is_archived'), FieldPanel('filter_on_dashboard'), FieldPanel('available_to_applicants'), FieldPanel('help_text'), ], heading="Options", ), ] def get_as_listing_header(self): depth = self.get_depth() rendered = render_to_string( 'categories/admin/includes/meta_term_list_header.html', { 'depth': depth, 'depth_minus_1': depth - 1, 'is_root': self.is_root(), 'name': self.name, 'is_archived': self.is_archived, } ) return rendered get_as_listing_header.short_description = 'Name' get_as_listing_header.admin_order_field = 'name' def get_parent(self, *args, **kwargs): return super().get_parent(*args, **kwargs) get_parent.short_description = 'Parent' search_fields = [ index.SearchField('name', partial_match=True), ] def delete(self): if self.is_root(): raise PermissionDenied('Cannot delete root term.') else: super().delete() @classmethod def get_root_descendants(cls): # Meta terms queryset without Root node root_node = cls.get_first_root_node() if root_node: return root_node.get_descendants() return cls.objects.none() def __str__(self): return self.name class Meta: verbose_name = 'Meta Term' verbose_name_plural = 'Meta Terms'
class Answer(Page): template = 'cms/answer_detail.html' # Determines type and whether its highlighted in overview list type = models.CharField( choices=[('answer', 'Antwoord'), ('column', 'Column')], max_length=100, default='answer', help_text= _('Choose between answer or discussion piece with a more prominent look' )) featured = models.BooleanField(default=False) content = RichTextField(blank=True) excerpt = models.CharField( verbose_name=_('Short description'), max_length=255, blank=False, null=True, help_text=_( 'This helps with search engines and when sharing on social media'), ) introduction = TextField( verbose_name=_('Introduction'), default='', blank=True, null=True, help_text=_( 'This text is displayed above the tags, useful as a TLDR section'), ) tags = ClusterTaggableManager(through=AnswerTag, blank=True) social_image = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', help_text= _('This is the image that will be displayed when sharing on social media' ), ) # Freeform content of answer page_content = StreamField([ ('richtext', AnswerRichTextBlock()), ('image', AnswerImageBlock()), ('quote', QuoteBlock()), ]) # Which experts and how was this answered? answer_origin = StreamField([('origin', AnswerOriginBlock())], blank=True) # Related items related_items = StreamField([('related_items', RelatedItemsBlock())], blank=True) parent_page_types = ['AnswerIndexPage'] content_panels = Page.content_panels + [ FieldPanel('type'), FieldPanel('featured', heading=_("Show this answer on the home page")), FieldPanel( 'excerpt', classname='full', ), FieldPanel('introduction', classname='full'), MultiFieldPanel([ InlinePanel('answer_category_relationship', label=_('Categorie(n)'), panels=None, min_num=1) ], heading=_('Categorie(s)')), FieldPanel( 'tags', heading= "Please use tags with a maximum length of 16 characters per single word to avoid overlap in the mobile view." ), MultiFieldPanel([ InlinePanel('answer_expert_relationship', label=_('Expert(s)'), panels=None, min_num=1) ], heading=_('Expert(s)')), StreamFieldPanel('page_content'), StreamFieldPanel('answer_origin'), StreamFieldPanel('related_items'), ImageChooserPanel( 'social_image', help_text=_('Image to be used when sharing on social media')), ] search_fields = Page.search_fields + [ index.SearchField('page_content'), ] @property def experts(self): experts = [n.expert for n in self.answer_expert_relationship.all()] return experts @property def categories(self): categories = [ n.category for n in self.answer_category_relationship.all() ] return categories @property def get_tags(self): tags = self.tags.all() for tag in tags: tag.url = '/' + '/'.join( s.strip('/') for s in [self.get_parent().url, 'tags', tag.slug]) return tags def get_references(self): """ Build reference list, in the order Wagtail returns them. ### , alphabetically to sort of comply with standards TODO: References for articles can be separated from the origin and make them a proper ListBlock that can be handled by editors as they see fit. Having the references within a StreamField of 'origins' seems counter intuitive. """ ref_list = [] try: component = self.answer_origin[0] except IndexError: return ref_list # Access streamfield elements for element in component.value['sources']: ref_list.append({ 'text': element['reference_text'], 'url': element['url_or_doi'], }) # Sort by text starting letter, best we can do for now # ref_list.sort(key=lambda e: e['text']) return ref_list def get_primary_expert(self): """ Gets the first expert associated with this answer if it exists. """ try: first = self.experts[0] except IndexError: return _('Unknown') else: return first def get_all_categories(self): return [{ 'title': c.name, 'url': c.get_prefiltered_search_params() } for c in self.categories] def get_card_data(self): return { 'title': self.title, 'url': self.url, 'author': self.get_primary_expert(), 'categories': self.get_all_categories(), 'type': 'answer' } def get_as_overview_row_card(self): if self.type == 'answer': return render_to_string('core/includes/answer_block.html', context=self.get_card_data()) else: # It's a column return render_to_string('core/includes/column_block.html', context=self.get_card_data()) def get_as_home_row_card(self): return render_to_string('core/includes/answer_home_block.html', context=self.get_card_data()) def get_as_related_row_card(self): return render_to_string('core/includes/related_item_block.html', context=self.get_card_data()) def get_context(self, request, *args, **kwargs): context = super(Answer, self).get_context(request, *args, **kwargs) categories = AnswerCategory.objects.all() context.update({ 'categories': categories, 'answers_page': AnswerIndexPage.objects.first().url, 'experts_page': ExpertIndexPage.objects.first(), }) return context class Meta: ordering = [ '-first_published_at', ]
class CompositionPage(Page): composition_title = RichTextField(features=['bold', 'italic']) description = StreamField([('rich_text', RichTextBlock()), ('image', ImageChooserBlock())], blank=True) location = RichTextField( blank=True, features=['bold', 'italic', 'link', 'document-link'], ) genre = ParentalManyToManyField(Genre, blank=True, related_name='compositions') instrumentation = ParentalManyToManyField( 'Instrument', blank=True, ) orchestration = RichTextField( blank=True, features=['bold', 'italic'], help_text=( 'If the composition is for an ensemble, use this field to enter ' 'the orchestration of the work.')) duration = DurationField(null=True, blank=True, help_text='Expects data in the format "HH:MM:SS"') dedicatee = RichTextField( blank=True, features=['bold', 'italic', 'link', 'document-link'], ) text_source = RichTextField( blank=True, features=['bold', 'italic', 'link', 'document-link'], help_text='The source of the text used in the compostition.') collaborator = RichTextField( blank=True, features=['bold', 'italic', 'link', 'document-link'], help_text='Others that Decruck collaborated with.') manuscript_status = RichTextField( blank=True, features=['bold', 'italic', 'link', 'document-link'], help_text='Notes about the location and condition of the manuscript.') recording = StreamField([('rich_text', RichTextBlock()), ('image', ImageChooserBlock())], blank=True) information_up_to_date = BooleanField(default=False) scanned = BooleanField(default=False) premiere = RichTextField( blank=True, features=['bold', 'italic', 'link', 'document-link'], ) # For preview score preview_score = FileField( upload_to='composition_preview_scores/', blank=True, null=True, validators=[FileExtensionValidator(allowed_extensions=['pdf'])]) preview_score_checksum = CharField(editable=False, max_length=256, blank=True) preview_score_checked = False preview_score_updated = False # Extended Date Time Format nat_lang_edtf_string = CharField( verbose_name='Natural Language Date', help_text=('The EDTF date in natural language. This field is help ' 'users who aren\'t familiar with the EDTF. It does not ' 'change how the date is represented.'), blank=True, max_length=256) edtf_string = CharField( verbose_name='EDTF Date', help_text=mark_safe( 'A date in the <a href="https://www.loc.gov/standards/datetime/" ' 'target="_blank"><strong>Extended Date Time Format</strong></a>'), blank=True, max_length=256) lower_fuzzy = DateField(editable=False, null=True, blank=True) upper_fuzzy = DateField(editable=False, null=True, blank=True) lower_strict = DateField(editable=False, null=True, blank=True) upper_strict = DateField(editable=False, null=True, blank=True) nat_lang_year = CharField(editable=False, max_length=9, blank=True) def instrumentation_list(self): return ', '.join([str(i) for i in self.instrumentation.all()]) class Meta: verbose_name = "Composition" def get_context(self, request, *args, **kwargs): ctx = super().get_context(request, *args, **kwargs) try: search_idx = request.session['comp_search_index'] if search_idx: idx = search_idx.index(self.pk) prev_url = None next_url = None if idx > 0: pk = search_idx[idx - 1] prev_url = CompositionPage.objects.get(pk=pk).url if idx < len(search_idx) - 1: pk = search_idx[idx + 1] next_url = CompositionPage.objects.get(pk=pk).url ctx['prev_url'] = prev_url ctx['next_url'] = next_url ctx['comp_search_qs'] = request.\ session.get('comp_search_qs', '') except (KeyError, ValueError): pass return ctx def clean(self): super().clean() # Per Django docs: validate and modify values in Model.clean() # https://docs.djangoproject.com/en/3.1/ref/models/instances/#django.db.models.Model.clean # Check that nat_lang_edtf_string and edtf_string are either both set, or both unset if (self.nat_lang_edtf_string and not self.edtf_string) or (not self.nat_lang_edtf_string and self.edtf_string): raise ValidationError( 'If setting a date on a composition, an EDTF string and a natural language EDTF string must be provided.' ) # Validate edtf_string if self.edtf_string and self.nat_lang_edtf_string: try: e = parse_edtf(self.edtf_string) except EDTFParseException: raise ValidationError({ 'edtf_string': '{} is not a valid EDTF string'.format(self.edtf_string) }) self.lower_fuzzy = struct_time_to_date(e.lower_fuzzy()) self.upper_fuzzy = struct_time_to_date(e.upper_fuzzy()) self.lower_strict = struct_time_to_date(e.lower_strict()) self.upper_strict = struct_time_to_date(e.upper_strict()) if self.lower_strict.year != self.upper_strict.year: self.nat_lang_year = '{}-{}'.format(self.lower_strict.year, self.upper_strict.year) else: self.nat_lang_year = str(self.lower_strict.year) def save(self, *args, **kwargs): # If there's no preview score file, then just save the model if not self.preview_score: return super().save(*args, **kwargs) if self.preview_score_checked: # This was the cause of a subtle bug. Because this method can be # called multiple times during model creation, leaving this flag # set would cause the post save hook to fire multiple times. self.preview_score_updated = False return super().save(*args, **kwargs) h = hashlib.md5() for chunk in iter(lambda: self.preview_score.read(8192), b''): h.update(chunk) self.preview_score.seek(0) checksum = h.hexdigest() if not self.preview_score_checksum == checksum: self.preview_score_checksum = checksum self.preview_score_updated = True self.preview_score_checked = True return super().save(*args, **kwargs) content_panels = Page.content_panels + [ FieldPanel('composition_title'), StreamFieldPanel('description'), MultiFieldPanel( [FieldPanel('edtf_string'), FieldPanel('nat_lang_edtf_string')], help_text='Enter a date in the LOC Extended Date Time Format', heading='Date'), FieldPanel('location'), FieldPanel('instrumentation'), FieldPanel('orchestration'), FieldPanel('duration'), FieldPanel('dedicatee'), FieldPanel('premiere'), FieldPanel('genre'), FieldPanel('text_source'), FieldPanel('collaborator'), FieldPanel('manuscript_status'), FieldPanel('information_up_to_date'), FieldPanel('scanned'), FieldPanel('preview_score'), StreamFieldPanel('recording'), ] search_fields = Page.search_fields + [ index.SearchField('description', partial_match=True), index.SearchField('location', partial_match=True), index.SearchField('dedicatee', partial_match=True), index.SearchField('premiere', partial_match=True), index.SearchField('text_source', partial_match=True), index.SearchField('collaborator', partial_match=True), index.SearchField('manuscript_status', partial_match=True), index.SearchField('recording', partial_match=True), index.RelatedFields('genre', [ index.SearchField('genre_en', partial_match=True), index.SearchField('genre_fr', partial_match=True), ]), index.RelatedFields('instrumentation', [ index.SearchField('instrument_en', partial_match=True), index.SearchField('instrument_fr', partial_match=True), ]), ] parent_page_types = ['CompositionListingPage']
class HomePage(RoutablePageMixin, Page): """Home page model.""" template = "home/home_page.html" max_count = 1 banner_title = models.CharField(max_length=100, blank=False, null=True) banner_subtitle = RichTextField(features=["bold", "italic"]) banner_image = models.ForeignKey( "wagtailimages.Image", null=True, blank=False, on_delete=models.SET_NULL, related_name="+", ) banner_cta = models.ForeignKey( "wagtailcore.Page", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) content = StreamField([("cta", blocks.CTABlock())], null=True, blank=True) api_fields = [ APIField("banner_title"), APIField("banner_subtitle"), APIField("banner_image"), APIField("banner_cta"), ] content_panels = Page.content_panels + [ MultiFieldPanel( [ FieldPanel("banner_title"), FieldPanel("banner_subtitle"), ImageChooserPanel("banner_image"), PageChooserPanel("banner_cta"), ], heading="Banner Options", ), MultiFieldPanel( [ InlinePanel( "carousel_images", max_num=5, min_num=1, label="Image") ], heading="Carousel Images", ), StreamFieldPanel("content"), ] class Meta: verbose_name = "Home Page" verbose_name_plural = "Home Pages" @route(r'^subscribe/$') def the_subscribe_page(self, request, *args, **kwargs): context = self.get_context(request, *args, **kwargs) return render(request, "home/subscribe.html", context)
class Topic(BasePage): resource_type = "topic" parent_page_types = ["Topics"] subpage_types = ["Topic"] template = "topic.html" # Content fields description = RichTextField( blank=True, default="", features=RICH_TEXT_FEATURES_SIMPLE, help_text="Optional short text description, max. 400 characters", max_length=400, ) featured = StreamField( StreamBlock( [ ( "post", PageChooserBlock(target_model=( "articles.Article", "externalcontent.ExternalArticle", )), ), ("external_page", FeaturedExternalBlock()), ], max_num=4, required=False, ), null=True, blank=True, help_text="Optional space for featured posts, max. 4", ) tabbed_panels = StreamField( StreamBlock([("panel", TabbedPanelBlock())], max_num=3, required=False), null=True, blank=True, help_text= "Optional tabbed panels for linking out to other resources, max. 3", verbose_name="Tabbed panels", ) latest_articles_count = IntegerField( choices=RESOURCE_COUNT_CHOICES, default=3, help_text="The number of posts to display for this topic.", ) # Card fields card_title = CharField("Title", max_length=140, blank=True, default="") card_description = TextField("Description", max_length=400, blank=True, default="") card_image = ForeignKey( "mozimages.MozImage", null=True, blank=True, on_delete=SET_NULL, related_name="+", verbose_name="Image", ) # Meta icon = FileField( upload_to="topics/icons", blank=True, default="", help_text=("MUST be a black-on-transparent SVG icon ONLY, " "with no bitmap embedded in it."), validators=[check_for_svg_file], ) color = CharField(max_length=14, choices=COLOR_CHOICES, default="blue-40") keywords = ClusterTaggableManager(through=TopicTag, blank=True) # Content panels content_panels = BasePage.content_panels + [ FieldPanel("description"), StreamFieldPanel("featured"), StreamFieldPanel("tabbed_panels"), FieldPanel("latest_articles_count"), MultiFieldPanel( [InlinePanel("people")], heading="People", help_text= "Optional list of people associated with this topic as experts", ), ] # Card panels card_panels = [ FieldPanel("card_title"), FieldPanel("card_description"), ImageChooserPanel("card_image"), ] # Meta panels meta_panels = [ MultiFieldPanel( [ InlinePanel("parent_topics", label="Parent topic(s)"), InlinePanel("child_topics", label="Child topic(s)"), ], heading="Parent/child topic(s)", classname="collapsible collapsed", help_text=("Topics with no parent (i.e. top-level topics) will be " "listed on the home page. Child topics are listed " "on the parent topic’s page."), ), MultiFieldPanel( [FieldPanel("icon"), FieldPanel("color")], heading="Theme", help_text=( "Theme settings used on topic page and any tagged content. " "For example, a post tagged with this topic " "will use the color specified here as its accent color."), ), MultiFieldPanel( [ FieldPanel("seo_title"), FieldPanel("search_description"), ImageChooserPanel("social_image"), FieldPanel("keywords"), ], heading="SEO", help_text=("Optional fields to override the default " "title and description for SEO purposes"), ), ] # Settings panels settings_panels = [FieldPanel("slug"), FieldPanel("show_in_menus")] # 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 articles(self): return get_combined_articles(self, topics__topic__pk=self.pk) @property def events(self): """Return upcoming events for this topic, ignoring events in the past, ordered by start date""" return get_combined_events(self, topics__topic__pk=self.pk, start_date__gte=datetime.datetime.now()) @property def experts(self): """Return Person instances for topic experts""" return [person.person for person in self.people.all()] @property def videos(self): """Return the latest videos and external videos for this topic. """ return get_combined_videos(self, topics__topic__pk=self.pk) @property def color_value(self): return dict(COLOR_VALUES)[self.color] @property def subtopics(self): return [topic.child for topic in self.child_topics.all()]
class Event(BasePage): resource_type = "event" parent_page_types = ["events.Events"] subpage_types = [] template = "event.html" # Content fields description = RichTextField( blank=True, default="", features=RICH_TEXT_FEATURES_SIMPLE, help_text="Optional short text description, max. 400 characters", max_length=400, ) image = ForeignKey( "mozimages.MozImage", null=True, blank=True, on_delete=SET_NULL, related_name="+", ) start_date = DateField(default=datetime.date.today) end_date = DateField(blank=True, null=True) latitude = FloatField(blank=True, null=True) longitude = FloatField(blank=True, null=True) register_url = URLField("Register URL", blank=True, null=True) body = CustomStreamField( blank=True, null=True, help_text=( "Optional body content. Supports rich text, images, embed via URL, " "embed via HTML, and inline code snippets" ), ) venue_name = CharField(max_length=100, blank=True, default="") venue_url = URLField("Venue URL", max_length=100, blank=True, default="") address_line_1 = CharField(max_length=100, blank=True, default="") address_line_2 = CharField(max_length=100, blank=True, default="") address_line_3 = CharField(max_length=100, blank=True, default="") city = CharField(max_length=100, blank=True, default="") state = CharField("State/Province/Region", max_length=100, blank=True, default="") zip_code = CharField("Zip/Postal code", max_length=100, blank=True, default="") country = CountryField(blank=True, default="") agenda = StreamField( StreamBlock([("agenda_item", AgendaItemBlock())], required=False), blank=True, null=True, help_text="Optional list of agenda items for this event", ) speakers = StreamField( StreamBlock( [ ("speaker", PageChooserBlock(target_model="people.Person")), ("external_speaker", ExternalSpeakerBlock()), ], required=False, ), blank=True, null=True, help_text="Optional list of speakers for this event", ) # Card fields card_title = CharField("Title", max_length=140, blank=True, default="") card_description = TextField("Description", max_length=400, blank=True, default="") card_image = ForeignKey( "mozimages.MozImage", null=True, blank=True, on_delete=SET_NULL, related_name="+", verbose_name="Image", ) # Meta fields keywords = ClusterTaggableManager(through=EventTag, blank=True) # Content panels content_panels = BasePage.content_panels + [ FieldPanel("description"), MultiFieldPanel( [ImageChooserPanel("image")], heading="Image", help_text=( "Optional header image. If not specified a fallback will be used. " "This image is also shown when sharing this page via social media" ), ), MultiFieldPanel( [ FieldPanel("start_date"), FieldPanel("end_date"), FieldPanel("latitude"), FieldPanel("longitude"), FieldPanel("register_url"), ], heading="Event details", classname="collapsible", help_text=mark_safe( "Optional time and location information for this event. Latitude and " "longitude are used to show a map of the event’s location. For more " "information on finding these values for a given location, " "'<a href='https://support.google.com/maps/answer/18539'>" "see this article</a>" ), ), StreamFieldPanel("body"), MultiFieldPanel( [ FieldPanel("venue_name"), FieldPanel("venue_url"), FieldPanel("address_line_1"), FieldPanel("address_line_2"), FieldPanel("address_line_3"), FieldPanel("city"), FieldPanel("state"), FieldPanel("zip_code"), FieldPanel("country"), ], heading="Event address", classname="collapsible", help_text=( "Optional address fields. The city and country are also shown " "on event cards" ), ), StreamFieldPanel("agenda"), StreamFieldPanel("speakers"), ] # Card panels card_panels = [ FieldPanel("card_title"), FieldPanel("card_description"), ImageChooserPanel("card_image"), ] # Meta panels meta_panels = [ MultiFieldPanel( [InlinePanel("topics")], heading="Topics", help_text=( "These are the topic pages the event will appear on. The first topic " "in the list will be treated as the primary topic and will be shown " "in the page’s related content." ), ), MultiFieldPanel( [ FieldPanel("seo_title"), FieldPanel("search_description"), ImageChooserPanel("social_image"), FieldPanel("keywords"), ], heading="SEO", help_text=( "Optional fields to override the default title and description " "for SEO purposes" ), ), ] # Settings panels settings_panels = [FieldPanel("slug")] 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 is_upcoming(self): """Returns whether an event is in the future.""" return self.start_date >= datetime.date.today() @property def primary_topic(self): """Return the first (primary) topic specified for the event.""" article_topic = self.topics.first() return article_topic.topic if article_topic else None @property def month_group(self): return self.start_date.replace(day=1) @property def country_group(self): return ( {"slug": self.country.code.lower(), "title": self.country.name} if self.country else {"slug": ""} ) @property def event_dates(self): """Return a formatted string of the event start and end dates""" event_dates = self.start_date.strftime("%b %-d") if self.end_date and self.end_date != self.start_date: event_dates += " – " start_month = self.start_date.strftime("%m") if self.end_date.strftime("%m") == start_month: event_dates += self.end_date.strftime("%-d") else: event_dates += self.end_date.strftime("%b %-d") return event_dates @property def event_dates_full(self): """Return a formatted string of the event start and end dates, including the year""" return self.event_dates + self.start_date.strftime(", %Y") def has_speaker(self, person): for speaker in self.speakers: # pylint: disable=not-an-iterable if speaker.block_type == "speaker" and str(speaker.value) == str( person.title ): return True return False
class BlogIndexPage(Page): intro = RichTextField(blank=True) content_panels = Page.content_panels + [ FieldPanel('intro', classname="full") ]
class PartnerPage(BasePage): STATUS = [('active', 'Active'), ('inactive', 'Inactive')] class Meta: verbose_name = _('Partner Page') parent_page_types = ['partner.PartnerIndexPage'] subpage_types = [] status = models.CharField(choices=STATUS, default='current_partner', max_length=20) public = models.BooleanField(default=True) description = RichTextField(blank=True) web_url = models.URLField(blank=True) logo = models.OneToOneField('images.CustomImage', null=True, blank=True, related_name='+', on_delete=models.SET_NULL) content_panels = Page.content_panels + [ FieldPanel('status'), FieldPanel('public'), FieldPanel('description'), FieldPanel('web_url'), ImageChooserPanel('logo'), ] def __str__(self): return self.title def get_context(self, request): context = super(PartnerPage, self).get_context(request) context['total_investments'] = sum( investment.amount_committed for investment in self.investments.all()) return context def get_absolute_url(self): return self.url @property def category_questions(self): category_questions = {} if not self.investments.exists(): return for investment in self.investments.all(): for category in investment.categories.all(): if category.name in category_questions.keys(): if category.value not in category_questions[category.name]: category_questions[category.name].append( category.value) else: category_questions[category.name] = [category.value] return category_questions def serve(self, request, *args, **kwargs): if not self.public: raise Http404 return super(PartnerPage, self).serve(request, *args, **kwargs)
class BlogPageAbstract(Page): body = RichTextField(verbose_name=_('body'), blank=True) tags = ClusterTaggableManager(through='BlogPageTag', blank=True) date = models.DateField( _("Post date"), default=datetime.datetime.today, help_text=_("This date may be displayed on the blog post. It is not " "used to schedule posts to go live at a later date.")) header_image = models.ForeignKey(get_image_model_string(), null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name=_('Header image')) author = models.ForeignKey( settings.AUTH_USER_MODEL, blank=True, null=True, limit_choices_to=limit_author_choices, verbose_name=_('Author'), on_delete=models.SET_NULL, related_name='author_pages', ) search_fields = Page.search_fields + [ index.SearchField('body'), ] blog_categories = models.ManyToManyField('BlogCategory', through='BlogCategoryBlogPage', blank=True) settings_panels = [ MultiFieldPanel([ FieldRowPanel([ FieldPanel('go_live_at'), FieldPanel('expire_at'), ], classname="label-above"), ], 'Scheduled publishing', classname="publishing"), FieldPanel('date'), FieldPanel('author'), ] def save_revision(self, *args, **kwargs): if not self.author: self.author = self.owner return super().save_revision(*args, **kwargs) def get_absolute_url(self): return self.url class Meta: abstract = True verbose_name = _('Blog page') verbose_name_plural = _('Blog pages') api_fields = [APIField('body')] content_panels = [ FieldPanel('title', classname="full title"), MultiFieldPanel([ FieldPanel('tags'), InlinePanel('categories', label=_("Categories")), ], heading="Tags and Categories"), ImageChooserPanel('header_image'), FieldPanel('body', classname="full"), ]
class ScorePage(RoutablePageMixin, Page): cover_image = ForeignKey('wagtailimages.Image', null=True, blank=True, on_delete=PROTECT, related_name='cover_image') description = StreamField([('rich_text', RichTextBlock()), ('image', ImageChooserBlock())]) duration = DurationField(null=True, blank=True, help_text='Expects data in the format "HH:MM:SS"') file = FileField( upload_to='scores/', validators=[FileExtensionValidator(allowed_extensions=['pdf', 'zip'])]) preview_score = FileField( upload_to='preview_scores/', validators=[FileExtensionValidator(allowed_extensions=['pdf'])]) preview_score_checksum = CharField(editable=False, max_length=256, blank=True) preview_score_checked = False preview_score_updated = False genre = ParentalManyToManyField(Genre, blank=True, related_name='scores') date = CharField(max_length=256, blank=True) instrumentation = ParentalManyToManyField( 'Instrument', blank=True, help_text='The instrumentation of the compostition.') price = DecimalField(max_digits=6, decimal_places=2) materials = RichTextField( blank=True, features=['bold', 'italic', 'link', 'document-link'], help_text='The materials sent in the PDF file.') def save(self, *args, **kwargs): if self.preview_score_checked: # This was the cause of a subtle bug. Because this method can be # called multiple times during model creation, leaving this flag # set would cause the post save hook to fire multiple times. self.preview_score_updated = False return super().save(*args, **kwargs) h = hashlib.md5() for chunk in iter(lambda: self.preview_score.read(8192), b''): h.update(chunk) self.preview_score.seek(0) checksum = h.hexdigest() if not self.preview_score_checksum == checksum: self.preview_score_checksum = checksum self.preview_score_updated = True self.preview_score_checked = True return super().save(*args, **kwargs) @route(r'^([\w-]+)/$') def get_score_file(self, request, score_link_slug): if request.method == 'GET': item_link = get_object_or_404(OrderItemLink, slug=score_link_slug) if item_link.is_expired(): raise Http404() item_link.access_ip = request.META.get('REMOTE_ADDR', '0.0.0.0') item_link.save() return render(request, "main/score_page_download.html", { 'self': self, 'page': self, }) else: raise Http404() @route(r'^$') def score(self, request): cart_page = ShoppingCartPage.objects.first() if request.method == 'POST': in_cart = toggle_score_in_cart(request, self.pk) return render( request, "main/score_page.html", { 'self': self, 'page': self, 'in_cart': in_cart, 'cart_page': cart_page }) else: return render( request, "main/score_page.html", { 'self': self, 'page': self, 'in_cart': score_in_cart(request, self.pk), 'cart_page': cart_page }) class Meta: verbose_name = "Score Page" content_panels = Page.content_panels + [ FieldPanel('date'), FieldPanel('duration'), FieldPanel('genre'), FieldPanel('instrumentation'), FieldPanel('price'), StreamFieldPanel('description'), FieldPanel('materials'), FieldPanel('file'), FieldPanel('preview_score'), ImageChooserPanel('cover_image') ]
class EventPage(AbstractFilterPage): # General content fields body = RichTextField('Subheading', blank=True) archive_body = RichTextField(blank=True) live_body = RichTextField(blank=True) future_body = RichTextField(blank=True) persistent_body = StreamField([ ('content', blocks.RichTextBlock(icon='edit')), ('content_with_anchor', molecules.ContentWithAnchor()), ('heading', v1_blocks.HeadingBlock(required=False)), ('image', molecules.ContentImage()), ('table_block', organisms.AtomicTableBlock(table_options={'renderer': 'html'})), ('reusable_text', v1_blocks.ReusableTextChooserBlock('v1.ReusableText')), ], blank=True) start_dt = models.DateTimeField("Start") end_dt = models.DateTimeField("End", blank=True, null=True) future_body = RichTextField(blank=True) archive_image = models.ForeignKey('wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+') video_transcript = models.ForeignKey('wagtaildocs.Document', null=True, blank=True, on_delete=models.SET_NULL, related_name='+') speech_transcript = models.ForeignKey('wagtaildocs.Document', null=True, blank=True, on_delete=models.SET_NULL, related_name='+') flickr_url = models.URLField("Flickr URL", blank=True) archive_video_id = models.CharField( 'YouTube video ID (archive)', null=True, blank=True, max_length=11, # This is a reasonable but not official regex for YouTube video IDs. # https://webapps.stackexchange.com/a/54448 validators=[RegexValidator(regex=r'^[\w-]{11}$')], help_text=organisms.VideoPlayer.YOUTUBE_ID_HELP_TEXT) live_stream_availability = models.BooleanField( "Streaming?", default=False, blank=True, help_text='Check if this event will be streamed live. This causes the ' 'event page to show the parts necessary for live streaming.') live_video_id = models.CharField( 'YouTube video ID (live)', null=True, blank=True, max_length=11, # This is a reasonable but not official regex for YouTube video IDs. # https://webapps.stackexchange.com/a/54448 validators=[RegexValidator(regex=r'^[\w-]{11}$')], help_text=organisms.VideoPlayer.YOUTUBE_ID_HELP_TEXT) live_stream_date = models.DateTimeField( "Go Live Date", blank=True, null=True, help_text='Enter the date and time that the page should switch from ' 'showing the venue image to showing the live video feed. ' 'This is typically 15 minutes prior to the event start time.') # Venue content fields venue_coords = models.CharField(max_length=100, blank=True) venue_name = models.CharField(max_length=100, blank=True) venue_street = models.CharField(max_length=100, blank=True) venue_suite = models.CharField(max_length=100, blank=True) venue_city = models.CharField(max_length=100, blank=True) venue_state = USStateField(blank=True) venue_zipcode = models.CharField(max_length=12, blank=True) venue_image_type = models.CharField( max_length=8, choices=( ('map', 'Map'), ('image', 'Image (selected below)'), ('none', 'No map or image'), ), default='map', help_text='If "Image" is chosen here, you must select the image you ' 'want below. It should be sized to 1416x796.', ) venue_image = models.ForeignKey('v1.CFGOVImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+') post_event_image_type = models.CharField( max_length=16, choices=( ('placeholder', 'Placeholder image'), ('image', 'Unique image (selected below)'), ), default='placeholder', verbose_name='Post-event image type', help_text='Choose what to display after an event concludes. This will ' 'be overridden by embedded video if the "YouTube video ID ' '(archive)" field on the previous tab is populated. If ' '"Unique image" is chosen here, you must select the image ' 'you want below. It should be sized to 1416x796.', ) post_event_image = models.ForeignKey('v1.CFGOVImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+') # Agenda content fields agenda_items = StreamField([('item', AgendaItemBlock())], blank=True) objects = CFGOVPageManager() search_fields = AbstractFilterPage.search_fields + [ index.SearchField('body'), index.SearchField('archive_body'), index.SearchField('live_video_id'), index.SearchField('flickr_url'), index.SearchField('archive_video_id'), index.SearchField('future_body'), index.SearchField('agenda_items') ] # General content tab content_panels = CFGOVPage.content_panels + [ FieldPanel('body'), FieldRowPanel([ FieldPanel('start_dt', classname="col6"), FieldPanel('end_dt', classname="col6"), ]), MultiFieldPanel([ FieldPanel('archive_body'), ImageChooserPanel('archive_image'), DocumentChooserPanel('video_transcript'), DocumentChooserPanel('speech_transcript'), FieldPanel('flickr_url'), FieldPanel('archive_video_id'), ], heading='Archive Information'), FieldPanel('live_body'), FieldPanel('future_body'), StreamFieldPanel('persistent_body'), MultiFieldPanel([ FieldPanel('live_stream_availability'), FieldPanel('live_video_id'), FieldPanel('live_stream_date'), ], heading='Live Stream Information'), ] # Venue content tab venue_panels = [ FieldPanel('venue_name'), MultiFieldPanel([ FieldPanel('venue_street'), FieldPanel('venue_suite'), FieldPanel('venue_city'), FieldPanel('venue_state'), FieldPanel('venue_zipcode'), ], heading='Venue Address'), MultiFieldPanel([ FieldPanel('venue_image_type'), ImageChooserPanel('venue_image'), ], heading='Venue Image'), MultiFieldPanel([ FieldPanel('post_event_image_type'), ImageChooserPanel('post_event_image'), ], heading='Post-event Image') ] # Agenda content tab agenda_panels = [ StreamFieldPanel('agenda_items'), ] # Promotion panels promote_panels = [ MultiFieldPanel(AbstractFilterPage.promote_panels, "Page configuration"), ] # Tab handler interface edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), ObjectList(venue_panels, heading='Venue Information'), ObjectList(agenda_panels, heading='Agenda Information'), ObjectList(AbstractFilterPage.sidefoot_panels, heading='Sidebar'), ObjectList(AbstractFilterPage.settings_panels, heading='Configuration'), ]) template = 'events/event.html' @property def event_state(self): if self.end_dt: end = convert_date(self.end_dt, 'America/New_York') if end < datetime.now(timezone('America/New_York')): return 'past' if self.live_stream_date: start = convert_date(self.live_stream_date, 'America/New_York') else: start = convert_date(self.start_dt, 'America/New_York') if datetime.now(timezone('America/New_York')) > start: return 'present' return 'future' @property def page_js(self): if ((self.live_stream_date and self.event_state == 'present') or (self.archive_video_id and self.event_state == 'past')): return super(EventPage, self).page_js + ['video-player.js'] return super(EventPage, self).page_js def location_image_url(self, scale='2', size='276x155', zoom='12'): if not self.venue_coords: self.venue_coords = get_venue_coords(self.venue_city, self.venue_state) api_url = 'https://api.mapbox.com/styles/v1/mapbox/streets-v11/static' static_map_image_url = '{}/{},{}/{}?access_token={}'.format( api_url, self.venue_coords, zoom, size, settings.MAPBOX_ACCESS_TOKEN) return static_map_image_url def clean(self): super(EventPage, self).clean() if self.venue_image_type == 'image' and not self.venue_image: raise ValidationError( {'venue_image': 'Required if "Venue image type" is "Image".'}) if self.post_event_image_type == 'image' and not self.post_event_image: raise ValidationError({ 'post_event_image': 'Required if "Post-event image type" is ' '"Image".' }) def save(self, *args, **kwargs): self.venue_coords = get_venue_coords(self.venue_city, self.venue_state) return super(EventPage, self).save(*args, **kwargs) def get_context(self, request): context = super(EventPage, self).get_context(request) context['event_state'] = self.event_state return context
class ParticipatePage2(PrimaryPage): parent_page_types = ['Homepage'] template = 'wagtailpages/static/participate_page2.html' ctaHero = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='primary_hero_participate', verbose_name='Primary Hero Image', ) ctaHeroHeader = models.TextField(blank=True, ) ctaHeroSubhead = RichTextField( features=[ 'bold', 'italic', 'link', ], blank=True, ) ctaCommitment = models.TextField(blank=True, ) ctaButtonTitle = models.CharField( verbose_name='Button Text', max_length=250, blank=True, ) ctaButtonURL = models.TextField( verbose_name='Button URL', blank=True, ) ctaHero2 = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='primary_hero_participate2', verbose_name='Primary Hero Image', ) ctaHeroHeader2 = models.TextField(blank=True, ) ctaHeroSubhead2 = RichTextField( features=[ 'bold', 'italic', 'link', ], blank=True, ) ctaCommitment2 = models.TextField(blank=True, ) ctaButtonTitle2 = models.CharField( verbose_name='Button Text', max_length=250, blank=True, ) ctaButtonURL2 = models.TextField( verbose_name='Button URL', blank=True, ) ctaHero3 = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='primary_hero_participate3', verbose_name='Primary Hero Image', ) ctaHeroHeader3 = models.TextField(blank=True, ) ctaHeroSubhead3 = RichTextField( features=[ 'bold', 'italic', 'link', ], blank=True, ) ctaCommitment3 = models.TextField(blank=True, ) ctaFacebook3 = models.TextField(blank=True, ) ctaTwitter3 = models.TextField(blank=True, ) ctaEmailShareBody3 = models.TextField(blank=True, ) ctaEmailShareSubject3 = models.TextField(blank=True, ) h2 = models.TextField(blank=True, ) h2Subheader = models.TextField( blank=True, verbose_name='H2 Subheader', ) content_panels = Page.content_panels + [ MultiFieldPanel([ ImageChooserPanel('ctaHero'), FieldPanel('ctaHeroHeader'), FieldPanel('ctaHeroSubhead'), FieldPanel('ctaCommitment'), FieldPanel('ctaButtonTitle'), FieldPanel('ctaButtonURL'), ], heading="Primary CTA"), FieldPanel('h2'), FieldPanel('h2Subheader'), InlinePanel( 'featured_highlights', label='Highlights Group 1', max_num=3), MultiFieldPanel([ ImageChooserPanel('ctaHero2'), FieldPanel('ctaHeroHeader2'), FieldPanel('ctaHeroSubhead2'), FieldPanel('ctaCommitment2'), FieldPanel('ctaButtonTitle2'), FieldPanel('ctaButtonURL2'), ], heading="CTA 2"), InlinePanel( 'featured_highlights2', label='Highlights Group 2', max_num=6), MultiFieldPanel([ ImageChooserPanel('ctaHero3'), FieldPanel('ctaHeroHeader3'), FieldPanel('ctaHeroSubhead3'), FieldPanel('ctaCommitment3'), FieldPanel('ctaFacebook3'), FieldPanel('ctaTwitter3'), FieldPanel('ctaEmailShareSubject3'), FieldPanel('ctaEmailShareBody3'), ], heading="CTA 3"), InlinePanel('cta4', label='CTA Group 4', max_num=3), ]
class AnswerPage(CFGOVPage): """Page type for Ask CFPB answers.""" from ask_cfpb.models.django import Answer last_edited = models.DateField( blank=True, null=True, help_text="Change the date to today if you make a significant change.") question = models.TextField(blank=True) statement = models.TextField( blank=True, help_text=( "(Optional) Use this field to rephrase the question title as " "a statement. Use only if this answer has been chosen to appear " "on a money topic portal (e.g. /consumer-tools/debt-collection).")) short_answer = RichTextField(blank=True, features=['link', 'document-link'], help_text='Optional answer intro') answer_content = StreamField(ask_blocks.AskAnswerContent(), blank=True, verbose_name='Answer') answer_base = models.ForeignKey(Answer, blank=True, null=True, related_name='answer_pages', on_delete=models.SET_NULL) redirect_to_page = models.ForeignKey( 'self', blank=True, null=True, on_delete=models.SET_NULL, related_name='redirect_to_pages', help_text="Choose another AnswerPage to redirect this page to") featured = models.BooleanField( default=False, help_text=("Check to make this one of two featured answers " "on the landing page.")) featured_rank = models.IntegerField(blank=True, null=True) category = models.ManyToManyField( 'Category', blank=True, help_text=("Categorize this answer. " "Avoid putting into more than one category.")) search_tags = models.CharField( max_length=1000, blank=True, help_text="Search words or phrases, separated by commas") related_resource = models.ForeignKey(RelatedResource, blank=True, null=True, on_delete=models.SET_NULL) related_questions = ParentalManyToManyField( 'self', symmetrical=False, blank=True, related_name='related_question', help_text='Maximum of 3 related questions') portal_topic = ParentalManyToManyField( PortalTopic, blank=True, help_text='Limit to 1 portal topic if possible') primary_portal_topic = ParentalKey( PortalTopic, blank=True, null=True, on_delete=models.SET_NULL, related_name='primary_portal_topic', help_text=("Use only if assigning more than one portal topic, " "to control which topic is used as a breadcrumb.")) portal_category = ParentalManyToManyField(PortalCategory, blank=True) user_feedback = StreamField([ ('feedback', v1_blocks.Feedback()), ], blank=True) share_and_print = models.BooleanField( default=False, help_text="Include share and print buttons above answer.") content_panels = CFGOVPage.content_panels + [ MultiFieldPanel([ FieldPanel('last_edited'), FieldPanel('question'), FieldPanel('statement'), FieldPanel('short_answer') ], heading="Page content", classname="collapsible"), FieldPanel('share_and_print'), StreamFieldPanel('answer_content'), MultiFieldPanel([ SnippetChooserPanel('related_resource'), AutocompletePanel('related_questions', target_model='ask_cfpb.AnswerPage') ], heading="Related resources", classname="collapsible"), MultiFieldPanel([ FieldPanel('portal_topic', widget=forms.CheckboxSelectMultiple), FieldPanel('primary_portal_topic'), FieldPanel('portal_category', widget=forms.CheckboxSelectMultiple) ], heading="Portal tags", classname="collapsible"), MultiFieldPanel([FieldPanel('featured')], heading="Featured answer on Ask landing page", classname="collapsible"), MultiFieldPanel([ AutocompletePanel('redirect_to_page', target_model='ask_cfpb.AnswerPage') ], heading="Redirect to another answer", classname="collapsible"), MultiFieldPanel([StreamFieldPanel('user_feedback')], heading="User feedback", classname="collapsible collapsed"), ] sidebar = StreamField([ ('call_to_action', molecules.CallToAction()), ('related_links', molecules.RelatedLinks()), ('related_metadata', molecules.RelatedMetadata()), ('email_signup', organisms.EmailSignUp()), ('sidebar_contact', organisms.SidebarContactInfo()), ('rss_feed', molecules.RSSFeed()), ('social_media', molecules.SocialMedia()), ('reusable_text', v1_blocks.ReusableTextChooserBlock(ReusableText)), ], blank=True) sidebar_panels = [ StreamFieldPanel('sidebar'), ] search_fields = Page.search_fields + [ index.SearchField('answer_content'), index.SearchField('short_answer') ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(sidebar_panels, heading='Sidebar'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) template = 'ask-cfpb/answer-page.html' objects = CFGOVPageManager() def get_sibling_url(self): if self.answer_base: if self.language == 'es': sibling = self.answer_base.english_page else: sibling = self.answer_base.spanish_page if sibling and sibling.live and not sibling.redirect_to_page: return sibling.url def get_meta_description(self): """Determine what the page's meta and OpenGraph description should be Checks several different possible fields in order of preference. If none are found, returns an empty string, which is preferable to a generic description repeated on many pages. This method is overriding the standard one on CFGOVPage to factor in Ask CFPB AnswerPage-specific fields. """ preference_order = [ 'search_description', 'short_answer', 'first_text', ] candidates = {} if self.search_description: candidates['search_description'] = self.search_description if self.short_answer: candidates['short_answer'] = strip_tags(self.short_answer) if hasattr(self, 'answer_content'): for block in self.answer_content: if block.block_type == 'text': candidates['first_text'] = truncate_by_words_and_chars( strip_tags(block.value['content'].source), word_limit=35, char_limit=160) break for entry in preference_order: if candidates.get(entry): return candidates[entry] return '' def get_context(self, request, *args, **kwargs): # self.get_meta_description() is not called here because it is called # and added to the context by CFGOVPage's get_context() method. portal_topic = self.primary_portal_topic or self.portal_topic.first() context = super(AnswerPage, self).get_context(request) context['related_questions'] = self.related_questions.all() context['last_edited'] = self.last_edited context['portal_page'] = get_portal_or_portal_search_page( portal_topic, language=self.language) context['breadcrumb_items'] = get_ask_breadcrumbs( language=self.language, portal_topic=portal_topic, ) context['about_us'] = get_standard_text(self.language, 'about_us') context['disclaimer'] = get_standard_text(self.language, 'disclaimer') context['sibling_url'] = self.get_sibling_url() return context def answer_content_text(self): raw_text = extract_raw_text(self.answer_content.stream_data) return strip_tags(" ".join([self.short_answer, raw_text])) def answer_content_data(self): return truncate_by_words_and_chars(self.answer_content_text()) def short_answer_data(self): return ' '.join( RichTextField.get_searchable_content(self, self.short_answer)) def text(self): short_answer = self.short_answer_data() answer_text = self.answer_content_text() full_text = f"{short_answer}\n\n{answer_text}\n\n{self.question}" return full_text def __str__(self): if self.answer_base: return f"{self.answer_base.id}: {self.title}" else: return self.title @property def clean_search_tags(self): return [tag.strip() for tag in self.search_tags.split(",")] @property def status_string(self): if self.redirect_to_page: if not self.live: return ("redirected but not live") else: return ("redirected") else: return super(AnswerPage, self).status_string # Returns an image for the page's meta Open Graph tag @property def meta_image(self): if self.social_sharing_image: return self.social_sharing_image if not self.category.exists(): return None return self.category.first().category_image # Overrides the default of page.id for comparing against split testing # clusters. See: core.feature_flags.in_split_testing_cluster @property def split_test_id(self): return self.answer_base.id
class PostPage(Page): body = RichTextField(blank=True) content_panels = Page.content_panels + [ FieldPanel('body', classname='full'), ]
class CustomMedia(AbstractMedia): fancy_caption = RichTextField(blank=True) admin_form_fields = Media.admin_form_fields + ("fancy_caption", )
class ArticlePage( BasicPageAbstract, ContentPage, FeatureablePageAbstract, FromTheArchivesPageAbstract, ShareablePageAbstract, ThemeablePageAbstract, ): class ArticleTypes(models.TextChoices): CIGI_IN_THE_NEWS = ('cigi_in_the_news', 'CIGI in the News') INTERVIEW = ('interview', 'Interview') NEWS_RELEASE = ('news_release', 'News Release') OP_ED = ('op_ed', 'Op-Ed') OPINION = ('opinion', 'Opinion') class Languages(models.TextChoices): DA = ('da', 'Danish') DE = ('de', 'German') EL = ('el', 'Greek') EN = ('en', 'English') ES = ('es', 'Spanish') FR = ('fr', 'French') ID = ('id', 'Indonesian') IT = ('it', 'Italian') NL = ('nl', 'Dutch') PL = ('pl', 'Polish') PT = ('pt', 'Portugese') RO = ('ro', 'Romanian') SK = ('sk', 'Slovak') SV = ('sv', 'Swedish') TR = ('tr', 'Turkish') ZH = ('zh', 'Chinese') class HeroTitlePlacements(models.TextChoices): BOTTOM = ('bottom', 'Bottom') TOP = ('top', 'Top') article_series = models.ForeignKey( 'wagtailcore.Page', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Opinion series', ) article_type = models.ForeignKey( 'articles.ArticleTypePage', null=True, blank=False, on_delete=models.SET_NULL, related_name='articles', ) body = StreamField( BasicPageAbstract.body_default_blocks + [ BasicPageAbstract.body_accordion_block, BasicPageAbstract.body_autoplay_video_block, BasicPageAbstract.body_chart_block, BasicPageAbstract.body_embedded_tiktok_block, BasicPageAbstract.body_external_quote_block, BasicPageAbstract.body_external_video_block, BasicPageAbstract.body_extract_block, BasicPageAbstract.body_highlight_title_block, BasicPageAbstract.body_image_full_bleed_block, BasicPageAbstract.body_image_scroll_block, BasicPageAbstract.body_poster_block, BasicPageAbstract.body_pull_quote_left_block, BasicPageAbstract.body_pull_quote_right_block, BasicPageAbstract.body_recommended_block, BasicPageAbstract.body_text_border_block, BasicPageAbstract.body_tool_tip_block, BasicPageAbstract.body_tweet_block, ], blank=True, ) 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.', ) embed_youtube_label = models.CharField( max_length=255, blank=True, help_text='Add a label to appear below the embedded video.', ) footnotes = RichTextField( blank=True, features=[ 'bold', 'endofarticle', 'h3', 'h4', 'italic', 'link', 'ol', 'ul', 'subscript', 'superscript', 'anchor', ], ) hero_title_placement = models.CharField( blank=True, max_length=16, choices=HeroTitlePlacements.choices, verbose_name='Hero Title Placement', help_text= 'Placement of the title within the hero section. Currently only works on the Longform 2 theme.', ) hide_excerpt = models.BooleanField( default=False, verbose_name='Hide Excerpt', help_text= 'For "CIGI in the News" only: when enabled, hide excerpt and display full article instead', ) image_banner = models.ForeignKey( 'images.CigionlineImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Banner Image', ) image_banner_small = models.ForeignKey('images.CigionlineImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Banner Image Small') 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 used in feature sections', ) interviewers = StreamField( [ ('interviewer', PageChooserBlock(required=True, page_type='people.PersonPage')), ], blank=True, ) language = models.CharField( blank=True, max_length=2, choices=Languages.choices, verbose_name='Language', help_text= 'If this content is in a language other than English, please select the language from the list.', ) multimedia_series = models.ForeignKey( 'wagtailcore.Page', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', ) related_files = StreamField( [ ('file', DocumentChooserBlock()), ], blank=True, ) short_description = RichTextField( blank=True, null=False, features=['bold', 'italic', 'link'], ) video_banner = models.ForeignKey( 'wagtailmedia.Media', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Banner Video', ) website_button_text = models.CharField( blank=True, max_length=64, help_text= 'Override the button text for the article website. If empty, the button will read "View Full Article".' ) website_url = models.URLField(blank=True, max_length=512) works_cited = RichTextField( blank=True, features=[ 'bold', 'endofarticle', 'h3', 'h4', 'italic', 'link', 'ol', 'ul', 'subscript', 'superscript', ], ) # Reference field for the Drupal-Wagtail migrator. Can be removed after. drupal_node_id = models.IntegerField(blank=True, null=True) @property def cigi_people_mentioned_ids(self): return [item.person.id for item in self.cigi_people_mentioned.all()] @property def expired_image(self): if self.publishing_date: return self.publishing_date < datetime.datetime( 2017, 1, 1).astimezone(pytz.timezone('America/Toronto')) return False @property def article_series_description(self): if self.article_series: return self.article_series.specific.series_items_description return None @property def article_series_disclaimer(self): if self.article_series: for series_item in self.article_series.specific.article_series_items: if series_item.content_page.specific == self and not series_item.hide_series_disclaimer: return self.article_series.specific.series_items_disclaimer return None def is_opinion(self): return self.article_type.title in [ 'Op-Eds', 'Opinion', ] def get_template(self, request, *args, **kwargs): standard_template = super(ArticlePage, self).get_template(request, *args, **kwargs) if self.theme: return f'themes/{self.get_theme_dir()}/article_page.html' return standard_template content_panels = [ BasicPageAbstract.title_panel, MultiFieldPanel([ FieldPanel('short_description'), StreamFieldPanel('body'), FieldPanel('footnotes'), FieldPanel('works_cited'), ], heading='Body', classname='collapsible collapsed'), MultiFieldPanel( [ PageChooserPanel( 'article_type', ['articles.ArticleTypePage'], ), FieldPanel('hide_excerpt'), FieldPanel('publishing_date'), FieldPanel('website_url'), FieldPanel('website_button_text'), FieldPanel('language'), ], heading='General Information', classname='collapsible collapsed', ), ContentPage.authors_panel, MultiFieldPanel( [ ImageChooserPanel('image_hero'), ImageChooserPanel('image_poster'), ImageChooserPanel('image_banner'), ImageChooserPanel('image_banner_small'), ], heading='Images', classname='collapsible collapsed', ), MultiFieldPanel( [ FieldPanel('embed_youtube'), FieldPanel('embed_youtube_label'), MediaChooserPanel('video_banner'), ], heading='Media', classname='collapsible collapsed', ), ContentPage.recommended_panel, MultiFieldPanel( [ FieldPanel('topics'), FieldPanel('projects'), PageChooserPanel( 'article_series', ['articles.ArticleSeriesPage'], ), PageChooserPanel( 'multimedia_series', ['multimedia.MultimediaSeriesPage'], ), InlinePanel('cigi_people_mentioned', label='People Mentioned'), StreamFieldPanel('interviewers'), StreamFieldPanel('related_files'), ], heading='Related', classname='collapsible collapsed', ), FromTheArchivesPageAbstract.from_the_archives_panel, ] promote_panels = Page.promote_panels + [ FeatureablePageAbstract.feature_panel, ShareablePageAbstract.social_panel, SearchablePageAbstract.search_panel, ] settings_panels = Page.settings_panels + [ ThemeablePageAbstract.theme_panel, ] search_fields = BasicPageAbstract.search_fields \ + ContentPage.search_fields \ + [ index.FilterField('article_type'), index.FilterField('cigi_people_mentioned_ids'), index.FilterField('publishing_date'), ] parent_page_types = ['articles.ArticleListPage'] subpage_types = [] templates = 'articles/article_page.html' @property def is_title_bottom(self): return self.title in [ 'Can the G20 Save Globalization\'s Waning Reputation?', 'Shoshana Zuboff on the Undetectable, Indecipherable World of Surveillance Capitalism' ] @property def article_series_category(self): category = '' for series_item in self.article_series.specific.article_series_items: if series_item.category_title: category = series_item.category_title if series_item.content_page.id == self.id: return category class Meta: verbose_name = 'Opinion' verbose_name_plural = 'Opinions'
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 get_content(self): ''' A generic and generative interface for getting all the content block for an article, including advanced content such as chapters. ''' for block in self.body: yield block for chapter in self.chapters: for block in chapter.value['body']: yield block 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"), ])
class ArticleSeriesPage( BasicPageAbstract, ContentPage, FeatureablePageAbstract, FromTheArchivesPageAbstract, ShareablePageAbstract, ThemeablePageAbstract, ): credits = RichTextField( blank=True, features=[ 'bold', 'italic', 'link', 'name', ], ) credits_stream_field = StreamField( [('title', StructBlock([ ('title', CharBlock()), ('people', StreamBlock([('name', CharBlock())])), ]))], blank=True, ) credits_artwork = models.CharField( max_length=255, blank=True, ) featured_items = StreamField( [ ('featured_item', PageChooserBlock( required=True, page_type=[ 'articles.ArticlePage', 'multimedia.MultimediaPage' ], )), ], blank=True, ) image_banner = models.ForeignKey( 'images.CigionlineImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Banner Image', ) image_banner_small = models.ForeignKey('images.CigionlineImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Banner Image Small') 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.', ) short_description = RichTextField( blank=True, null=False, features=['bold', 'italic', 'link'], ) series_items_description = RichTextField( blank=True, null=True, features=['bold', 'italic', 'link'], ) series_videos_description = RichTextField( blank=True, null=True, features=['bold', 'italic', 'link'], help_text= 'To be displayed on video/multimedia pages of the series in place of Series Items Description' ) series_items_disclaimer = RichTextField( blank=True, null=True, features=['bold', 'italic', 'link'], ) video_banner = models.ForeignKey( 'wagtailmedia.Media', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Banner Video', ) @property def image_poster_caption(self): return self.image_poster.caption @property def image_poster_url(self): return self.image_poster.get_rendition('fill-672x895').url @property def article_series_items(self): return self.series_items.prefetch_related( 'content_page', 'content_page__authors__author', ).all() # Reference field for the Drupal-Wagtail migrator. Can be removed after. drupal_node_id = models.IntegerField(blank=True, null=True) def get_template(self, request, *args, **kwargs): standard_template = super(ArticleSeriesPage, self).get_template(request, *args, **kwargs) if self.theme: return f'themes/{self.get_theme_dir()}/article_series_page.html' return standard_template content_panels = [ BasicPageAbstract.title_panel, MultiFieldPanel( [ FieldPanel('short_description'), StreamFieldPanel('body'), ], heading='Body', classname='collapsible collapsed', ), MultiFieldPanel( [ FieldPanel('publishing_date'), ], heading='General Information', classname='collapsible collapsed', ), MultiFieldPanel( [ FieldPanel('series_items_description'), FieldPanel('series_videos_description'), FieldPanel('series_items_disclaimer'), InlinePanel('series_items'), ], heading='Series Items', classname='collapsible collapsed', ), MultiFieldPanel( [ FieldPanel('credits'), FieldPanel('credits_artwork'), StreamFieldPanel('credits_stream_field'), ], heading='Credits', classname='collapsible collapsed', ), MultiFieldPanel( [ ImageChooserPanel('image_hero'), ImageChooserPanel('image_banner'), ImageChooserPanel('image_banner_small'), ImageChooserPanel('image_poster'), ], heading='Image', classname='collapsible collapsed', ), MultiFieldPanel( [ MediaChooserPanel('video_banner'), ], heading='Media', classname='collapsible collapsed', ), MultiFieldPanel( [ StreamFieldPanel('featured_items'), ], heading='Featured Series Items', classname='collapsible collapsed', ), MultiFieldPanel( [ FieldPanel('topics'), ], heading='Related', classname='collapsible collapsed', ), ] promote_panels = Page.promote_panels + [ FeatureablePageAbstract.feature_panel, ShareablePageAbstract.social_panel, SearchablePageAbstract.search_panel, ] settings_panels = Page.settings_panels + [ ThemeablePageAbstract.theme_panel, ] search_fields = Page.search_fields \ + BasicPageAbstract.search_fields \ + ContentPage.search_fields parent_page_types = ['home.HomePage'] subpage_types = [] templates = 'articles/article_series_page.html' @property def series_contributors_by_article(self): series_contributors = [] item_people = set() for series_item in self.article_series_items: people = series_item.content_page.authors.all() people_string = '' for person in people: person_string = person.author.title people_string += person_string # Add each person as well so if there's an article with just # a single author who's already been in another article in # collaboration, then we won't add their name to the list # again. if len(people) > 1: item_people.add(person_string) if people_string not in item_people: series_contributors.append({ 'item': series_item.content_page, 'contributors': people }) item_people.add(people_string) return series_contributors @property def series_contributors(self): series_contributors = [] item_people = set() for series_item in self.article_series_items: people = series_item.content_page.authors.all() for person in people: if person.author.title not in item_people: series_contributors.append({ 'id': person.author.id, 'title': person.author.title, 'url': person.author.url, }) item_people.add(person.author.title) return series_contributors @property def series_contributors_by_person(self): # Series contributors ordered by last name series_contributors = [] item_people = set() for series_item in self.article_series_items: people = series_item.content_page.authors.all() # Skip items that have more than 2 authors/speakers. For # example, in the After COVID series, there is an introductory # video with many authors. if len(people) > 2: continue else: for person in people: if person.author.title not in item_people: series_contributors.append({ 'item': series_item.content_page, 'contributors': [person.author], 'last_name': person.author.last_name, }) item_people.add(person.author.title) series_contributors.sort(key=lambda x: x['last_name']) return series_contributors @property def series_authors(self): series_authors = [] series_people = set() for series_item in self.article_series_items: people = series_item.content_page.authors.all() for person in people: if person.author.title not in series_people: series_authors.append(person.author) series_people.add(person.author.title) return series_authors class Meta: verbose_name = 'Opinion Series' verbose_name_plural = 'Opinion Series'
class AtlasCaseStudyIndexPage(Page): # title already in the Page class # slug already in the Page class subpage_types = ["atlascasestudies.AtlasCaseStudy"] body = RichTextField(blank=True) # so we can filter available categories based on the sub site as well as the # sub_site_categories = models.ForeignKey( # CategorySubSite, # on_delete=models.PROTECT, # related_name='category_blog_site', # null=True, # ) content_panels = Page.content_panels + [ # FieldPanel('sub_site_categories'), FieldPanel("body"), ] def get_latest_atlas_case_studies(self, num): return AtlasCaseStudy.objects.all().order_by( "-first_published_at")[:num] def get_context(self, request, *args, **kwargs): atlas_case_study_ordering = "-first_published_at" context = super().get_context(request, *args, **kwargs) if request.GET.get("setting"): context["chosen_setting_id"] = int(request.GET.get("setting")) atlas_case_studies = (AtlasCaseStudy.objects.live( ).order_by(atlas_case_study_ordering).filter( atlas_case_study_setting_relationship__setting=request.GET.get( "setting"))) elif request.GET.get("region"): context["chosen_region_id"] = int(request.GET.get("region")) atlas_case_studies = (AtlasCaseStudy.objects.live( ).order_by(atlas_case_study_ordering).filter( atlas_case_study_region_relationship__region=request.GET.get( "region"))) elif request.GET.get("category"): context["chosen_category_id"] = int(request.GET.get("category")) atlas_case_studies = (AtlasCaseStudy.objects.live().order_by( atlas_case_study_ordering).filter( atlas_case_study_category_relationship__category=request. GET.get("category"))) else: atlas_case_studies = AtlasCaseStudy.objects.live().order_by( atlas_case_study_ordering) paginator = Paginator(atlas_case_studies, 16) try: items = paginator.page(request.GET.get("page")) except PageNotAnInteger: items = paginator.page(1) except EmptyPage: items = paginator.page(paginator.num_pages) context["atlas_case_studies"] = items category_sub_site = CategorySubSite.objects.get(source="categories") context["categories"] = Category.objects.filter( sub_site=category_sub_site) context["setting"] = Setting.objects.all() context["regions"] = Region.objects.all() # an experiment to get only categories that are used by blogs # blog_pages_ids = [x.id for x in Blog.objects.all()] # print(blog_pages_ids) # print(Category.objects.filter(blog_categories__in=blog_pages_ids)) # context['categories'] = Category.objects.filter(blog_categories__in=blog_pages_ids) return context
class Article(BasePage): # IMPORTANT: EACH ARTICLE is NOW LABELLED "POST" IN THE FRONT END resource_type = "article" # If you change this, CSS will need updating, too parent_page_types = ["Articles"] subpage_types = [] template = "article.html" class Meta: verbose_name = "post" # NB verbose_name_plural = "posts" # NB # Content fields description = RichTextField( blank=True, default="", features=RICH_TEXT_FEATURES_SIMPLE, help_text="Optional short text description, max. 400 characters", max_length=400, ) body = CustomStreamField(help_text=( "The main post content. Supports rich text, images, embed via URL, " "embed via HTML, and inline code snippets")) related_links = StreamField( StreamBlock([("link", ExternalLinkBlock())], required=False), blank=True, null=True, help_text="Optional links further reading", verbose_name="Related links", ) # Card fields card_title = CharField("Title", max_length=140, blank=True, default="") card_description = TextField("Description", max_length=400, blank=True, default="") card_image = ForeignKey( "mozimages.MozImage", null=True, blank=True, on_delete=SET_NULL, related_name="+", verbose_name="Image", help_text="An image in 16:9 aspect ratio", ) card_image_3_2 = ForeignKey( "mozimages.MozImage", null=True, blank=True, on_delete=SET_NULL, related_name="+", verbose_name="Image", help_text="An image in 3:2 aspect ratio", ) # Meta fields date = DateField( "Post date", default=datetime.date.today, help_text="The date the post was published", ) authors = StreamField( StreamBlock( [ ("author", PageChooserBlock(target_model="people.Person")), ("external_author", ExternalAuthorBlock()), ], required=False, ), blank=True, null=True, help_text=( "Optional list of the post's authors. Use ‘External author’ to add " "guest authors without creating a profile on the system"), ) keywords = ClusterTaggableManager(through=ArticleTag, blank=True) # Content panels content_panels = BasePage.content_panels + [ FieldPanel("description"), StreamFieldPanel("body"), StreamFieldPanel("related_links"), ] # Card panels card_panels = [ FieldPanel("card_title"), FieldPanel("card_description"), MultiFieldPanel( [ImageChooserPanel("card_image")], heading="16:9 Image", help_text=( "Image used for representing this page as a Card. " "Should be 16:9 aspect ratio. " "If not specified a fallback will be used. " "This image is also shown when sharing this page via social " "media unless a social image is specified."), ), MultiFieldPanel( [ImageChooserPanel("card_image_3_2")], heading="3:2 Image", help_text=("Image used for representing this page as a Card. " "Should be 3:2 aspect ratio. " "If not specified a fallback will be used. "), ), ] # Meta panels meta_panels = [ FieldPanel("date"), StreamFieldPanel("authors"), MultiFieldPanel( [InlinePanel("topics")], heading="Topics", help_text= ("The topic pages this post will appear on. The first topic in the " "list will be treated as the primary topic and will be shown in the " "page’s related content."), ), MultiFieldPanel( [ FieldPanel("seo_title"), FieldPanel("search_description"), ImageChooserPanel("social_image"), FieldPanel("keywords"), ], heading="SEO", help_text=( "Optional fields to override the default title and description " "for SEO purposes"), ), ] # Settings panels settings_panels = BasePage.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"), ]) def get_absolute_url(self): # For the RSS feed return self.full_url @property def verbose_standfirst(self): """Return a marked-safe HTML snippet that can be used as a verbose standfirst""" template = "partials/verbose_standfirst.html" rendered = render_to_string(template, context={"page": self}) return rendered @property def primary_topic(self): """Return the first (primary) topic specified for the Article.""" article_topic = self.topics.first() return article_topic.topic if article_topic else None @property def read_time(self): return str(readtime.of_html(str(self.body))) @property def related_resources(self): """Returns resources that are related to the current resource, i.e. live, public Articles and Videos which have the same Topics.""" topic_pks = [topic.topic.pk for topic in self.topics.all()] return get_combined_articles_and_videos( self, topics__topic__pk__in=topic_pks) @property def month_group(self): return self.date.replace(day=1) def has_author(self, person): for author in self.authors: # pylint: disable=not-an-iterable if author.block_type == "author" and str(author.value) == str( person.title): return True return False
class HomePage(Page): carouselimage1 = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) carousellogo = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) carouseltext1 = RichTextField(blank=True, null=True) carouselimage2 = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) carouseltext2 = RichTextField(blank=True, null=True) cardimage1 = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) cardtext1 = RichTextField() cardimage2 = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) cardtext2 = RichTextField() cardimage3 = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) cardtext3 = RichTextField() invitation = RichTextField() # Editor panels configuration content_panels = Page.content_panels + [ ImageChooserPanel('carousellogo'), FieldPanel('carouseltext1', classname="full"), ImageChooserPanel('carouselimage1'), FieldPanel('carouseltext2', classname="full"), ImageChooserPanel('carouselimage2'), FieldPanel('cardtext1', classname="full"), ImageChooserPanel('cardimage1'), FieldPanel('cardtext2', classname="full"), ImageChooserPanel('cardimage2'), FieldPanel('cardtext3', classname="full"), ImageChooserPanel('cardimage3'), FieldPanel('invitation', classname="full"), ] def get_context(self, request): context = super().get_context(request) next_event = EventCalPage.objects.filter(start_dt__gte=timezone.now(), categories__slug='public').order_by('start_dt').first() context['nextevent'] = next_event return context
class Articles(BasePage): RESOURCES_PER_PAGE = 6 # IMPORTANT: ARTICLES ARE NOW LABELLED "POSTS" IN THE FRONT END parent_page_types = ["home.HomePage"] subpage_types = ["Article"] template = "articles.html" class Meta: verbose_name = "posts" verbose_name_plural = "posts" # Content fields description = RichTextField( blank=True, default="", features=RICH_TEXT_FEATURES_SIMPLE, help_text="Optional short text description, max. 400 characters", max_length=400, ) # Meta fields keywords = ClusterTaggableManager(through=ArticlesTag, blank=True) # Content panels content_panels = BasePage.content_panels + [FieldPanel("description")] # Meta panels meta_panels = [ MultiFieldPanel( [ FieldPanel("seo_title"), FieldPanel("search_description"), ImageChooserPanel("social_image"), FieldPanel("keywords"), ], heading="SEO", help_text=("Optional fields to override the default title " "and description for SEO purposes"), ) ] # Settings panels settings_panels = BasePage.settings_panels + [ FieldPanel("slug"), FieldPanel("show_in_menus"), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading="Content"), ObjectList(meta_panels, heading="Meta"), ObjectList(settings_panels, heading="Settings", classname="settings"), ]) @classmethod def can_create_at(cls, parent): # Allow only one instance of this page type return super().can_create_at(parent) and not cls.objects.exists() def get_context(self, request): context = super().get_context(request) context["filters"] = self.get_filters() context["resources"] = self.get_resources(request) return context def get_resources(self, request): # This Page class will show both Articles/Posts and Videos in its listing # We can't use __in in this deeply related query, so we have to make # a custom Q object instead and pass is in as a filter, then deal with # it later topics = request.GET.getlist(TOPIC_QUERYSTRING_KEY) topics_q = Q(topics__topic__slug__in=topics) if topics else Q() resources = get_combined_articles_and_videos(self, q_object=topics_q) resources = paginate_resources( resources, page_ref=request.GET.get(PAGINATION_QUERYSTRING_KEY), per_page=self.RESOURCES_PER_PAGE, ) return resources def get_filters(self): from ..topics.models import Topic return {"topics": Topic.published_objects.order_by("title")}
class Course(index.Indexed, ClusterableModel): """A Django model to create courses.""" type = models.CharField(_("Type"), max_length=15, choices=COURSE) name = models.CharField(_("Name"), max_length=254) introduction = models.CharField(_("Introduction"), max_length=254) start_date = models.DateField(_("Start date"), blank=True, null=True) end_date = models.DateField(_("End date"), blank=True, null=True) start_time = models.TimeField(_("Start time"), blank=True, null=True) end_time = models.TimeField(_("End time"), blank=True, null=True) price = models.DecimalField(_("Price"), max_digits=10, decimal_places=2, blank=True, null=True) vacancies = models.IntegerField(_("Vacancies"), blank=True, null=True) registered = models.IntegerField(_("Registered"), blank=True, default=0) pre_booking = models.IntegerField(_("Pre-booking"), blank=True, default=0) description = RichTextField(_("Description"), features=RICHTEXT_FEATURES, blank=True) image = models.ForeignKey('wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+') tags = ClusterTaggableManager(through=CourseTag, blank=True) panels = [ FieldPanel('type'), FieldPanel('name'), FieldPanel('introduction'), MultiFieldPanel([ FieldRowPanel([ FieldPanel('start_date', classname="col6"), FieldPanel('end_date', classname="col6"), ]) ], heading=_("Dates")), MultiFieldPanel([ FieldRowPanel([ FieldPanel('start_time', classname="col6"), FieldPanel('end_time', classname="col6"), ]) ], heading=_("Schedule")), FieldPanel('price'), FieldPanel('vacancies'), FieldPanel('registered'), FieldPanel('description'), ImageChooserPanel('image'), FieldPanel('tags'), ] search_fields = [ index.SearchField('name'), ] def __str__(self): return '{}'.format(self.name) @property def get_tags(self): """ Return all the tags that are related to the course into a list we can access on the template. We're additionally adding a URL to access Course objects with that tag """ tags = self.tags.all() for tag in tags: tag.url = '/' + '/'.join( s.strip('/') for s in [self.get_parent().url, 'tags', tag.slug]) return tags def show_course(self): return self.start_date >= datetime.datetime.now().date( ) if self.start_date else False class Meta: verbose_name = _("Course") verbose_name_plural = _("Courses")
class BankPage(Page): bank_name = models.CharField(max_length=100, default="string") deposit_name = models.CharField(max_length=100, default="string") income = models.BooleanField(default=False) dolar = models.FloatField(default=0.0) euro = models.FloatField(default=0.0) hryvna = models.FloatField(default=0.0) capitalization = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0), MaxValueValidator(3)]) bank_image = models.ImageField(default=None) body = RichTextField(blank=True) full_body = RichTextField(blank=True) page_description = models.CharField(max_length=200, default="string") deposit_path = models.CharField(max_length=100, default="string") external_link = models.CharField(max_length=200, default="link") search_fields = Page.search_fields + [ index.SearchField('bank_name'), index.SearchField('deposit_name'), index.SearchField('income'), index.SearchField('dolar'), index.SearchField('euro'), index.SearchField('hryvna'), index.SearchField('capitalization'), index.SearchField('bank_image'), index.SearchField('body'), index.SearchField('full_body'), index.SearchField('page_description'), index.SearchField('deposit_path'), index.SearchField('external_link'), ] content_panels = Page.content_panels + [ FieldPanel('bank_name'), FieldPanel('deposit_name'), FieldPanel('income'), FieldPanel('dolar'), FieldPanel('euro'), FieldPanel('hryvna'), FieldPanel('capitalization'), FieldPanel('bank_image'), FieldPanel('body', classname="full"), FieldPanel('full_body', classname="full"), FieldPanel('page_description'), FieldPanel('deposit_path'), FieldPanel('external_link'), ] api_fields = [ APIField('bank_name'), APIField('deposit_name'), APIField('income'), APIField('dolar'), APIField('euro'), APIField('hryvna'), APIField('capitalization'), APIField('bank_image'), APIField('body'), APIField('full_body'), APIField('page_description'), APIField('deposit_path'), APIField('external_link'), ]