Revision 8:92f04bf7349c
b/txapp/__init__.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
|
|
3 |
""" |
|
4 |
Django app to interact with Transifex. |
|
5 |
""" |
|
6 |
|
|
7 |
|
|
8 |
from txapp.init import setup |
|
9 |
setup() |
|
10 |
|
b/txapp/auth.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
|
|
3 |
""" |
|
4 |
Auth framework for the client of the API. |
|
5 |
""" |
|
6 |
|
|
7 |
import os |
|
8 |
import base64 |
|
9 |
import hashlib |
|
10 |
import binascii |
|
11 |
from txapp.models import AuthToken |
|
12 |
|
|
13 |
|
|
14 |
def generate_token(length=16): |
|
15 |
"""Generate a new authentication token.""" |
|
16 |
return base64.b64encode(os.urandom(length)) |
|
17 |
|
|
18 |
|
|
19 |
def create_auth_token(project): |
|
20 |
"""Create and save an authentication token to be used for the project. |
|
21 |
|
|
22 |
Args: |
|
23 |
project: The project the token is for. |
|
24 |
Returns: |
|
25 |
The token. |
|
26 |
""" |
|
27 |
token = generate_token() |
|
28 |
auth_info, created = AuthToken.objects.get_or_create(project=project) |
|
29 |
auth_info.token = _hash_token(token) |
|
30 |
auth_info.save() |
|
31 |
return token |
|
32 |
|
|
33 |
|
|
34 |
def clear_auth_token(project): |
|
35 |
"""Clear the authentication token used by a project.""" |
|
36 |
AuthToken.objects.filter(project=project).delete() |
|
37 |
|
|
38 |
|
|
39 |
def auth_token_exists(project): |
|
40 |
"""Return whether an auth token for the proejct exists already. |
|
41 |
|
|
42 |
Args: |
|
43 |
project: The project to check whether an auth token exists for. |
|
44 |
Returns: |
|
45 |
True, if it exists, else false. |
|
46 |
""" |
|
47 |
return AuthToken.objects.filter(project=project).exists() |
|
48 |
|
|
49 |
|
|
50 |
def authenticate(project_slug, token): |
|
51 |
"""Authenticate a project against the given token. |
|
52 |
|
|
53 |
Args: |
|
54 |
project_slug: The slug of the project to authenticate. |
|
55 |
token: The authentication token. |
|
56 |
Returns: |
|
57 |
True for success, False for failure. |
|
58 |
""" |
|
59 |
try: |
|
60 |
auth = AuthToken.objects.get(project__slug=project_slug) |
|
61 |
except AuthToken.DoesNotExist: |
|
62 |
return False |
|
63 |
|
|
64 |
salt = auth.token.split('$')[2] |
|
65 |
hashed_token = _hash_token(token, salt) |
|
66 |
|
|
67 |
# protect against timing attacks |
|
68 |
return int(binascii.b2a_hex(auth.token), 16) ^ int(binascii.b2a_hex(hashed_token), 16) == 0 |
|
69 |
|
|
70 |
|
|
71 |
def _hash_token(token, salt_used=None): |
|
72 |
"""Hash the token using the SHA512 function.""" |
|
73 |
salt = base64.b64encode(os.urandom(4)) if salt_used is None else salt_used |
|
74 |
algo_id = "6" # see man 3 crypt |
|
75 |
|
|
76 |
h = hashlib.sha512(salt) |
|
77 |
h.update(token) |
|
78 |
return "$".join(['', algo_id, salt, h.hexdigest()]).decode() |
|
79 |
|
|
80 |
def _do_match(hashed_token, token): |
|
81 |
"""Check whether the hash of the token is the hashed_token. |
|
82 |
|
|
83 |
Use a XOR operation to avoid timing attacks. |
|
84 |
""" |
|
85 |
hashed_token = hashed_token[1:] |
|
86 |
(algo_id, salt, digest) = hashed_token.split('$') |
|
87 |
assert algo_id == 6 |
|
88 |
new_digest = _hash_token(token, salt=salt) |
|
89 |
return digest ^ new_digest == 0 |
b/txapp/exceptions.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
|
|
3 |
""" |
|
4 |
Exceptions for a TxApp. |
|
5 |
""" |
|
6 |
|
|
7 |
|
|
8 |
class TxAppError(Exception): |
|
9 |
"""Base class for all TxApp related exceptions.""" |
|
10 |
|
|
11 |
|
|
12 |
class NetworkError(Exception): |
|
13 |
"""There was a network error while contacting Transifex.""" |
|
14 |
|
|
15 |
|
|
16 |
class ApiError(Exception): |
|
17 |
"""There was an error with the API request.""" |
|
18 |
|
|
19 |
|
|
20 |
class AuthError(Exception): |
|
21 |
"""There was an authentication error with the API request.""" |
b/txapp/init.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
|
|
3 |
""" |
|
4 |
Initialization functions for txapp. |
|
5 |
""" |
|
6 |
|
|
7 |
|
|
8 |
from django.conf import settings |
|
9 |
from txlib.http.http_requests import HttpRequest |
|
10 |
from txlib.registry import registry |
|
11 |
|
|
12 |
|
|
13 |
def setup(): |
|
14 |
"""Setup Transifex related functions.""" |
|
15 |
registry.http_handler = HttpRequest(settings.TX_URL) |
b/txapp/log.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
|
|
3 |
""" |
|
4 |
Log settings for txapp. |
|
5 |
""" |
|
6 |
|
|
7 |
import logging |
|
8 |
|
|
9 |
|
|
10 |
logger = logging.getLogger('txapp') |
b/txapp/models.py | ||
---|---|---|
1 |
from django.db import models |
|
2 |
|
|
3 |
|
|
4 |
class Project(models.Model): |
|
5 |
"""Save the settings per project.""" |
|
6 |
|
|
7 |
slug = models.SlugField( |
|
8 |
verbose_name='Slug', max_length=30, primary_key=True, |
|
9 |
help_text=('A short label to be used in the URL, containing only ' |
|
10 |
'letters, numbers, underscores or hyphens.') |
|
11 |
) |
|
12 |
source_language = models.CharField( |
|
13 |
verbose_name='Drupal source language', max_length=10, default='en', |
|
14 |
help_text="The language code used in the Drupal installation." |
|
15 |
) |
|
16 |
|
|
17 |
|
|
18 |
def __unicode__(self): |
|
19 |
return u"<Project: %s>" % self.slug |
|
20 |
|
|
21 |
|
|
22 |
class AuthToken(models.Model): |
|
23 |
"""Authentication token for projects.""" |
|
24 |
|
|
25 |
token = models.CharField( |
|
26 |
'Token', max_length=100, |
|
27 |
help_text="The authentication token used by the other end-point." |
|
28 |
) |
|
29 |
project = models.OneToOneField( |
|
30 |
'Project', verbose_name='Project', unique=True, |
|
31 |
help_text="The project this token is for." |
|
32 |
) |
|
33 |
|
|
34 |
|
|
35 |
class LanguageMapping(models.Model): |
|
36 |
"""Save language mappings.""" |
|
37 |
|
|
38 |
tx_lang = models.CharField( |
|
39 |
verbose_name='Transifex language', max_length=10, unique=True, |
|
40 |
help_text="The language code on the Tx side." |
|
41 |
) |
|
42 |
remote_lang = models.CharField( |
|
43 |
verbose_name="Remote langauge", max_length=10, unique=True, |
|
44 |
help_text="The language code on the remote side." |
|
45 |
) |
b/txapp/response.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
|
|
3 |
from django.utils import simplejson as json |
|
4 |
from django.template.response import TemplateResponse |
|
5 |
|
|
6 |
|
|
7 |
class TxTemplateResponse(TemplateResponse): |
|
8 |
"""Transifex specific response class.""" |
|
9 |
|
|
10 |
@property |
|
11 |
def rendered_content(self): |
|
12 |
""" |
|
13 |
Render the content. |
|
14 |
|
|
15 |
Convert the content to a suitable json-encoded string to send to |
|
16 |
Transifex. |
|
17 |
""" |
|
18 |
content = super(TxTemplateResponse, self).rendered_content |
|
19 |
return self._convert_to_json(content) |
|
20 |
|
|
21 |
def _convert_to_json(self, content): |
|
22 |
"""Convert the content to a json-encoded string along |
|
23 |
with any other information necessary. |
|
24 |
""" |
|
25 |
data = {'content': content} |
|
26 |
return json.dumps(data) |
b/txapp/signals.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
|
|
3 |
""" |
|
4 |
Signals raised by txapp. |
|
5 |
""" |
|
6 |
|
|
7 |
from django.dispatch import Signal |
|
8 |
|
|
9 |
|
|
10 |
# Signal raised whenever Transifex informs us about an update in a resource. |
|
11 |
translation_updated_signal = Signal( |
|
12 |
providing_args=["project", "resource", "language", "percentage", ] |
|
13 |
) |
b/txapp/templates/txapp_base.html | ||
---|---|---|
1 |
{% block txapp_body %} |
|
2 |
{% endblock %} |
b/txapp/templates/txapp_overview.html | ||
---|---|---|
1 |
{% block txapp_body %} |
|
2 |
<p>Default overview</p> |
|
3 |
{% endblock %} |
b/txapp/templates/txapp_settings.html | ||
---|---|---|
1 |
{% block txapp_body %} |
|
2 |
<p>Default settings</p> |
|
3 |
{% endblock %} |
b/txapp/tests.py | ||
---|---|---|
1 |
""" |
|
2 |
This file demonstrates writing tests using the unittest module. These will pass |
|
3 |
when you run "manage.py test". |
|
4 |
|
|
5 |
Replace this with more appropriate tests for your application. |
|
6 |
""" |
|
7 |
|
|
8 |
from django.test import TestCase |
|
9 |
from txapp.auth import generate_token, _hash_token, _match_token |
|
10 |
|
|
11 |
|
|
12 |
class TestAuth(TestCase): |
|
13 |
"""Test the authentication framework.""" |
|
14 |
|
|
15 |
def test_hash_format(self): |
|
16 |
"""Test the format of the hash generated.""" |
|
17 |
token = generate_token() |
|
18 |
hashed = _hash_token(token) |
|
19 |
self.assertEqual(hashed.count('$'), 4) |
|
20 |
|
|
21 |
def test_check_match(self): |
|
22 |
token1 = generate_token() |
|
23 |
original_digest = _hash_token(token1) |
|
24 |
self.assertTrue(_match_token(original_digest, token1)) |
|
25 |
token2 = ~token1 |
|
26 |
self.assertFalse(_match_token(original_digest, token2)) |
|
27 |
|
b/txapp/txwrapper.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
|
|
3 |
""" |
|
4 |
Tx API related functions. |
|
5 |
|
|
6 |
TODO: Fine-grained handling of the exceptions (see docstrings). |
|
7 |
""" |
|
8 |
|
|
9 |
from txlib.api.resources import Resource |
|
10 |
from txlib.api.translations import Translation |
|
11 |
from txapp.exceptions import NetworkError, ApiError, AuthError |
|
12 |
from txapp.models import Project |
|
13 |
from txapp.log import logger |
|
14 |
|
|
15 |
|
|
16 |
def create_resource(project_slug, resource_slug, **kwargs): |
|
17 |
"""Create a resource to Tx through the API. |
|
18 |
|
|
19 |
There are certain variables the user must submit, documented in the API of |
|
20 |
Transifex. All arguments should be passed as keyword arguments. |
|
21 |
|
|
22 |
Args: |
|
23 |
project_slug: The slug of the project in Transifex under which the |
|
24 |
resource will be created. |
|
25 |
resource_slug: The slug of the new resource. |
|
26 |
name: The name of the new resource. |
|
27 |
i18n_type: The type of the new resource. |
|
28 |
source_language: The source language for the new resource. Default is |
|
29 |
'en'. |
|
30 |
content: The source content of the new resource. |
|
31 |
auth: The authentication info to use in the API calls. |
|
32 |
Raises: |
|
33 |
NetworkError, in case of a network error in contacting Transifex. |
|
34 |
ApiError, in case of an error with the API request. |
|
35 |
AuthError, in case of an authentication error with the API reuqest. |
|
36 |
""" |
|
37 |
|
|
38 |
try: |
|
39 |
name = kwargs['name'] |
|
40 |
i18n_type = kwargs['i18n_type'] |
|
41 |
auth = kwargs['auth'] |
|
42 |
content = kwargs['content'] |
|
43 |
source_language = kwargs.get('source_language', 'en') |
|
44 |
except KeyError, e: |
|
45 |
logger.error("Error in arguments: %s" % e) |
|
46 |
raise ArgumentError(unicode(e)) |
|
47 |
|
|
48 |
logger.debug("Creating the resource %s.%s" % (project_slug, resource_slug)) |
|
49 |
p = Project.objects.get(slug=project_slug) |
|
50 |
try: |
|
51 |
r = Resource( |
|
52 |
project_slug=project_slug, slug=resource_slug, auth=auth |
|
53 |
) |
|
54 |
except Exception, e: |
|
55 |
logger.error(unicode(e), exc_info=True) |
|
56 |
raise |
|
57 |
r.name = name |
|
58 |
r.mimetype = i18n_type |
|
59 |
r.content = content |
|
60 |
r.source_language = source_language |
|
61 |
try: |
|
62 |
r.save() |
|
63 |
logger.info("Created resource %s.%s" % (project_slug, resource_slug)) |
|
64 |
except Exception, e: |
|
65 |
logger.error("Error creating resource: %s" % unicode(e), exc_info=True) |
|
66 |
raise ApiError(unicode(e)) |
|
67 |
|
|
68 |
|
|
69 |
def update_source_content(project_slug, resource_slug, **kwargs): |
|
70 |
"""Update the source content of a resource through the API. |
|
71 |
|
|
72 |
Args: |
|
73 |
project_slug: The slug of the project the resource belongs to. |
|
74 |
resource_slug: The slug of the resource to update. |
|
75 |
content: The source content of the new resource. |
|
76 |
auth: The authentication info to use in the API calls. |
|
77 |
Raises: |
|
78 |
NetworkError, in case of a network error in contacting Transifex. |
|
79 |
ApiError, in case of an error with the API request. |
|
80 |
AuthError, in case of an authentication error with the API reuqest. |
|
81 |
""" |
|
82 |
try: |
|
83 |
content = kwargs['content'] |
|
84 |
auth = kwargs['auth'] |
|
85 |
except KeyError, e: |
|
86 |
logger.error("Error in arguments: %s" % e) |
|
87 |
raise ArgumentError(unicode(e)) |
|
88 |
|
|
89 |
try: |
|
90 |
r = Resource.get( |
|
91 |
project_slug=project_slug, slug=resource_slug, auth=auth |
|
92 |
) |
|
93 |
except Exception, e: |
|
94 |
msg = "Error fetching resource for %s.%s: %s" |
|
95 |
logger.error(msg % (project_slug, resource_slug, e), exc_info=True) |
|
96 |
raise |
|
97 |
|
|
98 |
r.content = content |
|
99 |
try: |
|
100 |
r.save() |
|
101 |
msg = "Updated source content of resource %s.%s" |
|
102 |
logger.info(msg % (project_slug, resource_slug)) |
|
103 |
except Exception, e: |
|
104 |
msg = "Error saving translation for %s.%s: %s" |
|
105 |
logger.error(msg % (project_slug, resource_slug, e), exc_info=True) |
|
106 |
raise |
|
107 |
|
|
108 |
|
|
109 |
def get_translation(project_slug, resource_slug, language_code, **kwargs): |
|
110 |
"""Get a translation for a specific resource throug hthe API. |
|
111 |
|
|
112 |
Args: |
|
113 |
project_slug: The slug of the project the resource belongs to. |
|
114 |
resource_slug: The slug of the resource to update. |
|
115 |
language_code: The code of the language of the Translation. |
|
116 |
Returns: |
|
117 |
The translated content. |
|
118 |
Raises: |
|
119 |
NetworkError, in case of a network error in contacting Transifex. |
|
120 |
ApiError, in case of an error with the API request. |
|
121 |
AuthError, in case of an authentication error with the API reuqest. |
|
122 |
""" |
|
123 |
try: |
|
124 |
auth = kwargs['auth'] |
|
125 |
except KeyError, e: |
|
126 |
logger.error("Error in arguments: %s" % e) |
|
127 |
raise ArgumentError(unicode(e)) |
|
128 |
|
|
129 |
try: |
|
130 |
t = Translation.get( |
|
131 |
project_slug=project_slug, slug=resource_slug, |
|
132 |
lang=language_code, auth=auth |
|
133 |
) |
|
134 |
except Exception, e: |
|
135 |
msg = ( |
|
136 |
"Error with getting the translation of resource %s.%s " |
|
137 |
"for the language %s: %s" |
|
138 |
) |
|
139 |
logger.error( |
|
140 |
msg % (project_slug, resource_slug, language_code, e), |
|
141 |
exc_info=True |
|
142 |
) |
|
143 |
raise |
|
144 |
return t.content |
|
145 |
|
|
146 |
|
|
147 |
def delete_resource(project_slug, resource_slug, **kwargs): |
|
148 |
"""Delete a resource identified by (project_slug, resource_slug) in |
|
149 |
Transifex. |
|
150 |
|
|
151 |
Args: |
|
152 |
project_slug: The slug of the project the resource belongs to. |
|
153 |
resource_slug: The slug of the resource to delete. |
|
154 |
Raises: |
|
155 |
NetworkError, in case of a network error in contacting Transifex. |
|
156 |
ApiError, in case of an error with the API request. |
|
157 |
AuthError, in case of an authentication error with the API reuqest. |
|
158 |
""" |
|
159 |
try: |
|
160 |
auth = kwargs['auth'] |
|
161 |
except KeyError, e: |
|
162 |
logger.error("Error in arguments: %s" % e) |
|
163 |
raise ArgumentError(unicode(e)) |
|
164 |
|
|
165 |
try: |
|
166 |
r = Resource.get( |
|
167 |
project_slug=project_slug, slug=resource_slug, auth=auth |
|
168 |
) |
|
169 |
except Exception, e: |
|
170 |
msg = "Error fetching resource for %s.%s: %s" |
|
171 |
logger.error(msg % (project_slug, resource_slug, e), exc_info=True) |
|
172 |
raise |
|
173 |
|
|
174 |
try: |
|
175 |
r.delete() |
|
176 |
msg = "Delete resource %s.%s" |
|
177 |
logger.info(msg % (project_slug, resource_slug)) |
|
178 |
except Exception, e: |
|
179 |
msg = "Error deleting resource %s.%s: %s" |
|
180 |
logger.error(msg % (project_slug, resource_slug, e), exc_info=True) |
|
181 |
raise |
b/txapp/urls.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
|
|
3 |
from django.conf.urls.defaults import * |
|
4 |
from txapp.views import translation_updated, register_project, \ |
|
5 |
unregister_project |
|
6 |
|
|
7 |
|
|
8 |
urlpatterns = patterns('', |
|
9 |
url( |
|
10 |
r'^translation_updated/$', translation_updated, name='translation_updated' |
|
11 |
), |
|
12 |
url( |
|
13 |
r'^register/(?P<project_slug>[-\w]+)$', register_project, name='register_project' |
|
14 |
), |
|
15 |
url( |
|
16 |
r'^unregister/(?P<project_slug>[-\w]+)$', unregister_project, name='unregister_project' |
|
17 |
), |
|
18 |
) |
b/txapp/utils.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
|
|
3 |
""" |
|
4 |
Utility functions for a TxApp. |
|
5 |
""" |
|
6 |
|
|
7 |
from txapp.models import Project |
|
8 |
|
|
9 |
|
|
10 |
def register_project(project_slug): |
|
11 |
"""Register a project to the TxApp. |
|
12 |
|
|
13 |
Args: |
|
14 |
project_slug: The slug of the project to register. |
|
15 |
""" |
|
16 |
Project.objects.get_or_create(slug=project_slug) |
b/txapp/views.py | ||
---|---|---|
1 |
from django.utils import simplejson as json |
|
2 |
from django.utils.datastructures import MultiValueDictKeyError |
|
3 |
from django.http import HttpResponse, HttpResponseBadRequest |
|
4 |
from django.views.generic.base import TemplateView |
|
5 |
from django.views.generic.edit import FormView |
|
6 |
from txapp.response import TxTemplateResponse |
|
7 |
from txapp.signals import translation_updated_signal |
|
8 |
from txapp.models import Project |
|
9 |
|
|
10 |
|
|
11 |
class Overview(TemplateView): |
|
12 |
"""A view to present the user with an overview of the app. |
|
13 |
|
|
14 |
Subclasses should override the template name. |
|
15 |
""" |
|
16 |
|
|
17 |
template_name = "txapp_overview.html" |
|
18 |
response_class = TxTemplateResponse |
|
19 |
|
|
20 |
|
|
21 |
class Settings(FormView): |
|
22 |
"""A view to present the user with the settings for the project. |
|
23 |
|
|
24 |
Subclasses should override the variables ``template_name``, ``success_url`` |
|
25 |
and ``form_class`` and the methods ``get_initial``, ``form_valid`` and |
|
26 |
``form_invalid``. |
|
27 |
""" |
|
28 |
|
|
29 |
template_name = "txapp_settings.html" |
|
30 |
response_class = TxTemplateResponse |
|
31 |
|
|
32 |
def form_valid(self, form): |
|
33 |
data = {'next_url': self.get_success_url()} |
|
34 |
return HttpResponse(json.dumps(data)) |
|
35 |
|
|
36 |
|
|
37 |
def translation_updated(request): |
|
38 |
"""A view that is called whenever we get a signal from Transifex about |
|
39 |
a resource being updated. |
|
40 |
|
|
41 |
The view raises the ``translation_updated_signal``, which must be called by |
|
42 |
the app itself. |
|
43 |
""" |
|
44 |
if request.method != 'POST': |
|
45 |
return HttpResponseBadRequest( |
|
46 |
"Method '%s' not allowed" % request.method |
|
47 |
) |
|
48 |
try: |
|
49 |
project = request.POST['project'] |
|
50 |
resource = request.POST['resource'] |
|
51 |
language = request.POST['language'] |
|
52 |
percent = request.POST['percent'] |
|
53 |
except MultiValueDictKeyError, e: |
|
54 |
return HttpResponseBadRequest("Malformed POST request") |
|
55 |
|
|
56 |
translation_updated_signal.send( |
|
57 |
sender='Tx', project=project, resource=resource, |
|
58 |
language=language, percent=percent |
|
59 |
) |
|
60 |
return HttpResponse() |
|
61 |
|
|
62 |
|
|
63 |
def register_project(request, project_slug): |
|
64 |
"""Register a project for this app. |
|
65 |
|
|
66 |
Args: |
|
67 |
project_slug: The slug of the project to register. |
|
68 |
""" |
|
69 |
if request.method != 'POST': |
|
70 |
return HttpResponseBadRequest( |
|
71 |
"Method '%s' not allowed" % request.method |
|
72 |
) |
|
73 |
try: |
|
74 |
source_language = request.POST.get('source_language') |
|
75 |
Project.objects.get_or_create( |
|
76 |
slug=project_slug, defaults={'source_language': source_language} |
|
77 |
) |
|
78 |
except Exception, e: |
|
79 |
return HttpResponseBadRequest(unicode(e)) |
|
80 |
return HttpResponse('""', status=200) |
|
81 |
|
|
82 |
|
|
83 |
def unregister_project(request, project_slug): |
|
84 |
"""Unregister a project for this app. |
|
85 |
|
|
86 |
Args: |
|
87 |
project_slug: The slug of the project to unregister. |
|
88 |
""" |
|
89 |
if request.method != 'POST': |
|
90 |
return HttpResponseBadRequest( |
|
91 |
"Method '%s' not allowed" % request.method |
|
92 |
) |
|
93 |
try: |
|
94 |
Project.objects.filter(slug=project_slug).delete() |
|
95 |
except Exception, e: |
|
96 |
return HttpResponseBadRequest(unicode(e)) |
|
97 |
return HttpResponse('""', status=200) |
Also available in: Unified diff