Commits

Apostolis Bessas committed 13d243d

Merge second version of API.

  • Participants
  • Parent commits 68d1781
  • Tags 1.1-alpha

Comments (0)

Files changed (16)

File transifex/api/urls.py

 
 from transifex.languages.api import LanguageHandler
 from transifex.projects.api import ProjectHandler, ProjectResourceHandler
-from transifex.resources.api import (ResourceHandler, FileHandler, StatsHandler)
+from transifex.resources.api import ResourceHandler, FileHandler, StatsHandler, \
+        TranslationHandler
 from transifex.storage.api import StorageHandler
 from transifex.releases.api import ReleaseHandler
 
 projectresource_handler = Resource(ProjectResourceHandler, authentication=auth)
 translationfile_handler = Resource(FileHandler, authentication=auth)
 stats_handler = Resource(StatsHandler, authentication=auth)
+translation_handler = Resource(TranslationHandler, authentication=auth)
 
 urlpatterns = patterns('',
     url(
         r'^languages/$',
         Resource(LanguageHandler),
+        {'api_version': 1},
         name='api.languages',
     ), url(
         r'^projects/$',
         project_handler,
+        {'api_version': 1},
+        name='api_projects',
     ), url(
         r'^project/(?P<project_slug>[-\w]+)/$',
         never_cache(project_handler),
+        {'api_version': 1},
         name='api_project',
      ), url(
         r'^project/(?P<project_slug>[-\w]+)/files/$',
         projectresource_handler,
+        {'api_version': 1},
         name='api_project_files',
     ), url(
         r'^project/(?P<project_slug>[-\w]+)/resources/$',
         never_cache(resource_handler),
+        {'api_version': 1},
+        name="api_resources",
     ), url(
         r'^project/(?P<project_slug>[-\w]+)/resource/(?P<resource_slug>[-\w]+)/$',
         never_cache(resource_handler),
-        name='api_resource'
+        {'api_version': 1},
+        name='api_resource',
     ), url(
         r'^project/(?P<project_slug>[-\w]+)/release/(?P<release_slug>[-\w]+)/$',
         never_cache(release_handler),
-        name='api_release'
+        {'api_version': 1},
+        name='api_release',
     ), url(
         r'^project/(?P<project_slug>[-\w]+)/resource/(?P<resource_slug>[-\w]+)/stats/$',
         never_cache(stats_handler),
-        name='api_resource_stats'
+        {'api_version': 1},
+        name='api_resource_stats',
     ), url(
         r'^project/(?P<project_slug>[-\w]+)/resource/(?P<resource_slug>[-\w]+)/stats/(?P<lang_code>[\-_@\w]+)/$',
         never_cache(stats_handler),
-        name='api_resource_stats'
+        {'api_version': 1},
+        name='api_resource_stats',
     ), url(
         r'^project/(?P<project_slug>[-\w]+)/resource/(?P<resource_slug>[-\w]+)/(?P<language_code>[\-_@\w]+)/$',
         never_cache(projectresource_handler),
-        name='api_resource_storage'
+        {'api_version': 1},
+        name='api_resource_storage',
     ), url(
         r'^project/(?P<project_slug>[-\w]+)/resource/(?P<resource_slug>[-\w]+)/(?P<language_code>[\-_@\w]+)/file/$',
         never_cache(translationfile_handler),
-        name='api_translation_file'
+        {'api_version': 1},
+        name='api_translation_file',
     ), url(
         r'^storage/$',
         storage_handler,
-        name='api.storage'
+        {'api_version': 1},
+        name='api.storage',
     ), url(
         r'^storage/(?P<uuid>[-\w]+)/$',
         storage_handler,
-        name='api.storage.file'
+        {'api_version': 1},
+        name='api.storage.file',
+    ), url(
+        r'^2/projects/$',
+        never_cache(project_handler),
+        {'api_version': 2},
+        name='apiv2_projects',
+    ), url(
+        r'^2/project/(?P<project_slug>[-\w]+)/$',
+        never_cache(project_handler),
+        {'api_version': 2},
+        name='apiv2_project',
+    ), url(
+        r'^2/project/(?P<project_slug>[-\w]+)/resources/$',
+        never_cache(resource_handler),
+        {'api_version': 2},
+        name='apiv2_resources',
+    ), url(
+        r'^2/project/(?P<project_slug>[-\w]+)/resource/(?P<resource_slug>[-\w]+)/$',
+        never_cache(resource_handler),
+        {'api_version': 2},
+        name='apiv2_resource',
+    ), url(
+        r'^2/project/(?P<project_slug>[-\w]+)/resource/(?P<resource_slug>[-\w]+)/content/$',
+        never_cache(translation_handler),
+        {'api_version': 2, 'lang_code': 'source'},
+        name='apiv2_translations',
+    ), url(
+        r'^2/project/(?P<project_slug>[-\w]+)/resource/(?P<resource_slug>[-\w]+)/translation/(?P<lang_code>[\-_@\w]+)/$',
+        never_cache(translation_handler),
+        {'api_version': 2},
+        name='apiv2_translation',
+    ), url(
+        r'^1/languages/$',
+        Resource(LanguageHandler),
+        {'api_version': 1},
+        name='api.languages',
+    ), url(
+        r'^1/projects/$',
+        project_handler,
+        {'api_version': 1},
+        name='api_projects',
+    ), url(
+        r'^1/project/(?P<project_slug>[-\w]+)/$',
+        never_cache(project_handler),
+        {'api_version': 1},
+        name='api_project',
+     ), url(
+        r'^1/project/(?P<project_slug>[-\w]+)/files/$',
+        projectresource_handler,
+        {'api_version': 1},
+        name='api_project_files',
+    ), url(
+        r'^1/project/(?P<project_slug>[-\w]+)/resources/$',
+        never_cache(resource_handler),
+        {'api_version': 1},
+        name="api_resources",
+    ), url(
+        r'^1/project/(?P<project_slug>[-\w]+)/resource/(?P<resource_slug>[-\w]+)/$',
+        never_cache(resource_handler),
+        {'api_version': 1},
+        name='api_resource',
+    ), url(
+        r'^1/project/(?P<project_slug>[-\w]+)/release/(?P<release_slug>[-\w]+)/$',
+        never_cache(release_handler),
+        {'api_version': 1},
+        name='api_release',
+    ), url(
+        r'^1/project/(?P<project_slug>[-\w]+)/resource/(?P<resource_slug>[-\w]+)/stats/$',
+        never_cache(stats_handler),
+        {'api_version': 1},
+        name='api_resource_stats',
+    ), url(
+        r'^1/project/(?P<project_slug>[-\w]+)/resource/(?P<resource_slug>[-\w]+)/stats/(?P<lang_code>[\-_@\w]+)/$',
+        never_cache(stats_handler),
+        {'api_version': 1},
+        name='api_resource_stats',
+    ), url(
+        r'^1/project/(?P<project_slug>[-\w]+)/resource/(?P<resource_slug>[-\w]+)/(?P<language_code>[\-_@\w]+)/$',
+        never_cache(projectresource_handler),
+        {'api_version': 1},
+        name='api_resource_storage',
+    ), url(
+        r'^1/project/(?P<project_slug>[-\w]+)/resource/(?P<resource_slug>[-\w]+)/(?P<language_code>[\-_@\w]+)/file/$',
+        never_cache(translationfile_handler),
+        {'api_version': 1},
+        name='api_translation_file',
+    ), url(
+        r'^1/storage/$',
+        storage_handler,
+        {'api_version': 1},
+        name='api.storage',
+    ), url(
+        r'^1/storage/(?P<uuid>[-\w]+)/$',
+        storage_handler,
+        {'api_version': 1},
+        name='api.storage.file',
     ),
 )

File transifex/projects/api.py

 from django.conf import settings
 from django.core.urlresolvers import reverse
 from django.contrib.auth.models import User
-from django.db import transaction
+from django.db import transaction, IntegrityError
 from django.http import HttpResponse, HttpResponseServerError
 from django.utils import simplejson
 from django.utils.translation import ugettext_lazy as _
 from django.template.defaultfilters import slugify
 
 from piston.handler import BaseHandler
-from piston.utils import rc, throttle
+from piston.utils import rc, throttle, require_mime
 
 from transifex.actionlog.models import action_logging
 from transifex.languages.models import Language
 from transifex.teams.models import Team
 from transifex.txcommon.log import logger
 from transifex.txcommon.decorators import one_perm_required_or_403
+from transifex.txcommon.utils import paginate
 from transifex.api.utils import BAD_REQUEST
 from uuid import uuid4
 
 # Temporary
 from transifex.txcommon import notifications as txnotification
 
