├── apps ├── __init__.py └── Localizr │ ├── __init__.py │ ├── migrations │ ├── __init__.py │ ├── 0013_auto_20180717_1426.py │ ├── 0009_localizedstring_status.py │ ├── 0003_auto_20171123_0224.py │ ├── 0014_auto_20210613_0547.py │ ├── 0012_auto_20180523_1542.py │ ├── 0011_auto_20180523_1530.py │ ├── 0004_auto_20171129_0938.py │ ├── 0006_appuser.py │ ├── 0007_auto_20171204_2107.py │ ├── 0005_auto_20171203_1703.py │ ├── 0008_auto_20180123_1246.py │ ├── 0002_auto_20171123_0213.py │ ├── 0010_auto_20180523_1513.py │ └── 0001_initial.py │ ├── templatetags │ ├── __init__.py │ └── localizr_extras.py │ ├── apps.py │ ├── permissions.py │ ├── widgets.py │ ├── serializers.py │ ├── middlewares.py │ ├── resources.py │ ├── tests.py │ ├── renderers.py │ ├── views.py │ ├── admin.py │ └── models.py ├── project ├── __init__.py ├── permissions.py ├── wsgi.py ├── settings │ ├── production.py │ └── __init__.py ├── views.py ├── serializers.py └── urls.py ├── version.txt ├── runtime.txt ├── Procfile ├── .github ├── FUNDING.yml └── workflows │ ├── stale.yml │ └── python-app.yml ├── docs └── images │ ├── admin_import.png │ ├── admin_import_compare.png │ ├── fastlane_actions_localizr.png │ └── admin_localized_strings_import.png ├── sample_data ├── Apps.csv ├── Locales.csv ├── App's Keys.csv └── Localized String.csv ├── requirements_local.txt ├── .travis.yml ├── templates ├── rest_framework │ ├── api.html │ └── login_base.html └── admin │ ├── base.html │ └── index.html ├── requirements.txt ├── Dockerfile ├── .dockerignore ├── entrypoint-docker.sh ├── entrypoint-heroku.sh ├── manage.py ├── LICENSE ├── app.json ├── .gitignore ├── fastlane └── actions │ └── localizr.rb └── README.md /apps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /project/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 1.3 2 | -------------------------------------------------------------------------------- /apps/Localizr/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.8.10 -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: sh entrypoint-heroku.sh -------------------------------------------------------------------------------- /apps/Localizr/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/Localizr/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [michaelhenry] 4 | -------------------------------------------------------------------------------- /docs/images/admin_import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelhenry/Localizr/HEAD/docs/images/admin_import.png -------------------------------------------------------------------------------- /docs/images/admin_import_compare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelhenry/Localizr/HEAD/docs/images/admin_import_compare.png -------------------------------------------------------------------------------- /sample_data/Apps.csv: -------------------------------------------------------------------------------- 1 | name,slug,base_locale 2 | Another-App,another-app,en 3 | DemoApp,demo,en 4 | MySocialApp,mysocialapp,en 5 | -------------------------------------------------------------------------------- /apps/Localizr/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class LocalizrConfig(AppConfig): 5 | name = 'apps.Localizr' 6 | -------------------------------------------------------------------------------- /sample_data/Locales.csv: -------------------------------------------------------------------------------- 1 | name,code 2 | Chinese,zh 3 | English,en 4 | French,fr 5 | Japanese,ja 6 | Portuguese,pt 7 | Spanish,es 8 | -------------------------------------------------------------------------------- /docs/images/fastlane_actions_localizr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelhenry/Localizr/HEAD/docs/images/fastlane_actions_localizr.png -------------------------------------------------------------------------------- /docs/images/admin_localized_strings_import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelhenry/Localizr/HEAD/docs/images/admin_localized_strings_import.png -------------------------------------------------------------------------------- /requirements_local.txt: -------------------------------------------------------------------------------- 1 | boto3==1.17.59 2 | dj-database-url==0.5.0 3 | django==3.2.16 4 | django-debug-toolbar==3.2.1 5 | django_storages==1.11.1 6 | djangorestframework==3.12.4 7 | django-import_export==2.5.0 8 | pyzmq --pre 9 | whitenoise==5.2.0 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | - "3.7" 5 | - "3.8" 6 | 7 | install: 8 | - pip3 install -r requirements.txt 9 | 10 | env: 11 | - DJANGO_SETTINGS_MODULE=project.settings 12 | 13 | script: 14 | - python manage.py test 15 | -------------------------------------------------------------------------------- /templates/rest_framework/api.html: -------------------------------------------------------------------------------- 1 | {% extends "rest_framework/base.html" %} 2 | {% block title %} 3 | API 4 | {% endblock %} 5 | 6 | {% block branding %} 7 | 8 | Localizr 9 | 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.17.59 2 | dj-database-url==0.5.0 3 | django==3.2.16 4 | django-debug-toolbar==3.2.1 5 | django_storages==1.11.1 6 | djangorestframework==3.12.4 7 | django-import_export==2.5.0 8 | gunicorn==20.1.0 9 | psycopg2-binary==2.8.6 10 | postgres==3.0.0 11 | pyzmq --pre 12 | whitenoise==5.2.0 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8 2 | MAINTAINER Michael Henry Pantaleon 3 | 4 | ENV PROJECT_PATH /home 5 | WORKDIR $PROJECT_PATH 6 | 7 | COPY requirements.txt $PROJECT_PATH 8 | RUN pip install -r requirements.txt 9 | COPY . $PROJECT_PATH 10 | 11 | ENV DJANGO_SETTINGS_MODULE project.settings.production 12 | ENV PORT 8001 13 | 14 | EXPOSE $PORT 15 | ENTRYPOINT sh entrypoint-docker.sh 16 | -------------------------------------------------------------------------------- /apps/Localizr/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework.permissions import IsAuthenticated, SAFE_METHODS 2 | 3 | 4 | class IsObjectOwner(IsAuthenticated): 5 | 6 | def has_object_permission(self, request, view, obj): 7 | if request.method in SAFE_METHODS: 8 | return True 9 | if hasattr(obj, 'created_by'): 10 | return obj.created_by == request.user 11 | return False 12 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .ipynb_checkpoints/* 3 | /notebooks/* 4 | /unused/* 5 | Dockerfile 6 | Procfile 7 | .DS_Store 8 | .gitignore 9 | README.md 10 | env.* 11 | /devops/* 12 | runtime.txt 13 | app.json 14 | .travis.yml 15 | 16 | fastlane/* 17 | docs/* 18 | sample_data/* 19 | 20 | # To prevent storing dev/temporary container data 21 | *.csv 22 | /tmp/* 23 | venv/* 24 | .env 25 | *.sqlite3 26 | __pycache__ 27 | media/* -------------------------------------------------------------------------------- /project/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework.permissions import IsAuthenticated 2 | 3 | 4 | class IsAuthenticatedOrOptionsRequest(IsAuthenticated): 5 | """ 6 | If it is authenticated or options request. 7 | """ 8 | 9 | def has_permission(self, request, view): 10 | if request.method == 'OPTIONS': 11 | return True 12 | return super(IsAuthenticatedOrOptionsRequest, self).has_permission(request, view) 13 | -------------------------------------------------------------------------------- /entrypoint-docker.sh: -------------------------------------------------------------------------------- 1 | python manage.py migrate 2 | 3 | cat <', locale_detail_view, name='locale-detail'), 35 | path('v1/apps/', app_info_list_view, name='app-info-list'), 36 | path('v1/apps/', locale_list_view, name='app-info-detail'), 37 | path('v1/keys/', key_string_list_view, name='key-string-list'), 38 | path('v1/keys/', key_string_detail_view, name='key-string-detail'), 39 | path('v1/app-key-strings/', app_info_key_string_list_view, 40 | name='app-key-strings-list'), 41 | path('v1/app-key-strings/', app_info_key_string_detail_view, 42 | name='app-key-strings-detail'), 43 | path('v1/localized-strings/', localized_string_list_view, 44 | name='localized-string-list'), 45 | path('v1/localized-strings/', 46 | localized_string_detail_view, name='localized-string-detail'), 47 | path('app/.', 48 | key_value_list_view, name='key-value-list'), 49 | path('v1/login/', login_view, name='login'), 50 | path('', admin.site.urls), 51 | ] 52 | 53 | if settings.DEBUG_TOOLBAR: 54 | import debug_toolbar 55 | urlpatterns += [ 56 | path('__debug__/', include(debug_toolbar.urls)), 57 | ] 58 | 59 | 60 | admin.site.site_header = 'Localizr' 61 | admin.site.site_title = 'Localizr' 62 | admin.site.site_url = None 63 | -------------------------------------------------------------------------------- /apps/Localizr/middlewares.py: -------------------------------------------------------------------------------- 1 | from django.utils.deprecation import MiddlewareMixin 2 | from django.core.files.base import ContentFile 3 | from django.http import HttpResponseRedirect, HttpResponseGone 4 | from datetime import timedelta 5 | from django.utils import timezone 6 | from .models import ( 7 | Snapshot, 8 | SnapshotFile, 9 | ) 10 | 11 | 12 | class LocalizrSnapshotMiddleWare(MiddlewareMixin): 13 | 14 | def process_response(self, request, response): 15 | 16 | snapshot_key = request.GET.get('snapshot') 17 | format = request.GET.get('format') 18 | if snapshot_key and format: 19 | 20 | filename = request.path.split('/')[-1] 21 | locale_code = filename.split('.')[-1] 22 | app_slug = filename.split('.')[0] 23 | content_type = response['Content-Type'] 24 | 25 | snapshot, created = Snapshot.objects.get_or_create( 26 | key=snapshot_key, 27 | app_slug=app_slug, 28 | format=format, 29 | ) 30 | 31 | if not created: 32 | snapshot_file = None 33 | snapshot_file_query = SnapshotFile.objects.filter( 34 | snapshot=snapshot, 35 | locale_code=locale_code) 36 | 37 | if snapshot_file_query.exists(): 38 | snapshot_file = snapshot_file_query.first() 39 | else: 40 | 41 | if timezone.now() > (snapshot.created + timedelta(hours=12)): 42 | return HttpResponseGone() 43 | content = response.content 44 | 45 | snapshot_file = SnapshotFile( 46 | snapshot=snapshot, locale_code=locale_code) 47 | snapshot_file.key = snapshot_key 48 | snapshot_file.file.save(filename, ContentFile(content)) 49 | snapshot_file.save() 50 | 51 | return HttpResponseRedirect(snapshot_file.file.url, content_type=content_type) 52 | return response 53 | 54 | 55 | class CorsMiddleWare(MiddlewareMixin): 56 | 57 | def process_request(self, request): 58 | pass 59 | 60 | def process_response(self, request, response): 61 | 62 | # For now it's a wildcard. 63 | response['Access-Control-Allow-Origin'] = '*' 64 | response['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, Host, X-Date' 65 | return response 66 | -------------------------------------------------------------------------------- /apps/Localizr/resources.py: -------------------------------------------------------------------------------- 1 | from import_export import resources 2 | from import_export import fields 3 | 4 | from .models import ( 5 | Locale, 6 | AppInfo, 7 | KeyString, 8 | AppInfoKeyString, 9 | LocalizedString, 10 | ) 11 | 12 | from .widgets import ( 13 | AppInfoWidget, 14 | LocaleWidget, 15 | KeyStringWidget, 16 | ) 17 | 18 | 19 | class AppInfoResource(resources.ModelResource): 20 | 21 | base_locale = fields.Field( 22 | column_name='base_locale', 23 | attribute='base_locale', 24 | widget=LocaleWidget(Locale, 'code')) 25 | 26 | class Meta: 27 | model = AppInfo 28 | import_id_fields = ['slug'] 29 | export_order = ('name', 'slug', 'base_locale',) 30 | fields = ('name', 'slug', 'base_locale',) 31 | 32 | 33 | class LocaleResource(resources.ModelResource): 34 | 35 | class Meta: 36 | model = Locale 37 | import_id_fields = ['code'] 38 | export_order = ('name', 'code',) 39 | fields = ('name', 'code',) 40 | 41 | 42 | class LocalizedStringResource(resources.ModelResource): 43 | 44 | key = fields.Field( 45 | column_name='key', 46 | attribute='key_string', 47 | widget=KeyStringWidget(KeyString, 'key')) 48 | 49 | locale = fields.Field( 50 | column_name='locale', 51 | attribute='locale', 52 | widget=LocaleWidget(Locale, 'code')) 53 | 54 | class Meta: 55 | model = LocalizedString 56 | import_id_fields = ['key', 'locale'] 57 | export_order = ('key', 'value', 'locale') 58 | fields = ('key', 'value', 'locale') 59 | 60 | def after_import_instance(self, instance, new, **kwargs): 61 | if new: 62 | instance.created_by = kwargs['user'] 63 | else: 64 | instance.modified_by = kwargs['user'] 65 | return instance 66 | 67 | 68 | class AppInfoKeyStringResource(resources.ModelResource): 69 | 70 | app = fields.Field( 71 | column_name='app', 72 | attribute='app_info', 73 | widget=AppInfoWidget(AppInfo, 'slug')) 74 | 75 | key = fields.Field( 76 | column_name='key', 77 | attribute='key_string', 78 | widget=KeyStringWidget(KeyString, 'key')) 79 | 80 | value = fields.Field( 81 | column_name='value', 82 | attribute='value',) 83 | 84 | class Meta: 85 | model = AppInfoKeyString 86 | import_id_fields = ['app', 'key'] 87 | export_order = ('key', 'value', 'app') 88 | fields = ('key', 'value', 'app',) 89 | -------------------------------------------------------------------------------- /apps/Localizr/tests.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.contrib.auth.models import User, Permission 4 | from django.test import ( 5 | TestCase, 6 | Client, 7 | ) 8 | 9 | from .models import ( 10 | Locale, 11 | AppInfo, 12 | KeyString, 13 | LocalizedString, 14 | ) 15 | 16 | 17 | def jsonify(content="{}"): 18 | return json.loads(content.decode('utf-8')) 19 | 20 | 21 | class LocalizrTest(TestCase): 22 | 23 | def setUp(self): 24 | 25 | self.client = Client() 26 | 27 | username = "kel" 28 | password = "12345dad216790oeieiuri3urieure" 29 | email = "me@iamkel.net" 30 | 31 | localizr_permissions = Permission.objects.filter( 32 | codename__contains='_localizr' 33 | ) 34 | 35 | self.user = User.objects.create_user( 36 | username=username, email=email, password=password) 37 | self.user.user_permissions.set(list(localizr_permissions)) 38 | self.client.login(username=username, password=password) 39 | 40 | def test_create_locale(self): 41 | 42 | locale_en = Locale(name='English', code='en',) 43 | locale_en.save() 44 | self.assertEqual(locale_en.code, 'en') 45 | 46 | def test_create_app(self): 47 | 48 | app = AppInfo(name='DemoApp', slug='demo-app',) 49 | app.save() 50 | self.assertEqual(app.slug, 'demo-app') 51 | 52 | def test_create_keystring(self): 53 | 54 | key_string = KeyString(key='NEXT',) 55 | key_string.save() 56 | 57 | self.assertEqual(key_string.key, 'NEXT') 58 | 59 | def test_create_localized_strings_of_keystring(self): 60 | 61 | key_string = KeyString(key='NEXT',) 62 | key_string.save() 63 | 64 | locale = Locale(name='English', code='en',) 65 | locale.save() 66 | value = 'Next' 67 | locstring = LocalizedString( 68 | key_string=key_string, locale=locale, value=value) 69 | locstring.save() 70 | 71 | locale = Locale(name='Japanese', code='ja',) 72 | locale.save() 73 | value = '次' 74 | locstring = LocalizedString( 75 | key_string=key_string, locale=locale, value=value) 76 | locstring.save() 77 | 78 | locale = Locale(name='Chinese', code='zh',) 79 | locale.save() 80 | value = '下一个' 81 | locstring = LocalizedString( 82 | key_string=key_string, locale=locale, value=value) 83 | locstring.save() 84 | 85 | values = list(map(lambda x: x.value, list(key_string.values.all()))) 86 | self.assertEqual(key_string.key, 'NEXT') 87 | self.assertEqual(len(values), 3) 88 | self.assertEqual('Next' in values, True) 89 | self.assertEqual('次' in values, True) 90 | self.assertEqual('下一个' in values, True) 91 | -------------------------------------------------------------------------------- /templates/rest_framework/login_base.html: -------------------------------------------------------------------------------- 1 | {% extends "rest_framework/base.html" %} 2 | {% load staticfiles %} 3 | {% load rest_framework %} 4 | 5 | {% block title %} Localizr {% endblock %} 6 | 7 | {% block body %} 8 | 9 |
10 |
11 |
12 |
13 |
14 | {% block branding %}

