Пример #1
0
class ExternalVideo(ExternalContent):
    resource_type = "video"
    is_external = True

    # Meta fields
    date = DateField(
        "Video date",
        default=datetime.date.today,
        help_text="The date the video was published",
    )
    speakers = StreamField(
        StreamBlock(
            [("speaker", PageChooserBlock(target_model="people.Person"))],
            required=False,
        ),
        blank=True,
        null=True,
        help_text=
        "Optional list of people associated with or starring in the video",
    )
    duration = CharField(
        max_length=30,
        blank=True,
        null=True,
        help_text=("Optional video duration in MM:SS format e.g. “12:34”. "
                   "Shown when the video is displayed as a card"),
    )

    meta_panels = [
        FieldPanel("date"),
        StreamFieldPanel("speakers"),
        InlinePanel("topics", heading="Topics"),
        FieldPanel("duration"),
    ]

    settings_panels = BasePage.settings_panels + [FieldPanel("slug")]

    edit_handler = TabbedInterface([
        ObjectList(ExternalContent.card_panels, heading="Card"),
        ObjectList(meta_panels, heading="Meta"),
        ObjectList(settings_panels, heading="Settings", classname="settings"),
    ])

    # Search config
    search_fields = BasePage.search_fields + [  # Inherit search_fields from Page
        # "title" is already specced in BasePage
        index.SearchField("description"),
        # Add FilterFields for things we may be filtering on (eg topics)
        index.FilterField("slug"),
    ]

    @property
    def video(self):
        return self

    def has_speaker(self, person):
        for speaker in self.speakers:  # pylint: disable=not-an-iterable
            if str(speaker.value) == str(person.title):
                return True
        return False
Пример #2
0
class ExternalArticle(ExternalContent):
    resource_type = 'article'

    date = DateField('Article date',
                     default=datetime.date.today,
                     help_text='The date the article 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 article’s authors. Use ‘External author’ to add guest authors without creating a '
         'profile on the system'),
    )
    read_time = CharField(
        max_length=30,
        blank=True,
        help_text=
        ('Optional, approximate read-time for this article, e.g. “2 mins”. This '
         'is shown as a small hint when the article is displayed as a card.'))

    meta_panels = [
        FieldPanel('date'),
        StreamFieldPanel('authors'),
        MultiFieldPanel(
            [
                InlinePanel('topics'),
            ],
            heading='Topics',
            help_text='The topic pages this article will appear on'),
        FieldPanel('read_time'),
    ]

    edit_handler = TabbedInterface([
        ObjectList(ExternalContent.card_panels, heading='Card'),
        ObjectList(meta_panels, heading='Meta'),
        ObjectList(Page.settings_panels,
                   heading='Settings',
                   classname='settings'),
    ])

    @property
    def article(self):
        return self

    @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
Пример #3
0
class StudentPageStudentStories(models.Model):
    source_page = ParentalKey("SchoolPage", related_name="student_stories")
    title = models.CharField(max_length=125)
    slides = StreamField(
        StreamBlock([("Page", RelatedPageListBlockPage())], max_num=1))

    panels = [FieldPanel("title"), StreamFieldPanel("slides")]

    def __str__(self):
        return self.title
Пример #4
0
class HomePagePartnershipBlock(models.Model):
    # Partnership module
    source_page = ParentalKey("HomePage", related_name="partnerships_block")
    title = models.CharField(max_length=125)
    summary = models.CharField(max_length=250)
    slides = StreamField(StreamBlock([("Page", RelatedPageListBlockPage())], max_num=1))

    panels = [FieldPanel("title"), FieldPanel("summary"), StreamFieldPanel("slides")]

    def __str__(self):
        return self.title
Пример #5
0
class ExternalVideo(ExternalContent):
    resource_type = 'video'
    is_external = True

    # Meta fields
    date = DateField('Video date',
                     default=datetime.date.today,
                     help_text='The date the video was published')
    speakers = StreamField(
        StreamBlock([
            ('speaker', PageChooserBlock(target_model='people.Person')),
        ],
                    required=False),
        blank=True,
        null=True,
        help_text=
        'Optional list of people associated with or starring in the video',
    )
    duration = CharField(
        max_length=30,
        blank=True,
        null=True,
        help_text=
        ('Optional video duration in MM:SS format e.g. “12:34”. Shown when the video is displayed as a card'
         ))

    meta_panels = [
        FieldPanel('date'),
        StreamFieldPanel('speakers'),
        InlinePanel('topics', heading='Topics'),
        FieldPanel('duration'),
    ]

    edit_handler = TabbedInterface([
        ObjectList(ExternalContent.card_panels, heading='Card'),
        ObjectList(meta_panels, heading='Meta'),
        ObjectList(Page.settings_panels,
                   heading='Settings',
                   classname='settings'),
    ])

    @property
    def video(self):
        return self

    def has_speaker(self, person):
        for speaker in self.speakers:  # pylint: disable=not-an-iterable
            if str(speaker.value) == str(person.title):
                return True
        return False
Пример #6
0
class SchoolPageTeaser(models.Model):
    source_page = ParentalKey("SchoolPage", related_name="page_teasers")
    title = models.CharField(max_length=125)
    summary = models.CharField(max_length=250, blank=True)
    pages = StreamField(
        StreamBlock([("Page", RelatedPageListBlockPage(max_num=6))],
                    max_num=1))
    panels = [
        FieldPanel("title"),
        FieldPanel("summary"),
        StreamFieldPanel("pages")
    ]

    def __str__(self):
        return self.title
Пример #7
0
class ExternalArticle(ExternalContent):
    resource_type = 'article'

    date = DateField('Article date', default=datetime.date.today)
    authors = StreamField(StreamBlock([
        ('author', PageChooserBlock(target_model='people.Person')),
        ('external_author', ExternalAuthorBlock()),
    ]),
                          blank=True,
                          null=True)
    read_time = CharField(
        max_length=30,
        blank=True,
        help_text=
        ('Optional, approximate read-time for this article, e.g. “2 mins”. This '
         'is shown as a small hint when the article is displayed as a card.'))

    meta_panels = [
        FieldPanel('date'),
        StreamFieldPanel('authors'),
        InlinePanel('topics', heading='Topics'),
        FieldPanel('read_time'),
    ]

    edit_handler = TabbedInterface([
        ObjectList(ExternalContent.card_panels, heading='Card'),
        ObjectList(meta_panels, heading='Meta'),
        ObjectList(Page.settings_panels,
                   heading='Settings',
                   classname='settings'),
    ])

    @property
    def article(self):
        return self

    @property
    def month_group(self):
        return self.date.replace(day=1)

    def has_author(self, person):
        for author in self.authors:
            if (author.block_type == 'author'
                    and str(author.value) == str(person.title)):
                return True
        return False
Пример #8
0
class SchoolPageStatsBlock(models.Model):
    source_page = ParentalKey("SchoolPage", related_name="stats_block")
    title = models.CharField(max_length=125)
    # statistics = StreamField([("statistic", StatisticBlock(max_num=1))])
    statistics = StreamField(
        StreamBlock([("statistic", StatisticBlock())], max_num=5))
    background_image = models.ForeignKey(
        get_image_model_string(),
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )
    panels = [
        FieldPanel("title"),
        ImageChooserPanel("background_image"),
        StreamFieldPanel("statistics"),
    ]

    def __str__(self):
        return self.title
Пример #9
0
class FeatureItemsBlock(StructBlock):
    """ Features block """
    feature_layout = ChoiceBlock(
        required=False,
        choices=cr_settings['FEATURE_LAYOUT_CHOICES'],
        default=cr_settings['FEATURE_LAYOUT_CHOICES_DEFAULT'],
        label='Layout',
    )
    feautre_column_count = ChoiceBlock(
        required=False,
        choices=cr_settings['FEATURE_COLUMN_COUNT_CHOICES'],
        default=cr_settings['FEATURE_COLUMN_COUNT_CHOICES_DEFAULT'],
        label='Columns',
    )
    feature_items = StreamBlock(
        [('feature_block', FeatureItemBlock())],
        required=False,
        label="Feature items",
    )

    class Meta:
        template = 'block_sections/feature_items.html'
        label = 'Features'