+
 class ProjectHandler(BaseHandler):
     """
     API handler for model Project.
     """
     allowed_methods = ('GET','POST','PUT','DELETE')
-    model = Project
-    #TODO: Choose the fields we want to return
-    fields = ('slug', 'name', 'description', 'long_description', 'created',
-              'anyone_submit', 'bugtracker', ('owner', ('username', 'email')),
-              ('resources', ('slug', 'name',)))
+    details_fields = (
+        'slug', 'name', 'description', 'long_description', 'homepage', 'feed',
+        'created', 'anyone_submit', 'bug_tracker', 'trans_instructions',
+        'anyone_submit', 'tags', 'outsource', ('maintainers', ('username')),
+        ('owner', ('username')), ('resources', ('slug', 'name', )),
+    )
+    default_fields = ('slug', 'name', 'description', )
+    fields = default_fields
+    allowed_fields = (
+        'name', 'slug', 'description', 'long_description', 'private',
+        'homepage', 'feed', 'anyone_submit', 'hidden', 'bug_tracker',
+        'trans_instructions', 'tags', 'maintainers', 'outsource',
+    )
     exclude = ()
 
-    def read(self, request, project_slug=None):
+    def read(self, request, project_slug=None, api_version=1):
         """
         Get project details in json format
         """
-        if project_slug:
-            try:
-                project = Project.objects.get(slug=project_slug)
-            except Project.DoesNotExist:
-                return rc.NOT_FOUND
-            return project
-        else:
-            return Project.objects.for_user(request.user)
+        # Reset fields to default value
+        ProjectHandler.fields = ProjectHandler.default_fields
+        if api_version == 2:
+            if "details" in request.GET.iterkeys():
+                if project_slug is None:
+                    return rc.NOT_IMPLEMENTED
+                ProjectHandler.fields = ProjectHandler.details_fields
+        return self._read(request, project_slug)
 
+    @require_mime('json')
     @method_decorator(one_perm_required_or_403(pr_project_add))
-    def create(self, request,project_slug=None):
+    def create(self, request, project_slug=None, api_version=1):
         """
         API call to create new projects via POST.
         """
-        if 'application/json' in request.content_type: # we got JSON
-            data = getattr(request, 'data', None)
-            outsource = maintainers = None
-            outsource = data.pop('outsource', {})
-            maintainers = data.pop('maintainers', {})
+        data = getattr(request, 'data', None)
+        if api_version == 2:
+            if project_slug is not None:
+                return BAD_REQUEST("POSTing to this url is not allowed.")
+            if data is None:
+                return BAD_REQUEST(
+                    "At least parameters 'slug' and 'name' are needed."
+                )
+            return self._create(request, data)
+        else:
+            return self._createv1(request, data)
+
+    @require_mime('json')
+    @method_decorator(one_perm_required_or_403(pr_project_add_change,
+        (Project, 'slug__exact', 'project_slug')))
+    def update(self, request, project_slug, api_version=1):
+        """
+        API call to update project details via PUT.
+        """
+        if project_slug is None:
+            return BAD_REQUEST("Project slug not specified.")
+        data = request.data
+        if data is None:
+            return BAD_REQUEST("Empty request.")
+        if api_version == 2:
+            return self._update(request, project_slug, data)
+        else:
+            return self._updatev1(request, project_slug, data)
+
+    @method_decorator(one_perm_required_or_403(pr_project_delete,
+        (Project, 'slug__exact', 'project_slug')))
+    def delete(self, request, project_slug=None, api_version=1):
+        """
+        API call to delete projects via DELETE.
+        """
+        if project_slug is None:
+            return BAD_REQUEST("Project slug not specified.")
+        return self._delete(request, project_slug)
+
+    def _read(self, request, project_slug):
+        """
+        Return a list of projects or the details for a specific project.
+        """
+        if project_slug is None:
+            # Use pagination
+            p = Project.objects.for_user(request.user)
+            res, msg = paginate(
+                p, request.GET.get('start'), request.GET.get('end')
+            )
+            if res is None:
+                return BAD_REQUEST(msg)
+            return res
+        else:
             try:
-                p, created = Project.objects.get_or_create(**data)
-            except:
-                return BAD_REQUEST("Project not found")
+                p = Project.objects.get(slug=project_slug)
+                perm = ProjectPermission(request.user)
+                if not perm.private(p):
+                    return rc.FORBIDDEN
+            except Project.DoesNotExist:
+                return rc.NOT_FOUND
+            return p
 
-            if created:
-                # Owner
-                p.owner = request.user
+    def _create(self, request, data):
+        """
+        Create a new project.
+        """
+        # slug and name are mandatory fields for projects
+        if 'slug' not in data:
+            return BAD_REQUEST("Field slug is required to create a new project.")
+        if 'name' not in data:
+            return BAD_REQUEST("Field name is required to create a new project.")
+        if 'owner' in data:
+            return BAD_REQUEST("Owner cannot be set explicitly.")
 
-                # Outsourcing
-                if outsource:
+        try:
+            self._check_fields(data.iterkeys())
+        except AttributeError, e:
+            return BAD_REQUEST("Field '%s' is not available." % e.message)
+
+        # outsource and maintainers are ForeignKey
+        outsource = data.pop('outsource', {})
+        maintainers = data.pop('maintainers', {})
+        try:
+            p = Project(**data)
+        except Exception:
+            return BAD_REQUEST("Invalid arguments given.")
+        try:
+            p.save()
+        except IntegrityError:
+            return rc.DUPLICATE_ENTRY
+
+        p.owner = request.user
+        if outsource:
+            try:
+                outsource_project = Project.objects.get(slug=outsource)
+            except Project.DoesNotExist:
+                p.delete()
+                return BAD_REQUEST("Project for outsource does not exist.")
+            p.outsource = outsource_project
+
+        if maintainers:
+            for user in maintainers.split(','):
+                try:
+                    u = User.objects.get(username=user)
+                except User.DoesNotExist:
+                    p.delete()
+                    return BAD_REQUEST("User %s does not exist." % user)
+                p.maintainers.add(u)
+        p.save()
+        return rc.CREATED
+
+    def _create_v1(self, request, data):
+        """
+        Create a new project following the v1 API.
+        """
+        outsource = data.pop('outsource', {})
+        maintainers = data.pop('maintainers', {})
+        try:
+            p, created = Project.objects.get_or_create(**data)
+        except:
+            return BAD_REQUEST("Project not found")
+
+        if created:
+            # Owner
+            p.owner = request.user
+
+            # Outsourcing
+            if outsource:
+                try:
+                    outsource_project = Project.objects.get(slug=outsource)
+                except Project.DoesNotExist:
+                    # maybe fail when wrong user is given?
+                    pass
+                p.outsource = outsource_project
+
+            # Handler m2m with maintainers
+            if maintainers:
+                for user in maintainers.split(','):
                     try:
-                        outsource_project = Project.objects.get(slug=outsource)
-                    except Project.DoesNotExist:
+                        p.maintainers.add(User.objects.get(username=user))
+                    except User.DoesNotExist:
                         # maybe fail when wrong user is given?
                         pass
-                    p.outsource = outsource_project
-
-                # Handler m2m with maintainers
-                if maintainers:
-                    for user in maintainers.split(','):
-                        try:
-                            p.maintainers.add(User.objects.get(username=user))
-                        except User.DoesNotExist:
-                            # maybe fail when wrong user is given?
-                            pass
-                p.save()
+            p.save()
 
             return rc.CREATED
         else:
             return BAD_REQUEST("Unsupported request")
 
-    @method_decorator(one_perm_required_or_403(pr_project_add_change,
-        (Project, 'slug__exact', 'project_slug')))
-    def update(self, request,project_slug):
+    def _update(self, request, project_slug, data):
+        try:
+            self._check_fields(data.iterkeys())
+        except AttributeError, e:
+            return BAD_REQUEST("Field '%s' is not available." % e.message)
+
+        outsource = data.pop('outsource', {})
+        maintainers = data.pop('maintainers', {})
+        try:
+            p = Project.objects.get(slug=project_slug)
+        except Project.DoesNotExist:
+            return BAD_REQUEST("Project not found")
+
+        try:
+            for key,value in data.items():
+                setattr(p, key,value)
+
+            # Outsourcing
+            if outsource:
+                if outsource == p.slug:
+                    return BAD_REQUEST("Original and outsource projects are the same.")
+                try:
+                    outsource_project = Project.objects.get(slug=outsource)
+                except Project.DoesNotExist:
+                    return BAD_REQUEST("Project for outsource does not exist.")
+                p.outsource = outsource_project
+
+            # Handler m2m with maintainers
+            if maintainers:
+                # remove existing maintainers and add new ones
+                p.maintainers.clear()
+                for user in maintainers.split(','):
+                    try:
+                        p.maintainers.add(User.objects.get(username=user))
+                    except User.DoesNotExist:
+                        return BAD_REQUEST("User %s does not exist." % user)
+            p.save()
+        except Exception, e:
+            return BAD_REQUEST("Error parsing request data: %s" % e)
+        return rc.ALL_OK
+
+    def _updatev1(self, request, project_slug, data):
         """
-        API call to update project details via PUT.
+        Update a project per API v1.
         """
