Skip to content

Commit 3458caf

Browse files
authored
feat(ui): Add warning signpost for old snaps (#5352)
* feat: add old snap warning for snaps not updated in 2+ years - Add is_snap_old() function to detect snaps older than 2 years - Add convert_date_month_year() for cleaner date formatting - Display warning notification on snap details for old snaps - Use simple "Month Year" format for better readability - Add custom styling with orange left border for consistency - Include comprehensive test coverage for date logic - Message: "This snap in this channel hasn't been updated since [Month Year]. Contact the developer to ask for an update." Fixes #2988 * fix: changes based on PR feedback * fix: reverted unwanted refactoring Signed-off-by: Gajesh Bhat <gajeshbht@gmail.com> * Simplify is_snap_old to return boolean and update notification message - Changed notification message to vague 'hasn't been updated in a while' - Simplified is_snap_old to return boolean instead of dictionary - Removed convert_date_month_year function (no longer needed) - Updated all tests to work with boolean return value - Fixed test_is_snap_old_exactly_two_years timestamp to match exactly 2 years --------- Signed-off-by: Gajesh Bhat <gajeshbht@gmail.com>
1 parent 6bbc38f commit 3458caf

4 files changed

Lines changed: 128 additions & 0 deletions

File tree

templates/store/snap-details/_details.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ <h3 class="p-muted-heading">Last updated</h3>
1616
<li>{{ updates[1]["released-at-display"] }} - <small>{{ updates[1]["track"] }}/{{ updates[1]["risk"] }}</small></li>
1717
{% endif %}
1818
</ul>
19+
{% if old_snap_info %}
20+
<div class="p-notification--caution is-borderless">
21+
<div class="p-notification__content">
22+
<p class="p-notification__message">
23+
This snap hasn't been updated in a while. It might be unmaintained and have stability or security issues.
24+
</p>
25+
</div>
26+
</div>
27+
{% endif %}
1928
<hr class="p-rule--muted">
2029
{% endif %}
2130

tests/store/tests_public_logic.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,3 +482,72 @@ def test_get_last_updated_versions(self):
482482
self.assertEqual(
483483
result, [latest_edge["channel"], latest_stable["channel"]]
484484
)
485+
486+
def test_is_snap_old_empty_date(self):
487+
"""Test that empty or None date returns not old"""
488+
result = logic.is_snap_old(None)
489+
self.assertFalse(result)
490+
491+
result = logic.is_snap_old("")
492+
self.assertFalse(result)
493+
494+
def test_is_snap_old_invalid_date(self):
495+
"""Test that invalid date returns not old"""
496+
result = logic.is_snap_old("invalid-date")
497+
self.assertFalse(result)
498+
499+
@freeze_time("2024-01-01")
500+
def test_is_snap_old_recent_snap(self):
501+
"""Test that a recently updated snap is not considered old"""
502+
recent_date = "2023-06-01T10:00:00Z"
503+
result = logic.is_snap_old(recent_date)
504+
self.assertFalse(result)
505+
506+
@freeze_time("2024-01-02T10:00:00Z")
507+
def test_is_snap_old_exactly_two_years(self):
508+
"""Test that a snap updated exactly 2 years ago is considered old"""
509+
old_date = "2022-01-02T10:00:00Z" # Exactly 2 years ago
510+
result = logic.is_snap_old(old_date)
511+
self.assertTrue(result)
512+
513+
@freeze_time("2024-01-02")
514+
def test_is_snap_old_very_old_snap(self):
515+
"""Test that a very old snap is considered old"""
516+
very_old_date = "2020-01-01T10:00:00Z"
517+
result = logic.is_snap_old(very_old_date)
518+
self.assertTrue(result)
519+
520+
@freeze_time("2024-01-01")
521+
def test_is_snap_old_just_under_two_years(self):
522+
"""Test that a snap updated just under 2 years ago is not old"""
523+
almost_old_date = "2022-02-01T10:00:00Z"
524+
result = logic.is_snap_old(almost_old_date)
525+
self.assertFalse(result)
526+
527+
@freeze_time("2024-01-02")
528+
def test_is_snap_old_custom_threshold(self):
529+
"""Test that custom threshold works correctly"""
530+
date_one_year_ago = "2022-12-01T10:00:00Z" # Over 1 year ago
531+
532+
# With default threshold (2 years), should not be old
533+
result = logic.is_snap_old(date_one_year_ago)
534+
self.assertFalse(result)
535+
536+
# With custom threshold (1 year), should be old
537+
result = logic.is_snap_old(date_one_year_ago, old_threshold_years=1)
538+
self.assertTrue(result)
539+
540+
def test_is_snap_old_different_date_formats(self):
541+
"""Test that different ISO date formats are handled correctly"""
542+
# Test with different timezone formats
543+
dates_to_test = [
544+
"2020-01-01T10:00:00Z",
545+
"2020-01-01T10:00:00+00:00",
546+
"2020-01-01T10:00:00.000Z",
547+
"2020-01-01T10:00:00.123456+00:00",
548+
]
549+
550+
for date_str in dates_to_test:
551+
with freeze_time("2024-01-02"):
552+
result = logic.is_snap_old(date_str)
553+
self.assertTrue(result, f"Failed for date format: {date_str}")

webapp/store/logic.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import humanize
77
from dateutil import parser
8+
from dateutil.relativedelta import relativedelta
89
from webapp import helpers
910

1011

@@ -203,6 +204,36 @@ def convert_date(date_to_convert):
203204
return date_parsed.strftime("%-d %B %Y")
204205

205206

207+
def is_snap_old(last_updated_date, old_threshold_years=2.0):
208+
"""Check if a snap is considered 'old' based on its last update date
209+
210+
A snap is considered old if it hasn't been updated in the specified
211+
number of years (default: 2 years).
212+
213+
:param last_updated_date: The last updated date string in ISO format
214+
:param old_threshold_years: Number of years to consider a snap old
215+
(default: 2)
216+
:returns: True if snap is old, False otherwise
217+
"""
218+
if not last_updated_date:
219+
return False
220+
221+
try:
222+
date_parsed = parser.parse(last_updated_date)
223+
if date_parsed.tzinfo is None:
224+
date_parsed = date_parsed.replace(tzinfo=datetime.timezone.utc)
225+
226+
now = datetime.datetime.now(datetime.timezone.utc)
227+
228+
delta = relativedelta(now, date_parsed)
229+
years_since_update = delta.years
230+
231+
return years_since_update >= old_threshold_years
232+
except (ValueError, TypeError):
233+
# If we can't parse the date, assume it's not old
234+
return False
235+
236+
206237
categories_list = [
207238
"development",
208239
"games",

webapp/store/snap_details_views.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,24 @@ def _get_context_snap_details(snap_name, supported_architectures=None):
106106
lowest_risk_available,
107107
supported_architectures,
108108
)
109+
110+
# Determine the most recent update date from updates tuple
111+
# updates[0] is the stable channel, updates[1] is the most
112+
# recent non-stable
113+
most_recent_update = None
114+
if updates[0] and updates[1]:
115+
# Compare both and use the most recent
116+
date_0 = updates[0].get("released-at")
117+
date_1 = updates[1].get("released-at")
118+
if date_0 and date_1:
119+
most_recent_update = max(date_0, date_1)
120+
else:
121+
most_recent_update = date_0 or date_1
122+
elif updates[0]:
123+
most_recent_update = updates[0].get("released-at")
124+
elif updates[1]:
125+
most_recent_update = updates[1].get("released-at")
126+
109127
binary_filesize = latest_channel["download"]["size"]
110128

111129
# filter out banner and banner-icon images from screenshots
@@ -191,6 +209,7 @@ def _get_context_snap_details(snap_name, supported_architectures=None):
191209
"filesize": humanize.naturalsize(binary_filesize),
192210
"last_updated": logic.convert_date(last_updated),
193211
"last_updated_raw": last_updated,
212+
"old_snap_info": logic.is_snap_old(most_recent_update),
194213
"is_users_snap": is_users_snap,
195214
"unlisted": details.get("snap", {}).get("unlisted", False),
196215
"developer": developer,

0 commit comments

Comments
 (0)