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'
Beispiel #2
0
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