Пример #10
0
class SchoolPageStudentResearch(LinkFields):
    source_page = ParentalKey("schools.SchoolPage",
                              related_name="student_research")
    title = models.CharField(max_length=125)
    slides = StreamField(
        StreamBlock([("Page", RelatedPageListBlockPage())], max_num=1))

    panels = [
        FieldPanel("title"),
        StreamFieldPanel("slides"),
        *LinkFields.panels,
    ]

    def __str__(self):
        return self.title

    def clean(self):
        if self.link_page and self.link_url:
            raise ValidationError({
                "link_url":
                ValidationError(
                    "You must specify link page or link url. You can't use both."
                ),
                "link_page":
                ValidationError(
                    "You must specify link page or link url. You can't use both."
                ),
            })

        if self.link_url and not self.link_text:
            raise ValidationError({
                "link_text":
                ValidationError(
                    "You must specify link text, if you use the link url field."
                )
            })
Пример #11
0
class ExternalArticle(ExternalContent):
    class Meta:
        verbose_name = "External Post"
        verbose_name_plural = "External Posts"

    resource_type = "article"  #  if you change this, amend the related CSS, too

    date = DateField(
        "External Post date",
        default=datetime.date.today,
        help_text="The date the external 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 external post's authors. "
                   "Use ‘External author’ to add "
                   "guest authors without creating a profile on the system"),
    )
    read_time = CharField(
        max_length=30,
        blank=True,
        help_text=("Optional, approximate read-time for this external post, "
                   "e.g. “2 mins”. This is shown as a small hint when the "
                   "external post is displayed as a card."),
    )

    meta_panels = [
        FieldPanel("date"),
        StreamFieldPanel("authors"),
        MultiFieldPanel(
            [InlinePanel("topics")],
            heading="Topics",
            help_text="The topic pages this external post will appear on",
        ),
        FieldPanel("read_time"),
    ]

    settings_panels = BasePage.settings_panels + [FieldPanel("slug")]

    edit_handler = TabbedInterface([
        ObjectList(ExternalContent.card_panels, heading="Card"),
        ObjectList(meta_panels, heading="Meta"),
        ObjectList(settings_panels, heading="Settings", classname="settings"),
    ])

    # Search config
    search_fields = BasePage.search_fields + [  # Inherit search_fields from Page
        # "title" is already specced in BasePage
        index.SearchField("description"),
        # Add FilterFields for things we may be filtering on (eg topics)
        index.FilterField("slug"),
    ]

    @property
    def article(self):
        return self

    @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
Пример #12
0
class Video(Page):
    resource_type = 'video'
    parent_page_types = ['Videos']
    subpage_types = []
    template = 'video.html'

    # Content fields
    description = TextField(
        blank=True,
        default='',
        help_text='Optional short text description, max. 400 characters',
        max_length=400,
    )
    body = RichTextField(blank=True, default='', features=RICH_TEXT_FEATURES, help_text=(
        'Optional body content. Supports rich text, images, embed via URL, embed via HTML, and inline code snippets'
    ))
    related_links_mdn = StreamField(
        StreamBlock([
            ('link', ExternalLinkBlock())
        ], required=False),
        null=True,
        blank=True,
        help_text='Optional links to MDN Web Docs for further reading',
        verbose_name='Related MDN links',
    )
    image = ForeignKey(
        'mozimages.MozImage',
        null=True,
        blank=True,
        on_delete=SET_NULL,
        related_name='+',
    )
    types = CharField(max_length=14, choices=VIDEO_TYPE, default='conference')
    duration = CharField(max_length=30, blank=True, null=True, help_text=(
        'Optional video duration in MM:SS format e.g. “12:34”. Shown when the video is displayed as a card'
    ))
    transcript = RichTextField(
        blank=True,
        default='',
        features=RICH_TEXT_FEATURES,
        help_text='Optional text transcript of the video, supports rich text',
    )
    video_url = StreamField(
        StreamBlock([
            ('embed', EmbedBlock()),
        ], min_num=1, max_num=1, required=True),
        help_text='Embed URL for the video e.g. https://www.youtube.com/watch?v=kmk43_2dtn0',
    )
    speakers = StreamField(
        StreamBlock([
            ('speaker', PageChooserBlock(target_model='people.Person')),
        ], required=False),
        blank=True,
        null=True,
        help_text='Optional list of people associated with or starring in the video',
    )

    # 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
    date = DateField('Upload date', default=datetime.date.today)
    keywords = ClusterTaggableManager(through=VideoTag, blank=True)

    # Content panels
    content_panels = Page.content_panels + [
        FieldPanel('description'),
        ImageChooserPanel('image'),
        StreamFieldPanel('video_url'),
        FieldPanel('body'),
        StreamFieldPanel('related_links_mdn'),
        FieldPanel('transcript'),
    ]

    # Card panels
    card_panels = [
        FieldPanel('card_title'),
        FieldPanel('card_description'),
        ImageChooserPanel('card_image'),
    ]

    # Meta panels
    meta_panels = [
        FieldPanel('date'),
        StreamFieldPanel('speakers'),
        MultiFieldPanel([
            InlinePanel('topics'),
        ], heading='Topics', help_text=(
            'These are the topic pages the video 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.'
        )),
        FieldPanel('duration'),
        MultiFieldPanel([
            FieldPanel('types'),
        ], heading='Type', help_text='Choose a video type to help people search for the video'),

        MultiFieldPanel([
            FieldPanel('seo_title'),
            FieldPanel('search_description'),
            FieldPanel('keywords'),
        ], heading='SEO', help_text='Optional fields to override the default title and description for SEO purposes'),
    ]

    settings_panels = [
        FieldPanel('slug'),
    ]

    # Tabs
    edit_handler = TabbedInterface([
        ObjectList(content_panels, heading='Content'),
        ObjectList(card_panels, heading='Card'),
        ObjectList(meta_panels, heading='Meta'),
        ObjectList(settings_panels, heading='Settings', classname='settings'),
    ])

    @property
    def primary_topic(self):
        """Return the first (primary) topic specified for the video."""
        video_topic = self.topics.first()
        return video_topic.topic if video_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 = self.topics.values_list('topic')
        return get_combined_articles_and_videos(self, topics__topic__pk__in=topic_pks)

    def has_speaker(self, person):
        for speaker in self.speakers:  # pylint: disable=not-an-iterable
            if str(speaker.value) == str(person.title):
                return True
        return False
