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
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
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
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
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
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
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
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
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'
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." ) })
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
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
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" )
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
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'), }
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')
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
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 += " – " 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 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
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
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()]
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
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"), }
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
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"), }
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
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 += " – " 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()]
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": ""} )
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"), }