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