Пример #13
0
class HomePage(BasePage):
    subpage_types = [
        "articles.Articles",
        "content.ContentPage",
        "events.Events",
        "people.People",
        "topics.Topics",
        "videos.Videos",
    ]
    template = "home.html"

    # Content fields
    show_header = BooleanField(default=True)
    subtitle = TextField(max_length=250, blank=True, default="")
    button_text = CharField(max_length=30, blank=True, default="")
    button_url = CharField(max_length=2048, blank=True, default="")
    image = ForeignKey(
        "mozimages.MozImage",
        null=True,
        blank=True,
        on_delete=SET_NULL,
        related_name="+",
    )
    external_promos = StreamField(
        StreamBlock(
            [("external_promo", FeaturedExternalBlock())], max_num=2, required=False
        ),
        null=True,
        blank=True,
        help_text=(
            "Optional promo space under the header "
            "for linking to external sites, max. 2"
        ),
    )
    featured = StreamField(
        StreamBlock(
            [
                (
                    "article",
                    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 articles, max. 4",
    )

    featured_people = StreamField(
        StreamBlock(
            [("person", PageChooserBlock(target_model="people.Person"))],
            max_num=3,
            required=False,
        ),
        null=True,
        blank=True,
        help_text="Optional featured people, max. 3",
    )

    about_title = TextField(max_length=250, blank=True, default="")
    about_subtitle = TextField(max_length=250, blank=True, default="")
    about_button_text = CharField(max_length=30, blank=True, default="")
    about_button_url = URLField(max_length=140, blank=True, default="")

    # 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=HomePageTag, blank=True)

    # Editor panel configuration
    content_panels = BasePage.content_panels + [
        MultiFieldPanel(
            [
                FieldPanel("show_header"),
                FieldPanel("subtitle"),
                FieldPanel("button_text"),
                FieldPanel("button_url"),
            ],
            heading="Header section",
            help_text="Optional fields for the header section",
        ),
        MultiFieldPanel(
            [ImageChooserPanel("image")],
            heading="Image",
            help_text=(
                "Optional image shown when sharing this page through social media"
            ),
        ),
        StreamFieldPanel("external_promos"),
        StreamFieldPanel("featured"),
        StreamFieldPanel("featured_people"),
        MultiFieldPanel(
            [
                FieldPanel("about_title"),
                FieldPanel("about_subtitle"),
                FieldPanel("about_button_text"),
                FieldPanel("about_button_url"),
            ],
            heading="About section",
            help_text="Optional section to explain more about Mozilla",
        ),
    ]

    # Card panels
    card_panels = [
        MultiFieldPanel(
            [
                FieldPanel("card_title"),
                FieldPanel("card_description"),
                ImageChooserPanel("card_image"),
            ],
            heading="Card overrides",
            help_text=(
                (
                    "Optional fields to override the default title, "
                    "description and image when this page is shown as a card"
                )
            ),
        )
    ]

    # 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 = [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"),
        ]
    )

    @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()

    @property
    def primary_topics(self):
        """The site’s top-level topics, i.e. topics without a parent topic."""
        from ..topics.models import Topic

        return Topic.published_objects.filter(parent_topics__isnull=True).order_by(
            "title"
        )
Пример #14
0
class Article(Page):
    resource_type = 'article'
    parent_page_types = ['Articles']
    subpage_types = []
    template = 'article.html'

    # Content fields
    description = TextField(max_length=250, blank=True, default='')
    image = ForeignKey('mozimages.MozImage',
                       null=True,
                       blank=True,
                       on_delete=SET_NULL,
                       related_name='+')
    body = CustomStreamField()
    related_links_mdn = StreamField(
        StreamBlock([('link', ExternalLinkBlock())], required=False),
        null=True,
        blank=True,
        verbose_name='Related MDN links',
    )

    # Card fields
    card_title = CharField('Title', max_length=140, blank=True, default='')
    card_description = TextField('Description',
                                 max_length=140,
                                 blank=True,
                                 default='')
    card_image = ForeignKey(
        'mozimages.MozImage',
        null=True,
        blank=True,
        on_delete=SET_NULL,
        related_name='+',
        verbose_name='Image',
    )

    # Meta fields
    date = DateField('Article date', default=datetime.date.today)
    authors = StreamField(StreamBlock([
        ('author', PageChooserBlock(target_model='people.Person')),
        ('external_author', ExternalAuthorBlock()),
    ]),
                          blank=True,
                          null=True)
    keywords = ClusterTaggableManager(through=ArticleTag, blank=True)

    # Content panels
    content_panels = Page.content_panels + [
        FieldPanel('description'),
        ImageChooserPanel('image'),
        StreamFieldPanel('body'),
        StreamFieldPanel('related_links_mdn'),
    ]

    # Card panels
    card_panels = [
        FieldPanel('card_title'),
        FieldPanel('card_description'),
        ImageChooserPanel('card_image'),
    ]

    # Meta panels
    meta_panels = [
        FieldPanel('date'),
        StreamFieldPanel('authors'),
        MultiFieldPanel(
            [
                InlinePanel('topics'),
            ],
            heading='Topics',
            help_text=
            ('These are the topic pages the article will appear on. The first '
             'topic in the list will be treated as the primary topic.')),
        MultiFieldPanel([
            FieldPanel('seo_title'),
            FieldPanel('search_description'),
            FieldPanel('keywords'),
        ],
                        heading='SEO'),
    ]

    # Settings panels
    settings_panels = [
        FieldPanel('slug'),
    ]

    # Tabs
    edit_handler = TabbedInterface([
        ObjectList(content_panels, heading='Content'),
        ObjectList(card_panels, heading='Card'),
        ObjectList(meta_panels, heading='Meta'),
        ObjectList(settings_panels, heading='Settings', classname='settings'),
    ])

    # Rss feed
    def get_absolute_url(self):
        return self.full_url

    @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
Пример #15
0
class Events(Page):
    parent_page_types = ['home.HomePage']
    subpage_types = ['events.Event']
    template = 'events.html'

    # Content fields
    featured = StreamField(
        StreamBlock([
            ('event',
             PageChooserBlock(target_model=(
                 'events.Event',
                 'externalcontent.ExternalEvent',
             ))),
            ('external_page', FeaturedExternalBlock()),
        ],
                    max_num=1,
                    required=False),
        null=True,
        blank=True,
        help_text='Optional space to show a featured event',
    )

    # Meta fields
    keywords = ClusterTaggableManager(through=EventsTag, blank=True)

    # Content panels
    content_panels = Page.content_panels + [StreamFieldPanel('featured')]

    # Meta panels
    meta_panels = [
        MultiFieldPanel(
            [
                FieldPanel('seo_title'),
                FieldPanel('search_description'),
                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'),
    ]

    edit_handler = TabbedInterface([
        ObjectList(content_panels, heading='Content'),
        ObjectList(meta_panels, heading='Meta'),
        ObjectList(settings_panels, heading='Settings', classname='settings'),
    ])

    class Meta:
        verbose_name_plural = 'Events'

    @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()
        return context

    @property
    def events(self):
        """Return events in chronological order"""
        return get_combined_events(self)

    def get_filters(self):
        from ..topics.models import Topic
        return {
            'months': True,
            'topics': Topic.objects.live().public().order_by('title'),
        }
Пример #16
0
class HomePage(Page):
    subpage_types = [
        'articles.Articles',
        'content.ContentPage',
        'events.Events',
        'people.People',
        'topics.Topics',
        'videos.Videos',
    ]
    template = 'home.html'

    # Content fields
    subtitle = TextField(max_length=250, blank=True, default='')
    button_text = CharField(max_length=30, blank=True, default='')
    button_url = CharField(max_length=2048, blank=True, default='')
    image = ForeignKey('mozimages.MozImage',
                       null=True,
                       blank=True,
                       on_delete=SET_NULL,
                       related_name='+')
    external_promos = StreamField(
        StreamBlock([
            ('external_promo', FeaturedExternalBlock()),
        ],
                    min_num=0,
                    max_num=2,
                    required=False),
        null=True,
        blank=True,
    )
    featured = StreamField(
        StreamBlock([
            ('article',
             PageChooserBlock(required=False,
                              target_model=(
                                  'articles.Article',
                                  'externalcontent.ExternalArticle',
                              ))),
            ('external_page', FeaturedExternalBlock()),
        ],
                    min_num=0,
                    max_num=4,
                    required=False),
        null=True,
        blank=True,
    )
    about_title = TextField(max_length=250, blank=True, default='')
    about_subtitle = TextField(max_length=250, blank=True, default='')
    about_button_text = CharField(max_length=30, blank=True, default='')
    about_button_url = URLField(max_length=140, blank=True, default='')

    # Card fields
    card_title = CharField('Title', max_length=140, blank=True, default='')
    card_description = TextField('Description',
                                 max_length=140,
                                 blank=True,
                                 default='')
    card_image = ForeignKey(
        'mozimages.MozImage',
        null=True,
        blank=True,
        on_delete=SET_NULL,
        related_name='+',
        verbose_name='Image',
    )

    # Meta fields
    keywords = ClusterTaggableManager(through=HomePageTag, blank=True)

    # Editor panel configuration
    content_panels = Page.content_panels + [
        MultiFieldPanel(
            [
                FieldPanel('subtitle'),
                FieldPanel('button_text'),
                FieldPanel('button_url'),
            ],
            heading="Header section",
        ),
        ImageChooserPanel('image'),
        StreamFieldPanel('external_promos'),
        StreamFieldPanel('featured'),
        MultiFieldPanel(
            [
                FieldPanel('about_title'),
                FieldPanel('about_subtitle'),
                FieldPanel('about_button_text'),
                FieldPanel('about_button_url'),
            ],
            heading="About section",
        )
    ]

    # Card panels
    card_panels = [
        FieldPanel('card_title'),
        FieldPanel('card_description'),
        ImageChooserPanel('card_image'),
    ]

    # Meta panels
    meta_panels = [
        MultiFieldPanel([
            FieldPanel('seo_title'),
            FieldPanel('search_description'),
            FieldPanel('keywords'),
        ],
                        heading='SEO'),
    ]

    # Settings panels
    settings_panels = [
        FieldPanel('slug'),
    ]

    # Tabs
    edit_handler = TabbedInterface([
        ObjectList(content_panels, heading='Content'),
        ObjectList(card_panels, heading='Card'),
        ObjectList(meta_panels, heading='Meta'),
        ObjectList(settings_panels, heading='Settings', classname='settings'),
    ])

    @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()

    @property
    def primary_topics(self):
        """The site’s top-level topics, i.e. topics without a parent topic."""
        from ..topics.models import Topic
        return Topic.objects.filter(
            parent_topics__isnull=True).live().public().order_by('title')
Пример #17
0
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,
    )
    start_date = DateField(default=datetime.date.today)
    end_date = DateField(blank=True, null=True)
    latitude = FloatField(blank=True, null=True)  # DEPRECATED
    longitude = FloatField(blank=True, null=True)  # DEPRECATED
    register_url = URLField("Register URL", blank=True, null=True)
    official_website = URLField("Official website", blank=True, default="")
    event_content = URLField(
        "Event content",
        blank=True,
        default="",
        help_text=("Link to a page (in this site or elsewhere) "
                   "with content about this event."),
    )

    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="")  # DEPRECATED
    venue_url = URLField("Venue URL", max_length=100, blank=True,
                         default="")  # DEPRECATED
    address_line_1 = CharField(max_length=100, blank=True,
                               default="")  # DEPRECATED
    address_line_2 = CharField(max_length=100, blank=True,
                               default="")  # DEPRECATED
    address_line_3 = CharField(max_length=100, blank=True,
                               default="")  # DEPRECATED
    city = CharField(max_length=100, blank=True, default="")
    state = CharField("State/Province/Region",
                      max_length=100,
                      blank=True,
                      default="")  # DEPRECATED
    zip_code = CharField("Zip/Postal code",
                         max_length=100,
                         blank=True,
                         default="")  # DEPRECATED
    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",
    )  # DEPRECATED
    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",
    )  # DEPRECATED

    # 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
    keywords = ClusterTaggableManager(through=EventTag, blank=True)

    # Content panels
    content_panels = BasePage.content_panels + [
        FieldPanel("description"),
        MultiFieldPanel(
            [
                FieldPanel("start_date"),
                FieldPanel("end_date"),
                FieldPanel("register_url"),
                FieldPanel("official_website"),
                FieldPanel("event_content"),
            ],
            heading="Event details",
            classname="collapsible",
            help_text=(
                "'Event content' should be used to link to a page (anywhere) "
                "which summarises the content of the event"),
        ),
        StreamFieldPanel("body"),
        MultiFieldPanel(
            [FieldPanel("city"), FieldPanel("country")],
            heading="Event location",
            classname="collapsible",
            help_text=("The city and country are also shown on event cards"),
        ),
    ]

    # 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 = [
        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 = BasePage.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 >= get_past_event_cutoff()

    @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 += " – "  # rather than – so we don't have to mark safe
            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

    @property
    def summary_meta(self):
        """Return a simple plaintext string that can be used
        as a standfirst"""

        summary = ""
        if self.event_dates:
            summary += self.event_dates
            if self.city or self.country:
                summary += " | "

        if self.city:
            summary += self.city
            if self.country:
                summary += ", "
        if self.country:
            summary += self.country.code
        return summary
