diff --git a/.pep8 b/.pep8 new file mode 100644 index 0000000..4ad0546 --- /dev/null +++ b/.pep8 @@ -0,0 +1,3 @@ +[flake8] +ignore = E501 +exclude = south_migrations,migrations,.venv_*,docs diff --git a/CHANGES b/CHANGES index 8ecdca9..109948e 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,16 @@ Version History =============== +Version 0.8.0 +-------------- +* PR #194, huge thanks to @jbaldivieso: + + * Better, cleaner RESTful URLs + * Massive rewrite of Rosetta's view functions as CBVs + * Better management of cached content + +* Check for PEP8 validity during tests + Version 0.7.14 -------------- * Updated installation docs (PR #190, thanks @AuHau) diff --git a/docs/conf.py b/docs/conf.py index 35f748f..e3543ba 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,7 +19,8 @@ import shlex # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath('..')) +from rosetta import get_version # -- General configuration ------------------------------------------------ @@ -47,7 +48,7 @@ master_doc = 'index' # General information about the project. project = u'Django Rosetta' -copyright = u'2008 – 2017 Marco Bonetti and contributors' +copyright = u'2008 – 2018 Marco Bonetti and contributors' author = u'Marco Bonetti' # The version info for the project you're documenting, acts as replacement for @@ -55,9 +56,9 @@ author = u'Marco Bonetti' # built documents. # # The short X.Y version. -version = '0.7.14' +version = get_version() # The full version, including alpha/beta/rc tags. -release = '0.7.14' +release = get_version() # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/rosetta/__init__.py b/rosetta/__init__.py index f0cea18..405c6ca 100644 --- a/rosetta/__init__.py +++ b/rosetta/__init__.py @@ -1,4 +1,4 @@ -VERSION = (0, 7, 14) +VERSION = (0, 8, 0) default_app_config = "rosetta.apps.RosettaAppConfig" diff --git a/rosetta/conf/settings.py b/rosetta/conf/settings.py index 6c04c6f..d351de5 100644 --- a/rosetta/conf/settings.py +++ b/rosetta/conf/settings.py @@ -1,5 +1,4 @@ from django.conf import settings -from django.core.exceptions import ImproperlyConfigured # Number of messages to display per page. MESSAGES_PER_PAGE = getattr(settings, 'ROSETTA_MESSAGES_PER_PAGE', 10) diff --git a/rosetta/models.py b/rosetta/models.py deleted file mode 100644 index ca200a0..0000000 --- a/rosetta/models.py +++ /dev/null @@ -1,2 +0,0 @@ -from django.db import models -# Create your models here. diff --git a/rosetta/poutil.py b/rosetta/poutil.py index de6cc88..796bc4f 100644 --- a/rosetta/poutil.py +++ b/rosetta/poutil.py @@ -61,7 +61,6 @@ def find_pos(lang, project_apps=True, django_apps=False, third_party_apps=False) if os.path.isdir(localepath): paths.append(localepath) - # project/app/locale has_appconfig = False for appname in settings.INSTALLED_APPS: @@ -94,7 +93,6 @@ def find_pos(lang, project_apps=True, django_apps=False, third_party_apps=False) if not project_apps and abs_project_path in app_path: continue - if os.path.isdir(app_path): paths.append(app_path) @@ -127,10 +125,10 @@ def find_pos(lang, project_apps=True, django_apps=False, third_party_apps=False) langs = [lang, ] if u'-' in lang: _l, _c = map(lambda x: x.lower(), lang.split(u'-', 1)) - langs += [u'%s_%s' % (_l, _c), u'%s_%s' % (_l, _c.upper()), u'%s_%s' % (_l, _c.capitalize()),] + langs += [u'%s_%s' % (_l, _c), u'%s_%s' % (_l, _c.upper()), u'%s_%s' % (_l, _c.capitalize())] elif u'_' in lang: _l, _c = map(lambda x: x.lower(), lang.split(u'_', 1)) - langs += [u'%s-%s' % (_l, _c), u'%s-%s' % (_l, _c.upper()), u'%s_%s' % (_l, _c.capitalize()),] + langs += [u'%s-%s' % (_l, _c), u'%s-%s' % (_l, _c.upper()), u'%s_%s' % (_l, _c.capitalize())] paths = map(os.path.normpath, paths) paths = list(set(paths)) diff --git a/rosetta/signals.py b/rosetta/signals.py index 04c9b57..9840bb3 100644 --- a/rosetta/signals.py +++ b/rosetta/signals.py @@ -1,8 +1,8 @@ from django import dispatch entry_changed = dispatch.Signal( - providing_args=["user", "old_msgstr", "old_fuzzy", "pofile", "language_code",] + providing_args=["user", "old_msgstr", "old_fuzzy", "pofile", "language_code"] ) post_save = dispatch.Signal( - providing_args=["language_code","request",] + providing_args=["language_code", "request"] ) diff --git a/rosetta/storage.py b/rosetta/storage.py index e6b4b12..bdeefe6 100644 --- a/rosetta/storage.py +++ b/rosetta/storage.py @@ -101,19 +101,19 @@ class CacheRosettaStorage(BaseRosettaStorage): self.delete('rosetta_cache_test') def get(self, key, default=None): - #print ('get', self._key_prefix + key) + # print ('get', self._key_prefix + key) return cache.get(self._key_prefix + key, default) def set(self, key, val): - #print ('set', self._key_prefix + key) + # print ('set', self._key_prefix + key) cache.set(self._key_prefix + key, val, 86400) def has(self, key): - #print ('has', self._key_prefix + key) + # print ('has', self._key_prefix + key) return (self._key_prefix + key) in cache def delete(self, key): - #print ('del', self._key_prefix + key) + # print ('del', self._key_prefix + key) cache.delete(self._key_prefix + key) diff --git a/rosetta/templatetags/rosetta.py b/rosetta/templatetags/rosetta.py index 5ed270d..0d579b3 100644 --- a/rosetta/templatetags/rosetta.py +++ b/rosetta/templatetags/rosetta.py @@ -48,7 +48,7 @@ def do_incr(parser, token): name = args[1] if not hasattr(parser, '_namedIncrNodes'): parser._namedIncrNodes = {} - if not name in parser._namedIncrNodes: + if name not in parser._namedIncrNodes: parser._namedIncrNodes[name] = IncrNode(0) return parser._namedIncrNodes[name] do_incr = register.tag('increment', do_incr) diff --git a/rosetta/tests/__init__.py b/rosetta/tests/__init__.py index 8baa6e5..2d892b5 100644 --- a/rosetta/tests/__init__.py +++ b/rosetta/tests/__init__.py @@ -1 +1 @@ -from .tests import * +from .tests import * # NOQA diff --git a/rosetta/tests/tests.py b/rosetta/tests/tests.py index d2284c5..5d5d249 100644 --- a/rosetta/tests/tests.py +++ b/rosetta/tests/tests.py @@ -30,7 +30,7 @@ class RosettaTestCase(TestCase): self.curdir = os.path.dirname(__file__) self.dest_file = os.path.normpath( os.path.join(self.curdir, '../locale/xx/LC_MESSAGES/django.po') - ) + ) def setUp(self): from django.contrib.auth.models import User @@ -101,7 +101,7 @@ class RosettaTestCase(TestCase): r = self.client.get(self.third_party_file_list_url) self.assertTrue( os.path.normpath('rosetta/locale/xx/LC_MESSAGES/django.po') in str(r.content) - ) + ) def test_2_PickFile(self): r = self.client.get(self.xx_form_url) @@ -304,28 +304,28 @@ class RosettaTestCase(TestCase): r = self.client.get(self.third_party_file_list_url) self.assertTrue( os.path.normpath('rosetta/locale/xx/LC_MESSAGES/django.po') in str(r.content) - ) + ) self.assertTrue(('contrib') not in str(r.content)) url = reverse('rosetta-file-list', kwargs={'po_filter': 'django'}) r = self.client.get(url) self.assertTrue( os.path.normpath('rosetta/locale/xx/LC_MESSAGES/django.po') not in str(r.content) - ) + ) self.assertTrue(('contrib') in str(r.content)) url = reverse('rosetta-file-list', kwargs={'po_filter': 'all'}) r = self.client.get(url) self.assertTrue( os.path.normpath('rosetta/locale/xx/LC_MESSAGES/django.po') in str(r.content) - ) + ) self.assertTrue(('contrib') in str(r.content)) url = reverse('rosetta-file-list', kwargs={'po_filter': 'project'}) r = self.client.get(url) self.assertTrue( os.path.normpath('rosetta/locale/xx/LC_MESSAGES/django.po') not in str(r.content) - ) + ) self.assertTrue(('contrib') not in str(r.content)) def test_14_issue_99_context_and_comments(self): @@ -432,7 +432,7 @@ class RosettaTestCase(TestCase): 'm_ff7060c1a9aae9c42af4d54ac8551f67_0': 'Foo %s', 'm_ff7060c1a9aae9c42af4d54ac8551f67_1': 'Bar %s', 'm_09f7e02f1290be211da707a266f153b3': 'Salut', - } + } r = self.client.post(self.xx_form_url, data) with open(self.dest_file, 'r') as po_file: pofile_content = po_file.read() @@ -478,7 +478,7 @@ class RosettaTestCase(TestCase): self.assertNotEqual( self.client.session.get('rosetta_cache_storage_key_prefix'), self.client2.session.get('rosetta_cache_storage_key_prefix') - ) + ) # Clean up (restore perms) os.chmod(self.dest_file, 420) # 0644 @@ -498,7 +498,7 @@ class RosettaTestCase(TestCase): unicode_user = User.objects.create_user( 'test_unicode', 'save_header_data@test.com', 'test_unicode' - ) + ) unicode_user.first_name = "aéaéaé aàaàaàa" unicode_user.last_name = "aâââ üüüü" unicode_user.is_superuser, unicode_user.is_staff = True, True @@ -561,7 +561,7 @@ class RosettaTestCase(TestCase): url = reverse( 'rosetta-form', kwargs={'po_filter': 'all', 'lang_id': 'fr_FR.utf8', 'idx': 0} - ) + ) r = self.client.get(url) self.assertTrue('French (France), UTF8' in str(r.content)) self.assertTrue('m_03a603523bd75b00414a413657acdeb2' in str(r.content)) @@ -598,11 +598,11 @@ class RosettaTestCase(TestCase): os.unlink(self.dest_file) destfile = os.path.normpath( os.path.join(self.curdir, '../locale/xx/LC_MESSAGES/pr44.po') - ) + ) shutil.copy( os.path.normpath(os.path.join(self.curdir, './pr44.po.template')), destfile - ) + ) r = self.client.get(self.third_party_file_list_url) self.assertTrue('xx/LC_MESSAGES/pr44.po' in str(r.content)) @@ -847,7 +847,7 @@ class RosettaTestCase(TestCase): view=views.TranslationFormView(), request=request, **kwargs - ) + ) self.assertTrue(view.po_file_is_writable) # Now try again with the file not writable. (Regenerate the view, since @@ -858,7 +858,7 @@ class RosettaTestCase(TestCase): view=views.TranslationFormView(), request=request, **kwargs - ) + ) self.assertFalse(view.po_file_is_writable) # Cleanup @@ -878,7 +878,7 @@ class RosettaTestCase(TestCase): view=views.TranslationFormView(), request=request, **kwargs - ) + ) self.assertEqual(view.po_file_path, self.dest_file) # But if the language isn't an option, we get a 404 @@ -888,7 +888,7 @@ class RosettaTestCase(TestCase): view=views.TranslationFormView(), request=request, **kwargs - ) + ) with self.assertRaises(Http404): view.po_file_path @@ -901,7 +901,7 @@ class RosettaTestCase(TestCase): # Recycle request, even though url kwargs conflict with ones below. request=request, **new_kwargs - ) + ) with self.assertRaises(Http404): view.po_file_path diff --git a/rosetta/urls.py b/rosetta/urls.py index eb27e4d..94c7ac2 100644 --- a/rosetta/urls.py +++ b/rosetta/urls.py @@ -11,14 +11,18 @@ from . import views urlpatterns = [ url(r'^$', - RedirectView.as_view(url=reverse_lazy('rosetta-file-list', - kwargs={'po_filter': 'project'})), + RedirectView.as_view( + url=reverse_lazy('rosetta-file-list', kwargs={'po_filter': 'project'}), + permanent=False + ), name='rosetta-old-home-redirect', ), url(r'^files/$', - RedirectView.as_view(url=reverse_lazy('rosetta-file-list', - kwargs={'po_filter': 'project'})), + RedirectView.as_view( + url=reverse_lazy('rosetta-file-list', kwargs={'po_filter': 'project'}), + permanent=False + ), name='rosetta-file-list-redirect', ), @@ -41,5 +45,4 @@ urlpatterns = [ views.translate_text, name='translate_text', ), - ] diff --git a/rosetta/views.py b/rosetta/views.py index 5780141..9a0d614 100644 --- a/rosetta/views.py +++ b/rosetta/views.py @@ -1,5 +1,4 @@ import hashlib -import json import os import os.path import re @@ -15,7 +14,7 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required, user_passes_test from django.core.paginator import Paginator -from django.http import Http404, HttpResponseRedirect, HttpResponse +from django.http import Http404, HttpResponseRedirect, HttpResponse, JsonResponse from django.views.decorators.cache import never_cache from django.views.generic import TemplateView, View try: @@ -66,7 +65,7 @@ class RosettaBaseMixin(object): If the filter isn't in this list, throw a 404. """ - po_filter = self.kwargs['po_filter'] + po_filter = self.kwargs.get('po_filter') if po_filter not in {'all', 'django', 'third-party', 'project'}: raise Http404 return po_filter @@ -155,7 +154,7 @@ class RosettaFileLevelMixin(RosettaBaseMixin): six.text_type(entry.msgid) + six.text_type(entry.msgstr) + six.text_type(entry.msgctxt or '') - ).encode('utf8') + ).encode('utf8') entry.md5hash = hashlib.md5(str_to_hash).hexdigest() else: storage = get_storage(self.request) @@ -171,7 +170,7 @@ class RosettaFileLevelMixin(RosettaBaseMixin): six.text_type(entry.msgid) + six.text_type(entry.msgstr) + six.text_type(entry.msgctxt or '') - ).encode('utf8') + ).encode('utf8') entry.md5hash = hashlib.new('md5', str_to_hash).hexdigest() storage.set(self.po_file_cache_key, po_file) return po_file @@ -326,13 +325,11 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView): if plural_id is not None: # 0 is ok! entry.msgstr_plural[plural_id] = self.fix_nls( entry.msgid_plural, new_msgstr - ) + ) else: entry.msgstr = self.fix_nls(entry.msgid, new_msgstr) - is_fuzzy = bool( - self.request.POST.get('f_%s' % md5hash, False) - ) + is_fuzzy = bool(self.request.POST.get('f_%s' % md5hash, False)) old_fuzzy = 'fuzzy' in entry.flags if old_fuzzy and not is_fuzzy: @@ -356,7 +353,7 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView): _("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."), - ) + ) if file_change and self.po_file_is_writable: try: @@ -365,14 +362,14 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView): getattr(self.request.user, 'first_name', 'Anonymous'), getattr(self.request.user, 'last_name', 'User'), getattr(self.request.user, 'email', 'anonymous@user.tld') - ) - ).encode('ascii', 'ignore') - self.po_file.metadata['X-Translated-Using'] = u"django-rosetta %s" % ( - get_rosetta_version(False) ) + ).encode('ascii', 'ignore') + self.po_file.metadata['X-Translated-Using'] = u"django-rosetta %s" % ( + get_rosetta_version(False)) self.po_file.metadata['PO-Revision-Date'] = timestamp_with_timezone() except UnicodeDecodeError: pass + try: self.po_file.save() po_filepath, ext = os.path.splitext(self.po_file_path) @@ -390,7 +387,7 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView): self.request.environ.get('mod_wsgi.process_group', None) and 'SCRIPT_FILENAME' in self.request.environ and int(self.request.environ.get('mod_wsgi.script_reloading', 0)) - ) + ) if should_try_wsgi_reload: try: os.utime(self.request.environ.get('SCRIPT_FILENAME'), None) @@ -428,13 +425,13 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView): 'query': self.query, 'ref_lang': self.ref_lang, 'page': page, - } + } # Winnow down the query string args to non-blank ones query_string_args = {k: v for k, v in query_string_args.items() if v} return HttpResponseRedirect("{url}?{qs}".format( url=reverse('rosetta-form', kwargs=self.kwargs), qs=urlencode(query_string_args), - )) + )) def get_context_data(self, **kwargs): context = super(TranslationFormView, self).get_context_data(**kwargs) @@ -491,7 +488,7 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView): main_lang_po_path = self.po_file_path.replace( '/%s/' % self.language_id, '/%s/' % main_language_id, - ) + ) # XXX: brittle; what if this path doesn't exist? Isn't a .po file? main_lang_po = pofile(main_lang_po_path) @@ -507,7 +504,7 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView): ADMIN_IMAGE_DIR = ADMIN_MEDIA_PREFIX + 'img/' rosetta_i18n_lang_name = six.text_type( dict(settings.LANGUAGES).get(self.language_id) - ) + ) # "bidi" as in "bi-directional" rosetta_i18n_lang_bidi = self.language_id.split('-')[0] in settings.LANGUAGES_BIDI query_string_args = {} @@ -523,7 +520,7 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView): # numbers in these links. We just pass in ref_lang, if it's set. filter_query_string_base = urlencode( {k: v for k, v in query_string_args.items() if k == 'ref_lang'} - ) + ) context.update({ 'version': get_rosetta_version(True), @@ -548,7 +545,7 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView): 'paginator': paginator, 'rosetta_i18n_pofile': self.po_file, 'ref_lang': self.ref_lang, - }) + }) return context @@ -577,7 +574,7 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView): '/locale/[a-z]{2}/', '/locale/%s/' % (self.ref_lang), self.po_file_path, - ) + ) try: ref_pofile = pofile(ref_fn) except IOError: @@ -673,7 +670,7 @@ class TranslationFileDownload(RosettaFileLevelMixin, View): # XXX: should add a message! return HttpResponseRedirect( reverse('rosetta-file-list', kwargs={'po_filter': 'project'}) - ) + ) @user_passes_test(lambda user: can_translate(user), settings.LOGIN_URL) @@ -699,6 +696,6 @@ def translate_text(request): data = { 'success': False, 'error': "Translation API Exception: {0}".format(e.message), - } + } - return HttpResponse(json.dumps(data), content_type='application/json') + return JsonResponse(data) diff --git a/setup.py b/setup.py index e3363f3..9254851 100644 --- a/setup.py +++ b/setup.py @@ -45,8 +45,9 @@ setup( 'Topic :: Software Development :: Internationalization', 'Framework :: Django', 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', ], include_package_data=True, zip_safe=False, @@ -54,7 +55,7 @@ setup( 'six >=1.2.0', 'Django >= 1.8', 'requests >= 2.1.0', - 'polib >= 1.0.6', + 'polib >= 1.1.0', 'microsofttranslator >= 0.7' ], tests_require=['tox'], diff --git a/testproject/coverage.sh b/testproject/coverage.sh new file mode 100644 index 0000000..1bae19a --- /dev/null +++ b/testproject/coverage.sh @@ -0,0 +1,4 @@ +#!/bin/sh +coverage run --rcfile .coveragerc manage.py test --failfast rosetta +coverage xml +coverage html diff --git a/tox.ini b/tox.ini index 5490e6f..5e0251a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ [tox] envlist = + flake8, {py27,py36}-django{18,19,110,111}, py36-django20, gettext,docs @@ -26,11 +27,12 @@ deps = py36-django{17,18,19,110,111,20}: python3-memcached # py27-django18: pudb requests - polib>=1.0.6 + polib>=1.1.0 microsofttranslator>=0.7 six goslate vcrpy + coverage [testenv:gettext] basepython = python3 @@ -57,3 +59,10 @@ deps = sphinx changedir = docs commands= sphinx-build -W -b html . _build/html + +[testenv:flake8] +basepython = python3 +deps = flake8==2.4.1 +commands= + flake8 {toxinidir}/rosetta +