Skip to content

Commit da5889d

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 0fabc7d commit da5889d

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
@@ -277,6 +277,7 @@ dl {
277277
}
278278
}
279279

280+
280281
.snap-published-in-cell {
281282
@media (max-width: $breakpoint-large - 1) {
282283
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
@@ -482,3 +482,99 @@ 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["is_old"])
490+
self.assertEqual(result["years_since_update"], 0)
491+
492+
result = logic.is_snap_old("")
493+
self.assertFalse(result["is_old"])
494+
self.assertEqual(result["years_since_update"], 0)
495+
496+
def test_is_snap_old_invalid_date(self):
497+
"""Test that invalid date returns not old"""
498+
result = logic.is_snap_old("invalid-date")
499+
self.assertFalse(result["is_old"])
500+
self.assertEqual(result["years_since_update"], 0)
501+
502+
@freeze_time("2024-01-01")
503+
def test_is_snap_old_recent_snap(self):
504+
"""Test that a recently updated snap is not considered old"""
505+
recent_date = "2023-06-01T10:00:00Z"
506+
result = logic.is_snap_old(recent_date)
507+
self.assertFalse(result["is_old"])
508+
self.assertEqual(result["years_since_update"], 0)
509+
self.assertIn("last_updated_formatted", result)
510+
511+
@freeze_time("2024-01-02")
512+
def test_is_snap_old_exactly_two_years(self):
513+
"""Test that a snap updated over 2 years ago is considered old"""
514+
old_date = "2021-12-01T10:00:00Z" # Over 2 years ago
515+
result = logic.is_snap_old(old_date)
516+
self.assertTrue(result["is_old"])
517+
self.assertEqual(result["years_since_update"], 2)
518+
self.assertIn("last_updated_formatted", result)
519+
520+
@freeze_time("2024-01-02")
521+
def test_is_snap_old_very_old_snap(self):
522+
"""Test that a very old snap is considered old"""
523+
very_old_date = "2020-01-01T10:00:00Z"
524+
result = logic.is_snap_old(very_old_date)
525+
self.assertTrue(result["is_old"])
526+
self.assertEqual(result["years_since_update"], 4)
527+
self.assertIn("last_updated_formatted", result)
528+
529+
@freeze_time("2024-01-01")
530+
def test_is_snap_old_just_under_two_years(self):
531+
"""Test that a snap updated just under 2 years ago is not old"""
532+
almost_old_date = "2022-02-01T10:00:00Z"
533+
result = logic.is_snap_old(almost_old_date)
534+
self.assertFalse(result["is_old"])
535+
self.assertEqual(result["years_since_update"], 1)
536+
self.assertIn("last_updated_formatted", result)
537+
538+
@freeze_time("2024-01-02")
539+
def test_is_snap_old_custom_threshold(self):
540+
"""Test that custom threshold works correctly"""
541+
date_one_year_ago = "2022-12-01T10:00:00Z" # Over 1 year ago
542+
543+
# With default threshold (2 years), should not be old
544+
result = logic.is_snap_old(date_one_year_ago)
545+
self.assertFalse(result["is_old"])
546+
547+
# With custom threshold (1 year), should be old
548+
result = logic.is_snap_old(date_one_year_ago, old_threshold_years=1)
549+
self.assertTrue(result["is_old"])
550+
self.assertEqual(result["years_since_update"], 1)
551+
552+
def test_is_snap_old_different_date_formats(self):
553+
"""Test that different ISO date formats are handled correctly"""
554+
# Test with different timezone formats
555+
dates_to_test = [
556+
"2020-01-01T10:00:00Z",
557+
"2020-01-01T10:00:00+00:00",
558+
"2020-01-01T10:00:00.000Z",
559+
"2020-01-01T10:00:00.123456+00:00",
560+
]
561+
562+
for date_str in dates_to_test:
563+
with freeze_time("2024-01-02"):
564+
result = logic.is_snap_old(date_str)
565+
self.assertTrue(
566+
result["is_old"], f"Failed for date format: {date_str}"
567+
)
568+
self.assertEqual(result["years_since_update"], 4)
569+
570+
def test_convert_date_month_year(self):
571+
"""Test the month-year date formatting function"""
572+
test_cases = [
573+
("2022-01-14T10:00:00Z", "January 2022"),
574+
("2020-12-25T15:30:00Z", "December 2020"),
575+
("2019-06-01T00:00:00Z", "June 2019"),
576+
]
577+
578+
for date_str, expected in test_cases:
579+
result = logic.convert_date_month_year(date_str)
580+
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
@@ -193,16 +193,76 @@ def convert_date(date_to_convert):
193193
:param date_to_convert: Date to convert
194194
:returns: Readable date
195195
"""
196-
local_timezone = datetime.datetime.utcnow().tzinfo
197-
date_parsed = parser.parse(date_to_convert).replace(tzinfo=local_timezone)
198-
delta = datetime.datetime.utcnow() - datetime.timedelta(days=1)
196+
date_parsed = parser.parse(date_to_convert)
197+
# Ensure we have a timezone-aware datetime
198+
if date_parsed.tzinfo is None:
199+
date_parsed = date_parsed.replace(tzinfo=datetime.timezone.utc)
200+
201+
now = datetime.datetime.now(datetime.timezone.utc)
202+
delta = now - datetime.timedelta(days=1)
199203

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

205209

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

webapp/store/snap_details_views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ def _get_context_snap_details(snap_name, supported_architectures=None):
191191
"filesize": humanize.naturalsize(binary_filesize),
192192
"last_updated": logic.convert_date(last_updated),
193193
"last_updated_raw": last_updated,
194+
"old_snap_info": logic.is_snap_old(last_updated),
194195
"is_users_snap": is_users_snap,
195196
"unlisted": details.get("snap", {}).get("unlisted", False),
196197
"developer": developer,

0 commit comments

Comments
 (0)