-        if 'application/json' in request.content_type: # we got JSON
-            data = getattr(request, 'data', None)
-            outsource = maintainers = None
-            outsource = data.pop('outsource', {})
-            maintainers = data.pop('maintainers', {})
-            if project_slug:
+        outsource = data.pop('outsource', {})
+        maintainers = data.pop('maintainers', {})
+        try:
+            p = Project.objects.get(slug=project_slug)
+        except Project.DoesNotExist:
+            return BAD_REQUEST("Project not found")
+        try:
+            for key,value in data.items():
+                setattr(p, key,value)
+                # Outsourcing
+            if outsource:
+                if outsource == p.slug:
+                    return BAD_REQUEST("Original and outsource projects are the same.")
                 try:
-                    p = Project.objects.get(slug=project_slug)
+                    outsource_project = Project.objects.get(slug=outsource)
                 except Project.DoesNotExist:
-                    return BAD_REQUEST("Project not found")
-                try:
-                    for key,value in data.items():
-                        setattr(p, key,value)
-                    # Outsourcing
-                    if outsource:
-                        try:
-                            outsource_project = Project.objects.get(slug=outsource)
-                        except Project.DoesNotExist:
-                            # maybe fail when wrong user is given?
-                            pass
-                        p.outsource = outsource_project
+                    # maybe fail when wrong user is given?
+                    pass
+                p.outsource = outsource_project
 
-                    # Handler m2m with maintainers
-                    if maintainers:
-                        # remove existing maintainers
-                        p.maintainers.all().clear()
-                        # add then all anew
-                        for user in maintainers.split(','):
-                            try:
-                                p.maintainers.add(User.objects.get(username=user))
-                            except User.DoesNotExist:
-                                # maybe fail when wrong user is given?
-                                pass
-                    p.save()
-                except Exception, e:
-                    return BAD_REQUEST("Error parsing request data: %s" % e)
+            # Handler m2m with maintainers
+            if maintainers:
+                # remove existing maintainers
+                p.maintainers.all().clear()
+                # add then all anew
+                for user in maintainers.split(','):
+                    try:
+                        p.maintainers.add(User.objects.get(username=user))
+                    except User.DoesNotExist:
+                        # maybe fail when wrong user is given?
+                        pass
+            p.save()
+        except Exception, e:
+            return BAD_REQUEST("Error parsing request data: %s" % e)
+        return rc.ALL_OK
 
-                return rc.ALL_OK
+    def _delete(self, request, project_slug):
+        try:
+            project = Project.objects.get(slug=project_slug)
+        except Project.DoesNotExist:
+            return rc.NOT_FOUND
+        try:
+            project.delete()
+        except:
+            return rc.INTERNAL_ERROR
+        return rc.DELETED
 
-        return BAD_REQUEST("Unsupported request")
-
-
-    @method_decorator(one_perm_required_or_403(pr_project_delete,
-        (Project, 'slug__exact', 'project_slug')))
-    def delete(self, request,project_slug):
+    def _check_fields(self, fields):
         """
-        API call to delete projects via DELETE.
+        Check if supplied fields are allowed to be given in a
+        POST or PUT request.
         """
-        if project_slug:
-            try:
-                project = Project.objects.get(slug=project_slug)
-            except Project.DoesNotExist:
-                return rc.NOT_FOUND
-
-            try:
-                project.delete()
-            except:
-                return rc.INTERNAL_ERROR
-
-            return rc.DELETED
-        else:
-            return rc.BAD_REQUEST
+        for field in fields:
+            if field not in self.allowed_fields:
+                raise AttributeError(field)
 
 
 class ProjectResourceHandler(BaseHandler):
     @throttle(settings.API_MAX_REQUESTS, settings.API_THROTTLE_INTERVAL)
     @method_decorator(one_perm_required_or_403(pr_resource_add_change,
         (Project, 'slug__exact', 'project_slug')))
-    def create(self, request, project_slug):
+    def create(self, request, project_slug, api_version=1):
         """
         Create resource for project by UUID of StorageFile.
         """
         else:
             return BAD_REQUEST("Unsupported request")
 
-    def update(self, request, project_slug, resource_slug, language_code=None):
+    def update(self, request, project_slug, resource_slug, language_code=None, api_version=1):
         """
         Update resource translations of a project by the UUID of a StorageFile.
         """
                     strings_added, strings_updated = fhandler.save2db(
                         user=request.user)
                 except Exception, e:
+                    logger.error(e.message, exc_info=True)
                     return BAD_REQUEST("Error importing file: %s" % e)
                 else:
                     messages = []

File transifex/projects/tests/api.py

 # -*- coding: utf-8 -*-
 from django.core.urlresolvers import reverse
+from django.utils import simplejson
+from transifex.txcommon.tests.base import BaseTestCase
 from transifex.resources.models import RLStats, Resource
+from django.contrib.auth.models import User, Permission
+from transifex.projects.models import Project
 from transifex.storage.models import StorageFile
 from transifex.storage.tests.api import BaseStorageTests
 
         self.assertEqual(rls.translated, 3)
         self.assertEqual(rls.total, 3)
         self.assertEqual(rls.translated_perc, 100)