Пример #18
0
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 = BasePage.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 >= get_past_event_cutoff()

    @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 += " &ndash; "
            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
Пример #19
0
class Video(BasePage):
    resource_type = "video"
    parent_page_types = ["Videos"]
    subpage_types = []
    template = "video.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,
    )
    body = RichTextField(
        blank=True,
        default="",
        features=RICH_TEXT_FEATURES,
        help_text=(
            "Optional body content. Supports rich text, images, embed via URL, "
            "embed via HTML, and inline code snippets"),
    )
    related_links = StreamField(
        StreamBlock([("link", ExternalLinkBlock())], required=False),
        null=True,
        blank=True,
        help_text="Optional links further reading",
        verbose_name="Related links",
    )
    image = ForeignKey(
        "mozimages.MozImage",
        null=True,
        blank=True,
        on_delete=SET_NULL,
        related_name="+",
    )
    types = CharField(max_length=14, choices=VIDEO_TYPE, default="conference")
    duration = CharField(
        max_length=30,
        blank=True,
        null=True,
        help_text=
        ("Optional video duration in MM:SS format e.g. “12:34”. Shown when the "
         "video is displayed as a card"),
    )
    transcript = RichTextField(
        blank=True,
        default="",
        features=RICH_TEXT_FEATURES,
        help_text="Optional text transcript of the video, supports rich text",
    )
    video_url = StreamField(
        StreamBlock([("embed", EmbedBlock())],
                    min_num=1,
                    max_num=1,
                    required=True),
        help_text=
        ("Embed URL for the video e.g. https://www.youtube.com/watch?v=kmk43_2dtn0"
         ),
    )
    speakers = StreamField(
        StreamBlock(
            [("speaker", PageChooserBlock(target_model="people.Person"))],
            required=False,
        ),
        blank=True,
        null=True,
        help_text=
        "Optional list of people associated with or starring in the video",
    )

    # 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
    date = DateField("Upload date", default=datetime.date.today)
    keywords = ClusterTaggableManager(through=VideoTag, blank=True)

    # Content panels
    content_panels = BasePage.content_panels + [
        FieldPanel("description"),
        ImageChooserPanel("image"),
        StreamFieldPanel("video_url"),
        FieldPanel("body"),
        StreamFieldPanel("related_links"),
        FieldPanel("transcript"),
    ]

    # Card panels
    card_panels = [
        FieldPanel("card_title"),
        FieldPanel("card_description"),
        ImageChooserPanel("card_image"),
    ]

    # Meta panels
    meta_panels = [
        FieldPanel("date"),
        StreamFieldPanel("speakers"),
        MultiFieldPanel(
            [InlinePanel("topics")],
            heading="Topics",
            help_text=
            ("These are the topic pages the video 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."),
        ),
        FieldPanel("duration"),
        MultiFieldPanel(
            [FieldPanel("types")],
            heading="Type",
            help_text="Choose a video type to help people search for the video",
        ),
        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 = 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 primary_topic(self):
        """Return the first (primary) topic specified for the video."""
        video_topic = self.topics.first()
        return video_topic.topic if video_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)

    def has_speaker(self, person):
        for speaker in self.speakers:  # pylint: disable=not-an-iterable
            if str(speaker.value) == str(person.title):
                return True
        return False
Пример #20
0
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,
    )
    image = ForeignKey(
        "mozimages.MozImage",
        null=True,
        blank=True,
        on_delete=SET_NULL,
        related_name="+",
    )
    body = CustomStreamField(help_text=(
        "The main post content. Supports rich text, images, embed via URL, "
        "embed via HTML, and inline code snippets"))
    related_links_mdn = StreamField(
        StreamBlock([("link", ExternalLinkBlock())], required=False),
        blank=True,
        null=True,
        help_text="Optional links to MDN Web Docs for further reading",
        verbose_name="Related MDN 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",
    )

    # 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"),
        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"
             ),
        ),
        StreamFieldPanel("body"),
        StreamFieldPanel("related_links_mdn"),
    ]

    # Card panels
    card_panels = [
        FieldPanel("card_title"),
        FieldPanel("card_description"),
        ImageChooserPanel("card_image"),
    ]

    # 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 = [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 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
