class BlogPage(Page): body = StreamField(STANDARD_BLOCKS) tags = ClusterTaggableManager(through='blog.BlogPageTag', blank=True) date = models.DateField( _("Post date"), default=datetime.datetime.today, help_text=_("This date may be displayed on the blog post. It is not " "used to schedule posts to go live at a later date.")) header_image = models.ForeignKey(get_image_model_string(), null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name=_('Header Image')) author = models.ForeignKey( settings.AUTH_USER_MODEL, blank=True, null=True, limit_choices_to=limit_author_choices, verbose_name=_('Author'), on_delete=models.SET_NULL, related_name='author_pages', ) search_fields = Page.search_fields + [ index.SearchField('body'), ] blog_categories = models.ManyToManyField( 'blog.BlogCategory', through='blog.BlogCategoryBlogPage', blank=True) settings_panels = [ MultiFieldPanel([ FieldRowPanel([ FieldPanel('go_live_at'), FieldPanel('expire_at'), ], classname="label-above"), ], 'Scheduled publishing', classname="publishing"), FieldPanel('date'), FieldPanel('author'), ] content_panels = [ FieldPanel('title', classname="full title"), MultiFieldPanel([ FieldPanel('tags'), InlinePanel('categories', label=_("Categories")), ], heading="Tags and Categories"), ImageChooserPanel('header_image'), StreamFieldPanel('body', classname="full title"), ] def save_revision(self, *args, **kwargs): if not self.author: self.author = self.owner return super(BlogPage, self).save_revision(*args, **kwargs) 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'] = settings.COMMENTS_APP return context class Meta: verbose_name = _('Blog Page') verbose_name_plural = _('Blog Pages') parent_page_types = ['blog.BlogIndexPage']
class GeneralShelvePage(Page): release = models.ForeignKey( 'release.Release', related_name='%(class)s_pages', blank=True, null=True, default=None, on_delete=models.SET_NULL, limit_choices_to={'content_status': CONTENT_STATUS_PENDING}) page_theme = models.ForeignKey(Theme, related_name='%(class)s_pages', null=True, on_delete=models.SET_NULL, verbose_name='theme') hide_from_breadcrumb = models.BooleanField(default=False) @property def og_image(self): try: return self.og_image_fk.file.url except AttributeError: pass except ValueError: pass return '' @property def twitter_image(self): try: return self.twitter_image_fk.file.url except AttributeError: pass except ValueError: pass return '' @property def theme(self): return self.page_theme.to_dict() @property def link_url(self): # TODO: This could potentially use some base page methods import re url_path = self.url_path # Remove homepage slug from url_path site_settings = SiteSettings.objects.get(site=self.get_site()) homepage_slug_path = site_settings.site.root_page.slug regexp = r'/{0}(/.*)'.format(homepage_slug_path) matchObj = re.match(regexp, self.url_path) if matchObj: url_path = matchObj.group(1) return '/' + site_settings.uid + url_path @property def breadcrumbs(self): ancestors = self.get_ancestors().live()[1:] breadcrumbs = [] for ancestor in ancestors: # If root page it doesn't have link url try: breadcrumbs.append({ 'name': ancestor.specific.seo_title or ancestor.specific.title, 'url': ancestor.specific.link_url, 'visible': not ancestor.specific.hide_from_breadcrumb, }) except AttributeError: site_name = SiteSettings.objects.get(site=self.get_site()).uid breadcrumbs.append({ 'name': ancestor.specific.seo_title or ancestor.specific.title, 'url': '/' + site_name, 'visible': True, }) return breadcrumbs content_panels = Page.content_panels + [ StreamFieldPanel('body'), FieldPanel('release'), SnippetChooserPanel('page_theme'), ] info_content_panels = [ InlinePanel('change_history', label='Change history'), ] meta_content_panels = [ MultiFieldPanel([ FieldPanel('og_title'), FieldPanel('og_description'), FieldPanel('og_url'), ImageChooserPanel('og_image_fk'), FieldPanel('og_type'), ], heading='Open Graph Tags', classname='collapsible collapsed'), MultiFieldPanel([ FieldPanel('twitter_url'), FieldPanel('twitter_card'), FieldPanel('twitter_site'), FieldPanel('twitter_title'), FieldPanel('twitter_description'), ImageChooserPanel('twitter_image_fk'), ], heading='Twitter Tags', classname='collapsible collapsed'), MultiFieldPanel([ FieldPanel('use_share_button'), FieldPanel('use_email_button'), FieldPanel('use_print_button'), ], heading='Share buttons', classname='collapsible collapsed'), MultiFieldPanel([ FieldPanel('tracking_group'), ], heading='Tracking', classname='collapsible collapsed'), ] promote_panels = [ MultiFieldPanel( [ FieldPanel('slug'), FieldPanel('seo_title'), FieldPanel('show_in_menus'), FieldPanel('search_description'), FieldPanel('hide_from_breadcrumb'), ], heading='Common page configuration', ) ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(info_content_panels, heading='Notes'), ObjectList(meta_content_panels, heading='Meta'), ObjectList(promote_panels, heading='Settings'), ]) api_fields = ['body', 'path', 'depth', 'numchild', 'live', 'theme'] exclude_fields_in_copy = ['release'] class Meta: abstract = True @classmethod def get_serializer(cls): import importlib app = cls.__module__.rsplit('.', 1)[0] module = '{}.serializers'.format(app) serializer_name = '{}Serializer'.format(cls.__name__) serializers_module = importlib.import_module(module) serializer = getattr(serializers_module, serializer_name) return serializer @classmethod def create_from_dict(cls, obj_dict): return cls(title=obj_dict['title'], path=obj_dict['path'], depth=obj_dict['depth'], numchild=obj_dict['numchild'], slug=obj_dict['meta']['slug'], seo_title=obj_dict['meta']['seo_title'], show_in_menus=obj_dict['meta']['show_in_menus'], search_description=obj_dict['meta']['search_description'], first_published_at=obj_dict['meta']['first_published_at'], body=json.dumps(obj_dict['body']), live=obj_dict['live'], page_theme_id=obj_dict['theme']['id']) def update_from_dict(self, obj_dict, default_excludes=None, excludes=None): if not default_excludes: class_ptr_id = '{}_ptr_id'.format(t.__class__.__name__.lower()) default_excludes = [ 'id', 'path', 'depth', 'numchild', 'content_type_id', 'live_revision_id', 'page_ptr_id', class_ptr_id, 'release_id', 'live', 'locked', 'url_path' ] if not excludes: excludes = [] excludes = default_excludes + excludes for key, value in obj_dict.items(): if key not in excludes and not key.startswith('_'): setattr(self, key, value) return self def serializable_data(self): obj = get_serializable_data_for_fields(self) for rel in get_all_child_relations(self): rel_name = rel.get_accessor_name() children = getattr(self, rel_name).all() if hasattr(rel.related_model, 'serializable_data'): obj[rel_name] = [ child.serializable_data() for child in children ] else: obj[rel_name] = [ get_serializable_data_for_fields(child) for child in children ] for field in get_all_child_m2m_relations(self): children = getattr(self, field.name).all() obj[field.name] = [child.pk for child in children] return obj def save_revision(self, user=None, submitted_for_moderation=False, approved_go_live_at=None, changed=True): assigned_release = self.release self.release = None revision = super(GeneralShelvePage, self).save_revision(user, submitted_for_moderation, approved_go_live_at, changed) if assigned_release: from release.models import ReleasePage if submitted_for_moderation: ReleasePage.submit_for_moderation(revision, assigned_release) else: assigned_release.add_revision(revision) return revision def unpublish(self, release_id=None): if not release_id: pass else: from release.models import Release try: release = Release.objects.get(id=release_id) release.remove_page(self.id) except Release.DoesNotExist: pass def serve_preview(self, request, mode_name, site_name, revision_id='latest'): request.is_preview = True if mode_name == 'json': Serializer = self.__class__.get_serializer() latest_revision_as_page = self.get_latest_revision_as_page() serialized_page = Serializer(instance=latest_revision_as_page) return JsonResponse(serialized_page.data) if mode_name == 'react': path = self.get_url_parts(request)[2] if self.get_url_parts( request) is not None else '/home' context = { 'preview_url': '/{}{}?is_preview&revision={}'.format(site_name, path, revision_id) } return SimpleTemplateResponse(template='preview_wrapper.html', context=context) return self.serve(request) def serializable_data(self): obj = get_serializable_data_for_fields(self) for rel in get_all_child_relations(self): rel_name = rel.get_accessor_name() children = getattr(self, rel_name).all() if hasattr(rel.related_model, 'serializable_data'): obj[rel_name] = [ child.serializable_data() for child in children ] else: obj[rel_name] = [ get_serializable_data_for_fields(child) for child in children ] for field in get_all_child_m2m_relations(self): children = getattr(self, field.name).all() obj[field.name] = [child.pk for child in children] return obj def serve(self, request, *args, **kwargs): request.is_preview = getattr(request, 'is_preview', False) return TemplateResponse(request, self.get_template(request, *args, **kwargs), self.get_context(request, *args, **kwargs)) DEFAULT_PREVIEW_MODES = [ ('react', 'Default'), # ('html', 'AMP'), ('json', 'API'), ] @property def preview_modes(self): """ A list of (internal_name, display_name) tuples for the modes in which this page can be displayed for preview/moderation purposes. Ordinarily a page will only have one display mode, but subclasses of Page can override this - for example, a page containing a form might have a default view of the form, and a post-submission 'thankyou' page """ return self.DEFAULT_PREVIEW_MODES @property def default_preview_mode(self): return self.preview_modes[0][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'), # InlinePanel related model uses `pk` not `id` InlinePanel('head_counts', label='Head Counts'), ] EventPage.promote_panels = [ MultiFieldPanel(COMMON_PANELS, "Common page configuration"), ImageChooserPanel('feed_image'), ] class HeadCountRelatedModelUsingPK(models.Model):
class Project(BasePage, ClusterableModel): """Page type for a CDH sponsored project or working group.""" short_description = models.CharField( max_length=255, blank=True, help_text="Brief tagline for display on project card in browse view", ) highlight = models.BooleanField( default=False, help_text="Include in randomized project display on the home page.", ) cdh_built = models.BooleanField( "CDH Built", default=False, help_text="Project built by CDH Development & Design team.", ) working_group = models.BooleanField( "Working Group", default=False, help_text= "Project is a long-term collaborative group associated with the CDH.", ) image = models.ForeignKey( "wagtailimages.image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", help_text="Image for display on project detail page (optional)", ) thumbnail = models.ForeignKey( "wagtailimages.image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", help_text="Image for display on project card (optional)", ) members = models.ManyToManyField(Person, through="Membership") tags = ClusterTaggableManager(through=ProjectTag, blank=True) # TODO attachments (#245) # can only be created underneath project landing page parent_page_types = ["projects.ProjectsLandingPage"] # no allowed subpages subpage_types = [] # admin edit configuration content_panels = Page.content_panels + [ FieldRowPanel( ( FieldPanel("highlight"), FieldPanel("cdh_built"), FieldPanel("working_group"), ), "Settings", ), FieldRowPanel( (ImageChooserPanel("thumbnail"), ImageChooserPanel("image")), "Images"), FieldPanel("short_description"), StreamFieldPanel("body"), InlinePanel("related_links", label="Links"), InlinePanel( "grants", panels=[ FieldRowPanel( (FieldPanel("start_date"), FieldPanel("end_date"))), FieldPanel("grant_type"), ], label="Grants", ), InlinePanel( "memberships", panels=[ FieldRowPanel( (FieldPanel("start_date"), FieldPanel("end_date"))), FieldPanel("person"), FieldPanel("role"), ], label="Members", ), StreamFieldPanel("attachments"), ] promote_panels = Page.promote_panels + [FieldPanel("tags")] # custom manager/queryset logic objects = ProjectManager() # search fields search_fields = BasePage.search_fields + [ index.SearchField("short_description"), index.RelatedFields( "members", [ index.SearchField("first_name"), index.SearchField("last_name"), ], ), ] def __str__(self): return self.title @property def website_url(self): """URL for this Project's website, if set""" website = self.related_links.filter(type__name="Website").first() if website: return website.url def latest_grant(self): """Most recent :class:`Grant` for this Project""" if self.grants.count(): return self.grants.order_by("-start_date").first() def current_memberships(self): """:class:`MembershipQueryset` of current members sorted by role""" # NOTE memberships is a FakeQuerySet from modelcluster.ParentalKey when # the page is being previewed in wagtail, so Q lookups are not possible. # see: https://github.com/wagtail/django-modelcluster/issues/121 memberships = Membership.objects.filter(project__pk=self.pk) # uses memberships rather than members so that we can retain role # information attached to the membership today = timezone.now().date() # if the last grant for this project is over, display the team # for that grant period latest_grant = self.latest_grant() if latest_grant and latest_grant.end_date and latest_grant.end_date < today: return memberships.filter( start_date__lte=latest_grant.end_date).filter( models.Q(end_date__gte=latest_grant.start_date) | models.Q(end_date__isnull=True)) # otherwise, return current members based on date return memberships.filter(start_date__lte=today).filter( models.Q(end_date__gte=today) | models.Q(end_date__isnull=True)) def alums(self): """:class:`PersonQueryset` of past members sorted by last name""" # uses people rather than memberships so that we can use distinct() # to ensure people aren't counted multiple times for each grant # and because we don't care about role (always 'alum') return (self.members.distinct().exclude( membership__in=self.current_memberships()).order_by("last_name")) def get_sitemap_urls(self, request): """Override sitemap to prioritize projects built by CDH with a website.""" # output is a list of dict; there should only ever be one element. see: # https://docs.wagtail.io/en/stable/reference/contrib/sitemaps.html#urls urls = super().get_sitemap_urls(request=request) if self.website_url and self.cdh_built: urls[0]["priority"] = 0.7 elif self.website_url or self.cdh_built: urls[0]["priority"] = 0.6 return urls
class YekoGarageBlogPage(Page): """ A Blog Page We access the People object with an inline panel that references the ParentalKey's related_name in BlogPeopleRelationship. More docs: http://docs.wagtail.io/en/latest/topics/pages.html#inline-models """ 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( BlogStreamBlock(), verbose_name="Page body", blank=True ) subtitle = models.CharField(blank=True, max_length=255) tags = ClusterTaggableManager(through=YekoGarageBlogPageTag, blank=True) date_published = models.DateField( "Date article published", blank=True, null=True ) search_keywords = ClusterTaggableManager(through='YekoGarageBlogPageKeyword', blank=True, verbose_name='Keywords', related_name='yeko_garage_blog_page_search_keywords', help_text='Meta Keywords, son palabras clave que se usan para los buscadores') content_panels = Page.content_panels + [ FieldPanel('subtitle', classname="full"), FieldPanel('introduction', classname="full"), StreamFieldPanel('body'), FieldPanel('date_published'), InlinePanel( 'yeko_garage_blog_person_relationship', label="Author(s)", panels=None, min_num=1), FieldPanel('tags'), ] promote_panels = Page.promote_panels + [ ImageChooserPanel('image'), FieldPanel('search_keywords'), ] search_fields = Page.search_fields + [ index.SearchField('body'), ] def authors(self): """ Returns the BlogPage's related People. Again note that we are using the ParentalKey's related_name from the BlogPeopleRelationship model to access these objects. This allows us to access the People objects with a loop on the template. If we tried to access the blog_person_ relationship directly we'd print `blog.BlogPeopleRelationship.None` """ authors = [ n.people for n in self.yeko_garage_blog_person_relationship.all() ] return authors @property def get_tags(self): """ Similar to the authors function above we're returning all the tags that are related to the blog post into a list we can access on the template. We're additionally adding a URL to access BlogPage objects with that tag """ tags = self.tags.all() for tag in tags: tag.url = '/'+'/'.join(s.strip('/') for s in [ self.get_parent().url, 'tags', tag.slug ]) return tags # Specifies parent to BlogPage as being BlogIndexPages parent_page_types = ['YekoGarageBlogIndexPage'] # Specifies what content types can exist as children of BlogPage. # Empty list means that no child content types are allowed. subpage_types = []
class Homepage(MetadataPageMixin, 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', ] 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) print(settings.MEDIA_URL) context['MEDIA_URL'] = settings.MEDIA_URL return context
secondary_topics=BlogPageSecondaryTopic.objects.all().values_list( 'topic__pk', 'topic__title').distinct(), query_string=query_string, 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 = "Blog, News, Statement Page" BlogPage.content_panels = Page.content_panels + [ InlinePanel('authors', label="Authors"), SnippetChooserPanel('author_group'), FieldPanel('date_published'), FieldPanel('introduction'), StreamFieldPanel('body'), InlinePanel('primary_topics', label="Primary Topics"), InlinePanel('secondary_topics', label="Secondary Topics"), ] BlogPage.promote_panels = Page.promote_panels + PromoteMixin.panels class BlogIndexPage(Page): """ This page automatically redirects to the latest :model:`blog.BlogPage`.
class BlogPage(TranslatablePage): """ A Blog Page We access the People object with an inline panel that references the ParentalKey's related_name in BlogPeopleRelationship. More docs: http://docs.wagtail.io/en/latest/topics/pages.html#inline-models """ 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) categories = ParentalManyToManyField(BlogCategory, blank=True) tags = ClusterTaggableManager(through=BlogPageTag, blank=True) date_published = models.DateField("Date article published", blank=True, null=True) updated = models.DateTimeField(auto_now=True, auto_now_add=False) created = models.DateTimeField(auto_now_add=True, help_text="(automatic) created date") content_panels = Page.content_panels + [ FieldPanel("categories", widget=forms.CheckboxSelectMultiple), FieldPanel("introduction", classname="full"), ImageChooserPanel("image"), StreamFieldPanel("body"), FieldPanel("date_published"), InlinePanel("blog_person_relationship", label="Author(s)", panels=None, min_num=1), FieldPanel("tags"), ] search_fields = Page.search_fields + [index.SearchField("body")] def authors(self): """ Returns the BlogPage's related People. Again note that we are using the ParentalKey's related_name from the BlogPeopleRelationship model to access these objects. This allows us to access the People objects with a loop on the template. If we tried to access the blog_person_ relationship directly we'd print `blog.BlogPeopleRelationship.None` """ authors = [n.people for n in self.blog_person_relationship.all()] return authors @property def get_tags(self): """ Similar to the authors function above we're returning all the tags that are related to the blog post into a list we can access on the template. We're additionally adding a URL to access BlogPage objects with that tag """ tags = self.tags.all() for tag in tags: tag.url = "/" + "/".join( s.strip("/") for s in [self.get_parent().url, "tags", tag.slug]) return tags @property def comments(self): """ Similar to the authors function above we're returning all the comments that are related to the blog post into a list we can access on the template. """ return Comment.objects.filter_by_instance( object_id=self.id, content_type=TranslatablePage) @property def get_image(self): """ Similar to the authors function above we're returning the image related to the blog post into a list we can access on the template. """ return self.image.file.url @property def comment_count(self): return self.comments.count() # Specifies parent to BlogPage as being BlogIndexPages parent_page_types = ["BlogIndexPage", "BlogPage"] # Specifies what content types can exist as children of BlogPage. # Empty list means that no child content types are allowed. subpage_types = ["BlogPage"] api_fields = [APIField("image", serializer=ImageSerializer())]
] class EventPage(Page): date_from = models.DateField("Start date", null=True) date_to = models.DateField( "End date", null=True, blank=True, help_text="Not required if event is on a single day") time_from = models.TimeField("Start time", null=True, blank=True) time_to = models.TimeField("End time", null=True, blank=True) location = models.CharField(max_length=255) body = RichTextField(blank=True) cost = models.CharField(max_length=255) signup_link = models.URLField(blank=True) EventPage.content_panels = [ FieldPanel('title', classname="full title"), FieldPanel('date_from'), FieldPanel('date_to'), FieldPanel('time_from'), FieldPanel('time_to'), FieldPanel('location'), FieldPanel('cost'), FieldPanel('signup_link'), FieldPanel('body', classname="full"), InlinePanel('related_media', label="Related media"), ]
class OutstationRoutePage(MetadataPageMixin, PageLDMixin, Page): template = "route/outstation_route_page.html" #banner_title=models.CharField(max_length=100, null=False) banner_title = RichTextField(features=['h1', 'h2', 'h3', 'h4', 'h5', 'h6']) banner_image = models.ForeignKey("wagtailimages.Image", null=True, blank=False, on_delete=models.SET_NULL, related_name="+") origin = models.ForeignKey("outstationcore.Place", null=True, on_delete=models.SET_NULL, related_name="route_origin") destination = models.ForeignKey("outstationcore.Place", null=True, on_delete=models.SET_NULL, related_name="route_destination") road_condition_rating = models.PositiveSmallIntegerField() #road_map = models.TextField(null=False, help_text="Add road map details") #best_time_to_visit = models.TextField(null=False, help_text="Add road map details") #alternate_routes = models.TextField(null=False, help_text="Add alternate route details") #road_condition = models.TextField(null=False, help_text="Add road condition details") highway = models.CharField(max_length=100, null=False) total_distance = models.PositiveSmallIntegerField( verbose_name=('Total distance (km)')) likes = models.ManyToManyField(User, related_name='likes', blank=True) canonical_url = models.URLField(null=True, blank=False, help_text="Canonical url for this page") robots_tag = models.CharField(max_length=100, null=True) api_fields = [ APIField("banner_title"), APIField("banner_image"), APIField("origin", serializer=PlaceSerializer()), APIField("destination", serializer=PlaceSerializer()), APIField("on_route_places", serializer=PlaceListSerializer()), APIField("destination_places", serializer=PlaceListSerializer()), APIField("road_condition_rating"), APIField("highway"), #APIField("road_map"), #APIField("best_time_to_visit"), #APIField("alternate_routes"), #APIField("road_condition"), APIField("total_distance"), ] content_panels = Page.content_panels + [ FieldPanel("banner_title"), ImageChooserPanel("banner_image"), SnippetChooserPanel('origin'), SnippetChooserPanel('destination'), FieldPanel("highway"), MultiFieldPanel([ InlinePanel("on_route_places"), ], heading="On Route Places"), MultiFieldPanel([ InlinePanel("destination_places"), ], heading="Destination Tourist Places"), MultiFieldPanel([ InlinePanel("route_information"), ], heading="Route Information"), FieldPanel("road_condition_rating"), #FieldPanel("road_map"), #FieldPanel("best_time_to_visit"), #FieldPanel("alternate_routes"), #FieldPanel("road_condition"), FieldPanel("total_distance"), ] promote_panels = MetadataPageMixin.promote_panels + [ FieldPanel("canonical_url"), FieldPanel("robots_tag") ] def get_context(self, request, *args, **kwargs): context = super().get_context(request, *args, **kwargs) location_tags = LocationTag.objects.all() context["location_tags"] = location_tags trip_types = TripType.objects.all() context["trip_types"] = trip_types fare_table = FareTable.objects.all() context["fare_table"] = fare_table context["total_likes"] = self.total_likes() context["total_reviews"] = self.total_reviews() context["reviews_list"] = self.reviews_list() popular_routes = PopularRoutes.objects.all() context["popular_routes"] = popular_routes context["amenities"] = enums.AmenitiesChoice context["vehicle_types"] = enums.VehicleTypeChoice return context def total_likes(self): #print(self.banner_image.file.name) return self.likes.count() def total_reviews(self): return self.page_review.count() def reviews_list(self): reviews = self.page_review.all().order_by('-publish_date') return reviews def aggregate_rating(self): avg_rating = self.page_review.aggregate(Avg('rating'))["rating__avg"] return avg_rating #def __unicode__(self): # return self.title def get_absolute_url(self): """ Returns absolute url for banner_image to generate image site map """ kwargs = {'slug': self.slug} return reverse('outstationroute.detail', kwargs=kwargs) def banner_image_url(self): """ Returns the banner_image url for XML images sitemap. """ url = settings.MEDIA_URL + self.banner_image.file.name return url if self.banner_image else '' def banner_image_title(self): """ Returns the banner_image title for XML images sitemap. """ return self.banner_image.title if self.banner_image else '' def ld_entity(self): """ Generates structured data to be displayed as rich snippets on google search """ return extend( super().ld_entity(), { "@context": "http://schema.org/", "@type": "TaxiService", "provider": { "@type": "Organization", "name": "Meru", "url": "https://www.meru.in/" }, "aggregateRating": { "ratingValue": self.aggregate_rating(), "reviewCount": self.total_reviews() } })
class HomePage(Page): """Home Page Model""" templates = "templates/home/home_page.html" # if not specified template loads from templates/app_name/page_class # Banner Content Fields banner_title = models.CharField(max_length=100, blank=False, null=True) banner_subtitle = RichTextField(features=["bold", "italic"]) banner_image = models.ForeignKey( "wagtailimages.Image", null=True, # might cause migration problem, if previously not specified blank=False, on_delete=models.SET_NULL, related_name="+" # use same as field name ) # Call to action, create another wagtail page banner_cta = models.ForeignKey( "wagtailcore.Page", null=True, blank=True, on_delete=models.SET_NULL, related_name="+" ) content = StreamField( [ ("title_and_text", blocks.TitleAndTextBlock()), ("full_rich_text", blocks.RichTextBlock()), ("simple_richtext", blocks.SimpleRichTextBlock()), ("cards", blocks.CardBlock()), ("call_to_action", blocks.CTABlock()), ], null=True, blank=True ) # Created fields must be registered here content_panels = Page.content_panels + [ # Banner MultiFieldPanel([ FieldPanel("banner_title"), FieldPanel("banner_subtitle"), ImageChooserPanel("banner_image"), PageChooserPanel("banner_cta"), ], heading="Banner Options"), # content StreamFieldPanel("content"), # Image Carousel MultiFieldPanel([ # Why not stream field? # Because stream fields can be unlimited, we want to limit the no of fields InlinePanel("carousel_images", max_num=5, min_num=1, label="Image") ], heading="Carousel Images") ] # only one home page per site max_count = 1 class Meta: verbose_name = "Home Page" verbose_name_plural = "Home Pages"
class HomePage(RoutablePageMixin, Page): """Home page model.""" template = "home/home_page.html" subpage_types = [ 'blog.BlogListingPage', 'contact.ContactPage', 'flex.FlexPage', ] parent_page_type = ['wagtailcore.Page'] banner_title = models.CharField(max_length=100, blank=False, null=True) banner_subtitle = RichTextField(features=["bold", "italic"]) banner_image = models.ForeignKey( "wagtailimages.Image", null=True, blank=False, on_delete=models.SET_NULL, related_name="+", ) banner_cta = models.ForeignKey( "wagtailcore.Page", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) content = StreamField([ ("cta", blocks.CTABlock()), ], null=True, blank=True) api_fields = [ APIField("banner_title"), APIField("banner_subtitle"), APIField("banner_image"), APIField("banner_cta"), APIField("carousel_images"), APIField("content"), ] 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 Settings"), ObjectList(Page.promote_panels, heading='Promotional Stuff'), ObjectList(Page.settings_panels, heading='Settings Stuff'), ]) 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 Extensions(index.Indexed, ClusterableModel): # Reference Template template = "extensions/extensions_page.html" # Create Fields ////////////////// event_collection = models.ForeignKey( "events.CategoryEventCollection", null=True, blank=True, on_delete=models.SET_NULL, related_name="+" ) assignment_command_types = models.ForeignKey( "categories.ExtensionCommandType", null=True, blank=True, on_delete=models.SET_NULL, related_name="+" ) action = RichTextField(features=['bold', 'italic'], blank=True, null=True) explanation = RichTextField(features=['bold', 'italic'], blank=True, null=True) video_link = models.CharField( max_length=500, blank=True, null=True, ) # Admin Display Panels panels = [ FieldPanel("event_collection", widget=EventChooser), MultiFieldPanel( [ FieldPanel("assignment_command_types", widget=ExtensionChooser, classname="col12 line"), FieldPanel("action", classname="col12 line"), FieldPanel("explanation", classname="col12") ], heading="Extension Info", ), MultiFieldPanel( [ InlinePanel('extension_lexis_link', label='Linked Lexis', classname="col12 line"), FieldPanel("video_link", classname="col12"), ], heading="Extension Links", ), ] # display name # return assignment_command and reference its command name property def __str__(self): return self.assignment_command_types.command_name or '' # return '' search_fields = [ index.SearchField('action', partial_match=True), ] class Meta: verbose_name = "Extension" verbose_name_plural = "Extensions"
class Person(BasePage): resource_type = "person" parent_page_types = ["People"] subpage_types = [] template = "person.html" # Content fields job_title = CharField(max_length=250) role = CharField(max_length=250, choices=ROLE_CHOICES, default="staff") description = RichTextField( "About", blank=True, default="", features=RICH_TEXT_FEATURES_SIMPLE, help_text="Optional ‘About me’ section content, supports rich text", ) image = ForeignKey( "mozimages.MozImage", null=True, blank=True, on_delete=SET_NULL, related_name="+", ) # Card fields card_title = CharField("Title", max_length=140, blank=True, default="") card_description = TextField("Description", max_length=400, blank=True, default="") card_image = ForeignKey( "mozimages.MozImage", null=True, blank=True, on_delete=SET_NULL, related_name="+", verbose_name="Image", ) # Meta city = CharField(max_length=250, blank=True, default="") country = CountryField() twitter = CharField(max_length=250, blank=True, default="") facebook = CharField(max_length=250, blank=True, default="") linkedin = CharField(max_length=250, blank=True, default="") github = CharField(max_length=250, blank=True, default="") email = CharField(max_length=250, blank=True, default="") websites = StreamField( StreamBlock([("website", PersonalWebsiteBlock())], max_num=3, required=False), null=True, blank=True, help_text="Optional links to any other personal websites", ) keywords = ClusterTaggableManager(through=PersonTag, blank=True) # Content panels content_panels = [ MultiFieldPanel( [ CustomLabelFieldPanel("title", label="Full name"), FieldPanel("job_title"), FieldPanel("role"), ], heading="Details", ), FieldPanel("description"), MultiFieldPanel( [ImageChooserPanel("image")], heading="Image", help_text= ("Optional header image. If not specified a fallback will be used. " "This image is also shown when sharing this page via social media" ), ), ] # Card panels card_panels = [ FieldPanel("card_title"), FieldPanel("card_description"), ImageChooserPanel("card_image"), ] # Meta panels meta_panels = [ MultiFieldPanel( [FieldPanel("city"), FieldPanel("country")], heading="Location", help_text=("Location fields. The country field is also filterable " "via the people directory page."), ), MultiFieldPanel([InlinePanel("topics")], heading="Topics this person specializes in"), MultiFieldPanel( [ FieldPanel("twitter"), FieldPanel("facebook"), FieldPanel("linkedin"), FieldPanel("github"), FieldPanel("email"), ], heading="Profiles", help_text="", ), StreamFieldPanel("websites"), MultiFieldPanel( [ FieldPanel("seo_title"), FieldPanel("search_description"), ImageChooserPanel("social_image"), FieldPanel("keywords"), ], heading="SEO", help_text=( "Optional fields to override the default title and description " "for SEO purposes"), ), ] # Settings panels settings_panels = [FieldPanel("slug")] # Tabs edit_handler = TabbedInterface([ ObjectList(content_panels, heading="Content"), ObjectList(card_panels, heading="Card"), ObjectList(meta_panels, heading="Meta"), ObjectList(settings_panels, heading="Settings", classname="settings"), ]) @property def events(self): """ Return upcoming events where this person is a speaker, ordered by start date """ from ..events.models import Event upcoming_events = Event.published_objects.filter( start_date__gte=datetime.datetime.now()) speaker_events = Event.published_objects.none() for event in upcoming_events.all(): # add the event to the list if the current person is a speaker if event.has_speaker(self): speaker_events = speaker_events | Event.published_objects.page( event) return speaker_events.order_by("start_date") @property def articles(self): """ Return articles and external articles where this person is (one of) the authors, ordered by article date, most recent first """ from ..articles.models import Article from ..externalcontent.models import ExternalArticle articles = Article.published_objects.none() external_articles = ExternalArticle.published_objects.none() all_articles = Article.published_objects.all() all_external_articles = ExternalArticle.published_objects.all() for article in all_articles: if article.has_author(self): articles = articles | Article.published_objects.page(article) for external_article in all_external_articles: if external_article.has_author(self): external_articles = external_articles | ( ExternalArticle.published_objects.page(external_article)) return sorted(chain(articles, external_articles), key=attrgetter("date"), reverse=True) @property def videos(self): """ Return the most recent videos and external videos where this person is (one of) the speakers. """ from ..videos.models import Video from ..externalcontent.models import ExternalVideo videos = Video.published_objects.none() external_videos = ExternalVideo.published_objects.none() all_videos = Video.published_objects.all() all_external_videos = ExternalVideo.published_objects.all() for video in all_videos: if video.has_speaker(self): videos = videos | Video.published_objects.page(video) for external_video in all_external_videos: if external_video.has_speaker(self): external_videos = external_videos | ( ExternalVideo.published_objects.page(external_video)) return sorted(chain(videos, external_videos), key=attrgetter("date"), reverse=True) @property def role_group(self): return { "slug": self.role, "title": dict(ROLE_CHOICES).get(self.role, "") } @property def country_group(self): return ({ "slug": self.country.code.lower(), "title": self.country.name } if self.country else { "slug": "" })
class HomePageCarouselItem(Orderable, AbstractCarouselItem): page = ParentalKey('HomePage', related_name='carousel_items', on_delete=models.CASCADE) class HomePageRelatedLink(Orderable, AbstractRelatedLink): page = ParentalKey('HomePage', related_name='related_links', on_delete=models.CASCADE) HomePage.content_panels = Page.content_panels + [ FieldPanel('body', classname="full"), InlinePanel('carousel_items', label="Carousel items"), InlinePanel('related_links', label="Related links"), ] # Standard pages class StandardPage(Page): page_ptr = models.OneToOneField(Page, parent_link=True, related_name='+', on_delete=models.CASCADE) intro = RichTextField(blank=True) body = RichTextField(blank=True) feed_image = models.ForeignKey('wagtailimages.Image', null=True,
[ 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( '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'),
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 Application(ClusterableModel): """An application is made to strive to acquire an position""" position = models.ForeignKey( 'Position', related_name='applications', on_delete=models.PROTECT, blank=False, ) applicant = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=False, ) class Meta: verbose_name = _('Application') verbose_name_plural = _('Applications') unique_together = ('position', 'applicant') default_permissions = () STATUS_CHOICES = ( ('draft', _('Draft')), ('submitted', _('Submitted')), ('approved', _('Approved')), ('disapproved', _('Disapproved')), ('appointed', _('Appointed')), ('turned_down', _('Turned down')), ) status = models.CharField( max_length=20, choices=STATUS_CHOICES, verbose_name=_('Status'), blank=False, null=False, ) # ---- Application Information ------ cover_letter = models.TextField( verbose_name=_('Cover Letter'), help_text=_('Present yourself and state why you are who we are ' 'looking for'), blank=True, ) qualifications = models.TextField( verbose_name=_('Qualifications'), help_text=_('Give a summary of relevant qualifications'), blank=True, ) # Access overhead removed = models.BooleanField(default=False, ) def __str__(self) -> str: return '%(applicant)s - %(position)s' % { 'position': self.position, 'applicant': self.applicant } rejection_date = models.DateField(verbose_name=_('Rejection date'), null=True, blank=True) # ------ Administrator settings ------ panels = [ MultiFieldPanel([ FieldRowPanel([ FieldPanel('applicant'), FieldPanel('position'), ]), FieldPanel('cover_letter'), FieldPanel('qualifications'), InlinePanel('references'), FieldPanel('status'), ]) ]
class HomePage(ContentPage): # Only available at root level parent_page_types = ['wagtailcore.Page'] content_panels = [ InlinePanel('hero_image_layers', label="Hero Image Layers", help_text="Bottom-most layer goes first.") ] + ContentPage.content_panels # There can only be one HomePage @classmethod def can_create_at(cls, parent): return super(HomePage, cls).can_create_at(parent) and not cls.objects.exists() def get_context(self, request): context = super(HomePage, self).get_context(request) context['links'] = [{ "name": "Patreon", "icon": "fab fa-patreon", "link": "https://www.patreon.com/GemExchange" }, { "name": "Toyhouse", "icon": "fa fa-home", "link": "http://toyhou.se/~world/1420.gemexchange-jo-arca" }, { "name": "FurAffinity", "icon": "fa fa-paw", "link": "https://www.furaffinity.net/user/gemexchange" }, { "name": "Twitter", "icon": "fab fa-twitter", "link": "https://twitter.com/Gem_Exchange" }, { "name": "Github", "icon": "fab fa-github", "link": "https://github.com/juan0tron/the-gem-exchange" }, { "name": "DeviantArt", "icon": "fab fa-deviantart", "link": "https://gemexchange.deviantart.com/" }, { "name": "Discord", "icon": "fab fa-discord", "link": "https://discord.gg/T9Xrjs5" }] context['compendium_backgrounds'] = SubSpecies.objects.filter( name="Standard").exclude(background__isnull=True)[:3] return context
class Experiment(ClusterableModel): STATUS_CHOICES = [ ('draft', "Draft"), ('live', "Live"), ('completed', "Completed"), ] name = models.CharField(max_length=255) slug = models.SlugField(max_length=255) control_page = models.ForeignKey('wagtailcore.Page', related_name='+', on_delete=models.CASCADE) goal = models.ForeignKey('wagtailcore.Page', related_name='+', on_delete=models.SET_NULL, null=True, blank=True) status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft') winning_variation = models.ForeignKey('wagtailcore.Page', related_name='+', on_delete=models.SET_NULL, null=True) panels = [ FieldPanel('name'), FieldPanel('slug'), PageChooserPanel('control_page'), InlinePanel('alternatives', label="Alternatives"), PageChooserPanel('goal'), FieldPanel('status'), ] def __init__(self, *args, **kwargs): super(Experiment, self).__init__(*args, **kwargs) self._initial_status = self.status def activate_alternative_draft_content(self): # For any alternative pages that are unpublished, copy the latest draft revision # to the main table (with is_live=False) so that the revision shown as an alternative # is not an out-of-date one for alternative in self.alternatives.select_related('page'): if not alternative.page.live: revision = alternative.page.get_latest_revision_as_page() revision.live = False revision.has_unpublished_changes = True revision.save() def get_variations(self): return [self.control_page] + [ alt.page for alt in self.alternatives.select_related('page') ] def get_variation_for_user(self, user_id): variations = self.get_variations() # choose uniformly from variations, based on a hash of user_id and experiment.slug hash_input = "{0}.{1}".format(self.slug, user_id) hash_str = sha1(hash_input.encode('utf-8')).hexdigest()[:7] variation_index = int(hash_str, 16) % len(variations) return variations[variation_index] def start_experiment_for_user(self, user_id, request): """ Record a new participant and return the variation for them to use """ variation = self.get_variation_for_user(user_id) get_backend().record_participant(self, user_id, variation, request) return variation def record_completion_for_user(self, user_id, request): backend = get_backend() variation = self.get_variation_for_user(user_id) backend.record_completion(self, user_id, variation, request) def select_winner(self, variation): self.winning_variation = variation self.status = 'completed' self.save() def __str__(self): return self.name
class LandingPage(Page): template = 'home/index.jinja' FEATURED_CONTENT_COUNT = 6 MAX_CALLOUT_ENTRIES = 3 RECENT_FORUM_ACTIVITY_COUNT = 5 mission_statement = models.CharField(max_length=512) community_statement = models.TextField() def get_featured_content(self): return self.featured_content_queue.select_related( 'image', 'codebase_image', 'link_codebase', 'link_page').all()[:self.FEATURED_CONTENT_COUNT] def get_canned_forum_activity(self): random_submitters = User.objects.select_related( 'member_profile').filter(pk__in=(3, 5, 7, 11, 13, 17)) return [{ 'title': "Generated Forum Topic {}".format(i), 'submitter_name': random_submitters[i].member_profile.name, 'submitter_url': random_submitters[i].member_profile.get_absolute_url(), 'date_created': datetime.now(), 'url': "https://forum.example.com/topic/{}".format(i), } for i in range(self.RECENT_FORUM_ACTIVITY_COUNT)] def _discourse_username_to_submitter(self, username, topic, topic_title): submitter = None submitter_url = None if username != 'comses': try: submitter = User.objects.get(username=username) except User.DoesNotExist: pass if submitter is None: category_id = topic['category_id'] logger.debug("category id: %s, topic title: %s, topic: %s", category_id, topic_title, topic) # special case lookup for real submitter # FIXME: get rid of magic constants target_object = None if category_id == 6: # jobs and appointments target_object = Job.objects.filter( title=topic_title).order_by('-date_created').first() elif category_id == 7: # events target_object = Event.objects.filter( title=topic_title).order_by('-date_created').first() elif category_id == 8: target_object = Codebase.objects.filter( title=topic_title).order_by('-date_created').first() if target_object: submitter = target_object.submitter submitter_url = submitter.member_profile.get_absolute_url() else: submitter = User.get_anonymous() return submitter, submitter_url def get_recent_forum_activity(self): # FIXME: refactor and clean up logic to form a more sensible discourse api # Discourse API endpoint documented at http://docs.discourse.org/#tag/Topics%2Fpaths%2F~1latest.json%2Fget if settings.DEPLOY_ENVIRONMENT.is_development(): return self.get_canned_forum_activity() recent_forum_activity = cache.get('recent_forum_activity') if recent_forum_activity: return recent_forum_activity # transform topics list of dictionaries into web template format with title, submitter, date_created, and url. try: r = requests.get(build_discourse_url('latest.json'), params={ 'order': 'created', 'sort': 'asc' }, timeout=3.0) posts_dict = r.json() topics = posts_dict['topic_list']['topics'] recent_forum_activity = [] for topic in topics[:self.RECENT_FORUM_ACTIVITY_COUNT]: topic_title = topic['title'] topic_url = build_discourse_url('t/{0}/{1}'.format( topic['slug'], topic['id'])) # getting back to the original submitter involves some trickery. # The Discourse embed Javascript queues up a crawler to hit the given page and parses it for content to use # as the initial topic text. However, this topic gets added as a specific Discourse User (`comses`, # see https://meta.discourse.org/t/embedding-discourse-comments-via-javascript/31963/150 for more details) # and so we won't always have the direct username of the submitter without looking it up by # 1. Discourse category_id (6 = jobs & appointments, 7 = events, 8 = codebase) # 2. Title (not guaranteed to be unique) last_poster_username = topic['last_poster_username'] submitter, submitter_url = self._discourse_username_to_submitter( last_poster_username, topic, topic_title) recent_forum_activity.append({ 'title': topic_title, 'submitter_name': submitter.username, 'submitter_url': submitter_url, # FIXME: handle created_at=None gracefully, via default date? 'date_created': datetime.strptime(topic.get('created_at'), "%Y-%m-%dT%H:%M:%S.%fZ"), 'url': topic_url, }) cache.set('recent_forum_activity', recent_forum_activity, 3600) return recent_forum_activity except Exception as e: logger.exception(e) return [] def get_latest_jobs(self): return Job.objects.order_by('-date_created')[:self.MAX_CALLOUT_ENTRIES] def get_upcoming_events(self): return Event.objects.upcoming().order_by( 'start_date')[:self.MAX_CALLOUT_ENTRIES] def get_context(self, request, *args, **kwargs): context = super(LandingPage, self).get_context(request, *args, **kwargs) context['featured_content'] = self.get_featured_content() context['recent_forum_activity'] = self.get_recent_forum_activity() context['latest_jobs'] = self.get_latest_jobs() context['upcoming_events'] = self.get_upcoming_events() return context content_panels = Page.content_panels + [ FieldPanel('mission_statement', widget=forms.Textarea), FieldPanel('community_statement'), InlinePanel('featured_content_queue', label=_('Featured Content')), ]
class ServicePage(JanisBasePage): janis_url_page_type = "services" steps = StreamField( [ ('basic_step', RichTextBlock(features=WYSIWYG_SERVICE_STEP, label='Basic Step')), ( 'step_with_options_accordian', StructBlock( [ ( 'options_description', RichTextBlock( features=WYSIWYG_SERVICE_STEP, # richTextPlaceholder.js searches for the class 'coa-option-description' and replaces placeholder text # The placeholder text is not part of the richtext input, but rather a div mask. classname='coa-option-description', )), ('options', ListBlock( StructBlock([ ('option_name', TextBlock( label= 'Option name. (When clicked, this name will expand the content for this option' )), ('option_description', RichTextBlock( features=WYSIWYG_SERVICE_STEP, label='Option Content', )), ]), )), ], label="Step With Options")), ( 'step_with_locations', StructBlock( [ ( 'locations_description', RichTextBlock( features=WYSIWYG_SERVICE_STEP, # richTextPlaceholder.js searches for the class 'coa-option-description' and replaces placeholder text # The placeholder text is not part of the richtext input, but rather a div mask. classname='coa-locations-description', )), ('locations', ListBlock( PageChooserBlock(label="Location", page_type=[LocationPage]))), ], label="Step with locations")), ], verbose_name= 'Write out the steps a resident needs to take to use the service', # this gets called in the help panel help_text= 'A step may have a basic text step or an options accordion which reveals two or more options', blank=True) dynamic_content = StreamField( [ ('map_block', SnippetChooserBlockWithAPIGoodness('base.Map', icon='site')), ('what_do_i_do_with_block', WhatDoIDoWithBlock()), ('collection_schedule_block', CollectionScheduleBlock()), ('recollect_block', RecollectBlock()), ], verbose_name= 'Add any maps or apps that will help the resident use the service', blank=True) additional_content = RichTextField( features=WYSIWYG_GENERAL, verbose_name='Write any additional content describing the service', help_text='Section header: What else do I need to know?', blank=True) base_form_class = ServicePageForm short_description = models.TextField( max_length=SHORT_DESCRIPTION_LENGTH, blank=True, verbose_name='Write a description of this service') content_panels = [ FieldPanel('title_en', widget=countMe), FieldPanel('title_es', widget=countMe), FieldPanel('title_ar'), FieldPanel('title_vi'), FieldPanel('short_description', widget=countMeTextArea), InlinePanel('topics', label='Topics'), InlinePanel('related_departments', label='Related Departments'), MultiFieldPanel([ HelpPanel(steps.help_text, classname="coa-helpPanel"), StreamFieldPanel('steps') ], heading=steps.verbose_name, classname='coa-multiField-nopadding'), StreamFieldPanel('dynamic_content'), MultiFieldPanel([ HelpPanel(additional_content.help_text, classname="coa-helpPanel"), FieldPanel('additional_content') ], heading=additional_content.verbose_name, classname='coa-multiField-nopadding'), InlinePanel('contacts', label='Contacts'), ]
class StoryPage(Page): MAX_RELATED_STORIES = 10 # This is a leaf page subpage_types = [] parent_page_types = ['news.StoryIndexPage'] author = models.ForeignKey('common.User', null=True, blank=True, on_delete=models.SET_NULL, related_name="+") date = models.DateField("post date") lede = models.CharField( max_length=1024, help_text="A short intro that appears in the story index page") tags = ClusterTaggableManager(through=StoryTag, blank=True) categories = ParentalManyToManyField( 'news.StoryCategory', blank=True, help_text="The set of categories this page will be served") # Add allowed block types to StreamPanel content = StreamField([ ('paragraph', RichTextBlock( features=['h2', 'h3', 'bold', 'italic', 'link', 'ol', 'ul'])), ('markdown', ZOrderMarkdownBlock(icon='code')), ('image', StructBlock([('image', ImageChooserBlock()), ('caption', CharBlock(required=False))])), ('carousel', ImageCarouselBlock()), ('quote', BlockQuoteBlock()), ('embedded_content', EmbedContentBlock()), ]) def get_context(self, request): context = super().get_context(request) tags_list = list(self.tags.all()) # Collects set of stories that has the same tags as this story. related = StoryPage.objects.live().order_by('-first_published_at') related = related.filter(tags__in=tags_list) related = related.exclude(id=self.id) if related.count() > 0: related = related.distinct()[0:self.MAX_RELATED_STORIES] context["related"] = related try: context['prevstory'] = self.get_previous_by_date( tags__in=tags_list) except (StoryPage.DoesNotExist, ValueError): pass try: context['nextstory'] = self.get_next_by_date(tags__in=tags_list) except (StoryPage.DoesNotExist, ValueError): pass return context def hero_image(self): gallery_item = self.gallery_images.first() if gallery_item: return gallery_item.image else: return None def media_thumbnail_url(self): for child in self.content: if child.block.name == 'embedded_content': return child.value.thumbnail_url return None def hero_image_url(self): hero_image = self.hero_image() if hero_image: return hero_image.url return self.media_thumbnail_url() def author_name(self): if not self.author: return ANONYMOUS_AUTHOR_NAME else: return self.owner.username search_fields = Page.search_fields + [ index.SearchField('lede'), index.SearchField('content'), ] content_panels = Page.content_panels + [ MultiFieldPanel([ StoryAuthorFieldPanel('author', widget=forms.Select), FieldPanel('date'), FieldPanel('tags'), FieldPanel('categories', widget=forms.CheckboxSelectMultiple), ]), FieldPanel('lede'), InlinePanel('gallery_images', label="Gallery Images"), StreamFieldPanel('content') ]
class Article(BasePage): # IMPORTANT: EACH ARTICLE is NOW LABELLED "POST" IN THE FRONT END resource_type = "article" # If you change this, CSS will need updating, too parent_page_types = ["Articles"] subpage_types = [] template = "article.html" class Meta: verbose_name = "post" # NB verbose_name_plural = "posts" # NB # Content fields description = RichTextField( blank=True, default="", features=RICH_TEXT_FEATURES_SIMPLE, help_text="Optional short text description, max. 400 characters", max_length=400, ) image = ForeignKey( "mozimages.MozImage", null=True, blank=True, on_delete=SET_NULL, related_name="+", ) body = CustomStreamField(help_text=( "The main post content. Supports rich text, images, embed via URL, " "embed via HTML, and inline code snippets")) related_links_mdn = StreamField( StreamBlock([("link", ExternalLinkBlock())], required=False), blank=True, null=True, help_text="Optional links to MDN Web Docs for further reading", verbose_name="Related MDN links", ) # Card fields card_title = CharField("Title", max_length=140, blank=True, default="") card_description = TextField("Description", max_length=400, blank=True, default="") card_image = ForeignKey( "mozimages.MozImage", null=True, blank=True, on_delete=SET_NULL, related_name="+", verbose_name="Image", ) # Meta fields date = DateField( "Post date", default=datetime.date.today, help_text="The date the post was published", ) authors = StreamField( StreamBlock( [ ("author", PageChooserBlock(target_model="people.Person")), ("external_author", ExternalAuthorBlock()), ], required=False, ), blank=True, null=True, help_text=( "Optional list of the post's authors. Use ‘External author’ to add " "guest authors without creating a profile on the system"), ) keywords = ClusterTaggableManager(through=ArticleTag, blank=True) # Content panels content_panels = BasePage.content_panels + [ FieldPanel("description"), MultiFieldPanel( [ImageChooserPanel("image")], heading="Image", help_text= ("Optional header image. If not specified a fallback will be used. " "This image is also shown when sharing this page via social media" ), ), StreamFieldPanel("body"), StreamFieldPanel("related_links_mdn"), ] # Card panels card_panels = [ FieldPanel("card_title"), FieldPanel("card_description"), ImageChooserPanel("card_image"), ] # Meta panels meta_panels = [ FieldPanel("date"), StreamFieldPanel("authors"), MultiFieldPanel( [InlinePanel("topics")], heading="Topics", help_text= ("The topic pages this post will appear on. The first topic in the " "list will be treated as the primary topic and will be shown in the " "page’s related content."), ), MultiFieldPanel( [ FieldPanel("seo_title"), FieldPanel("search_description"), ImageChooserPanel("social_image"), FieldPanel("keywords"), ], heading="SEO", help_text=( "Optional fields to override the default title and description " "for SEO purposes"), ), ] # Settings panels settings_panels = [FieldPanel("slug")] # Tabs edit_handler = TabbedInterface([ ObjectList(content_panels, heading="Content"), ObjectList(card_panels, heading="Card"), ObjectList(meta_panels, heading="Meta"), ObjectList(settings_panels, heading="Settings", classname="settings"), ]) def get_absolute_url(self): # For the RSS feed return self.full_url @property def primary_topic(self): """Return the first (primary) topic specified for the Article.""" article_topic = self.topics.first() return article_topic.topic if article_topic else None @property def read_time(self): return str(readtime.of_html(str(self.body))) @property def related_resources(self): """Returns resources that are related to the current resource, i.e. live, public Articles and Videos which have the same Topics.""" topic_pks = [topic.topic.pk for topic in self.topics.all()] return get_combined_articles_and_videos( self, topics__topic__pk__in=topic_pks) @property def month_group(self): return self.date.replace(day=1) def has_author(self, person): for author in self.authors: # pylint: disable=not-an-iterable if author.block_type == "author" and str(author.value) == str( person.title): return True return False
class Movie(Page): """Фильм""" tagline = models.CharField("Слоган", max_length=100, default='') description = RichTextField("Описание", blank=True) image = models.ForeignKey('wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+') year = models.PositiveSmallIntegerField("Дата выхода", default=2019) country = models.CharField("Страна", max_length=30) directors = ParentalManyToManyField(Actor, verbose_name="режиссер", related_name="film_director") actors = ParentalManyToManyField(Actor, verbose_name="актеры", related_name="film_actor") genres = ParentalManyToManyField(Genre, verbose_name="жанры") world_premiere = models.DateField("Примьера в мире", default=date.today) budget = models.PositiveIntegerField( "Бюджет", default=0, help_text="указывать сумму в долларах") fees_in_usa = models.PositiveIntegerField( "Сборы в США", default=0, help_text="указывать сумму в долларах") fees_in_world = models.PositiveIntegerField( "Сборы в мире", default=0, help_text="указывать сумму в долларах") movie_category = ParentalManyToManyField(Category, verbose_name="Категория") search_fields = Page.search_fields + [ index.SearchField('description'), index.SearchField('year'), ] content_panels = Page.content_panels + [ FieldPanel('tagline'), ImageChooserPanel('image'), FieldPanel('year'), FieldPanel('country'), FieldPanel('directors'), FieldPanel('actors'), FieldPanel('world_premiere'), FieldPanel('budget'), FieldPanel('fees_in_usa'), FieldPanel('fees_in_world'), InlinePanel('gallery_images', label='Кадры из фильма'), InlinePanel('ratings', label='Рейтинг'), InlinePanel('reviews', label='Отзывы'), FieldPanel('movie_category', widget=forms.CheckboxSelectMultiple), FieldPanel('genres', widget=forms.CheckboxSelectMultiple), ] api_fields = [ APIField('title'), APIField('description'), APIField('actors'), APIField('genres'), APIField('movie_category'), ] def __str__(self): return self.title def get_review(self): return self.reviews_set.filter(parent__isnull=True) class Meta: verbose_name = "Фильм" verbose_name_plural = "Фильмы" db_table = "'catalog'.'movie'"
class Video(BasePage): resource_type = "video" parent_page_types = ["Videos"] subpage_types = [] template = "video.html" # Content fields description = RichTextField( blank=True, default="", features=RICH_TEXT_FEATURES_SIMPLE, help_text="Optional short text description, max. 400 characters", max_length=400, ) body = RichTextField( blank=True, default="", features=RICH_TEXT_FEATURES, help_text=( "Optional body content. Supports rich text, images, embed via URL, " "embed via HTML, and inline code snippets"), ) related_links = StreamField( StreamBlock([("link", ExternalLinkBlock())], required=False), null=True, blank=True, help_text="Optional links further reading", verbose_name="Related links", ) image = ForeignKey( "mozimages.MozImage", null=True, blank=True, on_delete=SET_NULL, related_name="+", ) types = CharField(max_length=14, choices=VIDEO_TYPE, default="conference") duration = CharField( max_length=30, blank=True, null=True, help_text= ("Optional video duration in MM:SS format e.g. “12:34”. Shown when the " "video is displayed as a card"), ) transcript = RichTextField( blank=True, default="", features=RICH_TEXT_FEATURES, help_text="Optional text transcript of the video, supports rich text", ) video_url = StreamField( StreamBlock([("embed", EmbedBlock())], min_num=1, max_num=1, required=True), help_text= ("Embed URL for the video e.g. https://www.youtube.com/watch?v=kmk43_2dtn0" ), ) speakers = StreamField( StreamBlock( [("speaker", PageChooserBlock(target_model="people.Person"))], required=False, ), blank=True, null=True, help_text= "Optional list of people associated with or starring in the video", ) # Card fields card_title = CharField("Title", max_length=140, blank=True, default="") card_description = TextField("Description", max_length=400, blank=True, default="") card_image = ForeignKey( "mozimages.MozImage", null=True, blank=True, on_delete=SET_NULL, related_name="+", verbose_name="Image", ) # Meta fields date = DateField("Upload date", default=datetime.date.today) keywords = ClusterTaggableManager(through=VideoTag, blank=True) # Content panels content_panels = BasePage.content_panels + [ FieldPanel("description"), ImageChooserPanel("image"), StreamFieldPanel("video_url"), FieldPanel("body"), StreamFieldPanel("related_links"), FieldPanel("transcript"), ] # Card panels card_panels = [ FieldPanel("card_title"), FieldPanel("card_description"), ImageChooserPanel("card_image"), ] # Meta panels meta_panels = [ FieldPanel("date"), StreamFieldPanel("speakers"), MultiFieldPanel( [InlinePanel("topics")], heading="Topics", help_text= ("These are the topic pages the video will appear on. The first topic " "in the list will be treated as the primary topic and will be shown " "in the page’s related content."), ), FieldPanel("duration"), MultiFieldPanel( [FieldPanel("types")], heading="Type", help_text="Choose a video type to help people search for the video", ), MultiFieldPanel( [ FieldPanel("seo_title"), FieldPanel("search_description"), ImageChooserPanel("social_image"), FieldPanel("keywords"), ], heading="SEO", help_text=( "Optional fields to override the default title and description " "for SEO purposes"), ), ] settings_panels = BasePage.settings_panels + [FieldPanel("slug")] # Tabs edit_handler = TabbedInterface([ ObjectList(content_panels, heading="Content"), ObjectList(card_panels, heading="Card"), ObjectList(meta_panels, heading="Meta"), ObjectList(settings_panels, heading="Settings", classname="settings"), ]) def get_absolute_url(self): # For the RSS feed return self.full_url @property def primary_topic(self): """Return the first (primary) topic specified for the video.""" video_topic = self.topics.first() return video_topic.topic if video_topic else None @property def read_time(self): return str(readtime.of_html(str(self.body))) @property def related_resources(self): """Returns resources that are related to the current resource, i.e. live, public articles and videos which have the same topics.""" topic_pks = [topic.topic.pk for topic in self.topics.all()] return get_combined_articles_and_videos( self, topics__topic__pk__in=topic_pks) def has_speaker(self, person): for speaker in self.speakers: # pylint: disable=not-an-iterable if str(speaker.value) == str(person.title): return True return False
class MoviePage(Page): moviePoster = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) movieBackDrop = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) director = models.CharField(max_length=255, null=True, blank=True) country = models.CharField(max_length=255, null=True,blank=True) releaseDate = models.DateField(null=True,blank=True) # releaseYear enough?? movieType = models.CharField(max_length=255, null=True, blank=True) spokenLanguage = models.CharField(max_length=255, null=True, blank=True) subtitleLanguage = models.CharField(max_length=255, null=True, blank=True) lengthInMinutes = models.IntegerField(null=True, blank=True) minimumAge = models.IntegerField(null=True, blank=True) premiere = models.BooleanField(default=False) showStartDate = models.DateField(null=True, blank=True) showEndDate = models.DateField(null=True, blank=True) classifications = ParentalManyToManyField(KijkWijzerClassification, blank=True) trailer = models.URLField("Trailer", blank=True, null=True) doubleBillMovie = models.ForeignKey( 'MoviePage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', ) body = StreamField([ ('heading', blocks.CharBlock(classname="full title")), ('paragraph', blocks.TextBlock()), ('image', ImageChooserBlock()), ('quotation', blocks.StructBlock([ ('text', blocks.TextBlock()), ('author', blocks.CharBlock()), ])), ], blank=True, null=True) content_panels = Page.content_panels + [ FieldPanel('director'), FieldPanel('country'), FieldPanel('releaseDate'), FieldPanel('movieType'), FieldPanel('spokenLanguage'), FieldPanel('subtitleLanguage'), FieldPanel('lengthInMinutes'), FieldPanel('minimumAge'), FieldPanel('premiere'), PageChooserPanel('doubleBillMovie', 'home.MoviePage'), ImageChooserPanel('moviePoster'), ImageChooserPanel('movieBackDrop'), FieldPanel('trailer'), InlinePanel('movieDates', label="Movie dates"), InlinePanel('externalLinks', label="External links"), FieldPanel('classifications',widget=forms.CheckboxSelectMultiple), # InlinePanel( 'classifications', label="Categories"), StreamFieldPanel('body'), ] api_fields = [ APIField('director'), APIField('country'), APIField('body'), APIField('releaseDate'), APIField('movieType'), APIField('spokenLanguage'), APIField('subtitleLanguage'), APIField('lengthInMinutes'), APIField('premiere'), APIField('minimumAge'), APIField('doubleBillMovie'), APIField('movieDates', serializer=MovieDateSerializer(many=True, read_only=True)), APIField('externalLinks', serializer=ExternalLinkSerializer(many=True, read_only=True)), APIField('moviePoster'), APIField('moviePosterThumb', serializer=ImageRenditionField('fill-100x100', source='moviePoster')), APIField('movieBackDrop'), APIField('trailer'), APIField('classifications', serializer=KijkWijzerClassificationSerializer(many=True, read_only=True)), ] def get_start_date(self, current_date, date_to_compare): return date_to_compare if (current_date is None) or (date_to_compare < current_date) else current_date def get_end_date(self, current_date, date_to_compare): return date_to_compare if (current_date is None) or (date_to_compare > current_date) else current_date def save(self, *args, **kwargs): start_date_time = None end_date_time = None for movie_date in self.movieDates.all(): start_date_time = self.get_start_date( start_date_time, movie_date.date) end_date_time = self.get_end_date( end_date_time, movie_date.date) if start_date_time and end_date_time: self.showStartDate = start_date_time.date() self.showEndDate = end_date_time.date() super().save(*args, **kwargs) # Call the "real" save() method.
class SeriesPage(Page): """Series page""" template = "series/series_page.html" sub_title = models.CharField(blank=False, null=False, max_length=255) produced_from = models.PositiveIntegerField(null=False, blank=False) produced_to = models.PositiveIntegerField(null=True, blank=True) description = RichTextField( features=['h3', 'h4', 'ol', 'ul', 'bold', 'italic', 'link'], null=True, blank=True) hero = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name="+", help_text= "The hero image is used in the page header as well as in the information description of the series." ) poster = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name="+", help_text= "The poster image is used in some promotional listings of the series.") multi_season_series = models.BooleanField( blank=True, null=False, default=False, verbose_name="Multi season series", help_text="Is this a part of a multi season series?") content = StreamField([ ('related_series', PromotedSeriesBlock()), ], null=True, blank=True) api_fields = [ APIField('sub_title'), APIField('produced_from'), APIField('produced_to'), APIField('description'), APIField('hero', serializer=ImageRenditionField('fill-1920x780')), APIField('poster', serializer=ImageRenditionField('width-218')), APIField('episodes'), APIField('content'), ] content_panels = Page.content_panels + [ MultiFieldPanel([ FieldPanel('sub_title'), ImageChooserPanel('hero'), ], heading="Header"), MultiFieldPanel([ FieldPanel('description'), FieldPanel('produced_from'), FieldPanel('produced_to'), ImageChooserPanel('poster'), FieldPanel('multi_season_series'), ], heading="Information"), InlinePanel('episodes', label="Episodes"), StreamFieldPanel('content'), ] @property def has_new_episodes(self): new_episodes = self.episodes.filter(is_new=True) return len(new_episodes) > 0 parent_page_types = ['series.SeriesIndexPage'] subpage_types = [] def get_admin_display_title(self): return '{} – {}'.format(super().get_admin_display_title(), self.sub_title) def __str__(self): return self.title class Meta: # noqa verbose_name_plural = "Series"
class SectionedRichTextPage(Page): content_panels = [ FieldPanel('title', classname="full title"), InlinePanel('sections') ]
class FixtureDetailPage(Page): """Fixture detail page.""" subpage_types = [] competition = models.CharField( max_length=100, blank=False, null=True, help_text='Name of Competition e.g. Div 2, Cup QF etc.', ) hometeam = models.CharField( max_length=200, blank=False, null=False, help_text='Home team name', ) hometeam_image = models.ForeignKey( "wagtailimages.Image", blank=False, null=True, related_name="+", on_delete=models.SET_NULL, help_text='Home team Logo', ) awayteam = models.CharField( max_length=200, blank=False, null=False, help_text='AWay Team Name', ) awayteam_image = models.ForeignKey( "wagtailimages.Image", blank=False, null=True, related_name="+", on_delete=models.SET_NULL, help_text='Away Team Logo', ) fixture_date_time = models.CharField( max_length=40, blank=False, null=False, help_text='Date and Time of Fixture', ) fixture_location = models.CharField( max_length=100, blank=False, null=False, help_text='Fixture location', ) fixture_video = models.URLField( blank=True, null=True, help_text='Enter the url link to the match video. (optional)', ) HT_FT_PTS = models.IntegerField(null=True, blank=True) AT_FT_PTS = models.IntegerField(null=True, blank=True) HT_HT_PTS = models.IntegerField(null=True, blank=True) AT_HT_PTS = models.IntegerField(null=True, blank=True) Home_Team_Scorers = RichTextField(features=["bold", "italic"], null=True, blank=True) Away_Team_Scorers = RichTextField(features=["bold", "italic"], null=True, blank=True) Match_Officials = RichTextField(features=["bold", "italic"], null=True, blank=True) Match_Report = RichTextField(features=["bold", "italic", "link"], null=True, blank=True) content_panels = Page.content_panels + [ FieldPanel("competition"), FieldPanel("hometeam"), ImageChooserPanel("hometeam_image"), FieldPanel("awayteam"), ImageChooserPanel("awayteam_image"), FieldPanel("fixture_date_time"), FieldPanel("fixture_location"), MultiFieldPanel([ InlinePanel("fixture_status_selector", label="Fixture Status", min_num=1, max_num=1) ], heading="Fixture Status"), FieldPanel("fixture_video"), FieldRowPanel([ FieldPanel("HT_FT_PTS"), FieldPanel("HT_HT_PTS"), FieldPanel("AT_FT_PTS"), FieldPanel("AT_HT_PTS"), ]), FieldPanel("Home_Team_Scorers"), FieldPanel("Away_Team_Scorers"), FieldPanel("Match_Officials"), FieldPanel("Match_Report") ]