+
+class TestProjectAPI(BaseTestCase):
+
+    def setUp(self):
+        super(TestProjectAPI, self).setUp()
+        self.url_projects = reverse('apiv2_projects')
+        self.url_project = reverse('apiv2_project', kwargs={'project_slug': 'foo'})
+
+    def test_get(self):
+        res = self.client['anonymous'].get(self.url_projects)
+        self.assertEquals(res.status_code, 401)
+        res = self.client['maintainer'].get(self.url_projects + "?details")
+        self.assertEquals(res.status_code, 501)
+        res = self.client['maintainer'].get(self.url_projects)
+        self.assertEquals(res.status_code, 200)
+        data = simplejson.loads(res.content)
+        self.assertEquals(len(data), 7)
+        self.assertFalse('created' in data[0])
+        self.assertTrue('slug' in data[0])
+        self.assertTrue('name' in data[0])
+        res = self.client['registered'].get(self.url_projects)
+        self.assertEquals(res.status_code, 200)
+        data = simplejson.loads(res.content)
+        self.assertEquals(len(data), 6)
+        res = self.client['anonymous'].get(self.url_project)
+        self.assertEquals(res.status_code, 401)
+        res = self.client['registered'].get(self.url_project)
+        self.assertEquals(res.status_code, 404)
+        private_url = "/".join([self.url_projects[:-2], self.project_private.slug, ''])
+        res = self.client['registered'].get(private_url)
+        self.assertEquals(res.status_code, 401)
+        res = self.client['maintainer'].get(private_url)
+        self.assertEquals(res.status_code, 200)
+        public_url = "/".join([self.url_projects[:-2], self.project.slug, ''])
+        res = self.client['registered'].get(public_url + "?details")
+        self.assertEquals(res.status_code, 200)
+        self.assertEquals(len(simplejson.loads(res.content)), 14)
+        self.assertTrue('created' in simplejson.loads(res.content))
+        public_url = "/".join(
+            [self.url_projects[:-2], self.project.slug, ""]
+        )
+        res = self.client['registered'].get(public_url)
+        self.assertEquals(res.status_code, 200)
+        self.assertTrue('slug' in simplejson.loads(res.content))
+        self.assertTrue('name' in simplejson.loads(res.content))
+        self.assertTrue('description' in simplejson.loads(res.content))
+        self.assertEquals(len(simplejson.loads(res.content)), 3)
+
+        # Test pagination
+        res = self.client['registered'].get(self.url_projects + "?start=5")
+        self.assertEquals(res.status_code, 200)
+        data = simplejson.loads(res.content)
+        self.assertEquals(len(data), 2)
+        res = self.client['registered'].get(self.url_projects + "?end=5")
+        self.assertEquals(res.status_code, 200)
+        data = simplejson.loads(res.content)
+        self.assertEquals(len(data), 4)
+        res = self.client['registered'].get(self.url_projects + "?start=a")
+        self.assertEquals(res.status_code, 400)
+        res = self.client['registered'].get(self.url_projects + "?start=0")
+        self.assertEquals(res.status_code, 400)
+        res = self.client['registered'].get(self.url_projects + "?end=0")
+        self.assertEquals(res.status_code, 400)
+        res = self.client['registered'].get(self.url_projects + "?start=1&end=4")
+        self.assertEquals(res.status_code, 200)
+        data = simplejson.loads(res.content)
+        self.assertEquals(len(data), 3)
+        res = self.client['registered'].get(self.url_projects + "?start=1&end=4")
+        data = simplejson.loads(res.content)
+        self.assertEquals(len(data), 3)
+        self.assertEquals(res.status_code, 200)
+
+    def test_post(self):
+        res = self.client['anonymous'].post(self.url_projects, content_type='application/json')
+        self.assertEquals(res.status_code, 401)
+        res = self.client['registered'].post(self.url_project, content_type='application/json')
+        self.assertEquals(res.status_code, 400)
+        self.assertIn("POSTing to this url is not allowed", res.content)
+        res = self.client['registered'].post(self.url_projects)
+        self.assertEquals(res.status_code, 400)
+        self.assertIn("Bad Request", res.content)
+        res = self.client['registered'].post(
+            self.url_projects, simplejson.dumps({'name': 'name of project'}),
+            content_type="application/json"
+        )
+        self.assertEquals(res.status_code, 400)
+        self.assertIn("Field slug is required to create a new project.", res.content)
+        res = self.client['registered'].post(
+            self.url_projects, simplejson.dumps({'slug': 'slug'}), content_type='application/json'
+        )
+        self.assertEquals(res.status_code, 400)
+        self.assertIn("Field name is required to create a new project.", res.content)
+        res = self.client['registered'].post(
+            self.url_projects, simplejson.dumps({
+                'slug': 'slug', 'name': 'name', 'owner': 'owner'
+            }),
+            content_type='application/json'
+        )
+        self.assertEquals(res.status_code, 400)
+        self.assertIn("Owner cannot be set explicitly.", res.content)
+        res = self.client['registered'].post(
+            self.url_projects, simplejson.dumps({
+                'slug': 'api_project',
+                'name': 'Project from API',
+                'outsource': 'not_exists',
+            }),
+            content_type='application/json'
+        )
+        self.assertEquals(res.status_code, 400)
+        self.assertIn("Project for outsource does not exist", res.content)
+        res = self.client['registered'].post(
+            self.url_projects, simplejson.dumps({
+                'slug': 'api_project', 'name': 'Project from API',
+                'maintainers': 'not_exists',
+            }),
+            content_type='application/json'
+        )
+        self.assertEquals(res.status_code, 400)
+        self.assertIn("User", res.content)
+        res = self.client['registered'].post(
+            self.url_projects, simplejson.dumps({
+                'slug': 'api_project_maintainers',
+                'name': 'Project from API',
+                'maintainers': 'registered',
+                'none': 'none'
+            }),
+            content_type='application/json'
+        )
+        self.assertEquals(res.status_code, 400)
+        self.assertIn("Field 'none'", res.content)
+        res = self.client['registered'].post(
+            self.url_projects, simplejson.dumps({
+                'slug': 'api_project_maintainers',
+                'name': 'Project from API',
+                'maintainers': 'registered'
+            }),
+            content_type='application/json'
+        )
+        self.assertEquals(res.status_code, 201)
+        self.assertEquals(len(Project.objects.all()), 8)
+        res = self.client['registered'].post(
+            self.url_projects, simplejson.dumps({
+                'slug': 'api_project', 'name': 'Project from API',
+            }),
+            content_type='application/json'
+        )
+        self.assertEquals(res.status_code, 201)
+        self.assertEquals(len(Project.objects.all()), 9)
+        res = self.client['registered'].post(
+            self.url_projects, simplejson.dumps({
+                'slug': 'api_project', 'name': 'Project from API',
+            }),
+            content_type='application/json'
+        )
+        self.assertEquals(res.status_code, 409)
+        # Check permissions
+        user = User.objects.get(username='registered')
+        user.groups = []
+        user.save()
+        res = self.client['registered'].post(
+            self.url_projects, simplejson.dumps({
+                'slug': 'api_project_2', 'name': 'Project from API - second',
+            }),
+            content_type='application/json'
+        )
+        self.assertEquals(res.status_code, 403)
+
+    def test_put(self):
+        res = self.client['anonymous'].put(
+            self.url_project, data=simplejson.dumps({}),
+            content_type='application/json'
+        )
+        self.assertEquals(res.status_code, 401)
+        res = self.client['registered'].put(self.url_project)
+        self.assertEquals(res.status_code, 400)
+        res = self.client['registered'].put(
+            self.url_project[:-1] + "1/",
+            simplejson.dumps({'name': 'name of project'}),
+            content_type="application/json"
+        )
+        self.assertEquals(res.status_code, 404)
+        res = self.client['registered'].post(
+            self.url_projects, simplejson.dumps({
+                'slug': 'foo', 'name': 'Foo Project',
+            }),
+            content_type='application/json'
+        )
+        self.assertEquals(res.status_code, 201)
+        res = self.client['registered'].put(
+            self.url_project, data=simplejson.dumps({}),
+            content_type='application/json'
+        )
+        self.assertEquals(res.status_code, 403)
+        user = User.objects.get(username='registered')
+        user.user_permissions.add(
+            Permission.objects.get(codename="change_project")
+        )
+        res = self.client['registered'].put(
+            self.url_project,
+            data=simplejson.dumps({'foo': 'foo'}),
+            content_type='application/json'
+        )
+        self.assertEquals(res.status_code, 400)
+        self.assertIn("Field 'foo'", res.content)
+        res = self.client['registered'].put(
+            self.url_project, data=simplejson.dumps({}),
+            content_type='application/json'
+        )
+        self.assertEquals(res.status_code, 200)
+        name = 'New name for foo'
+        res = self.client['registered'].put(
+            self.url_project,
+            data=simplejson.dumps({'name': name}),
+            content_type='application/json'
+        )
+        self.assertEquals(res.status_code, 200)
+        p_foo = Project.objects.get(slug="foo")
+        self.assertEquals(p_foo.name, name)
+        res = self.client['registered'].put(
+            self.url_project,
+            data=simplejson.dumps({'outsource': "foo"}),
+            content_type='application/json'
+        )
+        self.assertEquals(res.status_code, 400)
+        self.assertIn("Original and outsource projects are the same", res.content)
+        res = self.client['registered'].put(
+            self.url_project,
+            data=simplejson.dumps({'outsource': "bar"}),
+            content_type='application/json'
+        )
+        self.assertEquals(res.status_code, 400)
+        self.assertIn("Project for outsource does not exist", res.content)
+        res = self.client['registered'].put(
+            self.url_project,
+            data=simplejson.dumps({'maintainers': 'none, not'}),
+            content_type='application/json'
+        )
+        self.assertEquals(res.status_code, 400)
+        self.assertIn("User", res.content)
+
+
+    def test_delete(self):
+        res = self.client['anonymous'].delete(self.url_project)
+        self.assertEquals(res.status_code, 401)
+        res = self.client['registered'].delete(self.url_projects)
+        self.assertEquals(res.status_code, 403)
+        user = User.objects.get(username='registered')
+        user.user_permissions.add(
+            Permission.objects.get(codename="delete_project")
+        )
+        res = self.client['registered'].delete(self.url_projects)
+        self.assertEquals(res.status_code, 400)
+        self.assertIn("Project slug not specified", res.content)
+        res = self.client['registered'].delete(self.url_project)
+        self.assertEquals(res.status_code, 404)
+        res = self.client['registered'].post(
+            self.url_projects, simplejson.dumps({
+                'slug': 'foo', 'name': 'Foo Project',
+            }),
+            content_type='application/json'
+        )
+        self.assertEquals(res.status_code, 201)
+        res = self.client['registered'].delete(self.url_project)
+        self.assertEquals(res.status_code, 204)

File transifex/resources/api.py

 # -*- coding: utf-8 -*-
-from uuid import uuid4
-from django.db import transaction
+import os
+import tempfile
+from itertools import ifilter
+from django.db import transaction, IntegrityError
 from django.conf import settings
 from django.http import HttpResponse
 from django.core.urlresolvers import reverse
 from django.template.defaultfilters import slugify
 from django.contrib.auth.models import User
+from django.utils import simplejson
 from django.utils.encoding import smart_unicode
+from django.utils.translation import ugettext_lazy as _
 
 from piston.handler import BaseHandler, AnonymousBaseHandler
-from piston.utils import rc, throttle
+from piston.utils import rc, throttle, require_mime
 
 from transifex.txcommon.decorators import one_perm_required_or_403
 from transifex.txcommon.log import logger
+from transifex.txcommon.exceptions import FileCheckError
+from transifex.txcommon.utils import paginate
 from transifex.projects.permissions import *
 from transifex.languages.models import Language
 from transifex.projects.models import Project
