1
+ from datetime import date
2
+
1
3
from django .db import models
2
4
from django .db .models .fields .related import RelatedField
3
5
from django .urls import reverse
4
6
from django .utils .dateformat import format
7
+ from django .utils .functional import cached_property
5
8
from django .utils .text import Truncator
6
9
from django .utils .translation import gettext_lazy as _
7
10
from modelcluster .contrib .taggit import ClusterTaggableManager
8
11
from modelcluster .fields import ParentalKey
9
12
from modelcluster .models import ClusterableModel
10
13
from taggit .models import TaggedItemBase
11
14
from wagtail .admin .panels import FieldPanel , FieldRowPanel , InlinePanel , MultiFieldPanel
15
+ from wagtail .fields import RichTextField
12
16
from wagtail .models import Orderable , Page , PageManager , PageQuerySet
13
17
from wagtail .search import index
14
18
from wagtailautocomplete .edit_handlers import AutocompletePanel
15
19
20
+ from cdhweb .pages .mixin import StandardHeroMixinNoImage
16
21
from cdhweb .pages .models import BasePage , LinkPage , PagePreviewDescriptionMixin
17
22
from cdhweb .people .models import Person
18
23
@@ -54,40 +59,107 @@ class BlogPostTag(TaggedItemBase):
54
59
)
55
60
56
61
57
- class BlogPost (BasePage , ClusterableModel , PagePreviewDescriptionMixin ):
62
+ class BlogPost (BasePage , ClusterableModel ):
58
63
"""A Blog post, implemented as a Wagtail page."""
59
64
60
- featured_image = models .ForeignKey (
65
+ template = "blog/blog_post.html"
66
+
67
+ description = RichTextField (
68
+ max_length = 200 ,
69
+ blank = True ,
70
+ null = True ,
71
+ features = ["bold" , "italic" ],
72
+ verbose_name = "Blog description" ,
73
+ help_text = "Short introduction to the page, aim for max two clear sentences (max. 200 chars)." ,
74
+ )
75
+
76
+ image = models .ForeignKey (
61
77
"wagtailimages.image" ,
62
78
null = True ,
63
79
blank = True ,
64
80
on_delete = models .SET_NULL ,
65
81
related_name = "+" ,
66
82
help_text = "Appears on the homepage carousel when post is featured." ,
67
83
)
84
+ caption = RichTextField (
85
+ features = [
86
+ "italic" ,
87
+ "bold" ,
88
+ "link" ,
89
+ ],
90
+ help_text = "A short caption for the image." ,
91
+ blank = True ,
92
+ max_length = 180 ,
93
+ )
94
+
95
+ credit = RichTextField (
96
+ features = [
97
+ "italic" ,
98
+ "bold" ,
99
+ "link" ,
100
+ ],
101
+ help_text = "A credit line or attribution for the image." ,
102
+ blank = True ,
103
+ max_length = 80 ,
104
+ )
105
+
106
+ alt_text = models .CharField (
107
+ help_text = "Describe the image for screen readers" ,
108
+ blank = True ,
109
+ max_length = 80 ,
110
+ )
68
111
tags = ClusterTaggableManager (through = BlogPostTag , blank = True )
69
112
featured = models .BooleanField (
70
113
default = False , help_text = "Show the post in the carousel on the homepage."
71
114
)
72
115
people = models .ManyToManyField (Person , through = "blog.Author" , related_name = "posts" )
73
116
117
+ category = models .CharField (
118
+ verbose_name = "Category" ,
119
+ help_text = "Category tag to display on tile" ,
120
+ blank = True ,
121
+ max_length = 20 ,
122
+ )
123
+
74
124
# can only be created underneath special link page
75
- parent_page_types = ["blog.BlogLinkPage " ]
125
+ parent_page_types = ["blog.BlogLinkPageArchived" , "blog.BlogLandingPage " ]
76
126
# no allowed subpages
77
127
subpage_types = []
78
128
79
129
# admin edit configuration
80
130
content_panels = Page .content_panels + [
81
- FieldRowPanel ((FieldPanel ("featured_image" ), FieldPanel ("featured" ))),
82
131
FieldPanel ("description" ),
132
+ MultiFieldPanel (
133
+ [
134
+ FieldPanel ("image" ),
135
+ FieldPanel ("caption" ),
136
+ FieldPanel ("credit" ),
137
+ FieldPanel ("alt_text" ),
138
+ ],
139
+ heading = "Hero Image" ,
140
+ ),
83
141
MultiFieldPanel (
84
142
[InlinePanel ("authors" , [AutocompletePanel ("person" )], label = "Author" )],
85
143
heading = "Authors" ,
86
144
),
145
+ FieldPanel ("category" ),
87
146
FieldPanel ("body" ),
88
147
FieldPanel ("attachments" ),
89
148
]
90
- promote_panels = Page .promote_panels + [FieldPanel ("tags" )]
149
+ promote_panels = (
150
+ [
151
+ MultiFieldPanel (
152
+ [
153
+ FieldPanel ("short_title" ),
154
+ FieldPanel ("short_description" ),
155
+ FieldPanel ("feed_image" ),
156
+ ],
157
+ "Share Page" ,
158
+ ),
159
+ ]
160
+ + BasePage .promote_panels
161
+ + [FieldPanel ("tags" )]
162
+ )
91
163
92
164
# index description in addition to body content
93
165
search_fields = BasePage .search_fields + [
@@ -104,34 +176,16 @@ class BlogPost(BasePage, ClusterableModel, PagePreviewDescriptionMixin):
104
176
# custom manager/queryset logic
105
177
objects = BlogPostManager ()
106
178
107
- # configure template path for wagtail preview
108
- template = "blog/blogpost_detail.html"
109
-
110
- @property
111
- def short_title (self ):
112
- """Shorter title with ellipsis."""
113
- return Truncator (self .title ).chars (65 )
114
-
115
- @property
116
- def short_description (self ):
117
- """Shorter description with ellipsis."""
118
- return Truncator (self .get_description ()).chars (250 )
179
+ @cached_property
180
+ def breadcrumbs (self ):
181
+ ancestors = self .get_ancestors ().live ().public ().specific ()
182
+ return ancestors [1 :] # removing root
119
183
120
184
@property
121
185
def author_list (self ):
122
186
"""Comma-separated list of author names."""
123
187
return ", " .join (str (author .person ) for author in self .authors .all ())
124
188
125
- def __str__ (self ):
126
- # string is used for logging actions on drafts,
127
- # needs to handle cases where first published date is not set
128
- if self .first_published_at :
129
- pubdate = format (self .first_published_at , "F j, Y" )
130
- else :
131
- pubdate = "draft"
132
-
133
- return '"%s" (%s)' % (self .short_title , pubdate )
134
-
135
189
def get_url_parts (self , * args , ** kwargs ):
136
190
"""Custom blog post URLs of the form /updates/2014/03/01/my-post."""
137
191
url_parts = super ().get_url_parts (* args , ** kwargs )
@@ -162,7 +216,7 @@ def get_sitemap_urls(self, request):
162
216
return urls
163
217
164
218
165
- class BlogLinkPage (LinkPage ):
219
+ class BlogLinkPageArchived (LinkPage ):
166
220
"""Container page that defines where blog posts can be created."""
167
221
168
222
# NOTE this page can't be created in the page editor; it is only ever made
@@ -171,3 +225,15 @@ class BlogLinkPage(LinkPage):
171
225
# NOTE the only allowed child page type is a BlogPost; this is so that
172
226
# Events made in the admin automatically are created here.
173
227
subpage_types = [BlogPost ]
228
+
229
+
230
+ class BlogLandingPage (StandardHeroMixinNoImage , Page ):
231
+ """Container page that defines where Event pages can be created."""
232
+
233
+ content_panels = StandardHeroMixinNoImage .content_panels
234
+
235
+ search_fields = StandardHeroMixinNoImage .search_fields
236
+
237
+ settings_panels = Page .settings_panels
238
+
239
+ subpage_types = [BlogPost ]
0 commit comments