Login

{% endblock %} 15 |
16 |
17 | 18 |
19 |
20 |
21 | {% csrf_token %} 22 | 23 | 24 |
25 |
26 | 27 | 32 | {% if form.username.errors %} 33 |

34 | {{ form.username.errors|striptags }} 35 |

36 | {% endif %} 37 |
38 |
39 | 40 |
41 |
42 | 43 | 44 | {% if form.password.errors %} 45 |

46 | {{ form.password.errors|striptags }} 47 |

48 | {% endif %} 49 |
50 |
51 | 52 | {% if form.non_field_errors %} 53 | {% for error in form.non_field_errors %} 54 |
{{ error }}
55 | {% endfor %} 56 | {% endif %} 57 | 58 |
59 | 60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | 68 | {% endblock %} -------------------------------------------------------------------------------- /apps/Localizr/migrations/0005_auto_20171203_1703.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0rc1 on 2017-12-03 17:03 2 | 3 | import apps.Localizr.models 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ('Localizr', '0004_auto_20171129_0938'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Snapshot', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, 21 | primary_key=True, serialize=False, verbose_name='ID')), 22 | ('created', models.DateTimeField(auto_now=True)), 23 | ('modified', models.DateTimeField(auto_now_add=True)), 24 | ('key', models.CharField(max_length=36)), 25 | ('app_slug', models.CharField(max_length=30)), 26 | ('format', models.CharField(max_length=10)), 27 | ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, 28 | related_name='snapshot_creators', to=settings.AUTH_USER_MODEL)), 29 | ('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, 30 | related_name='snapshot_modifiers', to=settings.AUTH_USER_MODEL)), 31 | ], 32 | options={ 33 | 'verbose_name': 'Snapshot', 34 | 'verbose_name_plural': 'Snapshots', 35 | }, 36 | ), 37 | migrations.CreateModel( 38 | name='SnapshotFile', 39 | fields=[ 40 | ('id', models.AutoField(auto_created=True, 41 | primary_key=True, serialize=False, verbose_name='ID')), 42 | ('created', models.DateTimeField(auto_now=True)), 43 | ('modified', models.DateTimeField(auto_now_add=True)), 44 | ('locale_code', models.CharField(max_length=10)), 45 | ('file', models.FileField( 46 | upload_to=apps.Localizr.models.snapshot_folder)), 47 | ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, 48 | related_name='snapshotfile_creators', to=settings.AUTH_USER_MODEL)), 49 | ('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, 50 | related_name='snapshotfile_modifiers', to=settings.AUTH_USER_MODEL)), 51 | ('snapshot', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 52 | related_name='snapshots', to='Localizr.Snapshot')), 53 | ], 54 | options={ 55 | 'verbose_name': 'SnapshotFile', 56 | 'verbose_name_plural': 'SnapshotFiles', 57 | }, 58 | ), 59 | migrations.AlterUniqueTogether( 60 | name='snapshotfile', 61 | unique_together={('snapshot', 'locale_code')}, 62 | ), 63 | migrations.AlterUniqueTogether( 64 | name='snapshot', 65 | unique_together={('key', 'app_slug', 'format')}, 66 | ), 67 | ] 68 | -------------------------------------------------------------------------------- /apps/Localizr/migrations/0008_auto_20180123_1246.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0rc1 on 2018-01-23 03:46 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('Localizr', '0007_auto_20171204_2107'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='appinfo', 15 | name='created', 16 | field=models.DateTimeField(auto_now_add=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='appinfo', 20 | name='modified', 21 | field=models.DateTimeField(auto_now=True), 22 | ), 23 | migrations.AlterField( 24 | model_name='appinfokeystring', 25 | name='created', 26 | field=models.DateTimeField(auto_now_add=True), 27 | ), 28 | migrations.AlterField( 29 | model_name='appinfokeystring', 30 | name='modified', 31 | field=models.DateTimeField(auto_now=True), 32 | ), 33 | migrations.AlterField( 34 | model_name='appuser', 35 | name='created', 36 | field=models.DateTimeField(auto_now_add=True), 37 | ), 38 | migrations.AlterField( 39 | model_name='appuser', 40 | name='modified', 41 | field=models.DateTimeField(auto_now=True), 42 | ), 43 | migrations.AlterField( 44 | model_name='appusergroup', 45 | name='created', 46 | field=models.DateTimeField(auto_now_add=True), 47 | ), 48 | migrations.AlterField( 49 | model_name='appusergroup', 50 | name='modified', 51 | field=models.DateTimeField(auto_now=True), 52 | ), 53 | migrations.AlterField( 54 | model_name='keystring', 55 | name='created', 56 | field=models.DateTimeField(auto_now_add=True), 57 | ), 58 | migrations.AlterField( 59 | model_name='keystring', 60 | name='modified', 61 | field=models.DateTimeField(auto_now=True), 62 | ), 63 | migrations.AlterField( 64 | model_name='locale', 65 | name='created', 66 | field=models.DateTimeField(auto_now_add=True), 67 | ), 68 | migrations.AlterField( 69 | model_name='locale', 70 | name='modified', 71 | field=models.DateTimeField(auto_now=True), 72 | ), 73 | migrations.AlterField( 74 | model_name='localizedstring', 75 | name='created', 76 | field=models.DateTimeField(auto_now_add=True), 77 | ), 78 | migrations.AlterField( 79 | model_name='localizedstring', 80 | name='modified', 81 | field=models.DateTimeField(auto_now=True), 82 | ), 83 | migrations.AlterField( 84 | model_name='snapshot', 85 | name='created', 86 | field=models.DateTimeField(auto_now_add=True), 87 | ), 88 | migrations.AlterField( 89 | model_name='snapshot', 90 | name='modified', 91 | field=models.DateTimeField(auto_now=True), 92 | ), 93 | migrations.AlterField( 94 | model_name='snapshotfile', 95 | name='created', 96 | field=models.DateTimeField(auto_now_add=True), 97 | ), 98 | migrations.AlterField( 99 | model_name='snapshotfile', 100 | name='modified', 101 | field=models.DateTimeField(auto_now=True), 102 | ), 103 | ] 104 | -------------------------------------------------------------------------------- /apps/Localizr/migrations/0002_auto_20171123_0213.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0b1 on 2017-11-23 02:13 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('Localizr', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='appinfo', 17 | name='created_by', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, 19 | related_name='appinfo_creators', to=settings.AUTH_USER_MODEL), 20 | ), 21 | migrations.AlterField( 22 | model_name='appinfo', 23 | name='modified_by', 24 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, 25 | related_name='appinfo_modifiers', to=settings.AUTH_USER_MODEL), 26 | ), 27 | migrations.AlterField( 28 | model_name='appinfo', 29 | name='slug', 30 | field=models.SlugField(max_length=30), 31 | ), 32 | migrations.AlterField( 33 | model_name='appinfokeystring', 34 | name='created_by', 35 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, 36 | related_name='appinfokeystring_creators', to=settings.AUTH_USER_MODEL), 37 | ), 38 | migrations.AlterField( 39 | model_name='appinfokeystring', 40 | name='modified_by', 41 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, 42 | related_name='appinfokeystring_modifiers', to=settings.AUTH_USER_MODEL), 43 | ), 44 | migrations.AlterField( 45 | model_name='keystring', 46 | name='created_by', 47 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, 48 | related_name='keystring_creators', to=settings.AUTH_USER_MODEL), 49 | ), 50 | migrations.AlterField( 51 | model_name='keystring', 52 | name='modified_by', 53 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, 54 | related_name='keystring_modifiers', to=settings.AUTH_USER_MODEL), 55 | ), 56 | migrations.AlterField( 57 | model_name='locale', 58 | name='created_by', 59 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, 60 | related_name='locale_creators', to=settings.AUTH_USER_MODEL), 61 | ), 62 | migrations.AlterField( 63 | model_name='locale', 64 | name='modified_by', 65 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, 66 | related_name='locale_modifiers', to=settings.AUTH_USER_MODEL), 67 | ), 68 | migrations.AlterField( 69 | model_name='localizedstring', 70 | name='created_by', 71 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, 72 | related_name='localizedstring_creators', to=settings.AUTH_USER_MODEL), 73 | ), 74 | migrations.AlterField( 75 | model_name='localizedstring', 76 | name='modified_by', 77 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, 78 | related_name='localizedstring_modifiers', to=settings.AUTH_USER_MODEL), 79 | ), 80 | ] 81 | -------------------------------------------------------------------------------- /templates/admin/base.html: -------------------------------------------------------------------------------- 1 | {% load i18n static %} 2 | {% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %} 3 | 4 | 5 | {% block title %}{% endblock %} 6 | 7 | {% block extrastyle %}{% endblock %} 8 | {% if LANGUAGE_BIDI %}{% endif %} 9 | {% block extrahead %}{% endblock %} 10 | {% block responsive %} 11 | 12 | 13 | {% if LANGUAGE_BIDI %}{% endif %} 14 | {% endblock %} 15 | {% block blockbots %}{% endblock %} 16 | 17 | 18 | {% load i18n %} 19 | 20 | 22 | 23 | 24 |
25 | 26 | {% if not is_popup %} 27 | 28 | 59 | 60 | {% block breadcrumbs %} 61 | 65 | {% endblock %} 66 | {% endif %} 67 | 68 | {% block messages %} 69 | {% if messages %} 70 |
    {% for message in messages %} 71 | {{ message|capfirst }} 72 | {% endfor %}
73 | {% endif %} 74 | {% endblock messages %} 75 | 76 | 77 |
78 | {% block pretitle %}{% endblock %} 79 | {% block content_title %}{% if title %}

{{ title }}

{% endif %}{% endblock %} 80 | {% block content %} 81 | {% block object-tools %}{% endblock %} 82 | {{ content }} 83 | {% endblock %} 84 | {% block sidebar %}{% endblock %} 85 |
86 |
87 | 88 | 89 | {% block footer %}{% endblock %} 90 |
91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /templates/admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "admin/base_site.html" %} 3 | {% load i18n static localizr_extras %} 4 | 5 | {% block extrastyle %}{{ block.super }} 6 | 7 | {% endblock %} 8 | 9 | {% block coltype %}colMS{% endblock %} 10 | 11 | {% block bodyclass %}{{ block.super }} dashboard{% endblock %} 12 | 13 | {% block breadcrumbs %}{% endblock %} 14 | 15 | {% block content %} 16 |
17 | 18 | {% if app_list %} 19 | 20 | {% for app in app_list|sorted_apps %} 21 | 22 |
23 | 24 | 27 | {% for model in app.models %} 28 | 29 | 30 | {% if model.admin_url %} 31 | 32 | {% else %} 33 | 34 | {% endif %} 35 | 36 | {% if model.add_url %} 37 | 38 | {% else %} 39 | 40 | {% endif %} 41 | 42 | {% if model.admin_url %} 43 | 44 | {% else %} 45 | 46 | {% endif %} 47 | 48 | {% endfor %} 49 |
25 | {{ app.name }} 26 |
{{ model.name }}{{ model.name }}{% trans 'Add' %} {% trans 'Change' %} 
50 |
51 | {% endfor %} 52 | 53 | {% if request.get_full_path == '/' %} 54 |
55 | 56 | 59 | 60 | 61 | 63 | 64 | 65 | 67 | 68 | 69 | 71 | 72 | 73 | 75 | 76 | 77 | 79 |
57 | ABOUT LOCALIZR 58 |
Documentation 62 |
Contributors 66 |
Github Page 70 |
Contact the Developer 74 |
Submit an Issue or a Feature request 78 |
80 |
81 | {% endif %} 82 | 83 | {% else %} 84 |

