Repository: nextcloud/appstore

Issue:
Nightly handling on the release page
Currently the download page only displays stable releases. We need a way to also display nightly releases. Nightly releases are unstable and could break your install, so we need to show them less prominently and (probably) display a warning.



Discussion:

Resolving patch:
diff --git a/nextcloudappstore/core/models.py b/nextcloudappstore/core/models.py
--- a/nextcloudappstore/core/models.py
+++ b/nextcloudappstore/core/models.py
@@ -91,34 +91,77 @@ def can_delete(self, user: User) -> bool:
         return self.owner == user
 
     def releases_by_platform_v(self):
-        """Looks up all compatible releases for each platform version.
-        :return dict with all compatible releases for each platform version.
+        """Looks up all compatible non-nightly releases for each platform
+        version.
+
+        :return dict with all compatible non-nightly releases for each platform
+                version.
         """
-        releases = {}
-        for p_version in settings.PLATFORM_VERSIONS:
-            releases[p_version] = self.compatible_releases(p_version)
-        return releases
+
+        return dict(map(
+            lambda v: (v, self.compatible_releases(v)),
+            settings.PLATFORM_VERSIONS))
+
+    def nightly_releases_by_platform_v(self):
+        """Looks up all compatible nightly releases for each platform version.
+
+        :return dict with all compatible nightly releases for each platform
+                version.
+        """
+
+        return dict(map(
+            lambda v: (v, self.compatible_nightly_releases(v)),
+            settings.PLATFORM_VERSIONS))
 
     def latest_releases_by_platform_v(self):
         """Looks up the latest release for each platform version.
         Ignores nightly releases.
         :return dict with the latest release for each platform version.
         """
+
         all_latest = {}
         for p_version in settings.PLATFORM_VERSIONS:
             compatible = self.compatible_releases(p_version)
-            all_latest[p_version] = self._latest_non_nightly(compatible)
+            all_latest[p_version] = self._latest(compatible)
         return all_latest
 
     def compatible_releases(self, platform_version, inclusive=True):
-        all_releases = self.releases.all()
-        return sorted(list(filter(
-            lambda rel: rel.is_compatible(platform_version, inclusive),
-            all_releases)), key=lambda rel: Version(rel.version), reverse=True)
-
-    def _latest_non_nightly(self, releases):
-        releases = filter(lambda r: not r.version.endswith('-nightly'),
-                          releases)
+        """Returns all non-nightly releases of this app that are compatible
+        with the given platform version.
+
+        :param inclusive: Use inclusive version check (see
+                          AppRelease.is_compatible()).
+        :return a sorted list of all compatible non-nightly releases.
+        """
+
+        return sorted(
+            filter(
+                lambda rel:
+                    rel.is_compatible(platform_version, inclusive)
+                    and not rel.is_nightly,
+                self.releases.all()),
+            key=lambda rel: Version(rel.version),
+            reverse=True)
+
+    def compatible_nightly_releases(self, platform_version, inclusive=True):
+        """Returns all nightly releases of this app that are compatible with
+        the given platform version.
+
+        :param inclusive: Use inclusive version check (see
+                          AppRelease.is_compatible()).
+        :return a sorted list of all compatible nightly releases.
+        """
+
+        return sorted(
+            filter(
+                lambda rel:
+                    rel.is_compatible(platform_version, inclusive)
+                    and rel.is_nightly,
+                self.releases.all()),
+            key=lambda rel: Version(rel.version),
+            reverse=True)
+
+    def _latest(self, releases):
         try:
             return max(releases, key=lambda r: Version(r.version))
         except ValueError:
@@ -180,14 +223,15 @@ def __str__(self) -> str:
         return '%s %s' % (self.app, self.version)
 
     def is_compatible(self, platform_version, inclusive=False):
-        """
-        Checks if a release is compatible with a platform version
+        """Checks if a release is compatible with a platform version
+
         :param platform_version: the platform version, not required to be
-        semver compatible
+                                 semver compatible
         :param inclusive: if True the check will also return True if an app
-         requires 9.0.1 and the given platform version is 9.0
+                          requires 9.0.1 and the given platform version is 9.0
         :return: True if compatible, otherwise false
         """
