class FullWidthText(blocks.StreamBlock): content = blocks.RichTextBlock(icon='edit') content_with_anchor = molecules.ContentWithAnchor() heading = v1_blocks.HeadingBlock(required=False) image = molecules.ContentImage() table_block = AtomicTableBlock(table_options={'renderer': 'html'}) quote = molecules.Quote() cta = molecules.CallToAction() related_links = molecules.RelatedLinks() reusable_text = v1_blocks.ReusableTextChooserBlock('v1.ReusableText') email_signup = EmailSignUp() class Meta: icon = 'edit' template = '_includes/organisms/full-width-text.html'
class AudioPlayer(blocks.StructBlock): heading = v1_blocks.HeadingBlock(required=False) body = blocks.RichTextBlock(required=False) audio_file = AbstractMediaChooserBlock(help_text=mark_safe( 'Spoken word audio files should be in MP3 format with a 44.1 kHz ' 'sample rate, 96 kbps (CBR) bitrate, in mono. See ' '<a href="https://help.libsynsupport.com/hc/en-us/articles/' '360040796152-Recommended-Audio-File-Formats-Encoding">Libsyn’s ' 'guidance</a> for details. Note that the thumbnail and tag fields ' 'will not be used for audio files.')) additional_details = blocks.RichTextBlock( required=False, help_text=( 'If you have anything you want to appear below the audio player, ' 'such as a download link, put it in this field.')) class Meta: icon = 'media' template = '_includes/organisms/audio-player.html' class Media: js = ['audio-player.js']
class InfoUnitGroup(blocks.StructBlock): format = blocks.ChoiceBlock( choices=[ ('50-50', '50/50'), ('33-33-33', '33/33/33'), ('25-75', '25/75'), ], default='50-50', label='Format', help_text='Choose the number and width of info unit columns.', ) heading = v1_blocks.HeadingBlock(required=False) intro = blocks.RichTextBlock( required=False, help_text='If this field is not empty, ' 'the Heading field must also be set.' ) link_image_and_heading = blocks.BooleanBlock( default=True, required=False, help_text=('Check this to link all images and headings to the URL of ' 'the first link in their unit\'s list, if there is a link.') ) has_top_rule_line = blocks.BooleanBlock( default=False, required=False, help_text=('Check this to add a horizontal rule line to top of ' 'info unit group.') ) lines_between_items = blocks.BooleanBlock( default=False, required=False, label='Show rule lines between items', help_text=('Check this to show horizontal rule lines between info ' 'units.') ) info_units = blocks.ListBlock(molecules.InfoUnit()) sharing = blocks.StructBlock([ ('shareable', blocks.BooleanBlock(label='Include sharing links?', help_text='If checked, share links ' 'will be included below ' 'the items.', required=False)), ('share_blurb', blocks.CharBlock(help_text='Sets the tweet text, ' 'email subject line, and ' 'LinkedIn post text.', required=False)), ]) def clean(self, value): cleaned = super(InfoUnitGroup, self).clean(value) # Intro paragraph may only be specified with a heading. if cleaned.get('intro') and not cleaned.get('heading'): raise ValidationError( 'Validation error in InfoUnitGroup: intro with no heading', params={'heading': ErrorList([ 'Required if paragraph is not empty. (If it looks empty, ' 'click into it and hit the delete key a bunch of times.)' ])} ) # If 25/75, info units must have images. if cleaned.get('format') == '25-75': for unit in cleaned.get('info_units'): if not unit['image']['upload']: raise ValidationError( ('Validation error in InfoUnitGroup: ' '25-75 with no image'), params={'format': ErrorList([ 'Info units must include images when using the ' '25/75 format. Search for an "FPO" image if you ' 'need a temporary placeholder.' ])} ) return cleaned class Meta: icon = 'list-ul' template = '_includes/organisms/info-unit-group-2.html'
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