[damned-lies/api: 2/2] Added a separate api module



commit c96f4b90319cb44e5e71a35b6459d97614b59844
Author: Claude Paroz <claude 2xlibre net>
Date:   Fri Jan 11 20:16:42 2019 +0100

    Added a separate api module

 api/__init__.py        |   0
 api/tests.py           |  95 ++++++++++++++++++++++
 api/urls.py            |  23 ++++++
 api/views.py           | 210 +++++++++++++++++++++++++++++++++++++++++++++++++
 damnedlies/settings.py |   1 +
 damnedlies/urls.py     |   2 +
 people/models.py       |   2 +-
 vertimus/views.py      |  31 +++++---
 8 files changed, 350 insertions(+), 14 deletions(-)
---
diff --git a/api/__init__.py b/api/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/api/tests.py b/api/tests.py
new file mode 100644
index 00000000..fb7d67de
--- /dev/null
+++ b/api/tests.py
@@ -0,0 +1,95 @@
+from django.test import TestCase
+from django.urls import reverse
+
+
+class APITests(TestCase):
+    fixtures = ['sample_data.json']
+
+    def test_home(self):
+        response = self.client.get(reverse('api-home'))
+        self.assertEqual(response.json(), {
+            'languages': '/api/v1/languages/',
+            'modules': '/api/v1/modules/',
+            'releases': '/api/v1/releases/',
+            'teams': '/api/v1/teams/',
+        })
+
+    def test_modules(self):
+        response = self.client.get(reverse('api-modules'))
+        result = response.json()
+        self.assertEqual(result[0], {'name': 'gnome-hello', 'href': '/api/v1/modules/gnome-hello'})
+
+    def test_module(self):
+        response = self.client.get(reverse('api-module', args=['gnome-hello']))
+        result = response.json()
+        self.assertEqual(result['vcs_web'], 'https://gitlab.gnome.org/GNOME/gnome-hello/')
+        self.assertEqual(result['branches'], [{'name': 'master'}])
+        self.assertEqual(
+            result['domains'][1], 
+            {'description': 'UI Translations', 'name': 'po', 'dtype': 'ui', 'layout': 'po/{lang}.po'}
+        )
+        
+    def test_teams(self):
+        response = self.client.get(reverse('api-teams'))
+        result = response.json()
+        self.assertEqual(result[0], {'name': 'fr', 'href': '/api/v1/teams/fr', 'description': 'French'})
+
+    def test_team(self):
+        response = self.client.get(reverse('api-team', args=['fr']))
+        result = response.json()
+        self.assertEqual(result['coordinators'], [{'name': 'John Coordinator'}])
+
+    def test_languages(self):
+        response = self.client.get(reverse('api-languages'))
+        result = response.json()
+        self.assertEqual(result[0], {
+            'href': '/api/v1/teams/bem',
+            'locale': 'bem',
+            'name': 'Bemba',
+            'plurals': '',
+            'team__description': None
+        })
+
+    def test_releases(self):
+        response = self.client.get(reverse('api-releases'))
+        result = response.json()
+        self.assertEqual(result[0], {
+            'description': 'freedesktop.org (non-GNOME)',
+            'href': '/api/v1/releases/freedesktop-org',
+            'name': 'freedesktop-org'
+        })
+
+    def test_release(self):
+        response = self.client.get(reverse('api-release', args=['gnome-3-8']))
+        result = response.json()
+        self.assertEqual(result['description'], 'GNOME 3.8 (stable)')
+        self.assertEqual(
+            result['languages'][0],
+            {'href': '/api/v1/releases/gnome-3-8/languages/bem', 'locale': 'bem'}
+        )
+        self.assertEqual(result['branches'][0], 'gnome-3-8 (zenity)')
+        self.assertEqual(result['statistics'], '/api/v1/releases/gnome-3-8/stats')
+
+    def test_release_stats(self):
+        response = self.client.get(reverse('api-release-stats', args=['gnome-3-8']))
+        result = response.json()
+        self.assertEqual(result['statistics'][0]['lang_name'], 'French')
+        self.assertEqual(
+            result['statistics'][0]['ui'],
+            {'translated_perc': 100, 'untranslated_perc': 0, 'translated': 183,
+             'fuzzy_perc': 0, 'untranslated': 0, 'fuzzy': 0}
+        )
+
+    def test_release_language(self):
+        response = self.client.get(reverse('api-release-language', args=['gnome-3-8', 'fr']))
+        result = response.json()
+        self.assertEqual(len(result['modules']), 4)
+
+    def test_module_lang_stats(self):
+        response = self.client.get(
+            reverse('api-module-lang-stats', args=['gnome-hello', 'master', 'po', 'fr'])
+        )
+        result = response.json()
+        self.assertEqual(result['statistics'], {'untrans': 0, 'fuzzy': 0, 'trans': 47})
+        self.assertEqual(result['pot_file'], '/POT/gnome-hello.master/gnome-hello.master.pot')
+        self.assertEqual(result['po_file'], '/POT/gnome-hello.master/gnome-hello.master.fr.po')
diff --git a/api/urls.py b/api/urls.py
new file mode 100644
index 00000000..fba4dfb8
--- /dev/null
+++ b/api/urls.py
@@ -0,0 +1,23 @@
+from django.urls import path
+
+from . import views
+
+urlpatterns = [
+    path('', views.APIHomeView.as_view(), name='api-home'),
+    path('modules/', views.ModulesView.as_view(), name='api-modules'),
+    path('modules/<name:module_name>', views.ModuleView.as_view(), name='api-module'),
+    path('teams/', views.TeamsView.as_view(), name='api-teams'),
+    path('teams/<locale:team_name>', views.TeamView.as_view(), name='api-team'),
+    path('languages/', views.LanguagesView.as_view(), name='api-languages'),
+    path('releases/', views.ReleasesView.as_view(), name='api-releases'),
+    path('releases/<name:release>', views.ReleaseView.as_view(), name='api-release'),
+    path('releases/<name:release>/stats', views.ReleaseStatsView.as_view(), name='api-release-stats'),
+    path('releases/<name:release>/languages/<locale:lang>', views.ReleaseLanguageView.as_view(),
+        name='api-release-language'),
+    path(
+        'modules/<name:module_name>/branches/<name:branch_name>/domains/<name:domain_name>'
+        '/languages/<locale:lang>',
+        views.ModuleLangStatsView.as_view(),
+        name='api-module-lang-stats'
+    ),
+]
diff --git a/api/views.py b/api/views.py
new file mode 100644
index 00000000..ac21591e
--- /dev/null
+++ b/api/views.py
@@ -0,0 +1,210 @@
+from django.http import Http404, JsonResponse
+from django.shortcuts import get_object_or_404
+from django.urls import reverse
+from django.views.generic import View
+
+from languages.models import Language
+from stats.models import Module, Release
+from teams.models import Team
+from vertimus.models import State
+from vertimus.views import get_vertimus_state
+
+
+class SerializeListView(View):
+    def get(self, request, *args, **kwargs):
+        return JsonResponse(
+            self.serialize_qs(self.get_queryset().values(*['pk'] + self.fields)), safe=False
+        )
+
+    def serialize_obj(self, obj, fields=None):
+        data = {field: obj[field] for field in fields}
+        data['href'] = self.get_href(obj)
+        return data
+
+    def serialize_qs(self, qs):
+        return [self.serialize_obj(obj, self.fields) for obj in qs]
+
+
+class SerializeObjectView(SerializeListView):
+    def get(self, *args, **kwargs):
+        return JsonResponse(self.serialize_obj(self.get_object()))
+
+    def serialize_obj(self, obj, fields=None):
+        return serialize_instance(obj, fields or self.fields)
+
+
+class APIHomeView(View):
+    def get(self, *args, **kwargs):
+        return JsonResponse({
+            'modules': reverse('api-modules'),
+            'teams': reverse('api-teams'),
+            'languages': reverse('api-languages'),
+            'releases': reverse('api-releases'),
+        })
+
+
+class ModulesView(SerializeListView):
+    fields = ['name']
+
+    def get_queryset(self):
+        return Module.objects.filter(archived=False)
+
+    def get_href(self, obj):
+        return reverse('api-module', args=[obj['name']])
+
+
+class ModuleView(SerializeObjectView):
+    @property
+    def fields(self):
+        return [
+            f.name for f in Module._meta.get_fields()
+            if not f.one_to_many and f.name not in ['id', 'archived']
+        ]
+
+    def get_object(self):
+        return get_object_or_404(Module, name=self.kwargs['module_name'])
+
+    def serialize_obj(self, obj, **kwargs):
+        data = serialize_instance(obj, self.fields)
+        data['branches'] = [
+            serialize_instance(branch, ['name'])
+            for branch in obj.get_branches()
+        ]
+        data['domains'] = [
+            serialize_instance(domain, ['name', 'description', 'dtype', 'layout'])
+            for domain in obj.domain_set.all().order_by('name')
+        ]
+        return data
+
+
+class TeamsView(SerializeListView):
+    fields = ['name', 'description']
+
+    def get_queryset(self):
+        return Team.objects.all().order_by('name')
+
+    def get_href(self, obj):
+        return reverse('api-team', args=[obj['name']])
+
+
+class TeamView(SerializeObjectView):
+    @property
+    def fields(self):
+        return [
+            f.name for f in Team._meta.get_fields()
+            if not f.one_to_many and f.name not in ['id', 'members']
+        ]
+
+    def get_object(self):
+        return get_object_or_404(Team, name=self.kwargs['team_name'])
+
+    def serialize_obj(self, obj, **kwargs):
+        data = serialize_instance(obj, self.fields)
+        data['coordinators'] = [
+            serialize_instance(coord, ['name'])
+            for coord in obj.get_coordinators()
+        ]
+        return data
+
+
+class LanguagesView(SerializeListView):
+    fields = ['name', 'locale', 'team__description', 'plurals']
+
+    def get_queryset(self):
+        return Language.objects.all().order_by('name')
+
+    def get_href(self, obj):
+        return reverse('api-team', args=[obj['locale']])
+
+
+class ReleasesView(SerializeListView):
+    fields = ['name', 'description']
+
+    def get_queryset(self):
+        return Release.objects.all().order_by('name')
+
+    def get_href(self, obj):
+        return reverse('api-release', args=[obj['name']])
+
+
+class ReleaseView(SerializeObjectView):
+    fields = ['name', 'description', 'string_frozen', 'status', 'branches']
+
+    def get_object(self):
+        return get_object_or_404(Release, name=self.kwargs['release'])
+
+    def serialize_obj(self, obj, **kwargs):
+        data = serialize_instance(obj, self.fields)
+        data['languages'] = [{
+            'locale': lang.locale,
+            'href': reverse('api-release-language', args=[obj.name, lang.locale])
+        } for lang in Language.objects.all()]
+        data['statistics'] = reverse('api-release-stats', args=[self.kwargs['release']])
+        return data
+
+
+class ReleaseStatsView(View):
+    def get(self, *args, **kwargs):
+        release = get_object_or_404(Release, name=self.kwargs['release'])
+        lang_stats = release.get_global_stats()
+        return JsonResponse({
+            'release': serialize_instance(release, ['name', 'description', 'status']),
+            'statistics': lang_stats,
+        })
+
+
+class ReleaseLanguageView(View):
+    def get(self, *args, **kwargs):
+        release = get_object_or_404(Release, name=self.kwargs['release'])
+        language = get_object_or_404(Language, locale=self.kwargs['lang'])
+        item_list = []
+        for branch in release.branches.all().select_related('module'):
+            for domain in branch.get_domains():
+                item_list.append({
+                    'module': branch.module.name,
+                    'branch': branch.name,
+                    'stats': reverse('api-module-lang-stats', kwargs={
+                        'module_name': branch.module.name,
+                        'branch_name': branch.name,
+                        'domain_name': domain,
+                        'lang': language.locale,
+                    }),
+                })
+        return JsonResponse({
+            'release': release.name,
+            'language': language.locale,
+            'modules': item_list,
+        })
+
+
+class ModuleLangStatsView(View):
+    def get(self, *args, **kwargs):
+        module = get_object_or_404(Module, name=self.kwargs['module_name'])
+        branch = get_object_or_404(module.branch_set, name=self.kwargs['branch_name'])
+        domain = branch.get_domains().get(self.kwargs['domain_name'])
+        if not domain:
+            raise Http404
+        language = get_object_or_404(Language, locale=self.kwargs['lang'])
+        pot_stats, stats, state = get_vertimus_state(branch, domain, language)
+        return JsonResponse({
+            'module': module.name,
+            'branch': branch.name,
+            'domain': domain.name,
+            'language': language.locale,
+            'state': state.name,
+            'statistics': {
+                'trans': stats.translated(), 'fuzzy': stats.fuzzy(), 'untrans': stats.untranslated()
+            },
+            'pot_file': stats.pot_url(),
+            'po_file': stats.po_url(),
+        })
+
+
+def serialize_instance(instance, field_names):
+    data = {}
+    for field in field_names:
+        value = getattr(instance, field)
+        if hasattr(value, 'all'):
+            value = [str(o) for o in value.all()]
+        data[field] = value
+    return data
diff --git a/damnedlies/settings.py b/damnedlies/settings.py
index 962e5bec..ccef97a6 100644
--- a/damnedlies/settings.py
+++ b/damnedlies/settings.py
@@ -146,6 +146,7 @@ INSTALLED_APPS = [
     'teams',
     'vertimus',
     'feeds',
+    'api',
 ]
 
 AUTHENTICATION_BACKENDS = [
diff --git a/damnedlies/urls.py b/damnedlies/urls.py
index 9b71efa5..5fd6b246 100644
--- a/damnedlies/urls.py
+++ b/damnedlies/urls.py
@@ -18,6 +18,7 @@ class LocaleConverter(StringConverter):
 
 
 class NameConverter(StringConverter):
+    """Converter for module, branch, or domain names."""
     regex = '[-~\w\+\.]+'
 
 
@@ -75,6 +76,7 @@ urlpatterns = [
     path('i18n/', include('django.conf.urls.i18n')),
     path('admin/', admin.site.urls),
     path('rss/', include('feeds.urls')),
+    path('api/v1/', include('api.urls')),
     path('', include('social_django.urls', namespace='social')),
 ]
 
diff --git a/people/models.py b/people/models.py
index 41bd86c4..67ecdb8a 100644
--- a/people/models.py
+++ b/people/models.py
@@ -108,7 +108,7 @@ class Person(User):
     @property
     def name(self):
         if self.first_name or self.last_name:
-            return self.first_name + " " + self.last_name
+            return " ".join([name for name in [self.first_name, self.last_name] if name])
         else:
             return self.username
 
diff --git a/vertimus/views.py b/vertimus/views.py
index 3694b59f..7685c2af 100644
--- a/vertimus/views.py
+++ b/vertimus/views.py
@@ -61,19 +61,7 @@ def vertimus(request, branch, domain, language, stats=None, level="0"):
        grandparent, second (2) is the parent of the grandparent, etc."""
     level = int(level)
 
-    pot_stats = get_object_or_404(Statistics, branch=branch, domain=domain, language=None)
-    if not stats:
-        try:
-            stats = Statistics.objects.get(branch=branch, domain=domain, language=language)
-        except Statistics.DoesNotExist:
-            stats = FakeLangStatistics(pot_stats, language)
-
-    # Get the state of the translation
-    try:
-        state = State.objects.get(branch=branch, domain=domain, language=language)
-    except State.DoesNotExist:
-        # No need to save the state at this stage
-        state = State(branch=branch, domain=domain, language=language)
+    pot_stats, stats, state = get_vertimus_state(branch, domain, language, stats=stats)
     # Filtering on domain.name instead of domain because we can have several domains
     # working on the same set of strings (e.g. when an extraction method changed,
     # each extraction is mapped to a different domain with branch_from/branch_to delimitations)
@@ -155,6 +143,23 @@ def vertimus(request, branch, domain, language, stats=None, level="0"):
     return render(request, 'vertimus/vertimus_detail.html', context)
 
 
+def get_vertimus_state(branch, domain, language, stats=None):
+    pot_stats = get_object_or_404(Statistics, branch=branch, domain=domain, language=None)
+    if not stats:
+        try:
+            stats = Statistics.objects.get(branch=branch, domain=domain, language=language)
+        except Statistics.DoesNotExist:
+            stats = FakeLangStatistics(pot_stats, language)
+
+    # Get the state of the translation
+    try:
+        state = State.objects.get(branch=branch, domain=domain, language=language)
+    except State.DoesNotExist:
+        # No need to save the state at this stage
+        state = State(branch=branch, domain=domain, language=language)
+    return pot_stats, stats, state
+
+
 def vertimus_diff(request, action_id_1, action_id_2, level):
     """Show a diff between current action po file and previous file"""
     if int(level) != 0:


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]