Пример #21
0
class Topic(Page):
    resource_type = 'topic'
    parent_page_types = ['Topics']
    subpage_types = ['Topic']
    template = 'topic.html'

    # Content fields
    description = TextField(
        blank=True,
        default='',
        help_text='Optional short text description, max. 400 characters',
        max_length=400,
    )
    featured = StreamField(
        StreamBlock([
            ('article', 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 articles, max. 4',
    )
    tabbed_panels_title = CharField(max_length=250, blank=True, default='')
    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 articles 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='')
    color = CharField(max_length=14, choices=COLOR_CHOICES, default='blue-40')
    keywords = ClusterTaggableManager(through=TopicTag, blank=True)

    # Content panels
    content_panels = Page.content_panels + [
        FieldPanel('description'),
        StreamFieldPanel('featured'),
        FieldPanel('tabbed_panels_title'),
        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, an article tagged with this topic '
            'will use the color specified here as its accent color.'
        )),
        MultiFieldPanel([
            FieldPanel('seo_title'),
            FieldPanel('search_description'),
            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 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()]
Пример #22
0
class ExternalArticle(ExternalContent):
    resource_type = "article"

    date = DateField(
        "Article date",
        default=datetime.date.today,
        help_text="The date the article 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 article’s authors. Use ‘External author’ to add "
         "guest authors without creating a profile on the system"),
    )
    read_time = CharField(
        max_length=30,
        blank=True,
        help_text=
        ("Optional, approximate read-time for this article, e.g. “2 mins”. This "
         "is shown as a small hint when the article is displayed as a card."),
    )

    meta_panels = [
        FieldPanel("date"),
        StreamFieldPanel("authors"),
        MultiFieldPanel(
            [InlinePanel("topics")],
            heading="Topics",
            help_text="The topic pages this article will appear on",
        ),
        FieldPanel("read_time"),
    ]

    edit_handler = TabbedInterface([
        ObjectList(ExternalContent.card_panels, heading="Card"),
        ObjectList(meta_panels, heading="Meta"),
        ObjectList(BasePage.settings_panels,
                   heading="Settings",
                   classname="settings"),
    ])

    @property
    def article(self):
        return self

    @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
Пример #23
0
class Events(BasePage):

    EVENTS_PER_PAGE = 8

    parent_page_types = ["home.HomePage"]
    subpage_types = ["events.Event"]
    template = "events.html"

    # Content fields
    top_content = CustomStreamField(
        null=True,
        blank=True,
        help_text=(
            "Free-form content that appears above the 'Featured' section. "
            "Supports rich text, images, embed via URL, "
            "embed via HTML, and inline code snippets"),
    )
    featured = StreamField(
        StreamBlock(
            [
                (
                    "event",
                    PageChooserBlock(
                        target_model=("events.Event",
                                      "externalcontent.ExternalEvent")),
                ),
                ("external_page", FeaturedExternalBlock()),
            ],
            max_num=4,
            required=False,
        ),
        null=True,
        blank=True,
        help_text=("Optional space to show featured events."),
    )
    body = CustomStreamField(
        null=True,
        blank=True,
        help_text=
        ("Main page body content. Supports rich text, images, embed via URL, "
         "embed via HTML, and inline code snippets"),
    )
    bottom_content = CustomStreamField(
        null=True,
        blank=True,
        help_text=("Free-form content that appears below the list of Events. "
                   "Supports rich text, images, embed via URL, "
                   "embed via HTML, and inline code snippets"),
    )

    # Meta fields
    keywords = ClusterTaggableManager(through=EventsTag, blank=True)

    # Content panels
    content_panels = BasePage.content_panels + [
        StreamFieldPanel("top_content"),
        StreamFieldPanel("featured"),
        StreamFieldPanel("body"),
        StreamFieldPanel("bottom_content"),
    ]

    # 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"),
    ])

    class Meta:
        verbose_name_plural = "Events"

    @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(request)
        context["events"] = self.get_events(request)
        return context

    def _build_date_q(self, date_params):
        """Suport filtering events by 'all future events' or 'all past events' Booleans.

        Arguments:
            date_params: List(str) -- list of sentinel strings of
                FUTURE_EVENTS_QUERYSTRING_VALUE and/or
                PAST_EVENTS_QUERYSTRING_VALUE.

                Specifying FUTURE_EVENTS_QUERYSTRING_VALUE will return all FUTURE events
                Specifying PAST_EVENTS_QUERYSTRING_VALUE will return all PAST events
                Specifying both will return ALL events, past and future
                Specifying neither will trigger the default behaviour: to return events
                    between now and DEFAULT_EVENTS_LOOKAHEAD_WINDOW_MONTHS months time

        Returns:
            django.models.QuerySet -- configured QuerySet based on arguments.
        """

        # Assemble facts from the year_months querystring data
        past_events_flag = PAST_EVENTS_QUERYSTRING_VALUE in date_params
        future_events_flag = FUTURE_EVENTS_QUERYSTRING_VALUE in date_params

        if past_events_flag and not future_events_flag:
            date_q = Q(start_date__lte=get_past_event_cutoff())
        elif not past_events_flag and future_events_flag:
            date_q = Q(start_date__gte=get_past_event_cutoff())
        elif past_events_flag and future_events_flag:
            date_q = Q()  # Because we don't need to restrict
        else:
            window_start = get_past_event_cutoff()
            window_end = window_start + relativedelta.relativedelta(
                months=DEFAULT_EVENTS_LOOKAHEAD_WINDOW_MONTHS,
                days=1,  # Because get_past_event_cutoff() goes back to yesterday
            )
            date_q = Q(start_date__gte=window_start,
                       start_date__lte=window_end)
        return date_q

    def get_events(self, request):
        """Return filtered future events in chronological order"""

        countries = request.GET.getlist(LOCATION_QUERYSTRING_KEY)
        date_params = request.GET.getlist(DATE_PARAMS_QUERYSTRING_KEY)
        topics = request.GET.getlist(TOPIC_QUERYSTRING_KEY)

        countries_q = Q(country__in=countries) if countries else Q()
        topics_q = Q(topics__topic__slug__in=topics) if topics else Q()

        # date_params need a little more logic to construct
        date_q = self._build_date_q(date_params)

        combined_q = Q()
        if countries_q:
            combined_q.add(countries_q, Q.AND)
        if date_q:
            combined_q.add(date_q, Q.AND)
        if topics_q:
            combined_q.add(topics_q, Q.AND)

        events = get_combined_events(self, reverse=False, q_object=combined_q)

        events = paginate_resources(
            events,
            page_ref=request.GET.get(PAGINATION_QUERYSTRING_KEY),
            per_page=self.EVENTS_PER_PAGE,
        )

        return events

    def get_relevant_countries(self):
        # Relevant here means a country that a published Event is or was in
        raw_countries = (event.country
                         for event in Event.published_objects.distinct(
                             "country").order_by("country") if event.country)

        # Need to do a separate sort by country name because "Online" has a fake country
        # code of QQ - see settings.base.COUNTRIES_OVERRIDE
        sorted_raw_countries = sorted(raw_countries,
                                      key=lambda country: country.name)

        return [{
            "code": country.code,
            "name": country.name
        } for country in sorted_raw_countries]

    def get_event_date_options(self, request):
        return {
            "options_selected":
            ((PAST_EVENTS_QUERYSTRING_VALUE in request.GET.getlist(
                DATE_PARAMS_QUERYSTRING_KEY, []))
             or (FUTURE_EVENTS_QUERYSTRING_VALUE in request.GET.getlist(
                 DATE_PARAMS_QUERYSTRING_KEY, []))),
            "options": [
                {
                    "value": PAST_EVENTS_QUERYSTRING_VALUE,
                    "label": "Past events"
                },
                {
                    "value": FUTURE_EVENTS_QUERYSTRING_VALUE,
                    "label": "Future events"
                },
            ],
        }

    def get_filters(self, request):
        return {
            "countries": self.get_relevant_countries(),
            "event_dates": self.get_event_date_options(request),
            "topics": Topic.published_objects.order_by("title"),
        }
Пример #24
0
class Article(Page):
    resource_type = 'article'
    parent_page_types = ['Articles']
    subpage_types = []
    template = 'article.html'

    # Content fields
    description = TextField(
        blank=True,
        default='',
        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='+',
    )
    body = CustomStreamField(help_text=(
        'The main article content. Supports rich text, images, embed via URL, embed via HTML, and inline code snippets'
    ))
    related_links_mdn = StreamField(
        StreamBlock([('link', ExternalLinkBlock())], required=False),
        blank=True,
        null=True,
        help_text='Optional links to MDN Web Docs for further reading',
        verbose_name='Related MDN 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',
    )

    # Meta fields
    date = DateField('Article date',
                     default=datetime.date.today,
                     help_text='The date the article 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 article’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 = Page.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')),
        StreamFieldPanel('body'),
        StreamFieldPanel('related_links_mdn'),
    ]

    # Card panels
    card_panels = [
        FieldPanel('card_title'),
        FieldPanel('card_description'),
        ImageChooserPanel('card_image'),
    ]

    # Meta panels
    meta_panels = [
        FieldPanel('date'),
        StreamFieldPanel('authors'),
        MultiFieldPanel(
            [
                InlinePanel('topics'),
            ],
            heading='Topics',
            help_text=
            ('The topic pages this article 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'),
                FieldPanel('keywords'),
            ],
            heading='SEO',
            help_text=
            'Optional fields to override the default title and description for SEO purposes'
        ),
    ]

    # Settings panels
    settings_panels = [
        FieldPanel('slug'),
    ]

    # Tabs
    edit_handler = TabbedInterface([
        ObjectList(content_panels, heading='Content'),
        ObjectList(card_panels, heading='Card'),
        ObjectList(meta_panels, heading='Meta'),
        ObjectList(settings_panels, heading='Settings', classname='settings'),
    ])

    # Rss feed
    def get_absolute_url(self):
        return self.full_url

    @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