+
         min_version = Version(pad_min_version(platform_version))
         spec = Spec(self.platform_version_spec)
         if inclusive:
@@ -196,6 +240,10 @@ def is_compatible(self, platform_version, inclusive=False):
         else:
             return min_version in spec
 
+    @property
+    def is_nightly(self):
+        return self.version.endswith('-nightly')
+
 
 class Screenshot(Model):
     url = URLField(max_length=256, verbose_name=_('Image url'))
diff --git a/nextcloudappstore/core/views.py b/nextcloudappstore/core/views.py
--- a/nextcloudappstore/core/views.py
+++ b/nextcloudappstore/core/views.py
@@ -1,4 +1,3 @@
-from functools import reduce
 from urllib.parse import urlencode
 from django.contrib.auth.models import User
 from django.core.exceptions import ObjectDoesNotExist
@@ -10,6 +9,8 @@
 from django.views.generic.detail import DetailView
 from django.views.generic.list import ListView
 from nextcloudappstore.core.models import App, Category
+from nextcloudappstore.core.versioning import pad_min_version
+from semantic_version import Version
 
 
 def app_description(request, id):
@@ -40,10 +41,30 @@ class AppReleasesView(DetailView):
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
         context['categories'] = Category.objects.all()
+
+        releases = self.object.releases_by_platform_v()
+        nightlies = self.object.nightly_releases_by_platform_v()
+        versions = set(list(releases.keys()) + list(nightlies.keys()))
+        all_releases = list(map(
+            lambda v: (v, releases.get(v, []) + nightlies.get(v, [])),
+            versions))
         context['releases_by_platform_v'] = \
-            self.object.releases_by_platform_v()
+            self._sort_by_platform_v(all_releases)
+
         return context
 
+    def _sort_by_platform_v(self, releases_by_platform, reverse=True):
+        """Sorts a list of tuples like (<platform version>, [releases]) by
+        platform version.
+
+        :param releases_by_platform: A list of tuples.
+        :param reverse: Descending order if True, ascending otherwise.
+        :return sorted list of tuples.
+        """
+
+        return sorted(releases_by_platform, reverse=reverse,
+                      key=lambda v: Version(pad_min_version(v[0])))
+
 
 class CategoryAppListView(ListView):
     model = App


Test patch:
diff --git a/nextcloudappstore/core/tests/test_compatibility.py b/nextcloudappstore/core/tests/test_compatibility.py
--- a/nextcloudappstore/core/tests/test_compatibility.py
+++ b/nextcloudappstore/core/tests/test_compatibility.py
@@ -64,7 +64,7 @@ def test_compatible_releases(self):
         app1rel2 = self.app1.compatible_releases('9.0.0')
         app2rel = self.app2.compatible_releases('9.1.0')
         app2rel2 = self.app2.compatible_releases('9.0.0')
-        self.assertEqual(len(app1rel), 3)
+        self.assertEqual(len(app1rel), 2)
         self.assertEqual(len(app1rel2), 1)
         self.assertEqual(len(app2rel), 1)
         self.assertEqual(len(app2rel2), 1)
@@ -86,3 +86,9 @@ def test_correct_comparison_and_ignore_nightly(self):
         with self.settings(PLATFORM_VERSIONS=self.platform_versions):
             app1 = self.app1.latest_releases_by_platform_v()
             self.assertEqual(app1['10.0'].version, '5.0.0')
+
+    def test_nightlies(self):
+        with self.settings(PLATFORM_VERSIONS=self.platform_versions):
+            app1 = self.app1.compatible_nightly_releases('10.0')
+            self.assertEqual(app1[0].version, '6.0.0-nightly')
+            self.assertEqual(len(app1), 1)