Skip to content

Commit cf5f30c

Browse files
committed
DBC22-6585: highlighted advisory for new updates
DBC22-6585: highlighted advisory for new updates 2 DBC22-6585: highlighted bulletin for new updates DBC22-6585: added pill on the header when advisory or bulletin updated DBC22-6585: added publish minor updates from backend DBC22-6585: fix bugs 1 DBC22-6585: updated details page DBC22-6585: clear status when navigate to other pages DBC22-6585: updated header for numbered pill DBC22-6585: fixed bugs 1 DBC22-6585: fixed bugs 2 DBC22-6585: updated header for numbered pill test DBC22-6585: removed unused variables DBC22-6585: removed unused variables 1 DBC22-6585: removed unused variables 2 DBC22-6585: fixed bugs part 1 DBC22-6585: fixed bugs part 2 DBC22-6585: fixed the issue that displaying old time when last notified date is null DBC22-6585: fixed bulletin list styling issue DBC22-6585: test 1 DBC22-6585: fixed sync issue DBC22-6585: test 2 DBC22-6585: recalculate unread advisories or bulletins DBC22-6585: recalculate unread advisories or bulletins test 1 DBC22-6585: fixed github quality alerts DBC22-6585: removed format not needed DBC22-6585: removed bold styling for detail page
1 parent 0c4cb29 commit cf5f30c

18 files changed

Lines changed: 1176 additions & 199 deletions
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 5.2.14 on 2026-06-10 17:14
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('cms', '0025_advisoryindexpage_bulletinindexpage_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='advisory',
15+
name='last_notified_at',
16+
field=models.DateTimeField(blank=True, null=True),
17+
),
18+
migrations.AddField(
19+
model_name='bulletin',
20+
name='last_notified_at',
21+
field=models.DateTimeField(blank=True, null=True),
22+
),
23+
]

src/backend/apps/cms/models.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ def rendered_body(self):
196196
# Geo fields
197197
geometry = models.MultiPolygonField()
198198

199+
last_notified_at = models.DateTimeField(null=True, blank=True)
200+
199201
# Editor panels configuration
200202
content_panels = [
201203
FieldPanel("title", help_text=HelpText.GENERIC_TITLE),
@@ -209,7 +211,7 @@ def rendered_body(self):
209211
FieldPanel("body", help_text=HelpText.GENERIC_BODY),
210212
FieldPanel('created_at', read_only=True, heading="Created"),
211213
FieldPanel('first_published_at', read_only=True, heading="Published"),
212-
FieldPanel('last_published_at', read_only=True, heading="Updated"),
214+
FieldPanel('last_notified_at', read_only=True, heading="Updated"),
213215
]
214216
promote_panels = []
215217

@@ -252,6 +254,7 @@ class Bulletin(Page, BaseModel):
252254
body = StreamField(RichContent())
253255
image = models.ForeignKey(Image, on_delete=models.SET_NULL, null=True, blank=False)
254256
image_alt_text = models.CharField(max_length=125, default='', blank=False)
257+
last_notified_at = models.DateTimeField(null=True, blank=True)
255258

256259
def rendered_body(self):
257260
blocks = [wagtailcore_tags.richtext(block.render()) for block in self.body]
@@ -280,7 +283,7 @@ def save(self, *args, **kwargs):
280283
FieldPanel("body", help_text=HelpText.GENERIC_BODY),
281284
FieldPanel('created_at', read_only=True, heading="Created"),
282285
FieldPanel('first_published_at', read_only=True, heading="Published"),
283-
FieldPanel('last_published_at', read_only=True, heading="Updated"),
286+
FieldPanel('last_notified_at', read_only=True, heading="Updated"),
284287
]
285288
promote_panels = []
286289

src/backend/apps/cms/views.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
from django.template.loader import render_to_string
1616
from django.views.decorators.csrf import csrf_exempt
1717
from rest_framework import viewsets
18+
from django.contrib import messages
19+
from django.shortcuts import redirect
20+
from wagtail.models import Page
21+
from django.http import Http404
1822

1923

2024
class CMSViewSet(viewsets.ReadOnlyModelViewSet):
@@ -146,3 +150,38 @@ def access_denied_idir(request):
146150
return render(request, 'wagtailadmin/access_denied.html', context={
147151
"is_non_idir_login": True,
148152
})
153+
154+
155+
def publish_minor_update(request, page_id):
156+
if request.method != "POST":
157+
try:
158+
page_id = int(page_id)
159+
except (TypeError, ValueError):
160+
raise Http404()
161+
return redirect(f"/drivebc-cms/pages/{page_id}/edit/")
162+
163+
page = Page.objects.get(pk=page_id).specific
164+
latest_revision = page.get_latest_revision()
165+
166+
if latest_revision:
167+
# Snapshot the current last_notified_at before publishing
168+
last_notified_at = page.last_notified_at
169+
170+
# Publish the revision (updates content but triggers after_publish_page)
171+
latest_revision.publish(changed=False)
172+
173+
# Restore last_notified_at and also revert last_published_at
174+
# so the "Updated" panel field doesn't change
175+
Page.objects.filter(pk=page.pk).update(
176+
last_published_at=last_notified_at
177+
)
178+
type(page).objects.filter(pk=page.pk).update(
179+
last_notified_at=last_notified_at
180+
)
181+
182+
messages.success(request, f'"{page.title}" published as a minor update.')
183+
try:
184+
page_id = int(page_id)
185+
except (TypeError, ValueError):
186+
raise Http404()
187+
return redirect(f"/drivebc-cms/pages/{page_id}/edit/")

