Repository: nextcloud/appstore

Issue:
Releases are listed wrong
Releases are listed wrong in my case of the ocr app.

I know the reason for this. I did not understand how the versioning of the app releases should have been done. But now I corrected my mistake. The only thing I can't edit or delete are prioir app releases. So I ask you for it:

Could you please delete all but the last two of the app releases of my ocr app for nextcloud version 10?
Otherwise it could get weird for the users.

nextcloud version 11 is fine by the way.


Discussion:
Can you elaborate? You can use the rest api to delete releases BTW, check the docs
Sorry. I was very tired as i wrote that.. 

The following:
If I upload a new release (1.0.2) which aims only at NC10 it gets displayed 3 times on the download page. I guess it has something to do with the other release (2.0.0) which is only for NC11 and is already available.
You are right, looks like a bug. Issue is probably that there are currently 3 nextcloud releases for 10 which are tried for your release (10.0.0, 10.0.1, 10.0.2). Compatible apps are then collected based on their requirements but there's no reduplication logic afaik

Resolving patch:
diff --git a/nextcloudappstore/core/facades.py b/nextcloudappstore/core/facades.py
--- a/nextcloudappstore/core/facades.py
+++ b/nextcloudappstore/core/facades.py
@@ -57,3 +57,18 @@ def any_match(predicate, iterable) -> bool:
         if predicate(entry):
             return True
     return False
+
+
+def distinct(iterable, criteria=id):
+    """
+    :param iterable:
+    :param criteria: by default the object in the list. Pass a lambda to choose
+    a custom distinctness criteria
+    :return: a distinct iterator of elements from an iterable
+    """
+    occurred_values = set()
+    for element in iterable:
+        value = criteria(element)
+        if value not in occurred_values:
+            occurred_values.add(value)
+            yield element
diff --git a/nextcloudappstore/core/models.py b/nextcloudappstore/core/models.py
--- a/nextcloudappstore/core/models.py
+++ b/nextcloudappstore/core/models.py
@@ -15,6 +15,8 @@
 from parler.models import TranslatedFields, TranslatableModel, \
     TranslatableManager  # type: ignore
 from semantic_version import Version, Spec
+
+from nextcloudappstore.core.facades import distinct
 from nextcloudappstore.core.rating import compute_rating
 from nextcloudappstore.core.versioning import pad_min_version, \
     pad_max_inc_version, AppSemVer, group_by_main_version
@@ -122,7 +124,12 @@ def _get_grouped_releases(self, get_release_func):
         releases = NextcloudRelease.objects.all()
         versions = map(lambda r: r.version, releases)
         compatible_releases = map(lambda v: (v, get_release_func(v)), versions)
