mirror of
https://github.com/Hopiu/django-rosetta.git
synced 2026-03-16 21:30:24 +00:00
dropped support for python2 and django < 1.11
This commit is contained in:
parent
6cdc390567
commit
efdc0a649d
9 changed files with 331 additions and 300 deletions
2
.pep8
2
.pep8
|
|
@ -1,3 +1,3 @@
|
|||
[flake8]
|
||||
ignore = E501
|
||||
ignore = E501,W503
|
||||
exclude = south_migrations,migrations,.venv_*,docs
|
||||
|
|
|
|||
|
|
@ -3,10 +3,6 @@ services: memcached
|
|||
|
||||
matrix:
|
||||
include:
|
||||
- python: "2.7"
|
||||
env: TOX_ENV=py27-django111
|
||||
- python: "3.6"
|
||||
env: TOX_ENV=py36-django111
|
||||
- python: "3.6"
|
||||
env: TOX_ENV=py36-django20
|
||||
- python: "3.6"
|
||||
|
|
|
|||
2
CHANGES
2
CHANGES
|
|
@ -9,6 +9,8 @@ Version 0.9.5 (unreleased)
|
|||
* Added Kyrgyz translation (#239,thanks @Soyuzbek)
|
||||
* Ignore translator context hints checking unmatched variables (#238, #239, thanks @jeancochrane and @mondeja)
|
||||
* Uncheck fuzzy on translation keyup instead of change (#235 @mondeja)
|
||||
* Allow passing a function itself to the setting ROSETTA_ACCESS_CONTROL (#227, thanks @alvra)
|
||||
* Dropped support for Django 1.11 and Python 2
|
||||
|
||||
|
||||
Version 0.9.4
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ Rosetta can be configured via the following parameters, to be defined in your pr
|
|||
* ``ROSETTA_REQUIRES_AUTH``: Require authentication for all Rosetta views. Defaults to ``True``.
|
||||
* ``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_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.
|
||||
* ``ROSETTA_ACCESS_CONTROL_FUNCTION``: An alternative function (string or a callable) 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.
|
||||
* ``ROSETTA_LANGUAGE_GROUPS``: Set to ``True`` to enable language-specific groups, which can be used to give different translators access to different languages. Instead of creating a global ``translators`` group, create individual per-language groups, e.g. ``translators-de``, ``translators-fr``, and assign users to these.
|
||||
* ``ROSETTA_CACHE_NAME``: When using ``rosetta.storage.CacheRosettaStorage``, you can store the Rosetta data in a specific cache. This is particularly useful when your ``default`` cache is a ``django.core.cache.backends.dummy.DummyCache`` (which happens on pre-production environments). If unset, it will default to ``rosetta`` if a cache with this name exists, or ``default`` if not.
|
||||
* ``ROSETTA_POFILENAMES``: Defines which po file names are exposed in the web interface. Defaults to ``('django.po', 'djangojs.po')``
|
||||
|
|
|
|||
|
|
@ -1,34 +1,27 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import filecmp
|
||||
import hashlib
|
||||
import os
|
||||
import shutil
|
||||
import vcr
|
||||
try:
|
||||
# Python 3
|
||||
from urllib.parse import urlencode
|
||||
except ImportError:
|
||||
# Python 2
|
||||
from urllib import urlencode
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.conf import settings
|
||||
from django.dispatch import receiver
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import Http404
|
||||
from django.test import TestCase, RequestFactory, override_settings
|
||||
from django.test.client import Client
|
||||
from django import VERSION
|
||||
from django.urls import reverse, resolve
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.dispatch import receiver
|
||||
from django.http import Http404
|
||||
from django.test import RequestFactory, TestCase, override_settings
|
||||
from django.test.client import Client
|
||||
from django.urls import resolve, reverse
|
||||
from django.utils.encoding import force_bytes
|
||||
import six
|
||||
|
||||
import six
|
||||
import vcr
|
||||
from rosetta import views
|
||||
from rosetta.signals import entry_changed, post_save
|
||||
from rosetta.storage import get_storage
|
||||
from rosetta import views
|
||||
|
||||
|
||||
class RosettaTestCase(TestCase):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RosettaTestCase, self).__init__(*args, **kwargs)
|
||||
self.curdir = os.path.dirname(__file__)
|
||||
|
|
@ -39,9 +32,15 @@ class RosettaTestCase(TestCase):
|
|||
def setUp(self):
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
user = User.objects.create_superuser('test_admin', 'test@test.com', 'test_password')
|
||||
user2 = User.objects.create_superuser('test_admin2', 'test@test2.com', 'test_password')
|
||||
user3 = User.objects.create_superuser('test_admin3', 'test@test2.com', 'test_password')
|
||||
user = User.objects.create_superuser(
|
||||
'test_admin', 'test@test.com', 'test_password'
|
||||
)
|
||||
user2 = User.objects.create_superuser(
|
||||
'test_admin2', 'test@test2.com', 'test_password'
|
||||
)
|
||||
user3 = User.objects.create_superuser(
|
||||
'test_admin3', 'test@test2.com', 'test_password'
|
||||
)
|
||||
|
||||
user3.is_staff = False
|
||||
user3.save()
|
||||
|
|
@ -84,15 +83,14 @@ class RosettaTestCase(TestCase):
|
|||
def test_1_ListLoading(self):
|
||||
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)
|
||||
os.path.normpath('rosetta/locale/xx/LC_MESSAGES/django.po')
|
||||
in r.content.decode()
|
||||
)
|
||||
|
||||
@override_settings(ROSETTA_LANGUAGES=(
|
||||
('xx', 'dummy language'),
|
||||
))
|
||||
@override_settings(ROSETTA_LANGUAGES=(('xx', 'dummy language'),))
|
||||
def test_2_PickFile(self):
|
||||
r = self.client.get(self.xx_form_url)
|
||||
self.assertTrue('dummy language' in str(r.content))
|
||||
self.assertTrue('dummy language' in r.content.decode())
|
||||
|
||||
def test_3_DownloadZIP(self):
|
||||
kwargs = {'po_filter': 'third-party', 'lang_id': 'xx', 'idx': 0}
|
||||
|
|
@ -101,9 +99,7 @@ class RosettaTestCase(TestCase):
|
|||
self.assertTrue('content-type' in r._headers.keys())
|
||||
self.assertTrue('application/x-zip' in r._headers.get('content-type'))
|
||||
|
||||
@override_settings(ROSETTA_LANGUAGES=(
|
||||
('xx', 'dummy language'),
|
||||
))
|
||||
@override_settings(ROSETTA_LANGUAGES=(('xx', 'dummy language'),))
|
||||
def test_4_DoChanges(self):
|
||||
self.copy_po_file_from_template('./django.po.template')
|
||||
untranslated_url = self.xx_form_url + '?msg_filter=untranslated'
|
||||
|
|
@ -113,10 +109,10 @@ class RosettaTestCase(TestCase):
|
|||
r = self.client.get(untranslated_url)
|
||||
|
||||
# make sure both strings are untranslated
|
||||
self.assertTrue('dummy language' in str(r.content))
|
||||
self.assertTrue('String 1' in str(r.content))
|
||||
self.assertTrue('String 2' in str(r.content))
|
||||
self.assertTrue('m_e48f149a8b2e8baa81b816c0edf93890' in str(r.content))
|
||||
self.assertTrue('dummy language' in r.content.decode())
|
||||
self.assertTrue('String 1' in r.content.decode())
|
||||
self.assertTrue('String 2' in r.content.decode())
|
||||
self.assertTrue('m_e48f149a8b2e8baa81b816c0edf93890' in r.content.decode())
|
||||
|
||||
# post a translation
|
||||
data = {'m_e48f149a8b2e8baa81b816c0edf93890': 'Hello, world'}
|
||||
|
|
@ -126,20 +122,18 @@ class RosettaTestCase(TestCase):
|
|||
r = self.client.get(untranslated_url)
|
||||
|
||||
# the translated string no longer is up for translation
|
||||
self.assertTrue('String 1' in str(r.content))
|
||||
self.assertTrue('String 2' not in str(r.content))
|
||||
self.assertTrue('String 1' in r.content.decode())
|
||||
self.assertTrue('String 2' not in r.content.decode())
|
||||
|
||||
# display only translated strings
|
||||
r = self.client.get(translated_url)
|
||||
|
||||
# The translation was persisted
|
||||
self.assertTrue('String 1' not in str(r.content))
|
||||
self.assertTrue('String 2' in str(r.content))
|
||||
self.assertTrue('Hello, world' in str(r.content))
|
||||
self.assertTrue('String 1' not in r.content.decode())
|
||||
self.assertTrue('String 2' in r.content.decode())
|
||||
self.assertTrue('Hello, world' in r.content.decode())
|
||||
|
||||
@override_settings(ROSETTA_LANGUAGES=(
|
||||
('xx', 'dummy language'),
|
||||
))
|
||||
@override_settings(ROSETTA_LANGUAGES=(('xx', 'dummy language'),))
|
||||
def test_5_TestIssue67(self):
|
||||
# issue 67: http://code.google.com/p/django-rosetta/issues/detail?id=67
|
||||
self.copy_po_file_from_template('./django.po.issue67.template')
|
||||
|
|
@ -149,15 +143,15 @@ class RosettaTestCase(TestCase):
|
|||
content = f_.read()
|
||||
self.assertTrue('Hello, world' not in six.text_type(content))
|
||||
self.assertTrue('|| n%100>=20) ? 1 : 2)' in six.text_type(content))
|
||||
del(content)
|
||||
del content
|
||||
|
||||
r = self.client.get(self.xx_form_url + '?msg_filter=untranslated')
|
||||
|
||||
# make sure all strings are untranslated
|
||||
self.assertTrue('dummy language' in str(r.content))
|
||||
self.assertTrue('String 1' in str(r.content))
|
||||
self.assertTrue('String 2' in str(r.content))
|
||||
self.assertTrue('m_e48f149a8b2e8baa81b816c0edf93890' in str(r.content))
|
||||
self.assertTrue('dummy language' in r.content.decode())
|
||||
self.assertTrue('String 1' in r.content.decode())
|
||||
self.assertTrue('String 2' in r.content.decode())
|
||||
self.assertTrue('m_e48f149a8b2e8baa81b816c0edf93890' in r.content.decode())
|
||||
|
||||
# post a translation
|
||||
data = {'m_e48f149a8b2e8baa81b816c0edf93890': 'Hello, world'}
|
||||
|
|
@ -169,13 +163,11 @@ class RosettaTestCase(TestCase):
|
|||
self.assertTrue('Hello, world' in str(content))
|
||||
self.assertTrue('|| n%100>=20) ? 1 : 2)' in str(content))
|
||||
self.assertTrue('or n%100>=20) ? 1 : 2)' not in str(content))
|
||||
del(content)
|
||||
del content
|
||||
|
||||
@override_settings(ROSETTA_LANGUAGES=(
|
||||
('xx', 'dummy language'),
|
||||
))
|
||||
@override_settings(ROSETTA_LANGUAGES=(('xx', 'dummy language'),))
|
||||
def test_6_ExcludedApps(self):
|
||||
with self.settings(ROSETTA_EXCLUDED_APPLICATIONS=('rosetta', )):
|
||||
with self.settings(ROSETTA_EXCLUDED_APPLICATIONS=('rosetta',)):
|
||||
r = self.client.get(self.third_party_file_list_url)
|
||||
self.assertNotContains(r, 'rosetta/locale/xx/LC_MESSAGES/django.po')
|
||||
|
||||
|
|
@ -190,17 +182,15 @@ class RosettaTestCase(TestCase):
|
|||
r = self.client.get(self.project_file_list_url)
|
||||
self.assertNotContains(r, 'rosetta/locale/xx/LC_MESSAGES/django.po')
|
||||
|
||||
@override_settings(ROSETTA_LANGUAGES=(
|
||||
('xx', 'dummy language'),
|
||||
))
|
||||
@override_settings(ROSETTA_LANGUAGES=(('xx', 'dummy language'),))
|
||||
def test_8_hideObsoletes(self):
|
||||
r = self.client.get(self.xx_form_url)
|
||||
|
||||
# not in listing
|
||||
for p in range(1, 5):
|
||||
r = self.client.get(self.xx_form_url + '?page=%d' % p)
|
||||
self.assertTrue('dummy language' in str(r.content))
|
||||
self.assertTrue('Les deux' not in str(r.content))
|
||||
self.assertTrue('dummy language' in r.content.decode())
|
||||
self.assertTrue('Les deux' not in r.content.decode())
|
||||
|
||||
r = self.client.get(self.xx_form_url + '?query=Les%20Deux')
|
||||
self.assertContains(r, 'dummy language')
|
||||
|
|
@ -278,7 +268,7 @@ class RosettaTestCase(TestCase):
|
|||
|
||||
def test_11_issue_80_tab_indexes(self):
|
||||
r = self.client.get(self.xx_form_url)
|
||||
self.assertTrue('tabindex="3"' in str(r.content))
|
||||
self.assertTrue('tabindex="3"' in r.content.decode())
|
||||
|
||||
def test_12_issue_82_staff_user(self):
|
||||
self.client3 = Client()
|
||||
|
|
@ -297,37 +287,37 @@ class RosettaTestCase(TestCase):
|
|||
self.assertTrue(r.content)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
@override_settings(ROSETTA_LANGUAGES=(('fr', 'French'), ('xx', 'Dummy Language'),))
|
||||
@override_settings(ROSETTA_LANGUAGES=(('fr', 'French'), ('xx', 'Dummy Language')))
|
||||
def test_13_catalog_filters(self):
|
||||
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)
|
||||
os.path.normpath('rosetta/locale/xx/LC_MESSAGES/django.po')
|
||||
in r.content.decode()
|
||||
)
|
||||
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)
|
||||
os.path.normpath('rosetta/locale/xx/LC_MESSAGES/django.po')
|
||||
not in r.content.decode()
|
||||
)
|
||||
self.assertTrue(('contrib') in str(r.content))
|
||||
|
||||
r = self.client.get(self.all_file_list_url)
|
||||
self.assertTrue(
|
||||
os.path.normpath('rosetta/locale/xx/LC_MESSAGES/django.po') in str(r.content)
|
||||
os.path.normpath('rosetta/locale/xx/LC_MESSAGES/django.po')
|
||||
in r.content.decode()
|
||||
)
|
||||
self.assertTrue(('contrib') in str(r.content))
|
||||
|
||||
r = self.client.get(self.project_file_list_url)
|
||||
self.assertTrue(
|
||||
os.path.normpath('rosetta/locale/xx/LC_MESSAGES/django.po') not in str(r.content)
|
||||
os.path.normpath('rosetta/locale/xx/LC_MESSAGES/django.po')
|
||||
not in r.content.decode()
|
||||
)
|
||||
self.assertTrue(('contrib') not in str(r.content))
|
||||
|
||||
def test_14_issue_99_context_and_comments(self):
|
||||
r = self.client.get(self.xx_form_url)
|
||||
self.assertTrue('This is a text of the base template' in str(r.content))
|
||||
self.assertTrue('Context hint' in str(r.content))
|
||||
self.assertTrue('This is a text of the base template' in r.content.decode())
|
||||
self.assertTrue('Context hint' in r.content.decode())
|
||||
|
||||
def test_15_issue_87_entry_changed_signal(self):
|
||||
self.copy_po_file_from_template('./django.po.template')
|
||||
|
|
@ -338,7 +328,8 @@ class RosettaTestCase(TestCase):
|
|||
self.test_old_msgstr = kwargs.get('old_msgstr')
|
||||
self.test_new_msgstr = sender.msgstr
|
||||
self.test_msg_id = sender.msgid
|
||||
self.assertTrue('m_e48f149a8b2e8baa81b816c0edf93890' in str(r.content))
|
||||
|
||||
self.assertTrue('m_e48f149a8b2e8baa81b816c0edf93890' in r.content.decode())
|
||||
|
||||
# post a translation
|
||||
data = {'m_e48f149a8b2e8baa81b816c0edf93890': 'Hello, world'}
|
||||
|
|
@ -348,7 +339,7 @@ class RosettaTestCase(TestCase):
|
|||
self.assertTrue(self.test_new_msgstr == 'Hello, world')
|
||||
self.assertTrue(self.test_msg_id == 'String 2')
|
||||
|
||||
del(self.test_old_msgstr, self.test_new_msgstr, self.test_msg_id)
|
||||
del (self.test_old_msgstr, self.test_new_msgstr, self.test_msg_id)
|
||||
|
||||
def test_16_issue_101_post_save_signal(self):
|
||||
self.copy_po_file_from_template('./django.po.template')
|
||||
|
|
@ -358,14 +349,14 @@ class RosettaTestCase(TestCase):
|
|||
def test_receiver(sender, **kwargs):
|
||||
self.test_sig_lang = kwargs.get('language_code')
|
||||
|
||||
self.assertTrue('m_e48f149a8b2e8baa81b816c0edf93890' in str(r.content))
|
||||
self.assertTrue('m_e48f149a8b2e8baa81b816c0edf93890' in r.content.decode())
|
||||
|
||||
# post a translation
|
||||
data = {'m_e48f149a8b2e8baa81b816c0edf93890': 'Hello, world'}
|
||||
self.client.post(self.xx_form_url, data)
|
||||
|
||||
self.assertTrue(self.test_sig_lang == 'xx')
|
||||
del(self.test_sig_lang)
|
||||
del self.test_sig_lang
|
||||
|
||||
def test_17_issue_103_post_save_signal_has_request(self):
|
||||
self.copy_po_file_from_template('./django.po.template')
|
||||
|
|
@ -375,31 +366,32 @@ class RosettaTestCase(TestCase):
|
|||
def test_receiver(sender, **kwargs):
|
||||
self.test_16_has_request = 'request' in kwargs
|
||||
|
||||
self.assertTrue('m_e48f149a8b2e8baa81b816c0edf93890' in str(r.content))
|
||||
self.assertTrue('m_e48f149a8b2e8baa81b816c0edf93890' in r.content.decode())
|
||||
|
||||
# post a translation
|
||||
data = {'m_e48f149a8b2e8baa81b816c0edf93890': 'Hello, world'}
|
||||
r = self.client.post(self.xx_form_url, data)
|
||||
|
||||
self.assertTrue(self.test_16_has_request)
|
||||
del(self.test_16_has_request)
|
||||
del self.test_16_has_request
|
||||
|
||||
def test_18_Test_Issue_gh24(self):
|
||||
self.copy_po_file_from_template('./django.po.issue24gh.template')
|
||||
r = self.client.get(self.xx_form_url)
|
||||
|
||||
self.assertTrue('m_bb9d8fe6159187b9ea494c1b313d23d4' in str(r.content))
|
||||
self.assertTrue('m_bb9d8fe6159187b9ea494c1b313d23d4' in r.content.decode())
|
||||
|
||||
# Post a translation, it should have properly wrapped lines
|
||||
data = {'m_bb9d8fe6159187b9ea494c1b313d23d4':
|
||||
'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean '
|
||||
'commodo ligula eget dolor. Aenean massa. Cum sociis natoque '
|
||||
'penatibus et magnis dis parturient montes, nascetur ridiculus '
|
||||
'mus. Donec quam felis, ultricies nec, pellentesque eu, pretium '
|
||||
'quis, sem. Nulla consequat massa quis enim. Donec pede justo, '
|
||||
'fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, '
|
||||
'rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum '
|
||||
'felis eu pede mollis pretium.'}
|
||||
data = {
|
||||
'm_bb9d8fe6159187b9ea494c1b313d23d4': 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean '
|
||||
'commodo ligula eget dolor. Aenean massa. Cum sociis natoque '
|
||||
'penatibus et magnis dis parturient montes, nascetur ridiculus '
|
||||
'mus. Donec quam felis, ultricies nec, pellentesque eu, pretium '
|
||||
'quis, sem. Nulla consequat massa quis enim. Donec pede justo, '
|
||||
'fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, '
|
||||
'rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum '
|
||||
'felis eu pede mollis pretium.'
|
||||
}
|
||||
r = self.client.post(self.xx_form_url, data)
|
||||
with open(self.dest_file, 'r') as po_file:
|
||||
pofile_content = po_file.read()
|
||||
|
|
@ -410,7 +402,7 @@ class RosettaTestCase(TestCase):
|
|||
self.copy_po_file_from_template('./django.po.issue24gh.template')
|
||||
with self.settings(ROSETTA_POFILE_WRAP_WIDTH=0):
|
||||
r = self.client.get(self.xx_form_url)
|
||||
self.assertTrue('m_bb9d8fe6159187b9ea494c1b313d23d4' in str(r.content))
|
||||
self.assertTrue('m_bb9d8fe6159187b9ea494c1b313d23d4' in r.content.decode())
|
||||
r = self.client.post(self.xx_form_url, data)
|
||||
with open(self.dest_file, 'r') as po_file:
|
||||
pofile_content = po_file.read()
|
||||
|
|
@ -419,9 +411,9 @@ class RosettaTestCase(TestCase):
|
|||
def test_19_Test_Issue_gh34(self):
|
||||
self.copy_po_file_from_template('./django.po.issue34gh.template')
|
||||
r = self.client.get(self.xx_form_url)
|
||||
self.assertTrue('m_ff7060c1a9aae9c42af4d54ac8551f67_1' in str(r.content))
|
||||
self.assertTrue('m_ff7060c1a9aae9c42af4d54ac8551f67_0' in str(r.content))
|
||||
self.assertTrue('m_09f7e02f1290be211da707a266f153b3' in str(r.content))
|
||||
self.assertTrue('m_ff7060c1a9aae9c42af4d54ac8551f67_1' in r.content.decode())
|
||||
self.assertTrue('m_ff7060c1a9aae9c42af4d54ac8551f67_0' in r.content.decode())
|
||||
self.assertTrue('m_09f7e02f1290be211da707a266f153b3' in r.content.decode())
|
||||
|
||||
# post a translation, it should have properly wrapped lines
|
||||
data = {
|
||||
|
|
@ -443,15 +435,16 @@ class RosettaTestCase(TestCase):
|
|||
def test_20_Test_Issue_gh38(self):
|
||||
# (Have to log in again, since our session engine changed)
|
||||
self.client.login(username='test_admin', password='test_password')
|
||||
self.assertTrue('django.contrib.sessions.middleware.SessionMiddleware'
|
||||
in settings.MIDDLEWARE)
|
||||
self.assertTrue(
|
||||
'django.contrib.sessions.middleware.SessionMiddleware' in settings.MIDDLEWARE
|
||||
)
|
||||
|
||||
# Only one backend to test: cache backend
|
||||
self.copy_po_file_from_template('./django.po.issue38gh.template')
|
||||
|
||||
r = self.client.get(self.xx_form_url)
|
||||
self.assertFalse(len(str(self.client.cookies.get('sessionid'))) > 4096)
|
||||
self.assertTrue('m_9efd113f7919952523f06e0d88da9c54' in str(r.content))
|
||||
self.assertTrue('m_9efd113f7919952523f06e0d88da9c54' in r.content.decode())
|
||||
|
||||
data = {'m_9efd113f7919952523f06e0d88da9c54': 'Testing cookie length'}
|
||||
r = self.client.post(self.xx_form_url, data)
|
||||
|
|
@ -460,8 +453,8 @@ class RosettaTestCase(TestCase):
|
|||
self.assertTrue('Testing cookie length' in pofile_content)
|
||||
|
||||
r = self.client.get(self.xx_form_url + '?filter=translated')
|
||||
self.assertTrue('Testing cookie length' in str(r.content))
|
||||
self.assertTrue('m_9f6c442c6d579707440ba9dada0fb373' in str(r.content))
|
||||
self.assertTrue('Testing cookie length' in r.content.decode())
|
||||
self.assertTrue('m_9f6c442c6d579707440ba9dada0fb373' in r.content.decode())
|
||||
|
||||
@override_settings(ROSETTA_STORAGE_CLASS='rosetta.storage.CacheRosettaStorage')
|
||||
def test_21_concurrency_of_cache_backend(self):
|
||||
|
|
@ -474,7 +467,7 @@ class RosettaTestCase(TestCase):
|
|||
self.client2.get(self.xx_form_url)
|
||||
self.assertNotEqual(
|
||||
self.client.session.get('rosetta_cache_storage_key_prefix'),
|
||||
self.client2.session.get('rosetta_cache_storage_key_prefix')
|
||||
self.client2.session.get('rosetta_cache_storage_key_prefix'),
|
||||
)
|
||||
|
||||
# Clean up (restore perms)
|
||||
|
|
@ -485,12 +478,10 @@ class RosettaTestCase(TestCase):
|
|||
|
||||
r = self.client.get(self.xx_form_url)
|
||||
# We have distinct hashes, even though the msgid and msgstr are identical
|
||||
self.assertTrue('m_4765f7de94996d3de5975fa797c3451f' in str(r.content))
|
||||
self.assertTrue('m_08e4e11e2243d764fc45a5a4fba5d0f2' in str(r.content))
|
||||
self.assertTrue('m_4765f7de94996d3de5975fa797c3451f' in r.content.decode())
|
||||
self.assertTrue('m_08e4e11e2243d764fc45a5a4fba5d0f2' in r.content.decode())
|
||||
|
||||
@override_settings(ROSETTA_LANGUAGES=(
|
||||
('xx', 'dummy language'),
|
||||
))
|
||||
@override_settings(ROSETTA_LANGUAGES=(('xx', 'dummy language'),))
|
||||
def test_23_save_header_data(self):
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
|
@ -510,10 +501,10 @@ class RosettaTestCase(TestCase):
|
|||
r = self.client.get(self.xx_form_url + '?filter=untranslated')
|
||||
|
||||
# make sure both strings are untranslated
|
||||
self.assertTrue('dummy language' in str(r.content))
|
||||
self.assertTrue('String 1' in str(r.content))
|
||||
self.assertTrue('String 2' in str(r.content))
|
||||
self.assertTrue('m_e48f149a8b2e8baa81b816c0edf93890' in str(r.content))
|
||||
self.assertTrue('dummy language' in r.content.decode())
|
||||
self.assertTrue('String 1' in r.content.decode())
|
||||
self.assertTrue('String 2' in r.content.decode())
|
||||
self.assertTrue('m_e48f149a8b2e8baa81b816c0edf93890' in r.content.decode())
|
||||
|
||||
# post a translation
|
||||
data = {'m_e48f149a8b2e8baa81b816c0edf93890': 'Hello, world'}
|
||||
|
|
@ -533,10 +524,10 @@ class RosettaTestCase(TestCase):
|
|||
# Load the template file
|
||||
r = self.client.get(self.xx_form_url)
|
||||
|
||||
self.assertTrue('Progress: 0%' in str(r.content))
|
||||
self.assertTrue('Progress: 0%' in r.content.decode())
|
||||
data = {'m_e48f149a8b2e8baa81b816c0edf93890': 'Hello, world'}
|
||||
r = self.client.post(self.xx_form_url, data, follow=True)
|
||||
self.assertTrue('Progress: 25%' in str(r.content))
|
||||
self.assertTrue('Progress: 25%' in r.content.decode())
|
||||
|
||||
def test_25_replace_access_control(self):
|
||||
# Test default access control allows access
|
||||
|
|
@ -562,17 +553,18 @@ class RosettaTestCase(TestCase):
|
|||
|
||||
def test_27_extended_urlconf_language_code_loads_file(self):
|
||||
url = reverse(
|
||||
'rosetta-form',
|
||||
kwargs={'po_filter': 'all', 'lang_id': 'fr_FR.utf8', 'idx': 0}
|
||||
'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))
|
||||
self.assertTrue('French (France), UTF8' in r.content.decode())
|
||||
self.assertTrue('m_03a603523bd75b00414a413657acdeb2' in r.content.decode())
|
||||
|
||||
def test_28_issue_gh87(self):
|
||||
"""Make sure that rosetta_i18n_catalog_filter is passed into the context."""
|
||||
r = self.client.get(self.third_party_file_list_url)
|
||||
self.assertContains(r, '<li class="active"><a href="/rosetta/files/third-party/">')
|
||||
self.assertContains(
|
||||
r, '<li class="active"><a href="/rosetta/files/third-party/">'
|
||||
)
|
||||
|
||||
@override_settings(
|
||||
SESSION_ENGINE='django.contrib.sessions.backends.signed_cookies',
|
||||
|
|
@ -580,8 +572,10 @@ class RosettaTestCase(TestCase):
|
|||
)
|
||||
def test_29_unsupported_p3_django_16_storage(self):
|
||||
if VERSION[0:2] < (2, 0):
|
||||
self.assertTrue('django.contrib.sessions.middleware.SessionMiddleware'
|
||||
in settings.MIDDLEWARE)
|
||||
self.assertTrue(
|
||||
'django.contrib.sessions.middleware.SessionMiddleware'
|
||||
in settings.MIDDLEWARE
|
||||
)
|
||||
|
||||
# Force caching to be used by making the pofile read-only
|
||||
os.chmod(self.dest_file, 292) # 0444
|
||||
|
|
@ -596,8 +590,7 @@ class RosettaTestCase(TestCase):
|
|||
os.chmod(self.dest_file, 420) # 0644
|
||||
|
||||
@override_settings(
|
||||
ROSETTA_POFILENAMES=('pr44.po', ),
|
||||
ROSETTA_LANGUAGES=(('xx', 'dummy language'),)
|
||||
ROSETTA_POFILENAMES=('pr44.po',), ROSETTA_LANGUAGES=(('xx', 'dummy language'),)
|
||||
)
|
||||
def test_30_pofile_names(self):
|
||||
os.unlink(self.dest_file)
|
||||
|
|
@ -605,15 +598,14 @@ class RosettaTestCase(TestCase):
|
|||
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
|
||||
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))
|
||||
self.assertTrue('xx/LC_MESSAGES/pr44.po' in r.content.decode())
|
||||
|
||||
r = self.client.get(self.xx_form_url)
|
||||
self.assertTrue('dummy language' in str(r.content))
|
||||
self.assertTrue('dummy language' in r.content.decode())
|
||||
|
||||
# (Clean up)
|
||||
os.unlink(destfile)
|
||||
|
|
@ -654,32 +646,35 @@ class RosettaTestCase(TestCase):
|
|||
self.assertContains(r, 'rosetta/locale/xx/LC_MESSAGES/django.po')
|
||||
|
||||
@override_settings(
|
||||
ROSETTA_ENABLE_REFLANG=True,
|
||||
ROSETTA_LANGUAGES=(('xx', 'dummy language'),)
|
||||
ROSETTA_ENABLE_REFLANG=True, ROSETTA_LANGUAGES=(('xx', 'dummy language'),)
|
||||
)
|
||||
def test_33_reflang(self):
|
||||
self.copy_po_file_from_template('./django.po.issue60.template')
|
||||
r = self.client.get(self.xx_form_url)
|
||||
|
||||
# Verify that there's an option to select a reflang
|
||||
self.assertTrue('<option value="?ref_lang=xx">dummy language</option>' in str(r.content))
|
||||
self.assertTrue(
|
||||
'<option value="?ref_lang=xx">dummy language</option>' in r.content.decode()
|
||||
)
|
||||
|
||||
r = self.client.get(self.xx_form_url + '?ref_lang=xx')
|
||||
# The translated string in the test PO file ends up in the "Reference" column
|
||||
self.assertTrue('<span class="message">translated-string1</span>' in str(r.content))
|
||||
self.assertTrue(
|
||||
'<span class="message">translated-string1</span>' in r.content.decode()
|
||||
)
|
||||
|
||||
def test_show_occurrences(self):
|
||||
r = self.client.get(self.xx_form_url)
|
||||
# Verify that occurrences in view
|
||||
self.assertTrue('<td class="location">' in str(r.content))
|
||||
self.assertTrue('<td class="location">' in r.content.decode())
|
||||
with self.settings(ROSETTA_SHOW_OCCURRENCES=False):
|
||||
r = self.client.get(self.xx_form_url)
|
||||
# Verify that occurrences not in view
|
||||
self.assertFalse('<td class="location">' in str(r.content))
|
||||
self.assertFalse('<td class="location">' in r.content.decode())
|
||||
|
||||
def test_34_issue_113_app_configs(self):
|
||||
r = self.client.get(self.all_file_list_url)
|
||||
self.assertTrue('rosetta/files/all/xx/1/">Test_App' in str(r.content))
|
||||
self.assertTrue('rosetta/files/all/xx/1/">Test_App' in r.content.decode())
|
||||
|
||||
@override_settings(ROSETTA_STORAGE_CLASS='rosetta.storage.CacheRosettaStorage')
|
||||
def test_35_issue_135_display_exception_messages(self):
|
||||
|
|
@ -733,7 +728,7 @@ class RosettaTestCase(TestCase):
|
|||
def test_39_invalid_get_page(self):
|
||||
url = self.xx_form_url + '?filter=untranslated'
|
||||
|
||||
r = self.client.get(url) # Page not specified
|
||||
r = self.client.get(url) # Page not specified
|
||||
self.assertEqual(r.context['page'], 1)
|
||||
|
||||
r = self.client.get(url + '&page=') # No number given
|
||||
|
|
@ -746,7 +741,6 @@ class RosettaTestCase(TestCase):
|
|||
self.assertEqual(r.context['page'], 1)
|
||||
|
||||
def test_40_issue_155_auto_compile(self):
|
||||
|
||||
def file_hash(file_string):
|
||||
if six.PY3:
|
||||
with open(file_string, encoding="latin-1") as file:
|
||||
|
|
@ -786,32 +780,50 @@ class RosettaTestCase(TestCase):
|
|||
# Disable auto-compilation of the MO when the PO is saved
|
||||
with self.settings(ROSETTA_AUTO_COMPILE=False):
|
||||
# Make a change to the translations
|
||||
po_file_hash_before, mo_file_hash_before = po_file_hash_after, mo_file_hash_after
|
||||
po_file_hash_before, mo_file_hash_before = (
|
||||
po_file_hash_after,
|
||||
mo_file_hash_after,
|
||||
)
|
||||
msg_hashes = message_hashes()
|
||||
data = {msg_hashes['String 1']: "Translation 3"}
|
||||
self.client.post(self.xx_form_url, data)
|
||||
po_file_hash_after, mo_file_hash_after = file_hash(po_file), file_hash(mo_file)
|
||||
po_file_hash_after, mo_file_hash_after = (
|
||||
file_hash(po_file),
|
||||
file_hash(mo_file),
|
||||
)
|
||||
|
||||
# Only the PO should have changed, the MO should be unchanged
|
||||
self.assertNotEqual(po_file_hash_before, po_file_hash_after)
|
||||
self.assertEqual(mo_file_hash_before, mo_file_hash_after)
|
||||
|
||||
# Verify that translating another string also leaves the MO unchanged
|
||||
po_file_hash_before, mo_file_hash_before = po_file_hash_after, mo_file_hash_after
|
||||
po_file_hash_before, mo_file_hash_before = (
|
||||
po_file_hash_after,
|
||||
mo_file_hash_after,
|
||||
)
|
||||
msg_hashes = message_hashes()
|
||||
data = {msg_hashes['String 2']: "Translation 4"}
|
||||
self.client.post(self.xx_form_url, data)
|
||||
po_file_hash_after, mo_file_hash_after = file_hash(po_file), file_hash(mo_file)
|
||||
po_file_hash_after, mo_file_hash_after = (
|
||||
file_hash(po_file),
|
||||
file_hash(mo_file),
|
||||
)
|
||||
|
||||
self.assertNotEqual(po_file_hash_before, po_file_hash_after)
|
||||
self.assertEqual(mo_file_hash_before, mo_file_hash_after)
|
||||
|
||||
with self.settings(ROSETTA_AUTO_COMPILE=True):
|
||||
po_file_hash_before, mo_file_hash_before = po_file_hash_after, mo_file_hash_after
|
||||
po_file_hash_before, mo_file_hash_before = (
|
||||
po_file_hash_after,
|
||||
mo_file_hash_after,
|
||||
)
|
||||
msg_hashes = message_hashes()
|
||||
data = {msg_hashes['String 2']: "Translation 5"}
|
||||
self.client.post(self.xx_form_url, data)
|
||||
po_file_hash_after, mo_file_hash_after = file_hash(po_file), file_hash(mo_file)
|
||||
po_file_hash_after, mo_file_hash_after = (
|
||||
file_hash(po_file),
|
||||
file_hash(mo_file),
|
||||
)
|
||||
|
||||
self.assertNotEqual(po_file_hash_before, po_file_hash_after)
|
||||
self.assertNotEqual(mo_file_hash_before, mo_file_hash_after)
|
||||
|
|
@ -842,9 +854,7 @@ class RosettaTestCase(TestCase):
|
|||
request.user = self.user
|
||||
kwargs = {'po_filter': 'third-party', 'lang_id': 'xx', 'idx': 0}
|
||||
view = self._setup_view(
|
||||
view=views.TranslationFormView(),
|
||||
request=request,
|
||||
**kwargs
|
||||
view=views.TranslationFormView(), request=request, **kwargs
|
||||
)
|
||||
self.assertTrue(view.po_file_is_writable)
|
||||
|
||||
|
|
@ -853,9 +863,7 @@ class RosettaTestCase(TestCase):
|
|||
# make the pofile read-only
|
||||
os.chmod(self.dest_file, 292) # 0444
|
||||
view = self._setup_view(
|
||||
view=views.TranslationFormView(),
|
||||
request=request,
|
||||
**kwargs
|
||||
view=views.TranslationFormView(), request=request, **kwargs
|
||||
)
|
||||
self.assertFalse(view.po_file_is_writable)
|
||||
|
||||
|
|
@ -873,18 +881,16 @@ class RosettaTestCase(TestCase):
|
|||
request.user = self.user
|
||||
kwargs = {'po_filter': 'third-party', 'lang_id': 'xx', 'idx': 0}
|
||||
view = self._setup_view(
|
||||
view=views.TranslationFormView(),
|
||||
request=request,
|
||||
**kwargs
|
||||
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
|
||||
with self.settings(ROSETTA_LANGUAGES=[l for l in settings.LANGUAGES if l[0] != 'xx']):
|
||||
with self.settings(
|
||||
ROSETTA_LANGUAGES=[l for l in settings.LANGUAGES if l[0] != 'xx']
|
||||
):
|
||||
view = self._setup_view(
|
||||
view=views.TranslationFormView(),
|
||||
request=request,
|
||||
**kwargs
|
||||
view=views.TranslationFormView(), request=request, **kwargs
|
||||
)
|
||||
with self.assertRaises(Http404):
|
||||
view.po_file_path
|
||||
|
|
@ -975,16 +981,26 @@ class RosettaTestCase(TestCase):
|
|||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
@vcr.use_cassette('fixtures/vcr_cassettes/test_47_azure_ajax_translation.yaml', match_on=['method', 'scheme', 'host', 'port', 'path', 'query', 'raw_body'], record_mode='new_episodes')
|
||||
@vcr.use_cassette(
|
||||
'fixtures/vcr_cassettes/test_47_azure_ajax_translation.yaml',
|
||||
match_on=['method', 'scheme', 'host', 'port', 'path', 'query', 'raw_body'],
|
||||
record_mode='new_episodes',
|
||||
)
|
||||
def test_47_azure_ajax_translation(self):
|
||||
r = self.client.get(reverse('rosetta.translate_text') + '?from=en&to=fr&text=hello%20world')
|
||||
r = self.client.get(
|
||||
reverse('rosetta.translate_text') + '?from=en&to=fr&text=hello%20world'
|
||||
)
|
||||
self.assertContains(r, '"Salut tout le monde"')
|
||||
|
||||
@override_settings(ROSETTA_REQUIRES_AUTH=True)
|
||||
def test_48_requires_auth_not_respected_issue_203(self):
|
||||
self.client.logout()
|
||||
r = self.client.get(self.all_file_list_url)
|
||||
self.assertRedirects(r, '{}?next=/rosetta/files/all/'.format(settings.LOGIN_URL), fetch_redirect_response=False)
|
||||
self.assertRedirects(
|
||||
r,
|
||||
'{}?next=/rosetta/files/all/'.format(settings.LOGIN_URL),
|
||||
fetch_redirect_response=False,
|
||||
)
|
||||
self.assertEqual(302, r.status_code)
|
||||
|
||||
@override_settings(ROSETTA_REQUIRES_AUTH=False)
|
||||
|
|
@ -996,22 +1012,26 @@ class RosettaTestCase(TestCase):
|
|||
def test_50_custom_login_url(self):
|
||||
self.client.logout()
|
||||
r = self.client.get(self.all_file_list_url)
|
||||
self.assertRedirects(r, '/custom-url/?next=/rosetta/files/all/', fetch_redirect_response=False)
|
||||
self.assertRedirects(
|
||||
r, '/custom-url/?next=/rosetta/files/all/', fetch_redirect_response=False
|
||||
)
|
||||
self.assertEqual(302, r.status_code)
|
||||
|
||||
def test_51_rosetta_languages(self):
|
||||
self.assertTrue('xx' in dict(settings.LANGUAGES))
|
||||
self.assertFalse('yy' in dict(settings.LANGUAGES))
|
||||
|
||||
with self.settings(ROSETTA_LANGUAGES=(('xx', 'foo language'), )):
|
||||
with self.settings(ROSETTA_LANGUAGES=(('xx', 'foo language'),)):
|
||||
r = self.client.get(self.project_file_list_url)
|
||||
self.assertTrue('foo language' in str(r.content))
|
||||
self.assertFalse('bar language' in str(r.content))
|
||||
self.assertTrue('foo language' in r.content.decode())
|
||||
self.assertFalse('bar language' in r.content.decode())
|
||||
|
||||
with self.settings(ROSETTA_LANGUAGES=(('xx', 'foo language'), ('yy', 'bar language'), )):
|
||||
with self.settings(
|
||||
ROSETTA_LANGUAGES=(('xx', 'foo language'), ('yy', 'bar language'))
|
||||
):
|
||||
r = self.client.get(self.project_file_list_url)
|
||||
self.assertTrue('foo language' in str(r.content))
|
||||
self.assertTrue('bar language' in str(r.content))
|
||||
self.assertTrue('foo language' in r.content.decode())
|
||||
self.assertTrue('bar language' in r.content.decode())
|
||||
|
||||
|
||||
# Stubbed access control function
|
||||
|
|
|
|||
243
rosetta/views.py
243
rosetta/views.py
|
|
@ -6,6 +6,7 @@ import re
|
|||
import unicodedata
|
||||
import uuid
|
||||
import zipfile
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
|
|
@ -21,7 +22,7 @@ from django.urls import reverse
|
|||
from django.utils.decorators import method_decorator
|
||||
from django.utils.encoding import force_bytes
|
||||
from django.utils.functional import Promise, cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.generic import TemplateView, View
|
||||
|
||||
|
|
@ -37,14 +38,6 @@ from .signals import entry_changed, post_save
|
|||
from .storage import get_storage
|
||||
|
||||
|
||||
try:
|
||||
# Python 3
|
||||
from urllib.parse import urlencode
|
||||
except ImportError:
|
||||
# Python 2
|
||||
from urllib import urlencode
|
||||
|
||||
|
||||
def get_app_name(path):
|
||||
return path.split('/locale')[0].split('/')[-1]
|
||||
|
||||
|
|
@ -53,12 +46,15 @@ class LoginURL(Promise):
|
|||
"""
|
||||
Tests friendly login URL, url is resolved at runtime.
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
return rosetta_settings.LOGIN_URL
|
||||
|
||||
|
||||
@method_decorator(never_cache, 'dispatch')
|
||||
@method_decorator(user_passes_test(lambda user: can_translate(user), LoginURL()), 'dispatch')
|
||||
@method_decorator(
|
||||
user_passes_test(lambda user: can_translate(user), LoginURL()), 'dispatch'
|
||||
)
|
||||
class RosettaBaseMixin(object):
|
||||
"""A mixin class for Rosetta's class-based views. It provides:
|
||||
* security (see class decorators)
|
||||
|
|
@ -91,6 +87,7 @@ class RosettaFileLevelMixin(RosettaBaseMixin):
|
|||
* po_file (pofile object)
|
||||
* po_file_is_writable (bool: do we have filesystem write perms to file)
|
||||
"""
|
||||
|
||||
def _request_request(self, key, default=None):
|
||||
if key in self.request.GET:
|
||||
return self.request.GET.get(key)
|
||||
|
|
@ -130,11 +127,12 @@ class RosettaFileLevelMixin(RosettaBaseMixin):
|
|||
django_apps = self.po_filter in ('all', 'django')
|
||||
project_apps = self.po_filter in ('all', 'project')
|
||||
|
||||
po_paths = find_pos(self.language_id,
|
||||
project_apps=project_apps,
|
||||
django_apps=django_apps,
|
||||
third_party_apps=third_party_apps,
|
||||
)
|
||||
po_paths = find_pos(
|
||||
self.language_id,
|
||||
project_apps=project_apps,
|
||||
django_apps=django_apps,
|
||||
third_party_apps=third_party_apps,
|
||||
)
|
||||
po_paths.sort(key=get_app_name)
|
||||
|
||||
try:
|
||||
|
|
@ -154,17 +152,18 @@ class RosettaFileLevelMixin(RosettaBaseMixin):
|
|||
# If we can write changes to file, then we pull it up fresh with
|
||||
# each request.
|
||||
# XXX: brittle; what if this path doesn't exist? Isn't a .po file?
|
||||
po_file = pofile(self.po_file_path,
|
||||
wrapwidth=rosetta_settings.POFILE_WRAP_WIDTH)
|
||||
po_file = pofile(
|
||||
self.po_file_path, wrapwidth=rosetta_settings.POFILE_WRAP_WIDTH
|
||||
)
|
||||
for entry in po_file:
|
||||
# Entry is an object representing a single entry in the catalog.
|
||||
# We iterate through the *entire catalog*, pasting a hashed
|
||||
# value of the meat of each entry on its side in an attribute
|
||||
# called "md5hash".
|
||||
str_to_hash = (
|
||||
six.text_type(entry.msgid) +
|
||||
six.text_type(entry.msgstr) +
|
||||
six.text_type(entry.msgctxt or '')
|
||||
six.text_type(entry.msgid)
|
||||
+ six.text_type(entry.msgstr)
|
||||
+ six.text_type(entry.msgctxt or '')
|
||||
).encode('utf8')
|
||||
entry.md5hash = hashlib.md5(str_to_hash).hexdigest()
|
||||
else:
|
||||
|
|
@ -178,9 +177,9 @@ class RosettaFileLevelMixin(RosettaBaseMixin):
|
|||
# a hashed value of the meat of each entry on its side in
|
||||
# an attribute called "md5hash".
|
||||
str_to_hash = (
|
||||
six.text_type(entry.msgid) +
|
||||
six.text_type(entry.msgstr) +
|
||||
six.text_type(entry.msgctxt or '')
|
||||
six.text_type(entry.msgid)
|
||||
+ six.text_type(entry.msgstr)
|
||||
+ six.text_type(entry.msgctxt or '')
|
||||
).encode('utf8')
|
||||
entry.md5hash = hashlib.new('md5', str_to_hash).hexdigest()
|
||||
storage.set(self.po_file_cache_key, po_file)
|
||||
|
|
@ -206,6 +205,7 @@ class TranslationFileListView(RosettaBaseMixin, TemplateView):
|
|||
"""Lists the languages, the gettext catalog files that can be translated,
|
||||
and their translation progress for a filtered list of apps/projects.
|
||||
"""
|
||||
|
||||
http_method_names = ['get']
|
||||
template_name = 'rosetta/file-list.html'
|
||||
|
||||
|
|
@ -222,12 +222,15 @@ class TranslationFileListView(RosettaBaseMixin, TemplateView):
|
|||
if not can_translate_language(self.request.user, language[0]):
|
||||
continue
|
||||
|
||||
po_paths = find_pos(language[0],
|
||||
project_apps=project_apps,
|
||||
django_apps=django_apps,
|
||||
third_party_apps=third_party_apps,
|
||||
)
|
||||
po_files = [(get_app_name(l), os.path.realpath(l), pofile(l)) for l in po_paths]
|
||||
po_paths = find_pos(
|
||||
language[0],
|
||||
project_apps=project_apps,
|
||||
django_apps=django_apps,
|
||||
third_party_apps=third_party_apps,
|
||||
)
|
||||
po_files = [
|
||||
(get_app_name(l), os.path.realpath(l), pofile(l)) for l in po_paths
|
||||
]
|
||||
po_files.sort(key=lambda app: app[0])
|
||||
languages.append((language[0], _(language[1]), po_files))
|
||||
has_pos = has_pos or bool(po_paths)
|
||||
|
|
@ -255,6 +258,7 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
|
|||
searched include: source, translated text, "occurence" file path, or
|
||||
context hints.
|
||||
"""
|
||||
|
||||
# Note: due to the unorthodox nature of the form itself, we're not using
|
||||
# Django's generic FormView as our base class.
|
||||
http_method_names = ['get', 'post']
|
||||
|
|
@ -345,32 +349,38 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
|
|||
file_change = True
|
||||
|
||||
if old_msgstr != new_msgstr or old_fuzzy != is_fuzzy:
|
||||
entry_changed.send(sender=entry,
|
||||
user=request.user,
|
||||
old_msgstr=old_msgstr,
|
||||
old_fuzzy=old_fuzzy,
|
||||
pofile=self.po_file_path,
|
||||
language_code=self.language_id,
|
||||
)
|
||||
entry_changed.send(
|
||||
sender=entry,
|
||||
user=request.user,
|
||||
old_msgstr=old_msgstr,
|
||||
old_fuzzy=old_fuzzy,
|
||||
pofile=self.po_file_path,
|
||||
language_code=self.language_id,
|
||||
)
|
||||
else:
|
||||
messages.error(
|
||||
self.request,
|
||||
_("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."),
|
||||
_(
|
||||
"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:
|
||||
self.po_file.metadata['Last-Translator'] = unicodedata.normalize(
|
||||
'NFKD', u"%s %s <%s>" % (
|
||||
'NFKD',
|
||||
u"%s %s <%s>"
|
||||
% (
|
||||
getattr(self.request.user, 'first_name', 'Anonymous'),
|
||||
getattr(self.request.user, 'last_name', 'User'),
|
||||
getattr(self.request.user, 'email', 'anonymous@user.tld')
|
||||
)
|
||||
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())
|
||||
get_rosetta_version()
|
||||
)
|
||||
self.po_file.metadata['PO-Revision-Date'] = timestamp_with_timezone()
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
|
|
@ -382,16 +392,16 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
|
|||
if rosetta_settings.AUTO_COMPILE:
|
||||
self.po_file.save_as_mofile(po_filepath + '.mo')
|
||||
|
||||
post_save.send(sender=None, language_code=self.language_id,
|
||||
request=self.request
|
||||
)
|
||||
post_save.send(
|
||||
sender=None, language_code=self.language_id, request=self.request
|
||||
)
|
||||
# Try auto-reloading via the WSGI daemon mode reload mechanism
|
||||
should_try_wsgi_reload = (
|
||||
rosetta_settings.WSGI_AUTO_RELOAD and
|
||||
'mod_wsgi.process_group' in self.request.environ and
|
||||
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))
|
||||
rosetta_settings.WSGI_AUTO_RELOAD
|
||||
and 'mod_wsgi.process_group' in self.request.environ
|
||||
and 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:
|
||||
|
|
@ -402,6 +412,7 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
|
|||
if rosetta_settings.UWSGI_AUTO_RELOAD:
|
||||
try:
|
||||
import uwsgi
|
||||
|
||||
uwsgi.reload() # pretty easy right?
|
||||
except:
|
||||
pass # we may not be running under uwsgi :P
|
||||
|
|
@ -433,10 +444,12 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
|
|||
}
|
||||
# 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_safe(query_string_args)
|
||||
))
|
||||
return HttpResponseRedirect(
|
||||
"{url}?{qs}".format(
|
||||
url=reverse('rosetta-form', kwargs=self.kwargs),
|
||||
qs=urlencode_safe(query_string_args),
|
||||
)
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(TranslationFormView, self).get_context_data(**kwargs)
|
||||
|
|
@ -491,8 +504,7 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
|
|||
break
|
||||
if main_language:
|
||||
main_lang_po_path = self.po_file_path.replace(
|
||||
'/%s/' % self.language_id,
|
||||
'/%s/' % main_language_id,
|
||||
'/%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)
|
||||
|
|
@ -521,29 +533,31 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
|
|||
{k: v for k, v in query_string_args.items() if k == 'ref_lang'}
|
||||
)
|
||||
|
||||
context.update({
|
||||
'version': get_rosetta_version(),
|
||||
'LANGUAGES': LANGUAGES,
|
||||
'rosetta_settings': rosetta_settings,
|
||||
'rosetta_i18n_lang_name': rosetta_i18n_lang_name,
|
||||
'rosetta_i18n_lang_code': self.language_id,
|
||||
'rosetta_i18n_lang_code_normalized': self.language_id.replace('_', '-'),
|
||||
'rosetta_i18n_lang_bidi': rosetta_i18n_lang_bidi,
|
||||
'rosetta_i18n_filter': self.msg_filter,
|
||||
'rosetta_i18n_write': self.po_file_is_writable,
|
||||
'rosetta_messages': rosetta_messages,
|
||||
'page_range': needs_pagination and page_range,
|
||||
'needs_pagination': needs_pagination,
|
||||
'main_language': main_language,
|
||||
'rosetta_i18n_app': get_app_name(self.po_file_path),
|
||||
'page': page,
|
||||
'query': self.query,
|
||||
'pagination_query_string_base': pagination_query_string_base,
|
||||
'filter_query_string_base': filter_query_string_base,
|
||||
'paginator': paginator,
|
||||
'rosetta_i18n_pofile': self.po_file,
|
||||
'ref_lang': self.ref_lang,
|
||||
})
|
||||
context.update(
|
||||
{
|
||||
'version': get_rosetta_version(),
|
||||
'LANGUAGES': LANGUAGES,
|
||||
'rosetta_settings': rosetta_settings,
|
||||
'rosetta_i18n_lang_name': rosetta_i18n_lang_name,
|
||||
'rosetta_i18n_lang_code': self.language_id,
|
||||
'rosetta_i18n_lang_code_normalized': self.language_id.replace('_', '-'),
|
||||
'rosetta_i18n_lang_bidi': rosetta_i18n_lang_bidi,
|
||||
'rosetta_i18n_filter': self.msg_filter,
|
||||
'rosetta_i18n_write': self.po_file_is_writable,
|
||||
'rosetta_messages': rosetta_messages,
|
||||
'page_range': needs_pagination and page_range,
|
||||
'needs_pagination': needs_pagination,
|
||||
'main_language': main_language,
|
||||
'rosetta_i18n_app': get_app_name(self.po_file_path),
|
||||
'page': page,
|
||||
'query': self.query,
|
||||
'pagination_query_string_base': pagination_query_string_base,
|
||||
'filter_query_string_base': filter_query_string_base,
|
||||
'paginator': paginator,
|
||||
'rosetta_i18n_pofile': self.po_file,
|
||||
'ref_lang': self.ref_lang,
|
||||
}
|
||||
)
|
||||
|
||||
return context
|
||||
|
||||
|
|
@ -569,11 +583,10 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
|
|||
ref_pofile = None
|
||||
if rosetta_settings.ENABLE_REFLANG and self.ref_lang != 'msgid':
|
||||
replacement = '{separator}locale{separator}{ref_lang}'.format(
|
||||
separator=os.sep,
|
||||
ref_lang=self.ref_lang
|
||||
separator=os.sep, ref_lang=self.ref_lang
|
||||
)
|
||||
pattern = r'\{separator}locale\{separator}[a-z]{{2}}'.format(separator=os.sep)
|
||||
ref_fn = re.sub(pattern, replacement, self.po_file_path,)
|
||||
ref_fn = re.sub(pattern, replacement, self.po_file_path)
|
||||
try:
|
||||
ref_pofile = pofile(ref_fn)
|
||||
except IOError:
|
||||
|
|
@ -616,17 +629,21 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
|
|||
rx = re.compile(re.escape(self.query), re.IGNORECASE)
|
||||
|
||||
def concat_entry(e):
|
||||
return (six.text_type(e.msgstr) +
|
||||
six.text_type(e.msgid) +
|
||||
six.text_type(e.msgctxt) +
|
||||
six.text_type(e.comment) +
|
||||
u''.join([o[0] for o in e.occurrences]) +
|
||||
six.text_type(e.msgid_plural) +
|
||||
u''.join(e.msgstr_plural.values())
|
||||
)
|
||||
return (
|
||||
six.text_type(e.msgstr)
|
||||
+ six.text_type(e.msgid)
|
||||
+ six.text_type(e.msgctxt)
|
||||
+ six.text_type(e.comment)
|
||||
+ u''.join([o[0] for o in e.occurrences])
|
||||
+ six.text_type(e.msgid_plural)
|
||||
+ u''.join(e.msgstr_plural.values())
|
||||
)
|
||||
|
||||
entries = [e_ for e_ in self.po_file
|
||||
if not e_.obsolete and rx.search(concat_entry(e_))]
|
||||
entries = [
|
||||
e_
|
||||
for e_ in self.po_file
|
||||
if not e_.obsolete and rx.search(concat_entry(e_))
|
||||
]
|
||||
else:
|
||||
# Scenario #2: filtered list of messages
|
||||
if self.msg_filter == 'untranslated':
|
||||
|
|
@ -634,8 +651,7 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
|
|||
elif self.msg_filter == 'translated':
|
||||
entries = self.po_file.translated_entries()
|
||||
elif self.msg_filter == 'fuzzy':
|
||||
entries = [e_ for e_ in self.po_file.fuzzy_entries()
|
||||
if not e_.obsolete]
|
||||
entries = [e_ for e_ in self.po_file.fuzzy_entries() if not e_.obsolete]
|
||||
else:
|
||||
# ("all")
|
||||
entries = [e_ for e_ in self.po_file if not e_.obsolete]
|
||||
|
|
@ -647,6 +663,7 @@ class TranslationFileDownload(RosettaFileLevelMixin, View):
|
|||
and compiled (.mo) files, either as they exist on disk, or, if what's on
|
||||
disk is unwritable (permissions-wise), return what's in the cache.
|
||||
"""
|
||||
|
||||
http_method_names = [u'get']
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
|
@ -677,7 +694,6 @@ class TranslationFileDownload(RosettaFileLevelMixin, View):
|
|||
|
||||
@user_passes_test(lambda user: can_translate(user), LoginURL())
|
||||
def translate_text(request):
|
||||
|
||||
def translate(text, from_language, to_language, subscription_key):
|
||||
"""
|
||||
This method does the heavy lifting of connecting to the translator API and fetching a response
|
||||
|
|
@ -696,20 +712,20 @@ def translate_text(request):
|
|||
headers = {
|
||||
'Ocp-Apim-Subscription-Key': subscription_key,
|
||||
'Content-type': 'application/json',
|
||||
'X-ClientTraceId': str(uuid.uuid4())
|
||||
'X-ClientTraceId': str(uuid.uuid4()),
|
||||
}
|
||||
|
||||
url_parameters = {
|
||||
"from": from_language,
|
||||
"to": to_language
|
||||
}
|
||||
url_parameters = {"from": from_language, "to": to_language}
|
||||
|
||||
request_data = [
|
||||
{"text": text}
|
||||
]
|
||||
request_data = [{"text": text}]
|
||||
|
||||
api_hostname = AZURE_TRANSLATOR_HOST + AZURE_TRANSLATOR_PATH
|
||||
r = requests.post(api_hostname, headers=headers, params=url_parameters, data=json.dumps(request_data))
|
||||
r = requests.post(
|
||||
api_hostname,
|
||||
headers=headers,
|
||||
params=url_parameters,
|
||||
data=json.dumps(request_data),
|
||||
)
|
||||
return json.loads(r.text)
|
||||
|
||||
language_from = request.GET.get('from', None)
|
||||
|
|
@ -723,7 +739,9 @@ def translate_text(request):
|
|||
AZURE_CLIENT_SECRET = getattr(settings, 'AZURE_CLIENT_SECRET', None)
|
||||
|
||||
try:
|
||||
api_response = translate(text, language_from, language_to, AZURE_CLIENT_SECRET)
|
||||
api_response = translate(
|
||||
text, language_from, language_to, AZURE_CLIENT_SECRET
|
||||
)
|
||||
|
||||
# result will be a dict if there is an error, e.g.
|
||||
# {
|
||||
|
|
@ -737,7 +755,9 @@ def translate_text(request):
|
|||
error_message = api_error.get("message")
|
||||
data = {
|
||||
'success': False,
|
||||
'error': "Microsoft Translation API error: Error code {}, {}".format(error_code, error_message),
|
||||
'error': "Microsoft Translation API error: Error code {}, {}".format(
|
||||
error_code, error_message
|
||||
),
|
||||
}
|
||||
else:
|
||||
# response body will be of the form:
|
||||
|
|
@ -753,15 +773,14 @@ def translate_text(request):
|
|||
|
||||
translations = api_response[0].get("translations")
|
||||
translated_text = translations[0].get("text")
|
||||
data = {
|
||||
'success': True,
|
||||
'translation': translated_text
|
||||
}
|
||||
data = {'success': True, 'translation': translated_text}
|
||||
# catch general connection exception in the requests framework
|
||||
except requests.exceptions.RequestException as err:
|
||||
data = {
|
||||
'success': False,
|
||||
'error': "Error connecting to Microsoft Translation Service: {0}".format(err),
|
||||
'error': "Error connecting to Microsoft Translation Service: {0}".format(
|
||||
err
|
||||
),
|
||||
}
|
||||
|
||||
return JsonResponse(data)
|
||||
|
|
|
|||
11
setup.py
11
setup.py
|
|
@ -1,7 +1,8 @@
|
|||
from setuptools import setup, find_packages
|
||||
from setuptools.command.test import test as test_command
|
||||
import sys
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
from setuptools.command.test import test as test_command
|
||||
|
||||
|
||||
class Tox(test_command):
|
||||
user_options = [('tox-args=', 'a', "Arguments to pass to tox")]
|
||||
|
|
@ -19,12 +20,14 @@ class Tox(test_command):
|
|||
# import here, cause outside the eggs aren't loaded
|
||||
import tox
|
||||
import shlex
|
||||
|
||||
args = self.tox_args
|
||||
if args:
|
||||
args = shlex.split(self.tox_args)
|
||||
errno = tox.cmdline(args=args)
|
||||
sys.exit(errno)
|
||||
|
||||
|
||||
with open('README.rst') as readme:
|
||||
long_description = readme.read()
|
||||
|
||||
|
|
@ -57,9 +60,9 @@ setup(
|
|||
zip_safe=False,
|
||||
install_requires=[
|
||||
'six >=1.2.0',
|
||||
'Django >= 1.11',
|
||||
'Django >= 2.0',
|
||||
'requests >= 2.1.0',
|
||||
'polib >= 1.1.0'
|
||||
'polib >= 1.1.0',
|
||||
],
|
||||
tests_require=['tox', 'vcrpy'],
|
||||
cmdclass={'test': Tox},
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# from __future__ import unicode_literals
|
||||
import django
|
||||
import os
|
||||
import sys
|
||||
|
||||
import django
|
||||
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
|
|
@ -15,7 +14,7 @@ DJANGO_VERSION = django.get_version()
|
|||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(PROJECT_PATH, 'rosetta.db')
|
||||
'NAME': os.path.join(PROJECT_PATH, 'rosetta.db'),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -23,7 +22,7 @@ CACHES = {
|
|||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
|
||||
'LOCATION': '127.0.0.1:11211',
|
||||
'KEY_PREFIX': 'ROSETTA_TEST'
|
||||
'KEY_PREFIX': 'ROSETTA_TEST',
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +57,7 @@ MIDDLEWARE = (
|
|||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware'
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
)
|
||||
|
||||
# Note: languages are overridden in the test runner
|
||||
|
|
@ -75,15 +74,11 @@ LANGUAGES = (
|
|||
SILENCED_SYSTEM_CHECKS = ["translation.E002"]
|
||||
|
||||
|
||||
LOCALE_PATHS = [
|
||||
os.path.join(PROJECT_PATH, 'locale'),
|
||||
]
|
||||
LOCALE_PATHS = [os.path.join(PROJECT_PATH, 'locale')]
|
||||
|
||||
SOUTH_TESTS_MIGRATE = False
|
||||
|
||||
FIXTURE_DIRS = (
|
||||
os.path.join(PROJECT_PATH, 'fixtures'),
|
||||
)
|
||||
FIXTURE_DIRS = (os.path.join(PROJECT_PATH, 'fixtures'),)
|
||||
STATIC_URL = '/static/'
|
||||
ROOT_URLCONF = 'testproject.urls'
|
||||
|
||||
|
|
@ -102,10 +97,10 @@ TEMPLATES = [
|
|||
"django.template.context_processors.media",
|
||||
"django.template.context_processors.static",
|
||||
"django.template.context_processors.tz",
|
||||
"django.contrib.messages.context_processors.messages"
|
||||
)
|
||||
}
|
||||
},
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
),
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
|
|
|||
8
tox.ini
8
tox.ini
|
|
@ -1,7 +1,6 @@
|
|||
[tox]
|
||||
envlist =
|
||||
flake8,
|
||||
py{27,36}-django111,
|
||||
py{36,37}-django{20,21},
|
||||
py{36,37,38}-django{22,30},
|
||||
gettext,
|
||||
|
|
@ -19,16 +18,13 @@ setenv =
|
|||
PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
deps =
|
||||
django111: Django==1.11.*
|
||||
django20: Django==2.0.*
|
||||
django21: Django>=2.1a1,<=2.1.99
|
||||
django22: Django>=2.2.8,<=2.2.99
|
||||
django30: Django>=3.0,<=3.0.99
|
||||
|
||||
py27-django111: python-memcached
|
||||
py{36,37,38}-django{111,20,21,22,30}: python3-memcached
|
||||
py{36,37,38}-django{20,21,22,30}: python3-memcached
|
||||
|
||||
# py27-django111: pudb
|
||||
requests
|
||||
polib>=1.1.0
|
||||
six
|
||||
|
|
@ -62,9 +58,9 @@ changedir = docs
|
|||
commands=
|
||||
sphinx-build -W -b html . _build/html
|
||||
|
||||
|
||||
[testenv:flake8]
|
||||
basepython = python3
|
||||
deps = flake8==2.4.1
|
||||
commands=
|
||||
flake8 {toxinidir}/rosetta
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue