Skip to content

Commit 53a0829

Browse files
committed
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
1 parent 01f9cf2 commit 53a0829

5 files changed

Lines changed: 172 additions & 3 deletions

File tree

static/sass/styles.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ dl {
273273
}
274274
}
275275

276+
276277
.snap-published-in-cell {
277278
@media (max-width: $breakpoint-large - 1) {
278279
display: none !important;

templates/store/snap-details/_details.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ <h5 class="p-muted-heading">Last updated</h5>
1919
<hr>
2020
{% endif %}
2121

22+
{% if old_snap_info and old_snap_info.is_old %}
23+
<div class="p-notification--caution is-borderless">
24+
<div class="p-notification__content">
25+
<p class="p-notification__message">
26+
This snap in this channel hasn't been updated since {{ old_snap_info.last_updated_formatted }}. Contact the developer to ask for an update.
27+
</p>
28+
</div>
29+
</div>
30+
<hr>
31+
{% endif %}
32+
2233
{% if links %}
2334
{% if links["website"] %}
2435
<h5 class="p-muted-heading">Websites</h5>

tests/store/tests_public_logic.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,3 +472,99 @@ def test_get_last_updated_versions(self):
472472
self.assertEqual(
473473
result, [latest_edge["channel"], latest_stable["channel"]]
474474
)
475+
476+
def test_is_snap_old_empty_date(self):
477+
"""Test that empty or None date returns not old"""
478+
result = logic.is_snap_old(None)
479+
self.assertFalse(result["is_old"])
480+
self.assertEqual(result["years_since_update"], 0)
481+
482+
result = logic.is_snap_old("")
483+
self.assertFalse(result["is_old"])
484+
self.assertEqual(result["years_since_update"], 0)
485+
486+
def test_is_snap_old_invalid_date(self):
487+
"""Test that invalid date returns not old"""
488+
result = logic.is_snap_old("invalid-date")
489+
self.assertFalse(result["is_old"])
490+
self.assertEqual(result["years_since_update"], 0)
491+
492+
@freeze_time("2024-01-01")
493+
def test_is_snap_old_recent_snap(self):
494+
"""Test that a recently updated snap is not considered old"""
495+
recent_date = "2023-06-01T10:00:00Z"
496+
result = logic.is_snap_old(recent_date)
497+
self.assertFalse(result["is_old"])
498+
self.assertEqual(result["years_since_update"], 0)
499+
self.assertIn("last_updated_formatted", result)
500+
501+
@freeze_time("2024-01-02")
502+
def test_is_snap_old_exactly_two_years(self):
503+
"""Test that a snap updated over 2 years ago is considered old"""
504+
old_date = "2021-12-01T10:00:00Z" # Over 2 years ago
505+
result = logic.is_snap_old(old_date)
506+
self.assertTrue(result["is_old"])
507+
self.assertEqual(result["years_since_update"], 2)
508+
self.assertIn("last_updated_formatted", result)
509+
510+
@freeze_time("2024-01-02")
511+
def test_is_snap_old_very_old_snap(self):
512+
"""Test that a very old snap is considered old"""
513+
very_old_date = "2020-01-01T10:00:00Z"
514+
result = logic.is_snap_old(very_old_date)
515+
self.assertTrue(result["is_old"])
516+
self.assertEqual(result["years_since_update"], 4)
517+
self.assertIn("last_updated_formatted", result)
518+
519+
@freeze_time("2024-01-01")
520+
def test_is_snap_old_just_under_two_years(self):
521+
"""Test that a snap updated just under 2 years ago is not old"""
522+
almost_old_date = "2022-02-01T10:00:00Z"
523+
result = logic.is_snap_old(almost_old_date)
524+
self.assertFalse(result["is_old"])
525+
self.assertEqual(result["years_since_update"], 1)
526+
self.assertIn("last_updated_formatted", result)
527+
528+
@freeze_time("2024-01-02")
529+
def test_is_snap_old_custom_threshold(self):
530+
"""Test that custom threshold works correctly"""
531+
date_one_year_ago = "2022-12-01T10:00:00Z" # Over 1 year ago
532+
533+
# With default threshold (2 years), should not be old
534+
result = logic.is_snap_old(date_one_year_ago)
535+
self.assertFalse(result["is_old"])
536+
537+
# With custom threshold (1 year), should be old
538+
result = logic.is_snap_old(date_one_year_ago, old_threshold_years=1)
539+
self.assertTrue(result["is_old"])
540+
self.assertEqual(result["years_since_update"], 1)
541+
542+
def test_is_snap_old_different_date_formats(self):
543+
"""Test that different ISO date formats are handled correctly"""
544+
# Test with different timezone formats
545+
dates_to_test = [
546+
"2020-01-01T10:00:00Z",
547+
"2020-01-01T10:00:00+00:00",
548+
"2020-01-01T10:00:00.000Z",
549+
"2020-01-01T10:00:00.123456+00:00",
550+
]
551+
552+
for date_str in dates_to_test:
553+
with freeze_time("2024-01-02"):
554+
result = logic.is_snap_old(date_str)
555+
self.assertTrue(
556+
result["is_old"], f"Failed for date format: {date_str}"
557+
)
558+
self.assertEqual(result["years_since_update"], 4)
559+
560+
def test_convert_date_month_year(self):
561+
"""Test the month-year date formatting function"""
562+
test_cases = [
563+
("2022-01-14T10:00:00Z", "January 2022"),
564+
("2020-12-25T15:30:00Z", "December 2020"),
565+
("2019-06-01T00:00:00Z", "June 2019"),
566+
]
567+
568+
for date_str, expected in test_cases:
569+
result = logic.convert_date_month_year(date_str)
570+
self.assertEqual(result, expected, f"Failed for date: {date_str}")

webapp/store/logic.py

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,16 +192,76 @@ def convert_date(date_to_convert):
192192
:param date_to_convert: Date to convert
193193
:returns: Readable date
194194
"""
195-
local_timezone = datetime.datetime.utcnow().tzinfo
196-
date_parsed = parser.parse(date_to_convert).replace(tzinfo=local_timezone)
197-
delta = datetime.datetime.utcnow() - datetime.timedelta(days=1)
195+
date_parsed = parser.parse(date_to_convert)
196+
# Ensure we have a timezone-aware datetime
197+
if date_parsed.tzinfo is None:
198+
date_parsed = date_parsed.replace(tzinfo=datetime.timezone.utc)
199+
200+
now = datetime.datetime.now(datetime.timezone.utc)
201+
delta = now - datetime.timedelta(days=1)
198202

199203
if delta < date_parsed:
200204
return humanize.naturalday(date_parsed).title()
201205
else:
202206
return date_parsed.strftime("%-d %B %Y")
203207

204208

209+
def convert_date_month_year(date_to_convert):
210+
"""Convert date to month and year format: Month Year
211+
212+
Format of date to convert: 2019-01-12T16:48:41.821037+00:00
213+
Output: January 2019
214+
215+
:param date_to_convert: Date to convert
216+
:returns: Month and year only
217+
"""
218+
date_parsed = parser.parse(date_to_convert)
219+
if date_parsed.tzinfo is None:
220+
date_parsed = date_parsed.replace(tzinfo=datetime.timezone.utc)
221+
222+
return date_parsed.strftime("%B %Y")
223+
224+
225+
def is_snap_old(last_updated_date, old_threshold_years=2.0):
226+
"""Check if a snap is considered 'old' based on its last update date
227+
228+
A snap is considered old if it hasn't been updated in the specified
229+
number of years (default: 2 years).
230+
231+
:param last_updated_date: The last updated date string in ISO format
232+
:param old_threshold_years: Number of years to consider a snap old
233+
(default: 2)
234+
:returns: Dictionary with 'is_old' boolean and 'years_since_update'
235+
integer
236+
"""
237+
if not last_updated_date:
238+
return {"is_old": False, "years_since_update": 0}
239+
240+
try:
241+
date_parsed = parser.parse(last_updated_date)
242+
if date_parsed.tzinfo is None:
243+
date_parsed = date_parsed.replace(tzinfo=datetime.timezone.utc)
244+
245+
now = datetime.datetime.now(datetime.timezone.utc)
246+
247+
days_diff = (now - date_parsed).days
248+
years_diff = days_diff / 365.25 # Account for leap years
249+
years_since_update = int(years_diff)
250+
251+
is_old = days_diff >= (old_threshold_years * 365.25)
252+
253+
return {
254+
"is_old": is_old,
255+
"years_since_update": years_since_update,
256+
"last_updated_formatted": convert_date_month_year(
257+
last_updated_date
258+
),
259+
}
260+
except (ValueError, TypeError):
261+
# If we can't parse the date, assume it's not old
262+
return {"is_old": False, "years_since_update": 0}
263+
264+
205265
categories_list = [
206266
"development",
207267
"games",

webapp/store/snap_details_views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ def _get_context_snap_details(snap_name, supported_architectures=None):
177177
"filesize": humanize.naturalsize(binary_filesize),
178178
"last_updated": logic.convert_date(last_updated),
179179
"last_updated_raw": last_updated,
180+
"old_snap_info": logic.is_snap_old(last_updated),
180181
"is_users_snap": is_users_snap,
181182
"unlisted": details.get("snap", {}).get("unlisted", False),
182183
"developer": developer,

0 commit comments

Comments
 (0)