src/backend/apps/cms/wagtail_hooks.py

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from wagtail_modeladmin.options import ModelAdmin, modeladmin_register
1414

1515
from .models import Advisory, Bulletin, SubPage, get_or_create_advisory_index, get_or_create_bulletin_index
16-
from .views import access_requested
16+
from .views import access_requested, publish_minor_update
1717

1818
# from wagtail.admin.ui.components import ActionMenuItem
1919
from wagtail.admin.action_menu import ActionMenuItem
@@ -28,14 +28,21 @@
2828

2929
@hooks.register("after_publish_page")
3030
def post_edit_hook(request, page):
31-
# Only process published advisory pages
31+
from django.utils import timezone
32+
3233
if page.specific_class == Advisory:
3334
try:
35+
Advisory.objects.filter(pk=page.pk).update(last_notified_at=timezone.now())
3436
send_advisory_notifications(page.id)
35-
3637
except Exception:
3738
logger.error(request, 'There was a problem sending an advisory notification')
3839

40+
elif page.specific_class == Bulletin:
41+
try:
42+
Bulletin.objects.filter(pk=page.pk).update(last_notified_at=timezone.now())
43+
except Exception:
44+
logger.error(request, 'There was a problem updating bulletin notification timestamp')
45+
3946

4047
@hooks.register("insert_global_admin_css")
4148
def insert_global_admin_css():
@@ -207,6 +214,49 @@ def render_html(self, parent_context):
207214
preview_url,
208215
)
209216

217+
class PublishMinorUpdateMenuItem(ActionMenuItem):
218+
label = "Publish minor update"
219+
name = "publish-minor-update"
220+
icon_name = "upload"
221+
order = 15
222+
223+
def render_html(self, parent_context):
224+
context = parent_context.get("context", parent_context)
225+
page = context.get("page")
226+
227+
if not page:
228+
return ""
229+
230+
url = reverse("cms-publish-minor-update", args=[page.pk])
231+
232+
if isinstance(page.specific, Advisory):
233+
redirect_url = "/drivebc-cms/cms/advisory/"
234+
elif isinstance(page.specific, Bulletin):
235+
redirect_url = "/drivebc-cms/cms/bulletin/"
236+
else:
237+
redirect_url = "/drivebc-cms/"
238+
239+
return format_html(
240+
"""
241+
<li class="action-menu__item">
242+
<button
243+
type="button"
244+
class="action button"
245+
style="width: 100%; text-align: left;"
246+
onclick="fetch('{}', {{
247+
method: 'POST',
248+
headers: {{'X-CSRFToken': document.cookie.match(/csrftoken=([^;]+)/)[1]}},
249+
}}).then(() => window.location.href = '{}'); return false;"
250+
>
251+
<svg class="icon icon-upload icon" aria-hidden="true"><use href="#icon-upload"></use></svg>
252+
Publish minor update
253+
</button>
254+
</li>
255+
""",
256+
url,
257+
redirect_url,
258+
)
259+
210260

211261
@hooks.register("register_page_action_menu_item")
212262
def register_copy_preview_button():
@@ -281,3 +331,21 @@ def move_new_advisory_to_top(request, page):
281331
if page.specific_class == Advisory:
282332
parent = page.get_parent()
283333
page.move(parent, pos="first-child")
334+
335+
@hooks.register("register_page_action_menu_item")
336+
def register_publish_minor_update():
337+
return PublishMinorUpdateMenuItem(order=25)
338+
339+
@hooks.register("construct_page_action_menu")
340+
def customize_page_action_menu(menu_items, request, context):
341+
for item in menu_items:
342+
if item.name == "action-publish":
343+
item.label = "Publish with notifications"
344+
345+
346+
@hooks.register('register_admin_urls')
347+
def add_access_requested_url():
348+
return [
349+
path('access-requested', access_requested, name='cms-access-requested'),
350+
path('publish-minor-update/<int:page_id>/', publish_minor_update, name='cms-publish-minor-update'),
351+
]

