We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
App details are not updated to the latest upload if only pre-releases or nightlies exist
Hi, 

I've put a new app on the nextcloud app repo, with what seems to be a correct info.xml, but it doesn't take description categories and screenshot into account, 

see there

https://apps.nextcloud.com/apps/carnet

Any ideas why ? Thanks !
App details are not updated to the latest upload if only pre-releases or nightlies exist
Hi, 

I've put a new app on the nextcloud app repo, with what seems to be a correct info.xml, but it doesn't take description categories and screenshot into account, 

see there

https://apps.nextcloud.com/apps/carnet

Any ideas why ? Thanks !

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `nextcloudappstore/api/v1/release/importer.py`
Content:
```
1 from typing import Dict, Any, Set, Tuple  # type: ignore
2 
3 from django.utils import timezone
4 from django.conf import settings  # type: ignore
5 from semantic_version import Version  # type: ignore
6 
7 from nextcloudappstore.core.facades import any_match
8 from nextcloudappstore.core.models import App, Screenshot, Category, \
9     AppRelease, ShellCommand, License, Database, DatabaseDependency, \
10     PhpExtensionDependency, PhpExtension, AppAuthor
11 from nextcloudappstore.core.versioning import to_spec, to_raw_spec
12 
13 
14 def none_to_empty_string(value: str) -> str:
15     if value is None:
16         return ''
17     else:
18         return value.strip()
19 
20 
21 class Importer:
22     def __init__(self, importers: Dict[str, 'Importer'],
23                  ignored_fields: Set[str]) -> None:
24         self.importers = importers
25         self.ignored_fields = ignored_fields
26 
27     def import_data(self, key: str, value: Any, obj: Any) -> None:
28         obj = self._get_object(key, value, obj)
29         value, obj = self._before_import(key, value, obj)
30         for key, val in value.items():
31             if key not in self.ignored_fields:
32                 self.importers[key].import_data(key, val, obj)
33         obj.save()
34 
35     def _get_object(self, key: str, value: Any, obj: Any) -> Any:
36         raise NotImplementedError
37 
38     def _before_import(self, key: str, value: Any, obj: Any) -> Tuple[Any,
39                                                                       Any]:
40         raise NotImplementedError
41 
42 
43 class ScalarImporter(Importer):
44     def __init__(self) -> None:
45         super().__init__({}, set())
46 
47 
48 class PhpExtensionImporter(ScalarImporter):
49     def import_data(self, key: str, value: Any, obj: Any) -> None:
50         for ext in value:
51             version_spec = to_spec(ext['php_extension']['min_version'],
52                                    ext['php_extension']['max_version'])
53             raw_version_spec = to_raw_spec(
54                 ext['php_extension']['raw_min_version'],
55                 ext['php_extension']['raw_max_version'])
56             extension, created = PhpExtension.objects.get_or_create(
57                 id=ext['php_extension']['id'])
58             PhpExtensionDependency.objects.create(
59                 version_spec=version_spec,
60                 raw_version_spec=raw_version_spec,
61                 app_release=obj, php_extension=extension,
62             )
63 
64 
65 class DatabaseImporter(ScalarImporter):
66     def import_data(self, key: str, value: Any, obj: Any) -> None:
67         for db in value:
68             version_spec = to_spec(db['database']['min_version'],
69                                    db['database']['max_version'])
70             raw_version_spec = to_raw_spec(db['database']['raw_min_version'],
71                                            db['database']['raw_max_version'])
72             # all dbs should be known already
73             database = Database.objects.get(id=db['database']['id'])
74             DatabaseDependency.objects.create(
75                 version_spec=version_spec,
76                 raw_version_spec=raw_version_spec,
77                 app_release=obj, database=database,
78             )
79 
80 
81 class LicenseImporter(ScalarImporter):
82     def import_data(self, key: str, value: Any, obj: Any) -> None:
83         def map_models(data: Dict) -> License:
84             id = data['license']['id']
85             model, created = License.objects.get_or_create(id=id)
86             return model
87 
88         obj.licenses.set(list(map(map_models, value)))
89 
90 
91 class ShellCommandImporter(ScalarImporter):
92     def import_data(self, key: str, value: Any, obj: Any) -> None:
93         def map_commands(data: Dict) -> ShellCommand:
94             name = data['shell_command']['name']
95             command, created = ShellCommand.objects.get_or_create(name=name)
96             return command
97 
98         obj.shell_commands.set(list(map(map_commands, value)))
99 
100 
101 class AuthorImporter(ScalarImporter):
102     def import_data(self, key: str, value: Any, obj: Any) -> None:
103         def map_authors(data: Dict) -> AppAuthor:
104             author = data['author']
105             return AppAuthor.objects.create(
106                 name=author['name'],
107                 mail=none_to_empty_string(author['mail']),
108                 homepage=none_to_empty_string(author['homepage'])
109             )
110 
111         obj.authors.set(list(map(map_authors, value)))
112 
113 
114 class DefaultAttributeImporter(ScalarImporter):
115     def import_data(self, key: str, value: Any, obj: Any) -> None:
116         setattr(obj, key, value)
117 
118 
119 class StringAttributeImporter(ScalarImporter):
120     def import_data(self, key: str, value: Any, obj: Any) -> None:
121         setattr(obj, key, none_to_empty_string(value))
122 
123 
124 class ScreenshotsImporter(ScalarImporter):
125     def import_data(self, key: str, value: Any, obj: Any) -> None:
126         def create_screenshot(img: Dict[str, str]) -> Screenshot:
127             return Screenshot.objects.create(
128                 url=img['url'], app=obj, ordering=img['ordering'],
129                 small_thumbnail=none_to_empty_string(img['small_thumbnail']))
130 
131         shots = map(lambda val: create_screenshot(val['screenshot']), value)
132         obj.screenshots.set(list(shots))
133 
134 
135 class CategoryImporter(ScalarImporter):
136     def import_data(self, key: str, value: Any, obj: Any) -> None:
137         def map_categories(cat: Dict) -> Category:
138             id = cat['category']['id']
139             category, created = Category.objects.get_or_create(id=id)
140             return category
141 
142         obj.categories.set(list(map(map_categories, value)))
143 
144 
145 class L10NImporter(ScalarImporter):
146     def import_data(self, key: str, value: Any, obj: Any) -> None:
147         for lang, translation in value.items():
148             obj.set_current_language(lang)
149             setattr(obj, key, translation.strip())
150             obj.save()
151 
152 
153 class AppReleaseImporter(Importer):
154     def __init__(self, php_extension_importer: PhpExtensionImporter,
155                  database_importer: DatabaseImporter,
156                  license_importer: LicenseImporter,
157                  shell_command_importer: ShellCommandImporter,
158                  string_attribute_importer: StringAttributeImporter,
159                  default_attribute_importer: DefaultAttributeImporter,
160                  l10n_importer: L10NImporter) -> None:
161         super().__init__({
162             'php_extensions': php_extension_importer,
163             'databases': database_importer,
164             'licenses': license_importer,
165             'php_version_spec': string_attribute_importer,
166             'platform_version_spec': string_attribute_importer,
167             'raw_php_version_spec': string_attribute_importer,
168             'raw_platform_version_spec': string_attribute_importer,
169             'min_int_size': default_attribute_importer,
170             'shell_commands': shell_command_importer,
171             'signature': string_attribute_importer,
172             'download': string_attribute_importer,
173             'changelog': l10n_importer,
174             'is_nightly': default_attribute_importer,
175         }, {
176             'version',
177             'raw_version',
178             'php_min_version',
179             'php_max_version',
180             'raw_php_min_version',
181             'raw_php_max_version',
182             'platform_min_version',
183             'platform_max_version',
184             'raw_platform_min_version',
185             'raw_platform_max_version',
186         })
187 
188     def _before_import(self, key: str, value: Any, obj: Any) -> Tuple[Any,
189                                                                       Any]:
190         # combine versions into specs
191         value['platform_version_spec'] = to_spec(
192             value['platform_min_version'], value['platform_max_version'])
193         value['php_version_spec'] = to_spec(value['php_min_version'],
194                                             value['php_max_version'])
195         value['raw_platform_version_spec'] = to_raw_spec(
196             value['raw_platform_min_version'],
197             value['raw_platform_max_version'])
198         value['raw_php_version_spec'] = to_raw_spec(
199             value['raw_php_min_version'],
200             value['raw_php_max_version'])
201         obj.licenses.clear()
202         obj.shell_commands.clear()
203         obj.licenses.clear()
204         obj.php_extensions.clear()
205         obj.databases.clear()
206         obj.signature_digest = settings.CERTIFICATE_DIGEST
207         return value, obj
208 
209     def _get_object(self, key: str, value: Any, obj: Any) -> Any:
210         release, created = AppRelease.objects.get_or_create(
211             version=value['version'], app=obj, is_nightly=value['is_nightly']
212         )
213         return release
214 
215 
216 class AppImporter(Importer):
217     def __init__(self, release_importer: AppReleaseImporter,
218                  screenshots_importer: ScreenshotsImporter,
219                  attribute_importer: StringAttributeImporter,
220                  l10n_importer: L10NImporter,
221                  category_importer: CategoryImporter,
222                  author_importer: AuthorImporter,
223                  default_attribute_importer: DefaultAttributeImporter) -> None:
224         super().__init__({
225             'release': release_importer,
226             'screenshots': screenshots_importer,
227             'user_docs': attribute_importer,
228             'admin_docs': attribute_importer,
229             'website': attribute_importer,
230             'discussion': attribute_importer,
231             'developer_docs': attribute_importer,
232             'issue_tracker': attribute_importer,
233             'certificate': attribute_importer,
234             'name': l10n_importer,
235             'summary': l10n_importer,
236             'description': l10n_importer,
237             'categories': category_importer,
238             'authors': author_importer
239         }, {'id'})
240 
241     def _get_object(self, key: str, value: Any, obj: Any) -> Any:
242         # only update app if newest or equal to newest release
243         app, created = App.objects.get_or_create(pk=value['id'])
244         return app
245 
246     def _before_import(self, key: str, value: Any, obj: Any) -> Tuple[Any,
247                                                                       Any]:
248         obj.last_release = timezone.now()
249 
250         if 'is_nightly' not in value['release']:
251             value['release']['is_nightly'] = False
252         if value['release']['is_nightly']:
253             AppRelease.objects.filter(app__id=obj.id, is_nightly=True).delete()
254 
255         # only new releases update an app's data
256         if self._should_update_everything(value):
257             # clear all relations
258             obj.screenshots.all().delete()
259             obj.authors.all().delete()
260             obj.categories.clear()
261             for translation in obj.translations.all():
262                 translation.delete()
263         else:
264             value = {'id': value['id'], 'release': value['release']}
265 
266         return value, obj
267 
268     def _should_update_everything(self, value: Any) -> bool:
269         releases = AppRelease.objects.filter(app__id=value['id'])
270 
271         # if its the first release it should always set the required initial
272         # data
273         if len(releases) == 0:
274             return True
275 
276         current_version = value['release']['version']
277 
278         # we do not care about nightlies here so it's fine to just use a
279         # normal semver
280         uploaded_version = Version(current_version)
281         is_prerelease = '-' in current_version
282         is_nightly = value['release']['is_nightly']
283         is_stable = not is_prerelease and not is_nightly
284 
285         def is_newer_version(release: Any) -> bool:
286             return uploaded_version >= Version(release.version)
287 
288         # the main page should only be updated when stable and new releases
289         # are uploaded
290         is_latest_version = any_match(is_newer_version, releases)
291         return is_latest_version and is_stable
292
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/nextcloudappstore/api/v1/release/importer.py b/nextcloudappstore/api/v1/release/importer.py
--- a/nextcloudappstore/api/v1/release/importer.py
+++ b/nextcloudappstore/api/v1/release/importer.py
@@ -273,6 +273,16 @@
         if len(releases) == 0:
             return True
 
+        # if the app has no stable releases update everything
+        has_stable_release = False
+        for release in releases:
+            if '-' not in release.version and not release.is_nightly:
+                has_stable_release = True
+                break
+
+        if not has_stable_release:
+            return True
+
         current_version = value['release']['version']
 
         # we do not care about nightlies here so it's fine to just use a
@@ -282,10 +292,14 @@
         is_nightly = value['release']['is_nightly']
         is_stable = not is_prerelease and not is_nightly
 
+        # let's go out quickly
+        if not is_stable:
+            return False
+
         def is_newer_version(release: Any) -> bool:
             return uploaded_version >= Version(release.version)
 
         # the main page should only be updated when stable and new releases
         # are uploaded
         is_latest_version = any_match(is_newer_version, releases)
-        return is_latest_version and is_stable
+        return is_latest_version

{"golden_diff": "diff --git a/nextcloudappstore/api/v1/release/importer.py b/nextcloudappstore/api/v1/release/importer.py\n--- a/nextcloudappstore/api/v1/release/importer.py\n+++ b/nextcloudappstore/api/v1/release/importer.py\n@@ -273,6 +273,16 @@\n         if len(releases) == 0:\n             return True\n \n+        # if the app has no stable releases update everything\n+        has_stable_release = False\n+        for release in releases:\n+            if '-' not in release.version and not release.is_nightly:\n+                has_stable_release = True\n+                break\n+\n+        if not has_stable_release:\n+            return True\n+\n         current_version = value['release']['version']\n \n         # we do not care about nightlies here so it's fine to just use a\n@@ -282,10 +292,14 @@\n         is_nightly = value['release']['is_nightly']\n         is_stable = not is_prerelease and not is_nightly\n \n+        # let's go out quickly\n+        if not is_stable:\n+            return False\n+\n         def is_newer_version(release: Any) -> bool:\n             return uploaded_version >= Version(release.version)\n \n         # the main page should only be updated when stable and new releases\n         # are uploaded\n         is_latest_version = any_match(is_newer_version, releases)\n-        return is_latest_version and is_stable\n+        return is_latest_version\n", "issue": "App details are not updated to the latest upload if only pre-releases or nightlies exist\nHi, \r\n\r\nI've put a new app on the nextcloud app repo, with what seems to be a correct info.xml, but it doesn't take description categories and screenshot into account, \r\n\r\nsee there\r\n\r\nhttps://apps.nextcloud.com/apps/carnet\r\n\r\nAny ideas why ? Thanks !\nApp details are not updated to the latest upload if only pre-releases or nightlies exist\nHi, \r\n\r\nI've put a new app on the nextcloud app repo, with what seems to be a correct info.xml, but it doesn't take description categories and screenshot into account, \r\n\r\nsee there\r\n\r\nhttps://apps.nextcloud.com/apps/carnet\r\n\r\nAny ideas why ? Thanks !\n", "before_files": [{"content": "from typing import Dict, Any, Set, Tuple  # type: ignore\n\nfrom django.utils import timezone\nfrom django.conf import settings  # type: ignore\nfrom semantic_version import Version  # type: ignore\n\nfrom nextcloudappstore.core.facades import any_match\nfrom nextcloudappstore.core.models import App, Screenshot, Category, \\\n    AppRelease, ShellCommand, License, Database, DatabaseDependency, \\\n    PhpExtensionDependency, PhpExtension, AppAuthor\nfrom nextcloudappstore.core.versioning import to_spec, to_raw_spec\n\n\ndef none_to_empty_string(value: str) -> str:\n    if value is None:\n        return ''\n    else:\n        return value.strip()\n\n\nclass Importer:\n    def __init__(self, importers: Dict[str, 'Importer'],\n                 ignored_fields: Set[str]) -> None:\n        self.importers = importers\n        self.ignored_fields = ignored_fields\n\n    def import_data(self, key: str, value: Any, obj: Any) -> None:\n        obj = self._get_object(key, value, obj)\n        value, obj = self._before_import(key, value, obj)\n        for key, val in value.items():\n            if key not in self.ignored_fields:\n                self.importers[key].import_data(key, val, obj)\n        obj.save()\n\n    def _get_object(self, key: str, value: Any, obj: Any) -> Any:\n        raise NotImplementedError\n\n    def _before_import(self, key: str, value: Any, obj: Any) -> Tuple[Any,\n                                                                      Any]:\n        raise NotImplementedError\n\n\nclass ScalarImporter(Importer):\n    def __init__(self) -> None:\n        super().__init__({}, set())\n\n\nclass PhpExtensionImporter(ScalarImporter):\n    def import_data(self, key: str, value: Any, obj: Any) -> None:\n        for ext in value:\n            version_spec = to_spec(ext['php_extension']['min_version'],\n                                   ext['php_extension']['max_version'])\n            raw_version_spec = to_raw_spec(\n                ext['php_extension']['raw_min_version'],\n                ext['php_extension']['raw_max_version'])\n            extension, created = PhpExtension.objects.get_or_create(\n                id=ext['php_extension']['id'])\n            PhpExtensionDependency.objects.create(\n                version_spec=version_spec,\n                raw_version_spec=raw_version_spec,\n                app_release=obj, php_extension=extension,\n            )\n\n\nclass DatabaseImporter(ScalarImporter):\n    def import_data(self, key: str, value: Any, obj: Any) -> None:\n        for db in value:\n            version_spec = to_spec(db['database']['min_version'],\n                                   db['database']['max_version'])\n            raw_version_spec = to_raw_spec(db['database']['raw_min_version'],\n                                           db['database']['raw_max_version'])\n            # all dbs should be known already\n            database = Database.objects.get(id=db['database']['id'])\n            DatabaseDependency.objects.create(\n                version_spec=version_spec,\n                raw_version_spec=raw_version_spec,\n                app_release=obj, database=database,\n            )\n\n\nclass LicenseImporter(ScalarImporter):\n    def import_data(self, key: str, value: Any, obj: Any) -> None:\n        def map_models(data: Dict) -> License:\n            id = data['license']['id']\n            model, created = License.objects.get_or_create(id=id)\n            return model\n\n        obj.licenses.set(list(map(map_models, value)))\n\n\nclass ShellCommandImporter(ScalarImporter):\n    def import_data(self, key: str, value: Any, obj: Any) -> None:\n        def map_commands(data: Dict) -> ShellCommand:\n            name = data['shell_command']['name']\n            command, created = ShellCommand.objects.get_or_create(name=name)\n            return command\n\n        obj.shell_commands.set(list(map(map_commands, value)))\n\n\nclass AuthorImporter(ScalarImporter):\n    def import_data(self, key: str, value: Any, obj: Any) -> None:\n        def map_authors(data: Dict) -> AppAuthor:\n            author = data['author']\n            return AppAuthor.objects.create(\n                name=author['name'],\n                mail=none_to_empty_string(author['mail']),\n                homepage=none_to_empty_string(author['homepage'])\n            )\n\n        obj.authors.set(list(map(map_authors, value)))\n\n\nclass DefaultAttributeImporter(ScalarImporter):\n    def import_data(self, key: str, value: Any, obj: Any) -> None:\n        setattr(obj, key, value)\n\n\nclass StringAttributeImporter(ScalarImporter):\n    def import_data(self, key: str, value: Any, obj: Any) -> None:\n        setattr(obj, key, none_to_empty_string(value))\n\n\nclass ScreenshotsImporter(ScalarImporter):\n    def import_data(self, key: str, value: Any, obj: Any) -> None:\n        def create_screenshot(img: Dict[str, str]) -> Screenshot:\n            return Screenshot.objects.create(\n                url=img['url'], app=obj, ordering=img['ordering'],\n                small_thumbnail=none_to_empty_string(img['small_thumbnail']))\n\n        shots = map(lambda val: create_screenshot(val['screenshot']), value)\n        obj.screenshots.set(list(shots))\n\n\nclass CategoryImporter(ScalarImporter):\n    def import_data(self, key: str, value: Any, obj: Any) -> None:\n        def map_categories(cat: Dict) -> Category:\n            id = cat['category']['id']\n            category, created = Category.objects.get_or_create(id=id)\n            return category\n\n        obj.categories.set(list(map(map_categories, value)))\n\n\nclass L10NImporter(ScalarImporter):\n    def import_data(self, key: str, value: Any, obj: Any) -> None:\n        for lang, translation in value.items():\n            obj.set_current_language(lang)\n            setattr(obj, key, translation.strip())\n            obj.save()\n\n\nclass AppReleaseImporter(Importer):\n    def __init__(self, php_extension_importer: PhpExtensionImporter,\n                 database_importer: DatabaseImporter,\n                 license_importer: LicenseImporter,\n                 shell_command_importer: ShellCommandImporter,\n                 string_attribute_importer: StringAttributeImporter,\n                 default_attribute_importer: DefaultAttributeImporter,\n                 l10n_importer: L10NImporter) -> None:\n        super().__init__({\n            'php_extensions': php_extension_importer,\n            'databases': database_importer,\n            'licenses': license_importer,\n            'php_version_spec': string_attribute_importer,\n            'platform_version_spec': string_attribute_importer,\n            'raw_php_version_spec': string_attribute_importer,\n            'raw_platform_version_spec': string_attribute_importer,\n            'min_int_size': default_attribute_importer,\n            'shell_commands': shell_command_importer,\n            'signature': string_attribute_importer,\n            'download': string_attribute_importer,\n            'changelog': l10n_importer,\n            'is_nightly': default_attribute_importer,\n        }, {\n            'version',\n            'raw_version',\n            'php_min_version',\n            'php_max_version',\n            'raw_php_min_version',\n            'raw_php_max_version',\n            'platform_min_version',\n            'platform_max_version',\n            'raw_platform_min_version',\n            'raw_platform_max_version',\n        })\n\n    def _before_import(self, key: str, value: Any, obj: Any) -> Tuple[Any,\n                                                                      Any]:\n        # combine versions into specs\n        value['platform_version_spec'] = to_spec(\n            value['platform_min_version'], value['platform_max_version'])\n        value['php_version_spec'] = to_spec(value['php_min_version'],\n                                            value['php_max_version'])\n        value['raw_platform_version_spec'] = to_raw_spec(\n            value['raw_platform_min_version'],\n            value['raw_platform_max_version'])\n        value['raw_php_version_spec'] = to_raw_spec(\n            value['raw_php_min_version'],\n            value['raw_php_max_version'])\n        obj.licenses.clear()\n        obj.shell_commands.clear()\n        obj.licenses.clear()\n        obj.php_extensions.clear()\n        obj.databases.clear()\n        obj.signature_digest = settings.CERTIFICATE_DIGEST\n        return value, obj\n\n    def _get_object(self, key: str, value: Any, obj: Any) -> Any:\n        release, created = AppRelease.objects.get_or_create(\n            version=value['version'], app=obj, is_nightly=value['is_nightly']\n        )\n        return release\n\n\nclass AppImporter(Importer):\n    def __init__(self, release_importer: AppReleaseImporter,\n                 screenshots_importer: ScreenshotsImporter,\n                 attribute_importer: StringAttributeImporter,\n                 l10n_importer: L10NImporter,\n                 category_importer: CategoryImporter,\n                 author_importer: AuthorImporter,\n                 default_attribute_importer: DefaultAttributeImporter) -> None:\n        super().__init__({\n            'release': release_importer,\n            'screenshots': screenshots_importer,\n            'user_docs': attribute_importer,\n            'admin_docs': attribute_importer,\n            'website': attribute_importer,\n            'discussion': attribute_importer,\n            'developer_docs': attribute_importer,\n            'issue_tracker': attribute_importer,\n            'certificate': attribute_importer,\n            'name': l10n_importer,\n            'summary': l10n_importer,\n            'description': l10n_importer,\n            'categories': category_importer,\n            'authors': author_importer\n        }, {'id'})\n\n    def _get_object(self, key: str, value: Any, obj: Any) -> Any:\n        # only update app if newest or equal to newest release\n        app, created = App.objects.get_or_create(pk=value['id'])\n        return app\n\n    def _before_import(self, key: str, value: Any, obj: Any) -> Tuple[Any,\n                                                                      Any]:\n        obj.last_release = timezone.now()\n\n        if 'is_nightly' not in value['release']:\n            value['release']['is_nightly'] = False\n        if value['release']['is_nightly']:\n            AppRelease.objects.filter(app__id=obj.id, is_nightly=True).delete()\n\n        # only new releases update an app's data\n        if self._should_update_everything(value):\n            # clear all relations\n            obj.screenshots.all().delete()\n            obj.authors.all().delete()\n            obj.categories.clear()\n            for translation in obj.translations.all():\n                translation.delete()\n        else:\n            value = {'id': value['id'], 'release': value['release']}\n\n        return value, obj\n\n    def _should_update_everything(self, value: Any) -> bool:\n        releases = AppRelease.objects.filter(app__id=value['id'])\n\n        # if its the first release it should always set the required initial\n        # data\n        if len(releases) == 0:\n            return True\n\n        current_version = value['release']['version']\n\n        # we do not care about nightlies here so it's fine to just use a\n        # normal semver\n        uploaded_version = Version(current_version)\n        is_prerelease = '-' in current_version\n        is_nightly = value['release']['is_nightly']\n        is_stable = not is_prerelease and not is_nightly\n\n        def is_newer_version(release: Any) -> bool:\n            return uploaded_version >= Version(release.version)\n\n        # the main page should only be updated when stable and new releases\n        # are uploaded\n        is_latest_version = any_match(is_newer_version, releases)\n        return is_latest_version and is_stable\n", "path": "nextcloudappstore/api/v1/release/importer.py"}], "after_files": [{"content": "from typing import Dict, Any, Set, Tuple  # type: ignore\n\nfrom django.utils import timezone\nfrom django.conf import settings  # type: ignore\nfrom semantic_version import Version  # type: ignore\n\nfrom nextcloudappstore.core.facades import any_match\nfrom nextcloudappstore.core.models import App, Screenshot, Category, \\\n    AppRelease, ShellCommand, License, Database, DatabaseDependency, \\\n    PhpExtensionDependency, PhpExtension, AppAuthor\nfrom nextcloudappstore.core.versioning import to_spec, to_raw_spec\n\n\ndef none_to_empty_string(value: str) -> str:\n    if value is None:\n        return ''\n    else:\n        return value.strip()\n\n\nclass Importer:\n    def __init__(self, importers: Dict[str, 'Importer'],\n                 ignored_fields: Set[str]) -> None:\n        self.importers = importers\n        self.ignored_fields = ignored_fields\n\n    def import_data(self, key: str, value: Any, obj: Any) -> None:\n        obj = self._get_object(key, value, obj)\n        value, obj = self._before_import(key, value, obj)\n        for key, val in value.items():\n            if key not in self.ignored_fields:\n                self.importers[key].import_data(key, val, obj)\n        obj.save()\n\n    def _get_object(self, key: str, value: Any, obj: Any) -> Any:\n        raise NotImplementedError\n\n    def _before_import(self, key: str, value: Any, obj: Any) -> Tuple[Any,\n                                                                      Any]:\n        raise NotImplementedError\n\n\nclass ScalarImporter(Importer):\n    def __init__(self) -> None:\n        super().__init__({}, set())\n\n\nclass PhpExtensionImporter(ScalarImporter):\n    def import_data(self, key: str, value: Any, obj: Any) -> None:\n        for ext in value:\n            version_spec = to_spec(ext['php_extension']['min_version'],\n                                   ext['php_extension']['max_version'])\n            raw_version_spec = to_raw_spec(\n                ext['php_extension']['raw_min_version'],\n                ext['php_extension']['raw_max_version'])\n            extension, created = PhpExtension.objects.get_or_create(\n                id=ext['php_extension']['id'])\n            PhpExtensionDependency.objects.create(\n                version_spec=version_spec,\n                raw_version_spec=raw_version_spec,\n                app_release=obj, php_extension=extension,\n            )\n\n\nclass DatabaseImporter(ScalarImporter):\n    def import_data(self, key: str, value: Any, obj: Any) -> None:\n        for db in value:\n            version_spec = to_spec(db['database']['min_version'],\n                                   db['database']['max_version'])\n            raw_version_spec = to_raw_spec(db['database']['raw_min_version'],\n                                           db['database']['raw_max_version'])\n            # all dbs should be known already\n            database = Database.objects.get(id=db['database']['id'])\n            DatabaseDependency.objects.create(\n                version_spec=version_spec,\n                raw_version_spec=raw_version_spec,\n                app_release=obj, database=database,\n            )\n\n\nclass LicenseImporter(ScalarImporter):\n    def import_data(self, key: str, value: Any, obj: Any) -> None:\n        def map_models(data: Dict) -> License:\n            id = data['license']['id']\n            model, created = License.objects.get_or_create(id=id)\n            return model\n\n        obj.licenses.set(list(map(map_models, value)))\n\n\nclass ShellCommandImporter(ScalarImporter):\n    def import_data(self, key: str, value: Any, obj: Any) -> None:\n        def map_commands(data: Dict) -> ShellCommand:\n            name = data['shell_command']['name']\n            command, created = ShellCommand.objects.get_or_create(name=name)\n            return command\n\n        obj.shell_commands.set(list(map(map_commands, value)))\n\n\nclass AuthorImporter(ScalarImporter):\n    def import_data(self, key: str, value: Any, obj: Any) -> None:\n        def map_authors(data: Dict) -> AppAuthor:\n            author = data['author']\n            return AppAuthor.objects.create(\n                name=author['name'],\n                mail=none_to_empty_string(author['mail']),\n                homepage=none_to_empty_string(author['homepage'])\n            )\n\n        obj.authors.set(list(map(map_authors, value)))\n\n\nclass DefaultAttributeImporter(ScalarImporter):\n    def import_data(self, key: str, value: Any, obj: Any) -> None:\n        setattr(obj, key, value)\n\n\nclass StringAttributeImporter(ScalarImporter):\n    def import_data(self, key: str, value: Any, obj: Any) -> None:\n        setattr(obj, key, none_to_empty_string(value))\n\n\nclass ScreenshotsImporter(ScalarImporter):\n    def import_data(self, key: str, value: Any, obj: Any) -> None:\n        def create_screenshot(img: Dict[str, str]) -> Screenshot:\n            return Screenshot.objects.create(\n                url=img['url'], app=obj, ordering=img['ordering'],\n                small_thumbnail=none_to_empty_string(img['small_thumbnail']))\n\n        shots = map(lambda val: create_screenshot(val['screenshot']), value)\n        obj.screenshots.set(list(shots))\n\n\nclass CategoryImporter(ScalarImporter):\n    def import_data(self, key: str, value: Any, obj: Any) -> None:\n        def map_categories(cat: Dict) -> Category:\n            id = cat['category']['id']\n            category, created = Category.objects.get_or_create(id=id)\n            return category\n\n        obj.categories.set(list(map(map_categories, value)))\n\n\nclass L10NImporter(ScalarImporter):\n    def import_data(self, key: str, value: Any, obj: Any) -> None:\n        for lang, translation in value.items():\n            obj.set_current_language(lang)\n            setattr(obj, key, translation.strip())\n            obj.save()\n\n\nclass AppReleaseImporter(Importer):\n    def __init__(self, php_extension_importer: PhpExtensionImporter,\n                 database_importer: DatabaseImporter,\n                 license_importer: LicenseImporter,\n                 shell_command_importer: ShellCommandImporter,\n                 string_attribute_importer: StringAttributeImporter,\n                 default_attribute_importer: DefaultAttributeImporter,\n                 l10n_importer: L10NImporter) -> None:\n        super().__init__({\n            'php_extensions': php_extension_importer,\n            'databases': database_importer,\n            'licenses': license_importer,\n            'php_version_spec': string_attribute_importer,\n            'platform_version_spec': string_attribute_importer,\n            'raw_php_version_spec': string_attribute_importer,\n            'raw_platform_version_spec': string_attribute_importer,\n            'min_int_size': default_attribute_importer,\n            'shell_commands': shell_command_importer,\n            'signature': string_attribute_importer,\n            'download': string_attribute_importer,\n            'changelog': l10n_importer,\n            'is_nightly': default_attribute_importer,\n        }, {\n            'version',\n            'raw_version',\n            'php_min_version',\n            'php_max_version',\n            'raw_php_min_version',\n            'raw_php_max_version',\n            'platform_min_version',\n            'platform_max_version',\n            'raw_platform_min_version',\n            'raw_platform_max_version',\n        })\n\n    def _before_import(self, key: str, value: Any, obj: Any) -> Tuple[Any,\n                                                                      Any]:\n        # combine versions into specs\n        value['platform_version_spec'] = to_spec(\n            value['platform_min_version'], value['platform_max_version'])\n        value['php_version_spec'] = to_spec(value['php_min_version'],\n                                            value['php_max_version'])\n        value['raw_platform_version_spec'] = to_raw_spec(\n            value['raw_platform_min_version'],\n            value['raw_platform_max_version'])\n        value['raw_php_version_spec'] = to_raw_spec(\n            value['raw_php_min_version'],\n            value['raw_php_max_version'])\n        obj.licenses.clear()\n        obj.shell_commands.clear()\n        obj.licenses.clear()\n        obj.php_extensions.clear()\n        obj.databases.clear()\n        obj.signature_digest = settings.CERTIFICATE_DIGEST\n        return value, obj\n\n    def _get_object(self, key: str, value: Any, obj: Any) -> Any:\n        release, created = AppRelease.objects.get_or_create(\n            version=value['version'], app=obj, is_nightly=value['is_nightly']\n        )\n        return release\n\n\nclass AppImporter(Importer):\n    def __init__(self, release_importer: AppReleaseImporter,\n                 screenshots_importer: ScreenshotsImporter,\n                 attribute_importer: StringAttributeImporter,\n                 l10n_importer: L10NImporter,\n                 category_importer: CategoryImporter,\n                 author_importer: AuthorImporter,\n                 default_attribute_importer: DefaultAttributeImporter) -> None:\n        super().__init__({\n            'release': release_importer,\n            'screenshots': screenshots_importer,\n            'user_docs': attribute_importer,\n            'admin_docs': attribute_importer,\n            'website': attribute_importer,\n            'discussion': attribute_importer,\n            'developer_docs': attribute_importer,\n            'issue_tracker': attribute_importer,\n            'certificate': attribute_importer,\n            'name': l10n_importer,\n            'summary': l10n_importer,\n            'description': l10n_importer,\n            'categories': category_importer,\n            'authors': author_importer\n        }, {'id'})\n\n    def _get_object(self, key: str, value: Any, obj: Any) -> Any:\n        # only update app if newest or equal to newest release\n        app, created = App.objects.get_or_create(pk=value['id'])\n        return app\n\n    def _before_import(self, key: str, value: Any, obj: Any) -> Tuple[Any,\n                                                                      Any]:\n        obj.last_release = timezone.now()\n\n        if 'is_nightly' not in value['release']:\n            value['release']['is_nightly'] = False\n        if value['release']['is_nightly']:\n            AppRelease.objects.filter(app__id=obj.id, is_nightly=True).delete()\n\n        # only new releases update an app's data\n        if self._should_update_everything(value):\n            # clear all relations\n            obj.screenshots.all().delete()\n            obj.authors.all().delete()\n            obj.categories.clear()\n            for translation in obj.translations.all():\n                translation.delete()\n        else:\n            value = {'id': value['id'], 'release': value['release']}\n\n        return value, obj\n\n    def _should_update_everything(self, value: Any) -> bool:\n        releases = AppRelease.objects.filter(app__id=value['id'])\n\n        # if its the first release it should always set the required initial\n        # data\n        if len(releases) == 0:\n            return True\n\n        # if the app has no stable releases update everything\n        has_stable_release = False\n        for release in releases:\n            if '-' not in release.version and not release.is_nightly:\n                has_stable_release = True\n                break\n\n        if not has_stable_release:\n            return True\n\n        current_version = value['release']['version']\n\n        # we do not care about nightlies here so it's fine to just use a\n        # normal semver\n        uploaded_version = Version(current_version)\n        is_prerelease = '-' in current_version\n        is_nightly = value['release']['is_nightly']\n        is_stable = not is_prerelease and not is_nightly\n\n        # let's go out quickly\n        if not is_stable:\n            return False\n\n        def is_newer_version(release: Any) -> bool:\n            return uploaded_version >= Version(release.version)\n\n        # the main page should only be updated when stable and new releases\n        # are uploaded\n        is_latest_version = any_match(is_newer_version, releases)\n        return is_latest_version\n", "path": "nextcloudappstore/api/v1/release/importer.py"}]}