-from transifex.storage.models import StorageFile
+from transifex.projects.permissions.project import ProjectPermission
+from transifex.projects.signals import post_submit_translation
 
 from transifex.resources.decorators import method_decorator
-from transifex.resources.models import Resource, SourceEntity, Translation, RLStats
+from transifex.resources.models import Resource, SourceEntity, Translation, \
+        RLStats
 from transifex.resources.views import _compile_translation_template
+from transifex.resources.formats import get_i18n_method_from_mimetype, \
+        parser_for, get_file_extension_for_method, get_mimetype_from_method
+from transifex.resources.formats.qt import LinguistParseError
+from transifex.teams.models import Team
 
 from transifex.api.utils import BAD_REQUEST
 
+
+class BadRequestError(Exception):
+    pass
+
+class NoContentError(Exception):
+    pass
+
+
 class ResourceHandler(BaseHandler):
     """
     Resource Handler for CRUD operations.
     def project_slug(cls, sfk):
         """
         This is a work around to include the project slug in the resource API
-        details. Trying to work with the normal foreign key field caused some
-        kind of circular dependency in Piston so this field was added as a
-        solution.
+        details, so that it is shown as a normal field.
         """
         if sfk.project:
             return sfk.project.slug
         return None
 
+    @classmethod
+    def mimetype(cls, r):
+        """
+        Return the mimetype in a GET request instead of the i18n_type.
+        """
+        return get_mimetype_from_method(r.i18n_type)
+
+    @classmethod
+    def source_language_code(cls, r):
+        """
+        Return just the code of the source language.
+        """
+        return r.source_language.code
+
     allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
-    model = Resource
-    fields = ('slug', 'name', 'created', 'available_languages', 'i18n_type',
-        'source_language', 'project_slug')
+    default_fields = ('slug', 'name', 'mimetype', 'source_language', )
+    details_fields = (
+        'slug', 'name', 'created', 'available_languages', 'mimetype',
+        'source_language_code', 'project_slug', 'wordcount', 'total_entities',
+        'accept_translations', 'last_update',
+    )
+    fields = default_fields
+    allowed_fields = (
+        'slug', 'name', 'accept_translations', 'source_language',
+        'mimetype', 'content',
+    )
+    apiv1_fields = ('slug', 'name', 'created', 'available_languages', 'i18n_type',
+                    'source_language', 'project_slug')
     exclude = ()
 
-    def read(self, request, project_slug, resource_slug=None):
+    def read(self, request, project_slug, resource_slug=None, api_version=1):
         """
         Get details of a resource.
         """
-        if resource_slug:
-            try:
-                resource = Resource.objects.get(slug=resource_slug,
-                    project__slug=project_slug)
-            except Resource.DoesNotExist:
-                return rc.NOT_FOUND
-            return resource
+        # Reset fields to default value
+        ResourceHandler.fields = self.default_fields
+        if api_version == 2:
+            if "details" in request.GET:
+                if resource_slug is None:
+                    return rc.NOT_IMPLEMENTED
+                ResourceHandler.fields = ResourceHandler.details_fields
         else:
-            return Resource.objects.filter(project__slug=project_slug)
+            ResourceHandler.fields = ResourceHandler.apiv1_fields
+        return self._read(request, project_slug, resource_slug)
 
     @method_decorator(one_perm_required_or_403(pr_resource_add_change,
         (Project, 'slug__exact', 'project_slug')))
-    def create(self, request, project_slug, resource_slug=None):
+    def create(self, request, project_slug, resource_slug=None, api_version=1):
         """
         Create new resource under project `project_slug` via POST
         """
+        data = getattr(request, 'data', None)
+        if api_version == 2:
+            if resource_slug is not None:
+                return BAD_REQUEST("POSTing to this url is not allowed.")
+            if data is None:
+                return BAD_REQUEST(
+                    "At least parameters 'slug', 'name', 'i18n_type' "
+                    "and 'source_language' must be specified,"
+                    " as well as the source strings."
+                )
+            return self._create(request, project_slug, data)
+        else:
+            return self._createv1(request, project_slug, resource_slug, data)
+
+    @require_mime('json')
+    @method_decorator(one_perm_required_or_403(pr_resource_add_change,
+        (Project, 'slug__exact', 'project_slug')))
+    def update(self, request, project_slug, resource_slug=None, api_version=1):
+        """
+        API call to update resource details via PUT
+        """
+        if resource_slug is None:
+            return BAD_REQUEST("No resource specified in url")
+        return self._update(request, project_slug, resource_slug)
+
+    @method_decorator(one_perm_required_or_403(pr_resource_delete,
+        (Project, 'slug__exact', 'project_slug')))
+    def delete(self, request, project_slug, resource_slug=None, api_version=1):
+        """
+        API call to delete resources via DELETE.
+        """
+        if resource_slug is None:
+            return BAD_REQUEST("No resource provided.")
+        return self._delete(request, project_slug, resource_slug)
+
+    def _read(self, request, project_slug, resource_slug):
+        if resource_slug is None:
+            try:
+                p = Project.objects.get(slug=project_slug)
+            except Project.NotFound:
+                return rc.NOT_FOUND
+            if not self._has_perm(request.user, p):
+                return rc.FORBIDDEN
+            return p.resources.all()
+        try:
+            resource = Resource.objects.get(
+                slug=resource_slug, project__slug=project_slug
+            )
+        except Resource.DoesNotExist:
+            return rc.NOT_FOUND
+        if not self._has_perm(request.user, resource.project):
+            return rc.FORBIDDEN
+        return resource
+
+    def _has_perm(self, user, project):
+        """
+        Check that the user has access to this resource.
+        """
+        perm = ProjectPermission(user)
+        if not perm.private(project):
+            return False
+        return True
+
+    def _create(self, request, project_slug, data):
+        # Check for unavailable fields
+        try:
+            self._check_fields(data.iterkeys())
+        except AttributeError, e:
+            return BAD_REQUEST("Field '%s' is not allowed." % e.message)
+        # Check for obligatory fields
+        for field in ('name', 'slug', 'source_language', ):
+            if field not in data:
+                return BAD_REQUEST("Field '%s' must be specified." % field)
+
         try:
             project = Project.objects.get(slug=project_slug)
         except Project.DoesNotExist:
             return rc.NOT_FOUND
+        slang = data.get('source_language', None)
+        i18n_type = get_i18n_method_from_mimetype(data.get('mimetype', None))
+        if 'application/json' in request.content_type and i18n_type is None:
+            return BAD_REQUEST("Field 'mimetype' must be specified.")
+        if i18n_type is not None:
+            del data['mimetype']
+        try:
+            source_language = Language.objects.by_code_or_alias(slang)
+            del data['source_language']
+        except:
+            return BAD_REQUEST("Language code '%s' does not exist." % slang)
 
-        if 'application/json' in request.content_type: # we got JSON
-            data = getattr(request, 'data', None)
-            slang = data.pop('source_language', None)
-            source_language = None
-            try:
-                source_language = Language.objects.by_code_or_alias(slang)
-            except:
-                pass
+        # save resource
+        try:
+            r = Resource(
+                project=project, source_language=source_language,
+            )
+            r.i18n_type = i18n_type
+            for key in ifilter(lambda k: k != "content", data.iterkeys()):
+                setattr(r, key, data[key])
+        except:
+            return BAD_REQUEST("Invalid arguments given.")
+        try:
+            r.save()
+        except IntegrityError, e:
+            return BAD_REQUEST(
+                "A resource with the same slug exists in this project."
+            )
 
-            if not source_language:
-                return BAD_REQUEST("No source language was specified.")
+        # save source entities
+        try:
+            t = Translation.get_object("create", request, r, source_language)
+        except AttributeError, e:
+            r.delete()
+            return BAD_REQUEST("The content type of the request is not valid.")
+        try:
+            res = t.create()
+        except (BadRequestError, NoContentError), e:
+            r.delete()
+            return BAD_REQUEST(e.message)
+        res = t.__class__.to_http_for_create(t, res)
+        if res.status_code == 200:
+            res.status_code = 201
+        return res
 
-            try:
-                Resource.objects.get_or_create(project=project,
-                    source_language=source_language, **data)
-            except:
-                return BAD_REQUEST("The json you provided is misformatted.")
+    @require_mime('json')
+    def _createv1(self, request, project_slug, resource_slug, data):
+        try:
+            project = Project.objects.get(slug=project_slug)
+        except Project.DoesNotExist:
+            return rc.NOT_FOUN
+        slang = data.pop('source_language', None)
+        source_language = None
+        try:
+            source_language = Language.objects.by_code_or_alias(slang)
+        except:
+            pass
 
-            return rc.CREATED
-        else:
-            return BAD_REQUEST("The request data need to be in json encoding.")
+        if not source_language:
+            return BAD_REQUEST("No source language was specified.")
 
-    @method_decorator(one_perm_required_or_403(pr_resource_add_change,
-        (Project, 'slug__exact', 'project_slug')))
-    def update(self, request, project_slug, resource_slug):
-        """
-        API call to update resource details via PUT.
-        """
+        try:
+            Resource.objects.get_or_create(
+                project=project, source_language=source_language, **data
+            )
+        except:
+            return BAD_REQUEST("The json you provided is misformatted.")
+        return rc.CREATED
+
+    def _update(self, request, project_slug, resource_slug):
+        data = getattr(request, 'data', None)
+        if not data:            # Check for {} as well
+            return BAD_REQUEST("Empty request")
+        try:
+            self._check_fields(data.iterkeys())
+        except AttributeError, e:
+            return BAD_REQUEST("Field '%s' is not allowed." % e.message)
+
         try:
             project = Project.objects.get(slug=project_slug)
         except Project.DoesNotExist:
             return rc.NOT_FOUND
-
-        if 'application/json' in request.content_type: # we got JSON
-            data = getattr(request, 'data', None)
-            slang = data.pop('source_language', None)
-            source_language = None
+        slang = data.pop('source_language', None)
+        source_language = None
+        i18n_type = get_i18n_method_from_mimetype(data.pop('mimetype', None))
+        if slang is not None:
             try:
                 source_language = Language.objects.by_code_or_alias(slang)
-            except:
-                pass
+            except Language.DoesNotExist:
+                return BAD_REQUEST("Language code '%s' does not exist." % slang)
 
-            if resource_slug:
-                try:
-                    resource = Resource.objects.get(slug=resource_slug)
-                except Resource.DoesNotExist:
-                    return BAD_REQUEST("Request %s does not exist" % resource_slug)
-                try:
-                    for key,value in data.items():
-                        setattr(resource, key,value)
-                    if source_language:
-                        resource.source_language = source_language
-                    resource.save()
-                except:
-                    return rc.BAD_REQUEST
+        try:
+            resource = Resource.objects.get(slug=resource_slug)
+        except Resource.DoesNotExist:
+            return BAD_REQUEST("Resource %s does not exist" % resource_slug)
+        try:
+            for key, value in data.iteritems():
+                setattr(resource, key, value)
+            if source_language:
+                resource.source_language = source_language
+            if i18n_type is not None:
+                resource.i18n_type = i18n_type
+            resource.save()
+        except:
+            return rc.BAD_REQUEST
+        return rc.ALL_OK
 
-                return rc.ALL_OK
+    def _delete(self, request, project_slug, resource_slug):
+        try:
+            resource = Resource.objects.get(slug=resource_slug)
+        except Resource.DoesNotExist:
+            return rc.NOT_FOUND
+        try:
+            resource.delete()
+        except:
+            return rc.INTERNAL_ERROR
+        return rc.DELETED
 
-        return BAD_REQUEST("The request data need to be in json encoding.")
-
-
-    @method_decorator(one_perm_required_or_403(pr_resource_delete,
-        (Project, 'slug__exact', 'project_slug')))
-    def delete(self, request, project_slug, resource_slug):
-        """
-        API call to delete resources via DELETE.
-        """
-        if resource_slug:
-            try:
-                resource = Resource.objects.get(slug=resource_slug)
-            except Resource.DoesNotExist:
-                return rc.NOT_FOUND
-
-            try:
-                resource.delete()
-            except:
-                return rc.INTERNAL_ERROR
-
-            return rc.DELETED
-        else:
-            return rc.BAD_REQUEST
+    def _check_fields(self, fields):
+        for field in fields:
+            if not field in self.allowed_fields:
+                raise AttributeError(field)
 
 
 class StatsHandler(BaseHandler):
     allowed_methods = ('GET')
 
-    def read(self, request, project_slug, resource_slug, lang_code=None):
+    def read(self, request, project_slug, resource_slug,
+             lang_code=None, api_version=1):
         """
         This is an API handler to display translation statistics for individual
         resources.
     @throttle(settings.API_MAX_REQUESTS, settings.API_THROTTLE_INTERVAL)
     @method_decorator(one_perm_required_or_403(pr_project_private_perm,
         (Project, 'slug__exact', 'project_slug')))