src/frontend/src/Components/advisories/AdvisoriesList.js

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export default function AdvisoriesList(props) {
3030
const navigate = useNavigate();
3131

3232
// Props
33-
const { advisories, showDescription, showTimestamp, showPublished, isAdvisoriesListPage, showLoader } = props;
33+
const { advisories, showDescription, showTimestamp, showPublished, isAdvisoriesListPage, showLoader, trackedAdvisories = {}, advisoryRefs, dismissHighlight } = props;
3434

3535
function handleClick(advisory, keyEvent) {
3636
// Ignore key presses that aren't enter or space
@@ -39,6 +39,12 @@ export default function AdvisoriesList(props) {
3939
}
4040

4141
trackEvent('click', 'advisories-list', 'Advisory', advisory.title, advisory.teaser);
42+
// dismissedHighlightsRef.current.add(advisory.id);
43+
// sessionStorage.setItem(
44+
// 'dismissedHighlights',
45+
// JSON.stringify([...dismissedHighlightsRef.current])
46+
// );
47+
dismissHighlight(advisory.id); // use parent's function
4248
navigate(`/advisories/${advisory.slug}`);
4349
}
4450

@@ -50,7 +56,12 @@ export default function AdvisoriesList(props) {
5056
{!!sortedAdvisories && sortedAdvisories.map((advisory, index) => {
5157
if (isAdvisoriesListPage) {
5258
return (
53-
<div className="advisory-li" key={advisory.id}>
59+
<div
60+
className={`advisory-li${trackedAdvisories[advisory.id]?.highlight ? ' highlighted' : ''}`}
61+
key={advisory.id}
62+
data-key={advisory.id}
63+
ref={(el) => { if (advisoryRefs) advisoryRefs.current[advisory.id] = el; }}
64+
>
5465
<div className="advisory-li__content">
5566
<div className="advisory-li__content__partition advisory-li-title-container">
5667
<div className="advisory-li-title link-div"
@@ -65,8 +76,11 @@ export default function AdvisoriesList(props) {
6576
<Skeleton width={260} height={10} className="hidden-mobile" /> :
6677

6778
<div className="timestamp-container">
68-
<span className="advisory-li-state">{advisory.first_published_at != advisory.last_published_at ? "Updated" : "Published" }</span>
69-
<FriendlyTime date={advisory.latest_revision_created_at} />
79+
<span className="advisory-li-state">{advisory.last_notified_at != advisory.first_published_at ? "Updated" : "Published" }</span>
80+
<FriendlyTime date={advisory.last_notified_at ?? advisory.last_published_at} />
81+
{trackedAdvisories[advisory.id]?.highlight &&
82+
<div className="updated-pill">Updated</div>
83+
}
7084
</div>
7185
}
7286
</div>
@@ -95,8 +109,11 @@ export default function AdvisoriesList(props) {
95109
<Skeleton width={320} height={10} className='hidden-desktop' /> :
96110

97111
<div className="advisory-li__content__partition timestamp-container timestamp-container--mobile">
98-
<span className="advisory-li-state">{advisory.first_published_at != advisory.last_published_at ? "Updated" : "Published"}</span>
99-
<FriendlyTime date={advisory.latest_revision_created_at}/>
112+
<span className="advisory-li-state">{advisory.last_notified_at != advisory.first_published_at ? "Updated" : "Published"}</span>
113+
<FriendlyTime date={advisory.last_notified_at ?? advisory.last_published_at} />
114+
{trackedAdvisories[advisory.id]?.highlight &&
115+
<div className="updated-pill">Updated</div>
116+
}
100117
</div>
101118
}
102119

@@ -122,7 +139,7 @@ export default function AdvisoriesList(props) {
122139

123140
} else {
124141
return (
125-
<div className={`advisory-li link-div ${!cmsContext.readAdvisories.includes(advisory.id.toString() + '-' + advisory.live_revision.toString()) ? 'unread' : ''}`}
142+
<div className={`advisory-li link-div ${!cmsContext.readAdvisories.includes(advisory.id.toString() + '-' + advisory.last_notified_at?.toString()) ? 'unread' : ''}`}
126143
key={advisory.id}
127144
onClick={() => handleClick(advisory)}
128145
onKeyDown={(keyEvent) => handleClick(advisory, keyEvent)}
@@ -139,8 +156,11 @@ export default function AdvisoriesList(props) {
139156
{(showTimestamp && showPublished) &&
140157
<div className="timestamp-container">
141158
{!cmsContext.readAdvisories.includes(advisory.id.toString() + '-' + advisory.live_revision.toString()) && <div className="unread-display"></div>}
142-
<span className="advisory-li-state">{advisory.first_published_at != advisory.last_published_at ? "Updated" : "Published" }</span>
143-
<FriendlyTime date={advisory.latest_revision_created_at} />
159+
<span className="advisory-li-state">{advisory.last_notified_at != advisory.first_published_at ? "Updated" : "Published" }</span>
160+
<FriendlyTime date={advisory.last_notified_at ?? advisory.last_published_at} />
161+
{trackedAdvisories[advisory.id]?.highlight &&
162+
<div className="updated-pill">Updated</div>
163+
}
144164
</div>
145165
}
146166

@@ -165,7 +185,10 @@ export default function AdvisoriesList(props) {
165185
{showTimestamp &&
166186
<div className="timestamp-container timestamp-container--mobile">
167187
<span className="advisory-li-state">{advisory.first_published_at != advisory.last_published_at ? "Updated" : "Published" }</span>
168-
<FriendlyTime date={advisory.latest_revision_created_at} />
188+
<FriendlyTime date={advisory.last_notified_at ?? advisory.last_published_at} />
189+
{trackedAdvisories[advisory.id]?.highlight &&
190+
<div className="updated-pill">Updated</div>
191+
}
169192
</div>
170193
}
171194
</div>

src/frontend/src/Components/bulletins/BulletinsList.js

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,16 @@ import Skeleton from 'react-loading-skeleton';
1616
// Static files
1717
import logo from '../../images/dbc-logo--white.svg';
1818

19-
export default function Bulletins(props) {
20-
// State, props and context
21-
const { bulletins, showLoader } = props;
19+
export default function BulletinsList(props) {
20+
// Props
21+
const { bulletins, showLoader, trackedBulletins = {}, bulletinRefs, dismissHighlight } = props;
2222

2323
// Navigation
2424
const navigate = useNavigate();
2525

2626
function handleClick(bulletin) {
2727
trackEvent('click', 'bulletins-list', 'Bulletin', bulletin.title, bulletin.teaser);
28+
if (dismissHighlight) dismissHighlight(bulletin.id);
2829
navigate(`/bulletins/${bulletin.slug}`);
2930
}
3031

@@ -33,9 +34,14 @@ export default function Bulletins(props) {
3334
// Rendering
3435
return (
3536
<ul className='bulletins-list'>
36-
{!!sortedBulletins && sortedBulletins.map((bulletin, index) => {
37+
{!!sortedBulletins && sortedBulletins.map((bulletin) => {
3738
return (
38-
<li className='bulletin-li' key={bulletin.id} onClick={() => handleClick(bulletin)}
39+
<li
40+
className={`bulletin-li${trackedBulletins[bulletin.id]?.highlight ? ' highlighted' : ''}`}
41+
key={bulletin.id}
42+
data-key={bulletin.id}
43+
ref={(el) => { if (bulletinRefs) bulletinRefs.current[bulletin.id] = el; }}
44+
onClick={() => handleClick(bulletin)}
3945
onKeyDown={(keyEvent) => {
4046
if (['Enter', 'NumpadEnter'].includes(keyEvent.key)) {
4147
handleClick(bulletin);
@@ -45,7 +51,6 @@ export default function Bulletins(props) {
4551
<div className='bulletin-li-title-container' tabIndex={0}>
4652
{showLoader ?
4753
<p><Skeleton height={30} /></p> :
48-
4954
<h2 className='bulletin-li-title'>{bulletin.title}</h2>
5055
}
5156

@@ -54,18 +59,21 @@ export default function Bulletins(props) {
5459
<Skeleton height={10} />
5560
<Skeleton height={10} />
5661
</p> :
57-
5862
(bulletin.teaser &&
5963
<div className="bulletin-li-body">{bulletin.teaser}</div>
6064
)
6165
}
6266

6367
{showLoader ?
6468
<p><Skeleton width={150} height={10} /></p> :
65-
6669
<div className='timestamp-container'>
67-
<span className='bulletin-li-state'>{bulletin.first_published_at != bulletin.last_published_at ? 'Updated' : 'Published' }</span>
68-
<FriendlyTime date={bulletin.latest_revision_created_at} />
70+
<span className='bulletin-li-state'>
71+
{bulletin.first_published_at != bulletin.last_notified_at ? 'Updated' : 'Published'}
72+
</span>
73+
<FriendlyTime date={bulletin.last_notified_at ?? bulletin.last_published_at} />
74+
{trackedBulletins[bulletin.id]?.highlight &&
75+
<div className="updated-pill">Updated</div>
76+
}
6977
</div>
7078
}
7179
</div>
@@ -74,7 +82,6 @@ export default function Bulletins(props) {
7482
<div className={bulletin.image_url ? 'bulletin-li-thumbnail' : 'bulletin-li-thumbnail-default'}>
7583
{showLoader ?
7684
<Skeleton width={320} height={200} /> :
77-
7885
<img className='thumbnail-logo' src={bulletin.image_url ? bulletin.image_url : logo} alt={bulletin.image_alt_text} />
7986
}
8087
</div>

0 commit comments

Comments
 (0)