diff --git a/templates/store/snap-details/_details.html b/templates/store/snap-details/_details.html index 488d8fba01..1859d530eb 100644 --- a/templates/store/snap-details/_details.html +++ b/templates/store/snap-details/_details.html @@ -16,6 +16,15 @@

Last updated

  • {{ updates[1]["released-at-display"] }} - {{ updates[1]["track"] }}/{{ updates[1]["risk"] }}
  • {% endif %} + {% if old_snap_info %} +
    +
    +

    + This snap hasn't been updated in a while. It might be unmaintained and have stability or security issues. +

    +
    +
    + {% endif %}
    {% endif %} diff --git a/tests/store/tests_public_logic.py b/tests/store/tests_public_logic.py index 6622971130..20374b7ecd 100644 --- a/tests/store/tests_public_logic.py +++ b/tests/store/tests_public_logic.py @@ -482,3 +482,72 @@ def test_get_last_updated_versions(self): self.assertEqual( result, [latest_edge["channel"], latest_stable["channel"]] ) + + def test_is_snap_old_empty_date(self): + """Test that empty or None date returns not old""" + result = logic.is_snap_old(None) + self.assertFalse(result) + + result = logic.is_snap_old("") + self.assertFalse(result) + + def test_is_snap_old_invalid_date(self): + """Test that invalid date returns not old""" + result = logic.is_snap_old("invalid-date") + self.assertFalse(result) + + @freeze_time("2024-01-01") + def test_is_snap_old_recent_snap(self): + """Test that a recently updated snap is not considered old""" + recent_date = "2023-06-01T10:00:00Z" + result = logic.is_snap_old(recent_date) + self.assertFalse(result) + + @freeze_time("2024-01-02T10:00:00Z") + def test_is_snap_old_exactly_two_years(self): + """Test that a snap updated exactly 2 years ago is considered old""" + old_date = "2022-01-02T10:00:00Z" # Exactly 2 years ago + result = logic.is_snap_old(old_date) + self.assertTrue(result) + + @freeze_time("2024-01-02") + def test_is_snap_old_very_old_snap(self): + """Test that a very old snap is considered old""" + very_old_date = "2020-01-01T10:00:00Z" + result = logic.is_snap_old(very_old_date) + self.assertTrue(result) + + @freeze_time("2024-01-01") + def test_is_snap_old_just_under_two_years(self): + """Test that a snap updated just under 2 years ago is not old""" + almost_old_date = "2022-02-01T10:00:00Z" + result = logic.is_snap_old(almost_old_date) + self.assertFalse(result) + + @freeze_time("2024-01-02") + def test_is_snap_old_custom_threshold(self): + """Test that custom threshold works correctly""" + date_one_year_ago = "2022-12-01T10:00:00Z" # Over 1 year ago + + # With default threshold (2 years), should not be old + result = logic.is_snap_old(date_one_year_ago) + self.assertFalse(result) + + # With custom threshold (1 year), should be old + result = logic.is_snap_old(date_one_year_ago, old_threshold_years=1) + self.assertTrue(result) + + def test_is_snap_old_different_date_formats(self): + """Test that different ISO date formats are handled correctly""" + # Test with different timezone formats + dates_to_test = [ + "2020-01-01T10:00:00Z", + "2020-01-01T10:00:00+00:00", + "2020-01-01T10:00:00.000Z", + "2020-01-01T10:00:00.123456+00:00", + ] + + for date_str in dates_to_test: + with freeze_time("2024-01-02"): + result = logic.is_snap_old(date_str) + self.assertTrue(result, f"Failed for date format: {date_str}") diff --git a/webapp/store/logic.py b/webapp/store/logic.py index 509ff6d7f2..620fb1b64d 100644 --- a/webapp/store/logic.py +++ b/webapp/store/logic.py @@ -5,6 +5,7 @@ import humanize from dateutil import parser +from dateutil.relativedelta import relativedelta from webapp import helpers @@ -203,6 +204,36 @@ def convert_date(date_to_convert): return date_parsed.strftime("%-d %B %Y") +def is_snap_old(last_updated_date, old_threshold_years=2.0): + """Check if a snap is considered 'old' based on its last update date + + A snap is considered old if it hasn't been updated in the specified + number of years (default: 2 years). + + :param last_updated_date: The last updated date string in ISO format + :param old_threshold_years: Number of years to consider a snap old + (default: 2) + :returns: True if snap is old, False otherwise + """ + if not last_updated_date: + return False + + try: + date_parsed = parser.parse(last_updated_date) + if date_parsed.tzinfo is None: + date_parsed = date_parsed.replace(tzinfo=datetime.timezone.utc) + + now = datetime.datetime.now(datetime.timezone.utc) + + delta = relativedelta(now, date_parsed) + years_since_update = delta.years + + return years_since_update >= old_threshold_years + except (ValueError, TypeError): + # If we can't parse the date, assume it's not old + return False + + categories_list = [ "development", "games", diff --git a/webapp/store/snap_details_views.py b/webapp/store/snap_details_views.py index 177bd04289..8fb29530bb 100644 --- a/webapp/store/snap_details_views.py +++ b/webapp/store/snap_details_views.py @@ -106,6 +106,24 @@ def _get_context_snap_details(snap_name, supported_architectures=None): lowest_risk_available, supported_architectures, ) + + # Determine the most recent update date from updates tuple + # updates[0] is the stable channel, updates[1] is the most + # recent non-stable + most_recent_update = None + if updates[0] and updates[1]: + # Compare both and use the most recent + date_0 = updates[0].get("released-at") + date_1 = updates[1].get("released-at") + if date_0 and date_1: + most_recent_update = max(date_0, date_1) + else: + most_recent_update = date_0 or date_1 + elif updates[0]: + most_recent_update = updates[0].get("released-at") + elif updates[1]: + most_recent_update = updates[1].get("released-at") + binary_filesize = latest_channel["download"]["size"] # filter out banner and banner-icon images from screenshots @@ -191,6 +209,7 @@ def _get_context_snap_details(snap_name, supported_architectures=None): "filesize": humanize.naturalsize(binary_filesize), "last_updated": logic.convert_date(last_updated), "last_updated_raw": last_updated, + "old_snap_info": logic.is_snap_old(most_recent_update), "is_users_snap": is_users_snap, "unlisted": details.get("snap", {}).get("unlisted", False), "developer": developer,