diff --git a/readthedocs/projects/version_handling.py b/readthedocs/projects/version_handling.py index 7a730e61fd0..15cfe8be91e 100644 --- a/readthedocs/projects/version_handling.py +++ b/readthedocs/projects/version_handling.py @@ -16,7 +16,9 @@ def parse_version_failsafe(version_string): """ Parse a version in string form and return Version object. - If there is an error parsing the string, ``None`` is returned. + If there is an error parsing the string + or the version doesn't have a "comparable" version number, + ``None`` is returned. :param version_string: version as string object (e.g. '3.10.1') :type version_string: str or unicode @@ -30,13 +32,21 @@ def parse_version_failsafe(version_string): else: uni_version = version_string + final_form = '' + try: normalized_version = unicodedata.normalize('NFKD', uni_version) ascii_version = normalized_version.encode('ascii', 'ignore') final_form = ascii_version.decode('ascii') return Version(final_form) - except (UnicodeError, InvalidVersion): - return None + except InvalidVersion: + # Handle the special case of 1.x, 2.x or 1.0.x, 1.1.x + if final_form and '.x' in final_form: + return parse_version_failsafe(final_form.replace('.x', '.0')) + except UnicodeError: + pass + + return None def comparable_version(version_string): @@ -69,6 +79,9 @@ def sort_versions(version_list): """ Take a list of Version models and return a sorted list. + This only considers versions with comparable version numbers. + It excludes versions like "latest" and "stable". + :param version_list: list of Version models :type version_list: list(readthedocs.builds.models.Version) diff --git a/readthedocs/rtd_tests/tests/projects/test_version_sorting.py b/readthedocs/rtd_tests/tests/projects/test_version_sorting.py new file mode 100644 index 00000000000..2c8607d2e30 --- /dev/null +++ b/readthedocs/rtd_tests/tests/projects/test_version_sorting.py @@ -0,0 +1,67 @@ +from django.test import TestCase +from django_dynamic_fixture import get + +from readthedocs.builds.constants import BRANCH +from readthedocs.builds.models import Version +from readthedocs.projects.models import Project +from readthedocs.projects.templatetags.projects_tags import sort_version_aware + + +class SortVersionsTest(TestCase): + + def setUp(self): + self.project = get(Project) + + def test_basic_sort(self): + identifiers = ['1.0', '2.0', '1.1', '1.9', '1.10'] + for identifier in identifiers: + get( + Version, + project=self.project, + type=BRANCH, + identifier=identifier, + verbose_name=identifier, + slug=identifier, + ) + + versions = list(Version.objects.filter(project=self.project)) + self.assertEqual( + ['latest', '2.0', '1.10', '1.9', '1.1', '1.0'], + [v.slug for v in sort_version_aware(versions)], + ) + + def test_sort_wildcard(self): + identifiers = ['1.0.x', '2.0.x', '1.1.x', '1.9.x', '1.10.x'] + for identifier in identifiers: + get( + Version, + project=self.project, + type=BRANCH, + identifier=identifier, + verbose_name=identifier, + slug=identifier, + ) + + versions = list(Version.objects.filter(project=self.project)) + self.assertEqual( + ['latest', '2.0.x', '1.10.x', '1.9.x', '1.1.x', '1.0.x'], + [v.slug for v in sort_version_aware(versions)], + ) + + def test_sort_alpha(self): + identifiers = ['banana', 'apple', 'carrot'] + for identifier in identifiers: + get( + Version, + project=self.project, + type=BRANCH, + identifier=identifier, + verbose_name=identifier, + slug=identifier, + ) + + versions = list(Version.objects.filter(project=self.project)) + self.assertEqual( + ['latest', 'carrot', 'banana', 'apple'], + [v.slug for v in sort_version_aware(versions)], + )