Пример #25
0
class Events(BasePage):

    EVENTS_PER_PAGE = 8

    parent_page_types = ["home.HomePage"]
    subpage_types = ["events.Event"]
    template = "events.html"

    # Content fields
    featured = StreamField(
        StreamBlock(
            [
                (
                    "event",
                    PageChooserBlock(
                        target_model=("events.Event",
                                      "externalcontent.ExternalEvent")),
                ),
                ("external_page", FeaturedExternalBlock()),
            ],
            max_num=2,
            required=False,
        ),
        null=True,
        blank=True,
        help_text=(
            "Optional space to show featured events. Note that these are "
            "rendered two-up, so please set 0 or 2"),
    )
    body = CustomStreamField(
        null=True,
        blank=True,
        help_text=
        ("Main page body content. Supports rich text, images, embed via URL, "
         "embed via HTML, and inline code snippets"),
    )

    # Meta fields
    keywords = ClusterTaggableManager(through=EventsTag, blank=True)

    # Content panels
    content_panels = BasePage.content_panels + [
        StreamFieldPanel("featured"),
        StreamFieldPanel("body"),
    ]

    # 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"),
    ])

    class Meta:
        verbose_name_plural = "Events"

    @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["events"] = self.get_events(request)
        return context

    def _pop_past_events_marker_from_date_params(self, date_params):
        """For the given list of "YYYY-MM" strings and an optional sentinel that shows
        whether we should include past events, return a list of tuples containing
        the year and and month, as unmutated strings, PLUS a separate Boolean value,
        defaulting to False.

        Example input:  ["2020-03", "2020-12"]
        Example output:  (["2020-03", "2020-12"], False)

        Example input:  ["2020-03", "2020-12", "past"]
        Example output:  (["2020-03", "2020-12"], True)
        """

        past_events_flag = bool(date_params) and (PAST_EVENTS_QUERYSTRING_VALUE
                                                  in date_params)

        if past_events_flag:
            date_params.pop(date_params.index(PAST_EVENTS_QUERYSTRING_VALUE))

        return date_params, past_events_flag

    def _year_months_to_years_and_months_tuples(self, year_months):
        """For the given list of "YYYY-MM" strings, return a list of tuples
        containg the year and and month, still as strings.

        Example input:  ["2020-03", "2020-12"]
        Example output: [("2020", "03"), ("2020", "12")]
        """

        if not year_months:
            return []
        return [tuple(x.split("-")) for x in [y for y in year_months if y]]

    def _build_date_q(self, date_params):
        """Suport filtering events by selected year-month pair(s) and/or an
        'all past events' Boolean.

        Note that this method returns early, to avoid nested clauses

        Arguments:
            date_params: List(str) -- list of strings representing selected
            dates in the filtering panel, where each string is either in YYYY-MM format
            or the sentinel string PAST_EVENTS_QUERYSTRING_VALUE.

        Returns:
            django.models.QuerySet -- configured QuerySet based on arguments.
        """

        # Assemble facts from the year_months querystring data
        year_months, past_events_flag = self._pop_past_events_marker_from_date_params(  # noqa: E501
            date_params)
        years_and_months_tuples = self._year_months_to_years_and_months_tuples(
            year_months)

        if past_events_flag:
            default_events_q = Q(start_date__lte=get_past_event_cutoff())
        else:
            default_events_q = Q()  # Because we don't need to restrict

        if not years_and_months_tuples:
            # Covers case where no year_months, so no need to construct further queries
            return default_events_q

        # Build a Q where it's (Month X AND Year X) OR (Month Y AND Year Y), etc
        overall_date_q = None

        try:
            for year, month in years_and_months_tuples:
                date_q = Q(**{"start_date__year": year})
                date_q.add(Q(**{"start_date__month": month}), Q.AND)

                if overall_date_q is None:
                    overall_date_q = date_q
                else:
                    overall_date_q.add(date_q, Q.OR)
        except ValueError as e:
            logger.warning("%s (years_and_months_tuples is %s)" %
                           (e, years_and_months_tuples))
            # Handles bad input and keeps the show on the road
            overall_date_q = Q()

        if past_events_flag:
            # Regardless of what's been specified in terms of specific dates, if
            # "past events" has been selected, we want to include all events
            # UP TO the past/future threshold date but _without_ de-scoping
            # whatever the other dates may have configured.
            all_past_events_q = Q(start_date__lte=get_past_event_cutoff())

            overall_date_q.add(all_past_events_q, Q.OR)  # NB: OR
        else:
            # We want specific months, but none in the past, so
            # ensure we don't include past events here (ie, same month as
            # selected dates, but before _today_)
            overall_date_q.add(Q(start_date__gte=get_past_event_cutoff()),
                               Q.AND)  # NB: AND
        return overall_date_q

    def get_events(self, request):
        """Return filtered future events in chronological order"""

        countries = request.GET.getlist(COUNTRY_QUERYSTRING_KEY)
        date_params = request.GET.getlist(DATE_PARAMS_QUERYSTRING_KEY)
        topics = request.GET.getlist(TOPIC_QUERYSTRING_KEY)

        countries_q = Q(country__in=countries) if countries else Q()
        topics_q = Q(topics__topic__slug__in=topics) if topics else Q()

        # date_params need splitting to make them work, plus we need to see if
        # past events are also needed
        date_q = self._build_date_q(date_params)

        combined_q = Q()
        if countries_q:
            combined_q.add(countries_q, Q.AND)
        if date_q:
            combined_q.add(date_q, Q.AND)
        if topics_q:
            combined_q.add(topics_q, Q.AND)

        # Combined_q will always have something because it includes
        # the start_date__gte test
        events = get_combined_events(self, reverse=True, q_object=combined_q)

        events = paginate_resources(
            events,
            page_ref=request.GET.get(PAGINATION_QUERYSTRING_KEY),
            per_page=self.EVENTS_PER_PAGE,
        )

        return events

    def get_relevant_countries(self):
        # Relevant here means a country that a published Event is or was in
        raw_countries = (event.country
                         for event in Event.published_objects.distinct(
                             "country").order_by("country") if event.country)

        return [{
            "code": country.code,
            "name": country.name
        } for country in raw_countries]

    def get_relevant_dates(self):
        # Relevant here means a date for a published *future* event
        # TODO: would be good to cache this for short period of time
        raw_events = get_combined_events(
            self, start_date__gte=get_past_event_cutoff())
        return sorted([event.start_date for event in raw_events])

    def dates_to_unique_month_years(self, dates: List[datetime.date]):
        """From the given list of dates, generate another list of dates where the
        year-month combinations are unique and the `day` of each is set to the 1st.

        We do this because the filter-form.html template only uses Y and M when
        rendering the date options, so we must skip/merge dates that feature year-month
        pairs that _already_ appear in the list. If we don't, and if there is more than
        one Event for the same year-month, we end up with multiple Year-Months
        displayed in the filter options.

        NB: also note that the template slots in a special "all past dates" option.
        """
        return sorted(set([datetime.date(x.year, x.month, 1) for x in dates]),
                      reverse=True)

    def get_filters(self):
        return {
            "countries": self.get_relevant_countries(),
            "dates":
            self.dates_to_unique_month_years(self.get_relevant_dates()),
            "topics": Topic.published_objects.order_by("title"),
        }
