コード例 #1
0
            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')
コード例 #2
0
class SectionedRichTextPage(Page):
    content_panels = [
        FieldPanel('title', classname="full title"),
        InlinePanel('sections')
    ]
コード例 #3
0
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)
コード例 #4
0
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 = []
コード例 #5
0
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'
コード例 #6
0
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
コード例 #7
0
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")
    ]
コード例 #8
0
ファイル: models.py プロジェクト: HiteshMah-Jan/WF-website
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()
コード例 #9
0
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"),
    ]
コード例 #10
0
class StaffSection(Page):
    content_panels = Page.content_panels + [
        InlinePanel('staff', label="Staff"),
    ]
コード例 #11
0
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']
コード例 #12
0
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
コード例 #13
0
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'
コード例 #14
0
            # 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/$')
コード例 #15
0
class InlinePanelPage(WagtailPage):
    content_panels = [InlinePanel('related_page_model')]
コード例 #16
0
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),
    ]
コード例 #17
0
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
コード例 #18
0
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
コード例 #19
0
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
コード例 #20
0
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"
コード例 #21
0
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 += " &ndash; "
            start_month = self.start_date.strftime("%m")
            if self.end_date.strftime("%m") == start_month:
                event_dates += self.end_date.strftime("%-d")
            else:
                event_dates += self.end_date.strftime("%b %-d")
        return event_dates

    @property
    def event_dates_full(self):
        """Return a formatted string of the event start and end dates, including the year"""
        return self.event_dates + self.start_date.strftime(", %Y")
コード例 #22
0
    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"),
]
コード例 #23
0
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'
コード例 #24
0
 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"))
コード例 #25
0
    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(
コード例 #26
0
    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())
コード例 #27
0
class InlineStreamPage(Page):
    content_panels = [
        FieldPanel('title', classname="full title"),
        InlinePanel('sections')
    ]
コード例 #28
0
class InlinePanelSnippet(models.Model):
    panels = [InlinePanel('related_snippet_model')]
コード例 #29
0
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)
コード例 #30
0
ファイル: models.py プロジェクト: jrobind/developer-portal
class Event(Page):
    resource_type = 'event'
    parent_page_types = ['events.Events']
    subpage_types = []
    template = 'event.html'

    # Content fields
    description = TextField(
        blank=True,
        default='',
        help_text='Optional short text description, max. 400 characters',
        max_length=400,
    )
    image = ForeignKey(
        'mozimages.MozImage',
        null=True,
        blank=True,
        on_delete=SET_NULL,
        related_name='+',
    )
    body = CustomStreamField(
        blank=True,
        null=True,
        help_text=
        ('Optional body content. Supports rich text, images, embed via URL, embed via HTML, and inline code snippets'
         ))
    agenda = StreamField(
        StreamBlock([
            ('agenda_item', AgendaItemBlock()),
        ], required=False),
        blank=True,
        null=True,
        help_text='Optional list of agenda items for this event',
    )
    speakers = StreamField(
        StreamBlock([
            ('speaker', PageChooserBlock(target_model='people.Person')),
            ('external_speaker', ExternalSpeakerBlock()),
        ],
                    required=False),
        blank=True,
        null=True,
        help_text='Optional list of speakers for this event',
    )

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

    # Meta fields
    start_date = DateField(default=datetime.date.today)
    end_date = DateField(blank=True, null=True)
    latitude = FloatField(blank=True, null=True)
    longitude = FloatField(blank=True, null=True)
    register_url = URLField('Register URL', blank=True, null=True)
    venue_name = CharField(max_length=100, blank=True, default='')
    venue_url = URLField('Venue URL', max_length=100, blank=True, default='')
    address_line_1 = CharField(max_length=100, blank=True, default='')
    address_line_2 = CharField(max_length=100, blank=True, default='')
    address_line_3 = CharField(max_length=100, blank=True, default='')
    city = CharField(max_length=100, blank=True, default='')
    state = CharField('State/Province/Region',
                      max_length=100,
                      blank=True,
                      default='')
    zip_code = CharField('Zip/Postal code',
                         max_length=100,
                         blank=True,
                         default='')
    country = CountryField(blank=True, default='')
    keywords = ClusterTaggableManager(through=EventTag, blank=True)

    # Content panels
    content_panels = Page.content_panels + [
        FieldPanel('description'),
        MultiFieldPanel(
            [
                ImageChooserPanel('image'),
            ],
            heading='Image',
            help_text=
            ('Optional header image. If not specified a fallback will be used. This image is also shown when sharing '
             'this page via social media')),
        StreamFieldPanel('body'),
        StreamFieldPanel('agenda'),
        StreamFieldPanel('speakers'),
    ]

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

    # Meta panels
    meta_panels = [
        MultiFieldPanel(
            [
                FieldPanel('start_date'),
                FieldPanel('end_date'),
                FieldPanel('latitude'),
                FieldPanel('longitude'),
                FieldPanel('register_url'),
            ],
            heading='Event details',
            classname='collapsible',
            help_text=mark_safe(
                'Optional time and location information for this event. Latitude and longitude are used to show a map '
                'of the event’s location. For more information on finding these values for a given location, '
                '<a href="https://support.google.com/maps/answer/18539">see this article</a>'
            )),
        MultiFieldPanel(
            [
                FieldPanel('venue_name'),
                FieldPanel('venue_url'),
                FieldPanel('address_line_1'),
                FieldPanel('address_line_2'),
                FieldPanel('address_line_3'),
                FieldPanel('city'),
                FieldPanel('state'),
                FieldPanel('zip_code'),
                FieldPanel('country'),
            ],
            heading='Event address',
            classname='collapsible',
            help_text=
            'Optional address fields. The city and country are also shown on event cards'
        ),
        MultiFieldPanel(
            [
                InlinePanel('topics'),
            ],
            heading='Topics',
            help_text=
            ('These are the topic pages the event will appear on. The first topic in the list will be treated as the '
             'primary topic and will be shown in the page’s related content.'
             )),
        MultiFieldPanel(
            [
                FieldPanel('seo_title'),
                FieldPanel('search_description'),
                FieldPanel('keywords'),
            ],
            heading='SEO',
            help_text=
            'Optional fields to override the default title and description for SEO purposes'
        ),
    ]

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

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

    @property
    def is_upcoming(self):
        """Returns whether an event is in the future."""
        return self.start_date > datetime.date.today()

    @property
    def primary_topic(self):
        """Return the first (primary) topic specified for the event."""
        article_topic = self.topics.first()
        return article_topic.topic if article_topic else None

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

    @property
    def event_dates(self):
        """Return a formatted string of the event start and end dates"""
        event_dates = self.start_date.strftime("%b %-d")
        if self.end_date:
            event_dates += " &ndash; "
            start_month = self.start_date.strftime("%m")
            if self.end_date.strftime("%m") == start_month:
                event_dates += self.end_date.strftime("%-d")
            else:
                event_dates += self.end_date.strftime("%b %-d")
        return event_dates

    @property
    def event_dates_full(self):
        """Return a formatted string of the event start and end dates, including the year"""
        return self.event_dates + self.start_date.strftime(", %Y")

    def has_speaker(self, person):
        for speaker in self.speakers:  # pylint: disable=not-an-iterable
            if (speaker.block_type == 'speaker'
                    and str(speaker.value) == str(person.title)):
                return True
        return False