-    def read(self, request, project_slug, resource_slug=None, language_code=None):
+    def read(self, request, project_slug, resource_slug=None,
+             language_code=None, api_version=1):
         """
         API Handler to export translation files from the database
         """
         try:
-            resource = Resource.objects.get( project__slug = project_slug, slug = resource_slug)
-            language = Language.objects.by_code_or_alias( code=language_code)
+            resource = Resource.objects.get(
+                project__slug=project_slug, slug=resource_slug
+            )
+            language = Language.objects.by_code_or_alias(code=language_code)
         except (Resource.DoesNotExist, Language.DoesNotExist), e:
             return BAD_REQUEST("%s" % e )
 
         try:
             template = _compile_translation_template(resource, language)
         except Exception, e:
+            logger.error(e.message, exc_info=True)
             return BAD_REQUEST("Error compiling the translation file: %s" %e )
 
         i18n_method = settings.I18N_METHODS[resource.i18n_type]
         i18n_method['file-extensions'].split(', ')[0]))
 
         return response
+
+
+class TranslationHandler(BaseHandler):
+    allowed_methods = ('GET', 'PUT', )
+
+    @throttle(settings.API_MAX_REQUESTS, settings.API_THROTTLE_INTERVAL)
+    @method_decorator(one_perm_required_or_403(
+            pr_project_private_perm,
+            (Project, 'slug__exact', 'project_slug')
+    ))
+    def read(self, request, project_slug, resource_slug,
+             lang_code=None, api_version=2):
+        return self._read(request, project_slug, resource_slug, lang_code)
+
+    @throttle(settings.API_MAX_REQUESTS, settings.API_THROTTLE_INTERVAL)
+    @method_decorator(one_perm_required_or_403(
+            pr_resource_add_change,
+            (Project, 'slug__exact', 'project_slug')
+    ))
+    def update(self, request, project_slug, resource_slug,
+               lang_code, api_version=2):
+        return self._update(request, project_slug, resource_slug, lang_code)
+
+    def _read(self, request, project_slug, resource_slug, lang_code):
+        try:
+            r = Resource.objects.get(
+                slug=resource_slug, project__slug=project_slug
+            )
+        except Resource.DoesNotExist:
+            return rc.NOT_FOUND
+
+        if lang_code == "source":
+            language = r.source_language
+        else:
+            try:
+                language = Language.objects.by_code_or_alias(lang_code)
+            except Language.DoesNotExist:
+                return rc.NOT_FOUND
+        translation = Translation.get_object("get", request, r, language)
+        res = translation.get()
+        return translation.__class__.to_http_for_get(
+            translation, res
+        )
+
+    def _update(self, request, project_slug, resource_slug, lang_code=None):
+        # Permissions handling
+        try:
+            resource = Resource.objects.get(
+                slug=resource_slug, project__slug=project_slug
+            )
+        except Resource.DoesNotExist:
+            return rc.NOT_FOUND
+        if lang_code == "source":
+            language = resource.source_language
+        else:
+            try:
+                language =  Language.objects.by_code_or_alias(lang_code)
+            except Language.DoesNotExist:
+                logger.error("Weird! Selected language code (%s) does "
+                             "not match with any language in the database."
+                             % lang_code)
+                return BAD_REQUEST(
+                    "Selected language code (%s) does not match with any"
+                    "language in the database." % lang_code
+                )
+
+        team = Team.objects.get_or_none(resource.project, lang_code)
+        check = ProjectPermission(request.user)
+        if (not check.submit_translations(team or resource.project) or\
+            not resource.accept_translations) and not\
+                check.maintain(resource.project):
+            return rc.FORBIDDEN
+
+        try:
+            t = Translation.get_object("create", request, resource, language)
+            res = t.create()
+        except BadRequestError, e:
+            return BAD_REQUEST(e.message)
+        except NoContentError, e:
+            return BAD_REQUEST(e.message)
+        except AttributeError, e:
+            return BAD_REQUEST("The content type of the request is not valid.")
+        return t.__class__.to_http_for_create(t, res)
+
+
+class Translation(object):
+    """
+    Handle a translation for a resource.
+    """
+
+    @staticmethod
+    def get_object(type_, request, *args):
+        """
+        Factory method to get the suitable object for the request.
+        """
+        if type_ == "get":
+            if 'file' in request.GET:
+                return FileTranslation(request, *args)
+            else:
+                return StringTranslation(request, *args)
+        elif type_ == "create":
+            if request.content_type == "application/json":
+                return StringTranslation(request, *args)
+            elif "multipart/form-data" in request.content_type:
+                return FileTranslation(request, *args)
+        return None
+
+
+    @classmethod
+    def _to_http_response(cls, translation, result,
+                          status=200, mimetype='application/json'):
+        return HttpResponse(result, status=status, mimetype=mimetype)
+
+    @classmethod
+    def to_http_for_get(cls, translation, result):
+        """
+        Return the result to a suitable HttpResponse for a GET request.
+
+        Args:
+            translation: The translation object.
+            result: The result to convert to a HttpResponse.
+
+        Returns:
+            A HttpResponse with the result.
+        """
+        return cls._to_http_response(translation, result, status=200)
+
+    @classmethod
+    def to_http_for_create(cls, translation, result):
+        """
+        Return the result to a suitable HttpResponse for a PUT/POST request.
+
+        Args:
+            translation: The translation object.
+            result: The result to convert to a HttpResponse.
+
+        Returns:
+            A HttpResponse with the result.
+        """
+        return cls._to_http_response(translation, result, status=200)
+
+    def __init__(self, request, resource, language=None):
+        """
+        Initializer.
+
+        Args:
+            request: The request
+            resource: The resource the translation is asked for.
+            language: The language of the requested translation.
+
+        """
+        self.request = request
+        self.data = getattr(request, 'data', 'None')
+        self.resource = resource
+        self.language = language
+
+    def create(self):
+        """
+        Create a new translation.
+        """
+        raise NotImplementedError
+
+    def get(self):
+        """
+        Get a translation.
+
+        If lang_code is None, return all translations.
+        """
+        raise NotImplementedError
+
+    def _parse_translation(self, parser, filename):
+        strings_added, strings_updated = 0, 0
+        fhandler = parser(filename=filename)
+        fhandler.bind_resource(self.resource)
+        fhandler.set_language(self.language)
+
+        is_source = self.resource.source_language == self.language
+        try:
+            fhandler.contents_check(fhandler.filename)
+            fhandler.parse_file(is_source)
+            strings_added, strings_updated = fhandler.save2db(
+                is_source, user=self.request.user
+            )
+        except Exception, e:
+            raise BadRequestError("Could not import file: %s" % e)
+
+        messages = []
+        if strings_added > 0:
+            messages.append(_("%i strings added") % strings_added)
+        if strings_updated > 0:
+            messages.append(_("%i strings updated") % strings_updated)
+        retval= {
+            'strings_added': strings_added,
+            'strings_updated': strings_updated,
+            'redirect': reverse(
+                'resource_detail',
+                args=[self.resource.project.slug, self.resource.slug]
+            )
+        }
+        logger.debug("Extraction successful, returning: %s" % retval)
+
+        # If any string added/updated
+        if retval['strings_added'] > 0 or retval['strings_updated'] > 0:
+            modified = True
+        else:
+            modified=False
+        post_submit_translation.send(
+            None, request=self.request, resource=self.resource,
+            language=self.language, modified=modified
+        )
+
+        return retval
+
+
+class FileTranslation(Translation):
+    """
+    Handle requests for translation as files.
+    """
+
+    @classmethod
+    def to_http_for_get(cls, translation, result):
+        i18n_method = settings.I18N_METHODS[translation.resource.i18n_type]
+        response = HttpResponse(result, mimetype=i18n_method['mimetype'])
+        response['Content-Disposition'] = ('attachment; filename="%s_%s%s"' % (
+                smart_unicode(translation.resource.name),
+                translation.language.code,
+                i18n_method['file-extensions'].split(', ')[0])
+        )
+        return response
+
+    def get(self):
+        """
+        Return the requested translation as a file.
+
+        Returns:
+            The compiled template.
+
+        Raises:
+            BadRequestError: There was a problem with the request.
+        """
+        try:
+            template = _compile_translation_template(
+                self.resource, self.language
+            )
+        except Exception, e:
+            logger.error(e.message, exc_info=True)
+            return BadRequestError("Error compiling the translation file: %s" %e )
+        return template
+
+    def create(self):
+        """
+        Creates a new translation from file.
+
+        Returns:
+            A dict with information for the translation.
+
+        Raises:
+            BadRequestError: There was a problem with the request.
+            NoContentError: There was no file in the request.
+        """
+        if not self.request.FILES:
+            raise NoContentError("No file has been uploaded.")
+
+        submitted_file = self.request.FILES.values()[0]
+        name = str(submitted_file.name)
+        size = submitted_file.size
+
+        try:
+            file_ = tempfile.NamedTemporaryFile(
+                mode='wb',
+                suffix=name[name.rfind('.'):],
+                delete=False
+            )
+            for chunk in submitted_file.chunks():
+                file_.write(chunk)
+            file_.close()
+
+            parser = parser_for(file_.name)
+            if parser is None:
+                raise BadRequestError("Unknown file type")
+            if size == 0:
+                raise BadRequestError("Empty file")
+
+            try:
+                parser.contents_check(file_.name)
+                logger.debug("Uploaded file %s" % file_.name)
+            except (FileCheckError, LinguistParseError), e:
+                raise BadRequestError("Error uploading file: %s" % e.message)
+            except Exception, e:
+                logger.error(e.message, exc_info=True)
+                raise BadRequestError("A strange error happened.")
+
+            res = self._parse_translation(parser, file_.name)
+        finally:
+            os.unlink(file_.name)
+        return res
+
+
+class StringTranslation(Translation):
+    """
+    Handle requests for translation as strings.
+    """
+
+    def get(self, start=None, end=None):
+        """
+        Return the requested translation in a json string.
+
+        If self.language is None, return all translations.
+
+        Args:
+            start: Start for pagination.
+            end: End for pagination.
+
+        Returns:
+            A dict with the translation(s).
+
+        Raises:
+            BadRequestError: There was a problem with the request.
+        """
+        try:
+            template = _compile_translation_template(
+                self.resource, self.language
+            )
+        except Exception, e:
+            logger.error(e.message, exc_info=True)
+            raise BadRequestError(
+                "Error compiling the translation file: %s" % e
+            )
+
+        i18n_method = settings.I18N_METHODS[self.resource.i18n_type]
+        return {
+            'content': template,
+            'mimetype': i18n_method['mimetype']
+        }
+
+    def create(self):
+        """
+        Create a new translation supplied as a string.
+
+        Returns:
+            A dict with information for the request.
+
+        Raises:
+            BadRequestError: There was a problem with the request.
+            NoContentError: There was no content string in the request.
+        """
+        if 'content' not in self.data:
+            raise NoContentError("No content found.")
+        parser = parser_for(
+            mimetype=get_mimetype_from_method(self.resource.i18n_type)
+        )
+        if parser is None:
+            raise BadRequestError("Mimetype not supported")
+
+        file_ = tempfile.NamedTemporaryFile(
+            mode='wb',
+            suffix=get_file_extension_for_method(self.resource.i18n_type),
+            delete=False,
+        )
+        try:
+            file_.write(self.data['content'].encode('UTF-8'))
+            file_.close()
+            try:
+                parser.contents_check(file_.name)
+            except (FileCheckError, LinguistParseError), e:
+                raise BadRequestError(e.message)
+            except Exception, e:
+                logger.error(e.message, exc_info=True)
+                raise BadequestError("A strange error has happened.")
+
+            res = self._parse_translation(parser, file_.name)
+        finally:
+            os.unlink(file_.name)
+        return res
+