Пример #26
0
class Video(Page):
    resource_type = 'video'
    parent_page_types = ['Videos']
    subpage_types = []
    template = 'video.html'

    # Content fields
    description = TextField(default='', blank=True, max_length=250)
    body = RichTextField(default='', blank=True)
    related_links_mdn = StreamField(
        StreamBlock([
            ('link', ExternalLinkBlock())
        ], required=False),
        null=True,
        blank=True,
        verbose_name='Related MDN links',
    )
    image = ForeignKey(
        'mozimages.MozImage',
        null=True,
        blank=True,
        on_delete=SET_NULL,
        related_name='+'
    )
    types = CharField(max_length=14, choices=VIDEO_TYPE, default='conference')
    duration = CharField(max_length=30, blank=True, null=True, help_text=(
        'Optional. Video duration in MM:SS format e.g. “12:34”. Shown as a small hint when the video is displayed as a card.'
    ))
    transcript = RichTextField(default='', blank=True)
    video_url = StreamField(
        StreamBlock([
            ('embed', EmbedBlock()),
        ], max_num=1),
        null=True,
        blank=True,
    )
    speakers = StreamField(
        StreamBlock([
            ('speaker', PageChooserBlock(required=False, target_model='people.Person')),
        ], required=False),
        blank=True, null=True,
    )

    # Card fields
    card_title = CharField('Title', max_length=140, blank=True, default='')
    card_description = TextField('Description', max_length=140, blank=True, default='')
    card_image = ForeignKey(
        'mozimages.MozImage',
        null=True,
        blank=True,
        on_delete=SET_NULL,
        related_name='+',
        verbose_name='Image',
    )

    # Meta fields
    date = DateField('Upload date', default=datetime.date.today)
    keywords = ClusterTaggableManager(through=VideoTag, blank=True)

    # Content panels
    content_panels = Page.content_panels + [
        FieldPanel('description'),
        ImageChooserPanel('image'),
        StreamFieldPanel('video_url'),
        FieldPanel('body'),
        StreamFieldPanel('related_links_mdn'),
        FieldPanel('transcript'),
    ]

    # Card panels
    card_panels = [
        FieldPanel('card_title'),
        FieldPanel('card_description'),
        ImageChooserPanel('card_image'),
    ]

    # Meta panels
    meta_panels = [
        FieldPanel('date'),
        StreamFieldPanel('speakers'),
        MultiFieldPanel([
            InlinePanel('topics'),
        ], heading='Topics'),
        FieldPanel('duration'),
        MultiFieldPanel([
            FieldPanel('types'),
        ], heading='Type.'),


        MultiFieldPanel([
            FieldPanel('seo_title'),
            FieldPanel('search_description'),
            FieldPanel('keywords'),
        ], heading='SEO'),
    ]

    settings_panels = [
        FieldPanel('slug'),
    ]

    # Tabs
    edit_handler = TabbedInterface([
        ObjectList(content_panels, heading='Content'),
        ObjectList(card_panels, heading='Card'),
        ObjectList(meta_panels, heading='Meta'),
        ObjectList(settings_panels, heading='Settings', classname='settings'),
    ])

    @property
    def primary_topic(self):
        """Return the first (primary) topic specified for the video."""
        video_topic = self.topics.first()
        return video_topic.topic if video_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 = self.topics.values_list('topic')
        return get_combined_articles_and_videos(self, topics__topic__pk__in=topic_pks)

    def has_speaker(self, person):
        for speaker in self.speakers:
            if str(speaker.value)==str(person.title):
                return True
        return False
Пример #27
0
class Event(Page):
    resource_type = 'event'
    parent_page_types = ['events.Events']
    subpage_types = []
    template = 'event.html'

    # Content fields
    description = TextField(
        blank=True,
        default='',
        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='+',
    )
    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'
         ))
    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
    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)
    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='')
    keywords = ClusterTaggableManager(through=EventTag, blank=True)

    # Content panels
    content_panels = Page.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')),
        StreamFieldPanel('body'),
        StreamFieldPanel('agenda'),
        StreamFieldPanel('speakers'),
    ]

    # Card panels
    card_panels = [
        FieldPanel('card_title'),
        FieldPanel('card_description'),
        ImageChooserPanel('card_image'),
    ]

    # Meta panels
    meta_panels = [
        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>'
            )),
        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'
        ),
        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'),
                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 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:
            event_dates += " &ndash; "
            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 Topic(BasePage):
    resource_type = "topic"
    parent_page_types = ["Topics"]
    subpage_types = ["Topic", "content.ContentPage"]
    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()),
            ],
            min_num=2,
            max_num=5,
            required=True,
        ),
        null=True,
        blank=True,
        help_text="Optional space for featured items, max. 5",
    )
    #  "What We've Been Working On" panel
    recent_work = StreamField(
        StreamBlock(
            [
                (
                    "post",
                    PageChooserBlock(target_model=(
                        "articles.Article",
                        "externalcontent.ExternalArticle",
                    )),
                ),
                ("external_page", FeaturedExternalBlock()),
                (
                    "video",
                    PageChooserBlock(
                        target_model=("videos.Video",
                                      "externalcontent.ExternalVideo")),
                ),
            ],
            max_num=4,
            required=False,
        ),
        null=True,
        blank=True,
        help_text=
        ("Optional space for featured posts, videos or links, min. 1, max. 4."
         ),
    )

    # 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",
    )

    # Meta
    nav_description = TextField("Navigation description",
                                max_length=400,
                                blank=True,
                                default="")
    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("recent_work"),
        MultiFieldPanel(
            [InlinePanel("people")],
            heading="Content by",
            help_text=
            "Optional list of people who create content on this topic",
        ),
    ]

    # Card panels
    card_panels = [
        FieldPanel(
            "card_title",
            help_text=("Title displayed when this page is "
                       "represented by a card in a list of items. "
                       "If blank, the page's title is used."),
        ),
        FieldPanel(
            "card_description",
            help_text=("Summary text displayed when this page is "
                       "represented by a card in a list of items. "
                       "If blank, the page's description is used."),
        ),
        MultiFieldPanel(
            [ImageChooserPanel("card_image")],
            heading="16:9 Image",
            help_text=
            ("Image used for representing this page as a Card. "
             "Must be 16:9 aspect ratio. "
             "If not specified a fallback will be used. "
             "This image is also used for social media posts, unless overriden"
             ),
        ),
    ]

    # Meta panels
    meta_panels = [
        FieldPanel(
            "nav_description",
            help_text=
            "Text to display in the navigation with the title for this page.",
        ),
        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 = BasePage.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=get_past_event_cutoff())

    @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()]