-        return group_by_main_version(dict(compatible_releases))
+        grouped_releases = group_by_main_version(dict(compatible_releases))
+        # deduplicate releases
+        result = {}
+        for version, releases in grouped_releases.items():
+            result[version] = list(distinct(releases, lambda r: r.version))
+        return result
 
     def releases_by_platform_v(self):
         """Looks up all compatible stable releases for each platform


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
@@ -17,42 +17,31 @@ def setUp(self):
         self.app1.name = 'News'
         self.app1.description = 'RSS feed reader'
         self.app1.save()
+
+    def tearDown(self):
+        if hasattr(self, 'app1'):
+            self.app1.delete()
+        if hasattr(self, 'app2'):
+            self.app2.delete()
+        self.user.delete()
+
+    def test_compatible_apps_deduplication(self):
         AppRelease.objects.create(app=self.app1, version='1.0.0',
                                   platform_version_spec='>=9.0.0,<10.0.0')
         AppRelease.objects.create(app=self.app1, version='2.0.0',
-                                  platform_version_spec='>=9.0.5,<10.0.5')
+                                  platform_version_spec='>=10.0.0,<11.0.0')
         AppRelease.objects.create(app=self.app1, version='3.0.0',
-                                  platform_version_spec='>=10.0.0,<12.0.0')
-        AppRelease.objects.create(app=self.app1, version='4.0.0',
-                                  platform_version_spec='>=11.0.0')
-        AppRelease.objects.create(app=self.app1, version='5.0.0',
-                                  platform_version_spec='>=12.0.0')
-        AppRelease.objects.create(app=self.app1, version='6.0.0-alpha',
-                                  platform_version_spec='>=12.0.0')
-        AppRelease.objects.create(app=self.app1, version='6.0.0',
-                                  platform_version_spec='>=12.0.0',
-                                  is_nightly=True)
-
-        self.app2 = App.objects.create(pk='notes', owner=self.user)
-        self.app2.set_current_language('en')
-        self.app2.name = 'Notes'
-        self.app2.description = 'Notes application'
-        self.app2.save()
-        AppRelease.objects.create(app=self.app2, version='1.0.0',
-                                  platform_version_spec='>=9.0.0,<10.0.0')
-        AppRelease.objects.create(app=self.app2, version='2.0.0',
                                   platform_version_spec='>=10.0.0,<11.0.0')
-        AppRelease.objects.create(app=self.app2, version='3.0.0',
-                                  platform_version_spec='>=11.0.0,<12.0.0')
-        AppRelease.objects.create(app=self.app2, version='4.0.0',
-                                  platform_version_spec='>=11.0.0,<12.0.0')
-
-    def tearDown(self):
-        self.app1.delete()
-        self.app2.delete()
-        self.user.delete()
+        NextcloudRelease.objects.create(version='9.0.0')
+        NextcloudRelease.objects.create(version='9.0.1')
+        NextcloudRelease.objects.create(version='9.0.2')
+        NextcloudRelease.objects.create(version='10.0.0')
+        releases = self.app1.releases_by_platform_v()
+        self.assertEqual(1, len(releases['9']))
+        self.assertEqual(2, len(releases['10']))
 
     def test_compatible_apps(self):
+        self._create_example_releases()
         apps = App.objects.get_compatible('12.0.0')
         apps2 = App.objects.get_compatible('11.0.0')
         apps3 = App.objects.get_compatible('10.0.0')
@@ -64,6 +53,7 @@ def test_compatible_apps(self):
         self.assertEqual(len(apps4), 2)
 
     def test_compatible_releases(self):
+        self._create_example_releases()
         app1rel = self.app1.compatible_releases('12.0.0')
         app1rel2 = self.app1.compatible_releases('9.0.0')
         app2rel = self.app2.compatible_releases('10.0.0')
@@ -74,6 +64,7 @@ def test_compatible_releases(self):
         self.assertEqual(len(app2rel2), 1)
 
     def test_compatible_unstable_releases(self):
+        self._create_example_releases()
         app1 = self.app1.compatible_unstable_releases('12.0')
         app2 = self.app2.compatible_unstable_releases('12.0')
         self.assertEqual(app1[0].version, '6.0.0')
@@ -83,6 +74,7 @@ def test_compatible_unstable_releases(self):
         self.assertEqual(app2, [])
 
     def test_compatible_releases_by_platform_v(self):
+        self._create_example_releases()
         self._create_nextcloud_versions()
         app1 = self.app1.latest_releases_by_platform_v()
         app2 = self.app2.latest_releases_by_platform_v()
@@ -101,6 +93,7 @@ def test_compatible_releases_by_platform_v(self):
         self.assertEqual(app2['12']['stable'], None)
 
     def test_correct_comparison(self):
+        self._create_example_releases()
         self._create_nextcloud_versions()
         app1 = self.app1.latest_releases_by_platform_v()
         self.assertEqual(app1['10']['stable'].version, '3.0.0')
@@ -109,3 +102,34 @@ def test_correct_comparison(self):
     def _create_nextcloud_versions(self):
         for version in self.platform_versions:
             NextcloudRelease.objects.create(version=version)
+
+    def _create_example_releases(self):
+        AppRelease.objects.create(app=self.app1, version='1.0.0',
+                                  platform_version_spec='>=9.0.0,<10.0.0')
+        AppRelease.objects.create(app=self.app1, version='2.0.0',
+                                  platform_version_spec='>=9.0.5,<10.0.5')
+        AppRelease.objects.create(app=self.app1, version='3.0.0',
+                                  platform_version_spec='>=10.0.0,<12.0.0')
+        AppRelease.objects.create(app=self.app1, version='4.0.0',
+                                  platform_version_spec='>=11.0.0')
+        AppRelease.objects.create(app=self.app1, version='5.0.0',
+                                  platform_version_spec='>=12.0.0')
+        AppRelease.objects.create(app=self.app1, version='6.0.0-alpha',
+                                  platform_version_spec='>=12.0.0')
+        AppRelease.objects.create(app=self.app1, version='6.0.0',
+                                  platform_version_spec='>=12.0.0',
+                                  is_nightly=True)
+
+        self.app2 = App.objects.create(pk='notes', owner=self.user)
+        self.app2.set_current_language('en')
+        self.app2.name = 'Notes'
+        self.app2.description = 'Notes application'
+        self.app2.save()
+        AppRelease.objects.create(app=self.app2, version='1.0.0',
+                                  platform_version_spec='>=9.0.0,<10.0.0')
+        AppRelease.objects.create(app=self.app2, version='2.0.0',
+                                  platform_version_spec='>=10.0.0,<11.0.0')
+        AppRelease.objects.create(app=self.app2, version='3.0.0',
+                                  platform_version_spec='>=11.0.0,<12.0.0')
+        AppRelease.objects.create(app=self.app2, version='4.0.0',
+                                  platform_version_spec='>=11.0.0,<12.0.0')