File transifex/resources/formats/__init__.py

     return import_to_python(class_name)
 
 
+def get_i18n_method_from_mimetype(i18n_type):
+    """
+    Returns the i18n method that corresponds to the supplied mimetype.
+    """
+    for key, value in settings.I18N_METHODS.iteritems():
+        if value['mimetype'] == i18n_type:
+            return key
+    return None
 
+
+def get_file_extension_for_method(method):
+    """
+    Return a file extension for the given mimetype.
+    """
+    return settings.I18N_METHODS[method]['file-extensions'].split(',')[0].strip()
+
+
+def parser_for(filename=None, mimetype=None):
+    """
+    Get the appropriate parser.
+    """
+    from transifex.resources.models import PARSERS
+    for parser in PARSERS:
+        if parser.accepts(filename=filename,mime=mimetype):
+            return parser
+    return None
+
+def get_mimetype_from_method(method):
+    """
+    Get the mimetype for the method.
+    """
+    if method is None:
+        return None
+    return settings.I18N_METHODS[method]['mimetype']

File transifex/resources/formats/core.py

             raise Exception("Error opening file %s: %s" % ( filename, e))
 
 
-    def accept(self, filename=None, mime=None):
+    @classmethod
+    def accepts(self, filename=None, mime=None):
         return False
 
     def parse_file(self, filename, is_source=False, lang_rules=None):