Пример #29
0
class Person(BasePage):
    resource_type = "person"
    parent_page_types = ["People"]
    subpage_types = []
    template = "person.html"

    # Content fields
    nickname = CharField(max_length=250, null=True, blank=True)
    job_title = CharField(max_length=250)
    role = CharField(max_length=250, choices=ROLE_CHOICES, default="staff")
    description = RichTextField(
        "About",
        blank=True,
        default="",
        features=RICH_TEXT_FEATURES_SIMPLE,
        help_text="Optional ‘About me’ section content, supports rich text",
    )
    image = ForeignKey(
        "mozimages.MozImage",
        null=True,
        blank=True,
        on_delete=SET_NULL,
        related_name="+",
    )

    # Card fields
    card_title = CharField("Title", max_length=140, blank=True, default="")
    card_description = TextField("Description", max_length=400, blank=True, default="")
    card_image = ForeignKey(
        "mozimages.MozImage",
        null=True,
        blank=True,
        on_delete=SET_NULL,
        related_name="+",
        verbose_name="Image",
    )

    # Meta
    city = CharField(max_length=250, blank=True, default="")
    country = CountryField()
    twitter = CharField(max_length=250, blank=True, default="")
    facebook = CharField(max_length=250, blank=True, default="")
    linkedin = CharField(max_length=250, blank=True, default="")
    github = CharField(max_length=250, blank=True, default="")
    email = CharField(max_length=250, blank=True, default="")
    websites = StreamField(
        StreamBlock([("website", PersonalWebsiteBlock())], max_num=3, required=False),
        null=True,
        blank=True,
        help_text="Optional links to any other personal websites",
    )
    keywords = ClusterTaggableManager(through=PersonTag, blank=True)

    # Content panels
    content_panels = [
        MultiFieldPanel(
            [
                CustomLabelFieldPanel("title", label="Full name"),
                FieldPanel("nickname"),
                FieldPanel("job_title"),
                FieldPanel("role"),
            ],
            heading="Details",
        ),
        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"
            ),
        ),
    ]

    # Card panels
    card_panels = [
        FieldPanel("card_title"),
        FieldPanel("card_description"),
        ImageChooserPanel("card_image"),
    ]

    # Meta panels
    meta_panels = [
        MultiFieldPanel(
            [FieldPanel("city"), FieldPanel("country")],
            heading="Location",
            help_text=(
                "Location fields. The country field is also filterable "
                "via the people directory page."
            ),
        ),
        MultiFieldPanel(
            [InlinePanel("topics")], heading="Topics this person specializes in"
        ),
        MultiFieldPanel(
            [
                FieldPanel("twitter"),
                FieldPanel("facebook"),
                FieldPanel("linkedin"),
                FieldPanel("github"),
                FieldPanel("email"),
            ],
            heading="Profiles",
            help_text="",
        ),
        StreamFieldPanel("websites"),
        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"),
        ]
    )

    @property
    def display_title(self):
        """
        Return the display title for profile pages. Adds a nickname to the
        person's full name when one is provided.
        """
        return f'{self.title} aka "{self.nickname}"' if self.nickname else self.title

    @property
    def events(self):
        """
        Return upcoming events where this person is a speaker,
        ordered by start date
        """
        from ..events.models import Event

        upcoming_events = Event.published_objects.filter(
            start_date__gte=get_past_event_cutoff()
        )

        speaker_events = Event.published_objects.none()

        for event in upcoming_events.all():
            # add the event to the list if the current person is a speaker
            if event.has_speaker(self):
                speaker_events = speaker_events | Event.published_objects.page(event)

        return speaker_events.order_by("start_date")

    @property
    def articles(self):
        """
        Return articles and external articles where this person is (one of) the authors,
        ordered by article date, most recent first
        """
        from ..articles.models import Article
        from ..externalcontent.models import ExternalArticle

        articles = Article.published_objects.none()
        external_articles = ExternalArticle.published_objects.none()

        all_articles = Article.published_objects.all()
        all_external_articles = ExternalArticle.published_objects.all()

        for article in all_articles:
            if article.has_author(self):
                articles = articles | Article.published_objects.page(article)

        for external_article in all_external_articles:
            if external_article.has_author(self):
                external_articles = external_articles | (
                    ExternalArticle.published_objects.page(external_article)
                )

        return sorted(
            chain(articles, external_articles), key=attrgetter("date"), reverse=True
        )

    @property
    def videos(self):
        """
        Return the most recent videos and external videos where this person is (one of)
        the speakers.
        """
        from ..videos.models import Video
        from ..externalcontent.models import ExternalVideo

        videos = Video.published_objects.none()
        external_videos = ExternalVideo.published_objects.none()

        all_videos = Video.published_objects.all()
        all_external_videos = ExternalVideo.published_objects.all()

        for video in all_videos:
            if video.has_speaker(self):
                videos = videos | Video.published_objects.page(video)

        for external_video in all_external_videos:
            if external_video.has_speaker(self):
                external_videos = external_videos | (
                    ExternalVideo.published_objects.page(external_video)
                )

        return sorted(
            chain(videos, external_videos), key=attrgetter("date"), reverse=True
        )

    @property
    def role_group(self):
        return {"slug": self.role, "title": dict(ROLE_CHOICES).get(self.role, "")}

    @property
    def country_group(self):
        return (
            {"slug": self.country.code.lower(), "title": self.country.name}
            if self.country
            else {"slug": ""}
        )
Пример #30
0
class Events(BasePage):

    # Note that we only paginate PAST events right now, and not the future ones
    PAST_EVENTS_PER_PAGE = 20

    parent_page_types = ["home.HomePage"]
    subpage_types = ["events.Event"]
    template = "events.html"

    # Content fields
    featured = StreamField(
        StreamBlock(
            [
                (
                    "event",
                    PageChooserBlock(
                        target_model=("events.Event",
                                      "externalcontent.ExternalEvent")),
                ),
                ("external_page", FeaturedExternalBlock()),
            ],
            max_num=1,
            required=False,
        ),
        null=True,
        blank=True,
        help_text="Optional space to show a featured event",
    )
    body = CustomStreamField(help_text=(
        "Main page body content. Supports rich text, images, embed via URL, "
        "embed via HTML, and inline code snippets"))

    # Meta fields
    keywords = ClusterTaggableManager(through=EventsTag, blank=True)

    # Content panels
    content_panels = BasePage.content_panels + [
        StreamFieldPanel("featured"),
        StreamFieldPanel("body"),
    ]

    # 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"),
    ])

    class Meta:
        verbose_name_plural = "Events"

    @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["events"] = self.get_upcoming_events(request)
        past_events, total_past_events = self.get_past_events(request)
        context["past_events"] = past_events
        context["show_past_event_pagination"] = (total_past_events >
                                                 self.PAST_EVENTS_PER_PAGE)
        return context

    def _year_months_to_years_and_months_tuples(self, year_months):
        """For the given list of "YYYY-MM" strings, return a list of tuples
        containg the year and and month, still as strings.

        Example input:  ["2020-03", "2020-12"]
        Example output: [("2020", "03"), ("2020", "12")]
        """

        if not year_months:
            return []
        return [tuple(x.split("-")) for x in [y for y in year_months if y]]

    def _build_date_q(self, year_months):
        "Support filtering future events by selected year-month pair(s)"
        default_future_events_q = Q(start_date__gte=get_past_event_cutoff())

        years_and_months_tuples = self._year_months_to_years_and_months_tuples(
            year_months)
        if not years_and_months_tuples:
            # Covers case where no year_months
            return default_future_events_q

        # Build a Q where it's (Month X AND Year X) OR (Month Y AND Year Y), etc
        overall_date_q = None

        for year, month in years_and_months_tuples:
            date_q = Q(**{"start_date__year": year})
            date_q.add(Q(**{"start_date__month": month}), Q.AND)

            if overall_date_q is None:
                overall_date_q = date_q
            else:
                overall_date_q.add(date_q, Q.OR)

        # Finally, ensure we don't include past events here (ie, same month as
        # selected but before today)
        overall_date_q.add(default_future_events_q, Q.AND)
        return overall_date_q

    def get_upcoming_events(self, request):
        """Return filtered future events in chronological order"""
        # These are not paginated but ARE filtered

        countries = request.GET.getlist(COUNTRY_QUERYSTRING_KEY)
        years_months = request.GET.getlist(YEAR_MONTH_QUERYSTRING_KEY)
        topics = request.GET.getlist(TOPIC_QUERYSTRING_KEY)

        countries_q = Q(country__in=countries) if countries else Q()
        topics_q = Q(topics__topic__slug__in=topics) if topics else Q()

        # year_months need splitting to make them work
        date_q = self._build_date_q(years_months)

        combined_q = Q()
        if countries_q:
            combined_q.add(countries_q, Q.AND)
        if date_q:
            combined_q.add(date_q, Q.AND)
        if topics_q:
            combined_q.add(topics_q, Q.AND)

        # Combined_q will always have something because it includes
        # the start_date__gte test
        events = get_combined_events(self, q_object=combined_q)

        return events

    def get_past_events(self, request):
        """Return paginated past events in reverse chronological order,
        plus a count of how many there are in total
        """
        past_events = get_combined_events(
            self, reverse=True, start_date__lt=get_past_event_cutoff())
        total_past_events = len(past_events)

        past_events = paginate_resources(
            past_events,
            page_ref=request.GET.get(PAGINATION_QUERYSTRING_KEY),
            per_page=self.PAST_EVENTS_PER_PAGE,
        )
        return past_events, total_past_events

    def get_relevant_countries(self):
        # Relevant here means a country that a published Event is or was in
        raw_countries = (event.country
                         for event in Event.published_objects.filter(
                             start_date__gte=get_past_event_cutoff()).distinct(
                                 "country").order_by("country")
                         if event.country)

        return [{
            "code": country.code,
            "name": country.name
        } for country in raw_countries]

    def get_relevant_dates(self):
        # Relevant here means a date for a published *future* event
        # TODO: would be good to cache this for short period of time
        raw_events = get_combined_events(
            self, start_date__gte=get_past_event_cutoff())
        return sorted([event.start_date for event in raw_events])

    def dates_to_unique_month_years(self, dates: List[datetime.date]):
        """From the given list of dates, generate another list of dates where the
        year-month combinations are unique and the `day` of each is set to the 1st.

        We do this because the filter-form.html template only uses Y and M when
        rendering the date options, so we must skip/merge dates that feature year-month
        pairs that _already_ appear in the list. If we don't, and if there is more than
        one Event for the same year-month, we end up with multiple Year-Months
        displayed in the filter options.
        """
        return sorted(set([datetime.date(x.year, x.month, 1) for x in dates]))

    def get_filters(self):
        return {
            "countries": self.get_relevant_countries(),
            "dates":
            self.dates_to_unique_month_years(self.get_relevant_dates()),
            "topics": Topic.published_objects.order_by("title"),
        }