From b5e81ae8991fdf47ebef831e3f5f28b34224451c Mon Sep 17 00:00:00 2001 From: Bernhard Posselt Date: Wed, 5 Oct 2016 17:49:40 +0200 Subject: [PATCH] Make changelogs translatable (#333) * merge migrations * revert ocs change * add migrations * initialize english changelogs --- docs/developer.rst | 2 + docs/restapi.rst | 6 ++- nextcloudappstore/core/admin.py | 2 +- nextcloudappstore/core/api/v0/views.py | 2 +- .../core/api/v1/release/__init__.py | 1 + .../core/api/v1/release/importer.py | 5 +- .../core/api/v1/release/parser.py | 18 +++++-- .../core/api/v1/release/provider.py | 8 +-- nextcloudappstore/core/api/v1/serializers.py | 3 +- .../core/api/v1/tests/test_parser.py | 4 +- .../migrations/0007_auto_20161005_1424.py | 52 +++++++++++++++++++ nextcloudappstore/core/models.py | 9 ++-- .../core/templates/api/v0/apps.xml | 2 +- .../core/templates/api/v0/categories.xml | 1 + 14 files changed, 97 insertions(+), 18 deletions(-) create mode 100644 nextcloudappstore/core/migrations/0007_auto_20161005_1424.py diff --git a/docs/developer.rst b/docs/developer.rst index 61b16d37e8..dbf97cc25e 100644 --- a/docs/developer.rst +++ b/docs/developer.rst @@ -481,6 +481,8 @@ The version has to be equal to the version in your info.xml. If the parser can't The changelog for nightlies will be taken from the **## [Unreleased]** block +Changelogs can be translated as well. To add a changelog for a specific translation, use **CHANGELOG.code.md**, e.g.: **CHANGELOG.fr.md** + .. _info-schema: Schema Integration diff --git a/docs/restapi.rst b/docs/restapi.rst index 3e3860d321..03f0e1f76b 100644 --- a/docs/restapi.rst +++ b/docs/restapi.rst @@ -301,7 +301,11 @@ This route will return all releases to display inside Nextcloud's apps admin are ], "lastModified": "2016-06-25T16:49:25.319425Z", "signature": "909377e1a695bbaa415c10ae087ae1cc48e88066d20a5a7a8beed149e9fad3d5", - "changelog": "* **Bugfix**: Pad API last modified timestamp to milliseconds in updated items API to return only new items. API users however need to re-sync their complete contents, #24\n* **Bugfix**: Do not pad milliseconds for non millisecond timestamps in API" + "translations": { + "en": { + "changelog": "* **Bugfix**: Pad API last modified timestamp to milliseconds in updated items API to return only new items. API users however need to re-sync their complete contents, #24\n* **Bugfix**: Do not pad milliseconds for non millisecond timestamps in API" + } + } } ], "screenshots": [ diff --git a/nextcloudappstore/core/admin.py b/nextcloudappstore/core/admin.py index 1dafa5dbbc..f0a9d57f98 100644 --- a/nextcloudappstore/core/admin.py +++ b/nextcloudappstore/core/admin.py @@ -13,7 +13,7 @@ class PhpExtensionDependencyInline(admin.TabularInline): extra = 1 -class AppReleaseAdmin(admin.ModelAdmin): +class AppReleaseAdmin(TranslatableAdmin): inlines = (DatabaseDependencyInline, PhpExtensionDependencyInline) diff --git a/nextcloudappstore/core/api/v0/views.py b/nextcloudappstore/core/api/v0/views.py index 1b6200c14a..7e7ba0b919 100644 --- a/nextcloudappstore/core/api/v0/views.py +++ b/nextcloudappstore/core/api/v0/views.py @@ -35,7 +35,7 @@ def apps(request): if category is not None: apps = filter(lambda app: in_category(app, category), apps) return render_to_response('api/v0/apps.xml', { - 'apps': apps, + 'apps': list(apps), 'request': request, 'version': version }, content_type='application/xml') diff --git a/nextcloudappstore/core/api/v1/release/__init__.py b/nextcloudappstore/core/api/v1/release/__init__.py index 6cf33cc13b..137ea8a63b 100644 --- a/nextcloudappstore/core/api/v1/release/__init__.py +++ b/nextcloudappstore/core/api/v1/release/__init__.py @@ -12,3 +12,4 @@ class ReleaseConfig: self.info_schema = read_relative_file(__file__, 'info.xsd') self.info_xslt = read_relative_file(__file__, 'info.xslt') self.pre_info_xslt = read_relative_file(__file__, 'pre-info.xslt') + self.languages = settings.LANGUAGES diff --git a/nextcloudappstore/core/api/v1/release/importer.py b/nextcloudappstore/core/api/v1/release/importer.py index 6243c41d72..a4c10f1fcd 100644 --- a/nextcloudappstore/core/api/v1/release/importer.py +++ b/nextcloudappstore/core/api/v1/release/importer.py @@ -161,7 +161,8 @@ class AppReleaseImporter(Importer): license_importer: LicenseImporter, shell_command_importer: ShellCommandImporter, string_attribute_importer: StringAttributeImporter, - integer_attribute_importer: IntegerAttributeImporter) -> None: + integer_attribute_importer: IntegerAttributeImporter, + l10n_importer: L10NImporter) -> None: super().__init__({ 'php_extensions': php_extension_importer, 'databases': database_importer, @@ -174,7 +175,7 @@ class AppReleaseImporter(Importer): 'shell_commands': shell_command_importer, 'signature': string_attribute_importer, 'download': string_attribute_importer, - 'changelog': string_attribute_importer, + 'changelog': l10n_importer, }, { 'version', 'raw_version', diff --git a/nextcloudappstore/core/api/v1/release/parser.py b/nextcloudappstore/core/api/v1/release/parser.py index c2952b2667..623232e1ee 100644 --- a/nextcloudappstore/core/api/v1/release/parser.py +++ b/nextcloudappstore/core/api/v1/release/parser.py @@ -35,6 +35,9 @@ class XMLSyntaxError(APIException): pass +Metadata = Tuple[str, str, Dict[str, str]] + + class GunZipAppMetadataExtractor: def __init__(self, config: ReleaseConfig) -> None: """ @@ -43,7 +46,7 @@ class GunZipAppMetadataExtractor: self.config = config self.app_folder_regex = re.compile(r'^[a-z]+[a-z_]*(?:/.*)*$') - def extract_app_metadata(self, archive_path: str) -> Tuple[str, str, str]: + def extract_app_metadata(self, archive_path: str) -> Metadata: """ Extracts the info.xml from an tar.gz archive :argument archive_path: the path to the tar.gz archive @@ -60,10 +63,19 @@ class GunZipAppMetadataExtractor: result = self._parse_archive(tar) return result - def _parse_archive(self, tar: Any) -> Tuple[str, str, str]: + def _parse_archive(self, tar: Any) -> Metadata: app_id = self._find_app_id(tar) info = self._get_contents('%s/appinfo/info.xml' % app_id, tar) - changelog = self._get_contents('%s/CHANGELOG.md' % app_id, tar, '') + changelog = {} # type: Dict[str, str] + changelog['en'] = self._get_contents('%s/CHANGELOG.md' % app_id, tar, + '') + for code, _ in self.config.languages: + trans_changelog = self._get_contents( + '%s/CHANGELOG.%s.md' % (app_id, code), tar, '' + ) + if trans_changelog: + changelog[code] = trans_changelog + return info, app_id, changelog def _get_contents(self, path: str, tar: Any, default: Any = None) -> str: diff --git a/nextcloudappstore/core/api/v1/release/provider.py b/nextcloudappstore/core/api/v1/release/provider.py index 7f0998c984..66935f9115 100644 --- a/nextcloudappstore/core/api/v1/release/provider.py +++ b/nextcloudappstore/core/api/v1/release/provider.py @@ -38,11 +38,13 @@ class AppReleaseProvider: % (app_id, info_app_id) raise InvalidAppDirectoryException(msg) - version = info['app']['release']['version'] + release = info['app']['release'] + version = release['version'] if is_nightly: version += '-nightly' - info['app']['release']['changelog'] = parse_changelog(changelog, - version) + release['changelog'] = changelog + for code, value in changelog.items(): + release['changelog'][code] = parse_changelog(value, version) with open(download.filename, 'rb') as f: data = f.read() diff --git a/nextcloudappstore/core/api/v1/serializers.py b/nextcloudappstore/core/api/v1/serializers.py index 1355e96af5..494cf35d7f 100644 --- a/nextcloudappstore/core/api/v1/serializers.py +++ b/nextcloudappstore/core/api/v1/serializers.py @@ -67,6 +67,7 @@ class AppReleaseSerializer(serializers.ModelSerializer): raw_platform_version_spec = SerializerMethodField() version = SerializerMethodField() nightly = SerializerMethodField() + translations = TranslatedFieldsField(shared_model=AppRelease) class Meta: model = AppRelease @@ -75,7 +76,7 @@ class AppReleaseSerializer(serializers.ModelSerializer): 'php_version_spec', 'platform_version_spec', 'min_int_size', 'download', 'created', 'licenses', 'last_modified', 'nightly', 'raw_php_version_spec', 'raw_platform_version_spec', 'signature', - 'changelog', + 'translations', ) def get_platform_version_spec(self, obj): diff --git a/nextcloudappstore/core/api/v1/tests/test_parser.py b/nextcloudappstore/core/api/v1/tests/test_parser.py index e3aba70e75..945e15a0d1 100644 --- a/nextcloudappstore/core/api/v1/tests/test_parser.py +++ b/nextcloudappstore/core/api/v1/tests/test_parser.py @@ -168,7 +168,7 @@ class ParserTest(TestCase): extractor = GunZipAppMetadataExtractor(self.config) full_extracted, app_id, changes = extractor.extract_app_metadata(path) self.assertEqual('contacts', app_id) - self.assertEqual('', changes) + self.assertEqual('', changes['en']) def test_extract_gunzip_info(self): path = self.get_path('data/archives/full.tar.gz') @@ -176,7 +176,7 @@ class ParserTest(TestCase): full_extracted, app_id, changes = extractor.extract_app_metadata(path) full = self._get_contents('data/infoxmls/full.xml') self.assertEqual(full, full_extracted) - self.assertEqual('', changes) + self.assertEqual('', changes['en']) def test_extract_changelog(self): path = self.get_path('data/archives/changelog.tar.gz') diff --git a/nextcloudappstore/core/migrations/0007_auto_20161005_1424.py b/nextcloudappstore/core/migrations/0007_auto_20161005_1424.py new file mode 100644 index 0000000000..7278160075 --- /dev/null +++ b/nextcloudappstore/core/migrations/0007_auto_20161005_1424.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-10-05 14:24 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +def create_default_changelogs(apps, schema_editor): + model = apps.get_model('core', 'AppRelease') + for release in model.objects.all(): + release.set_current_language('en') + release.changelog = '' + release.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0006_apprelease_changelog'), + ] + + operations = [ + migrations.CreateModel( + name='AppReleaseTranslation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('language_code', models.CharField(db_index=True, max_length=15, verbose_name='Language')), + ('changelog', models.TextField(default='', help_text='The release changelog. Can contain Markdown', verbose_name='Changelog')), + ], + options={ + 'default_permissions': (), + 'verbose_name': 'App release Translation', + 'db_table': 'core_apprelease_translation', + 'db_tablespace': '', + 'managed': True, + }, + ), + migrations.RemoveField( + model_name='apprelease', + name='changelog', + ), + migrations.AddField( + model_name='appreleasetranslation', + name='master', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='core.AppRelease'), + ), + migrations.AlterUniqueTogether( + name='appreleasetranslation', + unique_together=set([('language_code', 'master')]), + ), + migrations.RunPython(create_default_changelogs, migrations.RunPython.noop) + ] diff --git a/nextcloudappstore/core/models.py b/nextcloudappstore/core/models.py index d9e3fc883a..4e5266dfbf 100644 --- a/nextcloudappstore/core/models.py +++ b/nextcloudappstore/core/models.py @@ -32,6 +32,7 @@ class AppManager(TranslatableManager): def get_compatible(self, platform_version, inclusive=False): apps = App.objects.prefetch_related( 'releases', + 'releases__translations', 'releases__databases', 'releases__licenses', 'releases__phpextensiondependencies__php_extension', @@ -298,7 +299,7 @@ class AppAuthor(Model): verbose_name_plural = _('App authors') -class AppRelease(Model): +class AppRelease(TranslatableModel): version = CharField(max_length=256, verbose_name=_('Version'), help_text=_('Version follows Semantic Versioning')) app = ForeignKey('App', on_delete=CASCADE, verbose_name=_('App'), @@ -335,8 +336,10 @@ class AppRelease(Model): verbose_name=_('Updated at')) signature = TextField(verbose_name=_('Signature'), help_text=_( 'A signature using SHA512 and the app\'s certificate')) - changelog = TextField(verbose_name=_('Changelog'), help_text=_( - 'The release changelog. Can contain Markdown'), default='') + translations = TranslatedFields( + changelog=TextField(verbose_name=_('Changelog'), help_text=_( + 'The release changelog. Can contain Markdown'), default='') + ) class Meta: verbose_name = _('App release') diff --git a/nextcloudappstore/core/templates/api/v0/apps.xml b/nextcloudappstore/core/templates/api/v0/apps.xml index 9798215fed..be01861698 100644 --- a/nextcloudappstore/core/templates/api/v0/apps.xml +++ b/nextcloudappstore/core/templates/api/v0/apps.xml @@ -5,7 +5,7 @@ 100 {{ apps|length }} - 1000000 + {{ apps|length }} {% for app in apps %} diff --git a/nextcloudappstore/core/templates/api/v0/categories.xml b/nextcloudappstore/core/templates/api/v0/categories.xml index 3447dfe602..f3bea10325 100644 --- a/nextcloudappstore/core/templates/api/v0/categories.xml +++ b/nextcloudappstore/core/templates/api/v0/categories.xml @@ -5,6 +5,7 @@ 100 {{ categories|length }} + {{ categories|length }} {% for cat in categories %}