File transifex/resources/formats/javaproperties.py

     separators = [' ', '\t', '\f', '=', ':', ]
 
     @classmethod
-    def accept(cls, filename=None, mime=None):
+    def accepts(cls, filename=None, mime=None):
         return filename.endswith(".properties") or mime in cls.mime_types
 
     @classmethod

File transifex/resources/formats/joomla.py

     comment_chars = ('#', ';', ) # '#' is for 1.5 and ';' for >1.6
 
     @classmethod
-    def accept(cls, filename=None, mime=None):
-        return filename.endswith(".ini") or mime in cls.mime_types
+    def accepts(cls, filename=None, mime=None):
+        accept = False
+        if filename is not None:
+            accept |= filename.endswith(".ini")
+        if mime is not None:
+            accept |= mime in cls.mime_types
+        return accept
 
     @classmethod
     def contents_check(self, filename):

File transifex/resources/formats/pofile.py

     format = "GNU Gettext Catalog (*.po, *.pot)"
 
     @classmethod
-    def accept(cls, filename=None, mime=None):
-        return filename.endswith(".po") or filename.endswith(".pot") or\
-            mime in cls.mime_types
+    def accepts(cls, filename=None, mime=None):
+        accept = False
+        if filename is not None:
+            accept |= filename.endswith(".po") or filename.endswith(".pot")
+        if mime is not None:
+            accept |= mime in cls.mime_types
+        return accept
 
     @classmethod
     def contents_check(self, filename):

File transifex/resources/formats/qt.py

     mime_types = ["application/x-linguist"]
 
     @classmethod
-    def accept(cls, filename=None, mime=None):
-        return filename.endswith(".ts") or mime in cls.mime_types
+    def accepts(cls, filename=None, mime=None):
+        # TODO better way to handle tests
+        # maybe remove None?
+        accept = False
+        if filename is not None:
+            accept |= filename.endswith(".ts")
+        if mime is not None:
+            accept |= mime in cls.mime_types
+        return accept
 
     @classmethod
     def contents_check(self, filename):

File transifex/resources/models.py

 
     content = CompressedTextField(null=False, blank=False,
         help_text=_("This is the actual content of the template"))
-    resource = models.ForeignKey(Resource,
+    resource = models.OneToOneField(Resource,
         verbose_name="Resource",unique=True,
         blank=False, null=False,related_name="source_file_template",
         help_text=_("This is the template of the imported source file which is"

File transifex/resources/tests/api/__init__.py

+# -*- coding: utf-8 -*-
+import os
+from django.core.urlresolvers import reverse
+from django.utils import simplejson
+from django.contrib.auth.models import User, Permission
+from transifex.resources.models import Resource
+from transifex.resources.tests.api.base import APIBaseTests
+from transifex.projects.models import Project
+from transifex.settings import PROJECT_PATH
 
+
+class TestResourceAPI(APIBaseTests):
+
+    def setUp(self):
+        super(TestResourceAPI, self).setUp()
+        self.po_file = os.path.join(self.pofile_path, "pt_BR.po")
+        self.url_resources = reverse(
+            'apiv2_resources', kwargs={'project_slug': 'project1'}
+        )
+        self.url_resources_private = reverse(
+            'apiv2_resources', kwargs={'project_slug': 'project2'}
+        )
+        self.url_resource = reverse(
+            'apiv2_resource',
+            kwargs={'project_slug': 'project1', 'resource_slug': 'resource1'}
+        )
+        self.url_resource_private = reverse(
+            'apiv2_resource',
+            kwargs={'project_slug': 'project2', 'resource_slug': 'resource1'}
+        )
+        self.url_new_project = reverse(
+            'apiv2_projects'
+        )
+        self.url_create_resource = reverse(
+            'apiv2_resources', kwargs={'project_slug': 'new_pr'}
+        )
+        self.url_new_resource = reverse(
+            'apiv2_resource',
+            kwargs={'project_slug': 'new_pr', 'resource_slug': 'r1', }
+        )
+        self.url_new_translation = reverse(
+            'apiv2_translation',
+            kwargs={
+                'project_slug': 'new_pr',
+                'resource_slug': 'new_r',
+                'lang_code': 'el'
+            }
+        )
+
+    def test_get(self):
+        res = self.client['anonymous'].get(self.url_resources)
+        self.assertEquals(res.status_code, 401)
+        res = self.client['registered'].get(
+            reverse(
+                'apiv2_resource',
+                kwargs={'project_slug': 'not_exists', 'resource_slug': 'resource1'}
+            )
+        )
+        self.assertEquals(res.status_code, 404)
+        res = self.client['registered'].get(self.url_resources)
+        self.assertEquals(res.status_code, 200)
+        self.assertFalse('created' in simplejson.loads(res.content)[0])
+        res = self.client['registered'].get(self.url_resources)
+        self.assertEquals(res.status_code, 200)
+        self.assertEquals(len(simplejson.loads(res.content)), 1)
+        self.assertFalse('created' in simplejson.loads(res.content)[0])
+        res = self.client['registered'].get(self.url_resources_private)
+        self.assertEquals(res.status_code, 401)
+        res = self.client['maintainer'].get(self.url_resources_private + "?details")
+        self.assertEquals(res.status_code, 501)
+        res = self.client['maintainer'].get(self.url_resources_private)
+        self.assertEquals(res.status_code, 200)
+        self.assertEqual(len(simplejson.loads(res.content)), 1)
+        self.assertFalse('created' in simplejson.loads(res.content)[0])
+        self.assertTrue('slug' in simplejson.loads(res.content)[0])
+        self.assertTrue('name' in simplejson.loads(res.content)[0])
+        res = self.client['anonymous'].get(self.url_resource)
+        self.assertEquals(res.status_code, 401)
+        url_not_exists = self.url_resource[:-1] + "none/"
+        res = self.client['registered'].get(url_not_exists)
+        self.assertEquals(res.status_code, 404)
+        res = self.client['registered'].get(self.url_resource_private)
+        self.assertEquals(res.status_code, 401)
+        res = self.client['maintainer'].get(self.url_resource_private)
+        self.assertEquals(res.status_code, 200)
+        res = self.client['maintainer'].get(self.url_resource_private)
+        self.assertEquals(res.status_code, 200)
+        data = simplejson.loads(res.content)
+        self.assertEquals(len(data), 4)
+        self.assertIn('slug', data)
+        self.assertIn('name', data)
+        self.assertIn('source_language', data)
+        res = self.client['maintainer'].get(self.url_resource_private + "?details")
+        self.assertEquals(res.status_code, 200)
+        data = simplejson.loads(res.content)
+        self.assertIn('source_language_code', data.iterkeys())
+        self._create_resource()
+        res = self.client['registered'].get(self.url_new_resource)
+        self.assertEquals(res.status_code, 200)
+        data = simplejson.loads(res.content)
+        self.assertIn('source_language', data)
+        res = self.client['registered'].get(
+            self.url_new_resource + "content/"
+        )
+        self.assertEquals(res.status_code, 200)
+        data = simplejson.loads(res.content)
+        self.assertIn('content', data)
+        res = self.client['registered'].get(
+            self.url_new_resource + "content/?file"
+        )
+        self.assertEquals(res.status_code, 200)
+
+
+    def test_post_errors(self):
+        res = self.client['anonymous'].post(
+            self.url_resource, content_type='application/json'
+        )
+        self.assertEquals(res.status_code, 401)
+        res = self.client['registered'].post(
+            self.url_resource, content_type='application/json'
+        )
+        self.assertEquals(res.status_code, 403)
+        self._create_resource()
+        url = reverse(
+            'apiv2_resource',
+            kwargs={'project_slug': 'new_pr', 'resource_slug': 'new_r'}
+        )
+        res = self.client['registered'].post(
+            url, content_type='application/json'
+        )
+        self.assertEquals(res.status_code, 400)
+        self.assertIn("POSTing to this url is not allowed", res.content)
+        res = self.client['registered'].post(
+            self.url_create_resource,
+            data=simplejson.dumps({
+                    'name': "resource1",
+                    'slug': 'r1',
+                    'source_language': 'el',
+                    'foo': 'foo'
+            }),
+            content_type='application/json'
+        )
+        self.assertEquals(res.status_code, 400)
+        self.assertIn("Field 'foo'", res.content)
+        res = self.client['registered'].post(
+            self.url_create_resource,
+            data=simplejson.dumps({
+                    'name': "resource1",
+                    'slug': 'r1',
+                    'source_language': 'el',
+                    'mimetype': 'text/x-po',
+            }),
+            content_type='application/json'
+        )
+        self.assertEquals(res.status_code, 400)
+        self.assertIn("same slug exists", res.content)