events = paginator.page(paginator.num_pages) # Update template context context = super(EventIndexPage, self).get_context(request) context['events'] = events context['tags'] = Tag.objects.filter( events_eventpagetag_items__isnull=False, events_eventpagetag_items__content_object__live=True).distinct( ).order_by('name') return context EventIndexPage.content_panels = [ FieldPanel('title', classname="full title"), FieldPanel('intro', classname="full"), InlinePanel('related_links', label="Related links"), ] EventIndexPage.promote_panels = [ MultiFieldPanel(Page.promote_panels, "Common page configuration"), ImageChooserPanel('feed_image'), ] class EventPageCarouselItem(Orderable, CarouselItem): page = ParentalKey('events.EventPage', related_name='carousel_items') class EventPageRelatedLink(Orderable, RelatedLink): page = ParentalKey('events.EventPage', related_name='related_links')
class SectionedRichTextPage(Page): content_panels = [ FieldPanel('title', classname="full title"), InlinePanel('sections') ]
class HomePage(RoutablePageMixin, Page): """Home page model.""" subpage_types = [ 'blog.BlogListingPage', 'contact.ContactPage', 'flex.FlexPage', ] parent_page_type = [ 'wagtailcore.Page' ] banner_title = models.CharField(max_length=100, blank=True, 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()), ], blank=True, null=True) api_fields = [ APIField("banner_title"), APIField("banner_subtitle"), APIField("banner_image"), APIField("banner_cta", serializer=BannerCTASerializer()), APIField("carousel_images"), APIField("content"), APIField("a_custom_api_response"), ] @property def a_custom_api_response(self): # return ["SOMETHING CUSTOM", 3.14, [1, 2, 3, 'a', 'b', 'c']] # logic goes in here return f"Banner Title Is: {self.banner_title}" content_panels = Page.content_panels + [ MultiFieldPanel([ InlinePanel("carousel_images", max_num=5, min_num=1, label="Image") ], heading="Carousel Images",), StreamFieldPanel("content"), ] # This is how you'd normally hide promote and settings tabs # promote_panels = [] # settings_panels = [] 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"), ObjectList(Page.promote_panels, heading="Promote"), ObjectList(Page.settings_panels, heading="Settings"), ] ) 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 LibraryItem(Page): publication_date = models.DateField("Publication date", null=True, blank=True) description = RichTextField(null=True, blank=True) body = StreamField( [ ("paragraph", blocks.RichTextBlock()), ("image", ImageChooserBlock()), ("document", DocumentChooserBlock()), ("embed", EmbedBlock()), ("url", blocks.URLBlock()), ("quote", blocks.BlockQuoteBlock()), ], null=True, blank=True, ) item_audience = models.ForeignKey("facets.Audience", on_delete=models.SET_NULL, null=True, blank=True) item_genre = models.ForeignKey("facets.Genre", on_delete=models.SET_NULL, null=True, blank=True) item_medium = models.ForeignKey("facets.Medium", on_delete=models.SET_NULL, null=True, blank=True) item_time_period = models.ForeignKey("facets.TimePeriod", on_delete=models.SET_NULL, null=True, blank=True) tags = ClusterTaggableManager(through=LibraryItemTag, blank=True) drupal_node_id = models.IntegerField(null=True, blank=True) content_panels = Page.content_panels + [ FieldPanel("description"), InlinePanel( "authors", heading="Authors", help_text= "Select one or more authors, who contributed to this article", ), FieldPanel("publication_date", widget=DatePickerInput()), StreamFieldPanel("body"), MultiFieldPanel( children=[ FieldPanel("item_audience"), FieldPanel("item_genre"), FieldPanel("item_medium"), FieldPanel("item_time_period"), InlinePanel("topics", label="topics"), FieldPanel("tags"), ], heading="Categorization", ), ] parent_page_types = ["LibraryIndexPage"] subpage_types = []
class PressReleasePage(ContentPage): date = models.DateField(default=datetime.date.today) formatted_title = models.CharField( max_length=255, null=True, blank=True, default='', help_text= "Use if you need italics in the title. e.g. <em>Italicized words</em>") category = models.CharField( max_length=255, choices=constants.press_release_page_categories.items()) read_next = models.ForeignKey('PressReleasePage', blank=True, null=True, default=get_previous_press_release_page, on_delete=models.SET_NULL) homepage_pin = models.BooleanField(default=False) homepage_pin_expiration = models.DateField(blank=True, null=True) homepage_pin_start = models.DateField(blank=True, null=True) homepage_hide = models.BooleanField(default=False) template = 'home/updates/press_release_page.html' content_panels = ContentPage.content_panels + [ FieldPanel('formatted_title'), FieldPanel('date'), InlinePanel('authors', label="Authors"), FieldPanel('category'), PageChooserPanel('read_next'), ] promote_panels = Page.promote_panels + [ MultiFieldPanel([ FieldPanel('homepage_pin'), FieldPanel('homepage_pin_start'), FieldPanel('homepage_pin_expiration'), FieldPanel('homepage_hide') ], heading="Home page feed") ] search_fields = ContentPage.search_fields + [ index.FilterField('category'), index.FilterField('date') ] @property def content_section(self): return 'about' @property def get_update_type(self): return constants.update_types['press-release'] @property def get_author_office(self): return 'Press Office' """ Because we removed the boilerplate from all 2016 releases this flag is used to show it in the templates as a print-only element """ @property def no_boilerplate(self): return self.date.year >= 2016 @property def social_image_identifier(self): return 'press-release'
class DatasetPage(DataSetMixin, TypesetBodyMixin, HeroMixin, Page): """ Content of each dataset """ class Meta(): verbose_name = 'Data Set Page' dataset_id = models.CharField(max_length=255, unique=True, blank=True, null=True) dataset_title = models.TextField(unique=True, blank=True, null=True) related_datasets_title = models.CharField(blank=True, max_length=255, default='Related datasets', verbose_name='Section Title') content_panels = Page.content_panels + [ hero_panels(), FieldPanel('dataset_id'), FieldPanel('dataset_title'), FieldPanel('release_date'), StreamFieldPanel('body'), StreamFieldPanel('authors'), InlinePanel('dataset_downloads', label='Downloads', max_num=None), metadata_panel(), MultiFieldPanel([ FieldPanel('related_datasets_title'), InlinePanel('related_datasets', label="Related Datasets") ], heading='Related Datasets'), other_pages_panel(), InlinePanel('page_notifications', label='Notifications') ] def get_context(self, request): context = super().get_context(request) context['topics'] = [ orderable.topic for orderable in self.dataset_topics.all() ] context['related_datasets'] = get_related_dataset_pages( self.related_datasets.all(), self) context['reports'] = self.get_usages() return context @cached_property def get_dataset_downloads(self): return self.dataset_downloads.all() @cached_property def get_dataset_sources(self): return self.dataset_sources.all() def get_usages(self): reports = Page.objects.live().filter( models.Q( publicationpage__publication_datasets__dataset__slug=self.slug) | models.Q( legacypublicationpage__publication_datasets__dataset__slug=self .slug) | models. Q(publicationsummarypage__publication_datasets__dataset__slug=self. slug) | models. Q(publicationchapterpage__publication_datasets__dataset__slug=self. slug) | models. Q(publicationappendixpage__publication_datasets__dataset__slug=self .slug) | models.Q( shortpublicationpage__publication_datasets__dataset__slug=self. slug)).specific() return reports def get_download_name(self): return self.title
class GoogleAdGrantsPage(Page): intro = RichTextField() form_title = models.CharField(max_length=255) form_subtitle = models.CharField(max_length=255) form_button_text = models.CharField(max_length=255) to_address = models.EmailField( verbose_name='to address', blank=True, help_text="Optional - form submissions will be emailed to this address" ) body = RichTextField() grants_managed_title = models.CharField(max_length=255) call_to_action_title = models.CharField(max_length=255, blank=True) call_to_action_embed_url = models.URLField(blank=True) search_fields = Page.search_fields + [ index.SearchField('intro'), index.SearchField('body') ] def get_context(self, request, *args, **kwargs): form = GoogleAdGrantApplicationForm() context = super(GoogleAdGrantsPage, self).get_context(request, *args, **kwargs) context['form'] = form return context def serve(self, request, *args, **kwargs): if request.is_ajax() and request.method == "POST": form = GoogleAdGrantApplicationForm(request.POST) if form.is_valid(): form.save() if self.to_address: subject = "{} form submission".format(self.title) content = '\n'.join([ x[1].label + ': ' + str(form.data.get(x[0])) for x in form.fields.items() ]) send_mail( subject, content, [self.to_address], ) return render( request, 'home/includes/ad_grant_application_landing.html', { 'self': self, 'form': form }) else: return render(request, 'home/includes/ad_grant_application_form.html', { 'self': self, 'form': form }) else: return super(GoogleAdGrantsPage, self).serve(request) content_panels = Page.content_panels + [ FieldPanel('intro', classname='full'), FieldPanel('body', classname='full'), MultiFieldPanel([ FieldPanel('form_title'), FieldPanel('form_subtitle'), FieldPanel('form_button_text'), FieldPanel('to_address'), ], "Application Form"), MultiFieldPanel([ FieldPanel('grants_managed_title'), InlinePanel('grants_managed', label="Grants Managed") ], "Grants Managed Section"), InlinePanel('quotes', label="Quotes"), MultiFieldPanel([ FieldPanel('call_to_action_title'), FieldPanel('call_to_action_embed_url'), InlinePanel('accreditations', label="Accreditations") ], "Call To Action") ]
class Order(ClusterableModel): purchaser_given_name = models.CharField( max_length=255, default="", help_text="Enter the given name for the purchaser.", blank=True, ) purchaser_family_name = models.CharField( max_length=255, blank=True, default="", help_text="Enter the family name for the purchaser.", ) purchaser_meeting_or_organization = models.CharField( max_length=255, blank=True, default="", help_text="Enter the meeting or organization name, if this purchaser is a meeting or organization.", ) purchaser_email = models.EmailField( help_text="Provide an email, so we can communicate any issues regarding this order." ) recipient_name = models.CharField( max_length=255, default="", help_text="Enter the recipient name (as it should appear on shipping label)." ) recipient_street_address = models.CharField( max_length=255, blank=True, default="", help_text="The street address where this order should be shipped.", ) recipient_postal_code = models.CharField( max_length=16, help_text="Postal code for the shipping address." ) recipient_po_box_number = models.CharField( max_length=32, blank=True, default="", help_text="P.O. Box, if relevant." ) recipient_address_locality = models.CharField( max_length=255, help_text="City for the shipping address." ) recipient_address_region = models.CharField( max_length=255, help_text="State for the shipping address.", blank=True, default="" ) recipient_address_country = models.CharField( max_length=255, default="United States", help_text="Country for shipping." ) shipping_cost = models.DecimalField(max_digits=10, decimal_places=2) paid = models.BooleanField(default=False) braintree_transaction_id = models.CharField(max_length=255, null=True, blank=True) panels = [ FieldPanel("purchaser_given_name"), FieldPanel("purchaser_family_name"), FieldPanel("purchaser_meeting_or_organization"), FieldPanel("purchaser_email"), FieldPanel("recipient_name"), FieldPanel("recipient_street_address"), FieldPanel("recipient_po_box_number"), FieldPanel("recipient_postal_code"), FieldPanel("recipient_address_locality"), FieldPanel("recipient_address_region"), FieldPanel("recipient_address_country"), FieldPanel("shipping_cost"), FieldPanel("paid"), InlinePanel("items", label="Order items"), ] def __str__(self): return f"Order {self.id}" def get_total_cost(self): return sum(item.get_cost() for item in self.items.all()) @property def purchaser_full_name(self): full_name = "" if self.purchaser_given_name: full_name += self.purchaser_given_name + " " if self.purchaser_family_name: full_name += self.purchaser_family_name + " " if self.purchaser_meeting_or_organization: full_name += self.purchaser_meeting_or_organization # Combine any available name data, removing leading or trailing whitespace return full_name.rstrip()
class BlogIndexPage(Page): intro = models.TextField(blank=True) search_fields = Page.search_fields + [ index.SearchField('intro'), ] def get_popular_tags(self): # Get a ValuesQuerySet of tags ordered by most popular (exclude 'planet-drupal' as this is effectively # the same as Drupal and only needed for the rss feed) popular_tags = BlogPageTagSelect.objects.all().values('tag').annotate(item_count=models.Count('tag')).order_by('-item_count') # Return first 10 popular tags as tag objects # Getting them individually to preserve the order return [Tag.objects.get(id=tag['tag']) for tag in popular_tags[:10]] @property def blog_posts(self): # Get list of blog pages that are descendants of this page blog_posts = BlogPage.objects.descendant_of(self).live() # Order by most recent date first blog_posts = blog_posts.order_by('-date', 'pk') return blog_posts def serve(self, request): # Get blog_posts blog_posts = self.blog_posts # Filter by tag tag = request.GET.get('tag') if tag: blog_posts = blog_posts.filter(tags__tag__slug=tag) # Pagination per_page = 12 page = request.GET.get('page') paginator = Paginator(blog_posts, per_page) # Show 10 blog_posts per page try: blog_posts = paginator.page(page) except PageNotAnInteger: blog_posts = paginator.page(1) except EmptyPage: blog_posts = paginator.page(paginator.num_pages) if request.is_ajax(): return render(request, "blog/includes/blog_listing.html", { 'self': self, 'blog_posts': blog_posts, 'per_page': per_page, }) else: return render(request, self.template, { 'self': self, 'blog_posts': blog_posts, 'per_page': per_page, }) content_panels = [ FieldPanel('title', classname="full title"), FieldPanel('intro', classname="full"), InlinePanel('related_links', label="Related links"), ] promote_panels = [ MultiFieldPanel(Page.promote_panels, "Common page configuration"), ]
class StaffSection(Page): content_panels = Page.content_panels + [ InlinePanel('staff', label="Staff"), ]
class LocationPage(Page): """ Detail for a specific bakery location. """ introduction = models.TextField( help_text='Text to describe the page', blank=True) image = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', help_text='Landscape mode only; horizontal width between 1000px and 3000px.' ) body = StreamField( BaseStreamBlock(), verbose_name="Page body", blank=True ) address = models.TextField() lat_long = models.CharField( max_length=36, help_text="Comma separated lat/long. (Ex. 64.144367, -21.939182) \ Right click Google Maps and select 'What\'s Here'", validators=[ RegexValidator( regex=r'^(\-?\d+(\.\d+)?),\s*(\-?\d+(\.\d+)?)$', message='Lat Long must be a comma-separated numeric lat and long', code='invalid_lat_long' ), ] ) # Search index configuration search_fields = Page.search_fields + [ index.SearchField('address'), index.SearchField('body'), ] # Fields to show to the editor in the admin view content_panels = [ FieldPanel('title', classname="full"), FieldPanel('introduction', classname="full"), ImageChooserPanel('image'), StreamFieldPanel('body'), FieldPanel('address', classname="full"), FieldPanel('lat_long'), InlinePanel('hours_of_operation', label="Hours of Operation"), ] def __str__(self): return self.title @property def operating_hours(self): hours = self.hours_of_operation.all() return hours # Determines if the location is currently open. It is timezone naive def is_open(self): now = datetime.now() current_time = now.time() current_day = now.strftime('%a').upper() try: self.operating_hours.get( day=current_day, opening_time__lte=current_time, closing_time__gte=current_time ) return True except LocationOperatingHours.DoesNotExist: return False # Makes additional context available to the template so that we can access # the latitude, longitude and map API key to render the map def get_context(self, request): context = super(LocationPage, self).get_context(request) context['lat'] = self.lat_long.split(",")[0] context['long'] = self.lat_long.split(",")[1] context['google_map_api_key'] = settings.GOOGLE_MAP_API_KEY return context # Can only be placed under a LocationsIndexPage object parent_page_types = ['LocationsIndexPage']
class Homepage(FoundationMetadataPageMixin, Page): hero_headline = models.CharField( max_length=140, help_text='Hero story headline', blank=True, ) hero_story_description = RichTextField( features=[ 'bold', 'italic', 'link', ] ) hero_image = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='hero_image' ) hero_button_text = models.CharField( max_length=50, blank=True ) hero_button_url = models.URLField( blank=True ) content_panels = Page.content_panels + [ MultiFieldPanel([ FieldPanel('hero_headline'), FieldPanel('hero_story_description'), FieldRowPanel([ FieldPanel('hero_button_text'), FieldPanel('hero_button_url'), ]), ImageChooserPanel('hero_image'), ], heading='hero', classname='collapsible' ), InlinePanel('featured_highlights', label='Highlights', max_num=5), InlinePanel('featured_news', label='News', max_num=4), ] subpage_types = [ 'PrimaryPage', 'PeoplePage', 'InitiativesPage', 'Styleguide', 'NewsPage', 'ParticipatePage', 'ParticipatePage2', 'MiniSiteNameSpace', 'RedirectingPage', 'OpportunityPage', 'BanneredCampaignPage', ] def get_context(self, request): # We need to expose MEDIA_URL so that the s3 images will show up properly # due to our custom image upload approach pre-wagtail context = super(Homepage, self).get_context(request) context['MEDIA_URL'] = settings.MEDIA_URL return context
class FormSection(SectionBase, SectionTitleBlock, ButtonAction, AbstractEmailForm): form_submit_button_text = models.CharField( blank=True, max_length=100, verbose_name='Submit button text', default='Submit', # help_text="Leave field empty to hide.", ) # intro = RichkTextField(blank=True) thank_you_text = RichTextField(blank=True) # basic tab panels basic_panels = AbstractEmailForm.content_panels + [ SectionBase.section_content_panels, SectionBase.section_layout_panels, SectionBase.section_design_panels, ] # advanced tab panels advanced_panels = (SectionTitleBlock.title_basic_panels, ) + ButtonAction.button_action_panels # form tab panels form_panels = [ InlinePanel('form_fields', label="Form fields"), FieldPanel('thank_you_text', classname="full"), MultiFieldPanel([ FieldRowPanel([ FieldPanel('from_address', classname="col6"), FieldPanel('to_address', classname="col6"), ]), FieldPanel('subject'), ], "Email"), ] # Register Tabs edit_handler = TabbedInterface([ ObjectList(basic_panels, heading="Basic"), ObjectList(form_panels, heading="Form"), ObjectList(advanced_panels, heading="Plus+"), ]) # Page settings template = 'sections/form_section_preview.html' parent_page_types = ['home.HomePage'] subpage_types = [] # Overriding Methods # def get_form(self, *args, **kwargs): # form = super().get_form(*args, **kwargs) # # Get form and update attributes # # https://stackoverflow.com/questions/48321770/how-to-modify-attributes-of-the-wagtail-form-input-fields # return form def serve(self, request): if request.is_ajax(): print('IS AXJAX') return super(FormSection, self).serve(request) def __str__(self): if self.title: return self.title + " (Form Section)" else: return super(AbstractEmailForm, self).__str__() class Meta: verbose_name = 'Form Section' verbose_name_plural = 'Form Sections'
# TODO blog_feed_title=feed_settings.blog_feed_title ) return context def serve_preview(self, request, mode_name): """ This is another hack to overcome the MRO issue we were seeing """ return BibliographyMixin.serve_preview(self, request, mode_name) class Meta: verbose_name = "IESG Statement Page" IESGStatementPage.content_panels = Page.content_panels + [ FieldPanel('date_published'), FieldPanel('introduction'), StreamFieldPanel('body'), InlinePanel('topics', label="Topics"), ] IESGStatementPage.promote_panels = Page.promote_panels + PromoteMixin.panels class IESGStatementIndexPage(RoutablePageMixin, Page): def get_context(self, request): context = super().get_context(request) context['statements'] = IESGStatementPage.objects.child_of(self).live().annotate( d=Coalesce('date_published', 'first_published_at') ).order_by('-d') return context @route(r'^all/$')
class InlinePanelPage(WagtailPage): content_panels = [InlinePanel('related_page_model')]
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 DataSectionPage(TypesetBodyMixin, HeroMixin, Page): """ Main page for datasets """ quotes = StreamField(QuoteStreamBlock, verbose_name="Quotes", null=True, blank=True) dataset_info = RichTextField(null=True, blank=True, help_text='A description of the datasets', features=RICHTEXT_FEATURES_NO_FOOTNOTES) tools = StreamField([ ('tool', BannerBlock(template='datasection/tools_banner_block.html')) ], verbose_name="Tools", null=True, blank=True) other_pages_heading = models.CharField(blank=True, max_length=255, verbose_name='Heading', default='More about') content_panels = Page.content_panels + [ hero_panels(allowed_pages=['datasection.DataSetListing']), StreamFieldPanel('body'), FieldPanel('dataset_info'), StreamFieldPanel('tools'), StreamFieldPanel('quotes'), MultiFieldPanel([ FieldPanel('other_pages_heading'), InlinePanel('other_pages', label='Related pages') ], heading='Other Pages/Related Links'), InlinePanel('page_notifications', label='Notifications') ] parent_page_types = ['home.HomePage'] subpage_types = [ 'general.General', 'datasection.DataSetListing', 'spotlight.SpotlightPage', 'publications.ShortPublicationPage' ] class Meta: verbose_name = "Data Section Page" @cached_property def get_dataset_listing_page(self): return self.get_children().type(DataSetListing)[0] def count_quotes(self): quote_counter = 0 for quote in self.quotes: quote_counter = quote_counter + 1 return quote_counter def get_random_quote(self): number_of_quotes = self.count_quotes() if number_of_quotes == 1: for quote in self.quotes: return quote elif number_of_quotes >= 2: random_number = random.randint(0, number_of_quotes - 1) for index, quote in enumerate(self.quotes): if random_number == index: return quote return def get_context(self, request, *args, **kwargs): context = super().get_context(request, *args, **kwargs) context['random_quote'] = self.get_random_quote() context['dataset_count'] = DatasetPage.objects.live().count() return context
class BaseOrderAdmin(ModelAdmin): model = Order menu_icon = 'form' index_view_class = OrderIndexView edit_view_class = OrderEditView list_display = [ 'admin_title', 'email', 'admin_status', 'total_display', 'admin_is_paid', 'date_created', ] list_filter = [ OrderStatusFilter, OrderIsPaidFilter, 'date_created', 'date_updated' ] search_fields = ['ref', 'email', 'token'] edit_template_name = 'salesman/admin/wagtail_edit.html' permission_helper_class = OrderPermissionHelper button_helper_class = OrderButtonHelper form_view_extra_css = ['salesman/admin/wagtail_form.css'] panels = [ MultiFieldPanel([ReadOnlyPanel('ref'), ReadOnlyPanel('token')], heading=_("Info")), MultiFieldPanel( [ FieldPanel('status', classname='choice_field', widget=OrderStatusSelect), ReadOnlyPanel('date_created_display'), ReadOnlyPanel('date_updated_display'), ReadOnlyPanel('is_paid_display', formatter=_format_is_paid), ], heading=_("Status"), ), MultiFieldPanel( [ ReadOnlyPanel('user'), ReadOnlyPanel('email'), ReadOnlyPanel('shipping_address_display'), ReadOnlyPanel('billing_address_display'), ], heading=_("Contact"), ), MultiFieldPanel( [ ReadOnlyPanel('subtotal_display'), ReadOnlyPanel('extra_rows_display'), ReadOnlyPanel('total_display'), ReadOnlyPanel('amount_paid_display'), ReadOnlyPanel('amount_outstanding_display'), ], heading=_("Totals"), ), MultiFieldPanel([ReadOnlyPanel('extra_display')], heading=_("Extra")), ] # Currently proxy related models don't work in Django and can't be used when # accessing them on an Order proxy through a related manager. It points back to # original models for items and payments. For that reason we can't use "display" # methods defined on proxy related models and are using formatter/renderer # functions instead. items_panels = [ ReadOnlyPanel( 'items', classname='salesman-order-items', renderer=_render_items, heading=_("Items"), ), ] payments_panels = [ InlinePanel( 'payments', [ FieldPanel('amount'), FieldPanel('transaction_id'), FieldPanel('payment_method', classname='choice_field', widget=PaymentSelect), ReadOnlyPanel('date_created', formatter=_format_date), ], heading=_("Payments"), ), ] notes_panels = [ InlinePanel( 'notes', [ FieldPanel('message', widget=forms.Textarea(attrs={'rows': 4})), FieldPanel('public'), ReadOnlyPanel('date_created', formatter=_format_date), ], heading=_("Notes"), ) ] edit_handler = TabbedInterface( [ ObjectList(panels, heading=_("Summary")), ObjectList(items_panels, heading=_("Items")), ObjectList(payments_panels, heading=_("Payments")), ObjectList(notes_panels, heading=_("Notes")), ], base_form_class=OrderModelForm, ) def admin_title(self, obj): url = self.url_helper.get_action_url('edit', obj.id) return format_html( '<div class="title">' '<div class="title-wrapper"><a href="{}">{}</a></div>' '</div>', url, obj, ) admin_title.short_description = _('Order') def admin_status(self, obj): faded_statuses = [obj.statuses.CANCELLED, obj.statuses.REFUNDED] tag_class = 'secondary' if obj.status in faded_statuses else 'primary' template = '<span class="status-tag {}">{}</span>' return format_html(template, tag_class, obj.status_display) admin_status.short_description = _('Status') def admin_is_paid(self, obj): return _format_is_paid(None, obj, None) admin_is_paid.short_description = Order.is_paid_display.short_description
class DataSetListing(DatasetListingMetadataPageMixin, TypesetBodyMixin, Page): """ http://development-initiatives.surge.sh/page-templates/21-1-dataset-listing """ class Meta(): verbose_name = 'DataSet Listing' parent_page_types = ['datasection.DataSectionPage'] subpage_types = ['datasection.DatasetPage'] hero_text = RichTextField(null=True, blank=True, help_text='A description of the page content', features=RICHTEXT_FEATURES_NO_FOOTNOTES) other_pages_heading = models.CharField(blank=True, max_length=255, verbose_name='Heading', default='More about') content_panels = Page.content_panels + [ FieldPanel('hero_text'), StreamFieldPanel('body'), MultiFieldPanel([ FieldPanel('other_pages_heading'), InlinePanel('other_pages', label='Related pages') ], heading='Other Pages/Related Links') ] def is_filtering(self, request): get = request.GET.get return get('topic', None) or get('country', None) or get( 'source', None) or get('report', None) def fetch_all_data(self): return DatasetPage.objects.live().specific() def fetch_filtered_data(self, context): topic = context['selected_topic'] country = context['selected_country'] source = context['selected_source'] report = context['selected_report'] if topic: datasets = DatasetPage.objects.live().specific().filter( dataset_topics__topic__slug=topic) else: datasets = self.fetch_all_data() if country: if 'all--' in country: try: region = re.search('all--(.*)', country).group(1) datasets = datasets.filter( page_countries__country__region__name=region) except AttributeError: pass else: datasets = datasets.filter( page_countries__country__slug=country) if source: datasets = datasets.filter(dataset_sources__source__slug=source) if report: pubs = Page.objects.filter( models.Q( publicationpage__publication_datasets__item__slug=report) | models.Q( publicationsummarypage__publication_datasets__item__slug= report) | models.Q( publicationappendixpage__publication_datasets__item__slug= report) | models.Q( publicationchapterpage__publication_datasets__item__slug= report) | models. Q(legacypublicationpage__publication_datasets__item__slug=report ) | models. Q(shortpublicationpage__publication_datasets__item__slug=report )).first() if (pubs and pubs.specific.publication_datasets): filtered_datasets = Page.objects.none() for dataset in pubs.specific.publication_datasets.all(): results = datasets.filter(slug__exact=dataset.dataset.slug) if results: filtered_datasets = filtered_datasets | results datasets = filtered_datasets else: datasets = None return datasets def get_active_countries(self): active_countries = [] datasets = DatasetPage.objects.all() for dataset in datasets: countries = dataset.page_countries.all() for country in countries: active_country = Country.objects.get(id=country.country_id) if active_country not in active_countries: active_countries.append(active_country) return active_countries def get_context(self, request, *args, **kwargs): context = super(DataSetListing, self).get_context(request, *args, **kwargs) page = request.GET.get('page', None) context['selected_topic'] = request.GET.get('topic', None) context['selected_country'] = request.GET.get('country', None) context['selected_source'] = request.GET.get('source', None) context['selected_report'] = request.GET.get('report', None) if not self.is_filtering(request): datasets = self.fetch_all_data() is_filtered = False else: is_filtered = True datasets = self.fetch_filtered_data(context) datasets = datasets.order_by('-first_published_at') if datasets else [] context['is_filtered'] = is_filtered paginator = Paginator(datasets, MAX_PAGE_SIZE) try: context['datasets'] = paginator.page(page) except PageNotAnInteger: context['datasets'] = paginator.page(1) except EmptyPage: context['datasets'] = paginator.page(paginator.num_pages) context['paginator_range'] = get_paginator_range( paginator, context['datasets']) context['topics'] = [ page_orderable.topic for page_orderable in DatasetPageTopic.objects.all().order_by('topic__name') if page_orderable.page.live ] context['countries'] = self.get_active_countries() context['sources'] = DataSource.objects.all() context['reports'] = Page.objects.live().filter( models.Q(publicationpage__publication_datasets__isnull=False) | models.Q( publicationsummarypage__publication_datasets__isnull=False) | models.Q( publicationappendixpage__publication_datasets__isnull=False) | models.Q( legacypublicationpage__publication_datasets__isnull=False) | models.Q( publicationchapterpage__publication_datasets__isnull=False) | models.Q(shortpublicationpage__publication_datasets__isnull=False )).distinct().order_by('title') return context
class ProductPage(FoundationMetadataPageMixin, Page): """ ProductPage is the superclass that SoftwareProductPage and GeneralProductPage inherit from. This should not be an abstract model as we need it to connect the two page types together. """ privacy_ding = models.BooleanField( help_text='Tick this box if privacy is not included for this product', default=False, ) adult_content = models.BooleanField( help_text='When checked, product thumbnail will appear blurred as well as have an 18+ badge on it', default=False, ) uses_wifi = models.BooleanField( help_text='Does this product rely on WiFi connectivity?', default=False, ) uses_bluetooth = models.BooleanField( help_text='Does this product rely on Bluetooth connectivity?', default=False, ) review_date = models.DateField( help_text='Review date of this product', auto_now_add=True, ) company = models.CharField( max_length=100, help_text='Name of Company', blank=True, ) blurb = models.TextField( max_length=5000, help_text='Description of the product', blank=True ) # TODO: We'll need to update this URL in the template product_url = models.URLField( max_length=2048, help_text='Link to this product page', blank=True, ) price = models.CharField( max_length=100, help_text='Price', blank=True, ) image = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', help_text='Image representing this product', ) cloudinary_image = CloudinaryField( help_text='Image representing this product - hosted on Cloudinary', blank=True, verbose_name='image', folder='foundationsite/buyersguide', use_filename=True ) worst_case = models.CharField( max_length=5000, help_text="What's the worst thing that could happen by using this product?", blank=True, ) """ product_privacy_policy_links = Orderable, defined in ProductPagePrivacyPolicyLink Other "magic" relations that use InlinePanels will follow the same pattern of using Wagtail Orderables. """ # What is required to sign up? signup_requires_email = ExtendedYesNoField( help_text='Does this product requires providing an email address in order to sign up?' ) signup_requires_phone = ExtendedYesNoField( help_text='Does this product requires providing a phone number in order to sign up?' ) signup_requires_third_party_account = ExtendedYesNoField( help_text='Does this product require a third party account in order to sign up?' ) signup_requirement_explanation = models.TextField( max_length=5000, blank=True, help_text='Describe the particulars around sign-up requirements here.' ) # How does it use this data? how_does_it_use_data_collected = models.TextField( max_length=5000, blank=True, help_text='How does this product use the data collected?' ) data_collection_policy_is_bad = models.BooleanField( default=False, verbose_name='Privacy ding' ) # Privacy policy user_friendly_privacy_policy = ExtendedYesNoField( help_text='Does this product have a user-friendly privacy policy?' ) # Minimum security standards show_ding_for_minimum_security_standards = models.BooleanField( default=False, verbose_name="Privacy ding" ) meets_minimum_security_standards = models.BooleanField( null=True, blank=True, help_text='Does this product meet our minimum security standards?', ) uses_encryption = ExtendedYesNoField( help_text='Does the product use encryption?', ) uses_encryption_helptext = models.TextField( max_length=5000, blank=True ) security_updates = ExtendedYesNoField( help_text='Security updates?', ) security_updates_helptext = models.TextField( max_length=5000, blank=True ) strong_password = ExtendedYesNoField() strong_password_helptext = models.TextField( max_length=5000, blank=True ) manage_vulnerabilities = ExtendedYesNoField( help_text='Manages security vulnerabilities?', ) manage_vulnerabilities_helptext = models.TextField( max_length=5000, blank=True ) privacy_policy = ExtendedYesNoField( help_text='Does this product have a privacy policy?' ) privacy_policy_helptext = models.TextField( # REPURPOSED: WILL REQUIRE A 'clear' MIGRATION max_length=5000, blank=True ) # How to contact the company phone_number = models.CharField( max_length=100, help_text='Phone Number', blank=True, ) live_chat = models.CharField( max_length=100, help_text='Live Chat', blank=True, ) email = models.CharField( max_length=100, help_text='Email', blank=True, ) twitter = models.CharField( max_length=100, help_text='Twitter username', blank=True, ) content_panels = Page.content_panels + [ MultiFieldPanel( [ FieldPanel('privacy_ding'), FieldPanel('adult_content'), FieldPanel('company'), FieldPanel('product_url'), FieldPanel('price'), FieldPanel('uses_wifi'), FieldPanel('uses_bluetooth'), FieldPanel('blurb'), image_field, FieldPanel('worst_case'), ], heading='General Product Details', classname='collapsible' ), MultiFieldPanel( [ InlinePanel('product_categories', label='Category'), ], heading='Product Categories', classname='collapsible', ), MultiFieldPanel( [ FieldPanel('signup_requires_email'), FieldPanel('signup_requires_phone'), FieldPanel('signup_requires_third_party_account'), FieldPanel('signup_requirement_explanation'), ], heading='What is required to sign up', classname='collapsible' ), MultiFieldPanel( [ FieldPanel('how_does_it_use_data_collected'), FieldPanel('data_collection_policy_is_bad'), ], heading='How does it use this data', classname='collapsible', ), MultiFieldPanel( [ FieldPanel('user_friendly_privacy_policy'), ], heading='Privacy policy', classname='collapsible' ), MultiFieldPanel( [ InlinePanel( 'product_privacy_policy_links', label='link', min_num=1, max_num=3, ), ], heading='Privacy policy links', classname='collapsible' ), MultiFieldPanel( [ FieldPanel('show_ding_for_minimum_security_standards'), FieldPanel('meets_minimum_security_standards'), FieldPanel('uses_encryption'), FieldPanel('uses_encryption_helptext'), FieldPanel('security_updates'), FieldPanel('security_updates_helptext'), FieldPanel('strong_password'), FieldPanel('strong_password_helptext'), FieldPanel('manage_vulnerabilities'), FieldPanel('manage_vulnerabilities_helptext'), FieldPanel('privacy_policy'), FieldPanel('privacy_policy_helptext'), ], heading='Security', classname='collapsible' ), MultiFieldPanel( [ FieldPanel('phone_number'), FieldPanel('live_chat'), FieldPanel('email'), FieldPanel('twitter'), ], heading='Ways to contact the company', classname='collapsible' ), MultiFieldPanel( [ InlinePanel('product_updates', label='Update') ], heading='Product Updates', ), MultiFieldPanel( [ InlinePanel('related_product_pages', label='Product') ], heading='Related Products', ), ] class Meta: verbose_name = "Product Page"
class ExternalEvent(ExternalContent): resource_type = 'event' start_date = DateField( default=datetime.date.today, help_text='The date the event is scheduled to start') end_date = DateField(blank=True, null=True, help_text='The date the event is scheduled to end') venue = TextField( max_length=250, blank=True, default='', help_text= ('Full address of the event venue, displayed on the event detail page' )) location = CharField( max_length=100, blank=True, default='', help_text=( 'Location details (city and country), displayed on event cards')) meta_panels = [ MultiFieldPanel([ FieldPanel('start_date'), FieldPanel('end_date'), FieldPanel('venue'), FieldPanel('location'), ], heading='Event details'), InlinePanel( 'topics', heading='Topics', help_text= ('Optional topics this event is associated with. Adds the event to the list of events on those topic pages' )), InlinePanel( 'speakers', heading='Speakers', help_text= ('Optional speakers associated with this event. Adds the event to the list of events on their profile pages' )), ] edit_handler = TabbedInterface([ ObjectList(ExternalContent.card_panels, heading='Card'), ObjectList(meta_panels, heading='Meta'), ObjectList(Page.settings_panels, heading='Settings', classname='settings'), ]) @property def event(self): return self @property def month_group(self): return self.start_date.replace(day=1) @property def event_dates(self): """Return a formatted string of the event start and end dates""" event_dates = self.start_date.strftime("%b %-d") if self.end_date: event_dates += " – " start_month = self.start_date.strftime("%m") if self.end_date.strftime("%m") == start_month: event_dates += self.end_date.strftime("%-d") else: event_dates += self.end_date.strftime("%b %-d") return event_dates @property def event_dates_full(self): """Return a formatted string of the event start and end dates, including the year""" return self.event_dates + self.start_date.strftime(", %Y")
def get_absolute_url(self): return self.url def get_blog_index(self): # Find closest ancestor which is a blog index return self.get_ancestors().type(BlogIndexPage).last() def get_context(self, request, *args, **kwargs): context = super(BlogPage, self).get_context(request, *args, **kwargs) context['blogs'] = self.get_blog_index().blogindexpage.blogs context = get_blog_context(context) context['COMMENTS_APP'] = COMMENTS_APP return context class Meta: verbose_name = _('Blog page') verbose_name_plural = _('Blog pages') parent_page_types = ['blog.BlogIndexPage'] BlogPage.content_panels = [ FieldPanel('title', classname="full title"), MultiFieldPanel([ FieldPanel('tags'), InlinePanel('categories', label=_("Categories")), ], heading="Tags and Categories"), ImageChooserPanel('header_image'), FieldPanel('body', classname="full"), ]
class RecordPage(ContentPage): formatted_title = models.CharField( max_length=255, null=True, blank=True, default='', help_text= "Use if you need italics in the title. e.g. <em>Italicized words</em>") date = models.DateField(default=datetime.date.today) category = models.CharField( max_length=255, choices=constants.record_page_categories.items()) read_next = models.ForeignKey('RecordPage', blank=True, null=True, default=get_previous_record_page, on_delete=models.SET_NULL) related_section_title = models.CharField( max_length=255, blank=True, default='Explore campaign finance data') related_section_url = models.CharField(max_length=255, blank=True, default='/data/') monthly_issue = models.CharField(max_length=255, blank=True, default='') monthly_issue_url = models.CharField(max_length=255, blank=True, default='') keywords = ClusterTaggableManager(through=RecordPageTag, blank=True) homepage_pin = models.BooleanField(default=False) homepage_pin_expiration = models.DateField(blank=True, null=True) homepage_pin_start = models.DateField(blank=True, null=True) homepage_hide = models.BooleanField(default=False) template = 'home/updates/record_page.html' content_panels = ContentPage.content_panels + [ FieldPanel('formatted_title'), FieldPanel('date'), FieldPanel('monthly_issue'), FieldPanel('category'), FieldPanel('keywords'), InlinePanel('authors', label='Authors'), PageChooserPanel('read_next'), FieldPanel('related_section_title'), FieldPanel('related_section_url') ] promote_panels = Page.promote_panels + [ MultiFieldPanel([ FieldPanel('homepage_pin'), FieldPanel('homepage_pin_start'), FieldPanel('homepage_pin_expiration'), FieldPanel('homepage_hide') ], heading="Home page feed") ] search_fields = ContentPage.search_fields + [ index.FilterField('category'), index.FilterField('date') ] @property def content_section(self): return get_content_section(self) @property def get_update_type(self): return constants.update_types['fec-record'] @property def get_author_office(self): return 'Information Division'
def test_invalid_inlinepanel_declaration(self): with self.ignore_deprecation_warnings(): self.assertRaises(TypeError, lambda: InlinePanel(label="Speakers")) self.assertRaises( TypeError, lambda: InlinePanel( EventPage, 'speakers', label="Speakers", bacon="chunky"))
password_required_template = 'tests/event_page_password_required.html' base_form_class = EventPageForm EventPage.content_panels = [ FieldPanel('title', classname="full title"), FieldPanel('date_from'), FieldPanel('date_to'), FieldPanel('time_from'), FieldPanel('time_to'), FieldPanel('location'), FieldPanel('audience'), FieldPanel('cost'), FieldPanel('signup_link'), InlinePanel('carousel_items', label="Carousel items"), FieldPanel('body', classname="full"), InlinePanel('speakers', label="Speakers", heading="Speaker lineup"), InlinePanel('related_links', label="Related links"), FieldPanel('categories'), ] EventPage.promote_panels = [ MultiFieldPanel(COMMON_PANELS, "Common page configuration"), ImageChooserPanel('feed_image'), ] # Just to be able to test multi table inheritance class SingleEventPage(EventPage): excerpt = models.TextField(
def test_render_with_panel_overrides(self): """ Check that inline panel renders the panels listed in the InlinePanel definition where one is specified """ speaker_object_list = ObjectList([ InlinePanel('speakers', label="Speakers", panels=[ FieldPanel('first_name', widget=forms.Textarea), ImageChooserPanel('image'), ]), ]).bind_to(model=EventPage, request=self.request) speaker_inline_panel = speaker_object_list.children[0] EventPageForm = speaker_object_list.get_form_class() # speaker_inline_panel should instruct the form class to include a 'speakers' formset self.assertEqual(['speakers'], list(EventPageForm.formsets.keys())) event_page = EventPage.objects.get(slug='christmas') form = EventPageForm(instance=event_page) panel = speaker_inline_panel.bind_to(instance=event_page, form=form) result = panel.render_as_field() # rendered panel should contain first_name rendered as a text area, but no last_name field self.assertIn('<label for="id_speakers-0-first_name">Name:</label>', result) self.assertIn('Father</textarea>', result) self.assertNotIn( '<label for="id_speakers-0-last_name">Surname:</label>', result) # test for #338: surname field should not be rendered as a 'stray' label-less field self.assertTagInHTML('<input id="id_speakers-0-last_name">', result, count=0, allow_extra_attrs=True) self.assertIn('<label for="id_speakers-0-image">Image:</label>', result) self.assertIn('Choose an image', result) # rendered panel must also contain hidden fields for id, DELETE and ORDER self.assertTagInHTML( '<input id="id_speakers-0-id" name="speakers-0-id" type="hidden">', result, allow_extra_attrs=True) self.assertTagInHTML( '<input id="id_speakers-0-DELETE" name="speakers-0-DELETE" type="hidden">', result, allow_extra_attrs=True) self.assertTagInHTML( '<input id="id_speakers-0-ORDER" name="speakers-0-ORDER" type="hidden">', result, allow_extra_attrs=True) # rendered panel must contain maintenance form for the formset self.assertTagInHTML( '<input id="id_speakers-TOTAL_FORMS" name="speakers-TOTAL_FORMS" type="hidden">', result, allow_extra_attrs=True) # render_js_init must provide the JS initializer self.assertIn('var panel = InlinePanel({', panel.render_js_init())
class InlineStreamPage(Page): content_panels = [ FieldPanel('title', classname="full title"), InlinePanel('sections') ]
class InlinePanelSnippet(models.Model): panels = [InlinePanel('related_snippet_model')]
class Product(ClusterableModel): """ A thing you can buy in stores and our review of it """ draft = models.BooleanField( help_text= 'When checked, this product will only show for CMS-authenticated users', default=True, ) adult_content = models.BooleanField( help_text= 'When checked, product thumbnail will appear blurred as well as have an 18+ badge on it', default=False, ) review_date = models.DateField(help_text='Review date of this product', ) @property def is_current(self): d = self.review_date review = datetime(d.year, d.month, d.day) cutoff = datetime(2019, 6, 1) return cutoff < review name = models.CharField( max_length=100, help_text='Name of Product', blank=True, ) slug = models.CharField(max_length=256, help_text='slug used in urls', blank=True, default=None, editable=False) company = models.CharField( max_length=100, help_text='Name of Company', blank=True, ) product_category = models.ManyToManyField(BuyersGuideProductCategory, related_name='product', blank=True) blurb = models.TextField(max_length=5000, help_text='Description of the product', blank=True) url = models.URLField( max_length=2048, help_text='Link to this product page', blank=True, ) price = models.CharField( max_length=100, help_text='Price', blank=True, ) image = models.FileField( max_length=2048, help_text='Image representing this product', upload_to=get_product_image_upload_path, blank=True, ) cloudinary_image = CloudinaryImageField( help_text='Image representing this product - hosted on Cloudinary', blank=True, verbose_name='image', ) meets_minimum_security_standards = models.BooleanField( null=True, help_text='Does this product meet minimum security standards?', ) # Minimum security standards (stars) uses_encryption = ExtendedYesNoField( help_text='Does the product use encryption?', ) uses_encryption_helptext = models.TextField(max_length=5000, blank=True) security_updates = ExtendedYesNoField(help_text='Security updates?', ) security_updates_helptext = models.TextField(max_length=5000, blank=True) strong_password = ExtendedYesNoField() strong_password_helptext = models.TextField(max_length=5000, blank=True) manage_vulnerabilities = ExtendedYesNoField( help_text='Manages security vulnerabilities?', ) manage_vulnerabilities_helptext = models.TextField(max_length=5000, blank=True) privacy_policy = ExtendedYesNoField( help_text='Does this product have a privacy policy?') privacy_policy_helptext = models.TextField( # REPURPOSED: WILL REQUIRE A 'clear' MIGRATION max_length=5000, blank=True) share_data = models.BooleanField( # TO BE REMOVED null=True, help_text='Does the maker share data with other companies?', ) share_data_helptext = models.TextField( # TO BE REMOVED max_length=5000, blank=True) # It uses your... camera_device = ExtendedYesNoField( help_text='Does this device have or access a camera?', ) camera_app = ExtendedYesNoField( help_text='Does the app have or access a camera?', ) microphone_device = ExtendedYesNoField( help_text='Does this Device have or access a microphone?', ) microphone_app = ExtendedYesNoField( help_text='Does this app have or access a microphone?', ) location_device = ExtendedYesNoField( help_text='Does this product access your location?', ) location_app = ExtendedYesNoField( help_text='Does this app access your location?', ) # How it handles privacy how_does_it_share = models.CharField( max_length=5000, help_text='How does this product handle data?', blank=True) delete_data = models.BooleanField( # TO BE REMOVED null=True, help_text='Can you request data be deleted?', ) delete_data_helptext = models.TextField( # TO BE REMOVED max_length=5000, blank=True) parental_controls = ExtendedYesNoField( null=True, help_text='Are there rules for children?', ) child_rules_helptext = models.TextField( # TO BE REMOVED max_length=5000, blank=True) collects_biometrics = ExtendedYesNoField( help_text='Does this product collect biometric data?', ) collects_biometrics_helptext = models.TextField(max_length=5000, blank=True) user_friendly_privacy_policy = ExtendedYesNoField( help_text='Does this product have a user-friendly privacy policy?') user_friendly_privacy_policy_helptext = models.TextField(max_length=5000, blank=True) """ privacy_policy_links = one to many, defined in PrivacyPolicyLink """ worst_case = models.CharField( max_length=5000, help_text= "What's the worst thing that could happen by using this product?", blank=True, ) PP_CHOICES = ( # TO BE REMOVED ('0', 'Can\'t Determine'), ('7', 'Grade 7'), ('8', 'Grade 8'), ('9', 'Grade 9'), ('10', 'Grade 10'), ('11', 'Grade 11'), ('12', 'Grade 12'), ('13', 'Grade 13'), ('14', 'Grade 14'), ('15', 'Grade 15'), ('16', 'Grade 16'), ('17', 'Grade 17'), ('18', 'Grade 18'), ('19', 'Grade 19'), ) privacy_policy_reading_level = models.CharField( # TO BE REMOVED IN FAVOUR OF USER_FRIENDLY_PRIVACY_POLICY choices=PP_CHOICES, default='0', max_length=2, ) privacy_policy_url = models.URLField( # TO BE REMOVED IN FAVOUR OF PRIVACY_POLICY_LINKS max_length=2048, help_text='Link to privacy policy', blank=True) privacy_policy_reading_level_url = models.URLField( # TO BE REMOVED max_length=2048, help_text='Link to privacy policy reading level', blank=True) # How to contact the company phone_number = models.CharField( max_length=100, help_text='Phone Number', blank=True, ) live_chat = models.CharField( max_length=100, help_text='Live Chat', blank=True, ) email = models.CharField( max_length=100, help_text='Email', blank=True, ) twitter = models.CharField( max_length=100, help_text='Twitter username', blank=True, ) updates = models.ManyToManyField(Update, related_name='products', blank=True) # comments are not a model field, but are "injected" on the product page instead related_products = models.ManyToManyField('self', related_name='rps', blank=True, symmetrical=False) # --- if settings.USE_CLOUDINARY: image_field = FieldPanel('cloudinary_image') else: image_field = FieldPanel('image') # List of fields to show in admin to hide the image/cloudinary_image field. There's probably a better way to do # this using `_meta.get_fields()`. To be refactored in the future. panels = [ MultiFieldPanel([ FieldPanel('draft'), ], heading="Publication status", classname="collapsible"), MultiFieldPanel([ FieldPanel('adult_content'), FieldPanel('review_date'), FieldPanel('name'), FieldPanel('company'), FieldPanel('product_category'), FieldPanel('blurb'), FieldPanel('url'), FieldPanel('price'), image_field, FieldPanel('meets_minimum_security_standards') ], heading="General Product Details", classname="collapsible"), MultiFieldPanel( [ FieldPanel('uses_encryption'), FieldPanel('uses_encryption_helptext'), FieldPanel('security_updates'), FieldPanel('security_updates_helptext'), FieldPanel('strong_password'), FieldPanel('strong_password_helptext'), FieldPanel('manage_vulnerabilities'), FieldPanel('manage_vulnerabilities_helptext'), FieldPanel('privacy_policy'), FieldPanel( 'privacy_policy_helptext'), # NEED A "clear" MIGRATION FieldPanel('share_data'), FieldPanel('share_data_helptext'), # DEPRECATED AND WILL BE REMOVED FieldPanel('privacy_policy_url'), FieldPanel('privacy_policy_reading_level'), FieldPanel('privacy_policy_reading_level_url'), ], heading="Minimum Security Standards", classname="collapsible"), MultiFieldPanel([ FieldPanel('camera_device'), FieldPanel('camera_app'), FieldPanel('microphone_device'), FieldPanel('microphone_app'), FieldPanel('location_device'), FieldPanel('location_app'), ], heading="Can it snoop?", classname="collapsible"), MultiFieldPanel([ FieldPanel('how_does_it_share'), FieldPanel('delete_data'), FieldPanel('delete_data_helptext'), FieldPanel('parental_controls'), FieldPanel('collects_biometrics'), FieldPanel('collects_biometrics_helptext'), FieldPanel('user_friendly_privacy_policy'), FieldPanel('user_friendly_privacy_policy_helptext'), FieldPanel('worst_case'), ], heading="How does it handle privacy", classname="collapsible"), MultiFieldPanel([ InlinePanel( 'privacy_policy_links', label='link', min_num=1, max_num=3, ), ], heading="Privacy policy links", classname="collapsible"), MultiFieldPanel([ FieldPanel('phone_number'), FieldPanel('live_chat'), FieldPanel('email'), FieldPanel('twitter'), ], heading="Ways to contact the company", classname="collapsible"), FieldPanel('updates'), FieldPanel('related_products'), ] @property def votes(self): votes = {} confidence_vote_breakdown = {} creepiness = {'vote_breakdown': {}} try: # Get vote QuerySets creepiness_votes = self.range_product_votes.get( attribute='creepiness') confidence_votes = self.boolean_product_votes.get( attribute='confidence') # Aggregate the Creepiness votes creepiness['average'] = creepiness_votes.average for vote_breakdown in creepiness_votes.rangevotebreakdown_set.all( ): creepiness['vote_breakdown'][str( vote_breakdown.bucket)] = vote_breakdown.count # Aggregate the confidence votes for boolean_vote_breakdown in confidence_votes.booleanvotebreakdown_set.all( ): confidence_vote_breakdown[str(boolean_vote_breakdown.bucket )] = boolean_vote_breakdown.count # Build + return the votes dict votes['creepiness'] = creepiness votes['confidence'] = confidence_vote_breakdown votes['total'] = BooleanVote.objects.filter(product=self).count() return votes except ObjectDoesNotExist: # There's no aggregate data available yet, return None return None @property def numeric_reading_grade(self): try: return int(self.privacy_policy_reading_level) except ValueError: return 0 @property def reading_grade(self): val = self.numeric_reading_grade if val == 0: return 0 if val <= 8: return 'Middle school' if val <= 12: return 'High school' if val <= 16: return 'College' return 'Grad school' def to_dict(self): """ Rather than rendering products based on the instance object, we serialize the product to a dictionary, and instead render the template based on that. NOTE: if you add indirect fields like Foreign/ParentalKey or @property definitions, those needs to be added! """ model_dict = model_to_dict(self) model_dict['votes'] = self.votes model_dict['slug'] = self.slug model_dict['delete_data'] = tri_to_quad(self.delete_data) # TODO: remove these two entries model_dict['numeric_reading_grade'] = self.numeric_reading_grade model_dict['reading_grade'] = self.reading_grade # model_to_dict does NOT capture related fields or @properties! model_dict['privacy_policy_links'] = list( self.privacy_policy_links.all()) model_dict['is_current'] = self.is_current return model_dict def save(self, *args, **kwargs): self.slug = slugify(self.name) models.Model.save(self, *args, **kwargs) def __str__(self): return str(self.name)
class Event(Page): resource_type = 'event' parent_page_types = ['events.Events'] subpage_types = [] template = 'event.html' # Content fields description = TextField( blank=True, default='', help_text='Optional short text description, max. 400 characters', max_length=400, ) image = ForeignKey( 'mozimages.MozImage', null=True, blank=True, on_delete=SET_NULL, related_name='+', ) body = CustomStreamField( blank=True, null=True, help_text= ('Optional body content. Supports rich text, images, embed via URL, embed via HTML, and inline code snippets' )) agenda = StreamField( StreamBlock([ ('agenda_item', AgendaItemBlock()), ], required=False), blank=True, null=True, help_text='Optional list of agenda items for this event', ) speakers = StreamField( StreamBlock([ ('speaker', PageChooserBlock(target_model='people.Person')), ('external_speaker', ExternalSpeakerBlock()), ], required=False), blank=True, null=True, help_text='Optional list of speakers for this event', ) # Card fields card_title = CharField('Title', max_length=140, blank=True, default='') card_description = TextField('Description', max_length=400, blank=True, default='') card_image = ForeignKey( 'mozimages.MozImage', null=True, blank=True, on_delete=SET_NULL, related_name='+', verbose_name='Image', ) # Meta fields start_date = DateField(default=datetime.date.today) end_date = DateField(blank=True, null=True) latitude = FloatField(blank=True, null=True) longitude = FloatField(blank=True, null=True) register_url = URLField('Register URL', blank=True, null=True) venue_name = CharField(max_length=100, blank=True, default='') venue_url = URLField('Venue URL', max_length=100, blank=True, default='') address_line_1 = CharField(max_length=100, blank=True, default='') address_line_2 = CharField(max_length=100, blank=True, default='') address_line_3 = CharField(max_length=100, blank=True, default='') city = CharField(max_length=100, blank=True, default='') state = CharField('State/Province/Region', max_length=100, blank=True, default='') zip_code = CharField('Zip/Postal code', max_length=100, blank=True, default='') country = CountryField(blank=True, default='') keywords = ClusterTaggableManager(through=EventTag, blank=True) # Content panels content_panels = Page.content_panels + [ FieldPanel('description'), MultiFieldPanel( [ ImageChooserPanel('image'), ], heading='Image', help_text= ('Optional header image. If not specified a fallback will be used. This image is also shown when sharing ' 'this page via social media')), StreamFieldPanel('body'), StreamFieldPanel('agenda'), StreamFieldPanel('speakers'), ] # Card panels card_panels = [ FieldPanel('card_title'), FieldPanel('card_description'), ImageChooserPanel('card_image'), ] # Meta panels meta_panels = [ MultiFieldPanel( [ FieldPanel('start_date'), FieldPanel('end_date'), FieldPanel('latitude'), FieldPanel('longitude'), FieldPanel('register_url'), ], heading='Event details', classname='collapsible', help_text=mark_safe( 'Optional time and location information for this event. Latitude and longitude are used to show a map ' 'of the event’s location. For more information on finding these values for a given location, ' '<a href="https://support.google.com/maps/answer/18539">see this article</a>' )), MultiFieldPanel( [ FieldPanel('venue_name'), FieldPanel('venue_url'), FieldPanel('address_line_1'), FieldPanel('address_line_2'), FieldPanel('address_line_3'), FieldPanel('city'), FieldPanel('state'), FieldPanel('zip_code'), FieldPanel('country'), ], heading='Event address', classname='collapsible', help_text= 'Optional address fields. The city and country are also shown on event cards' ), MultiFieldPanel( [ InlinePanel('topics'), ], heading='Topics', help_text= ('These are the topic pages the event will appear on. The first topic in the list will be treated as the ' 'primary topic and will be shown in the page’s related content.' )), MultiFieldPanel( [ FieldPanel('seo_title'), FieldPanel('search_description'), FieldPanel('keywords'), ], heading='SEO', help_text= 'Optional fields to override the default title and description for SEO purposes' ), ] # Settings panels settings_panels = [ FieldPanel('slug'), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(card_panels, heading='Card'), ObjectList(meta_panels, heading='Meta'), ObjectList(settings_panels, heading='Settings', classname='settings'), ]) @property def is_upcoming(self): """Returns whether an event is in the future.""" return self.start_date > datetime.date.today() @property def primary_topic(self): """Return the first (primary) topic specified for the event.""" article_topic = self.topics.first() return article_topic.topic if article_topic else None @property def month_group(self): return self.start_date.replace(day=1) @property def event_dates(self): """Return a formatted string of the event start and end dates""" event_dates = self.start_date.strftime("%b %-d") if self.end_date: event_dates += " – " start_month = self.start_date.strftime("%m") if self.end_date.strftime("%m") == start_month: event_dates += self.end_date.strftime("%-d") else: event_dates += self.end_date.strftime("%b %-d") return event_dates @property def event_dates_full(self): """Return a formatted string of the event start and end dates, including the year""" return self.event_dates + self.start_date.strftime(", %Y") def has_speaker(self, person): for speaker in self.speakers: # pylint: disable=not-an-iterable if (speaker.block_type == 'speaker' and str(speaker.value) == str(person.title)): return True return False