{% trans "You don't have permission to edit anything." %}

85 | {% endif %} 86 |
87 | {% endblock %} 88 | 89 | {% block sidebar %} 90 | 119 | {% endblock %} 120 | -------------------------------------------------------------------------------- /apps/Localizr/renderers.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import OrderedDict 3 | from rest_framework import renderers 4 | from xml.sax.saxutils import escape 5 | 6 | 7 | class KeyStringBaseRenderer(renderers.BaseRenderer): 8 | 9 | def process_key(self, key): 10 | raise NotImplementedError('process_key() must be implemented.') 11 | 12 | def process_value(self, value): 13 | raise NotImplementedError('process_value() must be implemented.') 14 | 15 | def attribution_text(self, last_modified): 16 | return \ 17 | "Generated by Localizr. \n" \ 18 | "(https://github.com/michaelhenry/Localizr)\n" \ 19 | "%s\n" % (last_modified) 20 | 21 | 22 | def escape_quotes(text): 23 | 24 | s = text 25 | s = s.replace("'", "\\'") 26 | s = s.replace('"', '\\"') 27 | 28 | return s 29 | 30 | 31 | class KeyStringIOSRenderer(KeyStringBaseRenderer): 32 | 33 | media_type = 'text/plain' 34 | format = 'ios' 35 | charset = 'utf-8' 36 | key_name = 'key' 37 | value_name = 'value' 38 | 39 | def process_key(self, key): 40 | s = str(key) 41 | s = escape_quotes(s) 42 | return s 43 | 44 | def process_value(self, value): 45 | 46 | v = str(value) 47 | v = v.replace("%s", "%@") 48 | 49 | # search for existing android sequence pattern and convert it to ios. 50 | sequence_pattern_regex_for_android = re.compile( 51 | r'([%]\d+[$]\d+[s])', re.S) 52 | v = sequence_pattern_regex_for_android.sub( 53 | lambda m: re.sub(r'\d+s', '@', m.group()), v) 54 | v = escape_quotes(v) 55 | return v 56 | 57 | def render(self, data, media_type=None, renderer_context=None): 58 | 59 | response = renderer_context['response'] 60 | response_headers = OrderedDict(sorted(response.items())) 61 | last_modified = response_headers.get('X-Last-Modified', '') 62 | 63 | if isinstance(data, dict): 64 | return '\n'.join('\"%s\" = \"%s\";' % (key, val) for (key, val) in data.items()) 65 | elif isinstance(data, list): 66 | r = '/*%s*/\n\n' % self.attribution_text(last_modified) 67 | r += '\n'.join('\"%s\" = \"%s\";' % ( 68 | self.process_value(val[self.key_name]), 69 | self.process_value(val[self.value_name])) for (val) in data) 70 | return r.encode(self.charset) 71 | 72 | 73 | class KeyStringAndroidRenderer(KeyStringBaseRenderer): 74 | 75 | media_type = 'application/xml' 76 | format = 'android' 77 | charset = 'utf-8' 78 | key_name = 'key' 79 | value_name = 'value' 80 | 81 | def process_key(self, key): 82 | pattern = '[^0-9a-zA-Z]+' 83 | 84 | k = str(key).lower() 85 | k = re.sub(pattern, '_', k).lower() 86 | return k 87 | 88 | def process_value(self, value): 89 | 90 | v = str(value) 91 | v = v.replace("%@", "%s") 92 | 93 | # search for existing ios sequence pattern and convert it to android. 94 | sequence_pattern_regex_for_ios = re.compile(r'([%]\d[$][@])', re.S) 95 | v = sequence_pattern_regex_for_ios.sub( 96 | lambda m: m.group().replace('@', "1s"), v) 97 | v = escape(v) 98 | v = escape_quotes(v) 99 | return v 100 | 101 | def render(self, data, media_type=None, renderer_context=None): 102 | 103 | response = renderer_context['response'] 104 | response_headers = OrderedDict(sorted(response.items())) 105 | last_modified = response_headers.get('X-Last-Modified', '') 106 | 107 | r = '' 108 | if isinstance(data, dict): 109 | r += '\n'.join('\t%s' % (key, val) 110 | for (key, val) in data.items()) 111 | elif isinstance(data, list): 112 | r += '\n\n\n' % self.attribution_text(last_modified) 113 | r += '\n'.join('\t%s' % ( 114 | self.process_key(val[self.key_name]), 115 | self.process_value(val[self.value_name])) for (val) in data) 116 | r += '\n' 117 | r += '' 118 | return r.encode(self.charset) 119 | 120 | 121 | class ReactNativeRenderer(renderers.JSONRenderer): 122 | 123 | format = 'react-native' 124 | 125 | def render(self, data, accepted_media_type=None, renderer_context=None): 126 | kv_list = list(map(lambda x: dict(x), data)) 127 | d = {y['key']: y['value'] for y in kv_list} 128 | return super(ReactNativeRenderer, self).render( 129 | d, 130 | accepted_media_type=accepted_media_type, 131 | renderer_context=renderer_context) 132 | -------------------------------------------------------------------------------- /apps/Localizr/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | from rest_framework.views import APIView 3 | from rest_framework.response import Response 4 | from django.http import Http404 5 | 6 | from .serializers import ( 7 | LocaleSerializer, 8 | AppInfoSerializer, 9 | KeyStringSerializer, 10 | AppInfoKeyStringSerializer, 11 | KeyValueSerializer, 12 | LocalizedStringSerializer, 13 | ) 14 | from .models import ( 15 | Locale, 16 | AppInfo, 17 | KeyString, 18 | AppInfoKeyString, 19 | LocalizedString, 20 | get_localized_strings, 21 | ) 22 | 23 | from rest_framework.renderers import ( 24 | JSONRenderer, 25 | BrowsableAPIRenderer, 26 | ) 27 | 28 | from .renderers import ( 29 | KeyStringIOSRenderer, 30 | KeyStringAndroidRenderer, 31 | ReactNativeRenderer, 32 | ) 33 | 34 | 35 | class LocaleViewSet(viewsets.ModelViewSet): 36 | """ 37 | API endpoint that allows Locale to be viewed or edited. 38 | """ 39 | queryset = Locale.objects.all() 40 | serializer_class = LocaleSerializer 41 | 42 | 43 | class AppInfoViewSet(viewsets.ModelViewSet): 44 | """ 45 | API endpoint that allows AppInfo to be viewed or edited. 46 | """ 47 | queryset = AppInfo.objects.all() 48 | serializer_class = AppInfoSerializer 49 | 50 | 51 | class KeyStringViewSet(viewsets.ModelViewSet): 52 | """ 53 | API endpoint that allows KeyString to be viewed or edited. 54 | """ 55 | queryset = KeyString.objects.all() 56 | serializer_class = KeyStringSerializer 57 | 58 | 59 | class AppInfoKeyStringViewSet(viewsets.ModelViewSet): 60 | """ 61 | API endpoint that allows AppInfoKeyString to be viewed or edited. 62 | """ 63 | queryset = AppInfoKeyString.objects.all() 64 | serializer_class = AppInfoKeyStringSerializer 65 | 66 | 67 | class LocalizedStringViewSet(viewsets.ModelViewSet): 68 | 69 | queryset = LocalizedString.objects.all() 70 | serializer_class = LocalizedStringSerializer 71 | 72 | 73 | class KeyStringLocalizedView(APIView): 74 | """ 75 | API endpoint that allows to view the key-value list of an specified app and locale 76 | """ 77 | 78 | renderer_classes = ( 79 | BrowsableAPIRenderer, 80 | JSONRenderer, 81 | KeyStringIOSRenderer, 82 | KeyStringAndroidRenderer, 83 | ReactNativeRenderer, 84 | ) 85 | 86 | def get(self, request, *args, **kwargs): 87 | 88 | try: 89 | app_slug = kwargs['app_slug'] 90 | locale_code = kwargs.get('locale_code', 'en') 91 | app = AppInfo.objects.select_related().get(slug=app_slug) 92 | keyvalues_q = get_localized_strings( 93 | app=app, locale_code=locale_code) 94 | queryset = keyvalues_q.order_by('key',) 95 | 96 | serializer = KeyValueSerializer(queryset, many=True) 97 | response = Response(serializer.data) 98 | last_modified_q = keyvalues_q.order_by( 99 | '-modified').values_list('modified') 100 | 101 | if last_modified_q.exists(): 102 | x_last_modified = "%s" % (last_modified_q.first()[ 103 | 0].strftime("%Y-%m-%d %H:%M %Z")) 104 | response['X-Last-Modified'] = x_last_modified 105 | return response 106 | except AppInfo.DoesNotExist: 107 | raise Http404("Not exist.") 108 | 109 | 110 | locale_list_view = LocaleViewSet.as_view({ 111 | 'get': 'list', 112 | 'post': 'create', 113 | }) 114 | 115 | locale_detail_view = LocaleViewSet.as_view({ 116 | 'get': 'retrieve', 117 | 'put': 'update', 118 | 'patch': 'partial_update', 119 | 'delete': 'destroy', 120 | }) 121 | 122 | app_info_list_view = AppInfoViewSet.as_view({ 123 | 'get': 'list', 124 | 'post': 'create', 125 | }) 126 | 127 | app_info_detail_view = AppInfoViewSet.as_view({ 128 | 'get': 'retrieve', 129 | 'put': 'update', 130 | 'patch': 'partial_update', 131 | 'delete': 'destroy', 132 | }) 133 | 134 | 135 | key_string_list_view = KeyStringViewSet.as_view({ 136 | 'get': 'list', 137 | 'post': 'create', 138 | }) 139 | 140 | key_string_detail_view = KeyStringViewSet.as_view({ 141 | 'get': 'retrieve', 142 | 'put': 'update', 143 | 'patch': 'partial_update', 144 | 'delete': 'destroy', 145 | }) 146 | 147 | app_info_key_string_list_view = AppInfoKeyStringViewSet.as_view({ 148 | 'get': 'list', 149 | 'post': 'create', 150 | }) 151 | 152 | app_info_key_string_detail_view = AppInfoKeyStringViewSet.as_view({ 153 | 'get': 'retrieve', 154 | 'put': 'update', 155 | 'patch': 'partial_update', 156 | 'delete': 'destroy', 157 | }) 158 | 159 | localized_string_list_view = LocalizedStringViewSet.as_view({ 160 | 'get': 'list', 161 | 'post': 'create', 162 | }) 163 | 164 | localized_string_detail_view = LocalizedStringViewSet.as_view({ 165 | 'get': 'retrieve', 166 | 'put': 'update', 167 | 'patch': 'partial_update', 168 | 'delete': 'destroy', 169 | }) 170 | 171 | key_value_list_view = KeyStringLocalizedView.as_view() 172 | -------------------------------------------------------------------------------- /project/settings/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for project project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.11/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname( 17 | os.path.dirname(os.path.abspath(__file__)))) 18 | 19 | 20 | def ENV(key, default_value=None): 21 | return os.environ.get(key, default_value) 22 | 23 | 24 | # Quick-start development settings - unsuitable for production 25 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 26 | 27 | # SECURITY WARNING: keep the secret key used in production secret! 28 | SECRET_KEY = ENV("DJANGO_SECRET_KEY", "YOUR_SECRET_KEY") 29 | 30 | # SECURITY WARNING: don't run with debug turned on in production! 31 | DEBUG = True 32 | DEBUG_TOOLBAR = True 33 | INTERNAL_IPS = ('127.0.0.1',) 34 | ALLOWED_HOSTS = ENV("ALLOWED_HOSTS", '127.0.0.1').split(',') 35 | 36 | # Application definition 37 | 38 | INSTALLED_APPS = [ 39 | 'django.contrib.admin', 40 | 'django.contrib.auth', 41 | 'django.contrib.contenttypes', 42 | 'django.contrib.sessions', 43 | 'django.contrib.messages', 44 | 'django.contrib.staticfiles', 45 | 'import_export', 46 | 'apps.Localizr', 47 | 'rest_framework.authtoken', 48 | 'rest_framework', 49 | 'debug_toolbar', 50 | ] 51 | 52 | MIDDLEWARE = [ 53 | 'django.middleware.security.SecurityMiddleware', 54 | 'whitenoise.middleware.WhiteNoiseMiddleware', 55 | 'django.contrib.sessions.middleware.SessionMiddleware', 56 | 'django.middleware.common.CommonMiddleware', 57 | 'django.middleware.csrf.CsrfViewMiddleware', 58 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 59 | 'django.contrib.messages.middleware.MessageMiddleware', 60 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 61 | 'apps.Localizr.middlewares.LocalizrSnapshotMiddleWare', 62 | 'apps.Localizr.middlewares.CorsMiddleWare', 63 | 'debug_toolbar.middleware.DebugToolbarMiddleware', 64 | ] 65 | 66 | ROOT_URLCONF = 'project.urls' 67 | 68 | TEMPLATES = [ 69 | { 70 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 71 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], 72 | 'APP_DIRS': True, 73 | 'OPTIONS': { 74 | 'context_processors': [ 75 | 'django.template.context_processors.debug', 76 | 'django.template.context_processors.request', 77 | 'django.contrib.auth.context_processors.auth', 78 | 'django.contrib.messages.context_processors.messages', 79 | ], 80 | }, 81 | }, 82 | ] 83 | 84 | WSGI_APPLICATION = 'project.wsgi.application' 85 | 86 | 87 | # Database 88 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 89 | 90 | DATABASES = { 91 | 'default': { 92 | 'ENGINE': 'django.db.backends.sqlite3', 93 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 94 | } 95 | } 96 | 97 | DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' 98 | 99 | # Password validation 100 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 101 | 102 | AUTH_PASSWORD_VALIDATORS = [ 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 105 | }, 106 | { 107 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 108 | }, 109 | { 110 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 111 | }, 112 | { 113 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 114 | }, 115 | ] 116 | 117 | 118 | # Internationalization 119 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 120 | 121 | LANGUAGE_CODE = 'en-us' 122 | 123 | TIME_ZONE = 'Asia/Tokyo' 124 | 125 | USE_I18N = True 126 | 127 | USE_L10N = True 128 | 129 | USE_TZ = True 130 | 131 | 132 | # Static files (CSS, JavaScript, Images) 133 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 134 | 135 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 136 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 137 | 138 | STATIC_URL = '/static/' 139 | MEDIA_URL = '/media/' 140 | 141 | REST_FRAMEWORK = { 142 | 143 | 'DEFAULT_PERMISSION_CLASSES': ( 144 | 'project.permissions.IsAuthenticatedOrOptionsRequest', 145 | ), 146 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 147 | 'rest_framework.authentication.TokenAuthentication', 148 | 'rest_framework.authentication.SessionAuthentication', 149 | ), 150 | 'DEFAULT_PARSER_CLASSES': ( 151 | 'rest_framework.parsers.JSONParser', 152 | 'rest_framework.parsers.MultiPartParser', 153 | 'rest_framework.parsers.FormParser', 154 | ), 155 | 'DEFAULT_RENDERER_CLASSES': ( 156 | 'rest_framework.renderers.JSONRenderer', 157 | 'rest_framework.renderers.BrowsableAPIRenderer', 158 | 'rest_framework.renderers.AdminRenderer', 159 | ), 160 | } 161 | 162 | IMPORT_EXPORT_USE_TRANSACTIONS = True 163 | 164 | DEBUG_TOOLBAR_PANELS = [ 165 | 'debug_toolbar.panels.versions.VersionsPanel', 166 | 'debug_toolbar.panels.timer.TimerPanel', 167 | 'debug_toolbar.panels.settings.SettingsPanel', 168 | 'debug_toolbar.panels.headers.HeadersPanel', 169 | 'debug_toolbar.panels.request.RequestPanel', 170 | 'debug_toolbar.panels.sql.SQLPanel', 171 | 'debug_toolbar.panels.staticfiles.StaticFilesPanel', 172 | 'debug_toolbar.panels.templates.TemplatesPanel', 173 | 'debug_toolbar.panels.cache.CachePanel', 174 | 'debug_toolbar.panels.signals.SignalsPanel', 175 | 'debug_toolbar.panels.logging.LoggingPanel', 176 | 'debug_toolbar.panels.redirects.RedirectsPanel', 177 | ] 178 | 179 | ADMIN_DASHBOARD_LAYOUT = { 180 | 181 | "Localizr": { 182 | "sequence": 0, 183 | "models": [ 184 | "AppUser", 185 | "AppUserGroup", 186 | "Snapshot", 187 | "Locale", 188 | "AppInfo", 189 | "KeyString", 190 | "LocalizedString", 191 | "AppInfoKeyString", 192 | ] 193 | }, 194 | "auth": { 195 | 196 | }, 197 | "authtoken": { 198 | 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /apps/Localizr/migrations/0010_auto_20180523_1513.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-05-23 06:13 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('Localizr', '0009_localizedstring_status'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='appinfo', 17 | name='created_by', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, 19 | related_name='appinfo_creators', to=settings.AUTH_USER_MODEL), 20 | ), 21 | migrations.AlterField( 22 | model_name='appinfo', 23 | name='modified_by', 24 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, 25 | related_name='appinfo_modifiers', to=settings.AUTH_USER_MODEL), 26 | ), 27 | migrations.AlterField( 28 | model_name='appinfokeystring', 29 | name='created_by', 30 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, 31 | related_name='appinfokeystring_creators', to=settings.AUTH_USER_MODEL), 32 | ), 33 | migrations.AlterField( 34 | model_name='appinfokeystring', 35 | name='modified_by', 36 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, 37 | related_name='appinfokeystring_modifiers', to=settings.AUTH_USER_MODEL), 38 | ), 39 | migrations.AlterField( 40 | model_name='appuser', 41 | name='created_by', 42 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, 43 | related_name='appuser_creators', to=settings.AUTH_USER_MODEL), 44 | ), 45 | migrations.AlterField( 46 | model_name='appuser', 47 | name='modified_by', 48 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, 49 | related_name='appuser_modifiers', to=settings.AUTH_USER_MODEL), 50 | ), 51 | migrations.AlterField( 52 | model_name='appusergroup', 53 | name='created_by', 54 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, 55 | related_name='appusergroup_creators', to=settings.AUTH_USER_MODEL), 56 | ), 57 | migrations.AlterField( 58 | model_name='appusergroup', 59 | name='modified_by', 60 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, 61 | related_name='appusergroup_modifiers', to=settings.AUTH_USER_MODEL), 62 | ), 63 | migrations.AlterField( 64 | model_name='keystring', 65 | name='created_by', 66 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, 67 | related_name='keystring_creators', to=settings.AUTH_USER_MODEL), 68 | ), 69 | migrations.AlterField( 70 | model_name='keystring', 71 | name='modified_by', 72 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, 73 | related_name='keystring_modifiers', to=settings.AUTH_USER_MODEL), 74 | ), 75 | migrations.AlterField( 76 | model_name='locale', 77 | name='created_by', 78 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, 79 | related_name='locale_creators', to=settings.AUTH_USER_MODEL), 80 | ), 81 | migrations.AlterField( 82 | model_name='locale', 83 | name='modified_by', 84 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, 85 | related_name='locale_modifiers', to=settings.AUTH_USER_MODEL), 86 | ), 87 | migrations.AlterField( 88 | model_name='localizedstring', 89 | name='created_by', 90 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, 91 | related_name='localizedstring_creators', to=settings.AUTH_USER_MODEL), 92 | ), 93 | migrations.AlterField( 94 | model_name='localizedstring', 95 | name='modified_by', 96 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, 97 | related_name='localizedstring_modifiers', to=settings.AUTH_USER_MODEL), 98 | ), 99 | migrations.AlterField( 100 | model_name='snapshot', 101 | name='created_by', 102 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, 103 | related_name='snapshot_creators', to=settings.AUTH_USER_MODEL), 104 | ), 105 | migrations.AlterField( 106 | model_name='snapshot', 107 | name='modified_by', 108 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, 109 | related_name='snapshot_modifiers', to=settings.AUTH_USER_MODEL), 110 | ), 111 | migrations.AlterField( 112 | model_name='snapshotfile', 113 | name='created_by', 114 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, 115 | related_name='snapshotfile_creators', to=settings.AUTH_USER_MODEL), 116 | ), 117 | migrations.AlterField( 118 | model_name='snapshotfile', 119 | name='modified_by', 120 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, 121 | related_name='snapshotfile_modifiers', to=settings.AUTH_USER_MODEL), 122 | ), 123 | ] 124 | -------------------------------------------------------------------------------- /fastlane/actions/localizr.rb: -------------------------------------------------------------------------------- 1 | module Fastlane 2 | module Actions 3 | 4 | class LocalizrAction < Action 5 | 6 | def self.localizr_request(server_url, app_slug, locale_code, base_locale_code, auth_token, output_target_path, platform) 7 | 8 | locale_folder_path = "" 9 | localized_file = "" 10 | 11 | case platform 12 | when "ios" 13 | lproj_name = locale_code.strip 14 | if locale_code.downcase.strip == base_locale_code.downcase.strip 15 | lproj_name = 'Base' 16 | end 17 | locale_folder_path = "#{output_target_path}/#{lproj_name}.lproj" 18 | localized_file = "#{locale_folder_path}/Localizable.strings" 19 | when "android" 20 | strings_folder = "values-#{locale_code.strip}" 21 | if locale_code.downcase.strip == base_locale_code.downcase.strip 22 | strings_folder = "values" 23 | end 24 | locale_folder_path = "#{output_target_path}/#{strings_folder}" 25 | localized_file = "#{locale_folder_path}/strings.xml" 26 | end 27 | 28 | sh "mkdir -p #{locale_folder_path}" 29 | sh "curl --fail --silent -o #{localized_file} #{server_url}/app/#{app_slug}.#{locale_code}?format=#{platform} -H 'Authorization: Token #{auth_token}'" 30 | end 31 | 32 | def self.run(params) 33 | UI.message "Platform: #{params[:platform]}" 34 | UI.message "Server URL: #{params[:localizr_server]}" 35 | UI.message "Base Locale code: #{params[:base_locale_code]}" 36 | UI.message "Locale codes: #{params[:locale_codes]}" 37 | UI.message "Output Target path: #{params[:output_target_path]}" 38 | 39 | params[:locale_codes].split(",").each { |locale_code| 40 | 41 | localizr_request( 42 | params[:localizr_server], 43 | params[:localizr_app_slug], 44 | locale_code, 45 | params[:base_locale_code], 46 | params[:localizr_api_token], 47 | params[:output_target_path], 48 | params[:platform], 49 | ) 50 | } 51 | rescue 52 | UI.user_error!("An error occured on localizr. Please verify the configuration and then try again.") 53 | end 54 | 55 | ##################################################### 56 | # @!group Documentation 57 | ##################################################### 58 | 59 | def self.description 60 | "A Localization DSL for IOS and Android." 61 | end 62 | 63 | def self.details 64 | "Localizr is a DSL that handles and automates localization files. Basically we give limited access to the translators to let them input or upload different keystrings and developer will just fetch it on development or deployment only when if there is an update. This will lessen or prevent the mistake that developer made because he/she has no clue what are those words are and most of them (including me, but not all) are just copy-pasting those words (especially when it comes to chinese or japanese characters) from excel to the Localizable.strings via Xcode." 65 | end 66 | 67 | def self.available_options 68 | [ 69 | FastlaneCore::ConfigItem.new(key: :localizr_server, 70 | env_name: "FL_LOCALIZR_SERVER", 71 | description: "Server URL for LocalizrAction", 72 | verify_block: proc do |value| 73 | UI.user_error!("No Server URL for LocalizrAction given, pass using `localizr_server: 'http://localizr.youdomain.com'`") unless (value and not value.empty?) 74 | end), 75 | FastlaneCore::ConfigItem.new(key: :localizr_api_token, 76 | env_name: "FL_LOCALIZR_API_TOKEN", 77 | description: "API Token for LocalizrAction", 78 | verify_block: proc do |value| 79 | UI.user_error!("No API token for LocalizrAction given, pass using `localizr_api_token: 'token'`") unless (value and not value.empty?) 80 | end), 81 | FastlaneCore::ConfigItem.new(key: :localizr_app_slug, 82 | env_name: "FL_LOCALIZR_APP_SLUG", 83 | description: "App slug for LocalizrAction", 84 | verify_block: proc do |value| 85 | UI.user_error!("No App slug for LocalizrAction given, pass using `localizr_app_slug: 'my-app'`") unless (value and not value.empty?) 86 | end), 87 | FastlaneCore::ConfigItem.new(key: :base_locale_code, 88 | env_name: "FL_LOCALIZR_BASE_LOCALE_CODE", 89 | description: "Base locale code for LocalizrAction", 90 | default_value: 'en'), 91 | FastlaneCore::ConfigItem.new(key: :locale_codes, 92 | env_name: "FL_LOCALIZR_LOCALE_CODES", 93 | description: "Locale codes for LocalizrAction", 94 | verify_block: proc do |value| 95 | UI.user_error!("No Locale codes for LocalizrAction given, pass using `locale_codes: 'en,ja,pt,zh'") unless (value and not value.empty?) 96 | end), 97 | 98 | FastlaneCore::ConfigItem.new(key: :platform, 99 | env_name: "FL_LOCALIZR_PLATFORM", 100 | description: "Platform for LocalizrAction", 101 | verify_block: proc do |value| 102 | UI.user_error!("No platform for LocalizrAction given, pass using `platform: 'ios'") unless (value and not value.empty?) 103 | end), 104 | FastlaneCore::ConfigItem.new(key: :output_target_path, 105 | env_name: "FL_LOCALIZR_OUTPUT_TARGET_PATH", 106 | description: "Output target path for LocalizrAction", 107 | verify_block: proc do |value| 108 | UI.user_error!("No output_target_path for LocalizrAction given, pass using `output_target_path: 'Project'") unless (value and not value.empty?) 109 | end), 110 | ] 111 | end 112 | 113 | def self.authors 114 | ["@michaelhenry119","https://github.com/michaelhenry"] 115 | end 116 | 117 | def self.is_supported?(platform) 118 | platform == :ios || platform == :android 119 | end 120 | end 121 | end 122 | end -------------------------------------------------------------------------------- /apps/Localizr/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0b1 on 2017-10-29 16:03 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='AppInfo', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, 21 | primary_key=True, serialize=False, verbose_name='ID')), 22 | ('created', models.DateTimeField(auto_now=True)), 23 | ('modified', models.DateTimeField(auto_now_add=True)), 24 | ('name', models.CharField(max_length=30)), 25 | ('description', models.CharField( 26 | blank=True, max_length=200, null=True)), 27 | ('slug', models.CharField(max_length=30)), 28 | ], 29 | ), 30 | migrations.CreateModel( 31 | name='AppInfoKeyString', 32 | fields=[ 33 | ('id', models.AutoField(auto_created=True, 34 | primary_key=True, serialize=False, verbose_name='ID')), 35 | ('created', models.DateTimeField(auto_now=True)), 36 | ('modified', models.DateTimeField(auto_now_add=True)), 37 | ('app_info', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 38 | related_name='keys', to='Localizr.AppInfo')), 39 | ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 40 | related_name='appinfokeystring_creators', to=settings.AUTH_USER_MODEL)), 41 | ], 42 | ), 43 | migrations.CreateModel( 44 | name='KeyString', 45 | fields=[ 46 | ('id', models.AutoField(auto_created=True, 47 | primary_key=True, serialize=False, verbose_name='ID')), 48 | ('created', models.DateTimeField(auto_now=True)), 49 | ('modified', models.DateTimeField(auto_now_add=True)), 50 | ('key', models.CharField(max_length=100)), 51 | ('description', models.CharField( 52 | blank=True, max_length=200, null=True)), 53 | ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 54 | related_name='keystring_creators', to=settings.AUTH_USER_MODEL)), 55 | ('modified_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 56 | related_name='keystring_modifiers', to=settings.AUTH_USER_MODEL)), 57 | ], 58 | ), 59 | migrations.CreateModel( 60 | name='Locale', 61 | fields=[ 62 | ('id', models.AutoField(auto_created=True, 63 | primary_key=True, serialize=False, verbose_name='ID')), 64 | ('created', models.DateTimeField(auto_now=True)), 65 | ('modified', models.DateTimeField(auto_now_add=True)), 66 | ('name', models.CharField(max_length=30)), 67 | ('code', models.CharField(max_length=10)), 68 | ('description', models.CharField( 69 | blank=True, max_length=200, null=True)), 70 | ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 71 | related_name='locale_creators', to=settings.AUTH_USER_MODEL)), 72 | ('modified_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 73 | related_name='locale_modifiers', to=settings.AUTH_USER_MODEL)), 74 | ], 75 | ), 76 | migrations.CreateModel( 77 | name='LocalizedString', 78 | fields=[ 79 | ('id', models.AutoField(auto_created=True, 80 | primary_key=True, serialize=False, verbose_name='ID')), 81 | ('created', models.DateTimeField(auto_now=True)), 82 | ('modified', models.DateTimeField(auto_now_add=True)), 83 | ('value', models.CharField(max_length=1000)), 84 | ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 85 | related_name='localizedstring_creators', to=settings.AUTH_USER_MODEL)), 86 | ('key_string', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 87 | related_name='values', to='Localizr.KeyString')), 88 | ('locale', models.ForeignKey( 89 | on_delete=django.db.models.deletion.CASCADE, to='Localizr.Locale')), 90 | ('modified_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 91 | related_name='localizedstring_modifiers', to=settings.AUTH_USER_MODEL)), 92 | ], 93 | ), 94 | migrations.AddField( 95 | model_name='appinfokeystring', 96 | name='key_string', 97 | field=models.ForeignKey( 98 | on_delete=django.db.models.deletion.CASCADE, to='Localizr.KeyString'), 99 | ), 100 | migrations.AddField( 101 | model_name='appinfokeystring', 102 | name='modified_by', 103 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 104 | related_name='appinfokeystring_modifiers', to=settings.AUTH_USER_MODEL), 105 | ), 106 | migrations.AddField( 107 | model_name='appinfo', 108 | name='base_locale', 109 | field=models.ForeignKey( 110 | on_delete=django.db.models.deletion.CASCADE, to='Localizr.Locale'), 111 | ), 112 | migrations.AddField( 113 | model_name='appinfo', 114 | name='created_by', 115 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 116 | related_name='appinfo_creators', to=settings.AUTH_USER_MODEL), 117 | ), 118 | migrations.AddField( 119 | model_name='appinfo', 120 | name='modified_by', 121 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 122 | related_name='appinfo_modifiers', to=settings.AUTH_USER_MODEL), 123 | ), 124 | migrations.AlterUniqueTogether( 125 | name='localizedstring', 126 | unique_together={('locale', 'key_string')}, 127 | ), 128 | migrations.AlterUniqueTogether( 129 | name='locale', 130 | unique_together={('name', 'code')}, 131 | ), 132 | migrations.AlterUniqueTogether( 133 | name='keystring', 134 | unique_together={('key',)}, 135 | ), 136 | migrations.AlterUniqueTogether( 137 | name='appinfokeystring', 138 | unique_together={('app_info', 'key_string')}, 139 | ), 140 | migrations.AlterUniqueTogether( 141 | name='appinfo', 142 | unique_together={('slug',)}, 143 | ), 144 | ] 145 | -------------------------------------------------------------------------------- /apps/Localizr/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.db.models import ( 3 | Q, 4 | OuterRef, 5 | Subquery, 6 | ) 7 | from .models import ( 8 | Locale, 9 | AppInfo, 10 | KeyString, 11 | AppInfoKeyString, 12 | LocalizedString, 13 | Snapshot, 14 | SnapshotFile, 15 | AppUser, 16 | AppUserGroup, 17 | ) 18 | from .resources import ( 19 | AppInfoResource, 20 | LocaleResource, 21 | AppInfoKeyStringResource, 22 | LocalizedStringResource, 23 | ) 24 | from import_export.admin import ImportExportModelAdmin 25 | 26 | 27 | class UserInfoSavableAdmin(object): 28 | 29 | exclude = ('created_by', 'modified_by',) 30 | 31 | 32 | class BaseModelAdmin(UserInfoSavableAdmin, admin.ModelAdmin): 33 | 34 | pass 35 | 36 | 37 | class BaseTabularInlineModelAdmin(UserInfoSavableAdmin, admin.TabularInline): 38 | 39 | pass 40 | 41 | 42 | class LocaleAdmin(BaseModelAdmin, ImportExportModelAdmin): 43 | 44 | ordering = ('name', 'code',) 45 | search_fields = ('name', 'code',) 46 | list_display = ('name', 'code', 'description') 47 | resource_class = LocaleResource 48 | 49 | 50 | class AppInfoAdmin(BaseModelAdmin, ImportExportModelAdmin): 51 | 52 | fields = ('name', 'slug', 'description', 'base_locale',) 53 | ordering = ('name',) 54 | search_fields = ('name', 'description',) 55 | list_display = ('name', 'slug', 'description', 'base_locale') 56 | prepopulated_fields = {'slug': ('name',)} 57 | resource_class = AppInfoResource 58 | 59 | def get_queryset(self, request): 60 | qs = super(AppInfoAdmin, self).get_queryset(request) 61 | if request.user.is_superuser: 62 | return qs 63 | user_app_ids = AppInfo.objects.user_app_ids_query(request.user) 64 | return qs.filter(pk__in=user_app_ids) 65 | 66 | 67 | class LocalizedStringInline(BaseTabularInlineModelAdmin): 68 | 69 | model = LocalizedString 70 | extra = 1 71 | 72 | 73 | class KeyStringAdmin(BaseModelAdmin): 74 | ordering = ('key',) 75 | search_fields = ('key', 'description',) 76 | list_display = ('key', 'description', 'available_locales') 77 | inlines = [LocalizedStringInline, ] 78 | readonly_fields = ('modified_by', 'modified', 'created_by', 'created',) 79 | 80 | fieldsets = ( 81 | ('KeyString', { 82 | 'fields': ('key', 'description',) 83 | }), 84 | ('Metadata (Read-only)', { 85 | 'fields': ('created_by', 'created', 'modified_by', 'modified',) 86 | }), 87 | ) 88 | 89 | def save_model(self, request, obj, form, change): 90 | 91 | super(KeyStringAdmin, self).save_model(request, obj, form, change) 92 | 93 | if change: 94 | obj.modified_by = request.user 95 | else: 96 | obj.created_by = request.user 97 | obj.save() 98 | 99 | for v in obj.values.all(): 100 | if not v.created_by: 101 | v.created_by = request.user 102 | else: 103 | v.modified_by = request.user 104 | v.save() 105 | 106 | 107 | class AppInfoKeyStringListFilter(admin.SimpleListFilter): 108 | 109 | title = "Apps" 110 | parameter_name = "app_info" 111 | 112 | def lookups(self, request, model_admin): 113 | user_app_ids = AppInfo.objects.user_app_ids_query(request.user) 114 | q = AppInfo.objects.filter(pk__in=user_app_ids) 115 | return q.order_by('name').values_list('id', 'name') 116 | 117 | def queryset(self, request, queryset): 118 | 119 | if self.value(): 120 | return queryset.filter(app_info__pk=self.value()) 121 | else: 122 | user_app_ids = AppInfo.objects.user_app_ids_query(request.user) 123 | return queryset.filter(app_info__pk__in=user_app_ids) 124 | 125 | 126 | class AppInfoKeyStringAdmin(BaseModelAdmin, ImportExportModelAdmin): 127 | 128 | fields = ('app_info', 'key_string',) 129 | ordering = ('key_string__key', 'app_info',) 130 | search_fields = ('key_string__key',) 131 | list_display = ('key_string', 'value', 'app_info',) 132 | list_filter = (AppInfoKeyStringListFilter, ) 133 | autocomplete_fields = ['key_string', 'app_info'] 134 | resource_class = AppInfoKeyStringResource 135 | 136 | def get_queryset(self, request): 137 | qs = super(AppInfoKeyStringAdmin, self).get_queryset(request) 138 | 139 | base_value = LocalizedString.objects.filter( 140 | locale=OuterRef('app_info__base_locale'), 141 | key_string=OuterRef('key_string'), 142 | ).values_list('value', flat=True) 143 | 144 | if request.user.is_superuser: 145 | return qs.annotate(value=Subquery(base_value)) 146 | 147 | user_app_ids = AppInfo.objects.user_app_ids_query(request.user) 148 | 149 | return qs.filter( 150 | app_info__pk__in=user_app_ids 151 | ).annotate(value=Subquery(base_value)) 152 | 153 | 154 | class AppLocalizedStringListFilter(admin.SimpleListFilter): 155 | 156 | title = "Apps" 157 | parameter_name = "app_info" 158 | 159 | def lookups(self, request, model_admin): 160 | user_app_ids = AppInfo.objects.user_app_ids_query(request.user) 161 | q = AppInfo.objects.filter(pk__in=user_app_ids) 162 | return q.order_by('name').values_list('id', 'name') 163 | 164 | def queryset(self, request, queryset): 165 | 166 | if self.value(): 167 | keystring_ids = AppInfoKeyString.objects.filter( 168 | app_info__pk=self.value()).values_list( 169 | 'key_string__pk', flat=True) 170 | return queryset.filter(key_string__pk__in=keystring_ids) 171 | else: 172 | if request.user.is_superuser: 173 | return queryset 174 | user_app_ids = AppInfo.objects.user_app_ids_query(request.user) 175 | keystring_ids = AppInfoKeyString.objects.filter( 176 | app_info__pk__in=user_app_ids).values_list( 177 | 'key_string__pk', flat=True) 178 | return queryset.filter(key_string__pk__in=keystring_ids) 179 | 180 | 181 | def make_localized_string_published(modeladmin, request, queryset): 182 | queryset.update(status=LocalizedString.STATUS_PUBLISHED) 183 | 184 | 185 | make_localized_string_published.short_description = "Publish selected LocalizedString" 186 | 187 | 188 | class LocalizedStringAdmin(BaseModelAdmin, ImportExportModelAdmin): 189 | 190 | ordering = ('key_string__key', 'value', 'locale',) 191 | search_fields = ('key_string__key', 'value',) 192 | list_display = ('value', 'key_string', 'locale', 'status', ) 193 | list_filter = ('locale', AppLocalizedStringListFilter, 'status',) 194 | autocomplete_fields = ['key_string', 'locale'] 195 | resource_class = LocalizedStringResource 196 | readonly_fields = ('modified_by', 'modified', 'created_by', 'created',) 197 | actions = [make_localized_string_published] 198 | 199 | fieldsets = ( 200 | ('LocalizedString', { 201 | 'fields': ('key_string', 'locale', 'value', 'status',) 202 | }), 203 | ('Metadata (Read-only)', { 204 | 'fields': ('created_by', 'created', 'modified_by', 'modified',) 205 | }), 206 | ) 207 | 208 | def get_queryset(self, request): 209 | qs = super(LocalizedStringAdmin, self).get_queryset(request) 210 | if request.user.is_superuser: 211 | return qs 212 | 213 | user_app_ids = AppInfo.objects.user_app_ids_query(request.user) 214 | keystring_ids = AppInfoKeyString.objects.filter( 215 | app_info__pk__in=user_app_ids 216 | ).values_list('key_string__pk', flat=True) 217 | return qs.filter(Q(key_string__pk__in=keystring_ids)) 218 | 219 | def save_model(self, request, obj, form, change): 220 | 221 | if change: 222 | obj.modified_by = request.user 223 | else: 224 | obj.created_by = request.user 225 | obj.save() 226 | 227 | 228 | class SnapshotFileInline(BaseTabularInlineModelAdmin): 229 | 230 | model = SnapshotFile 231 | extra = 1 232 | 233 | 234 | class SnapshotAdmin(BaseModelAdmin): 235 | 236 | ordering = ('key',) 237 | search_fields = ('key', 'app_slug',) 238 | list_display = ('key', 'app_slug', 'format', 'created',) 239 | list_filter = ('app_slug', 'format',) 240 | 241 | 242 | class AppUserAdmin(BaseModelAdmin): 243 | 244 | ordering = ('user',) 245 | search_fields = ('app_info', 'user',) 246 | list_display = ('user', 'app_info',) 247 | list_filter = ('app_info',) 248 | autocomplete_fields = ['app_info', 'user'] 249 | 250 | 251 | class AppUserGroupAdmin(BaseModelAdmin): 252 | 253 | ordering = ('group',) 254 | search_fields = ('app_info', 'group',) 255 | list_display = ('group', 'app_info',) 256 | list_filter = ('app_info',) 257 | autocomplete_fields = ['app_info', 'group'] 258 | 259 | 260 | admin.site.register(Locale, LocaleAdmin) 261 | admin.site.register(AppInfo, AppInfoAdmin) 262 | admin.site.register(KeyString, KeyStringAdmin) 263 | admin.site.register(AppInfoKeyString, AppInfoKeyStringAdmin) 264 | admin.site.register(LocalizedString, LocalizedStringAdmin) 265 | admin.site.register(Snapshot, SnapshotAdmin) 266 | admin.site.register(AppUser, AppUserAdmin) 267 | admin.site.register(AppUserGroup, AppUserGroupAdmin) 268 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Localizr 2 | 3 | [![Build Status](https://travis-ci.org/michaelhenry/Localizr.svg?branch=master)](https://travis-ci.org/michaelhenry/Localizr) 4 | [![Twitter](https://img.shields.io/badge/twitter-@michaelhenry119-blue.svg?style=flat)](https://twitter.com/michaelhenry119) 5 | [![Docker](https://img.shields.io/badge/docker-michaelhenry119/localizr-red.svg?style=flat)](https://hub.docker.com/r/michaelhenry119/localizr/tags/) 6 | [![Version](https://img.shields.io/badge/version-1.3-yellow.svg?style=flat)](#) 7 | 8 | 9 | Localizr is a Tool that handles and automates localization files. Basically we give limited access to the translators to let them input or upload different keystrings and the developer will just fetch it on development or deployment only when if there is an update or changes. This will lessen or prevent the mistake that developer made because he/she has no clue what are those words are and most of them (including me, but not all) are just copy pasting those words (especially when it comes to chinese or japanese characters) from excel to the Localizable.strings via Xcode. 10 | 11 | 12 | ## Features 13 | - Multi-App support. reusable keys for different applications. 14 | - Android and IOS support. 15 | - Integrated with `Fastlane actions`. (`Fastlane actions localizr`) 16 | - Default fallback for missing localizations. 17 | - Export and import to different file format. 18 | - Easy deployment: [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/michaelhenry/localizr) 19 | - Dockerized: `docker pull michaelhenry119/localizr` 20 | - Static files hosted in AWS S3 (Optional) 21 | - Snapshots 22 | 23 | 24 | ## DEMO 25 | - http://localizr.iamkel.net 26 | You can access the demo pages with this credential: 27 | ``` 28 | username: demo 29 | password: localizr 30 | ``` 31 | 32 | 33 | ## IOS Client 34 | I have created a demo IOS App, You can pull it from [Localizr.swift](https://github.com/michaelhenry/Localizr.swift). 35 | 36 | 37 | ## For Developers 38 | ### Strings Generator 39 | ``` 40 | http://{your_server.com}/app/{app_slug}.{locale_code} 41 | ``` 42 | - http://localizr.iamkel.net/app/demo.en 43 | - http://localizr.iamkel.net/app/demo.ja 44 | 45 | ### Format 46 | #### iOS 47 | - http://localizr.iamkel.net/app/demo.ja?format=ios 48 | 49 | ### Android 50 | - http://localizr.iamkel.net/app/demo.ja?format=android 51 | 52 | ### Integrate to your Android or IOS Project? 53 | By using `Fastlane`. Currently `localizr` action is not officially available in `fastlane` repo, so you have to manually grab it from here [fastlane/actions](/fastlane/actions) and paste the `actions` folder directly to your project 's `fastlane` folder in order to make this available to your local. 54 | here is the shortcut: 55 | ```bash 56 | # from your workplace root folder: 57 | $ curl -o fastlane/actions/localizr.rb https://raw.githubusercontent.com/michaelhenry/Localizr/master/fastlane/actions/localizr.rb 58 | ``` 59 | 60 | ```bash 61 | $ fastlane actions localizr 62 | ``` 63 | ![fastlane actions localizr](docs/images/fastlane_actions_localizr.png) 64 | 65 | #### Sample configuration on IOS 66 | ```ruby 67 | desc "Submit build to TestFlight." 68 | lane :beta do 69 | increment_build_number 70 | # ... 71 | localizr( 72 | localizr_server: 'http://your_localizr_server', 73 | localizr_api_token: 'your-auth-token-from-admin-page', 74 | locale_codes: 'en,ja,pt,zh,es', 75 | localizr_app_slug: 'your-app-slug', 76 | output_target_path: 'ExampleApp', 77 | platform: 'ios', 78 | ) 79 | gym 80 | # ... 81 | end 82 | ``` 83 | 84 | #### Sample configuration on Android 85 | ```ruby 86 | lane :beta do 87 | localizr( 88 | localizr_server: 'http://your_localizr_server', 89 | localizr_api_token: 'your-auth-token-from-admin-page', 90 | locale_codes: 'en,ja,pt,zh,es', 91 | localizr_app_slug: 'your-app-slug', 92 | output_target_path: 'res' 93 | ), 94 | gradle( 95 | task: 'assemble', 96 | build_type: 'Release' 97 | ) 98 | # ... 99 | end 100 | ``` 101 | 102 | ### You can also use environment variables if you dont want to configure it from Fastfile: 103 | 104 | ```bash 105 | export FL_LOCALIZR_SERVER='http://your_localizr_server' 106 | export FL_LOCALIZR_API_TOKEN='your-auth-token-from-admin-page' 107 | export FL_LOCALIZR_APP_SLUG='your-app-slug' 108 | export FL_LOCALIZR_BASE_LOCALE_CODE='en' 109 | export FL_LOCALIZR_LOCALE_CODES='en,es,ja,zh,pt' 110 | export FL_LOCALIZR_PLATFORM='ios' 111 | export FL_LOCALIZR_OUTPUT_TARGET_PATH='ExampleApp' 112 | ``` 113 | 114 | Example: 115 | 116 | ```ruby 117 | desc "Submit build to TestFlight." 118 | lane :beta do 119 | increment_build_number 120 | # ... 121 | localizr 122 | gym 123 | # ... 124 | end 125 | ``` 126 | 127 | ### S3 Configuration 128 | This is optional, but you can enable this by providing valid information for the following in the environment variables. 129 | ```bash 130 | export AWS_ACCESS_KEY_ID='Your aws access key id' 131 | export AWS_SECRET_ACCESS_KEY='Your secret key' 132 | export AWS_STORAGE_BUCKET_NAME='Name of the bucket' 133 | ``` 134 | 135 | ## For Non-Developers, Translators or even Developers. 136 | ### How to use Localizr? 137 | 138 | 1. Create different `Locales` set the `name` and the `code`. 139 | 2. Create an `App` and set the `base_locale` if you want to have a fallback for missing `localized strings`. 140 | 3. Create Different `Keys`. 141 | 4. Match the `Keys` with the `App` so you can re-use the keys to other apps too. 142 | 5. Finally, populate the `localized strings` . 143 | 144 | 145 | #### Does it look difficult? 146 | ### Then try to use the importer (csv, xls, xlsx, tsv, json, yaml). 147 | You can find the sample csv files in the [sample_data](/sample_data) folder. 148 | 149 | 1. Import the `Locales.csv` to `Locales` section. 150 | 2. Import the `Apps.csv` to `Apps` section. 151 | 3. Import the `App's Keys.csv` to `App 's Keys` section. 152 | 4. Import the `Localized String.csv` to `Localized String` section. 153 | 154 | ![import admin import](docs/images/admin_localized_strings_import.png) 155 | 156 | ![import button](docs/images/admin_import.png) 157 | 158 | ![import change diff](docs/images/admin_import_compare.png) 159 | 160 | 161 | ### How about exporting? 162 | Just find the `EXPORT` button, select the `format` and that's it. 163 | 164 | ## Deployment 165 | ### Using Heroku 166 | Just click this button >++> [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/michaelhenry/localizr) 167 | 168 | If you're using heroku's free dyno and the waking time is longer than expected, you disable the auto migration option by setting the environment variable: 169 | 170 | ``` 171 | LOCALIZR_DISABLE_AUTO_MIGRATION=1 172 | ``` 173 | 174 | ### Using Docker 175 | sample config of `docker-compose.yml` 176 | ```yml 177 | version: '3' 178 | 179 | services: 180 | db: 181 | image: postgres:9.4 182 | volumes: 183 | - pg-data:/var/lib/postgresql/data 184 | ports: 185 | - "5432:5432" 186 | environment: 187 | - POSTGRES_PASSWORD=your_db_password 188 | 189 | localizr: 190 | image: michaelhenry119/localizr:latest 191 | container_name: localizr 192 | ports: 193 | - "80:8001" 194 | environment: 195 | # Reference: postgres://USER:PASSWORD@HOST:PORT/NAME, this example is using the default postgres database. 196 | - DATABASE_URL=postgres://postgres:your_db_password@db:5432/postgres 197 | # You have to define your host name here to prevent any random attacks. 198 | - ALLOWED_HOSTS=0.0.0.0,localizr.domain.com,or_any_domain 199 | # This is optional, you can assign a default then change it later from the admin page. 200 | # Or you can do it programatically after you mount the image. 201 | - ADMIN_USERNAME=admin 202 | - ADMIN_PASSWORD=change_me_later 203 | - ADMIN_EMAIL=your_email@email.com 204 | depends_on: 205 | - db 206 | volumes: 207 | pg-data: 208 | ``` 209 | 210 | ### Local setup (via virtualenv) 211 | 212 | Install virtualenv 213 | ```bash 214 | $ pip install virtualenv 215 | ``` 216 | 217 | Create a virtual environment `venv` 218 | ```bash 219 | $ virtualenv venv 220 | ``` 221 | 222 | Activate the virtual environment 223 | ```bash 224 | $ source venv/bin/activate 225 | ``` 226 | 227 | Install the dependencies 228 | ```bash 229 | $ pip install -r requirements_local.txt 230 | ``` 231 | 232 | Migrate to create a local sqlite database 233 | ```bash 234 | $ python manage.py migrate --settings=project.settings 235 | ``` 236 | 237 | Create a super user (login account) 238 | ```bash 239 | $ python manage.py createsuperuser --settings=project.settings 240 | ``` 241 | 242 | Run the local server 243 | ```bash 244 | $ python manage.py runserver --settings=project.settings 245 | ``` 246 | 247 | open [http://127.0.0.1:8000/](http://127.0.0.1:8000/) from your browser and use the login credentials you provided. 248 | 249 | ## Snapshot 250 | Snapshot is supported by passing a query param `?snapshot=your_any_key_or_build_number` to the localizedkeystrings request. 251 | 252 | 253 | ## Recommendation and Automation: 254 | With using `CI` and `Fastlane`, create a script or use `fastlane actions localizr` to download and update all the localization strings before `gym` method, So we can always make sure that all strings are updated. 255 | 256 | 257 | ## TODO: 258 | 259 | - [x] iOS format support 260 | - [x] Android format support 261 | - [x] Import/Export contents via CSV file 262 | - [x] CI 263 | - [x] Test cases 264 | - [x] Docker container support. 265 | - [x] Snapshot support. 266 | - [ ] Interactive UI. 267 | - [ ] Able to use google translate for some missing translations. 268 | 269 | 270 | ## Author 271 | 272 | Michael Henry Pantaleon, me@iamkel.net 273 | 274 | ## License 275 | 276 | Localizr is available under the MIT license. See the LICENSE file for more info. 277 | -------------------------------------------------------------------------------- /apps/Localizr/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models.functions import Coalesce 3 | from django.db.models import Q, F, OuterRef, Subquery 4 | from django.contrib.auth.models import Group 5 | from django.conf import settings 6 | 7 | 8 | class UserInfoSavableModel(models.Model): 9 | 10 | created = models.DateTimeField(auto_now_add=True) 11 | modified = models.DateTimeField(auto_now=True) 12 | 13 | created_by = models.ForeignKey( 14 | settings.AUTH_USER_MODEL, 15 | related_name='%(class)s_creators', 16 | on_delete=models.SET_NULL, 17 | null=True, 18 | blank=True, 19 | ) 20 | 21 | modified_by = models.ForeignKey( 22 | settings.AUTH_USER_MODEL, 23 | related_name='%(class)s_modifiers', 24 | on_delete=models.SET_NULL, 25 | null=True, 26 | blank=True, 27 | ) 28 | 29 | class Meta: 30 | abstract = True 31 | 32 | 33 | class HasStatusFlag(models.Model): 34 | 35 | STATUS_PUBLISHED = 0 36 | STATUS_PENDING = 1 37 | STATUS_DRAFT = 3 38 | 39 | STATUS_TYPE = ( 40 | (STATUS_PUBLISHED, 'Published'), 41 | (STATUS_PENDING, 'Pending'), 42 | (STATUS_DRAFT, 'Draft'), 43 | ) 44 | 45 | status = models.IntegerField(choices=STATUS_TYPE, default=STATUS_DRAFT) 46 | 47 | class Meta: 48 | abstract = True 49 | 50 | 51 | class Locale(UserInfoSavableModel): 52 | 53 | name = models.CharField(max_length=30) 54 | code = models.CharField(max_length=10, unique=True) 55 | description = models.CharField( 56 | max_length=200, 57 | blank=True, 58 | null=True) 59 | 60 | def __str__(self): 61 | return "%s" % self.name 62 | 63 | def __unicode__(self): 64 | return '%s' % self.name 65 | 66 | class Meta(object): 67 | verbose_name = 'Locale' 68 | verbose_name_plural = 'Locales' 69 | 70 | 71 | class AppInfo(UserInfoSavableModel): 72 | 73 | name = models.CharField(max_length=30) 74 | description = models.CharField( 75 | max_length=200, 76 | blank=True, 77 | null=True) 78 | slug = models.SlugField(max_length=30) 79 | base_locale = models.ForeignKey(Locale, 80 | on_delete=models.CASCADE, 81 | blank=True, 82 | null=True,) 83 | 84 | def __str__(self): 85 | return "%s" % self.name 86 | 87 | def __unicode__(self): 88 | return '%s' % self.name 89 | 90 | class Meta(object): 91 | unique_together = ('slug',) 92 | verbose_name = 'App' 93 | verbose_name_plural = 'Apps' 94 | 95 | 96 | class KeyString(UserInfoSavableModel): 97 | 98 | key = models.CharField(max_length=100) 99 | description = models.CharField( 100 | max_length=200, 101 | blank=True, 102 | null=True) 103 | 104 | def __str__(self): 105 | return "%s" % self.key 106 | 107 | def __unicode__(self): 108 | return '%s' % self.key 109 | 110 | class Meta(object): 111 | unique_together = ('key',) 112 | verbose_name = 'Key' 113 | verbose_name_plural = 'Keys' 114 | 115 | def available_locales(self): 116 | return ", ".join(list(self.values.values_list( 117 | 'locale__code', flat=True).order_by('locale__code'))) 118 | 119 | 120 | class AppInfoKeyStringQuerySet(models.QuerySet): 121 | 122 | def filter_by_locale_code(self, locale_code): 123 | 124 | base_value = LocalizedString.objects.filter( 125 | locale=OuterRef('app_info__base_locale'), 126 | status=LocalizedString.STATUS_PUBLISHED, 127 | ).filter( 128 | key_string=OuterRef('key_string'), 129 | ).values_list('value', flat=True) 130 | 131 | value = LocalizedString.objects.filter( 132 | locale__code=locale_code, 133 | status=LocalizedString.STATUS_PUBLISHED, 134 | ).filter( 135 | key_string=OuterRef('key_string'), 136 | ).values_list('value', flat=True) 137 | 138 | return self\ 139 | .annotate( 140 | key=F('key_string__key'), 141 | value=Coalesce( 142 | Subquery(value), 143 | Subquery(base_value)))\ 144 | .exclude(value=None)\ 145 | .values_list('key', 'value', 'modified',) 146 | 147 | 148 | class AppInfoKeyStringManager(models.Manager): 149 | 150 | def get_queryset(self): 151 | return AppInfoKeyStringQuerySet(self.model) 152 | 153 | 154 | class AppInfoKeyString(UserInfoSavableModel): 155 | 156 | key_string = models.ForeignKey(KeyString, on_delete=models.CASCADE) 157 | app_info = models.ForeignKey(AppInfo, 158 | related_name='keys', 159 | on_delete=models.CASCADE) 160 | 161 | objects = AppInfoKeyStringManager() 162 | 163 | # just placeholder 164 | def value(self): 165 | return "" 166 | 167 | def __str__(self): 168 | return "%s" % self.key_string 169 | 170 | def __unicode__(self): 171 | return '%s' % self.key_string 172 | 173 | class Meta(object): 174 | unique_together = ('app_info', 'key_string',) 175 | verbose_name = 'App \'s Key' 176 | verbose_name_plural = 'App \'s Keys' 177 | 178 | 179 | class LocalizedString(UserInfoSavableModel, HasStatusFlag): 180 | 181 | locale = models.ForeignKey(Locale, on_delete=models.CASCADE) 182 | value = models.TextField() 183 | key_string = models.ForeignKey(KeyString, 184 | related_name='values', 185 | on_delete=models.CASCADE) 186 | 187 | def __str__(self): 188 | return "%s" % self.value 189 | 190 | def __unicode__(self): 191 | return '%s' % self.value 192 | 193 | class Meta(object): 194 | unique_together = ('locale', 'key_string',) 195 | verbose_name = 'Localized String' 196 | verbose_name_plural = 'Localized Strings' 197 | 198 | 199 | class Snapshot(UserInfoSavableModel): 200 | 201 | key = models.CharField(max_length=36,) 202 | app_slug = models.CharField(max_length=30) 203 | format = models.CharField(max_length=10) 204 | 205 | def __str__(self): 206 | return "%s" % (self.key) 207 | 208 | def __unicode__(self): 209 | return '%s' % (self.key) 210 | 211 | class Meta(object): 212 | unique_together = ('key', 'app_slug', 'format',) 213 | verbose_name = 'Snapshot' 214 | verbose_name_plural = 'Snapshots' 215 | 216 | 217 | def snapshot_folder(instance, filename): 218 | 219 | return "%s/%s/snapshots/%s/%s" % ( 220 | instance.snapshot.app_slug, 221 | instance.snapshot.format, 222 | instance.snapshot.key, 223 | filename 224 | ) 225 | 226 | 227 | class SnapshotFile(UserInfoSavableModel): 228 | 229 | snapshot = models.ForeignKey(Snapshot, 230 | related_name='snapshots', 231 | on_delete=models.CASCADE) 232 | locale_code = models.CharField(max_length=10) 233 | file = models.FileField(upload_to=snapshot_folder) 234 | 235 | def __str__(self): 236 | return "%s" % ( 237 | self.file.name 238 | ) 239 | 240 | def __unicode__(self): 241 | return "%s" % ( 242 | self.file.name, 243 | ) 244 | 245 | class Meta(object): 246 | unique_together = ('snapshot', 'locale_code',) 247 | verbose_name = 'SnapshotFile' 248 | verbose_name_plural = 'SnapshotFiles' 249 | 250 | 251 | class AppUser(UserInfoSavableModel): 252 | 253 | app_info = models.ForeignKey(AppInfo, 254 | on_delete=models.CASCADE) 255 | user = models.ForeignKey( 256 | settings.AUTH_USER_MODEL, 257 | on_delete=models.CASCADE,) 258 | 259 | def __str__(self): 260 | return "%s|%s" % ( 261 | self.user.username, self.app_info.slug 262 | ) 263 | 264 | def __unicode__(self): 265 | return "%s|%s" % ( 266 | self.user.username, self.app_info.slug 267 | ) 268 | 269 | class Meta(object): 270 | unique_together = ('app_info', 'user',) 271 | verbose_name = 'User-App Permission' 272 | verbose_name_plural = 'User-App Permissions' 273 | 274 | 275 | class AppUserGroup(UserInfoSavableModel): 276 | 277 | app_info = models.ForeignKey(AppInfo, 278 | on_delete=models.CASCADE) 279 | group = models.ForeignKey(Group, 280 | on_delete=models.CASCADE,) 281 | 282 | def __str__(self): 283 | return "%s|%s" % ( 284 | self.group.name, self.app_info.slug 285 | ) 286 | 287 | def __unicode__(self): 288 | return "%s|%s" % ( 289 | self.group.name, self.app_info.slug 290 | ) 291 | 292 | class Meta(object): 293 | unique_together = ('app_info', 'group',) 294 | verbose_name = 'User-Group-App Permission' 295 | verbose_name_plural = 'User-Group-App Permissions' 296 | 297 | 298 | # Extension method for AppInfo.objects 299 | def user_app_ids_query(user): 300 | 301 | q = None 302 | if user.is_superuser: 303 | q = AppInfo.objects.all() 304 | else: 305 | user_app_ids = AppUser.objects.filter( 306 | user=user 307 | ).values_list('app_info__pk', flat=True) 308 | 309 | group_app_ids = AppUserGroup.objects.filter( 310 | group_id__in=user.groups.values_list('id', flat=True) 311 | ).values_list('app_info__pk', flat=True) 312 | 313 | q = AppInfo.objects.filter( 314 | Q(pk__in=group_app_ids) | Q(pk__in=user_app_ids)) 315 | return q.values_list('id') 316 | 317 | 318 | def get_localized_strings(app, locale_code): 319 | 320 | return AppInfoKeyString.objects\ 321 | .filter(app_info=app)\ 322 | .filter_by_locale_code(locale_code=locale_code) 323 | 324 | 325 | setattr(AppInfo.objects, "user_app_ids_query", user_app_ids_query) 326 | --------------------------------------------------------------------------------