class AbstractBasePage(Page): """A base for all page types.""" heading = models.CharField(max_length=255, null=True, blank=True) excerpt = models.TextField(null=True, blank=True) translation_fields = ["heading", "excerpt"] search_fields = [ FilterField('live'), SearchField('heading'), SearchField('excerpt'), ] class Meta(object): """Meta data for the class.""" abstract = True def clean(self): """Override clean to remove trailing dashes from slugs with whitespaces.""" for lang in ['en', 'fr', 'es', 'pt']: slug_field = 'slug_{}'.format(lang) slug = getattr(self, slug_field) if slug: slug_clean = re.sub(r'[^\w\s-]', '', slug).strip("-").lower() setattr(self, slug_field, slug_clean) super(AbstractBasePage, self).clean()
class StandardPage(Page): introductory_headline = models.TextField(help_text='Introduce the topic of this page in 1-3 sentences.', blank=True) overview = RichTextField(help_text='Give a general overview of what this topic is about. Limit yourself to 3 paragraphs.', blank=True) body = StreamField(ContentStreamBlock()) listing_abstract = models.TextField(help_text='Give a brief blurb (about 1 sentence) of what this topic is about. It will appear on other pages that refer to this one.', blank=True) @property def parent(self): return self.get_parent().specific search_fields = Page.search_fields + [ SearchField('introductory_headline'), SearchField('overview'), SearchField('listing_abstract'), SearchField('body') ] content_panels = Page.content_panels + [ FieldPanel('listing_abstract'), FieldPanel('introductory_headline'), FieldPanel('overview'), StreamFieldPanel('body'), ] parent_page_types = [ 'home.HomePage', 'standard.StandardPage', 'standard.StandardIndexPage', 'standard.SectionPage' ] subpage_types = [ 'standard.StandardPage', 'standard.SectionPage' ]
class Article(Indexed, Model): title = CharField(max_length=200) body = RichTextField() search_fields = [ SearchField("title", partial_match=True, boost=2), SearchField("body"), ]
class AbstractBasePage(Page): """A base for all page types.""" heading = models.CharField(max_length=255, null=True, blank=True) excerpt = models.TextField(null=True, blank=True) social_media_image = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', help_text='This image will be used as the image for social media sharing cards.' ) translation_fields = [ "heading", "excerpt" ] search_fields = [ FilterField('live'), SearchField('heading'), SearchField('excerpt'), ] promote_panels = Page.promote_panels + [ ImageChooserPanel('social_media_image'), ] class Meta(object): """Meta data for the class.""" abstract = True def get_context(self, request, *args, **kwargs): """Override get_context method to check for active language length.""" context = super(AbstractBasePage, self).get_context(request, *args, **kwargs) context['has_multilanguage_support'] = len(settings.ACTIVE_LANGUAGES) context['social_twitter_handle'] = settings.TWITTER_HANDLE context['social_youtube_url'] = settings.YOUTUBE_CHANNEL_URL return context def clean(self): """Override clean to remove trailing dashes from slugs with whitespaces.""" for lang in tuple(x[0] for x in settings.LANGUAGES): slug_field = 'slug_{}'.format(lang) slug = getattr(self, slug_field) if slug: slug_stripped = re.sub(r'[^\w\s-]', '', slug).strip("-") slug_valid = slugify(slug_stripped) setattr(self, slug_field, slug_valid) super(AbstractBasePage, self).clean() @property def social_share_image_url(self): """Return a default social media image for any page.""" if self.social_media_image: return self.social_media_image.get_rendition('width-320|jpegquality-60').url if hasattr(self, 'feed_image'): if self.feed_image: return self.feed_image.get_rendition('width-320|jpegquality-60').url return static(settings.DEFAULT_SHARE_IMAGE_URL)
class Location(Page): """ Geocoded location details. """ region = models.ForeignKey(Region, related_name="locations", null=True, on_delete=models.SET_NULL) formatted_address = models.CharField("Full Address", max_length=255, blank=True, null=True) lat_lng = models.CharField("Latitude/Longitude", max_length=255, blank=True, null=True) @cached_property def point(self): return geosgeometry_str_to_struct(self.lat_lng) @property def lat(self): return self.point["y"] @property def lng(self): return self.point["x"] content_panels = Page.content_panels + [ FieldPanel("region"), MultiFieldPanel( [ FieldPanel("formatted_address"), GeoPanel("lat_lng", address_field="formatted_address"), ], "Geocoded Address", ), ] search_fields = Page.search_fields + [ SearchField("region", partial_match=True), SearchField("formatted_address", partial_match=True), ] subpage_types = ["Meeting"] class Meta: indexes = [ models.Index(fields=["region"]), models.Index(fields=["formatted_address"]), ] def __str__(self): return "{0}: {1}".format(self.region, self.title)
def test_search_searchable_fields(self): # Find root page root_page = Page.objects.get(id=2) # Create a page root_page.add_child(instance=SimplePage( title="Hi there!", slug='hello-world', content="good morning", live=True, has_unpublished_changes=False, )) # Confirm the slug is not being searched response = self.get({'q': "hello"}) self.assertNotContains(response, "There is one matching page") search_fields = Page.search_fields # Add slug to the search_fields Page.search_fields = Page.search_fields + [SearchField('slug', partial_match=True)] # Confirm the slug is being searched response = self.get({'q': "hello"}) self.assertContains(response, "There is one matching page") # Reset the search fields Page.search_fields = search_fields
class StandardIndexPage(Page): title_suffix = models.CharField( help_text= "Additional text to display after the page title e.g. '(Version 3.0)", max_length=255, blank=True) listing_abstract = models.TextField( help_text= 'Give a brief blurb (about 1 sentence) of what this topic is about. It will appear on other pages that refer to this one.', blank=True) introductory_headline = models.TextField( help_text='Introduce the topic of this page in 1-3 sentences.', blank=True) body = StreamField( LimitedStreamBlock( required= False # https://github.com/wagtail/wagtail/issues/4306#issuecomment-384099847 ), blank=True) @property def children(self): return self.get_children().specific().live() search_fields = Page.search_fields + [SearchField('introductory_headline')] content_panels = Page.content_panels + [ FieldPanel('title_suffix'), FieldPanel('listing_abstract'), FieldPanel('introductory_headline'), StreamFieldPanel('body') ] parent_page_types = ['home.HomePage']
class CustomIndexPage(Page): title_suffix = models.CharField( help_text= "Additional text to display after the page title e.g. '(Version 3.0)", max_length=255, blank=True) listing_abstract = models.TextField( help_text= 'Give a brief blurb (about 1 sentence) of what this topic is about. It will appear on other pages that refer to this one.', blank=True) introductory_headline = models.TextField( help_text='Introduce the topic of this page in 1-3 sentences.', blank=True) body = StreamField(ContentStreamBlock(), blank=True) custom_sidebar_link = StreamField(CustomSidebarLinkBlock(required=False), blank=True) @property def children(self): return self.get_children().specific().live() search_fields = Page.search_fields + [SearchField('introductory_headline')] content_panels = Page.content_panels + [ FieldPanel('title_suffix'), FieldPanel('listing_abstract'), FieldPanel('introductory_headline'), StreamFieldPanel('custom_sidebar_link'), StreamFieldPanel('body') ] class Meta: verbose_name = "Custom Index Page with Streamfield"
class AbstractContentPage(AbstractBasePage): """A base for the basic model blocks of all content type pages.""" content_editor = StreamField(IATIStreamBlock(required=False), null=True, blank=True) translation_fields = AbstractBasePage.translation_fields + ["content_editor"] search_fields = AbstractBasePage.search_fields + [SearchField('content_editor')] class Meta(object): """Meta data for the class.""" abstract = True
class PostPage(Page): subtitle = models.CharField( verbose_name=_('Subtítulo'), help_text=_('O subtítulo da página para os mecanismos de busca.'), max_length=255, blank=True) body = RichTextField(verbose_name=_('Conteúdo')) date = models.DateTimeField( verbose_name=_('Data de publicação'), help_text=_('Data de publicação que será exibida no site.'), default=now, ) image = models.ForeignKey( 'wagtailimages.Image', models.SET_NULL, '+', verbose_name=_('Imagem de destaque'), help_text=_('Imagem que servirá de capa para o post.'), blank=False, null=True, ) search_fields = Page.search_fields + [ SearchField('body'), ] content_panels = [ MultiFieldPanel(Page.content_panels + [ FieldPanel('subtitle'), ImageChooserPanel('image'), ], heading=_('Dados básicos')), FieldPanel('body', classname='full'), ] settings_panels = [ FieldPanel('date'), ] + Page.settings_panels parent_page_types = ['blog.IndexPage'] subpage_types = [] def get_author(self): return self.owner.get_full_name # TODO: include function to return post excerpt with default length of 10 class Meta: verbose_name = _("post")
class BasicPage(Page): body = RichTextField(verbose_name=_('Conteúdo'), help_text=_('Conteúdo que será publicado.')) search_fields = Page.search_fields + [ SearchField('body'), ] content_panels = Page.content_panels + [ FieldPanel('body', classname='full'), ] parent_page_types = ['blog.IndexPage'] subpage_types = [] class Meta: verbose_name = _("página")
class AbstractGithubPage(DefaultPageHeaderImageMixin, AbstractContentPage): """A model for abstract reference pages build by Github.""" class Meta(object): """Meta data for the class.""" abstract = True is_creatable = False edit_handler = TabbedInterface([]) parent_page_types = [] subpage_types = [] meta_order = models.IntegerField( default=0 ) ssot_path = models.TextField( null=True, blank=True, help_text='Folder path of SSOT object' ) tag = models.CharField( max_length=255, help_text='Associated git release tag', ) ssot_root_slug = models.CharField( null=True, blank=True, max_length=255, help_text='Slug of the highest parent folder.' ) data = models.TextField( null=True, blank=True, help_text='HTML data for the page' ) publish_date = models.TextField( null=True, blank=True ) translation_fields = AbstractContentPage.translation_fields + ["data"] search_fields = AbstractContentPage.search_fields + [ SearchField('data'), ] @cached_property def parent_path(self): """Return ssot_path of parent object.""" return "/".join(self.ssot_path.split("/")[:-1]) @cached_property def name(self): """Return the last item in the ssot_path as a name.""" return self.ssot_path.split("/")[-1] @cached_property def version(self): """Return the first item in the ssot_path as a version.""" return self.ssot_path.split("/")[0] def first_paragraph(self): """Extract first paragraph snippet.""" soup = BeautifulSoup(self.data, 'html.parser') para = soup.find("p") if para: first_paragraph = para.text.replace("¶", "") else: first_paragraph = "Read more about {}.".format(self.title) return first_paragraph fp_split = first_paragraph.split(" ") fp_trunc = "" if len(fp_split) >= 50: fp_trunc = "..." first_paragraph = " ".join(first_paragraph.split(" ")[:50]) + fp_trunc return first_paragraph def save(self, *args, **kwargs): """Overwrite save to automatically update title.""" soup = BeautifulSoup(self.data, 'html.parser') meta_order = soup.find("meta", {"name": "order"}) if meta_order: try: self.meta_order = int(meta_order["content"]) except ValueError: self.meta_order = 0 meta_date = soup.find("meta", {"name": "date"}) if meta_date: self.publish_date = meta_date["content"] else: self.publish_date = date(datetime.now(), "F d, Y") meta_title = soup.find("meta", {"name": "title"}) if meta_title: self.title = meta_title["content"].replace("¶", "") self.heading = meta_title["content"].replace("¶", "") else: title = soup.find("h1") if title: self.title = title.text.replace("¶", "") self.heading = title.text.replace("¶", "") meta_description = soup.find("meta", {"name": "description"}) if meta_description: self.excerpt = meta_description["content"].replace("¶", "") else: self.excerpt = self.first_paragraph() meta_guidance_type = soup.find("meta", {"name": "guidance_type"}) if meta_guidance_type: super(AbstractGithubPage, self).save(*args, **kwargs) guidance_types = meta_guidance_type["content"].split(",") for guidance_type in guidance_types: StandardGuidanceTypes.objects.create(page=self, guidance_type=guidance_type.strip()) self.ssot_root_slug = self.ssot_path.split("/")[0] super(AbstractGithubPage, self).save(*args, **kwargs)
class Location(Page): """ Geocoded location details. """ region = models.ForeignKey( Region, related_name="locations", on_delete=models.PROTECT, limit_choices_to={"parent__isnull": False}, ) formatted_address = models.CharField("Full Address", max_length=255, blank=True, null=True) lat_lng = models.CharField("Latitude/Longitude", max_length=255, blank=True, null=True) postal_code = models.CharField("Postal Code", max_length=12, blank=True) details = models.TextField( null=True, blank=True, help_text= "Details specific to the location, not the meeting. For example, " "'Located in shopping center behind the bank.'", ) @cached_property def point(self): return geosgeometry_str_to_struct(self.lat_lng) @property def lat(self): return self.point["y"] @property def lng(self): return self.point["x"] content_panels = Page.content_panels + [ FieldPanel("region"), FieldPanel("postal_code"), FieldPanel("details"), MultiFieldPanel( [ FieldPanel("formatted_address"), GeoPanel("lat_lng", address_field="formatted_address"), ], "Geocoded Address", ), ] search_fields = Page.search_fields + [ SearchField("region", partial_match=True), SearchField("formatted_address", partial_match=True), ] subpage_types = ["Meeting"] class Meta: indexes = [ models.Index(fields=["region"]), models.Index(fields=["formatted_address"]), ] def __str__(self): return "{0}: {1}".format(self.region, self.title)
class Meeting(Page): """ Model for storing meeting data. """ SUNDAY = 0 MONDAY = 1 TUESDAY = 2 WEDNESDAY = 3 THURSDAY = 4 FRIDAY = 5 SATURDAY = 6 DAY_OF_WEEK = ( (SUNDAY, "Sunday"), (MONDAY, "Monday"), (TUESDAY, "Tuesday"), (WEDNESDAY, "Wednesday"), (THURSDAY, "Thursday"), (FRIDAY, "Friday"), (SATURDAY, "Saturday"), ) INACTIVE = 0 ACTIVE = 1 STATUS_CHOICES = ( (ACTIVE, "Active"), (INACTIVE, "Inactive Permanently"), ) group = models.ForeignKey(Group, null=True, blank=True, on_delete=models.SET_NULL, related_name="meetings") meeting_location = models.ForeignKey(Location, related_name="meetings", null=True, on_delete=models.SET_NULL) start_time = models.TimeField(null=True) end_time = models.TimeField(null=True) day_of_week = models.SmallIntegerField(default=0, choices=DAY_OF_WEEK) status = models.SmallIntegerField(default=1, choices=STATUS_CHOICES) details = models.TextField( null=True, blank=True, help_text="Additional details about the meeting.") area = models.CharField(max_length=10, blank=True) district = models.CharField(max_length=10, blank=True) types = ParentalManyToManyField( MeetingType, related_name="meetings", limit_choices_to={"intergroup_code__isnull": False}, ) conference_url = models.URLField( blank=True, verbose_name="Conference URL", default="", help_text="Example: " \ "https://zoom.com/j/123456789?pwd=ExzUZMeT091pRU0Omc2QWjErUUUpxS1B", ) conference_phone = models.CharField( max_length=255, blank=True, default="", validators=[ConferencePhoneValidator()], help_text="Enter a valid conference phone number. The three groups of " \ "numbers in this example are a Zoom phone number, meeting code, and " \ "password: +19294362866,,2151234215#,,#,,12341234#", ) venmo = models.TextField( max_length= 31, # Venmo's max username length is 31 chars with the "@" prefix validators=[VenmoUsernameValidator()], blank=True, verbose_name="Venmo Account", default="", help_text="Example: @aa-mygroup", ) paypal = models.TextField( blank=True, verbose_name="PayPal Username", default="", max_length=255, validators=[PayPalUsernameValidator(), MinLengthValidator(3)], help_text="Example: aamygroup", ) cashapp = models.TextField( max_length= 31, # Venmo's max username length is 31 chars with the "@" prefix validators=[CashAppUsernameValidator()], blank=True, verbose_name="CashApp Account", default="", help_text="Example: $aa-mygroup", ) @property def day_sort_order(self): """ Returns 0 for today's day of the week, up to 6 for yesterday's day of the week rather than Sunday - Saturday. """ day_sort_order = self.day_of_week - datetime.datetime.today().weekday( ) - 1 if day_sort_order < 0: day_sort_order += 7 return day_sort_order content_panels = Page.content_panels + [ FieldRowPanel([ FieldPanel("day_of_week"), FieldPanel("start_time"), FieldPanel("end_time"), ], ), FieldRowPanel([ FieldPanel("group"), FieldPanel("status"), ], ), FieldRowPanel([ FieldPanel("area"), FieldPanel("district"), ], ), FieldRowPanel([ FieldPanel("venmo"), FieldPanel("paypal"), ], ), FieldRowPanel([ FieldPanel("conference_url"), FieldPanel("conference_phone"), ], ), FieldPanel("types", widget=CheckboxSelectMultiple), FieldPanel("details"), ] search_fields = Page.search_fields + [ SearchField("group", partial_match=True), SearchField("meeting_location", partial_match=True), ] parent_page_types = ["Location"] class Meta: indexes = [ models.Index(fields=["meeting_location"]), models.Index(fields=["day_of_week"]), ] def save(self, *args, **kwargs): """ Associate the meeting with the Location parent and save. Then automatically assign the ONLINE meeting type if the field is populated. """ # Associate with the parent meeting location, and save in case this # is new, before we change meeting types. self.meeting_location = Location.objects.get(pk=self.get_parent().id) # Automagically add or remove the online meeting type. online_meeting_type = MeetingType.objects.get(spec_code="ONL") if self.conference_url: self.types.add(online_meeting_type) else: self.types.remove(online_meeting_type) super().save(*args, **kwargs) def __str__(self): return "{0} ({1}): {2} @ {3}".format(self.title, self.group, self.day_of_week, self.start_time)
class Meeting(Page): """ Model for storing meeting data. """ SUNDAY = 0 MONDAY = 1 TUESDAY = 2 WEDNESDAY = 3 THURSDAY = 4 FRIDAY = 5 SATURDAY = 6 DAY_OF_WEEK = ( (SUNDAY, "Sunday"), (MONDAY, "Monday"), (TUESDAY, "Tuesday"), (WEDNESDAY, "Wednesday"), (THURSDAY, "Thursday"), (FRIDAY, "Friday"), (SATURDAY, "Saturday"), ) INACTIVE = 0 ACTIVE = 1 STATUS_CHOICES = ((INACTIVE, "Inactive"), (ACTIVE, "Active")) group = models.ForeignKey(Group, null=True, blank=True, on_delete=models.SET_NULL, related_name="meetings") meeting_location = models.ForeignKey(Location, related_name="meetings", null=True, on_delete=models.SET_NULL) start_time = models.TimeField(null=True) end_time = models.TimeField(null=True) day_of_week = models.SmallIntegerField(default=0, choices=DAY_OF_WEEK) status = models.SmallIntegerField(default=1, choices=STATUS_CHOICES) meeting_details = models.TextField( null=True, blank=True, help_text="Additional details about the meeting.") location_details = models.TextField( null=True, blank=True, help_text= "How to find the meeting at the location, I.e.: 'In the basement'," " 'In the rear building.'", ) types = ParentalManyToManyField( MeetingType, related_name="meetings", limit_choices_to={"intergroup_code__isnull": False}, ) @property def day_sort_order(self): """ Returns 0 for today's day of the week, up to 6 for yesterday's day of the week rather than Sunday - Saturday. """ day_sort_order = self.day_of_week - datetime.datetime.today().weekday( ) - 1 if day_sort_order < 0: day_sort_order += 7 return day_sort_order content_panels = Page.content_panels + [ FieldPanel("group"), FieldRowPanel([ FieldPanel("day_of_week"), FieldPanel("start_time"), FieldPanel("end_time"), ]), FieldPanel("types", widget=CheckboxSelectMultiple), FieldPanel("meeting_details"), FieldPanel("location_details"), ] search_fields = Page.search_fields + [ SearchField("group", partial_match=True), SearchField("meeting_location", partial_match=True), ] parent_page_types = ["Location"] class Meta: indexes = [ models.Index(fields=["meeting_location"]), models.Index(fields=["day_of_week"]), ] def save(self, *args, **kwargs): self.meeting_location = Location.objects.get(pk=self.get_parent().id) super(Meeting, self).save(*args, **kwargs) def __str__(self): return "{0} ({1}): {2} @ {3}".format(self.title, self.group, self.day_of_week, self.start_time)