diff --git a/CHANGES b/CHANGES index 1d9ad62..d51ae5a 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,10 @@ Version 0.7.2 ------------- * Fix for when settings imports unicode_literals for some reason (Issue #67) +* Fixed mess with app_id between pages (Issue #68, thanks @tsouvarev) +* Added Farsi translation. Thanks, @amiraliakbari +* Improved the permission system, allowing for more advanced permission mechanisms. Thanks, @codeinthehole and @tangentlabs +* Fixed the ordering of apps in the language selection screen. (Issue #73, thanks @tsouvarev, @kanu and everyone else involved in tracking this one down) Version 0.7.1 ------------- diff --git a/README.rst b/README.rst index fc2c3d4..242d956 100644 --- a/README.rst +++ b/README.rst @@ -62,6 +62,7 @@ Rosetta can be configured via the following parameters, to be defined in your pr * ``ROSETTA_POFILE_WRAP_WIDTH``: Sets the line-length of the edited PO file. Set this to ``0`` to mimic ``makemessage``'s ``--no-wrap`` option. Defaults to ``78``. * ``ROSETTA_STORAGE_CLASS``: See the note below on Storages. Defaults to ``rosetta.storage.CacheRosettaStorage`` * ``ROSETTA_ENABLE_REFLANG``: See the note below on Reference Language. Defaults to ``False``. +* ``ROSETTA_ACCESS_CONTROL_FUNCTION``: An alternative function that determines if a given user can access the translation views. This function receives a ``user`` as its argument, and returns a boolean specifying whether the passed user is allowed to use Rosetta or not. ******** Storages diff --git a/rosetta/access.py b/rosetta/access.py new file mode 100644 index 0000000..b3641bc --- /dev/null +++ b/rosetta/access.py @@ -0,0 +1,36 @@ +from django.conf import settings +from django.utils import importlib + + +def can_translate(user): + return get_access_control_function()(user) + + +def get_access_control_function(): + """ + Return a predicate for determining if a user can access the Rosetta views + """ + fn_path = getattr(settings, 'ROSETTA_ACCESS_CONTROL_FUNCTION', None) + if fn_path is None: + return is_superuser_staff_or_in_translators_group + # Dynamically load a permissions function + perm_module, perm_func = fn_path.rsplit('.', 1) + perm_module = importlib.import_module(perm_module) + return getattr(perm_module, perm_func) + + +# Default access control test +def is_superuser_staff_or_in_translators_group(user): + if not getattr(settings, 'ROSETTA_REQUIRES_AUTH', True): + return True + if not user.is_authenticated(): + return False + elif user.is_superuser and user.is_staff: + return True + else: + try: + from django.contrib.auth.models import Group + translators = Group.objects.get(name='translators') + return translators in user.groups.all() + except Group.DoesNotExist: + return False diff --git a/rosetta/conf/settings.py b/rosetta/conf/settings.py index af5894f..a7c68d8 100644 --- a/rosetta/conf/settings.py +++ b/rosetta/conf/settings.py @@ -16,6 +16,9 @@ MAIN_LANGUAGE = getattr(settings, 'ROSETTA_MAIN_LANGUAGE', None) MESSAGES_SOURCE_LANGUAGE_CODE = getattr(settings, 'ROSETTA_MESSAGES_SOURCE_LANGUAGE_CODE', 'en') MESSAGES_SOURCE_LANGUAGE_NAME = getattr(settings, 'ROSETTA_MESSAGES_SOURCE_LANGUAGE_NAME', 'English') +ACCESS_CONTROL_FUNCTION = getattr( + settings, 'ROSETTA_ACCESS_CONTROL_FUNCTION', None) + """ When running WSGI daemon mode, using mod_wsgi 2.0c5 or later, this setting diff --git a/rosetta/locale/fa/LC_MESSAGES/django.mo b/rosetta/locale/fa/LC_MESSAGES/django.mo new file mode 100644 index 0000000..246b1fc Binary files /dev/null and b/rosetta/locale/fa/LC_MESSAGES/django.mo differ diff --git a/rosetta/locale/fa/LC_MESSAGES/django.po b/rosetta/locale/fa/LC_MESSAGES/django.po new file mode 100644 index 0000000..a18c85d --- /dev/null +++ b/rosetta/locale/fa/LC_MESSAGES/django.po @@ -0,0 +1,206 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-04-01 18:36+0430\n" +"PO-Revision-Date: 2013-04-01 19:03+0430\n" +"Last-Translator: \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: \n" +"Plural-Forms: nplurals=1; plural=0\n" +"X-Translated-Using: django-rosetta 0.7.1\n" + +#: templates/rosetta/languages.html:5 templates/rosetta/languages.html.py:8 +msgid "Language selection" +msgstr "انتخاب زبان" + +#: templates/rosetta/languages.html:8 templates/rosetta/pofile.html:22 +msgid "Home" +msgstr "خانه" + +#: templates/rosetta/languages.html:9 +msgid "" +"Couldn't load the specified language file. This usually happens when using " +"the Encrypted Cookies Session Storage backend on Django 1.4 or " +"higher.
Setting ROSETTA_STORAGE_CLASS = " +"'rosetta.storage.CacheRosettaStorage' in your settings file should fix this." +msgstr "" +"بارگزاری کاتالوگ زبان مورد نظر ممکن نبود. این مشکل معمولا به خاطر استفاده از" +" Encrypted Cookies Session Storage backend در جنگو ۱.۴ یا بالاتر رخ " +"می‌دهد.
با افزودن خط زیر به فایل تنظیمات پروژه معمولا این مشکل را حل " +"می‌کند:
ROSETTA_STORAGE_CLASS = 'rosetta.storage.CacheRosettaStorage'" + +#: templates/rosetta/languages.html:15 +msgid "Filter" +msgstr "فیلتر" + +#: templates/rosetta/languages.html:16 +msgid "Project" +msgstr "پروژه" + +#: templates/rosetta/languages.html:17 +msgid "Third party" +msgstr "شخص ثالث" + +#: templates/rosetta/languages.html:19 templates/rosetta/pofile.html:39 +msgid "All" +msgstr "همه" + +#: templates/rosetta/languages.html:32 +msgid "Application" +msgstr "کارکرد" + +#: templates/rosetta/languages.html:33 +msgid "Progress" +msgstr "پیشرفت" + +#: templates/rosetta/languages.html:34 +msgid "Messages" +msgstr "پیام‌ها" + +#: templates/rosetta/languages.html:35 +msgid "Translated" +msgstr "ترجمه شده" + +#: templates/rosetta/languages.html:36 templates/rosetta/pofile.html:71 +msgid "Fuzzy" +msgstr "نیاز به بازبینی" + +#: templates/rosetta/languages.html:37 +msgid "Obsolete" +msgstr "کهنه" + +#: templates/rosetta/languages.html:38 +msgid "File" +msgstr "پرونده" + +#: templates/rosetta/languages.html:61 +msgid "Nothing to translate!" +msgstr "موردی برای ترجمه وجود ندارد!" + +#: templates/rosetta/languages.html:62 +msgid "" +"You haven't specified any languages in your settings file, or haven't yet " +"generated a batch of translation catalogs." +msgstr "" +"شما زبانی را در فایل تنظیمات تعیین نکردید، یا هنوز کاتالوگ‌های ترجمه را " +"ایجاد نکردید." + +#: templates/rosetta/languages.html:63 +#, python-format +msgid "" +"Please refer to Django's I18N " +"documentation for a guide on how to set up internationalization for your" +" project." +msgstr "" +"لطفا به مستندات جهانی‌سازی جنگو " +"documentation برای راهنما در مورد روش راه‌اندازی امکانات جهانی‌سازی برای" +" پروژه‌تان مراجعه کنید." + +#: templates/rosetta/pofile.html:9 +msgid "Pick another file" +msgstr "انتخاب یک فایل دیگر" + +#: templates/rosetta/pofile.html:10 +msgid "Download this catalog" +msgstr "دانلود این کاتالوگ" + +#: templates/rosetta/pofile.html:25 +#, python-format +msgid "Progress: %(percent_translated)s%%" +msgstr "درصد پیشرفت: %(percent_translated)s%%" + +#: templates/rosetta/pofile.html:27 +msgid "File is read-only: download the file when done editing!" +msgstr "" +"فایل ترجمه غیر قابل نوشتن است، پس از اتمام ترجمه فایل حاصل را دانلود نمایید!" + +#: templates/rosetta/pofile.html:28 +msgid "" +"Some items in your last translation block couldn't be saved: this usually " +"happens when the catalog file changes on disk after you last loaded it." +msgstr "" +"برخی موارد در آخرین دسته‌ی ترجمه قابل ذخیره نبود: این مشکل معمولا زمانی رخ " +"می‌دهد که فایل کاتالوگ روی دیسک پس از آخرین دفعه‌ی بارگزاری آن، تغییر کرده " +"باشد." + +#: templates/rosetta/pofile.html:32 +#, python-format +msgid "Translate into %(rosetta_i18n_lang_name)s" +msgstr "در حال ترجمه به %(rosetta_i18n_lang_name)s" + +#: templates/rosetta/pofile.html:35 +msgid "Display:" +msgstr "نمایش:" + +#: templates/rosetta/pofile.html:36 +msgid "Untranslated only" +msgstr "فقط ترجمه نشده‌ها" + +#: templates/rosetta/pofile.html:37 +msgid "Translated only" +msgstr "فقط ترجمه شده‌ها" + +#: templates/rosetta/pofile.html:38 +msgid "Fuzzy only" +msgstr "فقط موارد برای بازبینی" + +#: templates/rosetta/pofile.html:46 +msgid "Search" +msgstr "جستجو" + +#: templates/rosetta/pofile.html:48 +msgid "Go" +msgstr "برو" + +#: templates/rosetta/pofile.html:68 +msgid "Original" +msgstr "اصلی" + +#: templates/rosetta/pofile.html:72 +msgid "Occurrences(s)" +msgstr "موارد رخداد" + +#: templates/rosetta/pofile.html:86 templates/rosetta/pofile.html.py:89 +#: templates/rosetta/pofile.html:104 templates/rosetta/pofile.html.py:107 +msgid "Context hint" +msgstr "راهنمای محلی" + +#: templates/rosetta/pofile.html:114 +msgid "suggest" +msgstr "پیشنهاد" + +#: templates/rosetta/pofile.html:125 +#, python-format +msgid "%(more_count)s more" +msgid_plural "%(more_count)s more" +msgstr[0] "%(more_count)s مورد بیش‌تر" +msgstr[1] "%(more_count)s مورد بیش‌تر" + +#: templates/rosetta/pofile.html:137 +msgid "Save and translate next block" +msgstr "ذخیره و ترجمه‌ی دسته‌ی بعدی" + +#: templates/rosetta/pofile.html:141 +msgid "Skip to page:" +msgstr "پرش به صفحه:" + +#: templates/rosetta/pofile.html:154 +msgid "Displaying:" +msgstr "در حال نمایش:" + +#: templates/rosetta/pofile.html:158 +#, python-format +msgid "%(hits)s/%(message_number)s message" +msgid_plural "%(hits)s/%(message_number)s messages" +msgstr[0] "%(hits)s از %(message_number)s پیام" +msgstr[1] "%(hits)s از %(message_number)s پیام" diff --git a/rosetta/poutil.py b/rosetta/poutil.py index f22c6e9..cd63047 100644 --- a/rosetta/poutil.py +++ b/rosetta/poutil.py @@ -113,7 +113,7 @@ def find_pos(lang, project_apps=True, django_apps=False, third_party_apps=False) filename = os.path.join(dirname, fn) if os.path.isfile(filename): ret.add(os.path.abspath(filename)) - return list(ret) + return list(sorted(ret)) def pagination_range(first, last, current): diff --git a/rosetta/tests/__init__.py b/rosetta/tests/__init__.py index 2a96c6b..4a316b5 100644 --- a/rosetta/tests/__init__.py +++ b/rosetta/tests/__init__.py @@ -537,3 +537,22 @@ class RosettaTestCase(TestCase): r = self.client.post(reverse('rosetta-home'), dict(m_e48f149a8b2e8baa81b816c0edf93890='Hello, world', _next='_next')) r = self.client.get(reverse('rosetta-home')) self.assertTrue('Progress: 25.00%' in str(r.content)) + + def test_24_replace_access_control(self): + # Test default access control allows access + url = reverse('rosetta-home') + response = self.client.get(url) + self.assertEqual(200, response.status_code) + + # Now replace access control, and check we get redirected + settings.ROSETTA_ACCESS_CONTROL_FUNCTION = 'rosetta.tests.no_access' + response = self.client.get(url) + self.assertEqual(302, response.status_code) + + # Restore setting to default + settings.ROSETTA_ACCESS_CONTROL_FUNCTION = None + + +# Stubbed access control function +def no_access(user): + return False diff --git a/rosetta/views.py b/rosetta/views.py index 26977d5..35b3e0b 100644 --- a/rosetta/views.py +++ b/rosetta/views.py @@ -13,6 +13,7 @@ from rosetta.polib import pofile from rosetta.poutil import find_pos, pagination_range, timestamp_with_timezone from rosetta.signals import entry_changed, post_save from rosetta.storage import get_storage +from rosetta.access import can_translate import re import rosetta import unicodedata @@ -269,6 +270,7 @@ def home(request): ADMIN_MEDIA_PREFIX=ADMIN_MEDIA_PREFIX, ADMIN_IMAGE_DIR=ADMIN_IMAGE_DIR, ENABLE_REFLANG=rosetta_settings.ENABLE_REFLANG, + LANGUAGES=LANGUAGES, rosetta_settings=rosetta_settings, rosetta_i18n_lang_name=_(storage.get('rosetta_i18n_lang_name')), rosetta_i18n_lang_code=rosetta_i18n_lang_code, @@ -395,7 +397,7 @@ def lang_sel(request, langid, idx): third_party_apps = rosetta_i18n_catalog_filter in ('all', 'third-party') django_apps = rosetta_i18n_catalog_filter in ('all', 'django') project_apps = rosetta_i18n_catalog_filter in ('all', 'project') - file_ = find_pos(langid, project_apps=project_apps, django_apps=django_apps, third_party_apps=third_party_apps)[int(idx)] + file_ = sorted(find_pos(langid, project_apps=project_apps, django_apps=django_apps, third_party_apps=third_party_apps), key=get_app_name)[int(idx)] storage.set('rosetta_i18n_lang_code', langid) storage.set('rosetta_i18n_lang_name', six.text_type([l[1] for l in settings.LANGUAGES if l[0] == langid][0])) @@ -429,20 +431,3 @@ def ref_sel(request, langid): return HttpResponseRedirect(reverse('rosetta-home')) ref_sel = never_cache(ref_sel) ref_sel = user_passes_test(lambda user: can_translate(user), settings.LOGIN_URL)(ref_sel) - - -def can_translate(user): - if not getattr(settings, 'ROSETTA_REQUIRES_AUTH', True): - return True - if not user.is_authenticated(): - return False - elif user.is_superuser and user.is_staff: - return True - else: - try: - from django.contrib.auth.models import Group - translators = Group.objects.get(name='translators') - return translators in user.groups.all() - except Group.DoesNotExist: - return False - diff --git a/runtests.sh b/runtests.sh old mode 100644 new mode 100755 diff --git a/runtests_coverage.sh b/runtests_coverage.sh old mode 100644 new mode 100755 diff --git a/runtests_multi_venv.sh b/runtests_multi_venv.sh old mode 100644 new mode 100755