def setUp(self): self.EventPageForm = get_form_for_model( EventPage, form_class=WagtailAdminPageForm, formsets=[]) self.event = EventPage(title='Abergavenny sheepdog trials', date_from=date(2014, 7, 20), date_to=date(2014, 7, 21)) self.EndDatePanel = FieldPanel('date_to', classname='full-width').bind_to_model(EventPage)
class ContentPage(Page): parent_page_types = ['home.HomePage', 'content.ContentPage'] subpage_types = ['people.People', 'content.ContentPage'] template = 'content.html' # Content fields hero_image = ForeignKey('mozimages.MozImage', null=True, blank=True, on_delete=SET_NULL, related_name='+') body = CustomStreamField(help_text=( 'Main page body content. Supports rich text, images, embed via URL, embed via HTML, and inline code snippets' )) # 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=ContentPageTag, blank=True) # Editor panel configuration content_panels = Page.content_panels + [ MultiFieldPanel( [ImageChooserPanel('hero_image')], heading='Hero image', help_text= 'Image should be at least 1024px x 438px (21:9 aspect ratio)'), StreamFieldPanel('body'), ] # 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', 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'), ])
class BlogArticlePage(MixinSeoFields, Page, MixinPageMethods, GoogleAdsMixin): template = "blog_post.haml" date = models.DateField("Post date") page_title = models.CharField( max_length=MAX_BLOG_ARTICLE_TITLE_LENGTH, help_text=ArticleBlogPageHelpTexts.ARTICLE_PAGE_TITLE.value) body = StreamField([ (ArticleBodyBlockNames.MARKDOWN.value, MarkdownBlock(icon="code")), (ArticleBodyBlockNames.HEADER.value, CharBlock()), (ArticleBodyBlockNames.PARAGRAPH.value, RichTextBlock(features=RICH_TEXT_BLOCK_FEATURES)), (ArticleBodyBlockNames.TABLE.value, TableBlock()), (ArticleBodyBlockNames.IMAGE.value, CaptionedImageBlock()), ], ) search_fields = Page.search_fields + [ index.SearchField("intro"), index.SearchField("body") ] author = models.ForeignKey(Employees, on_delete=models.DO_NOTHING) read_time = models.PositiveIntegerField() table_of_contents = models.BooleanField(default=False) recommended_articles = StreamField( [("page", PageChooserBlock(can_choose_root=False, page_type="blog.BlogArticlePage"))], null=True, blank=True, ) views = models.PositiveIntegerField(default=0) cover_photo = models.ForeignKey("wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+") cover_photo_alt_description = models.CharField(max_length=125, blank=True, default="Open the article") article_photo = models.ForeignKey("wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+") article_photo_alt_description = models.CharField(max_length=125, blank=True, default="") is_main_article = models.BooleanField(default=False) Page._meta.get_field( "title").help_text = ArticleBlogPageHelpTexts.PAGE_TITLE.value content_panels = Page.content_panels + [ FieldPanel("page_title", classname="title full"), FieldPanel("date"), FieldPanel("author"), FieldPanel("read_time"), StreamFieldPanel("recommended_articles"), FieldPanel("views"), FieldPanel("is_main_article"), ImageChooserPanel("cover_photo"), FieldPanel("cover_photo_alt_description"), ImageChooserPanel("article_photo"), FieldPanel("article_photo_alt_description"), FieldPanel("table_of_contents"), StreamFieldPanel("body"), ] @cached_property def headers_list(self) -> List[str]: list_of_headers = [] for stream_child in self.body: # pylint: disable=not-an-iterable if stream_child.block.name == ArticleBodyBlockNames.HEADER.value: list_of_headers.append(stream_child.value) return list_of_headers def get_header_id(self, title: str) -> int: return self.headers_list.index(title) @cached_property def intro(self) -> str: paragraph_text = self._get_text_for_intro( MAX_BLOG_ARTICLE_INTRO_LENGTH) if len(paragraph_text) == 0: return "Article intro not available." words_cycle = cycle(paragraph_text.split()) intro_text = self._concatenate_intro_text_from_paragraphs_text( words_cycle, MAX_BLOG_ARTICLE_INTRO_LENGTH) end_ellipsis = INTRO_ELLIPSIS return intro_text + end_ellipsis def _get_text_for_intro(self, character_limit: int) -> str: text_blocks: list = self._get_list_of_text_blocks() paragraphs_text = "" if len(text_blocks) == 0: return paragraphs_text blocks_cycle = cycle(text_blocks) while len(paragraphs_text) < character_limit: paragraphs_text = self._extract_paragraph_text_from_block( blocks_cycle, paragraphs_text) return paragraphs_text def _get_list_of_text_blocks(self) -> list: whitelisted_block_names = [ ArticleBodyBlockNames.PARAGRAPH.value, ArticleBodyBlockNames.MARKDOWN.value ] return list( filter( lambda body_element: body_element.block.name in whitelisted_block_names, self.body)) @staticmethod def _extract_paragraph_text_from_block(blocks: cycle, text: str) -> str: space_between_texts = " " next_block = next(blocks) if next_block.block.name == ArticleBodyBlockNames.MARKDOWN.value: source_text = "".join( BeautifulSoup(markdown(next_block.value), "html.parser").findAll(text=True)) else: source_text = next_block.value.source next_text = strip_tags(source_text) if len(text) == 0: text = next_text else: text += f"{space_between_texts}{next_text}" return text def _concatenate_intro_text_from_paragraphs_text( self, words_cycle: cycle, character_limit: int) -> str: intro_text = "" new_text = next(words_cycle) while len(new_text) < character_limit: intro_text = new_text new_text = self._concatenate_strings(intro_text, words_cycle) return intro_text @staticmethod def _concatenate_strings(text: str, words: cycle) -> str: space_between_texts = " " text += f"{space_between_texts}{next(words)}" return text def get_proper_url(self) -> str: return self.slug def get_absolute_url(self) -> str: return self.url_path def get_context(self, request: WSGIRequest, *args: Any, **kwargs: Any) -> dict: context = super().get_context(request, *args, **kwargs) self._increase_view_counter() context["URL_PREFIX"] = settings.URL_PREFIX context["article_body_block_names"] = ArticleBodyBlockNames context["GOOGLE_ADS_CONVERSION_ID"] = settings.GOOGLE_ADS_CONVERSION_ID context["GOOGLE_TAG_MANAGER_ID"] = settings.GOOGLE_TAG_MANAGER_ID return context def _increase_view_counter(self) -> None: # increase page view counter self.views += 1 self.full_clean() self.save() def save(self, *args: Any, **kwargs: Any) -> None: # pylint: disable=signature-differs if not BlogArticlePage.objects.filter( is_main_article=True) and not self.is_main_article: self.is_main_article = True if self.is_main_article: try: article = BlogArticlePage.objects.get( is_main_article=self.is_main_article) article.is_main_article = False article.save() except BlogArticlePage.DoesNotExist: pass if self.table_of_contents and len(self.headers_list) == 0: self.table_of_contents = False self._validate_parent_page() super().save(*args, **kwargs) def clean(self) -> None: super().clean() self._clean_recommended_articles() self._validate_recommended_articles_uniqueness() def _clean_recommended_articles(self) -> None: self.recommended_articles = StreamValue( stream_block=StreamBlock([("page", PageChooserBlock())]), stream_data=[("page", stream_child.value) for stream_child in self.recommended_articles if stream_child.value is not None], ) def _validate_parent_page(self) -> None: if not isinstance(self.get_parent().specific, BlogIndexPage): raise ValidationError( message=f"{self.title} must be child of BlogIndexPage") def _validate_recommended_articles_uniqueness(self) -> None: article_pages_set = set() for stream_child in self.recommended_articles: # pylint: disable=not-an-iterable if stream_child.value in article_pages_set: raise ValidationError( message=f"'{stream_child.value}' is listed more than once!" ) article_pages_set.add(stream_child.value)
if self.signup_limit == -1: context['can_signup'] = False elif self.signup_limit == 0: context['can_signup'] = in_signup_window elif self.signups.count() < self.signup_limit: context['can_signup'] = in_signup_window else: context['can_signup'] = False return context EventPage.content_panels = [ MultiFieldPanel([ FieldPanel('title', classname="full title"), FieldPanel('cancelled'), FieldPanel('description'), FieldPanel('category'), FieldPanel('location'), FieldPanel('facebook_link'), FieldPanel('start'), FieldPanel('finish'), StreamFieldPanel('body'), ], heading='Event details'), MultiFieldPanel([ FieldPanel('signup_limit'), FieldPanel('signup_open'), FieldPanel('signup_close'), FieldPanel('signup_freshers_open')
class Constant(models.Model): name = models.CharField(max_length=100, unique=True) description = models.TextField(null=True, blank=True) key = models.SlugField( max_length=255, unique=True, help_text=mark_safe( _("The key that can be used in tags to include the value.<br/>" "For example: <code>{{ ga_id }}</code>.")), ) value = models.CharField( max_length=255, help_text=_( "The value to be rendered when this constant is included."), ) panels = [ FieldPanel("name", classname="full title"), FieldPanel("description", classname="full"), MultiFieldPanel( [FieldRowPanel([FieldPanel("key"), FieldPanel("value")])], heading=_("Data")), ] def as_dict(self): return { "name": self.name, "description": self.description, "key": self.key, "value": self.value, } def get_value(self): return self.value @classmethod def create_context(cls): context = cache.get("wtm_constant_cache", {}) if not context: for constant in cls.objects.all(): context[constant.key] = constant.get_value() timeout = getattr(settings, "WTM_CACHE_TIMEOUT", 60 * 30) cache.set("wtm_constant_cache", context, timeout) return context def clean(self): from wagtail_tag_manager.models.variables import Variable if Variable.objects.filter(key=self.key).exists(): raise ValidationError( f"A variable with the key '{ self.key }' already exists.") else: super().clean() def save(self, force_insert=False, force_update=False, using=None, update_fields=None): self.full_clean() return super().save(force_insert, force_update, using, update_fields) def __str__(self): return self.name
class ArticlePage( BasicPageAbstract, ContentPage, FeatureablePageAbstract, FromTheArchivesPageAbstract, ShareablePageAbstract, ThemeablePageAbstract, ): class ArticleTypes(models.TextChoices): CIGI_IN_THE_NEWS = ('cigi_in_the_news', 'CIGI in the News') INTERVIEW = ('interview', 'Interview') NEWS_RELEASE = ('news_release', 'News Release') OP_ED = ('op_ed', 'Op-Ed') OPINION = ('opinion', 'Opinion') class Languages(models.TextChoices): DA = ('da', 'Danish') DE = ('de', 'German') EL = ('el', 'Greek') EN = ('en', 'English') ES = ('es', 'Spanish') FR = ('fr', 'French') ID = ('id', 'Indonesian') IT = ('it', 'Italian') NL = ('nl', 'Dutch') PL = ('pl', 'Polish') PT = ('pt', 'Portugese') RO = ('ro', 'Romanian') SK = ('sk', 'Slovak') SV = ('sv', 'Swedish') TR = ('tr', 'Turkish') ZH = ('zh', 'Chinese') class HeroTitlePlacements(models.TextChoices): BOTTOM = ('bottom', 'Bottom') TOP = ('top', 'Top') article_series = models.ForeignKey( 'wagtailcore.Page', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Opinion series', ) article_type = models.ForeignKey( 'articles.ArticleTypePage', null=True, blank=False, on_delete=models.SET_NULL, related_name='articles', ) body = StreamField( BasicPageAbstract.body_default_blocks + [ BasicPageAbstract.body_accordion_block, BasicPageAbstract.body_autoplay_video_block, BasicPageAbstract.body_chart_block, BasicPageAbstract.body_embedded_tiktok_block, BasicPageAbstract.body_external_quote_block, BasicPageAbstract.body_external_video_block, BasicPageAbstract.body_extract_block, BasicPageAbstract.body_highlight_title_block, BasicPageAbstract.body_image_full_bleed_block, BasicPageAbstract.body_image_scroll_block, BasicPageAbstract.body_poster_block, BasicPageAbstract.body_pull_quote_left_block, BasicPageAbstract.body_pull_quote_right_block, BasicPageAbstract.body_recommended_block, BasicPageAbstract.body_text_border_block, BasicPageAbstract.body_tool_tip_block, BasicPageAbstract.body_tweet_block, ], blank=True, ) embed_youtube = models.URLField( blank=True, verbose_name='YouTube Embed', help_text= 'Enter the YouTube URL (https://www.youtube.com/watch?v=4-Xkn1U1DkA) or short URL (https://youtu.be/o5acQ2GxKbQ) to add an embedded video.', ) embed_youtube_label = models.CharField( max_length=255, blank=True, help_text='Add a label to appear below the embedded video.', ) footnotes = RichTextField( blank=True, features=[ 'bold', 'endofarticle', 'h3', 'h4', 'italic', 'link', 'ol', 'ul', 'subscript', 'superscript', 'anchor', ], ) hero_title_placement = models.CharField( blank=True, max_length=16, choices=HeroTitlePlacements.choices, verbose_name='Hero Title Placement', help_text= 'Placement of the title within the hero section. Currently only works on the Longform 2 theme.', ) hide_excerpt = models.BooleanField( default=False, verbose_name='Hide Excerpt', help_text= 'For "CIGI in the News" only: when enabled, hide excerpt and display full article instead', ) image_banner = models.ForeignKey( 'images.CigionlineImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Banner Image', ) image_banner_small = models.ForeignKey('images.CigionlineImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Banner Image Small') image_poster = models.ForeignKey( 'images.CigionlineImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Poster Image', help_text='A poster image used in feature sections', ) interviewers = StreamField( [ ('interviewer', PageChooserBlock(required=True, page_type='people.PersonPage')), ], blank=True, ) language = models.CharField( blank=True, max_length=2, choices=Languages.choices, verbose_name='Language', help_text= 'If this content is in a language other than English, please select the language from the list.', ) multimedia_series = models.ForeignKey( 'wagtailcore.Page', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', ) related_files = StreamField( [ ('file', DocumentChooserBlock()), ], blank=True, ) short_description = RichTextField( blank=True, null=False, features=['bold', 'italic', 'link'], ) video_banner = models.ForeignKey( 'wagtailmedia.Media', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Banner Video', ) website_button_text = models.CharField( blank=True, max_length=64, help_text= 'Override the button text for the article website. If empty, the button will read "View Full Article".' ) website_url = models.URLField(blank=True, max_length=512) works_cited = RichTextField( blank=True, features=[ 'bold', 'endofarticle', 'h3', 'h4', 'italic', 'link', 'ol', 'ul', 'subscript', 'superscript', ], ) # Reference field for the Drupal-Wagtail migrator. Can be removed after. drupal_node_id = models.IntegerField(blank=True, null=True) @property def cigi_people_mentioned_ids(self): return [item.person.id for item in self.cigi_people_mentioned.all()] @property def expired_image(self): if self.publishing_date: return self.publishing_date < datetime.datetime( 2017, 1, 1).astimezone(pytz.timezone('America/Toronto')) return False @property def article_series_description(self): if self.article_series: return self.article_series.specific.series_items_description return None @property def article_series_disclaimer(self): if self.article_series: for series_item in self.article_series.specific.article_series_items: if series_item.content_page.specific == self and not series_item.hide_series_disclaimer: return self.article_series.specific.series_items_disclaimer return None def is_opinion(self): return self.article_type.title in [ 'Op-Eds', 'Opinion', ] def get_template(self, request, *args, **kwargs): standard_template = super(ArticlePage, self).get_template(request, *args, **kwargs) if self.theme: return f'themes/{self.get_theme_dir()}/article_page.html' return standard_template content_panels = [ BasicPageAbstract.title_panel, MultiFieldPanel([ FieldPanel('short_description'), StreamFieldPanel('body'), FieldPanel('footnotes'), FieldPanel('works_cited'), ], heading='Body', classname='collapsible collapsed'), MultiFieldPanel( [ PageChooserPanel( 'article_type', ['articles.ArticleTypePage'], ), FieldPanel('hide_excerpt'), FieldPanel('publishing_date'), FieldPanel('website_url'), FieldPanel('website_button_text'), FieldPanel('language'), ], heading='General Information', classname='collapsible collapsed', ), ContentPage.authors_panel, MultiFieldPanel( [ ImageChooserPanel('image_hero'), ImageChooserPanel('image_poster'), ImageChooserPanel('image_banner'), ImageChooserPanel('image_banner_small'), ], heading='Images', classname='collapsible collapsed', ), MultiFieldPanel( [ FieldPanel('embed_youtube'), FieldPanel('embed_youtube_label'), MediaChooserPanel('video_banner'), ], heading='Media', classname='collapsible collapsed', ), ContentPage.recommended_panel, MultiFieldPanel( [ FieldPanel('topics'), FieldPanel('projects'), PageChooserPanel( 'article_series', ['articles.ArticleSeriesPage'], ), PageChooserPanel( 'multimedia_series', ['multimedia.MultimediaSeriesPage'], ), InlinePanel('cigi_people_mentioned', label='People Mentioned'), StreamFieldPanel('interviewers'), StreamFieldPanel('related_files'), ], heading='Related', classname='collapsible collapsed', ), FromTheArchivesPageAbstract.from_the_archives_panel, ] promote_panels = Page.promote_panels + [ FeatureablePageAbstract.feature_panel, ShareablePageAbstract.social_panel, SearchablePageAbstract.search_panel, ] settings_panels = Page.settings_panels + [ ThemeablePageAbstract.theme_panel, ] search_fields = BasicPageAbstract.search_fields \ + ContentPage.search_fields \ + [ index.FilterField('article_type'), index.FilterField('cigi_people_mentioned_ids'), index.FilterField('publishing_date'), ] parent_page_types = ['articles.ArticleListPage'] subpage_types = [] templates = 'articles/article_page.html' @property def is_title_bottom(self): return self.title in [ 'Can the G20 Save Globalization\'s Waning Reputation?', 'Shoshana Zuboff on the Undetectable, Indecipherable World of Surveillance Capitalism' ] @property def article_series_category(self): category = '' for series_item in self.article_series.specific.article_series_items: if series_item.category_title: category = series_item.category_title if series_item.content_page.id == self.id: return category class Meta: verbose_name = 'Opinion' verbose_name_plural = 'Opinions'
class PrimaryPage(FoundationMetadataPageMixin, Page): """ Basically a straight copy of modular page, but with restrictions on what can live 'under it'. Ideally this is just PrimaryPage(ModularPage) but setting that up as a migration seems to be causing problems. """ header = models.CharField(max_length=250, blank=True) banner = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='primary_banner', verbose_name='Hero Image', ) intro = models.CharField( max_length=250, blank=True, help_text='Intro paragraph to show in hero cutout box') narrowed_page_content = models.BooleanField( default=False, help_text= 'For text-heavy pages, turn this on to reduce the overall width of the content on the page.' ) zen_nav = models.BooleanField( default=False, help_text= 'For secondary nav pages, use this to collapse the primary nav under a toggle hamburger.' ) body = StreamField(base_fields) settings_panels = Page.settings_panels + [ MultiFieldPanel([ FieldPanel('narrowed_page_content'), ]), MultiFieldPanel([ FieldPanel('zen_nav'), ]) ] content_panels = Page.content_panels + [ FieldPanel('header'), ImageChooserPanel('banner'), FieldPanel('intro'), StreamFieldPanel('body'), ] parent_page_types = [ 'Homepage', 'PrimaryPage', ] subpage_types = ['PrimaryPage', 'RedirectingPage'] show_in_menus_default = True def get_context(self, request): context = super(PrimaryPage, self).get_context(request) return get_page_tree_information(self, context)
class CompositionPage(Page): composition_title = RichTextField(features=['bold', 'italic']) description = StreamField([('rich_text', RichTextBlock()), ('image', ImageChooserBlock())], blank=True) location = RichTextField( blank=True, features=['bold', 'italic', 'link', 'document-link'], ) genre = ParentalManyToManyField(Genre, blank=True, related_name='compositions') instrumentation = ParentalManyToManyField( 'Instrument', blank=True, ) orchestration = RichTextField( blank=True, features=['bold', 'italic'], help_text=( 'If the composition is for an ensemble, use this field to enter ' 'the orchestration of the work.')) duration = DurationField(null=True, blank=True, help_text='Expects data in the format "HH:MM:SS"') dedicatee = RichTextField( blank=True, features=['bold', 'italic', 'link', 'document-link'], ) text_source = RichTextField( blank=True, features=['bold', 'italic', 'link', 'document-link'], help_text='The source of the text used in the compostition.') collaborator = RichTextField( blank=True, features=['bold', 'italic', 'link', 'document-link'], help_text='Others that Decruck collaborated with.') manuscript_status = RichTextField( blank=True, features=['bold', 'italic', 'link', 'document-link'], help_text='Notes about the location and condition of the manuscript.') recording = StreamField([('rich_text', RichTextBlock()), ('image', ImageChooserBlock())], blank=True) information_up_to_date = BooleanField(default=False) scanned = BooleanField(default=False) premiere = RichTextField( blank=True, features=['bold', 'italic', 'link', 'document-link'], ) # For preview score preview_score = FileField( upload_to='composition_preview_scores/', blank=True, null=True, validators=[FileExtensionValidator(allowed_extensions=['pdf'])]) preview_score_checksum = CharField(editable=False, max_length=256, blank=True) preview_score_checked = False preview_score_updated = False # Extended Date Time Format nat_lang_edtf_string = CharField( verbose_name='Natural Language Date', help_text=('The EDTF date in natural language. This field is help ' 'users who aren\'t familiar with the EDTF. It does not ' 'change how the date is represented.'), blank=True, max_length=256) edtf_string = CharField( verbose_name='EDTF Date', help_text=mark_safe( 'A date in the <a href="https://www.loc.gov/standards/datetime/" ' 'target="_blank"><strong>Extended Date Time Format</strong></a>'), blank=True, max_length=256) lower_fuzzy = DateField(editable=False, null=True, blank=True) upper_fuzzy = DateField(editable=False, null=True, blank=True) lower_strict = DateField(editable=False, null=True, blank=True) upper_strict = DateField(editable=False, null=True, blank=True) nat_lang_year = CharField(editable=False, max_length=9, blank=True) def instrumentation_list(self): return ', '.join([str(i) for i in self.instrumentation.all()]) class Meta: verbose_name = "Composition" def get_context(self, request, *args, **kwargs): ctx = super().get_context(request, *args, **kwargs) try: search_idx = request.session['comp_search_index'] if search_idx: idx = search_idx.index(self.pk) prev_url = None next_url = None if idx > 0: pk = search_idx[idx - 1] prev_url = CompositionPage.objects.get(pk=pk).url if idx < len(search_idx) - 1: pk = search_idx[idx + 1] next_url = CompositionPage.objects.get(pk=pk).url ctx['prev_url'] = prev_url ctx['next_url'] = next_url ctx['comp_search_qs'] = request.\ session.get('comp_search_qs', '') except (KeyError, ValueError): pass return ctx def clean(self): super().clean() # Per Django docs: validate and modify values in Model.clean() # https://docs.djangoproject.com/en/3.1/ref/models/instances/#django.db.models.Model.clean # Check that nat_lang_edtf_string and edtf_string are either both set, or both unset if (self.nat_lang_edtf_string and not self.edtf_string) or (not self.nat_lang_edtf_string and self.edtf_string): raise ValidationError( 'If setting a date on a composition, an EDTF string and a natural language EDTF string must be provided.' ) # Validate edtf_string if self.edtf_string and self.nat_lang_edtf_string: try: e = parse_edtf(self.edtf_string) except EDTFParseException: raise ValidationError({ 'edtf_string': '{} is not a valid EDTF string'.format(self.edtf_string) }) self.lower_fuzzy = struct_time_to_date(e.lower_fuzzy()) self.upper_fuzzy = struct_time_to_date(e.upper_fuzzy()) self.lower_strict = struct_time_to_date(e.lower_strict()) self.upper_strict = struct_time_to_date(e.upper_strict()) if self.lower_strict.year != self.upper_strict.year: self.nat_lang_year = '{}-{}'.format(self.lower_strict.year, self.upper_strict.year) else: self.nat_lang_year = str(self.lower_strict.year) def save(self, *args, **kwargs): # If there's no preview score file, then just save the model if not self.preview_score: return super().save(*args, **kwargs) if self.preview_score_checked: # This was the cause of a subtle bug. Because this method can be # called multiple times during model creation, leaving this flag # set would cause the post save hook to fire multiple times. self.preview_score_updated = False return super().save(*args, **kwargs) h = hashlib.md5() for chunk in iter(lambda: self.preview_score.read(8192), b''): h.update(chunk) self.preview_score.seek(0) checksum = h.hexdigest() if not self.preview_score_checksum == checksum: self.preview_score_checksum = checksum self.preview_score_updated = True self.preview_score_checked = True return super().save(*args, **kwargs) content_panels = Page.content_panels + [ FieldPanel('composition_title'), StreamFieldPanel('description'), MultiFieldPanel( [FieldPanel('edtf_string'), FieldPanel('nat_lang_edtf_string')], help_text='Enter a date in the LOC Extended Date Time Format', heading='Date'), FieldPanel('location'), FieldPanel('instrumentation'), FieldPanel('orchestration'), FieldPanel('duration'), FieldPanel('dedicatee'), FieldPanel('premiere'), FieldPanel('genre'), FieldPanel('text_source'), FieldPanel('collaborator'), FieldPanel('manuscript_status'), FieldPanel('information_up_to_date'), FieldPanel('scanned'), FieldPanel('preview_score'), StreamFieldPanel('recording'), ] search_fields = Page.search_fields + [ index.SearchField('description', partial_match=True), index.SearchField('location', partial_match=True), index.SearchField('dedicatee', partial_match=True), index.SearchField('premiere', partial_match=True), index.SearchField('text_source', partial_match=True), index.SearchField('collaborator', partial_match=True), index.SearchField('manuscript_status', partial_match=True), index.SearchField('recording', partial_match=True), index.RelatedFields('genre', [ index.SearchField('genre_en', partial_match=True), index.SearchField('genre_fr', partial_match=True), ]), index.RelatedFields('instrumentation', [ index.SearchField('instrument_en', partial_match=True), index.SearchField('instrument_fr', partial_match=True), ]), ] parent_page_types = ['CompositionListingPage']
class Event(BasePage): resource_type = "event" parent_page_types = ["events.Events"] subpage_types = [] template = "event.html" # Content fields description = RichTextField( blank=True, default="", features=RICH_TEXT_FEATURES_SIMPLE, help_text="Optional short text description, max. 400 characters", max_length=400, ) image = ForeignKey( "mozimages.MozImage", null=True, blank=True, on_delete=SET_NULL, related_name="+", ) start_date = DateField(default=datetime.date.today) end_date = DateField(blank=True, null=True) latitude = FloatField(blank=True, null=True) longitude = FloatField(blank=True, null=True) register_url = URLField("Register URL", blank=True, null=True) body = CustomStreamField( blank=True, null=True, help_text=( "Optional body content. Supports rich text, images, embed via URL, " "embed via HTML, and inline code snippets" ), ) venue_name = CharField(max_length=100, blank=True, default="") venue_url = URLField("Venue URL", max_length=100, blank=True, default="") address_line_1 = CharField(max_length=100, blank=True, default="") address_line_2 = CharField(max_length=100, blank=True, default="") address_line_3 = CharField(max_length=100, blank=True, default="") city = CharField(max_length=100, blank=True, default="") state = CharField("State/Province/Region", max_length=100, blank=True, default="") zip_code = CharField("Zip/Postal code", max_length=100, blank=True, default="") country = CountryField(blank=True, default="") agenda = StreamField( StreamBlock([("agenda_item", AgendaItemBlock())], required=False), blank=True, null=True, help_text="Optional list of agenda items for this event", ) speakers = StreamField( StreamBlock( [ ("speaker", PageChooserBlock(target_model="people.Person")), ("external_speaker", ExternalSpeakerBlock()), ], required=False, ), blank=True, null=True, help_text="Optional list of speakers for this event", ) # Card fields card_title = CharField("Title", max_length=140, blank=True, default="") card_description = TextField("Description", max_length=400, blank=True, default="") card_image = ForeignKey( "mozimages.MozImage", null=True, blank=True, on_delete=SET_NULL, related_name="+", verbose_name="Image", ) # Meta fields keywords = ClusterTaggableManager(through=EventTag, blank=True) # Content panels content_panels = BasePage.content_panels + [ FieldPanel("description"), MultiFieldPanel( [ImageChooserPanel("image")], heading="Image", help_text=( "Optional header image. If not specified a fallback will be used. " "This image is also shown when sharing this page via social media" ), ), MultiFieldPanel( [ FieldPanel("start_date"), FieldPanel("end_date"), FieldPanel("latitude"), FieldPanel("longitude"), FieldPanel("register_url"), ], heading="Event details", classname="collapsible", help_text=mark_safe( "Optional time and location information for this event. Latitude and " "longitude are used to show a map of the event’s location. For more " "information on finding these values for a given location, " "'<a href='https://support.google.com/maps/answer/18539'>" "see this article</a>" ), ), StreamFieldPanel("body"), MultiFieldPanel( [ FieldPanel("venue_name"), FieldPanel("venue_url"), FieldPanel("address_line_1"), FieldPanel("address_line_2"), FieldPanel("address_line_3"), FieldPanel("city"), FieldPanel("state"), FieldPanel("zip_code"), FieldPanel("country"), ], heading="Event address", classname="collapsible", help_text=( "Optional address fields. The city and country are also shown " "on event cards" ), ), StreamFieldPanel("agenda"), StreamFieldPanel("speakers"), ] # Card panels card_panels = [ FieldPanel("card_title"), FieldPanel("card_description"), ImageChooserPanel("card_image"), ] # Meta panels meta_panels = [ MultiFieldPanel( [InlinePanel("topics")], heading="Topics", help_text=( "These are the topic pages the event will appear on. The first topic " "in the list will be treated as the primary topic and will be shown " "in the page’s related content." ), ), MultiFieldPanel( [ FieldPanel("seo_title"), FieldPanel("search_description"), ImageChooserPanel("social_image"), FieldPanel("keywords"), ], heading="SEO", help_text=( "Optional fields to override the default title and description " "for SEO purposes" ), ), ] # Settings panels settings_panels = [FieldPanel("slug")] edit_handler = TabbedInterface( [ ObjectList(content_panels, heading="Content"), ObjectList(card_panels, heading="Card"), ObjectList(meta_panels, heading="Meta"), ObjectList(settings_panels, heading="Settings", classname="settings"), ] ) @property def is_upcoming(self): """Returns whether an event is in the future.""" return self.start_date >= datetime.date.today() @property def primary_topic(self): """Return the first (primary) topic specified for the event.""" article_topic = self.topics.first() return article_topic.topic if article_topic else None @property def month_group(self): return self.start_date.replace(day=1) @property def country_group(self): return ( {"slug": self.country.code.lower(), "title": self.country.name} if self.country else {"slug": ""} ) @property def event_dates(self): """Return a formatted string of the event start and end dates""" event_dates = self.start_date.strftime("%b %-d") if self.end_date and self.end_date != self.start_date: event_dates += " – " start_month = self.start_date.strftime("%m") if self.end_date.strftime("%m") == start_month: event_dates += self.end_date.strftime("%-d") else: event_dates += self.end_date.strftime("%b %-d") return event_dates @property def event_dates_full(self): """Return a formatted string of the event start and end dates, including the year""" return self.event_dates + self.start_date.strftime(", %Y") def has_speaker(self, person): for speaker in self.speakers: # pylint: disable=not-an-iterable if speaker.block_type == "speaker" and str(speaker.value) == str( person.title ): return True return False
class WorkGroup ( UserGeneratedPage2, ActiveInactiveMixin ): subpage_types = [ 'userinput.MemberContainer', 'userinput.ProjectContainer', ] parent_page_types = [ 'userinput.WorkGroupContainer' ] child_template = 'userinput/workgroup_child.html' view_template = 'userinput/workgroup_view.html' objects = ActiveInactivePageManager() class Meta: verbose_name = _('workgroup') verbose_name_plural = _('workgroups') department_de = models.CharField( max_length = 64, blank = True, null = True, verbose_name = _('department (de)') ) department_en = models.CharField( max_length = 64, blank = True, null = True, verbose_name = _('department (en)') ) institute_en = models.CharField( max_length = 64, verbose_name = _('institute (en)') ) institute_de = models.CharField( max_length = 64, verbose_name = _('institute (de)') ) university_de = models.CharField( max_length = 64, verbose_name = _('university (de)') ) university_en = models.CharField( max_length = 64, verbose_name = _('university (en)') ) department = TranslatedField('department') institute = TranslatedField('institute') university = TranslatedField('university') homepage = models.CharField( max_length = 128, blank = True, null = True, verbose_name = _( 'internet address' ), help_text = _('Please include http:// or https:// and www, if required.') ) content_panels = [ MultiFieldPanel([ FieldPanel('title_de'), FieldPanel('institute_de'), FieldPanel('department_de'), FieldPanel('university_de'), ], heading =_ ('German Information')), MultiFieldPanel([ FieldPanel('title'), FieldPanel('institute_en'), FieldPanel('department_en'), FieldPanel('university_en'), ], heading =_ ('English Information')), MultiFieldPanel([ FieldPanel('homepage'), ], heading =_ ('Additional Information')), ] comment_panel = [ FieldPanel('internal_rubion_comment') ] edit_handler = TabbedInterface([ ObjectList( content_panels, _('Information')), ObjectList( comment_panel, _('Internal comments')), ]) @property def under_revision( self ): return self.has_unpublished_changes @property def is_active( self ): return not self.locked @property def has_active_projects( self ): return self.get_projects().filter(expire_at__gte = datetime.datetime.now()).exists() def add_member_container( self ): # Generates a container for the workgroup members if len( MemberContainer.objects.child_of( self ) ) == 0: title = "Members" title_de = "Mitglieder" mc = MemberContainer() mc.title = title mc.title_de = title_de mc.slug = "members" self.add_child( instance = mc ) return mc def add_project_container( self ): # Generate a container for Projects if len( ProjectContainer.objects.child_of( self ) ) == 0: title = "Projects" title_de = "Projekte" pc = ProjectContainer() pc.title = title pc.title_de = title_de pc.slug = "project" self.add_child( instance = pc ) return pc def after_create_hook( self, request ): # Auto-generate child containers self.add_member_container() self.add_project_container() # Adding a workgroup requires revision by RUBION # self.unpublish() # self.save_revision() def serve_success( self, request, edit = False ): if edit: # if edited, the workgroup is available return redirect( self.url ) else: # Created, workgroup awaits verification. Show add user page to add work group leader ident = Identification() ident.page = self ident.create_user = True ident.login_user = True ident.mail_text = 'Workgroup.create.identify' ident.save() pk = ident.id return redirect( reverse('rubauth.identify', kwargs = {'pk' : pk}) ) def create_project_page( self ): pc = ProjectContainer.objects.child_of(self).first() return UGCCreatePage2.objects.child_of(pc).first() def create_member_page( self ): mc = MemberContainer.objects.child_of(self).first() return UGCCreatePage2.objects.child_of(mc).first() def get_member_container( self ): return MemberContainer.objects.child_of(self).first() def get_head( self ): return RUBIONUser.objects.live().descendant_of(self).filter(is_leader=True).first() # for displaying in the admin overview: get_head.short_description = _('Group leader') def get_members( self ): return RUBIONUser.objects.live().descendant_of(self).filter(expire_at__isnull = True) def get_projects( self ): return Project.objects.live().descendant_of(self) def get_methods( self ): projects = self.get_projects() methods = [] for project in projects: pr_methods = project.get_methods() for method in pr_methods: if method not in methods: methods.append(method) return methods def create_group_leader ( self, user ): mc = MemberContainer.objects.child_of(self).first() group_leader = RUBIONUser() group_leader.is_leader = True group_leader.linked_user = user group_leader.owner = user group_leader.is_rub = user.username.find('@') == -1 group_leader.may_create_projects = True if not group_leader.is_rub: group_leader.title = user.username group_leader.title_de = user.username group_leader.slug = slugify(user.username) # Avoid cleaning on save... group_leader.dont_clean = True else: group_leader.title = '{}, {}'.format(user.last_name, user.first_name) group_leader.title_de = '{}, {}'.format(user.last_name, user.first_name) group_leader.slug = slugify('{}, {}'.format(user.last_name, user.first_name)) group_leader.first_name_db = user.first_name group_leader.last_name_db = user.last_name mc.add_child(group_leader) group_leader.save_revision( submitted_for_moderation = True, user = user ) return group_leader def validate( self, request, user = None, username = None): if RUBIONUser.exists( user ): return TemplateResponse( request, 'userinput/errors/user_has_workgroup.html', { 'user' : user } ) else: self.save() r_user = self.create_group_leader( user ) self.owner = r_user.linked_user self.save_revision(submitted_for_moderation = True, user = r_user.linked_user) return redirect (r_user.full_url + r_user.reverse_subpage('edit')) def user_passes_test( self, user, what ): if not user.is_authenticated: return False if user.is_superuser: return True r_user = RUBIONUser.objects.get( linked_user = user ) user_in_wg = r_user.get_workgroup() == self # Every RUBION-User may see the workgroups if what == self.VIEW: if self.under_revision: # Instead of returning False, which would result in a `403 Forbidden` # response, I raise a `404 Not found` here. A 403 would indicate that the group # has applied to work in RUBION, which should be treated confidential. # # Of course, someone would have to guess the name of the group to construct # the URL. But anyway... if not user_in_wg: raise Http404() return user.is_authenticated if what == self.EDIT: r_user = RUBIONUser.objects.get( linked_user = user ) return r_user == self.get_head() return False def get_context(self, request): context = super(WorkGroup, self).get_context(request) context['user_may_edit'] = self.user_passes_test(request.user, self.EDIT) context['user_is_workgroup_member'] = False if request.user.is_authenticated: try: r_user = RUBIONUser.objects.get(linked_user = request.user) except: r_user = None if r_user: is_workgroup_member = ( r_user.get_workgroup() == self ) context['user_is_workgroup_member'] = is_workgroup_member if is_workgroup_member: context['user_may_add_projects'] = r_user.may('project') context['user_may_add_members'] = r_user.may('member') return context def clean( self ): if not self.slug: self.slug = self._get_autogenerated_slug( slugify( self.title ) ) def inactivate( self, user = None ): # inactivate this group, all projects and users super().inactivate(user = user) for member in self.get_members(): member.inactivate(user = user) for project in self.get_projects(): if project.is_active: project.inactivate(user = user) def is_inactivated(self): return self.locked and (self.expire_at is not None and self.expire_at < datetime.datetime.now()) def __str__( self ): if self.department: return '{}, {}, {}, {}'.format( self.title_trans, self.institute, self.department, self.university ) else: return '{}, {}, {}'.format( self.title_trans, self.institute, self.university )
class PartnerPage(BasePage): STATUS = [('active', 'Active'), ('inactive', 'Inactive')] class Meta: verbose_name = _('Partner Page') parent_page_types = ['partner.PartnerIndexPage'] subpage_types = [] status = models.CharField(choices=STATUS, default='current_partner', max_length=20) public = models.BooleanField(default=True) description = RichTextField(blank=True) web_url = models.URLField(blank=True) logo = models.OneToOneField('images.CustomImage', null=True, blank=True, related_name='+', on_delete=models.SET_NULL) content_panels = Page.content_panels + [ FieldPanel('status'), FieldPanel('public'), FieldPanel('description'), FieldPanel('web_url'), ImageChooserPanel('logo'), ] def __str__(self): return self.title def get_context(self, request): context = super(PartnerPage, self).get_context(request) context['total_investments'] = sum( investment.amount_committed for investment in self.investments.all()) return context def get_absolute_url(self): return self.url @property def category_questions(self): category_questions = {} if not self.investments.exists(): return for investment in self.investments.all(): for category in investment.categories.all(): if category.name in category_questions.keys(): if category.value not in category_questions[category.name]: category_questions[category.name].append( category.value) else: category_questions[category.name] = [category.value] return category_questions def serve(self, request, *args, **kwargs): if not self.public: raise Http404 return super(PartnerPage, self).serve(request, *args, **kwargs)
class HomePage(RoutablePageMixin, Page): templates = 'home/home_page.html' subpage_types = [ 'blog.BlogListingPage', 'contact.ContactPage', 'flex.FlexPage', ] parent_page_type = [ 'wagtailcore.Page' ] #max_count = 1 banner_title = models.CharField(max_length=100, blank=False, null=True) banner_subtitle = RichTextField(features=['bold', 'italic']) banner_image = models.ForeignKey( "wagtailimages.Image", null=True, blank=False, on_delete=models.SET_NULL, related_name='+' ) banner_cta = models.ForeignKey( 'wagtailcore.Page', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) content = StreamField( [ ('cta', blocks.CTABlock()), ], null=True, blank=True ) api_fields = [ APIField('banner_title'), APIField('banner_subtitle'), APIField('banner_image'), APIField('banner_cta'), APIField('carousel_images'), APIField('content'), ] max_count = 1 content_panels = Page.content_panels + [ MultiFieldPanel( [InlinePanel("carousel_images", max_num=5, min_num=1, label="Image")], heading="Carousel Images", ), StreamFieldPanel("content"), ] banner_panels = [ MultiFieldPanel( [ FieldPanel("banner_title"), FieldPanel("banner_subtitle"), ImageChooserPanel("banner_image"), PageChooserPanel("banner_cta"), ], heading="Banner Options", ), ] edit_handler = TabbedInterface( [ ObjectList(content_panels, heading='Content'), ObjectList(banner_panels, heading="Banner Settings"), ObjectList(Page.promote_panels, heading='Promotional Stuff'), ObjectList(Page.settings_panels, heading='Settings Stuff'), ] ) @route(r'^subscribe/$') def the_subscribe_page(self, request, *args, **kwargs): context = self.get_context(request, *args, **kwargs) return render(request, 'home/subscribe.html', context)
class LabPage(BasePage): subpage_types = ['RFPPage'] parent_page_types = ['LabIndex'] introduction = models.TextField(blank=True) icon = models.ForeignKey('images.CustomImage', null=True, blank=True, related_name='+', on_delete=models.SET_NULL) lab_type = models.ForeignKey( 'wagtailcore.Page', blank=True, null=True, on_delete=models.SET_NULL, related_name='lab_public', ) lab_link = models.CharField(blank=True, max_length=255, verbose_name='External link', validators=[MailToAndURLValidator()]) link_text = models.CharField( max_length=255, help_text='Text to display on the button for external links', blank=True) body = StreamField(LabBlock()) search_fields = BasePage.search_fields + [ index.SearchField('introduction'), index.SearchField('body') ] content_panels = BasePage.content_panels + [ ImageChooserPanel('icon'), FieldPanel('introduction'), MultiFieldPanel([ PageChooserPanel('lab_type', 'funds.LabType'), FieldRowPanel([ FieldPanel('lab_link'), FieldPanel('link_text'), ]), ], heading='Link for lab application'), StreamFieldPanel('body'), InlinePanel('related_pages', label="Related pages"), ] def get_context(self, request): context = super().get_context(request) context['rfps'] = self.get_children().live().public() return context can_open = True @property def is_open(self): try: return bool(self.lab_type.specific.open_round) except AttributeError: return bool(self.lab_link) def clean(self): if self.lab_type and self.lab_link: raise ValidationError({ 'lab_type': 'Cannot link to both a Lab page and external link', 'lab_link': 'Cannot link to both a Lab page and external link', }) if not self.lab_type and not self.lab_link: raise ValidationError({ 'lab_type': 'Please provide a way for applicants to apply', 'lab_link': 'Please provide a way for applicants to apply', }) if self.lab_type and self.link_text: raise ValidationError({ 'link_text': 'Cannot customise the text for internal lab pages, leave blank', }) if self.lab_link and not self.link_text: raise ValidationError({ 'link_text': 'Please provide some text for the link button', })
class JournalPage(AbstractBase): authors = ParentalManyToManyField(settings.AUTH_USER_MODEL, blank=True, verbose_name=_('Authors'), related_name='author_posts') date = models.DateTimeField(verbose_name="Post date", default=timezone.now) categories = ParentalManyToManyField('journal.JournalCategory', blank=True) tags = ClusterTaggableManager(through='journal.JournalPageTag', blank=True) body = StreamField(BLOCK_TYPES) content_panels = AbstractBase.content_panels + [ FieldPanel('categories', widget=forms.CheckboxSelectMultiple), FieldPanel('tags'), InlinePanel( 'gallery_images', label=_('gallery images'), help_text=_( "Gallery images are displayed along the left side of the page") ), StreamFieldPanel('body'), ] promote_panels = AbstractBase.promote_panels + [ FieldPanel( 'authors', help_text= _("If left blank, this will be set to the currently logged in user" )), FieldPanel('date') ] parent_page_types = ['journal.JournalIndexPage'] search_fields = AbstractBase.search_fields + [ index.SearchField('body'), index.SearchField('authors'), index.FilterField('date') ] @property def journal_index_page(self): return self.get_parent().specific def get_context(self, request, *args, **kwargs): context = super(JournalPage, self).get_context(request, *args, **kwargs) context['journal_index_page'] = self.journal_index_page context['post'] = self return context def save_revision(self, *args, **kwargs): if self.get_parent( ).slug == 'whats-blooming-now' and self.banner is None: # Get the appropriate banner based on the current month season = get_season(date.today()) banner_query = Image.objects.filter().search( "what's blooming now banner " + season) try: banner = banner_query[0] self.banner = banner except IndexError as e: logger.error( '[!] Failed to find seasonal banner for Journal Page: ', e) if not self.authors.all(): self.authors.add(self.owner) return super().save_revision(*args, **kwargs)
feed_image = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) search_fields = Page.search_fields + [ index.SearchField('body'), ] @property def blog_index(self): # Find closest ancestor which is a blog index return self.get_ancestors().type(BlogIndexPage).last() content_panels = [ FieldPanel('title', classname="full title"), FieldPanel('date'), StreamFieldPanel('body'), InlinePanel('carousel_items', label="Carousel items"), InlinePanel('related_links', label="Related links"), ] BlogPage.promote_panels = Page.promote_panels + [ ImageChooserPanel('feed_image'), FieldPanel('tags'), ]
class Events(BasePage): 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 = [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 future events in chronological order""" return get_combined_events(self, start_date__gte=get_past_event_cutoff()) @property def past_events(self): """Return past events in reverse chronological order""" return get_combined_events( self, reverse=True, start_date__lt=datetime.date.today() ) def get_filters(self): from ..topics.models import Topic return { "countries": True, "months": True, "topics": Topic.published_objects.order_by("title"), }
class CompositionEDTF(Model): composition = ParentalKey('CompositionPage', on_delete=CASCADE, unique=True, related_name='date') nat_lang_edtf_string = CharField( verbose_name='Natural Language Date', help_text=('The EDTF date in natural language. This field is help ' 'users who aren\'t familiar with the EDTF. It does not ' 'change how the date is represented.'), max_length=256) edtf_string = CharField( verbose_name='EDTF Date', help_text=mark_safe( 'A date in the <a href="https://www.loc.gov/standards/datetime/" ' 'target="_blank"><strong>Extended Date Time Format</strong></a>'), max_length=256) lower_fuzzy = DateField(editable=False) upper_fuzzy = DateField(editable=False) lower_strict = DateField(editable=False) upper_strict = DateField(editable=False) nat_lang_year = CharField(editable=False, max_length=9) panels = [FieldPanel('edtf_string'), FieldPanel('nat_lang_edtf_string')] def __str__(self): return self.edtf_string def clean(self): try: e = parse_edtf(self.edtf_string) except EDTFParseException: raise ValidationError({ 'edtf_string': '{} is not a valid EDTF string'.format(self.edtf_string) }) self.lower_fuzzy = struct_time_to_date(e.lower_fuzzy()) self.upper_fuzzy = struct_time_to_date(e.upper_fuzzy()) self.lower_strict = struct_time_to_date(e.lower_strict()) self.upper_strict = struct_time_to_date(e.upper_strict()) if self.lower_strict.year != self.upper_strict.year: self.nat_lang_year = '{}-{}'.format(self.lower_strict.year, self.upper_strict.year) else: self.nat_lang_year = str(self.lower_strict.year) def save(self, *args, **kwargs): try: e = parse_edtf(self.edtf_string) except EDTFParseException: raise ValidationError('{} is not a valid EDTF string'.format( self.edtf_string)) self.lower_fuzzy = struct_time_to_date(e.lower_fuzzy()) self.upper_fuzzy = struct_time_to_date(e.upper_fuzzy()) self.lower_strict = struct_time_to_date(e.lower_strict()) self.upper_strict = struct_time_to_date(e.upper_strict()) if self.lower_strict.year != self.upper_strict.year: self.nat_lang_year = '{}-{}'.format(self.lower_strict.year, self.upper_strict.year) else: self.nat_lang_year = str(self.lower_strict.year) super().save(*args, **kwargs)
class HomePage(RoutablePageMixin, Page): """Home page model.""" template = "home/home_page.html" max_count = 1 banner_title = models.CharField(max_length=100, blank=False, null=True) banner_subtitle = RichTextField(features=["bold", "italic"]) banner_image = models.ForeignKey( "wagtailimages.Image", null=True, blank=False, on_delete=models.SET_NULL, related_name="+", ) banner_cta = models.ForeignKey( "wagtailcore.Page", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) content = StreamField([("cta", blocks.CTABlock())], null=True, blank=True) api_fields = [ APIField("banner_title"), APIField("banner_subtitle"), APIField("banner_image"), APIField("banner_cta"), ] content_panels = Page.content_panels + [ MultiFieldPanel( [ FieldPanel("banner_title"), FieldPanel("banner_subtitle"), ImageChooserPanel("banner_image"), PageChooserPanel("banner_cta"), ], heading="Banner Options", ), MultiFieldPanel( [ InlinePanel( "carousel_images", max_num=5, min_num=1, label="Image") ], heading="Carousel Images", ), StreamFieldPanel("content"), ] class Meta: verbose_name = "Home Page" verbose_name_plural = "Home Pages" @route(r'^subscribe/$') def the_subscribe_page(self, request, *args, **kwargs): context = self.get_context(request, *args, **kwargs) return render(request, "home/subscribe.html", context)
class ScorePage(RoutablePageMixin, Page): cover_image = ForeignKey('wagtailimages.Image', null=True, blank=True, on_delete=PROTECT, related_name='cover_image') description = StreamField([('rich_text', RichTextBlock()), ('image', ImageChooserBlock())]) duration = DurationField(null=True, blank=True, help_text='Expects data in the format "HH:MM:SS"') file = FileField( upload_to='scores/', validators=[FileExtensionValidator(allowed_extensions=['pdf', 'zip'])]) preview_score = FileField( upload_to='preview_scores/', validators=[FileExtensionValidator(allowed_extensions=['pdf'])]) preview_score_checksum = CharField(editable=False, max_length=256, blank=True) preview_score_checked = False preview_score_updated = False genre = ParentalManyToManyField(Genre, blank=True, related_name='scores') date = CharField(max_length=256, blank=True) instrumentation = ParentalManyToManyField( 'Instrument', blank=True, help_text='The instrumentation of the compostition.') price = DecimalField(max_digits=6, decimal_places=2) materials = RichTextField( blank=True, features=['bold', 'italic', 'link', 'document-link'], help_text='The materials sent in the PDF file.') def save(self, *args, **kwargs): if self.preview_score_checked: # This was the cause of a subtle bug. Because this method can be # called multiple times during model creation, leaving this flag # set would cause the post save hook to fire multiple times. self.preview_score_updated = False return super().save(*args, **kwargs) h = hashlib.md5() for chunk in iter(lambda: self.preview_score.read(8192), b''): h.update(chunk) self.preview_score.seek(0) checksum = h.hexdigest() if not self.preview_score_checksum == checksum: self.preview_score_checksum = checksum self.preview_score_updated = True self.preview_score_checked = True return super().save(*args, **kwargs) @route(r'^([\w-]+)/$') def get_score_file(self, request, score_link_slug): if request.method == 'GET': item_link = get_object_or_404(OrderItemLink, slug=score_link_slug) if item_link.is_expired(): raise Http404() item_link.access_ip = request.META.get('REMOTE_ADDR', '0.0.0.0') item_link.save() return render(request, "main/score_page_download.html", { 'self': self, 'page': self, }) else: raise Http404() @route(r'^$') def score(self, request): cart_page = ShoppingCartPage.objects.first() if request.method == 'POST': in_cart = toggle_score_in_cart(request, self.pk) return render( request, "main/score_page.html", { 'self': self, 'page': self, 'in_cart': in_cart, 'cart_page': cart_page }) else: return render( request, "main/score_page.html", { 'self': self, 'page': self, 'in_cart': score_in_cart(request, self.pk), 'cart_page': cart_page }) class Meta: verbose_name = "Score Page" content_panels = Page.content_panels + [ FieldPanel('date'), FieldPanel('duration'), FieldPanel('genre'), FieldPanel('instrumentation'), FieldPanel('price'), StreamFieldPanel('description'), FieldPanel('materials'), FieldPanel('file'), FieldPanel('preview_score'), ImageChooserPanel('cover_image') ]
class Topic(BasePage): resource_type = "topic" parent_page_types = ["Topics"] subpage_types = ["Topic"] template = "topic.html" # Content fields description = RichTextField( blank=True, default="", features=RICH_TEXT_FEATURES_SIMPLE, help_text="Optional short text description, max. 400 characters", max_length=400, ) featured = StreamField( StreamBlock( [ ( "post", PageChooserBlock(target_model=( "articles.Article", "externalcontent.ExternalArticle", )), ), ("external_page", FeaturedExternalBlock()), ], max_num=4, required=False, ), null=True, blank=True, help_text="Optional space for featured posts, max. 4", ) tabbed_panels = StreamField( StreamBlock([("panel", TabbedPanelBlock())], max_num=3, required=False), null=True, blank=True, help_text= "Optional tabbed panels for linking out to other resources, max. 3", verbose_name="Tabbed panels", ) latest_articles_count = IntegerField( choices=RESOURCE_COUNT_CHOICES, default=3, help_text="The number of posts to display for this topic.", ) # Card fields card_title = CharField("Title", max_length=140, blank=True, default="") card_description = TextField("Description", max_length=400, blank=True, default="") card_image = ForeignKey( "mozimages.MozImage", null=True, blank=True, on_delete=SET_NULL, related_name="+", verbose_name="Image", ) # Meta icon = FileField( upload_to="topics/icons", blank=True, default="", help_text=("MUST be a black-on-transparent SVG icon ONLY, " "with no bitmap embedded in it."), validators=[check_for_svg_file], ) color = CharField(max_length=14, choices=COLOR_CHOICES, default="blue-40") keywords = ClusterTaggableManager(through=TopicTag, blank=True) # Content panels content_panels = BasePage.content_panels + [ FieldPanel("description"), StreamFieldPanel("featured"), StreamFieldPanel("tabbed_panels"), FieldPanel("latest_articles_count"), MultiFieldPanel( [InlinePanel("people")], heading="People", help_text= "Optional list of people associated with this topic as experts", ), ] # Card panels card_panels = [ FieldPanel("card_title"), FieldPanel("card_description"), ImageChooserPanel("card_image"), ] # Meta panels meta_panels = [ MultiFieldPanel( [ InlinePanel("parent_topics", label="Parent topic(s)"), InlinePanel("child_topics", label="Child topic(s)"), ], heading="Parent/child topic(s)", classname="collapsible collapsed", help_text=("Topics with no parent (i.e. top-level topics) will be " "listed on the home page. Child topics are listed " "on the parent topic’s page."), ), MultiFieldPanel( [FieldPanel("icon"), FieldPanel("color")], heading="Theme", help_text=( "Theme settings used on topic page and any tagged content. " "For example, a post tagged with this topic " "will use the color specified here as its accent color."), ), MultiFieldPanel( [ FieldPanel("seo_title"), FieldPanel("search_description"), ImageChooserPanel("social_image"), FieldPanel("keywords"), ], heading="SEO", help_text=("Optional fields to override the default " "title and description for SEO purposes"), ), ] # Settings panels settings_panels = [FieldPanel("slug"), FieldPanel("show_in_menus")] # Tabs edit_handler = TabbedInterface([ ObjectList(content_panels, heading="Content"), ObjectList(card_panels, heading="Card"), ObjectList(meta_panels, heading="Meta"), ObjectList(settings_panels, heading="Settings", classname="settings"), ]) @property def articles(self): return get_combined_articles(self, topics__topic__pk=self.pk) @property def events(self): """Return upcoming events for this topic, ignoring events in the past, ordered by start date""" return get_combined_events(self, topics__topic__pk=self.pk, start_date__gte=datetime.datetime.now()) @property def experts(self): """Return Person instances for topic experts""" return [person.person for person in self.people.all()] @property def videos(self): """Return the latest videos and external videos for this topic. """ return get_combined_videos(self, topics__topic__pk=self.pk) @property def color_value(self): return dict(COLOR_VALUES)[self.color] @property def subtopics(self): return [topic.child for topic in self.child_topics.all()]
class ParticipatePage2(PrimaryPage): parent_page_types = ['Homepage'] template = 'wagtailpages/static/participate_page2.html' ctaHero = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='primary_hero_participate', verbose_name='Primary Hero Image', ) ctaHeroHeader = models.TextField(blank=True, ) ctaHeroSubhead = RichTextField( features=[ 'bold', 'italic', 'link', ], blank=True, ) ctaCommitment = models.TextField(blank=True, ) ctaButtonTitle = models.CharField( verbose_name='Button Text', max_length=250, blank=True, ) ctaButtonURL = models.TextField( verbose_name='Button URL', blank=True, ) ctaHero2 = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='primary_hero_participate2', verbose_name='Primary Hero Image', ) ctaHeroHeader2 = models.TextField(blank=True, ) ctaHeroSubhead2 = RichTextField( features=[ 'bold', 'italic', 'link', ], blank=True, ) ctaCommitment2 = models.TextField(blank=True, ) ctaButtonTitle2 = models.CharField( verbose_name='Button Text', max_length=250, blank=True, ) ctaButtonURL2 = models.TextField( verbose_name='Button URL', blank=True, ) ctaHero3 = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='primary_hero_participate3', verbose_name='Primary Hero Image', ) ctaHeroHeader3 = models.TextField(blank=True, ) ctaHeroSubhead3 = RichTextField( features=[ 'bold', 'italic', 'link', ], blank=True, ) ctaCommitment3 = models.TextField(blank=True, ) ctaFacebook3 = models.TextField(blank=True, ) ctaTwitter3 = models.TextField(blank=True, ) ctaEmailShareBody3 = models.TextField(blank=True, ) ctaEmailShareSubject3 = models.TextField(blank=True, ) h2 = models.TextField(blank=True, ) h2Subheader = models.TextField( blank=True, verbose_name='H2 Subheader', ) content_panels = Page.content_panels + [ MultiFieldPanel([ ImageChooserPanel('ctaHero'), FieldPanel('ctaHeroHeader'), FieldPanel('ctaHeroSubhead'), FieldPanel('ctaCommitment'), FieldPanel('ctaButtonTitle'), FieldPanel('ctaButtonURL'), ], heading="Primary CTA"), FieldPanel('h2'), FieldPanel('h2Subheader'), InlinePanel( 'featured_highlights', label='Highlights Group 1', max_num=3), MultiFieldPanel([ ImageChooserPanel('ctaHero2'), FieldPanel('ctaHeroHeader2'), FieldPanel('ctaHeroSubhead2'), FieldPanel('ctaCommitment2'), FieldPanel('ctaButtonTitle2'), FieldPanel('ctaButtonURL2'), ], heading="CTA 2"), InlinePanel( 'featured_highlights2', label='Highlights Group 2', max_num=6), MultiFieldPanel([ ImageChooserPanel('ctaHero3'), FieldPanel('ctaHeroHeader3'), FieldPanel('ctaHeroSubhead3'), FieldPanel('ctaCommitment3'), FieldPanel('ctaFacebook3'), FieldPanel('ctaTwitter3'), FieldPanel('ctaEmailShareSubject3'), FieldPanel('ctaEmailShareBody3'), ], heading="CTA 3"), InlinePanel('cta4', label='CTA Group 4', max_num=3), ]
class Answer(Page): template = 'cms/answer_detail.html' # Determines type and whether its highlighted in overview list type = models.CharField( choices=[('answer', 'Antwoord'), ('column', 'Column')], max_length=100, default='answer', help_text= _('Choose between answer or discussion piece with a more prominent look' )) featured = models.BooleanField(default=False) content = RichTextField(blank=True) excerpt = models.CharField( verbose_name=_('Short description'), max_length=255, blank=False, null=True, help_text=_( 'This helps with search engines and when sharing on social media'), ) introduction = TextField( verbose_name=_('Introduction'), default='', blank=True, null=True, help_text=_( 'This text is displayed above the tags, useful as a TLDR section'), ) tags = ClusterTaggableManager(through=AnswerTag, blank=True) social_image = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', help_text= _('This is the image that will be displayed when sharing on social media' ), ) # Freeform content of answer page_content = StreamField([ ('richtext', AnswerRichTextBlock()), ('image', AnswerImageBlock()), ('quote', QuoteBlock()), ]) # Which experts and how was this answered? answer_origin = StreamField([('origin', AnswerOriginBlock())], blank=True) # Related items related_items = StreamField([('related_items', RelatedItemsBlock())], blank=True) parent_page_types = ['AnswerIndexPage'] content_panels = Page.content_panels + [ FieldPanel('type'), FieldPanel('featured', heading=_("Show this answer on the home page")), FieldPanel( 'excerpt', classname='full', ), FieldPanel('introduction', classname='full'), MultiFieldPanel([ InlinePanel('answer_category_relationship', label=_('Categorie(n)'), panels=None, min_num=1) ], heading=_('Categorie(s)')), FieldPanel( 'tags', heading= "Please use tags with a maximum length of 16 characters per single word to avoid overlap in the mobile view." ), MultiFieldPanel([ InlinePanel('answer_expert_relationship', label=_('Expert(s)'), panels=None, min_num=1) ], heading=_('Expert(s)')), StreamFieldPanel('page_content'), StreamFieldPanel('answer_origin'), StreamFieldPanel('related_items'), ImageChooserPanel( 'social_image', help_text=_('Image to be used when sharing on social media')), ] search_fields = Page.search_fields + [ index.SearchField('page_content'), ] @property def experts(self): experts = [n.expert for n in self.answer_expert_relationship.all()] return experts @property def categories(self): categories = [ n.category for n in self.answer_category_relationship.all() ] return categories @property def get_tags(self): tags = self.tags.all() for tag in tags: tag.url = '/' + '/'.join( s.strip('/') for s in [self.get_parent().url, 'tags', tag.slug]) return tags def get_references(self): """ Build reference list, in the order Wagtail returns them. ### , alphabetically to sort of comply with standards TODO: References for articles can be separated from the origin and make them a proper ListBlock that can be handled by editors as they see fit. Having the references within a StreamField of 'origins' seems counter intuitive. """ ref_list = [] try: component = self.answer_origin[0] except IndexError: return ref_list # Access streamfield elements for element in component.value['sources']: ref_list.append({ 'text': element['reference_text'], 'url': element['url_or_doi'], }) # Sort by text starting letter, best we can do for now # ref_list.sort(key=lambda e: e['text']) return ref_list def get_primary_expert(self): """ Gets the first expert associated with this answer if it exists. """ try: first = self.experts[0] except IndexError: return _('Unknown') else: return first def get_all_categories(self): return [{ 'title': c.name, 'url': c.get_prefiltered_search_params() } for c in self.categories] def get_card_data(self): return { 'title': self.title, 'url': self.url, 'author': self.get_primary_expert(), 'categories': self.get_all_categories(), 'type': 'answer' } def get_as_overview_row_card(self): if self.type == 'answer': return render_to_string('core/includes/answer_block.html', context=self.get_card_data()) else: # It's a column return render_to_string('core/includes/column_block.html', context=self.get_card_data()) def get_as_home_row_card(self): return render_to_string('core/includes/answer_home_block.html', context=self.get_card_data()) def get_as_related_row_card(self): return render_to_string('core/includes/related_item_block.html', context=self.get_card_data()) def get_context(self, request, *args, **kwargs): context = super(Answer, self).get_context(request, *args, **kwargs) categories = AnswerCategory.objects.all() context.update({ 'categories': categories, 'answers_page': AnswerIndexPage.objects.first().url, 'experts_page': ExpertIndexPage.objects.first(), }) return context class Meta: ordering = [ '-first_published_at', ]
class ArticleSeriesPage( BasicPageAbstract, ContentPage, FeatureablePageAbstract, FromTheArchivesPageAbstract, ShareablePageAbstract, ThemeablePageAbstract, ): credits = RichTextField( blank=True, features=[ 'bold', 'italic', 'link', 'name', ], ) credits_stream_field = StreamField( [('title', StructBlock([ ('title', CharBlock()), ('people', StreamBlock([('name', CharBlock())])), ]))], blank=True, ) credits_artwork = models.CharField( max_length=255, blank=True, ) featured_items = StreamField( [ ('featured_item', PageChooserBlock( required=True, page_type=[ 'articles.ArticlePage', 'multimedia.MultimediaPage' ], )), ], blank=True, ) image_banner = models.ForeignKey( 'images.CigionlineImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Banner Image', ) image_banner_small = models.ForeignKey('images.CigionlineImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Banner Image Small') image_poster = models.ForeignKey( 'images.CigionlineImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Poster image', help_text= 'A poster image which will be used in the highlights section of the homepage.', ) short_description = RichTextField( blank=True, null=False, features=['bold', 'italic', 'link'], ) series_items_description = RichTextField( blank=True, null=True, features=['bold', 'italic', 'link'], ) series_videos_description = RichTextField( blank=True, null=True, features=['bold', 'italic', 'link'], help_text= 'To be displayed on video/multimedia pages of the series in place of Series Items Description' ) series_items_disclaimer = RichTextField( blank=True, null=True, features=['bold', 'italic', 'link'], ) video_banner = models.ForeignKey( 'wagtailmedia.Media', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Banner Video', ) @property def image_poster_caption(self): return self.image_poster.caption @property def image_poster_url(self): return self.image_poster.get_rendition('fill-672x895').url @property def article_series_items(self): return self.series_items.prefetch_related( 'content_page', 'content_page__authors__author', ).all() # Reference field for the Drupal-Wagtail migrator. Can be removed after. drupal_node_id = models.IntegerField(blank=True, null=True) def get_template(self, request, *args, **kwargs): standard_template = super(ArticleSeriesPage, self).get_template(request, *args, **kwargs) if self.theme: return f'themes/{self.get_theme_dir()}/article_series_page.html' return standard_template content_panels = [ BasicPageAbstract.title_panel, MultiFieldPanel( [ FieldPanel('short_description'), StreamFieldPanel('body'), ], heading='Body', classname='collapsible collapsed', ), MultiFieldPanel( [ FieldPanel('publishing_date'), ], heading='General Information', classname='collapsible collapsed', ), MultiFieldPanel( [ FieldPanel('series_items_description'), FieldPanel('series_videos_description'), FieldPanel('series_items_disclaimer'), InlinePanel('series_items'), ], heading='Series Items', classname='collapsible collapsed', ), MultiFieldPanel( [ FieldPanel('credits'), FieldPanel('credits_artwork'), StreamFieldPanel('credits_stream_field'), ], heading='Credits', classname='collapsible collapsed', ), MultiFieldPanel( [ ImageChooserPanel('image_hero'), ImageChooserPanel('image_banner'), ImageChooserPanel('image_banner_small'), ImageChooserPanel('image_poster'), ], heading='Image', classname='collapsible collapsed', ), MultiFieldPanel( [ MediaChooserPanel('video_banner'), ], heading='Media', classname='collapsible collapsed', ), MultiFieldPanel( [ StreamFieldPanel('featured_items'), ], heading='Featured Series Items', classname='collapsible collapsed', ), MultiFieldPanel( [ FieldPanel('topics'), ], heading='Related', classname='collapsible collapsed', ), ] promote_panels = Page.promote_panels + [ FeatureablePageAbstract.feature_panel, ShareablePageAbstract.social_panel, SearchablePageAbstract.search_panel, ] settings_panels = Page.settings_panels + [ ThemeablePageAbstract.theme_panel, ] search_fields = Page.search_fields \ + BasicPageAbstract.search_fields \ + ContentPage.search_fields parent_page_types = ['home.HomePage'] subpage_types = [] templates = 'articles/article_series_page.html' @property def series_contributors_by_article(self): series_contributors = [] item_people = set() for series_item in self.article_series_items: people = series_item.content_page.authors.all() people_string = '' for person in people: person_string = person.author.title people_string += person_string # Add each person as well so if there's an article with just # a single author who's already been in another article in # collaboration, then we won't add their name to the list # again. if len(people) > 1: item_people.add(person_string) if people_string not in item_people: series_contributors.append({ 'item': series_item.content_page, 'contributors': people }) item_people.add(people_string) return series_contributors @property def series_contributors(self): series_contributors = [] item_people = set() for series_item in self.article_series_items: people = series_item.content_page.authors.all() for person in people: if person.author.title not in item_people: series_contributors.append({ 'id': person.author.id, 'title': person.author.title, 'url': person.author.url, }) item_people.add(person.author.title) return series_contributors @property def series_contributors_by_person(self): # Series contributors ordered by last name series_contributors = [] item_people = set() for series_item in self.article_series_items: people = series_item.content_page.authors.all() # Skip items that have more than 2 authors/speakers. For # example, in the After COVID series, there is an introductory # video with many authors. if len(people) > 2: continue else: for person in people: if person.author.title not in item_people: series_contributors.append({ 'item': series_item.content_page, 'contributors': [person.author], 'last_name': person.author.last_name, }) item_people.add(person.author.title) series_contributors.sort(key=lambda x: x['last_name']) return series_contributors @property def series_authors(self): series_authors = [] series_people = set() for series_item in self.article_series_items: people = series_item.content_page.authors.all() for person in people: if person.author.title not in series_people: series_authors.append(person.author) series_people.add(person.author.title) return series_authors class Meta: verbose_name = 'Opinion Series' verbose_name_plural = 'Opinion Series'
class IndexPage(RoutablePageMixin, Page): custom_title = models.CharField(max_length=100, blank=False, null=False, help_text="Overwrites default title") content_panels = Page.content_panels + [FieldPanel("custom_title")] def get_context(self, request, *args, **kwargs): context = super().get_context(request, *args, **kwargs) context["posts"] = IndexDetailPage.objects.live().public().order_by( Lower("pub_date")) context["categories"] = IndexCategory.objects.all() json_list = list(context["posts"].values( 'slug', 'title', 'author_founder', 'rownum', 'pub_date', 'end_date', 'about', 'location', 'external_link', 'external_link_two', 'images_list', 'page_ptr_id')) context['json_dict'] = json.dumps(json_list) context["image_entries"] = [] for index in context["posts"]: for c in index.images_list.all(): context["image_entries"].append({ "slug": index.slug, "img_name": str(c) }) context['json_img_dict'] = json.dumps(list(context["image_entries"])) return context @route(r"^orderby/(?P<order>[-\w]+)/$", name="orderby_view") @never_cache def orderby_view(self, request, order): context = self.get_context(request) try: orderby = context["posts"].order_by(Lower(order)) except Exception: orderby = None if orderby is None: pass context["posts"] = orderby # clear index page only key = make_template_fragment_key("preview_index") print(key) cache.delete(key) print(orderby) # cache.clear() return render(request, "index/index_page.html", context) @route(r"^tag/(?P<cat_slug>[-\w]+)/$", name="tag_view") def tag_view(self, request, cat_slug): context = self.get_context(request) try: category = IndexCategory.objects.get(slug=cat_slug) render() except Exception: category = None if category is None: pass context["posts"] = IndexDetailPage.objects.live().public().filter( categories__in=[category]) print(context["posts"]) return render(request, "index/index_page.html", context)
class OrganisationSettings(BaseSetting): address = fields.RichTextField() contacts = fields.RichTextField() panels = [FieldPanel('address'), FieldPanel('contacts')]
class IndexDetailPage(Page): about = MarkdownField(null=True, blank=True) sourceforabouttext = models.CharField("Source for about text", max_length=255, null=True, blank=True) categories = ParentalManyToManyField("index.IndexCategory", blank=True) pub_date = models.PositiveSmallIntegerField("Date Published / Created", null=True, blank=True) end_date = models.PositiveSmallIntegerField("End Date", null=True, blank=True) author_founder = models.CharField("Author/Founder", max_length=500, null=True, blank=True) contributed_by = models.CharField("Contributed By", max_length=500, null=True, blank=True) external_link = models.URLField(null=True, blank=True) external_link_two = models.URLField(null=True, blank=True) autoincrement_num = models.PositiveSmallIntegerField(null=True, blank=True) rownum = models.PositiveSmallIntegerField(null=True, blank=True) location = models.CharField("location", max_length=255, null=True, blank=True) # slug = models.SlugField(verbose_name=_('slug'), allow_unicode=True, max_length=255) search_fields = Page.search_fields + [ index.SearchField('title'), index.SearchField('author_founder'), index.SearchField('about'), index.SearchField('contributed_by'), index.SearchField('location'), index.SearchField('pub_date'), index.SearchField('end_date'), ] content_panels = Page.content_panels + [ MarkdownPanel('about', classname="full"), MultiFieldPanel([ FieldRowPanel([ FieldPanel('pub_date'), FieldPanel('end_date'), ]), FieldRowPanel([ FieldPanel('author_founder'), FieldPanel('location'), ]), FieldRowPanel([ FieldPanel('external_link'), FieldPanel('external_link_two'), ]), FieldRowPanel([ FieldPanel('contributed_by'), ]), ], 'Details'), MultiFieldPanel( [ InlinePanel('images_list', label='Image'), ], heading="Image(s)", ), MultiFieldPanel( [FieldPanel("categories", widget=forms.CheckboxSelectMultiple)], heading="Categories"), MultiFieldPanel( [ InlinePanel('collections_list', label='Curator'), ], heading="Curator(s)", ), ] promote_panels = [] class Meta: # noqa verbose_name = "Index Detail Page" verbose_name_plural = "Index Detail Pages"
class MetaTerm(index.Indexed, MP_Node): """ Hierarchal "Meta" terms """ name = models.CharField( max_length=50, unique=True, help_text='Keep the name short, ideally one word.' ) is_archived = models.BooleanField( default=False, verbose_name=_("Archived"), help_text='Archived terms can be viewed but not set on content.' ) filter_on_dashboard = models.BooleanField( default=True, help_text='Make available to filter on dashboard' ) available_to_applicants = models.BooleanField( default=False, help_text='Make available to applicants' ) help_text = RichTextField(features=[ 'h2', 'h3', 'bold', 'italic', 'link', 'hr', 'ol', 'ul'], blank=True) # node tree specific fields and attributes node_order_index = models.IntegerField(blank=True, default=0, editable=False) node_child_verbose_name = 'child' # important: node_order_by should NOT be changed after first Node created node_order_by = ['node_order_index', 'name'] panels = [ FieldPanel('name'), FieldPanel('parent'), MultiFieldPanel( [ FieldPanel('is_archived'), FieldPanel('filter_on_dashboard'), FieldPanel('available_to_applicants'), FieldPanel('help_text'), ], heading="Options", ), ] def get_as_listing_header(self): depth = self.get_depth() rendered = render_to_string( 'categories/admin/includes/meta_term_list_header.html', { 'depth': depth, 'depth_minus_1': depth - 1, 'is_root': self.is_root(), 'name': self.name, 'is_archived': self.is_archived, } ) return rendered get_as_listing_header.short_description = 'Name' get_as_listing_header.admin_order_field = 'name' def get_parent(self, *args, **kwargs): return super().get_parent(*args, **kwargs) get_parent.short_description = 'Parent' search_fields = [ index.SearchField('name', partial_match=True), ] def delete(self): if self.is_root(): raise PermissionDenied('Cannot delete root term.') else: super().delete() @classmethod def get_root_descendants(cls): # Meta terms queryset without Root node root_node = cls.get_first_root_node() if root_node: return root_node.get_descendants() return cls.objects.none() def __str__(self): return self.name class Meta: verbose_name = 'Meta Term' verbose_name_plural = 'Meta Terms'
class TriggerCondition(Orderable): trigger = ParentalKey(Trigger, on_delete=models.CASCADE, related_name="conditions") variable = models.CharField(max_length=255, null=True, blank=False) CONDITION_EXACT_MATCH = "exact_match" CONDITION_NOT_EXACT_MATCH = "not_exact_match" CONDITION_CONTAINS = "contains" CONDITION_NOT_CONTAINS = "not_contains" CONDITION_STARTS_WITH = "starts_with" CONDITION_NOT_STARTS_WITH = "not_starts_with" CONDITION_ENDS_WITH = "ends_with" CONDITION_NOT_ENDS_WITH = "not_ends_with" CONDITION_REGEX_MATCH = "regex_match" CONDITION_NOT_REGEX_MATCH = "not_regex_match" CONDITION_REGEX_IMATCH = "regex_imatch" CONDITION_NOT_REGEX_IMATCH = "not_regex_imatch" CONDITION_LT = "lower_than" CONDITION_LTE = "lower_than_equal" CONDITION_GT = "greater_than" CONDITION_GTE = "greater_than_equal" CONDITION_CHOICES = ( ( _("Text"), ( (CONDITION_EXACT_MATCH, _("exact match")), (CONDITION_NOT_EXACT_MATCH, _("not exact match")), (CONDITION_CONTAINS, _("contains")), (CONDITION_NOT_CONTAINS, _("does not contain")), (CONDITION_STARTS_WITH, _("starts with")), (CONDITION_NOT_STARTS_WITH, _("does not start with")), (CONDITION_ENDS_WITH, _("ends with")), (CONDITION_NOT_ENDS_WITH, _("does not end with")), ), ), ( _("Regex"), ( (CONDITION_REGEX_MATCH, _("matches regex")), (CONDITION_NOT_REGEX_MATCH, _("does not match regex")), (CONDITION_REGEX_IMATCH, _("matches regex (case insensitive)")), ( CONDITION_NOT_REGEX_IMATCH, _("does not match regex (case insensitive)"), ), ), ), ( _("Numbers"), ( (CONDITION_LT, _("is lower than")), (CONDITION_LTE, _("is lower than or equal to")), (CONDITION_GT, _("is greater than")), (CONDITION_GTE, _("is greater than or equal to")), ), ), ) condition_type = models.CharField(max_length=255, choices=CONDITION_CHOICES, default=CONDITION_CONTAINS) value = models.CharField(max_length=255) panels = [ FieldPanel("variable", widget=VariableSelect), FieldPanel("condition_type"), FieldPanel("value"), ] def validate(self, context) -> bool: if self.variable in context: variable = context.get(self.variable, None) validator = getattr(self, self.condition_type) return validator(variable, self.value) return False # Text @staticmethod def exact_match(variable, value): return str(value) == str(variable) def not_exact_match(self, *args, **kwargs): return not self.exact_match(*args, **kwargs) @staticmethod def contains(variable, value): return str(value) in str(variable) def not_contains(self, *args, **kwargs): return not self.contains(*args, **kwargs) @staticmethod def starts_with(variable, value): return str(variable).startswith(str(value)) def not_starts_with(self, *args, **kwargs): return not self.starts_with(*args, **kwargs) @staticmethod def ends_with(variable, value): return str(variable).endswith(str(value)) def not_ends_with(self, *args, **kwargs): return not self.ends_with(*args, **kwargs) # Regex @staticmethod def regex_match(variable, value): return re.match(value, str(variable)) is not None def not_regex_match(self, *args, **kwargs): return not self.regex_match(*args, **kwargs) @staticmethod def regex_imatch(variable, value): return re.match(value, str(variable), re.IGNORECASE) is not None def not_regex_imatch(self, *args, **kwargs): return not self.regex_imatch(*args, **kwargs) # Numbers @staticmethod def lower_than(variable, value): return float(variable) < float(value) @staticmethod def lower_than_equal(variable, value): return float(variable) <= float(value) @staticmethod def greater_than(variable, value): return float(variable) > float(value) @staticmethod def greater_than_equal(variable, value): return float(variable) >= float(value)
date_to = models.DateField( "End date", null=True, blank=True, help_text="Not required if event is on a single day", ) time_from = models.TimeField("Start time", null=True, blank=True) time_to = models.TimeField("End time", null=True, blank=True) location = models.CharField(max_length=255) body = RichTextField(blank=True) cost = models.CharField(max_length=255) signup_link = models.URLField(blank=True) EventPage.content_panels = [ FieldPanel("title", classname="full title"), FieldPanel("date_from"), FieldPanel("date_to"), FieldPanel("time_from"), FieldPanel("time_to"), FieldPanel("location"), FieldPanel("cost"), FieldPanel("signup_link"), FieldPanel("body", classname="full"), InlinePanel("related_media", label="Related media"), ] class TestMediaBlock(AbstractMediaChooserBlock): def render_basic(self, value, context=None): if not value:
class Trigger(ClusterableModel): name = models.CharField(max_length=100, unique=True) slug = models.SlugField(max_length=100, unique=True, editable=False) description = models.TextField(null=True, blank=True) active = models.BooleanField( default=True, help_text=_("Uncheck to disable this trigger from firing.")) TYPE_CLICK_ALL_ELEMENTS = "click_all_elements" TYPE_CLICK_SOME_ELEMENTS = "click_some_elements+" TYPE_VISIBILITY_ONCE_PER_PAGE = "visibility_once_per_page+" TYPE_VISIBILITY_ONCE_PER_ELEMENT = "visibility_once_per_element+" TYPE_VISIBILITY_RECURRING = "visibility_recurring+" TYPE_FORM_SUBMIT = "form_submit" TYPE_HISTORY_CHANGE = "history_change" TYPE_JAVASCRIPT_ERROR = "javascript_error" TYPE_SCROLL_VERTICAL = "scroll_vertical+" TYPE_SCROLL_HORIZONTAL = "scroll_horizontal+" TYPE_TIMER_TIMEOUT = "timer_timeout+" TYPE_TIMER_INTERVAL = "timer_interval+" TYPE_CHOICES = ( (TYPE_FORM_SUBMIT, _("Form submit")), (TYPE_HISTORY_CHANGE, _("History change")), (TYPE_JAVASCRIPT_ERROR, _("JavaScript error")), ( _("Click"), ( (TYPE_CLICK_ALL_ELEMENTS, _("Click on all elements")), (TYPE_CLICK_SOME_ELEMENTS, _("Click on some elements")), ), ), ( _("Visibility"), # TODO: Advanced options... ( (TYPE_VISIBILITY_ONCE_PER_PAGE, _("Monitor once per page")), (TYPE_VISIBILITY_ONCE_PER_ELEMENT, _("Monitor once per element")), (TYPE_VISIBILITY_RECURRING, _("Monitor recurringingly")), ), ), ( _("Scroll"), ( (TYPE_SCROLL_VERTICAL, _("Scroll vertical")), (TYPE_SCROLL_HORIZONTAL, _("Scroll horizontal")), ), ), ( _("Timer"), ( (TYPE_TIMER_TIMEOUT, _("Timer with timeout")), (TYPE_TIMER_INTERVAL, _("Timer with interval")), ), ), ) trigger_type = models.CharField(max_length=255, choices=TYPE_CHOICES, default=TYPE_FORM_SUBMIT) value = models.CharField( max_length=255, null=True, blank=True, help_text=mark_safe( _("<b>Click:</b> the query selector of the element(s).<br/>" "<b>Visibility:</b> the query selector of the element(s).<br/>" "<b>Scroll:</b> the distance after which to trigger as percentage.<br/>" "<b>Timer:</b> the time in milliseconds after which to trigger.") ), ) tags = models.ManyToManyField( Tag, help_text=_("The tags to include when this trigger is fired.")) objects = TriggerQuerySet.as_manager() panels = [ FieldPanel("name", classname="full title"), FieldPanel("description", classname="full"), MultiFieldPanel( [ FieldPanel("trigger_type"), FieldPanel("value"), FieldPanel("active") ], heading=_("Configuration"), ), InlinePanel("conditions", label=_("Conditions")), FieldPanel("tags", widget=CheckboxSelectMultiple), ] def as_dict(self): return { "slug": self.slug, "type": re.sub(r"[+]", "", self.trigger_type), "value": self.get_value(), } def get_value(self): numbered = [ self.TYPE_SCROLL_VERTICAL, self.TYPE_SCROLL_HORIZONTAL, self.TYPE_TIMER_TIMEOUT, self.TYPE_TIMER_INTERVAL, ] if self.trigger_type in numbered: return int(self.value) return self.value def validate(self, context) -> bool: if self.conditions.count() == 0: return True return all([ condition.validate(context) for condition in self.conditions.all() ]) def clean(self): super().clean() self.slug = slugify(self.name) if self.trigger_type.endswith("+") and not self.value: raise ValidationError( _("A value is required for this trigger type.")) elif self.value and not self.trigger_type.endswith("+"): raise ValidationError( _("A value is not allowed for this trigger type.")) def __str__(self): return self.name
class HomePage(BasePage): template = 'patterns/pages/home/home_page.html' # Only allow creating HomePages at the root level parent_page_types = ['wagtailcore.Page'] subpage_types = [ 'news.NewsIndex', 'standardpages.StandardPage', 'articles.ArticleIndex', 'people.PersonIndex' ] hero_title = models.CharField(null=True, blank=False, max_length=80) hero_introduction = models.CharField(blank=False, max_length=255) hero_button_text = models.CharField(blank=True, max_length=55) hero_button_link = models.ForeignKey( 'wagtailcore.Page', on_delete=models.SET_NULL, blank=True, null=True, related_name='+', ) featured_image = models.ForeignKey( 'images.CustomImage', null=True, blank=False, related_name='+', on_delete=models.SET_NULL, ) search_fields = BasePage.search_fields + [ index.SearchField('hero_introduction'), ] articles_title = models.CharField(null=True, blank=True, max_length=150) articles_link = models.ForeignKey( 'wagtailcore.Page', on_delete=models.SET_NULL, blank=True, null=True, related_name='+', ) articles_linktext = models.CharField(null=True, blank=True, max_length=80) featured_pages_title = models.CharField(null=True, blank=True, max_length=150) pages_link = models.ForeignKey( 'wagtailcore.Page', on_delete=models.SET_NULL, blank=True, null=True, related_name='+', ) pages_linktext = models.CharField(null=True, blank=True, max_length=80) news_title = models.CharField(null=True, blank=True, max_length=150) news_link = models.ForeignKey( 'wagtailcore.Page', on_delete=models.SET_NULL, blank=True, null=True, related_name='+', ) news_linktext = models.CharField(null=True, blank=True, max_length=80) content_panels = BasePage.content_panels + [ MultiFieldPanel( [ FieldPanel('hero_title'), FieldPanel('hero_introduction'), FieldPanel('hero_button_text'), PageChooserPanel('hero_button_link'), ImageChooserPanel('featured_image'), ], heading="Hero Section", ), InlinePanel('featured_pages', label="Featured Pages", max_num=6, heading='Featured Pages, Maximum 6'), MultiFieldPanel( [ FieldPanel('articles_title'), PageChooserPanel('articles_link'), FieldPanel('featured_pages_title'), PageChooserPanel('pages_link'), FieldPanel('news_title'), PageChooserPanel('news_link'), ], heading="Front page sections", ), ] def get_context(self, request, *args, **kwargs): context = super().get_context(request, *args, **kwargs) context['articles_title'] = self.articles_title context['articles_link'] = self.articles_link context['articles_linktext'] = self.articles_linktext context['featured_pages_title'] = self.featured_pages_title context['pages_link'] = self.pages_link context['pages_linktext'] = self.pages_linktext context['news_title'] = self.news_title context['news_link'] = self.news_link context['news_linktext'] = self.news_linktext if ArticlePage.objects.live().public().count() >= 1: latest_articles = ArticlePage.objects.live().public().order_by( '-first_published_at') context['article_top'] = latest_articles[0] context['articles_row_1'] = latest_articles[1:4] context['articles_row_2'] = latest_articles[4:7] context['featured_row_1'] = self.featured_pages.all()[:3] context['featured_row_2'] = self.featured_pages.all()[3:6] if NewsPage.objects.live().public().count() >= 1: latest_news = NewsPage.objects.live().public().order_by( '-first_published_at') context['latest_news'] = latest_news[0:8] return context
class TestFieldPanel(TestCase): def setUp(self): self.EventPageForm = get_form_for_model( EventPage, form_class=WagtailAdminPageForm, formsets=[]) self.event = EventPage(title='Abergavenny sheepdog trials', date_from=date(2014, 7, 20), date_to=date(2014, 7, 21)) self.EndDatePanel = FieldPanel('date_to', classname='full-width').bind_to_model(EventPage) def test_render_as_object(self): form = self.EventPageForm( {'title': 'Pontypridd sheepdog trials', 'date_from': '2014-07-20', 'date_to': '2014-07-22'}, instance=self.event) form.is_valid() field_panel = self.EndDatePanel( instance=self.event, form=form ) result = field_panel.render_as_object() # check that label appears as a legend in the 'object' wrapper, # but not as a field label (that would be provided by ObjectList instead) self.assertIn('<legend>End date</legend>', result) self.assertNotIn('<label for="id_date_to">End date:</label>', result) # check that help text is not included (it's provided by ObjectList instead) self.assertNotIn('Not required if event is on a single day', result) # check that the populated form field is included self.assertIn('value="2014-07-22"', result) # there should be no errors on this field self.assertNotIn('<p class="error-message">', result) def test_render_as_field(self): form = self.EventPageForm( {'title': 'Pontypridd sheepdog trials', 'date_from': '2014-07-20', 'date_to': '2014-07-22'}, instance=self.event) form.is_valid() field_panel = self.EndDatePanel( instance=self.event, form=form ) result = field_panel.render_as_field() # check that label is output in the 'field' style self.assertIn('<label for="id_date_to">End date:</label>', result) self.assertNotIn('<legend>End date</legend>', result) # check that help text is included self.assertIn('Not required if event is on a single day', result) # check that the populated form field is included self.assertIn('value="2014-07-22"', result) # there should be no errors on this field self.assertNotIn('<p class="error-message">', result) def test_required_fields(self): result = self.EndDatePanel.required_fields() self.assertEqual(result, ['date_to']) def test_error_message_is_rendered(self): form = self.EventPageForm( {'title': 'Pontypridd sheepdog trials', 'date_from': '2014-07-20', 'date_to': '2014-07-33'}, instance=self.event) form.is_valid() field_panel = self.EndDatePanel( instance=self.event, form=form ) result = field_panel.render_as_field() self.assertIn('<p class="error-message">', result) self.assertIn('<span>Enter a valid date.</span>', result)